Merge branch 'bug/JAL-2750' into develop
authorJim Procter <jprocter@issues.jalview.org>
Mon, 17 Dec 2018 17:04:56 +0000 (17:04 +0000)
committerJim Procter <jprocter@issues.jalview.org>
Mon, 17 Dec 2018 17:04:56 +0000 (17:04 +0000)
542 files changed:
.checkstyle
.classpath
.gitignore
.settings/org.eclipse.jdt.core.prefs
.settings/org.eclipse.jdt.ui.prefs
AUTHORS
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
doc/patching-vaqua.txt [new file with mode: 0644]
examples/example.json
examples/exampleFeatures.txt
examples/groovy/PIDmatrix.groovy [new file with mode: 0644]
examples/rna_ss_test.stk [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/example_annot_file.jva
examples/testdata/test.aln
help/help.jhm
help/helpTOC.xml
help/html/calculations/pairwise.html
help/html/calculations/sorting.html
help/html/editing/index.html
help/html/features/chimera.html
help/html/features/clarguments.html
help/html/features/commandline.html
help/html/features/dasfeatures.html [deleted file]
help/html/features/dassettings.html [deleted file]
help/html/features/featuresFormat.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/search.html
help/html/features/searchclearhist.png
help/html/features/seqfeatures.html
help/html/features/seqfetch.html
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/memory.html
help/html/menus/alignmentMenu.html
help/html/menus/alwview.html
help/html/menus/desktopMenu.html
help/html/menus/wsmenu.html
help/html/releases.html
help/html/webServices/dbreffetcher.html
help/html/webServices/index.html
help/html/whatsNew.html
lib/VAqua5-patch.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-1.133.jar [deleted file]
lib/htsjdk-2.12.0.jar [new file with mode: 0644]
lib/jdas-1.0.4.jar [deleted file]
nbbuild.xml
resources/authors.props
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/ChimeraManager.java
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraResidue.java
src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/AlignSeq.java
src/jalview/analysis/AlignmentSorter.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Conservation.java
src/jalview/analysis/CrossRef.java
src/jalview/analysis/Dna.java
src/jalview/analysis/Rna.java
src/jalview/analysis/SeqsetUtils.java
src/jalview/analysis/scoremodels/FeatureDistanceModel.java
src/jalview/api/AlignViewportI.java
src/jalview/api/AlignmentViewPanel.java
src/jalview/api/FeatureColourI.java
src/jalview/api/FeatureRenderer.java
src/jalview/api/FeaturesDisplayedI.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/Finder.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/commands/EditCommand.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/AlignedCodonFrame.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/AlignmentI.java
src/jalview/datamodel/Annotation.java
src/jalview/datamodel/BinarySequence.java
src/jalview/datamodel/CigarArray.java
src/jalview/datamodel/ContiguousI.java [moved from src/jalview/io/ClansFile.java with 86% similarity]
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 [new file with mode: 0644]
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/SearchResultsI.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceCursor.java [new file with mode: 0644]
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 [moved from src/jalview/io/MatrixFile.java with 66% similarity]
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 [new file with mode: 0644]
src/jalview/datamodel/features/NCList.java [new file with mode: 0644]
src/jalview/datamodel/features/NCNode.java [new file with mode: 0644]
src/jalview/datamodel/features/RangeComparator.java [new file with mode: 0644]
src/jalview/datamodel/features/SequenceFeatures.java [new file with mode: 0644]
src/jalview/datamodel/features/SequenceFeaturesI.java [new file with mode: 0644]
src/jalview/datamodel/xdb/embl/EmblEntry.java
src/jalview/datamodel/xdb/uniprot/UniprotEntry.java [moved from src/jalview/datamodel/UniprotEntry.java with 90% similarity]
src/jalview/datamodel/xdb/uniprot/UniprotFeature.java [new file with mode: 0644]
src/jalview/datamodel/xdb/uniprot/UniprotFile.java [moved from src/jalview/datamodel/UniprotFile.java with 96% similarity]
src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java [moved from src/jalview/datamodel/UniprotProteinName.java with 97% similarity]
src/jalview/datamodel/xdb/uniprot/UniprotSequence.java [moved from src/jalview/datamodel/UniprotSequence.java with 97% similarity]
src/jalview/ext/ensembl/EnsemblCdna.java
src/jalview/ext/ensembl/EnsemblCds.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/ext/rbvi/chimera/ChimeraCommands.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/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/AppVarna.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/CutAndPasteTransfer.java
src/jalview/gui/DasSourceBrowser.java [deleted file]
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/Finder.java
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/Jalview2XML_V1.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/TreeCanvas.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/BLCFile.java
src/jalview/io/ClustalFile.java
src/jalview/io/FeaturesFile.java
src/jalview/io/FileFormat.java
src/jalview/io/FileLoader.java
src/jalview/io/FileParse.java
src/jalview/io/FormatAdapter.java
src/jalview/io/IdentifyFile.java
src/jalview/io/InputStreamParser.java
src/jalview/io/JSONFile.java
src/jalview/io/JalviewFileChooser.java
src/jalview/io/JalviewFileFilter.java
src/jalview/io/JalviewFileView.java
src/jalview/io/JnetAnnotationMaker.java
src/jalview/io/MSFfile.java
src/jalview/io/PfamFile.java
src/jalview/io/PhylipFile.java
src/jalview/io/PileUpfile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/StructureFile.java
src/jalview/io/WSWUBlastClient.java
src/jalview/io/cache/JvCacheableInputBox.java
src/jalview/io/gff/ExonerateHelper.java
src/jalview/io/gff/Gff3Helper.java
src/jalview/io/gff/GffHelperBase.java
src/jalview/io/gff/InterProScanHelper.java
src/jalview/io/gff/SequenceOntologyI.java
src/jalview/io/gff/SequenceOntologyLite.java
src/jalview/io/vamsas/Datasetsequence.java
src/jalview/io/vamsas/Sequencefeature.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/FeatureColourFinder.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/ClustalxColourScheme.java
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/Comparison.java
src/jalview/util/IntRangeComparator.java [moved from src/jalview/util/RangeComparator.java with 81% similarity]
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/viewmodel/seqfeatures/FeaturesDisplayed.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 [deleted file]
src/jalview/ws/SequenceFetcher.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/dbsources/das/api/DasSourceRegistryI.java [deleted file]
src/jalview/ws/dbsources/das/api/jalviewSourceI.java [deleted file]
src/jalview/ws/dbsources/das/datamodel/DasSequenceSource.java [deleted file]
src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java [deleted file]
src/jalview/ws/dbsources/das/datamodel/JalviewSource.java [deleted file]
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/rest/params/SeqVector.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/AlignmentSorterTest.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/analysis/CrossRefTest.java
test/jalview/analysis/DnaTest.java
test/jalview/analysis/RnaTest.java
test/jalview/analysis/SeqsetUtilsTest.java
test/jalview/analysis/TestAlignSeq.java
test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java
test/jalview/bin/CommandLineOperations.java
test/jalview/commands/EditCommandTest.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/SeqCigarTest.java
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/FeatureStoreTest.java [new file with mode: 0644]
test/jalview/datamodel/features/NCListTest.java [new file with mode: 0644]
test/jalview/datamodel/features/NCNodeTest.java [new file with mode: 0644]
test/jalview/datamodel/features/RangeComparatorTest.java [new file with mode: 0644]
test/jalview/datamodel/features/SequenceFeaturesTest.java [new file with mode: 0644]
test/jalview/ext/ensembl/EnsemblCdnaTest.java
test/jalview/ext/ensembl/EnsemblCdsTest.java
test/jalview/ext/ensembl/EnsemblGeneTest.java
test/jalview/ext/ensembl/EnsemblGenomeTest.java
test/jalview/ext/ensembl/EnsemblRestClientTest.java
test/jalview/ext/ensembl/EnsemblSeqProxyAdapter.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/JmolParserTest.java
test/jalview/ext/jmol/JmolViewerTest.java
test/jalview/ext/paradise/TestAnnotate3D.java
test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java
test/jalview/ext/rbvi/chimera/ChimeraConnect.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.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/IdentifyFileTest.java
test/jalview/io/JSONFileTest.java
test/jalview/io/Jalview2xmlBase.java
test/jalview/io/Jalview2xmlTests.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/StockholmFileTest.java
test/jalview/io/cache/JvCacheableInputBoxTest.java
test/jalview/io/gff/Gff3HelperTest.java
test/jalview/io/gff/InterProScanHelperTest.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 [new file with mode: 0644]
test/jalview/schemes/AnnotationColourGradientTest.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/viewmodel/styles/ViewStyleTest.java
test/jalview/ws/PDBSequenceFetcherTest.java
test/jalview/ws/SequenceFetcherTest.java
test/jalview/ws/dbsources/PfamFullTest.java [moved from test/jalview/ws/seqfetcher/DasSequenceFetcher.java with 62% similarity]
test/jalview/ws/dbsources/PfamSeedTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/RemoteFormatTest.java
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/seqfetcher/DbRefFetcherTest.java
test/jalview/ws/sifts/SiftsClientTest.java
utils/InstallAnywhere/Jalview.iap_xml
utils/InstallAnywhere/README_IA
utils/InstallAnywhere/jalview_buildinstaller.xml
utils/MessageBundleChecker.java
utils/checkstyle/README.txt
utils/checkstyle/checkstyle-suppress.xml
utils/checkstyle/checkstyle.xml
utils/checkstyle/import-control.xml
utils/classgraph-4.1.6.jar [new file with mode: 0644]
utils/proguard.jar [deleted file]
utils/proguard_5.3.3.jar [new file with mode: 0755]

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..0da91bb 100644 (file)
@@ -36,7 +36,6 @@
        <classpathentry kind="lib" path="lib/miglayout-4.0-swing.jar"/>
        <classpathentry kind="lib" path="lib/jswingreader-0.3.jar" sourcepath="/jswingreader"/>
        <classpathentry kind="lib" path="lib/commons-codec-1.3.jar"/>
-       <classpathentry kind="lib" path="lib/jdas-1.0.4.jar"/>
        <classpathentry kind="lib" path="lib/spring-core-3.0.5.RELEASE.jar"/>
        <classpathentry kind="lib" path="lib/spring-web-3.0.5.RELEASE.jar"/>
        <classpathentry kind="lib" path="lib/jabaws-min-client-2.2.0.jar" sourcepath="/clustengine"/>
        <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/VAqua5-patch.jar"/>
+       <classpathentry kind="lib" path="utils/classgraph-4.1.6.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..86637df 100644 (file)
@@ -1,5 +1,6 @@
 .project
 /dist
+/clover
 /classes
 /tests
 /test-reports
@@ -11,3 +12,5 @@
 .gitattributes
 TESTNG
 /jalviewApplet.jar
+/benchmarking/lib
+*.class
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
index 30e76be..958613b 100644 (file)
@@ -40,6 +40,7 @@ sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=
 sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
 sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
 sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_modifiers=false
 sp_cleanup.remove_redundant_type_arguments=true
 sp_cleanup.remove_trailing_whitespaces=false
 sp_cleanup.remove_trailing_whitespaces_all=true
diff --git a/AUTHORS b/AUTHORS
index 1bfc734..2fe3fce 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -7,15 +7,17 @@ or might otherwise be considered author of Jalview.
 The people listed below are 'The Jalview Authors', who collectively
 own the copyright to the Jalview source code and permit it to be released under GPL.
 
-This is the authoritative list. It was correct on 23rd November 2016.
+This is the authoritative list: it was correct on 5th September 2018 (or the last commit date!)
+
 If you are releasing a version of Jalview, please make sure any
 statement of authorship in the GUI reflects the list shown here.
 In particular, check the resources/authors.props file ! 
 
 Jim Procter
 Mungo Carstairs
-Tochukwu 'Charles' Ofoegbu
+Ben Soares
 Kira Mourao
+Tochukwu 'Charles' Ofoegbu
 Andrew Waterhouse
 Jan Engelhardt
 Lauren Lui
diff --git a/README b/README
index cbc93b1..8172066 100755 (executable)
--- a/README
+++ b/README
@@ -21,11 +21,19 @@ For more help, read the file doc/building.html
 
 ##################
 
-To run application:
+To run application...
+[ NOTE: when using the -classpath option with the '*' wildcard, the argument must be quoted to avoid shell expansion of the wildcard,
+  ALSO, the wildcard MUST be as DIR/* and not DIR/*.jar etc or it will not be interpreted correctly ]
 
-java -Djava.ext.dirs=JALVIEW_HOME/lib -cp JALVIEW_HOME/jalview.jar jalview.bin.Jalview
+on Windows use:
+  java -classpath "JALVIEW_HOME/lib/*;JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+and on MacOS or Linux:
+  java -classpath "JALVIEW_HOME/lib/*:JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+
+Replace JALVIEW_HOME with the full path to Jalview Installation Directory. If building from source:
+
+  java -classpath "JALVIEW_BUILD/dist/*" jalview.bin.Jalview
 
-Replace JALVIEW_HOME with the full path to Jalview Installation Directory.
 
 ##################
 
diff --git a/RELEASE b/RELEASE
index cecefec..1960368 100644 (file)
--- a/RELEASE
+++ b/RELEASE
@@ -1,2 +1,2 @@
-jalview.release=releases/Release_2_10_2b1_Branch
-jalview.version=2.10.2b2
+jalview.release=releases/Release_2_11_Branch
+jalview.version=2.11.0
index e0be904..afa99d2 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:
 
@@ -29,7 +30,6 @@ httpclient-4.0.3.jar
 httpcore-4.0.1.jar
 httpmime-4.0.3.jar
 jaxrpc.jar
-jdas-1.0.4.jar : Apache License - built from http://code.google.com/p/jdas/ (29th Feb 2012)
 jhall.jar
 jswingreader-0.3.jar : Apache license - built from http://jswingreader.sourceforge.net/ svn/trunk v12
 log4j-1.2.8.jar
@@ -38,8 +38,8 @@ miglayout-4.0-swing.jar       BSD http://www.migcalendar.com/miglayout/versions/4.0/li
 min-jaba-client.jar
 regex.jar
 saaj.jar
-spring-core-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven
-spring-web-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven
+spring-core-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven - TODO: JAL-3035 remove if no longer needed ?
+spring-web-3.0.5.RELEASE.jar : Apache License: jdas runtime dependencies retrieved via maven - TODO: JAL-3035 remove if no longer needed ?
 vamsas-client.jar
 wsdl4j.jar
 xercesImpl.jar
@@ -49,13 +49,21 @@ 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/ 
 
-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
+vaqua5-patch: This is a patched version of VAqua v5 (latest stable) by Alan Snyder et al. GPLv3 with Classpath exception, also includes contributions from Quaqua: http://violetlib.org/vaqua/overview.html - see doc/patching-vaqua.txt for patch details, and http://issues.jalview.org/browse/JAL-2988 for details of the bug that the patch addresses.
+
+lib/htsjdk-2.12.jar: 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
 
 lib/biojava-ontology-4.1.0.jar LGPLv2.1 - latest license at https://github.com/biojava/biojava/blob/master/LICENSE
 
 
+Libraries for Test Suite
+
+utils/classgraph-4.1.6.jar: BSD License - allows recovery of classpath for programmatic construction of a Java command line to launch Jalview
+                      version 4.1.6 downloaded from https://mvnrepository.com/artifact/io.github.classgraph/classgraph/4.1.6
+
+
 Additional dependencies
 
 examples/javascript/deployJava.js : http://java.com/js/deployJava.js
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..5ca105a 100755 (executable)
--- a/build.xml
+++ b/build.xml
  * 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">
+  <taskdef classpath="${clover.jar}" resource="cloverlib.xml" if:set="clover.jar"/>
+  <clover-env if:set="clover.jar"/>
+
   <target name="help" depends="usage" />
   <target name="usage" depends="init">
     <echo message="~~~Jalview Ant build.xml Usage~~~~" />
@@ -36,8 +41,9 @@
     <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" />
+    <echo message="Useful -D flags: -Dclover.jar to specify path to openclover for testng coverage report" />
   </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 -->
     <property name="docDir" value="doc" />
     <property name="sourceDir" value="src" />
     <property name="schemaDir" value="schemas" />
-    <property name="outputDir" value="classes" />
+    <property name="outputDir" value="classes" unless:set="clover.jar"/>
+    <property name="outputDir" value="cloverclasses" if:set="clover.jar"/>
     <property name="packageDir" value="dist" />
     <property name="outputJar" value="jalview.jar" />
     <!-- Jalview Applet JMol Jar Dependency -->
     </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" />
       verbose="2">
       <classpath>
         <pathelement location="${testOutputDir}" />
+        <pathelement location="${clover.jar}" if:set="clover.jar"/>
         <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>
     </jar>
 
     <antcall target="writejnlpf">
-      <param name="jnlpFile" value="${packageDir}/jalview.jnlp" />
+      <param name="jnlpFile" value="${packageDir}/jalview_256M.jnlp" />
       <param name="inih" value="10M" />
       <param name="maxh" value="256M" />
     </antcall>
+    <antcall target="writejnlpf">
+      <param name="jnlpFile" value="${packageDir}/jalview.jnlp" />
+      <param name="inih" value="800M" />
+      <param name="maxh" value="1024M" />
+    </antcall>
 
     <antcall target="writejnlpf">
       <param name="jnlpFile" value="${packageDir}/jalview_1G.jnlp" />
 
     <antcall target="writejnlpf">
       <param name="jnlpFile" value="${packageDir}/jalview_2G.jnlp" />
-      <param name="inih" value="256M" />
+      <param name="inih" value="800M" />
       <param name="maxh" value="1024M" />
     </antcall>
 
         <association mime-type="application-x/ext-file" extensions="jar"/>-->
   </target>
 
-  <target name="-jarsignwithtsa" depends="makedist,preparejnlp" if="timestamp">
+  <target name="-jarsignwithtsa" depends="makedist,preparejnlp" if="timestamp" unless="nosign">
     <signjar storepass="${jalview.keystore.pass}" keypass="${jalview.key.pass}" keystore="${jalview.keystore}" alias="${jalview.key}" lazy="false" verbose="false" sigalg="${jalview.keyalg}" digestalg="${jalview.keydig}"
       tsaproxyhost="${proxyHost}" tsaproxyport="${proxyPort}" tsaurl="${jalview.tsaurl}">
       <fileset dir="${packageDir}">
       </fileset>
     </signjar>
   </target>
-  <target name="-jarsignnotsa" depends="makedist,preparejnlp" unless="timestamp">
+  <target name="-jarsignnotsa" depends="makedist,preparejnlp" if:blank="timestamp" unless="nosign">
     <signjar storepass="${jalview.keystore.pass}" keypass="${jalview.key.pass}" keystore="${jalview.keystore}" alias="${jalview.key}" lazy="false" verbose="false" sigalg="${jalview.keyalg}" digestalg="${jalview.keydig}">
       <fileset dir="${packageDir}">
         <include name="*.jar" />
           <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="makedist" depends="build, buildPropertiesFile, linkcheck, buildindices">
+  <fail if="clover.jar">
+    Ignoring request to build jalview distribution with clover-instrumented classes
+  </fail>
   <!-- make the package jar if not already existing -->
   <mkdir dir="${packageDir}" />
   <!-- clean dir if it already existed -->
 </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" />
     <include name="ap_${jsonSimple}" />
   </fileset>
 </target>
-<target name="-signappletnotsa" unless="timestamp" depends="-signapplet">
+<target name="-signappletnotsa" if:blank="timestamp" depends="-signapplet" unless="nosign">
   <signjar storepass="${jalview.keystore.pass}" keypass="${jalview.key.pass}" keystore="${jalview.keystore}" alias="${jalview.key}" lazy="false" verbose="false">
     <fileset refid="signappletjarset" />
   </signjar>
 </target>
 
-<target name="-signapplettsa" if="timestamp" depends="-signapplet">
+<target name="-signapplettsa" if="timestamp" depends="-signapplet" unless="nosign">
   <signjar storepass="${jalview.keystore.pass}" keypass="${jalview.key.pass}" keystore="${jalview.keystore}" alias="${jalview.key}" lazy="false" verbose="false" tsaproxyhost="${proxyHost}" tsaproxyport="${proxyPort}" tsaurl="${jalview.tsaurl}">
     <fileset refid="signappletjarset" />
   </signjar>
diff --git a/doc/patching-vaqua.txt b/doc/patching-vaqua.txt
new file mode 100644 (file)
index 0000000..65c9974
--- /dev/null
@@ -0,0 +1,27 @@
+VAqua5-patched.jar - how the patch was created
+
+1. Download VAqua5 source from https://violetlib.org/release/vaqua/5/VAqua5Source.zip
+2. Unzip to a directory and apply this patch
+
+diff --git a/src/org/violetlib/aqua/fc/AquaFileChooserUI.java b/src/org/violetlib/aqua/fc/AquaFileChooserUI.java
+index 833366d..61f66e5 100644
+--- a/src/org/violetlib/aqua/fc/AquaFileChooserUI.java
++++ b/src/org/violetlib/aqua/fc/AquaFileChooserUI.java
+@@ -1171,7 +1171,8 @@ public class AquaFileChooserUI extends BasicFileChooserUI {
+         goToFolderCancelButtonText = getString("FileChooser.goToFolderCancelButtonText", l, "Cancel");
+         goToFolderAcceptButtonText = getString("FileChooser.goToFolderAcceptButtonText", l, "Accept");
+         goToFolderErrorText = getString("FileChooser.goToFolderErrorText", l, "The folder can\u2019t be found.");
+-        defaultInitialSaveFileName = getString("FileChooser.defaultSaveFileName", l, "Untitled");
++        // Don't set an initial filename for saving (or loading) !  
++       // defaultInitialSaveFileName = getString("FileChooser.defaultSaveFileName", l, "Untitled");
+     }
+     /**
+
+3. Ensure XCode is installed, along with command line tools and the OSX developer packs
+ - you should have /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
+
+4. Download the VAqua rendering library from violetlib.org and save to the VAqua source's lib folder as lib/VAquaRendering.jar
+
+5. change to the release directory and execute 'ant' - a few warnings are generated but providing a final jar is created, all is good!
+
index 5f6e784..93c19db 100644 (file)
@@ -1 +1 @@
-{"seqs":[{"name":"FER_CAPAN/3-34","start":3,"svid":"1.0","end":34,"id":"1665704504","seq":"SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALF","order":1},{"name":"FER1_SOLLC/3-34","start":3,"svid":"1.0","end":34,"id":"1003594867","seq":"SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALF","order":2},{"name":"Q93XJ9_SOLTU/3-34","start":3,"svid":"1.0","end":34,"id":"1332961135","seq":"SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALF","order":3},{"name":"FER1_PEA/6-37","start":6,"svid":"1.0","end":37,"id":"1335040546","seq":"ALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFL","order":4},{"name":"Q7XA98_TRIPR/6-39","start":6,"svid":"1.0","end":39,"id":"1777084554","seq":"ALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGF","order":5},{"name":"FER_TOCH/3-34","start":3,"svid":"1.0","end":34,"id":"823528539","seq":"FILGTMISKSFLFRKPAVTSL-KAISNVGE--ALF","order":6}],"appSettings":{"globalColorScheme":"zappo","webStartUrl":"www.jalview.org/services/launchApp","application":"Jalview","hiddenSeqs":"823528539","showSeqFeatures":"true","version":"2.9","hiddenCols":"32-33;34-34"},"seqGroups":[{"displayText":true,"startRes":21,"groupName":"JGroup:1883305585","endRes":29,"colourText":false,"sequenceRefs":["1003594867","1332961135","1335040546","1777084554"],"svid":"1.0","showNonconserved":false,"colourScheme":"Zappo","displayBoxes":true}],"alignAnnotation":[{"svid":"1.0","annotations":[{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"}],"description":"New description","label":"Secondary Structure"}],"svid":"1.0","seqFeatures":[{"fillColor":"#7d1633","score":0,"sequenceRef":"1332961135","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1335040546","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1777084554","featureGroup":"Jalview","svid":"1.0","description":"desciption","xStart":3,"xEnd":13,"type":"feature_x"}]}
\ No newline at end of file
+{"seqs":[{"name":"FER_CAPAN/3-34","start":3,"svid":"1.0","end":34,"id":"1665704504","seq":"SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALF","order":1},{"name":"FER1_SOLLC/3-34","start":3,"svid":"1.0","end":34,"id":"1003594867","seq":"SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALF","order":2},{"name":"Q93XJ9_SOLTU/3-34","start":3,"svid":"1.0","end":34,"id":"1332961135","seq":"SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALF","order":3},{"name":"FER1_PEA/6-37","start":6,"svid":"1.0","end":37,"id":"1335040546","seq":"ALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFL","order":4},{"name":"Q7XA98_TRIPR/6-39","start":6,"svid":"1.0","end":39,"id":"1777084554","seq":"ALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGF","order":5},{"name":"FER_TOCH/3-34","start":3,"svid":"1.0","end":34,"id":"823528539","seq":"FILGTMISKSFLFRKPAVTSL-KAISNVGE--ALF","order":6}],"appSettings":{"globalColorScheme":"zappo","webStartUrl":"www.jalview.org/services/launchApp","application":"Jalview","hiddenSeqs":"823528539","showSeqFeatures":"true","version":"2.9","hiddenCols":"32-33;34-34"},"seqGroups":[{"displayText":true,"startRes":21,"groupName":"JGroup:1883305585","endRes":29,"colourText":false,"sequenceRefs":["1003594867","1332961135","1335040546","1777084554"],"svid":"1.0","showNonconserved":false,"colourScheme":"Zappo","displayBoxes":true}],"alignAnnotation":[{"svid":"1.0","annotations":[{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"β","value":0,"secondaryStructure":"E"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"α","value":0,"secondaryStructure":"H"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"},{"displayCharacter":"","value":0,"secondaryStructure":"\u0000"}],"description":"New description","label":"Secondary Structure"}],"svid":"1.0","seqFeatures":[{"fillColor":"#7d1633","score":0,"otherDetails":{"status":"+"},"sequenceRef":"1332961135","featureGroup":"Pfam","svid":"1.0","description":"My description","xStart":0,"xEnd":0,"type":"Domain"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1332961135","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1335040546","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"},{"fillColor":"#7d1633","score":0,"sequenceRef":"1777084554","featureGroup":"Jalview","svid":"1.0","description":"theDesc","xStart":3,"xEnd":13,"type":"feature_x"}]}
\ No newline at end of file
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/rna_ss_test.stk b/examples/rna_ss_test.stk
new file mode 100644 (file)
index 0000000..429612e
--- /dev/null
@@ -0,0 +1,6 @@
+# STOCKHOLM 1.0
+#=GF ID RNA.SS.TEST
+#=GF TP RNA;
+Test.sequence         GUACAAAAAAAAAA
+#=GC SS_cons          <(EHBheb(E)e)>
+//
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 1779247..6b9faa4 100644 (file)
@@ -18,5 +18,5 @@ SEQUENCE_GROUP        Group_A 30      50      *
 SEQUENCE_GROUP Group_B 1       351     2-5
 SEQUENCE_GROUP Group_C 12      14      -1      seq1    seq2    seq3
 PROPERTIES     Group_A description=This is the description     colour=Helix Propensity pidThreshold=0  outlineColour=red       displayBoxes=true       displayText=false       colourText=false        textCol1=black  textCol2=black  textColThreshold=0
-PROPERTIES     Group_B outlineColour=red       colour=None
+PROPERTIES     Group_B outlineColour=green     colour=None
 PROPERTIES     Group_C colour=Clustal
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..78f86b6 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"/>     
@@ -69,8 +70,6 @@
    <mapID target="annotations.fileformat" url="html/features/annotationsFormat.html"/>
    <mapID target="features.fileformat" url="html/features/featuresFormat.html"/>
    <mapID target="features.featureschemes" url="html/features/featureschemes.html"/>
-   <mapID target="das.settings" url="html/features/dassettings.html"/>
-   <mapID target="das.viewing" url="html/features/dasfeatures.html"/>
    <mapID target="edit" url="html/editing/index.html"/>
    <mapID target="jalarchive" url="html/features/jalarchive.html"/>
    <mapID target="multipleviews" url="html/features/multipleViews.html"/>
index 4636ea3..77ddd88 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" />
@@ -65,8 +60,6 @@
                        <tocitem text="Feature Colourschemes" target="features.featureschemes" />
                        <tocitem text="User Defined Sequence Features" target="seqfeatcreat" />
                        <tocitem text="Editing Sequence Features" target="seqfeatedit" />
-                       <tocitem text="DAS Feature Retrieval" target="das.viewing" />
-                       <tocitem text="DAS Feature Settings" target="das.settings" />
                        <tocitem text="HTML annotation report" target="io.seqreport" />
                </tocitem>
                
@@ -83,8 +76,7 @@
                        <tocitem text="Alignment Conservation Analysis" target="aacon" />
                        <tocitem text="Multi-Harmony Alignment Analysis" target="shmrws" />
                        <tocitem text="Sequence Retrieval" target="seqfetch" />
-                       <tocitem text="Database Reference Retrieval" target="dbreffetcher" />
-                       <tocitem text="DAS Feature Retrieval" target="das.viewing" />
+                       <tocitem text="Database Reference Retrieval" target="dbreffetcher" />                   
                </tocitem>
                
                <tocitem text="Colour Schemes" target="colours" expand="false">
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 fd8c5a3..b3600c9 100755 (executable)
@@ -61,7 +61,7 @@
     <em>Undo / redo</em> - editing of sequences (insertion/removal of
     gaps, removal of sequences, trimming sequences etc) may be undone or
     redone at any time using the appropriate menu items from the edit
-    menu. The undo history list only allows a maximum of 10 actions.
+    menu.
   <p>
     <em>Trimming alignment</em> - First select a column by clicking the
     scale indicator (above the sequences) The alignment may then be
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 e065494..fa273a5 100644 (file)
     </tr>
     <tr>
       <td>
-        <div align="center">-dasserver nickname=URL</div>
-      <td>
-        <div align="left">
-          Add and enable a <a href="dassettings.html">DAS server</a>
-          with given nickname (alphanumeric or underscores only) for
-          retrieval of features for all alignments<br> Sources that
-          also support the sequence command may be specified by
-          prepending the URL with 'sequence:'<br> <em>e.g.</em>
-          sequence:http://localdas.somewhere.org/das/source
-        </div>
-      </td>
-    </tr>
-    <tr>
-      <td>
-        <div align="center">-fetchfrom nickname</div>
-      <td>
-        <div align="left">
-          Query a <a href="dassettings.html">DAS source</a> called
-          nickname for features for the alignments and display them
-        </div>
-      </td>
-    </tr>
-    <tr>
-      <td>
         <div align="center">-groovy FILE/URL</div>
       <td>
         <div align="left">Execute groovy script in FILE (where
index 9cffc51..92d9323 100644 (file)
     provided by InstallAnywhere any output from the application will be
     sent to output.txt, not standard out.<br> The Jalview
     application also requires a number of additional libraries on the
-    class path. The command line below adds the Jalview installation's
-    'lib' directory to the list of directories that are searched for
-    jars to be added to the classpath:
+    class path. The command line below adds all the jar files in the
+    Jalview installation's 'lib' directory to the classpath, as well as
+    the Jalview application jar file:
   </p>
-  <pre>java -Djava.ext.dirs=$INSTALL_DIR$/lib -cp $INSTALL_DIR$/jalview.jar jalview.bin.Jalview -open [FILE] </pre>
+  <pre>java -classpath "$INSTALL_DIR$/lib/*:$INSTALL_DIR$/jalview.jar" jalview.bin.Jalview -open [FILE] </pre>
   <p>
     Use '-help' to get more information on the <a
       href="clarguments.html">command line arguments</a> that
diff --git a/help/html/features/dasfeatures.html b/help/html/features/dasfeatures.html
deleted file mode 100644 (file)
index 1965e70..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<html>
-<!--
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- -->
-<head>
-<title>DAS Features</title>
-</head>
-
-<body>
-  <p>
-    <strong>DAS Sequence Feature Retrieval</strong>
-  </p>
-  <p>Jalview includes a client for retrieving sequences and their
-    features via the Distributed Annotation System.</p>
-  <ol>
-    <li>Open the Feature Settings panel by selecting &quot;View
-      -&gt; Feature Settings...&quot;</li>
-    <li>Click on the &quot;<a href="dassettings.html">DAS
-        Settings</a>&quot; tabbed pane.
-    </li>
-    <li>Select the sources to use for DAS feature retrieval, then
-      click the &quot;Fetch DAS Features&quot; button.
-      <ul>
-        <li>Cancelling Feature Retrieval<br> Press the <strong>Cancel
-            Fetch</strong> button to immediately stop feature retrieval. This
-          will not remove any features already added to the alignment,
-          but will halt any outstanding DAS requests.<em>The cancel
-            fetch button is of particular use when one or more DAS
-            annotation servers are not responding!</em>
-      </ul>
-    </li>
-  </ol>
-  <p>
-    If your DAS source selection contains sources which use UniProt
-    accession ids, you will be asked whether Jalview should find UniProt
-    Accession ids for the given sequence names. It is important to
-    realise that many DAS sources only use UniProt accession ids, rather
-    than Swissprot/UniProt sequence names.<br> The <a
-      href="../webServices/dbreffetcher.html">database
-      reference fetcher</a> documentation describes how Jalview discovers
-    what database references are appropriate for the sequences in the
-    alignment.
-  <ul>
-    <li><em>Note</em><br> Please remember to save your
-      alignment if either the start/end numbering, or the sequence IDs
-      were updated during the ID retrieval process.</li>
-  </ul>
-  <p>&nbsp;
-  <p>
-    <em>DAS support was introduced in Jalview Version 2.1.</em>
-  </p>
-  <br />
-  <p>
-    <em>The DAS registry at http://www.dasregistry.org was
-      decommissioned early in 2015. An unmaintained mirror is currently
-      hosted at http://www.ebi.ac.uk/das-srv/registry/.</em>
-  </p>
-  <p>&nbsp;
-</body>
-</html>
diff --git a/help/html/features/dassettings.html b/help/html/features/dassettings.html
deleted file mode 100644 (file)
index 9600070..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<html>
-<!--
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- -->
-<head>
-<title>DAS Settings</title>
-</head>
-
-<body>
-  <p>
-    <strong>DAS Settings</strong>
-  </p>
-  <p>
-    Jalview can retrieve sequences and features from many <a
-      href="http://biodas.org/">DAS</a> sources. The DAS sources
-    that it uses are discovered and selected <em>via</em> the DAS
-    settings panel, opened either from the <a
-      href="featuresettings.html">View&#8594;Feature Settings</a> dialog
-    box from the alignment window's menu bar, or the <a
-      href="featuresettings.html">Tools&#8594;Preferences</a>
-    dialog box opened from the Desktop menu bar.
-  </p>
-  <p>
-    <img src="das.gif">
-  <p>The available sources are listed in the table using each
-    source's Nickname as its identifier. Clicking on a source's entry in
-    the table reveals more information about that service in the panel
-    to the right. Select the tickbox in the &quot;Use Source&quot;
-    column for a source to add it to the set Jalview queries for
-    alignment and sequence features.</p>
-  <p>You can filter the visible DAS sources by authority, type and
-    &quot;label&quot;. You should read the DAS documentation to
-    understand more about these values.
-  <p>
-    <strong>Updating the list of sources</strong>
-  </p>
-  <p>
-    When the DAS Settings panel is first opened, and when the <strong>'Refresh
-      source'</strong> buton is pressed, a list of DAS sources is retrieved from
-    the DAS registry URL. Note that the registry hosted at
-    http://www.dasregistry.org/das/ was retired at the start of 2015. An
-    alternative service is currently hosted at
-    http://www.ebi.ac.uk/das-srv/registry/das/. To connect to this
-    service, ensure your .jalview_properties file includes the following
-    line:<br> <b>DAS_REGISTRY_URL=http\://www.ebi.ac.uk/das-srv/registry/das/</b>
-  </p>
-  <p>
-    <strong>Adding your own DAS Sources</strong>
-  </p>
-  <p>You can add your own DAS source to the list by clicking the
-    &quot;Add Local Source&quot; button. Enter the URL and nickname of
-    your additional service. It should be noted that Jalview 2.1 will
-    not query additional sources for more information, but this will be
-    implemented in future editions.
-  <p>&nbsp;
-</body>
-</html>
index fd6b99f..4d13dcd 100755 (executable)
     contains tab separated text fields. <strong>No comments are
       allowed</strong>.
   </p>
-  <p>The first set of lines contain type definitions:
+  <p>
+    <strong>Feature Colours</strong>
+  </p>
+  <p>The first set of lines contain feature type definitions and their colours:
   <pre>
 <strong><em>Feature label</em>&#9;<em>Feature Colour</em>
 <!-- &#9;<em>Feature links</em>  --></strong>
   <ul>
     <li>A single colour specified as either a red,green,blue 24 bit
       triplet in hexadecimal (eg. 00ff00) or as comma separated numbers
-      (ranging from 0 to 255))</li>
+      (ranging from 0 to 255))<br>
+      (For help with colour values, see <a href="https://www.w3schools.com/colors/colors_converter.asp">https://www.w3schools.com/colors/colors_converter.asp</a>.)</li>
 
     <li>A <a href="featureschemes.html">graduated colourscheme</a>
       specified as a "|" separated list of fields: <pre>
-[label|]&lt;mincolor&gt;|&lt;maxcolor&gt;|[absolute|]&lt;minvalue&gt;|&lt;maxvalue&gt;[|&lt;thresholdtype&gt;|[&lt;threshold value&gt;]]
+[label <em>or</em> score<em> or</em> attribute|attName|]&lt;mincolor&gt;|&lt;maxcolor&gt;|[absolute|]&lt;minvalue&gt;|&lt;maxvalue&gt;[|&lt;novalue&gt;][|&lt;thresholdtype&gt;|[&lt;threshold value&gt;]]
 </pre> The fields are as follows:
 
       <ul>
-        <li><em>label</em><br> Indicate that the feature
+        <li><em>label</em><br> Indicates that the feature
           description should be used to create a colour for features of
           this type.<br> <em>Note: if no threshold value is
-            needed then the final '|' may be omitted.<br> This
+            needed then only 'label' is required.<br> This
             keyword was added in Jalview 2.6
         </em></li>
 
+        <li><em>score</em><br> Indicates that the feature
+          score should be used to create a graduated colour for features of
+          this type, in conjunction with mincolor, maxcolor.<br><em>This keyword was added in Jalview 2.11.
+          It may be omitted (score is the default) if mincolor and maxcolor are specified.
+        </em></li>
+
+        <li><em>attribute|attName</em><br> Indicates that the value of feature
+          attribute 'attName' should be used to create a colour for features of
+          this type.
+          <br>For example, <em>attribute|clinical_significance</em> to colour by clinical_significance.
+          <br>To colour by range of a numeric attribute, include <em>mincolor</em> and <em>maxcolor</em>, or omit to colour by text (category).
+          <br>(Note: the value of the attribute used for colouring will also be shown in the tooltip as you mouse over features.)
+          <br>A sub-attribute should be written as, for example, CSQ:IMPACT.
+          <br><em>This keyword was added in Jalview 2.11</em></li>
+
         <li><em>mincolor</em> and <em>maxcolor</em><br> Colour
           triplets specified as hexadecimal or comma separated values
           (may be left blank for a <em>label</em> style colourscheme,
 
         <li><em>minvalue</em> and <em>maxvalue</em><br>
           Minimum and maximum values defining the range of scores for
-          which the colour range will be defined over. If minvalue is
+          which the colour range will be defined over.<br>If minvalue is
           greater than maxvalue then the linear mapping will have
           negative gradient.</li>
 
+        <li><em>novalue</em> <br>
+          Specifies the colour to use if colouring by attribute, when the attribute is absent.
+          Valid options are <em>novaluemin, novaluemax, novaluenone</em>, to use mincolor, maxcolor, or no colour.
+          <br>If not specified this will default to novaluemin.</li>
+
         <li><em>thresholdtype</em><br> Either
           &quot;none&quot;, &quot;below&quot;, or &quot;above&quot;. <em>below</em>
           and <em>above</em> require an additional <em>threshold
   </ul>
   </p>
 
+  <p>
+    <strong>Feature Filters</strong>
+  </p>
+  <p>This section is optional, and allows one or more filters to be defined for each feature type.
+     <br>Only features that satisfy the filter conditions will be displayed.
+     <br>Begin with a line which is just STARTFILTERS, and end with a line which is just ENDFILTERS.
+     <br>Each line has the format:
+     <pre>featureType <em>&lt;tab&gt;</em> (filtercondition1) [and|or] (filtercondition2) [and|or]...<br></pre>
+     The parentheses are not needed if there is only one condition. 
+     Combine multiple conditions with either <em>and</em> or <em>or</em> (but not a mixture).
+     <br>Each condition is written as:
+     <pre>Label <em>or</em> Score <em>or</em> AttributeName condition [value]</pre>
+     where either the label (description), (numeric) score, or (text or numeric) attribute is tested against the condition.
+     <br><em>condition</em> is not case sensitive, and should be one of
+     <ul>
+     <li><em>Contains</em> - description (or attribute) should contain the given value (not case sensitive); example <em>clinical_significance contains Pathogenic</em></li> 
+     <li><em>NotContains</em> - description (or attribute) should not contain the given value</li> 
+     <li><em>Matches</em> - description (or attribute)  should match the given value (not case sensitive)</li> 
+     <li><em>NotMatches</em> - description (or attribute) should not match the given value (not case sensitive)</li> 
+     <li><em>Present</em> - attribute is present on the feature (no value required); example <em>CSQ:SIFT present</em></li> 
+     <li><em>NotPresent</em> - attribute is not present on the feature (no value required)</li> 
+     <li><em>EQ</em> - feature score, or specified attribute, is equal to the (numeric) value</li> 
+     <li><em>NE, LT, LE, GT, GE</em> - tests for not equal to / less than / less than or equal to / greater than / greater than or equal to the value</li> 
+     </ul>
+     A non-numeric value always fails a numeric test.<br>If either attribute name, or value to compare, contains spaces, then enclose in single quotes:
+     <em>'mutagenesis site' contains 'decreased affinity'</em>
+     <br>Tip: some examples of filter syntax are given below; or to see more, first configure colours and filters in Jalview, then <em>File | Export Features</em> to Textbox in Jalview Format.
+     <br><em>Feature filters were added in Jalview 2.11</em>
+  </p>
+
+  <p>
+    <strong>Feature Instances</strong>
+  </p>
+
   <p>The remaining lines in the file are the sequence annotation
     definitions, where the now defined features are attached to regions
     on particular sequences. Each feature can optionally include some
     descriptive text which is displayed in a tooltip when the mouse is
-    near the feature on that sequence (and can also be used to generate
-    a colour the feature).</p>
+    near the feature on that sequence (and may also be used to generate
+    a colour for the feature).</p>
 
   <p>
     If your sequence annotation is already available in <a href="http://gmod.org/wiki/GFF2">GFF2</a> (http://gmod.org/wiki/GFF2) or
@@ -204,6 +262,13 @@ signal peptide&#9;0,155,165
 helix&#9;ff0000
 strand&#9;00ff00
 coil&#9;cccccc
+kdHydrophobicity&#9;ccffcc|333300|-3.9|4.5|above|-2.0
+
+STARTFILTERS
+metal ion-binding site&#9;Label Contains sulfur
+kdHydrophobicity&#9;(Score LT 1.5) OR (Score GE 2.8)
+ENDFILTERS
+
 Your Own description here&#9;FER_CAPAA&#9;-1&#9;3&#9;93&#9;domain
 Your Own description here&#9;FER_CAPAN&#9;-1&#9;48&#9;144&#9;chain
 Your Own description here&#9;FER_CAPAN&#9;-1&#9;50&#9;140&#9;domain
@@ -211,10 +276,16 @@ Your Own description here&#9;FER_CAPAN&#9;-1&#9;136&#9;136&#9;modified residue
 Your Own description here&#9;FER1_LYCES&#9;-1&#9;1&#9;47&#9;transit peptide
 Your Own description here&#9;Q93XJ9_SOLTU&#9;-1&#9;1&#9;48&#9;signal peptide
 Your Own description here&#9;Q93XJ9_SOLTU&#9;-1&#9;49&#9;144&#9;chain
-startgroup&#9;secondarystucture
+
+STARTGROUP&#9;secondarystucture
 PDB secondary structure annotation&#9;FER1_SPIOL&#9;-1&#9;52&#9;59&#9;strand
 PDB secondary structure annotation&#9;FER1_SPIOL&#9;-1&#9;74&#9;80&#9;helix
-endgroup&#9;secondarystructure
+ENDGROUP&#9;secondarystructure
+
+STARTGROUP&#9;kd
+Hydrophobicity score by kD     Q93XJ9_SOLTU    -1      48      48      kdHydrophobicity        1.8
+ENDGROUP&#9;kd
+
 GFF
 FER_CAPAA&#9;GffGroup&#9;domain&#9;3&#9;93&#9;.&#9;.
 </pre>
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..52e88db 100755 (executable)
         Preferences</a> tab contains settings affecting the export of
       sequence alignments and EPS files.
     </li>
-    <li>The <a href="dassettings.html"><strong>&quot;DAS
-          Settings&quot;</strong> Preferences</a> tab allows you to select which DAS
-      sources to use when fetching DAS Features.
-    </li>
     <li>The <a href="../webServices/webServicesPrefs.html"><strong>&quot;Web
           Service&quot;</strong> Preferences</a> tab allows you to configure the <a
       href="http://www.compbio.dundee.ac.uk/jabaws">JABAWS</a>
     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 72e5bdf..eec68ee 100755 (executable)
@@ -159,7 +159,7 @@ td {
   <p>The search history keeps up to 99 queries by default. To clear
     the history, or modify the size of the history, right-click the text
     box.</p>
-  <img src="searchclearhist.png" width="402" height="121" align="left" />
+  <img src="searchclearhist.png" width="402" height="127" align="left" />
   <p>
     <strong>Other dialogs that provide a query history</strong>
   </p>
index 200c4d3..29cc34f 100644 (file)
Binary files a/help/html/features/searchclearhist.png and b/help/html/features/searchclearhist.png differ
index 3c9d2b8..eeb63f6 100755 (executable)
       href="featuresettings.html">Sequence Feature Settings</a>
     dialog box. Feature colour schemes and display parameters are unique
     to a particular alignment, so it is possible to colour the same
-    sequence features differently in different alignment views.<br>
-    Since Jalview 2.1, it is possible to add <a href="dassettings.html">DAS
-      features</a> to an alignment via the DAS tabbed pane of the feature
-    settings window.
+    sequence features differently in different alignment views.
   </p>
   <p>
     <strong>View&#8594;Sequence ID Tooltip&#8594;Show
index 44aa1c2..e726c49 100755 (executable)
   <p>
     <strong>Sequence Fetcher</strong>
   </p>
-  <p>
-    Jalview can retrieve sequences from certain databases using either
-    the DBFetch service provided by the EMBL European Bioinformatics
-    Institute, or, since Jalview 2.4, DAS servers capable of the <em>sequence</em>
-    command (configured in <a href="dassettings.html">DAS settings</a>).
-  </p>
-  <p>The Sequence Fetcher can be opened via the &quot;File&quot;
+       <p>Jalview can retrieve sequences from a range of sequence, 3D
+               structure, genomic and domain family databases provided by EMBL-EBI.</p>
+       <p>The Sequence Fetcher can be opened via the &quot;File&quot;
     menu on the main desktop in order to retrieve sequences as a new
     alignment, or opened via the &quot;File&quot; menu of an existing
     alignment to import additional sequences. There may be a short delay
-    when the sequence fetcher is first opened, whilst Jalview compiles
-    the list of available sequence datasources from the currently
-    defined DAS server registry.</p>
+    when the sequence fetcher is first opened, whilst Jalview contacts each database's web API.</p>
   <p>
     Every time a new fetcher is opened, you will need to <strong>select
       the database you want to retrieve sequences</strong> from the database
     tooltips are shown if you mouse over some sources, explaining what
     the database will retrieve. You can select one by using the up/down
     arrow keys and hitting return, or by double clicking with the mouse.
-    <br />
-    <em>If you have DAS sources enabled, then you may have several
-      sources for the same type of sequence identifier, and these will
-      be grouped together in a sub-branch branch labeled with the
-      identifier.</em>
   </p>
   <p>Once you have selected a sequence database, its fetcher dialog
     will open. Jalview provides two types of dialog:</p>
       currently selected database into the retrieval box. Finally, press
       &quot;OK&quot; to initiate the retrieval.</li>
   </ol>
-  <p>
-    <strong>Only retrieving part of a sequence</strong>
-  </p>
-  <p>
-    When using DAS sources (indicated by a &quot;<em>(DAS)</em>&quot;),
-    you can append a range in addition to a sequence ID. For example, to
-    retrieve 50 residues starting at position 35 in UNIPROT sequence
-    P73137 using the UNIPROT DAS server, you would enter
-    &quot;'P73137:35,84'.<br /> <em>Full support for DAS range
-      queries was introduced in Jalview 2.8</em>
-  </p>
 
   <p>If you use the WSDBFetch sequence fetcher services (EMBL,
     UniProt, PFAM, and RFAM) in work for publication, please cite:</p>
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 2142f98..9437a60 100755 (executable)
@@ -115,7 +115,7 @@ lax.nl.java.option.java.heap.size.initial=500m
 ! &lt;string&gt;-Xms2M&lt;/string&gt;
 ! &lt;string&gt;-Xmx64M&lt;/string&gt;
 &lt;/array&gt;
-</pre> Exchange the above two string tags for : <pre>
+</pre>Exchange the above two string tags for : <pre>
 &lt;string&gt;-Xms500M&lt;/string&gt;
 &lt;string&gt;-Xmx1000M&lt;/string&gt;
 </pre>
@@ -125,6 +125,11 @@ lax.nl.java.option.java.heap.size.initial=500m
       the file and try to start Jalview in the normal way. If it doesn't
       start, see below...</li>
   </ul>
+  <p>
+    <em>Please Note:</em> We do modify the default memory settings in
+    Jalview from time to time, so you may find different numbers to
+    those shown in the examples above.
+  </p>
   <font size="3"><em>Jalview doesn't start... What do the
       memory settings mean ?<a name="memsetting"></a>
   </em></font>
@@ -140,6 +145,12 @@ lax.nl.java.option.java.heap.size.initial=500m
     enlighten us if you know better!). Our experiments found 1000m to be
     the biggest setting that could be used on a 1GB machine. Just try
     reducing the sizes until Jalview starts up properly!</p>
+  <p>
+    We increased the default memory in Jalview 2.10.5 to 1G. To launch
+    Jalview with the pre 2.10.5 default memory allocation, use the <a
+      href="http://www.jalview.org/webstart/jalview_256MB.jnlp">Jalview
+      256MB JNLP</a>.
+  </p>
   <p>&nbsp;</p>
 </body>
 </html>
index 00a2ec4..f3ab75d 100755 (executable)
             href="../features/featuresettings.html">Sequence
               Feature Settings...</a> </strong><br> <em>Opens the
               Sequence Feature Settings dialog box to control the colour
-              and display of sequence features on the alignment, and
-              configure and retrieve features from DAS annotation
-              servers.</em></li>
+              and display of sequence features on the alignment.</em></li>
         <li><strong>Sequence ID Tooltip</strong><em>
             (application only) <br>This submenu's options allow the
             inclusion or exclusion of non-positional sequence features
       is dynamic, and may contain user-defined web service entries in
       addition to any of the following ones:</em>
     <ul>
-      <li><strong>Fetch DB References</strong><br> <em>This
-          submenu contains options for accessing any of the database
-          services that Jalview is aware of (e.g. DAS sequence servers
-          and the WSDBFetch service provided by the EBI) to verify
-          sequence start/end positions and retrieve all database cross
-          references and PDB ids associated with all or just the
-          selected sequences in the alignment.
-          <ul>
-            <li>'Trim Retrieved Sequences' - when checked, Jalview
-              will discard any additional sequence data for accessions
-              associated with sequences in the alignment. <br> <strong>Note:
-                Disabling this could cause out of memory errors when
-                working with genomic sequence records !</strong><br> <strong>Added
-                in Jalview 2.8.1</strong>
-            </li>
-            <li>'Standard Databases' will check sequences against
-              the EBI databases plus any active DAS sequence sources</li>
-          </ul> Other sub-menus allow you to pick a specific source to query
-          - sources are listed alphabetically according to their
-          nickname.
-      </em><br></li>
-    </ul>
+                               <li><strong>Fetch DB References</strong><br> <em>This
+                                               submenu contains options for accessing any of the database
+                                               services that Jalview is aware of (e.g. those provided by
+                                               EMBL-EBI) to verify sequence start/end positions and retrieve all
+                                               database cross references and PDB ids associated with all or just
+                                               the selected sequences in the alignment.
+                                               <ul>
+                                                       <li>'Trim Retrieved Sequences' - when checked, Jalview will
+                                                               discard any additional sequence data for accessions associated
+                                                               with sequences in the alignment. <br> <strong>Note:
+                                                                       Disabling this could cause out of memory errors when working
+                                                                       with genomic sequence records !</strong><br> <strong>Added
+                                                                       in Jalview 2.8.1</strong>
+                                                       </li>
+                                                       <li>'Standard Databases' will check sequences against the
+                                                               EBI databases.</li>
+                                               </ul> Other sub-menus allow you to pick a specific source to query -
+                                               sources are listed alphabetically according to their nickname.
+                               </em><br></li>
+                       </ul>
     <p>Selecting items from the following submenus will start a
       remote service on compute facilities at the University of Dundee,
       or elsewhere. You need a continuous network connection in order to
index 2028304..33ec77f 100755 (executable)
@@ -53,8 +53,7 @@
     <li><strong><a href="../features/featuresettings.html">Sequence
           Feature Settings...</a></strong><em><br> Opens the Sequence
         Feature Settings dialog box to control the colour and display of
-        sequence features on the alignment, and configure and retrieve
-        features from DAS annotation servers.</em></li>
+        sequence features on the alignment.</em></li>
     <li><strong>Sequence ID Tooltip</strong><em> (application
         only) <br>This submenu's options allow the inclusion or
         exclusion of non-positional sequence features or database cross
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 b71bc1a..b8b357e 100755 (executable)
       dynamic, and may contain user-defined web service entries in
       addition to any of the following ones:</em>
   <ul>
-    <li><strong>Fetch DB References</strong><br> <em>This
-        submenu contains options for accessing any of the database
-        services that Jalview is aware of (e.g. DAS sequence servers and
-        the WSDBFetch service provided by the EBI) to verify sequence
-        start/end positions and retrieve all database cross references
-        and PDB ids associated with all or just the selected sequences
-        in the alignment.
-        <ul>
+    <li><strong>Fetch DB References</strong><br> <em>This submenu
+                               contains options for accessing any of the database services that
+                               Jalview is aware of (e.g services provided by the EBI) to verify
+                               sequence start/end positions and retrieve all database cross
+                               references and PDB ids associated with all or just the selected
+                               sequences in the alignment.
+                               <ul>
           <li>'Retrieve full Sequence' - when checked, Jalview will
             retrieve the full sequence for any accessions associated
             with sequences in the alignment. <br> <strong>Note:
@@ -45,7 +44,7 @@
               in Jalview 2.8.1</strong>
           </li>
           <li>'Standard Databases' will check sequences against the
-            EBI databases plus any active DAS sequence sources<</li>
+            EBI databases</li>
         </ul> Other submenus allow you to pick a specific source to query -
         sources are listed alphabetically according to their nickname.
     </em></li>
index 4be594a..eb13ab3 100755 (executable)
@@ -67,28 +67,218 @@ li:before {
         </div>
       </td>
     </tr>
+    
+      <td width="60" nowrap>
+        <div align="center">
+          <strong><a name="Jalview.2.11.0">2.11.0</a><br />
+            <em>29/01/2019</em></strong>
+        </div>
+      </td>
+    <td><div align="left">
+        <em></em>
+        <ul>
+          <li>
+            <!-- JAL-2865 -->Jalview doesn't hang when closing windows
+            or the overview updates with large alignments.
+          </li>
+        </ul>
+      </div></td>
+    <td><div align="left">
+        <em></em>
+        <ul>
+          <li>
+            <!-- JAL-3035 -->DAS sequence retrieval and annotation
+            capabilities removed from the Jalview Desktop
+          </li>
+        </ul>
+        <em>Editing</em>
+        <ul>
+          <li>
+            <!-- JAL-2822 -->Start and End should be updated when
+            sequence data at beginning or end of alignment added/removed
+            via 'Edit' sequence
+          </li>
+          <li>
+            <!-- JAL-2541 -->Delete/Cut selection doesn't relocate
+            sequence features correctly when start of sequence is
+            removed (Known defect since 2.10)
+          </li>
+          <li>
+            <!-- JAL- -->
+          </li>
+        </ul>
+      </div></td>
+    </tr>
+    <tr>
+    <td width="60" nowrap>
+      <div align="center">
+        <strong><a name="Jalview.2.10.5">2.10.5</a><br /> <em>10/09/2018</em></strong>
+      </div>
+    </td>
+    <td><div align="left">
+        <em></em>
+        <ul>
+            <li>
+              <!-- JAL-3101 -->Default memory for Jalview webstart and
+              InstallAnywhere increased to 1G.
+            </li>
+            <li>
+              <!-- JAL-247 -->Hidden sequence markers and representative
+              sequence bolding included when exporting alignment as EPS,
+              SVG, PNG or HTML. <em>Display is configured via the
+                Format menu, or for command-line use via a jalview
+                properties file.</em>
+            </li>
+            <li>
+              <!-- JAL-3076 -->Ensembl client updated to Version 7 REST
+              API and sequence data now imported as JSON.
+            </li>
+            <li>
+              <!-- JAL-3065 -->Change in recommended way of starting
+              Jalview via a Java command line: add jars in lib directory
+              to CLASSPATH, rather than via the deprecated java.ext.dirs
+              property.
+            </li>
+          </ul>
+          <em>Development</em>
+          <ul>
+            <li>
+              <!-- JAL-3047 -->Support added to execute test suite
+              instrumented with <a href="http://openclover.org/">Open
+                Clover</a>
+            </li>
+          </ul>
+        </div></td>
+    <td><div align="left">
+        <em></em>
+        <ul>
+            <li>
+              <!-- JAL-3104 -->Poorly scaled bar in quality annotation
+              row shown in Feredoxin Structure alignment view of example
+              alignment.
+            </li>
+            <li>
+              <!-- JAL-2854 -->Annotation obscures sequences if lots of
+              annotation displayed.
+            </li>
+            <li>
+              <!-- JAL-3107 -->Group conservation/consensus not shown
+              for newly created group when 'Apply to all groups'
+              selected
+            </li>
+            <li>
+              <!-- JAL-3087 -->Corrupted display when switching to
+              wrapped mode when sequence panel's vertical scrollbar is
+              visible.
+            </li>
+            <li>
+              <!-- JAL-3003 -->Alignment is black in exported EPS file
+              when sequences are selected in exported view.</em>
+            </li>
+            <li>
+              <!-- JAL-3059 -->Groups with different coloured borders
+              aren't rendered with correct colour.
+            </li>
+            <li>
+              <!-- JAL-3092 -->Jalview could hang when importing certain
+              types of knotted RNA secondary structure.
+            </li>
+            <li>
+              <!-- JAL-3095 -->Sequence highlight and selection in
+              trimmed VARNA 2D structure is incorrect for sequences that
+              do not start at 1.
+            </li>
+            <li>
+              <!-- JAL-3061 -->'.' inserted into RNA secondary structure
+              annotation when columns are inserted into an alignment,
+              and when exporting as Stockholm flatfile.
+            </li>
+            <li>
+              <!-- JAL-3053 -->Jalview annotation rows containing upper
+              and lower-case 'E' and 'H' do not automatically get
+              treated as RNA secondary structure.
+            </li>
+            <li>
+              <!-- JAL-3106 -->.jvp should be used as default extension
+              (not .jar) when saving a jalview project file.
+            </li>
+            <li>
+              <!-- JAL-3105 -->Mac Users: closing a window correctly
+              transfers focus to previous window on OSX
+            </li>
+          </ul>
+          <em>Java 10 Issues Resolved</em>
+          <ul>
+            <li>
+              <!-- JAL-2988 -->OSX - Can't save new files via the File
+              or export menus by typing in a name into the Save dialog
+              box.
+            </li>
+            <li>
+              <!-- JAL-2988 JAL-2968 -->Jalview now uses patched version
+              of the <a href="https://violetlib.org/vaqua/overview.html">VAqua5</a>
+              'look and feel' which has improved compatibility with the
+              latest version of OSX.
+            </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>
+          <strong><a name="Jalview.2.10.4b1">2.10.4b1</a><br />
+            <em>7/06/2018</em></strong>
         </div>
       </td>
       <td><div align="left">
-          <em>New features in Jalview Desktop</em>
+          <em></em>
           <ul>
             <li>
-              <!-- JAL-2748 -->Uniprot Sequence Fetcher now uses web API at uniprot.org 
-              <!-- JAL-2745 -->HTTPS used for all connections to ebi.ac.uk 
+              <!-- JAL-2920 -->Use HGVS nomenclature for variant
+              annotation retrieved from Uniprot
+            </li>
+            <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-3017 -->Cannot import features with multiple
+              variant elements (blocks import of some Uniprot records)
+            </li>
+            <li>
+              <!-- JAL-2997 -->Clustal files with sequence positions in
+              right-hand column parsed correctly
+            </li>
+            <li>
+              <!-- JAL-2991 -->Wrap view - export to SVG - IDs shown but
+              not alignment area in exported graphic
+            </li>
+            <li>
+              <!-- JAL-2993 -->F2/Keyboard mode edits work when Overview
+              window has input focus
+            </li>
+            <li>
+              <!-- JAL-2992 -->Annotation panel set too high when
+              annotation added to view (Windows)
+            </li>
+            <li>
+              <!-- JAL-3009 -->Jalview Desktop is slow to start up when
+              network connectivity is poor
+            </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>
           </ul>
         </div></td>
@@ -96,6 +286,448 @@ li:before {
     <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>
@@ -173,7 +805,7 @@ li:before {
             </li>
           </ul>
         </div></td>
-    </tr>    
+    </tr>
     <tr>
       <td width="60" nowrap>
         <div align="center">
@@ -1428,6 +2060,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 83c80ba..1f61dea 100644 (file)
@@ -30,8 +30,7 @@
   ID, and can be viewed in full via the
   <a href="../io/exportseqreport.html">Sequence Details</a> window. .
   Jalview also uses references for the retrieval of
-  <a href="../features/viewingpdbs.html">PDB structures</a> and <a
-    href="../features/dasfeatures.html">DAS features</a>, and for
+  <a href="../features/viewingpdbs.html">PDB structures</a>, and for
   retrieving sequence cross-references such as the protein products of a
   DNA sequence.
 </p>
   application provides three ways to access the retrieval function.
   Either:
 <ul>
-  <li>select the <strong>Discover PDB IDs</strong> option from the
-    structure submenu of the sequence's popup menu
-  </li>
-  <li>Choose one of the options from the 'Fetch DB Refs' submenu in
+       <li>select the <strong>Structure Chooser...</strong> option from
+               the Sequence ID popup menu.
+       </li>
+       <li>Choose one of the options from the 'Fetch DB Refs' submenu in
     the alignment window's <strong>Web Services</strong> menu:
     <ul>
-      <li><em>Standard Databases</em> will fetch references from
-        the EBI databases plus currently selected DAS sources</li>
-      <li>The other entries submenus leading to lists of individual
+                       <li><em>Standard Databases</em> will fetch references from EBI
+                               databases appropriate for the sequence type (Nucleotide or Protein)</li>
+                       <li>The other entries submenus leading to lists of individual
         database sources that Jalview can access.</li>
     </ul>
   </li>
-  <li>Answer 'Yes' when asked if you wish to retrieve database
-    references for your sequences after initiating a DAS Sequence
-    Feature fetch.</li>
 </ul>
 <p>Jalview discovers references for a sequence by generating a set
   of ID queries from the ID string of each sequence in the alignment. It
index d0b497c..8f36013 100755 (executable)
       Institute (EBI) and Distributed Annotation System servers that are
       capable of serving sequences.
     </li>
-    <li>The <a href="../features/dasfeatures.html">DAS Feature
-        Fetcher</a> enables the retrieval and visualization of features from
-      DAS annotation sources
-    </li>
-    <li>The <a href="dbreffetcher.html">Database Reference
-        Fetcher</a> transfers database references from records available
-      from DAS or the public sequence databases.
-    </li>
-    <li>The <strong>Web Services</strong> menu in each alignment
+               <li>The <a href="dbreffetcher.html">Database Reference
+                               Fetcher</a> transfers database references and annotation from the public
+                       sequence databases.
+               </li>
+               <li>The <strong>Web Services</strong> menu in each alignment
       window also provides access to the following:
       <ul>
         <li>Programs for <a href="msaclient.html">multiple
index f12b9b5..13fe656 100755 (executable)
 </head>
 <body>
   <p>
-    <strong>Jalview 2.10.2b2 bugfix release</strong>
-  </p>
-  <p>
-    This is patch release for 2.10.2. See the
-    <a href="releases.html#Jalview.2.10.2b2">release notes</a> for full
-    details about the bugs addressed. This second patch release fixes
-    problems with the Uniprot sequence fetcher and introduces secure SSL
-    connections for access to EMBL-EBI resources. The previous patch
-    release introduced 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.
+    <strong>What's new in Jalview 2.10.5 ?</strong>
   </p>
+  <p>Jalview 2.10.5 is a minor release that includes critical
+    patches for users working with Ensembl, RNA secondary structure
+    annotation, and those running Jalview on OSX with Java 10.</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>Jalview's default memory limit increased to 1G. <br/>If you have
+      problems starting Jalview 2.10.5 and you have 1G or less
+      physical memory on your machine, you will need to <a
+      href="memory.html#memsetting">reduce the memory</a> allocated to
+      Jalview.
+    </li>
+    <li>EPS, PNG and SVG export now includes hidden sequence
+      markers, and representative sequences are marked in bold.</li>
+    <li>Ensembl Client updated for Ensembl Rest API v7.<br />The
+      latest Ensembl API is not backwards compatible with earlier
+      versions of Jalview, so if you require Ensembl functionality you
+      will need to install this release.
+    </li>
+    <li>Improved support for VIENNA extended dot-bracket notation
+      for RNA secondary structure.</li>
+    <li>Positional and selected region highlighting in VARNA
+      'trimmed sequence' view made more reliable.</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>
+    The full list of bugs fixed in this release can be found in the <a
+      href="releases.html#Jalview.2.10.5">2.10.5 Release Notes</a>. The
+    majority of bug fixes and improvements in 2.10.5 are due to Jalview users
+    contacting us via the jalview-discuss email list. Thanks to everyone
+    who took the time to help make Jalview better !
   </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.
+    <strong>Jalview and Java 10</strong>
   </p>
+  <p>This release addresses a critical bug for OSX users who are
+    running Jalview with Java 10 which can prevent files being saved
+    correctly through the 'Save As' dialog box.</p>
+    <em>Known Issues</em>
   <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>
+    <li>OSX: The 'Open File' dialog for Jalview's Groovy Console
+      appears with the title 'Save As', and attempting to select a file to load yields a FileNotFound exception.</br>The workaround is to first clear the 'Untitled' filename before selecting the file you wish to load.
+      </li>
+    <li>OSX: Links don't open when clicked on or via the Sequence or Alignment window popup menu.</li>
+    <li>OSX (Webstart): Jalview only displays old news feed items</li>
   </ul>
-
 </body>
 </html>
diff --git a/lib/VAqua5-patch.jar b/lib/VAqua5-patch.jar
new file mode 100644 (file)
index 0000000..7b5c27b
Binary files /dev/null and b/lib/VAqua5-patch.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-1.133.jar b/lib/htsjdk-1.133.jar
deleted file mode 100644 (file)
index f084258..0000000
Binary files a/lib/htsjdk-1.133.jar and /dev/null 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/lib/jdas-1.0.4.jar b/lib/jdas-1.0.4.jar
deleted file mode 100644 (file)
index fb5d128..0000000
Binary files a/lib/jdas-1.0.4.jar and /dev/null differ
index d6e4cf3..ca8a275 100644 (file)
@@ -17,7 +17,7 @@
  * 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.
 -->
-<!--
+
 <!-- You may freely edit this file. See commented blocks below for -->
 <!-- some examples of how to customize the build. -->
 <!-- (If you delete it and reopen the project it will be recreated.) -->
index 3488ac6..3c06708 100644 (file)
@@ -1,4 +1,4 @@
-YEAR=2016
-AUTHORS=J Procter, M Carstairs, TC Ofoegbu, K Mourao, AM Waterhouse, J Engelhardt, LM Lui, A Menard, D Barton, N Sherstnev, D Roldan-Martinez, M Clamp, S Searle, G Barton
-AUTHORFNAMES=Jim Procter, Mungo Carstairs, Tochukwu 'Charles' Ofoegbu, Kira Mourao, Andrew Waterhouse, Jan Engelhardt, Lauren Lui, Anne Menard, Daniel Barton, Natasha Sherstnev, David Roldan-Martinez, Michele Clamp, James Cuff, Steve Searle, David Martin & Geoff Barton
+YEAR=2018
+AUTHORS=J Procter, M Carstairs, B Soares, K Mourao, TC Ofoegbu, AM Waterhouse, J Engelhardt, LM Lui, A Menard, D Barton, N Sherstnev, D Roldan-Martinez, M Clamp, S Searle, G Barton
+AUTHORFNAMES=Jim Procter, Mungo Carstairs, Ben Soares, Kira Mourao, Tochukwu 'Charles' Ofoegbu, Andrew Waterhouse, Jan Engelhardt, Lauren Lui, Anne Menard, Daniel Barton, Natasha Sherstnev, David Roldan-Martinez, Michele Clamp, James Cuff, Steve Searle, David Martin & Geoff Barton
  
\ No newline at end of file
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 9f3ab70..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
@@ -913,7 +916,6 @@ label.as_percentage = As Percentage
 error.not_implemented = Not implemented
 error.no_such_method_as_clone1_for = No such method as clone1 for {0}
 error.null_from_clone1 = Null from clone1!
-error.implementation_error_sortbyfeature = Implementation Error - sortByFeature method must be one of FEATURE_SCORE, FEATURE_LABEL or FEATURE_DENSITY.
 error.not_yet_implemented = Not yet implemented
 error.unknown_type_dna_or_pep = Unknown Type {0} - dna or pep are the only allowed values.
 error.implementation_error_dont_know_threshold_annotationcolourgradient = Implementation error: don't know about threshold setting for current AnnotationColourGradient.
@@ -1213,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
@@ -1296,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
@@ -1321,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 1e3a391..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
@@ -838,7 +841,6 @@ label.as_percentage = Como Porcentaje
 error.not_implemented = No implementado
 error.no_such_method_as_clone1_for = No existe ese método como un clone1 de {0}
 error.null_from_clone1 = Nulo de clone1!
-error.implementation_error_sortbyfeature = Error de implementación - sortByFeature debe ser uno de FEATURE_SCORE, FEATURE_LABEL o FEATURE_DENSITY.
 error.not_yet_implemented = No se ha implementado todavía
 error.unknown_type_dna_or_pep = Tipo desconocido {0} - dna o pep son los Ãºnicos valores permitidos
 error.implementation_error_dont_know_threshold_annotationcolourgradient = Error de implementación: no se conoce el valor umbral para el AnnotationColourGradient actual.
@@ -1172,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
@@ -1224,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
@@ -1296,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
@@ -1321,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 4a981ad..68868c4 100755 (executable)
  * The Jalview Authors are detailed in the 'AUTHORS' file.
 -->
 <mapping>
-       <class name="jalview.datamodel.UniprotFile">
+  <!-- see https://www.uniprot.org/docs/uniprot.xsd for latest Uniprot XML schema -->
+       <class name="jalview.datamodel.xdb.uniprot.UniprotFile">
                  <map-to xml="uniprot"/>               
-                 <field name="UniprotEntries" type="jalview.datamodel.UniprotEntry" collection="vector">
+                 <field name="UniprotEntries" type="jalview.datamodel.xdb.uniprot.UniprotEntry" collection="vector">
                      <bind-xml name="entry"/>
                 </field>               
         </class>
                 
-        <class name="jalview.datamodel.UniprotEntry">
+        <class name="jalview.datamodel.xdb.uniprot.UniprotEntry">
                <field name="name" type="string" collection="vector"/>
                <field name="accession" type="string" collection="vector"/>
-               <field name="protein" type="jalview.datamodel.UniprotProteinName"/>
-           <field name="UniprotSequence" type="jalview.datamodel.UniprotSequence">
+               <field name="protein" type="jalview.datamodel.xdb.uniprot.UniprotProteinName"/>
+           <field name="UniprotSequence" type="jalview.datamodel.xdb.uniprot.UniprotSequence">
                <bind-xml name="sequence"/> 
            </field>
-           <field name="feature" type="jalview.datamodel.SequenceFeature" collection="vector"/>
+           <field name="feature" type="jalview.datamodel.xdb.uniprot.UniprotFeature" collection="vector"/>
            <field name="dbReference" type="jalview.datamodel.PDBEntry" collection="vector"/>
                       
         </class>
-       <class name="jalview.datamodel.UniprotProteinName">
+       <class name="jalview.datamodel.xdb.uniprot.UniprotProteinName">
                <field name="name" collection="vector" type="string">
                        <bind-xml name="fullName" location="recommendedName" node="element"/>
                </field>
        </class>
         <!-- uniprot protein name is now a collection of collections - the INCLUDES and CONTAINS entries of the uniprot
                record. This means this doesn't exist anymore...
-        <class name="jalview.datamodel.UniprotProteinName">
+        <class name="jalview.datamodel.xdb.uniprot.UniprotProteinName">
                <field name="name" type="string" collection="vector">
                        <bind-xml name="name"/>
                </field>                
         </class>
         -->
         
-        <class name="jalview.datamodel.SequenceFeature">
+        <class name="jalview.datamodel.xdb.uniprot.UniprotFeature">
                <field name="type">
                        <bind-xml node="attribute"/>
                </field>
                </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" collection="vector" type="string">
+     <bind-xml name="variation"/>
+    </field>
+    <field name="original">
+     <bind-xml name="original"/>
+    </field>
         </class>
        
-          <class name="jalview.datamodel.UniprotSequence">
+          <class name="jalview.datamodel.xdb.uniprot.UniprotSequence">
                     <field name="content" type="string">
                        <bind-xml name="sequence" node="text"/>
                     </field>
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 f2dd3d0..904a860 100755 (executable)
@@ -41,20 +41,15 @@ public class PDBChain
 {
   public static final String RESNUM_FEATURE = "RESNUM";
 
-  /**
-   * SequenceFeature group for PDB File features added to sequences
-   */
-  private static final String PDBFILEFEATURE = "PDBFile";
-
   private static final String IEASTATUS = "IEA:jalview";
 
   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;
 
@@ -83,10 +78,10 @@ public class PDBChain
 
   public String pdbid = "";
 
-  public PDBChain(String pdbid, String id)
+  public PDBChain(String thePdbid, String theId)
   {
-    this.pdbid = pdbid == null ? pdbid : pdbid.toLowerCase();
-    this.id = id;
+    this.pdbid = thePdbid == null ? thePdbid : thePdbid.toLowerCase();
+    this.id = theId;
   }
 
   /**
@@ -167,15 +162,58 @@ public class PDBChain
   }
 
   /**
-   * copy over the RESNUM seqfeatures from the internal chain sequence to the
+   * 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
    * 
    * @param seq
    * @param status
    *          The Status of the transferred annotation
-   * @return the features added to sq (or its dataset)
    */
-  public SequenceFeature[] transferRESNUMFeatures(SequenceI seq,
+  public void transferRESNUMFeatures(SequenceI seq,
           String status)
   {
     SequenceI sq = seq;
@@ -184,10 +222,11 @@ public class PDBChain
       sq = sq.getDatasetSequence();
       if (sq == sequence)
       {
-        return null;
+        return;
       }
     }
-    /**
+
+    /*
      * Remove any existing features for this chain if they exist ?
      * SequenceFeature[] seqsfeatures=seq.getSequenceFeatures(); int
      * totfeat=seqsfeatures.length; // Remove any features for this exact chain
@@ -197,21 +236,19 @@ public class PDBChain
     {
       status = PDBChain.IEASTATUS;
     }
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-    if (features == null)
-    {
-      return null;
-    }
-    for (int i = 0; i < features.length; i++)
+
+    List<SequenceFeature> features = sequence.getSequenceFeatures();
+    for (SequenceFeature feature : features)
     {
-      if (features[i].getFeatureGroup() != null
-              && features[i].getFeatureGroup().equals(pdbid))
+      if (feature.getFeatureGroup() != null
+              && feature.getFeatureGroup().equals(pdbid))
       {
-        SequenceFeature tx = new SequenceFeature(features[i]);
-        tx.setBegin(1 + residues.elementAt(tx.getBegin() - offset).atoms
-                .elementAt(0).alignmentMapping);
-        tx.setEnd(1 + residues.elementAt(tx.getEnd() - offset).atoms
-                .elementAt(0).alignmentMapping);
+        int newBegin = 1 + residues.elementAt(feature.getBegin() - offset).atoms
+                .elementAt(0).alignmentMapping;
+        int newEnd = 1 + residues.elementAt(feature.getEnd() - offset).atoms
+                .elementAt(0).alignmentMapping;
+        SequenceFeature tx = new SequenceFeature(feature, newBegin, newEnd,
+                feature.getFeatureGroup(), feature.getScore());
         tx.setStatus(status
                 + ((tx.getStatus() == null || tx.getStatus().length() == 0)
                         ? ""
@@ -222,7 +259,6 @@ public class PDBChain
         }
       }
     }
-    return features;
   }
 
   /**
@@ -307,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;
@@ -326,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()))
@@ -354,25 +391,25 @@ public class PDBChain
               && residues.lastElement().atoms
                       .get(0).resNumber == currAtom.resNumber)
       {
-        SequenceFeature sf = new SequenceFeature("INSERTION",
-                currAtom.resName + ":" + currAtom.resNumIns + " " + pdbid
-                        + id,
-                "", offset + count - 1, offset + count - 1, "PDB_INS");
+        String desc = currAtom.resName + ":" + currAtom.resNumIns + " "
+                + pdbid + id;
+        SequenceFeature sf = new SequenceFeature("INSERTION", desc, offset
+                + count - 1, offset + count - 1, "PDB_INS");
         resFeatures.addElement(sf);
         residues.lastElement().atoms.addAll(resAtoms);
       }
       else
       {
-
         // Make a new Residue object with the new atoms vector
         residues.addElement(new Residue(resAtoms, resNumber - 1, count));
 
         Residue tmpres = residues.lastElement();
         Atom tmpat = tmpres.atoms.get(0);
         // Make A new SequenceFeature for the current residue numbering
-        SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE,
-                tmpat.resName + ":" + tmpat.resNumIns + " " + pdbid + id,
-                "", offset + count, offset + count, pdbid);
+        String desc = tmpat.resName
+                + ":" + tmpat.resNumIns + " " + pdbid + id;
+        SequenceFeature sf = new SequenceFeature(RESNUM_FEATURE, desc,
+                offset + count, offset + count, pdbid);
         resFeatures.addElement(sf);
         resAnnotation.addElement(new Annotation(tmpat.tfactor));
         // Keep totting up the sequence
@@ -433,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);
@@ -442,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);
     }
@@ -558,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 85ae718..a910a5a 100644 (file)
@@ -40,6 +40,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -83,7 +84,7 @@ public class ChimeraManager
     this.structureManager = structureManager;
     chimera = null;
     chimeraListenerThread = null;
-    currentModelsMap = new HashMap<Integer, ChimeraModel>();
+    currentModelsMap = new HashMap<>();
 
   }
 
@@ -98,7 +99,7 @@ public class ChimeraManager
   public List<ChimeraModel> getChimeraModels(String modelName,
           ModelType modelType)
   {
-    List<ChimeraModel> models = new ArrayList<ChimeraModel>();
+    List<ChimeraModel> models = new ArrayList<>();
     for (ChimeraModel model : currentModelsMap.values())
     {
       if (modelName.equals(model.getModelName())
@@ -112,7 +113,7 @@ public class ChimeraManager
 
   public Map<String, List<ChimeraModel>> getChimeraModelsMap()
   {
-    Map<String, List<ChimeraModel>> models = new HashMap<String, List<ChimeraModel>>();
+    Map<String, List<ChimeraModel>> models = new HashMap<>();
     for (ChimeraModel model : currentModelsMap.values())
     {
       String modelName = model.getModelName();
@@ -393,7 +394,7 @@ public class ChimeraManager
 
   public Map<Integer, ChimeraModel> getSelectedModels()
   {
-    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<Integer, ChimeraModel>();
+    Map<Integer, ChimeraModel> selectedModelsMap = new HashMap<>();
     List<String> chimeraReply = sendChimeraCommand(
             "list selection level molecule", true);
     if (chimeraReply != null)
@@ -418,7 +419,7 @@ public class ChimeraManager
    */
   public List<String> getSelectedResidueSpecs()
   {
-    List<String> selectedResidues = new ArrayList<String>();
+    List<String> selectedResidues = new ArrayList<>();
     List<String> chimeraReply = sendChimeraCommand(
             "list selection level residue", true);
     if (chimeraReply != null)
@@ -471,7 +472,7 @@ public class ChimeraManager
   // TODO: [Optional] Handle smiles names in a better way in Chimera?
   public List<ChimeraModel> getModelList()
   {
-    List<ChimeraModel> modelList = new ArrayList<ChimeraModel>();
+    List<ChimeraModel> modelList = new ArrayList<>();
     List<String> list = sendChimeraCommand("list models type molecule",
             true);
     if (list != null)
@@ -494,7 +495,7 @@ public class ChimeraManager
    */
   public List<String> getPresets()
   {
-    ArrayList<String> presetList = new ArrayList<String>();
+    ArrayList<String> presetList = new ArrayList<>();
     List<String> output = sendChimeraCommand("preset list", true);
     if (output != null)
     {
@@ -550,17 +551,19 @@ public class ChimeraManager
     // iterate over possible paths for starting Chimera
     for (String chimeraPath : chimeraPaths)
     {
-      File path = new File(chimeraPath);
-      // uncomment the next line to simulate Chimera not installed
-      // path = new File(chimeraPath + "x");
-      if (!path.canExecute())
-      {
-        error += "File '" + path + "' does not exist.\n";
-        continue;
-      }
       try
       {
-        List<String> args = new ArrayList<String>();
+        // ensure symbolic links are resolved
+        chimeraPath = Paths.get(chimeraPath).toRealPath().toString();
+        File path = new File(chimeraPath);
+        // uncomment the next line to simulate Chimera not installed
+        // path = new File(chimeraPath + "x");
+        if (!path.canExecute())
+        {
+          error += "File '" + path + "' does not exist.\n";
+          continue;
+        }
+        List<String> args = new ArrayList<>();
         args.add(chimeraPath);
         // shows Chimera output window but suppresses REST responses:
         // args.add("--debug");
@@ -573,7 +576,7 @@ public class ChimeraManager
         break;
       } catch (Exception e)
       {
-        // Chimera could not be started
+        // Chimera could not be started using this path
         error += e.getMessage();
       }
     }
@@ -699,7 +702,7 @@ public class ChimeraManager
 
   public List<String> getAttrList()
   {
-    List<String> attributes = new ArrayList<String>();
+    List<String> attributes = new ArrayList<>();
     final List<String> reply = sendChimeraCommand("list resattr", true);
     if (reply != null)
     {
@@ -718,7 +721,7 @@ public class ChimeraManager
   public Map<ChimeraResidue, Object> getAttrValues(String aCommand,
           ChimeraModel model)
   {
-    Map<ChimeraResidue, Object> values = new HashMap<ChimeraResidue, Object>();
+    Map<ChimeraResidue, Object> values = new HashMap<>();
     final List<String> reply = sendChimeraCommand("list residue spec "
             + model.toSpec() + " attribute " + aCommand, true);
     if (reply != null)
@@ -818,10 +821,10 @@ public class ChimeraManager
   protected List<String> sendRestCommand(String command)
   {
     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
-    List<NameValuePair> commands = new ArrayList<NameValuePair>(1);
+    List<NameValuePair> commands = new ArrayList<>(1);
     commands.add(new BasicNameValuePair("command", command));
 
-    List<String> reply = new ArrayList<String>();
+    List<String> reply = new ArrayList<>();
     BufferedReader response = null;
     try
     {
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 f8e8379..e4f2dfa 100755 (executable)
@@ -151,10 +151,9 @@ public class AAFrequency
                   "WARNING: Consensus skipping null sequence - possible race condition.");
           continue;
         }
-        char[] seq = sequences[row].getSequence();
-        if (seq.length > column)
+        if (sequences[row].getLength() > column)
         {
-          char c = seq[column];
+          char c = sequences[row].getCharAt(column);
           residueCounts.add(c);
           if (Comparison.isNucleotide(c))
           {
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 6b8ea4a..b5cefe0 100755 (executable)
@@ -29,11 +29,11 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.SequenceNode;
-import jalview.util.MessageManager;
 import jalview.util.QuickSort;
 
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 
 /**
@@ -53,7 +53,7 @@ import java.util.List;
  */
 public class AlignmentSorter
 {
-  /**
+  /*
    * todo: refactor searches to follow a basic pattern: (search property, last
    * search state, current sort direction)
    */
@@ -71,19 +71,18 @@ public class AlignmentSorter
 
   static boolean sortTreeAscending = true;
 
-  /**
-   * last Annotation Label used by sortByScore
+  /*
+   * last Annotation Label used for sort by Annotation score
    */
-  private static String lastSortByScore;
-
-  private static boolean sortByScoreAscending = true;
+  private static String lastSortByAnnotation;
 
-  /**
-   * compact representation of last arguments to SortByFeatureScore
+  /*
+   * string hash of last arguments to sortByFeature
+   * (sort order toggles if this is unchanged between sorts)
    */
-  private static String lastSortByFeatureScore;
+  private static String sortByFeatureCriteria;
 
-  private static boolean sortByFeatureScoreAscending = true;
+  private static boolean sortByFeatureAscending = true;
 
   private static boolean sortLengthAscending;
 
@@ -659,9 +658,9 @@ public class AlignmentSorter
     }
 
     jalview.util.QuickSort.sort(scores, seqs);
-    if (lastSortByScore != scoreLabel)
+    if (lastSortByAnnotation != scoreLabel)
     {
-      lastSortByScore = scoreLabel;
+      lastSortByAnnotation = scoreLabel;
       setOrder(alignment, seqs);
     }
     else
@@ -683,98 +682,40 @@ public class AlignmentSorter
   public static String FEATURE_DENSITY = "density";
 
   /**
-   * sort the alignment using the features on each sequence found between start
-   * and stop with the given featureLabel (and optional group qualifier)
+   * Sort sequences by feature score or density, optionally restricted by
+   * feature types, feature groups, or alignment start/end positions.
+   * <p>
+   * If the sort is repeated for the same combination of types and groups, sort
+   * order is reversed.
    * 
-   * @param featureLabel
-   *          (may not be null)
-   * @param groupLabel
-   *          (may be null)
-   * @param start
-   *          (-1 to include non-positional features)
-   * @param stop
-   *          (-1 to only sort on non-positional features)
+   * @param featureTypes
+   *          a list of feature types to include (or null for all)
+   * @param groups
+   *          a list of feature groups to include (or null for all)
+   * @param startCol
+   *          start column position to include (base zero)
+   * @param endCol
+   *          end column position to include (base zero)
    * @param alignment
-   *          - aligned sequences containing features
+   *          the alignment to be sorted
    * @param method
-   *          - one of the string constants FEATURE_SCORE, FEATURE_LABEL,
-   *          FEATURE_DENSITY
+   *          either "average_score" or "density" ("text" not yet implemented)
    */
-  public static void sortByFeature(String featureLabel, String groupLabel,
-          int start, int stop, AlignmentI alignment, String method)
-  {
-    sortByFeature(
-            featureLabel == null ? null : Arrays.asList(new String[]
-            { featureLabel }),
-            groupLabel == null ? null : Arrays.asList(new String[]
-            { groupLabel }), start, stop, alignment, method);
-  }
-
-  private static boolean containsIgnoreCase(final String lab,
-          final List<String> labs)
-  {
-    if (labs == null)
-    {
-      return true;
-    }
-    if (lab == null)
-    {
-      return false;
-    }
-    for (String label : labs)
-    {
-      if (lab.equalsIgnoreCase(label))
-      {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public static void sortByFeature(List<String> featureLabels,
-          List<String> groupLabels, int start, int stop,
+  public static void sortByFeature(List<String> featureTypes,
+          List<String> groups, final int startCol, final int endCol,
           AlignmentI alignment, String method)
   {
     if (method != FEATURE_SCORE && method != FEATURE_LABEL
             && method != FEATURE_DENSITY)
     {
-      throw new Error(MessageManager
-              .getString("error.implementation_error_sortbyfeature"));
+      String msg = String
+              .format("Implementation Error - sortByFeature method must be either '%s' or '%s'",
+                      FEATURE_SCORE, FEATURE_DENSITY);
+      System.err.println(msg);
+      return;
     }
 
-    boolean ignoreScore = method != FEATURE_SCORE;
-    StringBuffer scoreLabel = new StringBuffer();
-    scoreLabel.append(start + stop + method);
-    // This doesn't quite work yet - we'd like to have a canonical ordering that
-    // can be preserved from call to call
-    if (featureLabels != null)
-    {
-      for (String label : featureLabels)
-      {
-        scoreLabel.append(label);
-      }
-    }
-    if (groupLabels != null)
-    {
-      for (String label : groupLabels)
-      {
-        scoreLabel.append(label);
-      }
-    }
-
-    /*
-     * if resorting the same feature, toggle sort order
-     */
-    if (lastSortByFeatureScore == null
-            || !scoreLabel.toString().equals(lastSortByFeatureScore))
-    {
-      sortByFeatureScoreAscending = true;
-    }
-    else
-    {
-      sortByFeatureScoreAscending = !sortByFeatureScoreAscending;
-    }
-    lastSortByFeatureScore = scoreLabel.toString();
+    flipFeatureSortIfUnchanged(method, featureTypes, groups, startCol, endCol);
 
     SequenceI[] seqs = alignment.getSequencesArray();
 
@@ -783,57 +724,44 @@ public class AlignmentSorter
     int hasScores = 0; // number of scores present on set
     double[] scores = new double[seqs.length];
     int[] seqScores = new int[seqs.length];
-    Object[] feats = new Object[seqs.length];
-    double min = 0, max = 0;
+    Object[][] feats = new Object[seqs.length][];
+    double min = 0d;
+    double max = 0d;
+
     for (int i = 0; i < seqs.length; i++)
     {
-      SequenceFeature[] sf = seqs[i].getSequenceFeatures();
-      if (sf == null)
-      {
-        sf = new SequenceFeature[0];
-      }
-      else
-      {
-        SequenceFeature[] tmp = new SequenceFeature[sf.length];
-        for (int s = 0; s < tmp.length; s++)
-        {
-          tmp[s] = sf[s];
-        }
-        sf = tmp;
-      }
-      int sstart = (start == -1) ? start : seqs[i].findPosition(start);
-      int sstop = (stop == -1) ? stop : seqs[i].findPosition(stop);
+      /*
+       * get sequence residues overlapping column region
+       * and features for residue positions and specified types
+       */
+      String[] types = featureTypes == null ? null : featureTypes
+              .toArray(new String[featureTypes.size()]);
+      List<SequenceFeature> sfs = seqs[i].findFeatures(startCol + 1,
+              endCol + 1, types);
+
       seqScores[i] = 0;
       scores[i] = 0.0;
-      int n = sf.length;
-      for (int f = 0; f < sf.length; f++)
+
+      Iterator<SequenceFeature> it = sfs.listIterator();
+      while (it.hasNext())
       {
-        // filter for selection criteria
-        SequenceFeature feature = sf[f];
+        SequenceFeature sf = it.next();
 
         /*
-         * double-check feature overlaps columns (JAL-2544)
-         * (could avoid this with a findPositions(fromCol, toCol) method)
-         * findIndex returns base 1 column values, startCol/endCol are base 0
+         * accept all features with null or empty group, otherwise
+         * check group is one of the currently visible groups
          */
-        boolean noOverlap = seqs[i].findIndex(feature.getBegin()) > stop + 1
-                || seqs[i].findIndex(feature.getEnd()) < start + 1;
-        boolean skipFeatureType = featureLabels != null && !AlignmentSorter
-                .containsIgnoreCase(feature.type, featureLabels);
-        boolean skipFeatureGroup = groupLabels != null
-                && (feature.getFeatureGroup() != null
-                        && !AlignmentSorter.containsIgnoreCase(
-                                feature.getFeatureGroup(), groupLabels));
-        if (noOverlap || skipFeatureType || skipFeatureGroup)
+        String featureGroup = sf.getFeatureGroup();
+        if (groups != null && featureGroup != null
+                && !"".equals(featureGroup)
+                && !groups.contains(featureGroup))
         {
-          // forget about this feature
-          sf[f] = null;
-          n--;
+          it.remove();
         }
         else
         {
-          // or, also take a look at the scores if necessary.
-          if (!ignoreScore && !Float.isNaN(feature.getScore()))
+          float score = sf.getScore();
+          if (FEATURE_SCORE.equals(method) && !Float.isNaN(score))
           {
             if (seqScores[i] == 0)
             {
@@ -841,34 +769,26 @@ public class AlignmentSorter
             }
             seqScores[i]++;
             hasScore[i] = true;
-            scores[i] += feature.getScore(); // take the first instance of this
-            // score.
+            scores[i] += score;
+            // take the first instance of this score // ??
           }
         }
       }
-      SequenceFeature[] fs;
-      feats[i] = fs = new SequenceFeature[n];
-      if (n > 0)
+
+      feats[i] = sfs.toArray(new SequenceFeature[sfs.size()]);
+      if (!sfs.isEmpty())
       {
-        n = 0;
-        for (int f = 0; f < sf.length; f++)
-        {
-          if (sf[f] != null)
-          {
-            ((SequenceFeature[]) feats[i])[n++] = sf[f];
-          }
-        }
         if (method == FEATURE_LABEL)
         {
-          // order the labels by alphabet
-          String[] labs = new String[fs.length];
-          for (int l = 0; l < labs.length; l++)
+          // order the labels by alphabet (not yet implemented)
+          String[] labs = new String[sfs.size()];
+          for (int l = 0; l < sfs.size(); l++)
           {
-            labs[l] = (fs[l].getDescription() != null
-                    ? fs[l].getDescription()
-                    : fs[l].getType());
+            SequenceFeature sf = sfs.get(l);
+            String description = sf.getDescription();
+            labs[l] = (description != null ? description : sf.getType());
           }
-          QuickSort.sort(labs, ((Object[]) feats[i]));
+          QuickSort.sort(labs, feats[i]);
         }
       }
       if (hasScore[i])
@@ -878,23 +798,18 @@ public class AlignmentSorter
         // update the score bounds.
         if (hasScores == 1)
         {
-          max = min = scores[i];
+          min = scores[i];
+          max = min;
         }
         else
         {
-          if (max < scores[i])
-          {
-            max = scores[i];
-          }
-          if (min > scores[i])
-          {
-            min = scores[i];
-          }
+          max = Math.max(max, scores[i]);
+          min = Math.min(min, scores[i]);
         }
       }
     }
 
-    if (method == FEATURE_SCORE)
+    if (FEATURE_SCORE.equals(method))
     {
       if (hasScores == 0)
       {
@@ -919,9 +834,9 @@ public class AlignmentSorter
           }
         }
       }
-      QuickSort.sortByDouble(scores, seqs, sortByFeatureScoreAscending);
+      QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
     }
-    else if (method == FEATURE_DENSITY)
+    else if (FEATURE_DENSITY.equals(method))
     {
       for (int i = 0; i < seqs.length; i++)
       {
@@ -931,18 +846,53 @@ public class AlignmentSorter
         // System.err.println("Sorting on Density: seq "+seqs[i].getName()+
         // " Feats: "+featureCount+" Score : "+scores[i]);
       }
-      QuickSort.sortByDouble(scores, seqs, sortByFeatureScoreAscending);
+      QuickSort.sortByDouble(scores, seqs, sortByFeatureAscending);
     }
-    else
+
+    setOrder(alignment, seqs);
+  }
+
+  /**
+   * Builds a string hash of criteria for sorting, and if unchanged from last
+   * time, reverse the sort order
+   * 
+   * @param method
+   * @param featureTypes
+   * @param groups
+   * @param startCol
+   * @param endCol
+   */
+  protected static void flipFeatureSortIfUnchanged(String method,
+          List<String> featureTypes, List<String> groups,
+          final int startCol, final int endCol)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    sb.append(startCol).append(method).append(endCol);
+    if (featureTypes != null)
     {
-      if (method == FEATURE_LABEL)
-      {
-        throw new Error(
-                MessageManager.getString("error.not_yet_implemented"));
-      }
+      Collections.sort(featureTypes);
+      sb.append(featureTypes.toString());
     }
+    if (groups != null)
+    {
+      Collections.sort(groups);
+      sb.append(groups.toString());
+    }
+    String scoreCriteria = sb.toString();
 
-    setOrder(alignment, seqs);
+    /*
+     * if resorting on the same criteria, toggle sort order
+     */
+    if (sortByFeatureCriteria == null
+            || !scoreCriteria.equals(sortByFeatureCriteria))
+    {
+      sortByFeatureAscending = true;
+    }
+    else
+    {
+      sortByFeatureAscending = !sortByFeatureAscending;
+    }
+    sortByFeatureCriteria = scoreCriteria;
   }
 
 }
index 1b8f84f..d1217bf 100644 (file)
@@ -29,20 +29,22 @@ 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;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.io.gff.SequenceOntologyFactory;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
+import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
-import jalview.util.RangeComparator;
 import jalview.util.StringUtils;
 
 import java.io.UnsupportedEncodingException;
@@ -51,7 +53,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -106,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());
+    }
   }
 
   /**
@@ -118,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())
     {
@@ -248,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();
@@ -257,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);
@@ -284,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
@@ -385,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)
@@ -455,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))
         {
@@ -526,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;
       }
@@ -558,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;
       }
@@ -654,15 +666,16 @@ public class AlignmentUtils
     int toOffset = alignTo.getStart() - 1;
     int sourceGapMappedLength = 0;
     boolean inExon = false;
-    final char[] thisSeq = alignTo.getSequence();
-    final char[] thatAligned = alignFrom.getSequence();
-    StringBuilder thisAligned = new StringBuilder(2 * thisSeq.length);
+    final int toLength = alignTo.getLength();
+    final int fromLength = alignFrom.getLength();
+    StringBuilder thisAligned = new StringBuilder(2 * toLength);
 
     /*
      * Traverse the 'model' aligned sequence
      */
-    for (char sourceChar : thatAligned)
+    for (int i = 0; i < fromLength; i++)
     {
+      char sourceChar = alignFrom.getCharAt(i);
       if (sourceChar == sourceGap)
       {
         sourceGapMappedLength += ratio;
@@ -702,9 +715,9 @@ public class AlignmentUtils
        */
       int intronLength = 0;
       while (basesWritten + toOffset < mappedCodonEnd
-              && thisSeqPos < thisSeq.length)
+              && thisSeqPos < toLength)
       {
-        final char c = thisSeq[thisSeqPos++];
+        final char c = alignTo.getCharAt(thisSeqPos++);
         if (c != myGapChar)
         {
           basesWritten++;
@@ -730,7 +743,7 @@ public class AlignmentUtils
             int gapsToAdd = calculateGapsToInsert(preserveMappedGaps,
                     preserveUnmappedGaps, sourceGapMappedLength, inExon,
                     trailingCopiedGap.length(), intronLength, startOfCodon);
-            for (int i = 0; i < gapsToAdd; i++)
+            for (int k = 0; k < gapsToAdd; k++)
             {
               thisAligned.append(myGapChar);
             }
@@ -758,9 +771,9 @@ public class AlignmentUtils
      * At end of model aligned sequence. Copy any remaining target sequence, optionally
      * including (intron) gaps.
      */
-    while (thisSeqPos < thisSeq.length)
+    while (thisSeqPos < toLength)
     {
-      final char c = thisSeq[thisSeqPos++];
+      final char c = alignTo.getCharAt(thisSeqPos++);
       if (c != myGapChar || preserveUnmappedGaps)
       {
         thisAligned.append(c);
@@ -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);
@@ -952,7 +965,7 @@ public class AlignmentUtils
       SequenceI peptide = mapping.findAlignedSequence(cdsSeq, protein);
       if (peptide != null)
       {
-        int peptideLength = peptide.getLength();
+        final int peptideLength = peptide.getLength();
         Mapping map = mapping.getMappingBetween(cdsSeq, peptide);
         if (map != null)
         {
@@ -961,9 +974,9 @@ public class AlignmentUtils
           {
             mapList = mapList.getInverse();
           }
-          int cdsLength = cdsDss.getLength();
-          int mappedFromLength = MappingUtils
-                  .getLength(mapList.getFromRanges());
+          final int cdsLength = cdsDss.getLength();
+          int mappedFromLength = MappingUtils.getLength(mapList
+                  .getFromRanges());
           int mappedToLength = MappingUtils
                   .getLength(mapList.getToRanges());
           boolean addStopCodon = (cdsLength == mappedFromLength
@@ -988,14 +1001,15 @@ public class AlignmentUtils
            * walk over the aligned peptide sequence and insert mapped 
            * codons for residues in the aligned cds sequence 
            */
-          char[] alignedPeptide = peptide.getSequence();
-          char[] nucleotides = cdsDss.getSequence();
           int copiedBases = 0;
           int cdsStart = cdsDss.getStart();
           int proteinPos = peptide.getStart() - 1;
           int cdsCol = 0;
-          for (char residue : alignedPeptide)
+
+          for (int col = 0; col < peptideLength; col++)
           {
+            char residue = peptide.getCharAt(col);
+
             if (Comparison.isGap(residue))
             {
               cdsCol += CODON_LENGTH;
@@ -1013,7 +1027,7 @@ public class AlignmentUtils
               {
                 for (int j = codon[0]; j <= codon[1]; j++)
                 {
-                  char mappedBase = nucleotides[j - cdsStart];
+                  char mappedBase = cdsDss.getCharAt(j - cdsStart);
                   alignedCds[cdsCol++] = mappedBase;
                   copiedBases++;
                 }
@@ -1025,7 +1039,7 @@ public class AlignmentUtils
            * append stop codon if not mapped from protein,
            * closing it up to the end of the mapped sequence
            */
-          if (copiedBases == nucleotides.length - CODON_LENGTH)
+          if (copiedBases == cdsLength - CODON_LENGTH)
           {
             for (int i = alignedCds.length - 1; i >= 0; i--)
             {
@@ -1035,10 +1049,9 @@ public class AlignmentUtils
                 break;
               }
             }
-            for (int i = nucleotides.length
-                    - CODON_LENGTH; i < nucleotides.length; i++)
+            for (int i = cdsLength - CODON_LENGTH; i < cdsLength; i++)
             {
-              alignedCds[cdsCol++] = nucleotides[i];
+              alignedCds[cdsCol++] = cdsDss.getCharAt(i);
             }
           }
           cdsSeq.setSequence(new String(alignedCds));
@@ -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())
@@ -1208,21 +1221,26 @@ public class AlignmentUtils
           List<SequenceI> unmappedProtein)
   {
     /*
-     * Prefill aligned sequences with gaps before inserting aligned protein
-     * residues.
+     * prefill peptide sequences with gaps 
      */
     int alignedWidth = alignedCodons.size();
     char[] gaps = new char[alignedWidth];
     Arrays.fill(gaps, protein.getGapCharacter());
-    String allGaps = String.valueOf(gaps);
+    Map<SequenceI, char[]> peptides = new HashMap<>();
     for (SequenceI seq : protein.getSequences())
     {
       if (!unmappedProtein.contains(seq))
       {
-        seq.setSequence(allGaps);
+        peptides.put(seq, Arrays.copyOf(gaps, gaps.length));
       }
     }
 
+    /*
+     * Traverse the codons left to right (as defined by CodonComparator)
+     * and insert peptides in each column where the sequence is mapped.
+     * This gives a peptide 'alignment' where residues are aligned if their
+     * corresponding codons occupy the same columns in the cdna alignment.
+     */
     int column = 0;
     for (AlignedCodon codon : alignedCodons.keySet())
     {
@@ -1230,12 +1248,20 @@ public class AlignmentUtils
               .get(codon);
       for (Entry<SequenceI, AlignedCodon> entry : columnResidues.entrySet())
       {
-        // place translated codon at its column position in sequence
-        entry.getKey().getSequence()[column] = entry.getValue().product
-                .charAt(0);
+        char residue = entry.getValue().product.charAt(0);
+        peptides.get(entry.getKey())[column] = residue;
       }
       column++;
     }
+
+    /*
+     * and finally set the constructed sequences
+     */
+    for (Entry<SequenceI, char[]> entry : peptides.entrySet())
+    {
+      entry.getKey().setSequence(new String(entry.getValue()));
+    }
+
     return 0;
   }
 
@@ -1295,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);
@@ -1432,7 +1458,7 @@ public class AlignmentUtils
       {
         continue;
       }
-      final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
+      final List<AlignmentAnnotation> result = new ArrayList<>();
       for (AlignmentAnnotation dsann : datasetAnnotations)
       {
         /*
@@ -1614,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());
       }
     }
 
@@ -1717,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());
@@ -1741,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);
@@ -1751,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
@@ -1769,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);
           }
 
@@ -1801,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.
@@ -1820,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,
@@ -1845,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;
+      }
     }
 
     /*
@@ -1870,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())
@@ -1976,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())
@@ -2010,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)
@@ -2061,14 +2141,14 @@ public class AlignmentUtils
    * 
    * @param fromSeq
    * @param toSeq
+   * @param mapping
+   *          the mapping from 'fromSeq' to 'toSeq'
    * @param select
    *          if not null, only features of this type are copied (including
    *          subtypes in the Sequence Ontology)
-   * @param mapping
-   *          the mapping from 'fromSeq' to 'toSeq'
    * @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;
@@ -2077,82 +2157,84 @@ public class AlignmentUtils
       copyTo = copyTo.getDatasetSequence();
     }
 
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    /*
+     * get features, optionally restricted by an ontology term
+     */
+    List<SequenceFeature> sfs = select == null ? fromSeq.getFeatures()
+            .getPositionalFeatures() : fromSeq.getFeatures()
+            .getFeaturesByOntology(select);
+
     int count = 0;
-    SequenceFeature[] sfs = fromSeq.getSequenceFeatures();
-    if (sfs != null)
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
+      String type = sf.getType();
+      boolean omit = false;
+      for (String toOmit : omitting)
       {
-        String type = sf.getType();
-        if (select != null && !so.isA(type, select))
+        if (type.equals(toOmit))
         {
-          continue;
-        }
-        boolean omit = false;
-        for (String toOmit : omitting)
-        {
-          if (type.equals(toOmit))
-          {
-            omit = true;
-          }
-        }
-        if (omit)
-        {
-          continue;
+          omit = true;
         }
+      }
+      if (omit)
+      {
+        continue;
+      }
 
-        /*
-         * locate the mapped range - null if either start or end is
-         * not mapped (no partial overlaps are calculated)
-         */
-        int start = sf.getBegin();
-        int end = sf.getEnd();
-        int[] mappedTo = mapping.locateInTo(start, end);
-        /*
-         * if whole exon range doesn't map, try interpreting it
-         * as 5' or 3' exon overlapping the CDS range
-         */
-        if (mappedTo == null)
-        {
-          mappedTo = mapping.locateInTo(end, end);
-          if (mappedTo != null)
-          {
-            /*
-             * end of exon is in CDS range - 5' overlap
-             * to a range from the start of the peptide
-             */
-            mappedTo[0] = 1;
-          }
-        }
-        if (mappedTo == null)
+      /*
+       * locate the mapped range - null if either start or end is
+       * not mapped (no partial overlaps are calculated)
+       */
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+      int[] mappedTo = mapping.locateInTo(start, end);
+      /*
+       * if whole exon range doesn't map, try interpreting it
+       * as 5' or 3' exon overlapping the CDS range
+       */
+      if (mappedTo == null)
+      {
+        mappedTo = mapping.locateInTo(end, end);
+        if (mappedTo != null)
         {
-          mappedTo = mapping.locateInTo(start, start);
-          if (mappedTo != null)
-          {
-            /*
-             * start of exon is in CDS range - 3' overlap
-             * to a range up to the end of the peptide
-             */
-            mappedTo[1] = toSeq.getLength();
-          }
+          /*
+           * end of exon is in CDS range - 5' overlap
+           * to a range from the start of the peptide
+           */
+          mappedTo[0] = 1;
         }
+      }
+      if (mappedTo == null)
+      {
+        mappedTo = mapping.locateInTo(start, start);
         if (mappedTo != null)
         {
-          SequenceFeature copy = new SequenceFeature(sf);
-          copy.setBegin(Math.min(mappedTo[0], mappedTo[1]));
-          copy.setEnd(Math.max(mappedTo[0], mappedTo[1]));
-          copyTo.addSequenceFeature(copy);
-          count++;
+          /*
+           * start of exon is in CDS range - 3' overlap
+           * to a range up to the end of the peptide
+           */
+          mappedTo[1] = toSeq.getLength();
         }
       }
+      if (mappedTo != null)
+      {
+        int newBegin = Math.min(mappedTo[0], mappedTo[1]);
+        int newEnd = Math.max(mappedTo[0], mappedTo[1]);
+        SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+                sf.getFeatureGroup(), sf.getScore());
+        copyTo.addSequenceFeature(copy);
+        count++;
+      }
     }
     return count;
   }
 
   /**
    * 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
@@ -2164,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();
@@ -2178,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)
@@ -2187,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 });
@@ -2199,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.
@@ -2207,62 +2303,46 @@ 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[]>();
-    SequenceFeature[] sfs = dnaSeq.getSequenceFeatures();
-    if (sfs == null)
+    List<int[]> result = new ArrayList<>();
+
+    List<SequenceFeature> sfs = dnaSeq.getFeatures().getFeaturesByOntology(
+            SequenceOntologyI.CDS);
+    if (sfs.isEmpty())
     {
       return result;
     }
-
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
-    int startPhase = 0;
+    SequenceFeatures.sortFeatures(sfs, true);
 
     for (SequenceFeature sf : sfs)
     {
+      int phase = 0;
+      try
+      {
+        phase = Integer.parseInt(sf.getPhase());
+      } catch (NumberFormatException e)
+      {
+        // ignore
+      }
       /*
-       * process a CDS feature (or a sub-type of CDS)
+       * phase > 0 on first codon means 5' incomplete - skip to the start
+       * of the next codon; example ENST00000496384
        */
-      if (so.isA(sf.getType(), SequenceOntologyI.CDS))
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
+      if (result.isEmpty() && phase > 0)
       {
-        int phase = 0;
-        try
+        begin += phase;
+        if (begin > end)
         {
-          phase = Integer.parseInt(sf.getPhase());
-        } catch (NumberFormatException e)
-        {
-          // ignore
+          // shouldn't happen!
+          System.err
+                  .println("Error: start phase extends beyond start CDS in "
+                          + dnaSeq.getName());
         }
-        /*
-         * phase > 0 on first codon means 5' incomplete - skip to the start
-         * of the next codon; example ENST00000496384
-         */
-        int begin = sf.getBegin();
-        int end = sf.getEnd();
-        if (result.isEmpty())
-        {
-          begin += phase;
-          if (begin > end)
-          {
-            // shouldn't happen!
-            System.err.println(
-                    "Error: start phase extends beyond start CDS in "
-                            + dnaSeq.getName());
-          }
-        }
-        result.add(new int[] { begin, end });
       }
-    }
-
-    /*
-     * 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;
+      result.add(new int[] { begin, end });
     }
 
     /*
@@ -2272,7 +2352,7 @@ public class AlignmentUtils
      * ranges are assembled in order. Other cases should not use this method,
      * but instead construct an explicit mapping for CDS (e.g. EMBL parsing).
      */
-    Collections.sort(result, new RangeComparator(true));
+    Collections.sort(result, IntRangeComparator.ASCENDING);
     return result;
   }
 
@@ -2325,24 +2405,6 @@ public class AlignmentUtils
       count += computePeptideVariants(peptide, peptidePos, codonVariants);
     }
 
-    /*
-     * sort to get sequence features in start position order
-     * - would be better to store in Sequence as a TreeSet or NCList?
-     */
-    if (peptide.getSequenceFeatures() != null)
-    {
-      Arrays.sort(peptide.getSequenceFeatures(),
-              new Comparator<SequenceFeature>()
-              {
-                @Override
-                public int compare(SequenceFeature o1, SequenceFeature o2)
-                {
-                  int c = Integer.compare(o1.getBegin(), o2.getBegin());
-                  return c == 0 ? Integer.compare(o1.getEnd(), o2.getEnd())
-                          : c;
-                }
-              });
-    }
     return count;
   }
 
@@ -2376,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++;
+              }
             }
           }
         }
@@ -2398,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++;
+              }
             }
           }
         }
@@ -2420,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++;
+              }
             }
           }
         }
@@ -2439,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
@@ -2460,63 +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;
-      // set score to 0f so 'graduated colour' option is offered! JAL-2060
-      SequenceFeature sf = new SequenceFeature(
-              SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
-              peptidePos, 0f, 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
+      {
+        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)
       {
-        sf.setAttributes(attributes.toString());
+        // 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
@@ -2530,11 +2631,11 @@ 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>[]>();
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<>();
 
-    SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures();
-    if (dnaFeatures == null)
+    List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
+    if (dnaFeatures.isEmpty())
     {
       return variants;
     }
@@ -2554,84 +2655,89 @@ public class AlignmentUtils
         // not handling multi-locus variant features
         continue;
       }
-      if (so.isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT))
+
+      /*
+       * ignore variant if not a SNP
+       */
+      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
+      if (alls == null)
       {
-        int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
-        if (mapsTo == null)
-        {
-          // feature doesn't lie within coding region
-          continue;
-        }
-        int peptidePosition = mapsTo[0];
-        List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
-        if (codonVariants == null)
-        {
-          codonVariants = new ArrayList[CODON_LENGTH];
-          codonVariants[0] = new ArrayList<DnaVariant>();
-          codonVariants[1] = new ArrayList<DnaVariant>();
-          codonVariants[2] = new ArrayList<DnaVariant>();
-          variants.put(peptidePosition, codonVariants);
-        }
+        continue; // non-SNP VCF variant perhaps - can't process this
+      }
 
-        /*
-         * 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)
+      String[] alleles = alls.toUpperCase().split(",");
+      boolean isSnp = true;
+      for (String allele : alleles)
+      {
+        if (allele.trim().length() > 1)
         {
-          alleles[i++] = allele.trim(); // lose any space characters "A, G"
+          isSnp = false;
         }
+      }
+      if (!isSnp)
+      {
+        continue;
+      }
 
-        /*
-         * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
-         */
-        int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
-                : MappingUtils.flattenRanges(dnaToProtein
-                        .locateInFrom(peptidePosition, peptidePosition));
-        lastPeptidePostion = peptidePosition;
-        lastCodon = codon;
+      int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
+      if (mapsTo == null)
+      {
+        // feature doesn't lie within coding region
+        continue;
+      }
+      int peptidePosition = mapsTo[0];
+      List<DnaVariant>[] codonVariants = variants.get(peptidePosition);
+      if (codonVariants == null)
+      {
+        codonVariants = new ArrayList[CODON_LENGTH];
+        codonVariants[0] = new ArrayList<>();
+        codonVariants[1] = new ArrayList<>();
+        codonVariants[2] = new ArrayList<>();
+        variants.put(peptidePosition, codonVariants);
+      }
 
-        /*
-         * save nucleotide (and any variant) for each codon position
-         */
-        for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
+      /*
+       * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
+       */
+      int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
+              : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
+                      peptidePosition, peptidePosition));
+      lastPeptidePostion = peptidePosition;
+      lastCodon = codon;
+
+      /*
+       * save nucleotide (and any variant) for each codon position
+       */
+      for (int codonPos = 0; codonPos < CODON_LENGTH; codonPos++)
+      {
+        String nucleotide = String.valueOf(
+                dnaSeq.getCharAt(codon[codonPos] - dnaStart)).toUpperCase();
+        List<DnaVariant> codonVariant = codonVariants[codonPos];
+        if (codon[codonPos] == dnaCol)
         {
-          String nucleotide = String
-                  .valueOf(dnaSeq.getCharAt(codon[codonPos] - dnaStart))
-                  .toUpperCase();
-          List<DnaVariant> codonVariant = codonVariants[codonPos];
-          if (codon[codonPos] == dnaCol)
+          if (!codonVariant.isEmpty()
+                  && codonVariant.get(0).variant == null)
           {
-            if (!codonVariant.isEmpty()
-                    && codonVariant.get(0).variant == null)
-            {
-              /*
-               * already recorded base value, add this variant
-               */
-              codonVariant.get(0).variant = sf;
-            }
-            else
-            {
-              /*
-               * add variant with base value
-               */
-              codonVariant.add(new DnaVariant(nucleotide, sf));
-            }
+            /*
+             * already recorded base value, add this variant
+             */
+            codonVariant.get(0).variant = sf;
           }
-          else if (codonVariant.isEmpty())
+          else
           {
             /*
-             * record (possibly non-varying) base value
+             * add variant with base value
              */
-            codonVariant.add(new DnaVariant(nucleotide));
+            codonVariant.add(new DnaVariant(nucleotide, sf));
           }
         }
+        else if (codonVariant.isEmpty())
+        {
+          /*
+           * record (possibly non-varying) base value
+           */
+          codonVariant.add(new DnaVariant(nucleotide));
+        }
       }
     }
     return variants;
@@ -2710,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();
@@ -2785,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();
@@ -2848,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
@@ -2910,9 +3016,7 @@ public class AlignmentUtils
               seqMap.getMap().getInverse());
     }
 
-    char[] fromChars = fromSeq.getSequence();
     int toStart = seq.getStart();
-    char[] toChars = seq.getSequence();
 
     /*
      * traverse [start, end, start, end...] ranges in fromSeq
@@ -2943,10 +3047,10 @@ public class AlignmentUtils
          * of the next character of the mapped-to sequence; stop when all
          * the characters of the range have been counted
          */
-        while (mappedCharPos <= range[1] && fromCol <= fromChars.length
+        while (mappedCharPos <= range[1] && fromCol <= fromSeq.getLength()
                 && fromCol >= 0)
         {
-          if (!Comparison.isGap(fromChars[fromCol - 1]))
+          if (!Comparison.isGap(fromSeq.getCharAt(fromCol - 1)))
           {
             /*
              * mapped from sequence has a character in this column
@@ -2955,10 +3059,10 @@ 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, toChars[mappedCharPos - toStart]);
+            seqsMap.put(seq, seq.getCharAt(mappedCharPos - toStart));
             mappedCharPos++;
           }
           fromCol += (forward ? 1 : -1);
index ba4f705..0af5d20 100755 (executable)
@@ -273,7 +273,7 @@ public class Conservation
        * or not conserved (-1)
        * Using TreeMap means properties are displayed in alphabetical order
        */
-      SortedMap<String, Integer> resultHash = new TreeMap<String, Integer>();
+      SortedMap<String, Integer> resultHash = new TreeMap<>();
       SymbolCounts symbolCounts = values.getSymbolCounts();
       char[] symbols = symbolCounts.symbols;
       int[] counts = symbolCounts.values;
@@ -567,7 +567,7 @@ public class Conservation
    */
   private void percentIdentity(ScoreMatrix sm)
   {
-    seqNums = new Vector<int[]>();
+    seqNums = new Vector<>();
     int i = 0, iSize = sequences.length;
     // Do we need to calculate this again?
     for (i = 0; i < iSize; i++)
@@ -622,7 +622,7 @@ public class Conservation
   protected void findQuality(int startCol, int endCol,
           ScoreMatrix scoreMatrix)
   {
-    quality = new Vector<Double>();
+    quality = new Vector<>();
 
     double max = -Double.MAX_VALUE;
     float[][] scores = scoreMatrix.getMatrix();
@@ -721,8 +721,8 @@ public class Conservation
 
   /**
    * Complete the given consensus and quuality annotation rows. Note: currently
-   * this method will enlarge the given annotation row if it is too small,
-   * otherwise will leave its length unchanged.
+   * this method will reallocate the given annotation row if it is different to
+   * the calculated width, otherwise will leave its length unchanged.
    * 
    * @param conservation
    *          conservation annotation row
@@ -736,30 +736,25 @@ public class Conservation
   public void completeAnnotations(AlignmentAnnotation conservation,
           AlignmentAnnotation quality2, int istart, int alWidth)
   {
-    char[] sequence = getConsSequence().getSequence();
-    float minR;
-    float minG;
-    float minB;
-    float maxR;
-    float maxG;
-    float maxB;
-    minR = 0.3f;
-    minG = 0.0f;
-    minB = 0f;
-    maxR = 1.0f - minR;
-    maxG = 0.9f - minG;
-    maxB = 0f - minB; // scalable range for colouring both Conservation and
-    // Quality
+    SequenceI cons = getConsSequence();
+
+    /*
+     * colour scale for Conservation and Quality;
+     */
+    float minR = 0.3f;
+    float minG = 0.0f;
+    float minB = 0f;
+    float maxR = 1.0f - minR;
+    float maxG = 0.9f - minG;
+    float maxB = 0f - minB;
 
     float min = 0f;
     float max = 11f;
     float qmin = 0f;
     float qmax = 0f;
 
-    char c;
-
     if (conservation != null && conservation.annotations != null
-            && conservation.annotations.length < alWidth)
+            && conservation.annotations.length != alWidth)
     {
       conservation.annotations = new Annotation[alWidth];
     }
@@ -768,7 +763,7 @@ public class Conservation
     {
       quality2.graphMax = (float) qualityMaximum;
       if (quality2.annotations != null
-              && quality2.annotations.length < alWidth)
+              && quality2.annotations.length != alWidth)
       {
         quality2.annotations = new Annotation[alWidth];
       }
@@ -780,7 +775,7 @@ public class Conservation
     {
       float value = 0;
 
-      c = sequence[i];
+      char c = cons.getCharAt(i);
 
       if (Character.isDigit(c))
       {
@@ -866,8 +861,8 @@ public class Conservation
    */
   String getTooltip(int column)
   {
-    char[] sequence = getConsSequence().getSequence();
-    char val = column < sequence.length ? sequence[column] : '-';
+    SequenceI cons = getConsSequence();
+    char val = column < cons.getLength() ? cons.getCharAt(column) : '-';
     boolean hasConservation = val != '-' && val != '0';
     int consp = column - start;
     String tip = (hasConservation && consp > -1 && consp < consSymbs.length)
index 1a56393..e6bae9b 100644 (file)
@@ -619,28 +619,25 @@ public class CrossRef
                  * duplication (e.g. same variation from two 
                  * transcripts)
                  */
-                SequenceFeature[] sfs = ms.getSequenceFeatures();
-                if (sfs != null)
+                List<SequenceFeature> sfs = ms.getFeatures()
+                        .getAllFeatures();
+                for (SequenceFeature feat : sfs)
                 {
-                  for (SequenceFeature feat : sfs)
+                  /*
+                   * make a flyweight feature object which ignores Parent
+                   * attribute in equality test; this avoids creating many
+                   * otherwise duplicate exon features on genomic sequence
+                   */
+                  SequenceFeature newFeature = new SequenceFeature(feat)
                   {
-                    /*
-                     * make a flyweight feature object which ignores Parent
-                     * attribute in equality test; this avoids creating many
-                     * otherwise duplicate exon features on genomic sequence
-                     */
-                    SequenceFeature newFeature = new SequenceFeature(feat)
+                    @Override
+                    public boolean equals(Object o)
                     {
-                      @Override
-                      public boolean equals(Object o)
-                      {
-                        return super.equals(o, true);
-                      }
-                    };
-                    matched.addSequenceFeature(newFeature);
-                  }
+                      return super.equals(o, true);
+                    }
+                  };
+                  matched.addSequenceFeature(newFeature);
                 }
-
               }
               cf.addMap(retrievedSequence, map.getTo(), map.getMap());
             } catch (Exception e)
@@ -785,15 +782,15 @@ public class CrossRef
     {
       return false;
     }
-    char[] c1 = seq1.getSequence();
-    char[] c2 = seq2.getSequence();
-    if (c1.length != c2.length)
+
+    if (seq1.getLength() != seq2.getLength())
     {
       return false;
     }
-    for (int i = 0; i < c1.length; i++)
+    int length = seq1.getLength();
+    for (int i = 0; i < length; i++)
     {
-      int diff = c1[i] - c2[i];
+      int diff = seq1.getCharAt(i) - seq2.getCharAt(i);
       /*
        * same char or differ in case only ('a'-'A' == 32)
        */
index 0128624..2ad8487 100644 (file)
@@ -44,8 +44,8 @@ import jalview.util.ShiftList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 
 public class Dna
 {
@@ -57,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.
@@ -92,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);
@@ -101,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;
   }
 
   /**
@@ -120,8 +163,7 @@ public class Dna
    * @param ac2
    * @return
    */
-  public static final int compareCodonPos(AlignedCodon ac1,
-          AlignedCodon ac2)
+  public static final int compareCodonPos(AlignedCodon ac1, AlignedCodon ac2)
   {
     return comparator.compare(ac1, ac2);
     // return jalview_2_8_2compare(ac1, ac2);
@@ -163,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),
@@ -215,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++)
         {
@@ -393,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);
@@ -546,7 +575,7 @@ public class Dna
             skip.add(skipint);
             skipint = null;
           }
-          if (aa.equals("STOP"))
+          if (aa.equals(ResidueProperties.STOP))
           {
             aa = STOP_ASTERIX;
           }
@@ -689,7 +718,7 @@ public class Dna
          */
         MapList map = new MapList(scontigs, new int[] { 1, resSize }, 3, 1);
 
-        transferCodedFeatures(selection, newseq, map, null, null);
+        transferCodedFeatures(selection, newseq, map);
 
         /*
          * Construct a dataset sequence for our new peptide.
@@ -758,25 +787,15 @@ public class Dna
 
   /**
    * Given a peptide newly translated from a dna sequence, copy over and set any
-   * features on the peptide from the DNA. If featureTypes is null, all features
-   * on the dna sequence are searched (rather than just the displayed ones), and
-   * similarly for featureGroups.
+   * features on the peptide from the DNA.
    * 
    * @param dna
    * @param pep
    * @param map
-   * @param featureTypes
-   *          hash whose keys are the displayed feature type strings
-   * @param featureGroups
-   *          hash where keys are feature groups and values are Boolean objects
-   *          indicating if they are displayed.
    */
   private static void transferCodedFeatures(SequenceI dna, SequenceI pep,
-          MapList map, Map<String, Object> featureTypes,
-          Map<String, Boolean> featureGroups)
+          MapList map)
   {
-    SequenceFeature[] sfs = dna.getSequenceFeatures();
-    Boolean fgstate;
     DBRefEntry[] dnarefs = DBRefUtils.selectRefs(dna.getDBRefs(),
             DBRefSource.DNACODINGDBS);
     if (dnarefs != null)
@@ -790,24 +809,15 @@ public class Dna
         }
       }
     }
-    if (sfs != null)
+    for (SequenceFeature sf : dna.getFeatures().getAllFeatures())
     {
-      for (SequenceFeature sf : sfs)
-      {
-        fgstate = (featureGroups == null) ? null
-                : featureGroups.get(sf.featureGroup);
-        if ((featureTypes == null || featureTypes.containsKey(sf.getType()))
-                && (fgstate == null || fgstate.booleanValue()))
+        if (FeatureProperties.isCodingFeature(null, sf.getType()))
         {
-          if (FeatureProperties.isCodingFeature(null, sf.getType()))
+          // if (map.intersectsFrom(sf[f].begin, sf[f].end))
           {
-            // if (map.intersectsFrom(sf[f].begin, sf[f].end))
-            {
 
-            }
           }
         }
-      }
     }
   }
 
@@ -821,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(),
@@ -872,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 94544e7..e5cda93 100644 (file)
@@ -31,10 +31,11 @@ import jalview.datamodel.SequenceFeature;
 import jalview.util.MessageManager;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 import java.util.Stack;
-import java.util.Vector;
 
 public class Rna
 {
@@ -134,11 +135,11 @@ public class Rna
    * @return
    * @throw {@link WUSSParseException}
    */
-  public static Vector<SimpleBP> getSimpleBPs(CharSequence line)
+  protected static List<SimpleBP> getSimpleBPs(CharSequence line)
           throws WUSSParseException
   {
     Hashtable<Character, Stack<Integer>> stacks = new Hashtable<Character, Stack<Integer>>();
-    Vector<SimpleBP> pairs = new Vector<SimpleBP>();
+    List<SimpleBP> pairs = new ArrayList<SimpleBP>();
     int i = 0;
     while (i < line.length())
     {
@@ -197,25 +198,9 @@ public class Rna
     return pairs;
   }
 
-  public static SequenceFeature[] getBasePairs(List<SimpleBP> bps)
-          throws WUSSParseException
-  {
-    SequenceFeature[] outPairs = new SequenceFeature[bps.size()];
-    for (int p = 0; p < bps.size(); p++)
-    {
-      SimpleBP bp = bps.get(p);
-      outPairs[p] = new SequenceFeature("RNA helix", "", "", bp.getBP5(),
-              bp.getBP3(), "");
-    }
-    return outPairs;
-  }
+  
 
-  public static List<SimpleBP> getModeleBP(CharSequence line)
-          throws WUSSParseException
-  {
-    Vector<SimpleBP> bps = getSimpleBPs(line);
-    return new ArrayList<SimpleBP>(bps);
-  }
+  
 
   /**
    * Function to get the end position corresponding to a given start position
@@ -232,88 +217,6 @@ public class Rna
    */
 
   /**
-   * Figures out which helix each position belongs to and stores the helix
-   * number in the 'featureGroup' member of a SequenceFeature Based off of RALEE
-   * code ralee-helix-map.
-   * 
-   * @param pairs
-   *          Array of SequenceFeature (output from Rna.GetBasePairs)
-   */
-  public static void HelixMap(SequenceFeature[] pairs)
-  {
-
-    int helix = 0; // Number of helices/current helix
-    int lastopen = 0; // Position of last open bracket reviewed
-    int lastclose = 9999999; // Position of last close bracket reviewed
-    int i = pairs.length; // Number of pairs
-
-    int open; // Position of an open bracket under review
-    int close; // Position of a close bracket under review
-    int j; // Counter
-
-    Hashtable<Integer, Integer> helices = new Hashtable<Integer, Integer>();
-    // Keep track of helix number for each position
-
-    // Go through each base pair and assign positions a helix
-    for (i = 0; i < pairs.length; i++)
-    {
-
-      open = pairs[i].getBegin();
-      close = pairs[i].getEnd();
-
-      // System.out.println("open " + open + " close " + close);
-      // System.out.println("lastclose " + lastclose + " lastopen " + lastopen);
-
-      // we're moving from right to left based on closing pair
-      /*
-       * catch things like <<..>>..<<..>> |
-       */
-      if (open > lastclose)
-      {
-        helix++;
-      }
-
-      /*
-       * catch things like <<..<<..>>..<<..>>>> |
-       */
-      j = pairs.length - 1;
-      while (j >= 0)
-      {
-        int popen = pairs[j].getBegin();
-
-        // System.out.println("j " + j + " popen " + popen + " lastopen "
-        // +lastopen + " open " + open);
-        if ((popen < lastopen) && (popen > open))
-        {
-          if (helices.containsValue(popen)
-                  && ((helices.get(popen)) == helix))
-          {
-            continue;
-          }
-          else
-          {
-            helix++;
-            break;
-          }
-        }
-
-        j -= 1;
-      }
-
-      // Put positions and helix information into the hashtable
-      helices.put(open, helix);
-      helices.put(close, helix);
-
-      // Record helix as featuregroup
-      pairs[i].setFeatureGroup(Integer.toString(helix));
-
-      lastopen = open;
-      lastclose = close;
-
-    }
-  }
-
-  /**
    * Answers true if the character is a recognised symbol for RNA secondary
    * structure. Currently accepts a-z, A-Z, ()[]{}<>.
    * 
@@ -502,4 +405,75 @@ public class Rna
       return c;
     }
   }
+
+  public static SequenceFeature[] getHelixMap(CharSequence rnaAnnotation)
+          throws WUSSParseException
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    int helix = 0; // Number of helices/current helix
+    int lastopen = 0; // Position of last open bracket reviewed
+    int lastclose = 9999999; // Position of last close bracket reviewed
+
+    Map<Integer, Integer> helices = new HashMap<Integer, Integer>();
+    // Keep track of helix number for each position
+
+    // Go through each base pair and assign positions a helix
+    List<SimpleBP> bps = getSimpleBPs(rnaAnnotation);
+    for (SimpleBP basePair : bps)
+    {
+      final int open = basePair.getBP5();
+      final int close = basePair.getBP3();
+
+      // System.out.println("open " + open + " close " + close);
+      // System.out.println("lastclose " + lastclose + " lastopen " + lastopen);
+
+      // we're moving from right to left based on closing pair
+      /*
+       * catch things like <<..>>..<<..>> |
+       */
+      if (open > lastclose)
+      {
+        helix++;
+      }
+
+      /*
+       * catch things like <<..<<..>>..<<..>>>> |
+       */
+      int j = bps.size();
+      while (--j >= 0)
+      {
+        int popen = bps.get(j).getBP5();
+
+        // System.out.println("j " + j + " popen " + popen + " lastopen "
+        // +lastopen + " open " + open);
+        if ((popen < lastopen) && (popen > open))
+        {
+          if (helices.containsValue(popen)
+                  && ((helices.get(popen)) == helix))
+          {
+            continue;
+          }
+          else
+          {
+            helix++;
+            break;
+          }
+        }
+      }
+
+      // Put positions and helix information into the hashtable
+      helices.put(open, helix);
+      helices.put(close, helix);
+
+      // Record helix as featuregroup
+      result.add(new SequenceFeature("RNA helix", "", open, close,
+              String.valueOf(helix)));
+
+      lastopen = open;
+      lastclose = close;
+    }
+
+    return result.toArray(new SequenceFeature[result.size()]);
+  }
 }
index 2b21e5e..fabd0c6 100755 (executable)
@@ -27,6 +27,7 @@ import jalview.datamodel.SequenceI;
 
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Vector;
 
 public class SeqsetUtils
@@ -50,15 +51,11 @@ public class SeqsetUtils
     {
       sqinfo.put("Description", seq.getDescription());
     }
-    Vector sfeat = new Vector();
-    jalview.datamodel.SequenceFeature[] sfarray = seq.getSequenceFeatures();
-    if (sfarray != null && sfarray.length > 0)
-    {
-      for (int i = 0; i < sfarray.length; i++)
-      {
-        sfeat.addElement(sfarray[i]);
-      }
-    }
+
+    Vector<SequenceFeature> sfeat = new Vector<SequenceFeature>();
+    List<SequenceFeature> sfs = seq.getFeatures().getAllFeatures();
+    sfeat.addAll(sfs);
+
     if (seq.getDatasetSequence() == null)
     {
       sqinfo.put("SeqFeatures", sfeat);
@@ -95,7 +92,8 @@ public class SeqsetUtils
     String oldname = (String) sqinfo.get("Name");
     Integer start = (Integer) sqinfo.get("Start");
     Integer end = (Integer) sqinfo.get("End");
-    Vector sfeatures = (Vector) sqinfo.get("SeqFeatures");
+    Vector<SequenceFeature> sfeatures = (Vector<SequenceFeature>) sqinfo
+            .get("SeqFeatures");
     Vector<PDBEntry> pdbid = (Vector<PDBEntry>) sqinfo.get("PdbId");
     String description = (String) sqinfo.get("Description");
     Sequence seqds = (Sequence) sqinfo.get("datasetSequence");
@@ -118,14 +116,9 @@ public class SeqsetUtils
       sq.setEnd(end.intValue());
     }
 
-    if ((sfeatures != null) && (sfeatures.size() > 0))
+    if (sfeatures != null && !sfeatures.isEmpty())
     {
-      SequenceFeature[] sfarray = new SequenceFeature[sfeatures.size()];
-      for (int is = 0, isize = sfeatures.size(); is < isize; is++)
-      {
-        sfarray[is] = (SequenceFeature) sfeatures.elementAt(is);
-      }
-      sq.setSequenceFeatures(sfarray);
+      sq.setSequenceFeatures(sfeatures);
     }
     if (description != null)
     {
index c20d4f1..e506be2 100644 (file)
@@ -177,10 +177,12 @@ public class FeatureDistanceModel extends DistanceScoreModel
   /**
    * Builds and returns a map containing a (possibly empty) list (one per
    * SeqCigar) of visible feature types at the given column position. The map
-   * has no entry for sequences which are gapped at the column position.
+   * does not include entries for features which straddle a gapped column
+   * positions.
    * 
    * @param seqs
    * @param columnPosition
+   *          (0..)
    * @return
    */
   protected Map<SeqCigar, Set<String>> findFeatureTypesAtColumn(
@@ -192,9 +194,12 @@ public class FeatureDistanceModel extends DistanceScoreModel
       int spos = seq.findPosition(columnPosition);
       if (spos != -1)
       {
+        /*
+         * position is not a gap
+         */
         Set<String> types = new HashSet<String>();
-        List<SequenceFeature> sfs = fr.findFeaturesAtRes(seq.getRefSeq(),
-                spos);
+        List<SequenceFeature> sfs = fr.findFeaturesAtResidue(
+                seq.getRefSeq(), spos);
         for (SequenceFeature sf : sfs)
         {
           types.add(sf.getType());
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 01eb7fa..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
@@ -146,7 +143,9 @@ public interface FeatureColourI
   boolean hasThreshold();
 
   /**
-   * Returns the computed colour for the given sequence feature
+   * Returns the computed colour for the given sequence feature. Answers null if
+   * the score of this feature instance is outside the range to render (if any),
+   * i.e. lies below or above a configured threshold.
    * 
    * @param feature
    * @return
@@ -154,18 +153,10 @@ public interface FeatureColourI
   Color getColor(SequenceFeature feature);
 
   /**
-   * Answers true if the feature has a simple colour, or is coloured by label,
-   * or has a graduated colour and the score of this feature instance is within
-   * the range to render (if any), i.e. does not lie below or above any
-   * threshold set.
-   * 
-   * @param feature
-   * @return
-   */
-  boolean isColored(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
@@ -178,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 7123b8c..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;
@@ -60,6 +61,7 @@ public interface FeatureRenderer
    * 
    * @param sequence
    * @param column
+   *          aligned column position (1..)
    * @param g
    * @return
    */
@@ -131,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
@@ -139,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
@@ -147,14 +149,27 @@ public interface FeatureRenderer
   void setGroupVisibility(String group, boolean visible);
 
   /**
-   * Returns features at the specified position on the given sequence.
+   * 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
+   *          aligned column position (1..)
+   * @return
+   */
+  List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column);
+
+  /**
+   * Returns features at the specified residue position on the given sequence.
    * Non-positional features are not included.
    * 
    * @param sequence
-   * @param res
+   * @param resNo
+   *          residue position (start..)
    * @return
    */
-  List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res);
+  List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence, int resNo);
 
   /**
    * get current displayed types, in ordering of rendering (on top last)
@@ -165,9 +180,9 @@ public interface FeatureRenderer
   List<String> getDisplayedFeatureTypes();
 
   /**
-   * get current displayed groups
+   * Returns a (possibly empty) list of currently visible feature groups
    * 
-   * @return a (possibly empty) list of feature groups
+   * @return
    */
   List<String> getDisplayedFeatureGroups();
 
@@ -200,4 +215,54 @@ public interface FeatureRenderer
    * @return
    */
   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 32b0565..e69785f 100644 (file)
 package jalview.api;
 
 import java.util.Collection;
-import java.util.Iterator;
+import java.util.Set;
 
 public interface FeaturesDisplayedI
 {
 
-  Iterator<String> getVisibleFeatures();
+  /**
+   * answers an unmodifiable view of the set of visible feature types
+   */
+  Set<String> getVisibleFeatures();
 
   boolean isVisible(String featureType);
 
@@ -36,6 +39,12 @@ public interface FeaturesDisplayedI
 
   void setVisible(String featureType);
 
+  /**
+   * Sets all the specified feature types to visible. Visibility of other
+   * feature types is not changed.
+   * 
+   * @param featureTypes
+   */
   void setAllVisible(Collection<String> featureTypes);
 
   boolean isRegistered(String type);
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 6fdb49c..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++)
       {
@@ -835,7 +835,7 @@ public class APopupMenu extends java.awt.PopupMenu
         if (start <= end)
         {
           seqs.add(sg.getSequenceAt(i));
-          features.add(new SequenceFeature(null, null, null, start, end,
+          features.add(new SequenceFeature(null, null, start, end,
                   "Jalview"));
         }
       }
@@ -847,7 +847,8 @@ public class APopupMenu extends java.awt.PopupMenu
         {
           ap.alignFrame.sequenceFeatures.setState(true);
           ap.av.setShowSequenceFeatures(true);
-          ap.highlightSearchResults(null);
+          ap.av.setSearchResults(null); // clear highlighting
+          ap.repaint(); // draw new/amended features
         }
       }
     }
@@ -900,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();
@@ -929,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);
     }
   }
 
@@ -1162,7 +1160,7 @@ public class APopupMenu extends java.awt.PopupMenu
 
   void refresh()
   {
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   protected void clustalColour_actionPerformed()
@@ -1338,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()
@@ -1439,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 b48dec9..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)
@@ -1426,6 +1428,17 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     return null;
   }
 
+  private List<String> getDisplayedFeatureGroups()
+  {
+    if (alignPanel.getFeatureRenderer() != null
+            && viewport.getFeaturesDisplayed() != null)
+    {
+      return alignPanel.getFeatureRenderer().getDisplayedFeatureGroups();
+
+    }
+    return null;
+  }
+
   public String outputFeatures(boolean displayTextbox, String format)
   {
     String features;
@@ -1434,13 +1447,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       features = formatter.printJalviewFormat(
               viewport.getAlignment().getSequencesArray(),
-              getDisplayedFeatureCols());
+              getDisplayedFeatureCols(), null, getDisplayedFeatureGroups(),
+              true);
     }
     else
     {
-      features = formatter.printGffFormat(
-              viewport.getAlignment().getSequencesArray(),
-              getDisplayedFeatureCols());
+      features = formatter.printGffFormat(viewport.getAlignment()
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
 
     if (displayTextbox)
@@ -1590,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();
   }
@@ -1725,7 +1741,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       EditCommand editCommand = (EditCommand) command;
       al = editCommand.getAlignment();
-      Vector comps = (Vector) PaintRefresher.components
+      Vector comps = PaintRefresher.components
               .get(viewport.getSequenceSetId());
       for (int i = 0; i < comps.size(); i++)
       {
@@ -1770,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
@@ -1782,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);
     }
   }
 
@@ -1889,7 +1906,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
   static StringBuffer copiedSequences;
 
-  static Vector<int[]> copiedHiddenColumns;
+  static HiddenColumns copiedHiddenColumns;
 
   protected void copy_actionPerformed()
   {
@@ -1913,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
     {
@@ -1989,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())
       {
@@ -2027,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);
@@ -2215,7 +2225,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       PaintRefresher.Refresh(this, viewport.getSequenceSetId());
       alignPanel.updateAnnotation();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
   }
 
@@ -2252,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();
   }
@@ -2272,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();
   }
@@ -2292,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();
   }
@@ -2485,7 +2495,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     PaintRefresher.Register(newaf.alignPanel.seqPanel.seqCanvas,
             newaf.alignPanel.av.getSequenceSetId());
 
-    Vector comps = (Vector) PaintRefresher.components
+    Vector comps = PaintRefresher.components
             .get(viewport.getSequenceSetId());
     int viewSize = -1;
     for (int i = 0; i < comps.size(); i++)
@@ -2584,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()
@@ -2606,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()
@@ -2649,7 +2659,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   {
     viewport.setGlobalColourScheme(cs);
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
   }
 
   protected void modifyPID_actionPerformed()
@@ -2724,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()
@@ -2733,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()
@@ -2742,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()
@@ -2751,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);
 
   }
 
@@ -2791,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
@@ -2855,7 +2865,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
           current.insertCharAt(Width - 1, viewport.getGapCharacter());
         }
       }
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(false, false);
 
     }
 
@@ -2908,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);
   }
 
   /**
@@ -2966,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;
   }
 
@@ -4133,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..e5767b6 100644 (file)
@@ -173,14 +173,12 @@ class AppletJmolBinding extends JalviewJmolBinding
   @Override
   public int[] resizeInnerPanel(String data)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
   @Override
   public Map<String, Object> getJSpecViewProperty(String arg0)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 }
index 3966536..b0d3f7a 100644 (file)
@@ -67,9 +67,6 @@ public class ExtJmol extends JalviewJmolBinding
   @Override
   public void updateColours(Object source)
   {
-
-    // TODO Auto-generated method stub
-
   }
 
   @Override
@@ -92,6 +89,7 @@ public class ExtJmol extends JalviewJmolBinding
     }
   }
 
+
   @Override
   public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
   {
@@ -137,8 +135,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++)
     {
@@ -181,7 +179,6 @@ public class ExtJmol extends JalviewJmolBinding
   protected JmolAppConsoleInterface createJmolConsole(
           Container consolePanel, String buttonsToShow)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
@@ -196,14 +193,11 @@ public class ExtJmol extends JalviewJmolBinding
   @Override
   public void releaseReferences(Object svl)
   {
-    // TODO Auto-generated method stub
-
   }
 
   @Override
   public Map<String, Object> getJSpecViewProperty(String arg0)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
index 0d479f9..d9eae11 100644 (file)
@@ -42,6 +42,8 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseEvent;
@@ -56,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;
@@ -165,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);
@@ -196,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));
@@ -232,6 +238,14 @@ public class FeatureColourChooser extends Panel implements ActionListener,
     threshold.addItem(MessageManager
             .getString("label.threshold_feature_below_threshold"));
     thresholdValue.addActionListener(this);
+    thresholdValue.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
     slider.setBackground(Color.white);
     slider.setEnabled(false);
     slider.setSize(new Dimension(93, 21));
@@ -272,19 +286,7 @@ public class FeatureColourChooser extends Panel implements ActionListener,
   {
     if (evt.getSource() == thresholdValue)
     {
-      try
-      {
-        float f = new Float(thresholdValue.getText()).floatValue();
-        slider.setValue((int) (f * SCALE_FACTOR_1K));
-        adjustmentValueChanged(null);
-
-        /*
-         * force repaint of any Overview window or structure
-         */
-        changeColour(true);
-      } catch (NumberFormatException ex)
-      {
-      }
+      thresholdValue_actionPerformed();
     }
     else if (evt.getSource() == minColour)
     {
@@ -300,6 +302,26 @@ public class FeatureColourChooser extends Panel implements ActionListener,
     }
   }
 
+  /**
+   * Action on input of a value for colour score threshold
+   */
+  protected void thresholdValue_actionPerformed()
+  {
+    try
+    {
+      float f = new Float(thresholdValue.getText()).floatValue();
+      slider.setValue((int) (f * SCALE_FACTOR_1K));
+      adjustmentValueChanged(null);
+
+      /*
+       * force repaint of any Overview window or structure
+       */
+      changeColour(true);
+    } catch (NumberFormatException ex)
+    {
+    }
+  }
+
   @Override
   public void itemStateChanged(ItemEvent evt)
   {
index 194b18f..df407d6 100644 (file)
@@ -399,11 +399,14 @@ public class FeatureRenderer
     /*
      * only update default type and group if we used defaults
      */
-    String enteredType = name.getText().trim();
+    final String enteredType = name.getText().trim();
+    final String enteredGroup = group.getText().trim();
+    final String enteredDesc = description.getText().replace('\n', ' ');
+
     if (dialog.accept && useLastDefaults)
     {
       lastFeatureAdded = enteredType;
-      lastFeatureGroupAdded = group.getText().trim();
+      lastFeatureGroupAdded = enteredGroup;
     }
 
     if (!create)
@@ -411,29 +414,36 @@ public class FeatureRenderer
       SequenceFeature sf = features.get(featureIndex);
       if (dialog.accept)
       {
-        sf.type = enteredType;
-        sf.featureGroup = group.getText().trim();
-        if (sf.featureGroup != null && sf.featureGroup.length() < 1)
-        {
-          sf.featureGroup = null;
-        }
-        sf.description = description.getText().replace('\n', ' ');
         if (!colourPanel.isGcol)
         {
           // update colour - otherwise its already done.
-          setColour(sf.type,
+          setColour(enteredType,
                   new FeatureColour(colourPanel.getBackground()));
         }
+        int newBegin = sf.begin;
+        int newEnd = sf.end;
         try
         {
-          sf.begin = Integer.parseInt(start.getText());
-          sf.end = Integer.parseInt(end.getText());
+          newBegin = Integer.parseInt(start.getText());
+          newEnd = Integer.parseInt(end.getText());
         } catch (NumberFormatException ex)
         {
-          //
+          // 
         }
-        boolean typeOrGroupChanged = (!featureType.equals(sf.type)
-                || !featureGroup.equals(sf.featureGroup));
+
+        /*
+         * replace the feature by deleting it and adding a new one
+         * (to ensure integrity of SequenceFeatures data store)
+         */
+        sequences.get(0).deleteFeature(sf);
+        SequenceFeature newSf = new SequenceFeature(sf, enteredType,
+                newBegin, newEnd, enteredGroup, sf.getScore());
+        newSf.setDescription(enteredDesc);
+        ffile.parseDescriptionHTML(newSf, false);
+        // amend features dialog only updates one sequence at a time
+        sequences.get(0).addSequenceFeature(newSf);
+        boolean typeOrGroupChanged = (!featureType.equals(newSf.getType()) || !featureGroup
+                .equals(newSf.getFeatureGroup()));
 
         ffile.parseDescriptionHTML(sf, false);
         if (typeOrGroupChanged)
@@ -457,12 +467,11 @@ public class FeatureRenderer
       {
         for (int i = 0; i < sequences.size(); i++)
         {
-          features.get(i).type = enteredType;
-          features.get(i).featureGroup = group.getText().trim();
-          features.get(i).description = description.getText().replace('\n',
-                  ' ');
-          sequences.get(i).addSequenceFeature(features.get(i));
-          ffile.parseDescriptionHTML(features.get(i), false);
+          SequenceFeature sf = features.get(i);
+          SequenceFeature sf2 = new SequenceFeature(enteredType,
+                  enteredDesc, sf.getBegin(), sf.getEnd(), enteredGroup);
+          ffile.parseDescriptionHTML(sf2, false);
+          sequences.get(i).addSequenceFeature(sf2);
         }
 
         Color newColour = colourPanel.getBackground();
@@ -484,7 +493,7 @@ public class FeatureRenderer
     }
     // findAllFeatures();
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
 
     return true;
   }
index 5e15364..a60aacd 100755 (executable)
@@ -23,8 +23,9 @@ package jalview.appletgui;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.BorderLayout;
 import java.awt.Button;
@@ -56,17 +57,16 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Vector;
 
 public class FeatureSettings extends Panel
         implements ItemListener, MouseListener, MouseMotionListener,
-        ActionListener, AdjustmentListener, FeatureSettingsControllerI
+        AdjustmentListener, FeatureSettingsControllerI
 {
   FeatureRenderer fr;
 
@@ -121,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);
@@ -378,36 +387,38 @@ public class FeatureSettings extends Panel
   // Group selection states
   void resetTable(boolean groupsChanged)
   {
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector<String> visibleChecks = new Vector<String>();
-    Set<String> foundGroups = new HashSet<String>();
+    List<String> displayableTypes = new ArrayList<>();
+    Set<String> foundGroups = new HashSet<>();
+
     AlignmentI alignment = av.getAlignment();
 
     for (int i = 0; i < alignment.getHeight(); i++)
     {
-      if (alignment.getSequenceAt(i).getSequenceFeatures() == null)
-      {
-        continue;
-      }
+      SequenceI seq = alignment.getSequenceAt(i);
 
-      tmpfeatures = alignment.getSequenceAt(i).getSequenceFeatures();
-      int index = 0;
-      while (index < tmpfeatures.length)
+      /*
+       * get the sequence's groups for positional features
+       * and keep track of which groups are visible
+       */
+      Set<String> groups = seq.getFeatures().getFeatureGroups(true);
+      Set<String> visibleGroups = new HashSet<>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-        foundGroups.add(group);
-
+        // if (group == null || fr.checkGroupVisibility(group, true))
         if (group == null || checkGroupState(group))
         {
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
+          visibleGroups.add(group);
         }
-        index++;
       }
+      foundGroups.addAll(groups);
+
+      /*
+       * get distinct feature types for visible groups
+       * record distinct visible types
+       */
+      Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
+              visibleGroups.toArray(new String[visibleGroups.size()]));
+      displayableTypes.addAll(types);
     }
 
     /*
@@ -424,7 +435,7 @@ public class FeatureSettings extends Panel
     {
       comps = featurePanel.getComponents();
       check = (MyCheckbox) comps[i];
-      if (!visibleChecks.contains(check.type))
+      if (!displayableTypes.contains(check.type))
       {
         featurePanel.remove(i);
         cSize--;
@@ -441,24 +452,24 @@ public class FeatureSettings extends Panel
       {
         String item = rol.get(ro);
 
-        if (!visibleChecks.contains(item))
+        if (!displayableTypes.contains(item))
         {
           continue;
         }
 
-        visibleChecks.removeElement(item);
+        displayableTypes.remove(item);
 
         addCheck(false, item);
       }
     }
 
-    // now add checkboxes which should be visible,
-    // if they have not already been added
-    Enumeration<String> en = visibleChecks.elements();
-
-    while (en.hasMoreElements())
+    /*
+     * now add checkboxes which should be visible,
+     * if they have not already been added
+     */
+    for (String type : displayableTypes)
     {
-      addCheck(groupsChanged, en.nextElement().toString());
+      addCheck(groupsChanged, type);
     }
 
     featurePanel.setLayout(
@@ -544,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++)
     {
@@ -582,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;
@@ -679,7 +686,7 @@ public class FeatureSettings extends Panel
   {
     featurePanel.removeAll();
     resetTable(false);
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   @Override
@@ -731,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 3e089bd..675b862 100644 (file)
@@ -122,7 +122,7 @@ public class Finder extends Panel implements ActionListener
     for (SearchResultMatchI match : searchResults.getResults())
     {
       seqs.add(match.getSequence().getDatasetSequence());
-      features.add(new SequenceFeature(searchString, "Search Results", null,
+      features.add(new SequenceFeature(searchString, "Search Results",
               match.getStart(), match.getEnd(), "Search Results"));
     }
 
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 8ac02be..15e269c 100755 (executable)
@@ -20,7 +20,6 @@
  */
 package jalview.appletgui;
 
-import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -57,11 +56,11 @@ public class IdPanel extends Panel
 
   UrlProviderI urlProvider = null;
 
-  public IdPanel(AlignViewport av, AlignmentPanel parent)
+  public IdPanel(AlignViewport viewport, AlignmentPanel parent)
   {
-    this.av = av;
+    this.av = viewport;
     alignPanel = parent;
-    idCanvas = new IdCanvas(av);
+    idCanvas = new IdCanvas(viewport);
     setLayout(new BorderLayout());
     add(idCanvas, BorderLayout.CENTER);
     idCanvas.addMouseListener(this);
@@ -71,13 +70,13 @@ 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>();
-    if (av.applet != null)
+    HashMap<String, String> urlList = new HashMap<>();
+    if (viewport.applet != null)
     {
       for (int i = 1; i < 10; i++)
       {
-        label = av.applet.getParameter("linkLabel_" + i);
-        url = av.applet.getParameter("linkURL_" + i);
+        label = viewport.applet.getParameter("linkLabel_" + i);
+        url = viewport.applet.getParameter("linkURL_" + i);
 
         // only add non-null parameters
         if (label != null)
@@ -89,7 +88,7 @@ public class IdPanel extends Panel
       if (!urlList.isEmpty())
       {
         // set default as first entry in list
-        String defaultUrl = av.applet.getParameter("linkLabel_1");
+        String defaultUrl = viewport.applet.getParameter("linkLabel_1");
         UrlProviderFactoryI factory = new AppletUrlProviderFactory(
                 defaultUrl, urlList);
         urlProvider = factory.createUrlProvider();
@@ -106,64 +105,57 @@ public class IdPanel extends Panel
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
-    // look for non-pos features
     StringBuffer tooltiptext = new StringBuffer();
-    if (sequence != null)
+    if (sequence == null)
     {
-      if (sequence.getDescription() != null)
+      return;
+    }
+    if (sequence.getDescription() != null)
+    {
+      tooltiptext.append(sequence.getDescription());
+      tooltiptext.append("\n");
+    }
+
+    for (SequenceFeature sf : sequence.getFeatures()
+            .getNonPositionalFeatures())
+    {
+      boolean nl = false;
+      if (sf.getFeatureGroup() != null)
       {
-        tooltiptext.append(sequence.getDescription());
-        tooltiptext.append("\n");
+        tooltiptext.append(sf.getFeatureGroup());
+        nl = true;
       }
-
-      SequenceFeature sf[] = sequence.getSequenceFeatures();
-      for (int sl = 0; sf != null && sl < sf.length; sl++)
+      if (sf.getType() != null)
       {
-        if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0)
-        {
-          boolean nl = false;
-          if (sf[sl].getFeatureGroup() != null)
-          {
-            tooltiptext.append(sf[sl].getFeatureGroup());
-            nl = true;
-          }
-          ;
-          if (sf[sl].getType() != null)
-          {
-            tooltiptext.append(" ");
-            tooltiptext.append(sf[sl].getType());
-            nl = true;
-          }
-          ;
-          if (sf[sl].getDescription() != null)
-          {
-            tooltiptext.append(" ");
-            tooltiptext.append(sf[sl].getDescription());
-            nl = true;
-          }
-          ;
-          if (!Float.isNaN(sf[sl].getScore()) && sf[sl].getScore() != 0f)
-          {
-            tooltiptext.append(" Score = ");
-            tooltiptext.append(sf[sl].getScore());
-            nl = true;
-          }
-          ;
-          if (sf[sl].getStatus() != null && sf[sl].getStatus().length() > 0)
-          {
-            tooltiptext.append(" (");
-            tooltiptext.append(sf[sl].getStatus());
-            tooltiptext.append(")");
-            nl = true;
-          }
-          ;
-          if (nl)
-          {
-            tooltiptext.append("\n");
-          }
-        }
+        tooltiptext.append(" ");
+        tooltiptext.append(sf.getType());
+        nl = true;
+      }
+      if (sf.getDescription() != null)
+      {
+        tooltiptext.append(" ");
+        tooltiptext.append(sf.getDescription());
+        nl = true;
+      }
+      if (!Float.isNaN(sf.getScore()) && sf.getScore() != 0f)
+      {
+        tooltiptext.append(" Score = ");
+        tooltiptext.append(sf.getScore());
+        nl = true;
+      }
+      if (sf.getStatus() != null && sf.getStatus().length() > 0)
+      {
+        tooltiptext.append(" (");
+        tooltiptext.append(sf.getStatus());
+        tooltiptext.append(")");
+        nl = true;
+      }
+      if (nl)
+      {
+        tooltiptext.append("\n");
       }
     }
+
     if (tooltiptext.length() == 0)
     {
       // nothing to display - so clear tooltip if one is visible
@@ -206,7 +198,7 @@ public class IdPanel extends Panel
     }
 
     lastid = seq;
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
   }
 
   @Override
@@ -290,10 +282,12 @@ public class IdPanel extends Panel
     if ((e.getModifiers()
             & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq);
+      SequenceI sq = av.getAlignment().getSequenceAt(seq);
 
-      // build a new links menu based on the current links + any non-positional
-      // features
+      /*
+       *  build a new links menu based on the current links
+       *  and any non-positional features
+       */
       List<String> nlinks;
       if (urlProvider != null)
       {
@@ -301,19 +295,16 @@ public class IdPanel extends Panel
       }
       else
       {
-        nlinks = new ArrayList<String>();
+        nlinks = new ArrayList<>();
       }
-      SequenceFeature sf[] = sq == null ? null : sq.getSequenceFeatures();
-      for (int sl = 0; sf != null && sl < sf.length; sl++)
+
+      for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
       {
-        if (sf[sl].begin == sf[sl].end && sf[sl].begin == 0)
+        if (sf.links != null)
         {
-          if (sf[sl].links != null && sf[sl].links.size() > 0)
+          for (String link : sf.links)
           {
-            for (int l = 0, lSize = sf[sl].links.size(); l < lSize; l++)
-            {
-              nlinks.add(sf[sl].links.elementAt(l));
-            }
+            nlinks.add(link);
           }
         }
       }
@@ -342,7 +333,7 @@ public class IdPanel extends Panel
       selectSeq(seq);
     }
 
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
   }
 
   void selectSeq(int seq)
@@ -426,9 +417,9 @@ public class IdPanel extends Panel
 
     boolean up = true;
 
-    public ScrollThread(boolean up)
+    public ScrollThread(boolean isUp)
     {
-      this.up = up;
+      this.up = isUp;
       start();
     }
 
@@ -468,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 28c5291..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;
@@ -559,8 +550,8 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     return annotations.adjustPanelHeight();
   }
 
-  private void drawPanel(Graphics g1, int startRes, int endRes,
-          int startSeq, int endSeq, int offset)
+  private void drawPanel(Graphics g1, final int startRes, final int endRes,
+          final int startSeq, final int endSeq, final int offset)
   {
 
     if (!av.hasHiddenColumns())
@@ -569,62 +560,45 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     }
     else
     {
-
       int screenY = 0;
-      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];
-
-          if (hideStart <= blockStart)
-          {
-            blockStart += (hideEnd - hideStart) + 1;
-            continue;
-          }
-
-          blockEnd = hideStart - 1;
-
-          g1.translate(screenY * avcharWidth, 0);
-
-          draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
-
-          if (av.getShowHiddenMarkers())
-          {
-            g1.setColor(Color.blue);
-            g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
-                    0 + offset,
-                    (blockEnd - blockStart + 1) * avcharWidth - 1,
-                    (endSeq - startSeq + 1) * avcharHeight + offset);
-          }
+      int blockStart;
+      int blockEnd;
 
-          g1.translate(-screenY * avcharWidth, 0);
-          screenY += blockEnd - blockStart + 1;
-          blockStart = hideEnd + 1;
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      VisibleContigsIterator regions = (VisibleContigsIterator) hidden
+              .getVisContigsIterator(startRes, endRes + 1, true);
 
-          if (screenY > (endRes - startRes))
-          {
-            // already rendered last block
-            return;
-          }
-        }
-      }
-      if (screenY <= (endRes - startRes))
+      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,
@@ -880,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)
         {
@@ -915,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 57bfa68..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;
@@ -54,10 +53,8 @@ import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Vector;
 
 public class SeqPanel extends Panel implements MouseMotionListener,
@@ -150,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()
@@ -169,7 +166,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       seqCanvas.cursorX = getKeyboardNo1() - 1;
       seqCanvas.cursorY = getKeyboardNo2() - 1;
-      scrollToVisible();
+      scrollToVisible(true);
     }
   }
 
@@ -178,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)
@@ -204,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)
     {
@@ -228,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())
-      {
-        ranges.scrollUp(true);
-      }
-      while (seqCanvas.cursorY > ranges.getEndSeq())
+      if (av.getWrapAlignment())
       {
-        ranges.scrollUp(false);
+        av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
       }
-      while (seqCanvas.cursorX < hidden
-              .adjustForHiddenColumns(ranges.getStartRes()))
+      else
       {
-
-        if (!ranges.scrollRight(false))
-        {
-          break;
-        }
-      }
-      while (seqCanvas.cursorX > hidden
-              .adjustForHiddenColumns(ranges.getEndRes()))
-      {
-        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)
@@ -337,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();
   }
 
@@ -419,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)
   {
@@ -530,7 +522,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     int seq = findSeq(evt);
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (seq < 0 || res < 0)
     {
@@ -566,14 +558,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         av.setSelectionGroup(null);
       }
 
-      int column = findRes(evt);
-      boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
-      List<SequenceFeature> features = findFeaturesAtRes(sequence,
-              sequence.findPosition(column));
-      if (isGapped)
-      {
-        removeAdjacentFeatures(features, column + 1, sequence);
-      }
+      int column = findColumn(evt);
+      List<SequenceFeature> features = findFeaturesAtColumn(sequence,
+              column + 1);
 
       if (!features.isEmpty())
       {
@@ -583,8 +570,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         seqCanvas.highlightSearchResults(highlight);
         seqCanvas.getFeatureRenderer().amendFeatures(
                 Collections.singletonList(sequence), features, false, ap);
-
-        seqCanvas.highlightSearchResults(null);
+        av.setSearchResults(null); // clear highlighting
+        seqCanvas.repaint(); // draw new/amended features
       }
     }
   }
@@ -610,7 +597,14 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   int wrappedBlock = -1;
 
-  int findRes(MouseEvent evt)
+  /**
+   * Returns the aligned sequence position (base 0) at the mouse position, or
+   * the closest visible one
+   * 
+   * @param evt
+   * @return
+   */
+  int findColumn(MouseEvent evt)
   {
     int res = 0;
     int x = evt.getX();
@@ -653,7 +647,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     return res;
@@ -714,7 +708,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   {
 
     int seq = findSeq(evt);
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (seq < av.getAlignment().getHeight()
             && res < av.getAlignment().getSequenceAt(seq).getLength())
@@ -786,7 +780,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   @Override
   public void mouseMoved(MouseEvent evt)
   {
-    final int column = findRes(evt);
+    final int column = findColumn(evt);
     int seq = findSeq(evt);
 
     if (seq >= av.getAlignment().getHeight() || seq < 0 || column < 0)
@@ -869,12 +863,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
      */
     if (av.isShowSequenceFeatures())
     {
-      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
-              sequence.findPosition(column));
-      if (isGapped)
-      {
-        removeAdjacentFeatures(allFeatures, column + 1, sequence);
-      }
+      List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
+              column + 1);
       for (SequenceFeature sf : allFeatures)
       {
         tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
@@ -907,63 +897,18 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   }
 
   /**
-   * Removes from the list of features any that start after, or end before, the
-   * given column position. This allows us to retain only those features
-   * adjacent to a gapped position that straddle the position. Contact features
-   * that 'straddle' the position are also removed, since they are not 'at' the
-   * position.
+   * 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).
    * 
-   * @param features
-   * @param column
-   *          alignment column (1..)
    * @param sequence
+   * @param column
+   *          (1..)
+   * @return
    */
-  protected void removeAdjacentFeatures(List<SequenceFeature> features,
-          int column, SequenceI sequence)
+  List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
   {
-    // TODO should this be an AlignViewController method (shared by gui)?
-    ListIterator<SequenceFeature> it = features.listIterator();
-    while (it.hasNext())
-    {
-      SequenceFeature sf = it.next();
-      if (sf.isContactFeature()
-              || sequence.findIndex(sf.getBegin()) > column
-              || sequence.findIndex(sf.getEnd()) < column)
-      {
-        it.remove();
-      }
-    }
-  }
-
-  List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res)
-  {
-    List<SequenceFeature> result = new ArrayList<>();
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-    if (features != null)
-    {
-      for (int i = 0; i < features.length; i++)
-      {
-        if (av.getFeaturesDisplayed() == null || !av.getFeaturesDisplayed()
-                .isVisible(features[i].getType()))
-        {
-          continue;
-        }
-
-        if (features[i].featureGroup != null && !seqCanvas.fr
-                .checkGroupVisibility(features[i].featureGroup, false))
-        {
-          continue;
-        }
-
-        if ((features[i].getBegin() <= res)
-                && (features[i].getEnd() >= res))
-        {
-          result.add(features[i]);
-        }
-      }
-    }
-
-    return result;
+    return seqCanvas.getFeatureRenderer().findFeaturesAtColumn(sequence, column);
   }
 
   Tooltip tooltip;
@@ -1032,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;
     }
@@ -1043,7 +988,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (res < 0)
     {
@@ -1178,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))
@@ -1225,7 +1170,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         // Find the next gap before the end
         // of the visible region boundary
         boolean blank = false;
-        for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
+        for (; fixedRight > lastres; fixedRight--)
         {
           blank = true;
 
@@ -1252,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;
@@ -1455,7 +1401,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       scrollThread = null;
     }
 
-    int res = findRes(evt);
+    int column = findColumn(evt);
     int seq = findSeq(evt);
     oldSeq = seq;
     startWrapBlock = wrappedBlock;
@@ -1467,16 +1413,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
-    if (sequence == null || res > sequence.getLength())
+    if (sequence == null || column > sequence.getLength())
     {
       return;
     }
 
     stretchGroup = av.getSelectionGroup();
 
-    if (stretchGroup == null || !stretchGroup.contains(sequence, res))
+    if (stretchGroup == null || !stretchGroup.contains(sequence, column))
     {
-      stretchGroup = av.getAlignment().findGroup(sequence, res);
+      stretchGroup = av.getAlignment().findGroup(sequence, column);
       if (stretchGroup != null)
       {
         // only update the current selection if the popup menu has a group to
@@ -1489,8 +1435,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     if ((evt.getModifiers()
             & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
-              sequence.findPosition(res));
+      List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
+              sequence.findPosition(column + 1));
 
       Vector<String> links = null;
       for (SequenceFeature sf : allFeatures)
@@ -1501,10 +1447,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
           {
             links = new Vector<>();
           }
-          for (int j = 0; j < sf.links.size(); j++)
-          {
-            links.addElement(sf.links.elementAt(j));
-          }
+          links.addAll(sf.links);
         }
       }
       APopupMenu popup = new APopupMenu(ap, null, links);
@@ -1515,7 +1458,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     if (av.cursorMode)
     {
-      seqCanvas.cursorX = findRes(evt);
+      seqCanvas.cursorX = findColumn(evt);
       seqCanvas.cursorY = findSeq(evt);
       seqCanvas.repaint();
       return;
@@ -1527,8 +1470,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       // define a new group here
       SequenceGroup sg = new SequenceGroup();
-      sg.setStartRes(res);
-      sg.setEndRes(res);
+      sg.setStartRes(column);
+      sg.setEndRes(column);
       sg.addSequence(sequence, false);
       av.setSelectionGroup(sg);
       stretchGroup = sg;
@@ -1576,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;
@@ -1586,7 +1529,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   public void doMouseDraggedDefineMode(MouseEvent evt)
   {
-    int res = findRes(evt);
+    int res = findColumn(evt);
     int y = findSeq(evt);
 
     if (wrappedBlock != startWrapBlock)
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..83bc810 100755 (executable)
@@ -28,8 +28,6 @@ import jalview.schemes.UserColourScheme;
 import jalview.structure.StructureImportSettings;
 import jalview.urls.IdOrgSettings;
 import jalview.util.ColorUtils;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.datamodel.DasSourceRegistry;
 import jalview.ws.sifts.SiftsSettings;
 
 import java.awt.Color;
@@ -281,7 +279,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 +332,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 +370,7 @@ public class Cache
       {
         fis = new FileInputStream(propertiesFile);
       }
+      applicationProperties.clear();
       applicationProperties.load(fis);
 
       // remove any old build properties
@@ -621,14 +623,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 +642,7 @@ public class Cache
       System.out.println(
               "Error setting property: " + key + " " + obj + "\n" + ex);
     }
-    return obj;
+    return oldValue;
   }
 
   /**
@@ -978,22 +980,6 @@ public class Cache
     return null;
   }
 
-  private static DasSourceRegistryI sourceRegistry = null;
-
-  /**
-   * initialise and ..
-   * 
-   * @return instance of the das source registry
-   */
-  public static DasSourceRegistryI getDasSourceRegistry()
-  {
-    if (sourceRegistry == null)
-    {
-      sourceRegistry = new DasSourceRegistry();
-    }
-    return sourceRegistry;
-  }
-
   /**
    * Set the specified value, or remove it if null or empty. Does not save the
    * properties file.
index 9ec0033..3270144 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,12 +61,23 @@ 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>
- * start with java -Djava.ext.dirs=$PATH_TO_LIB$ jalview.bin.Jalview
+ * start with: java -classpath "$PATH_TO_LIB$/*:$PATH_TO_CLASSES$" \
+ * jalview.bin.Jalview
+ * 
+ * or on Windows: java -classpath "$PATH_TO_LIB$/*;$PATH_TO_CLASSES$" \
+ * jalview.bin.Jalview jalview.bin.Jalview
+ * 
+ * (ensure -classpath arg is quoted to avoid shell expansion of '*' and do not
+ * embellish '*' to e.g. '*.jar')
  * 
  * @author $author$
  * @version $Revision$
@@ -148,7 +156,6 @@ public class Jalview
           af.setProgressBar(MessageManager
                   .getString("status.das_features_being_retrived"), id);
           af.featureSettings_actionPerformed(null);
-          af.featureSettings.fetchDasFeatures(dasSources, true);
           af.setProgressBar(null, id);
           synchronized (us)
           {
@@ -264,7 +271,7 @@ public class Jalview
     {
       error.printStackTrace();
       System.out.println("\nEssential logging libraries not found."
-              + "\nUse: java -Djava.ext.dirs=$PATH_TO_LIB$ jalview.bin.Jalview");
+              + "\nUse: java -classpath \"$PATH_TO_LIB$/*:$PATH_TO_CLASSES$\" jalview.bin.Jalview");
       System.exit(0);
     }
 
@@ -275,20 +282,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 +1000,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 +1066,7 @@ public class Jalview
                         + nickname + "|" + url);
         if (source == null)
         {
-          source = new Vector<String>();
+          source = new Vector<>();
         }
         source.addElement(nickname);
       }
@@ -1054,7 +1084,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 d319249..1a227c5 100644 (file)
  */
 package jalview.commands;
 
+import jalview.analysis.AlignSeq;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.Range;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.Comparison;
 import jalview.util.ReverseListIterator;
 import jalview.util.StringUtils;
 
@@ -114,7 +118,7 @@ public class EditCommand implements CommandI
     public abstract Action getUndoAction();
   };
 
-  private List<Edit> edits = new ArrayList<Edit>();
+  private List<Edit> edits = new ArrayList<>();
 
   String description;
 
@@ -122,15 +126,15 @@ public class EditCommand implements CommandI
   {
   }
 
-  public EditCommand(String description)
+  public EditCommand(String desc)
   {
-    this.description = description;
+    this.description = desc;
   }
 
-  public EditCommand(String description, Action command, SequenceI[] seqs,
+  public EditCommand(String desc, Action command, SequenceI[] seqs,
           int position, int number, AlignmentI al)
   {
-    this.description = description;
+    this.description = desc;
     if (command == Action.CUT || command == Action.PASTE)
     {
       setEdit(new Edit(command, seqs, position, number, al));
@@ -139,10 +143,10 @@ public class EditCommand implements CommandI
     performEdit(0, null);
   }
 
-  public EditCommand(String description, Action command, String replace,
+  public EditCommand(String desc, Action command, String replace,
           SequenceI[] seqs, int position, int number, AlignmentI al)
   {
-    this.description = description;
+    this.description = desc;
     if (command == Action.REPLACE)
     {
       setEdit(new Edit(command, seqs, position, number, al, replace));
@@ -330,20 +334,8 @@ public class EditCommand implements CommandI
           int position, int number, AlignmentI al, boolean performEdit,
           AlignmentI[] views)
   {
-    Edit edit = new Edit(command, seqs, position, number,
-            al.getGapCharacter());
-    if (al.getHeight() == seqs.length)
-    {
-      edit.al = al;
-      edit.fullAlignmentHeight = true;
-    }
-
-    addEdit(edit);
-
-    if (performEdit)
-    {
-      performEdit(edit, views);
-    }
+    Edit edit = new Edit(command, seqs, position, number, al);
+    appendEdit(edit, al, performEdit, views);
   }
 
   /**
@@ -534,37 +526,62 @@ public class EditCommand implements CommandI
         command.string[i] = sequence.getSequence(command.position,
                 command.position + command.number);
         SequenceI oldds = sequence.getDatasetSequence();
-        if (command.oldds != null && command.oldds[i] != null)
-        {
-          // we are redoing an undone cut.
-          sequence.setDatasetSequence(null);
-        }
-        sequence.deleteChars(command.position,
+        Range cutPositions = sequence.findPositions(command.position + 1,
                 command.position + command.number);
+        boolean cutIsInternal = cutPositions != null
+                && sequence.getStart() != cutPositions
+                .getBegin() && sequence.getEnd() != cutPositions.getEnd();
+
+        /*
+         * perform the cut; if this results in a new dataset sequence, add
+         * that to the alignment dataset
+         */
+        SequenceI ds = sequence.getDatasetSequence();
+        sequence.deleteChars(command.position, command.position
+                + command.number);
+
         if (command.oldds != null && command.oldds[i] != null)
         {
-          // oldds entry contains the cut dataset sequence.
+          /*
+           * we are Redoing a Cut, or Undoing a Paste - so
+           * oldds entry contains the cut dataset sequence,
+           * with sequence features in expected place
+           */
           sequence.setDatasetSequence(command.oldds[i]);
           command.oldds[i] = oldds;
         }
         else
         {
-          // modify the oldds if necessary
+          /* 
+           * new cut operation: save the dataset sequence 
+           * so it can be restored in an Undo
+           */
+          if (command.oldds == null)
+          {
+            command.oldds = new SequenceI[command.seqs.length];
+          }
+          command.oldds[i] = oldds;// todo not if !cutIsInternal?
+
+          // do we need to edit sequence features for new sequence ?
           if (oldds != sequence.getDatasetSequence()
-                  || sequence.getSequenceFeatures() != null)
+                  || (cutIsInternal
+                          && sequence.getFeatures().hasFeatures()))
+          // todo or just test cutIsInternal && cutPositions != null ?
           {
-            if (command.oldds == null)
+            if (cutPositions != null)
             {
-              command.oldds = new SequenceI[command.seqs.length];
+              cutFeatures(command, sequence, cutPositions.getBegin(),
+                              cutPositions.getEnd(), cutIsInternal);
             }
-            command.oldds[i] = oldds;
-            adjustFeatures(command, i,
-                    sequence.findPosition(command.position),
-                    sequence.findPosition(
-                            command.position + command.number),
-                    false);
           }
         }
+        SequenceI newDs = sequence.getDatasetSequence();
+        if (newDs != ds && command.al != null
+                && command.al.getDataset() != null
+                && !command.al.getDataset().getSequences().contains(newDs))
+        {
+          command.al.getDataset().addSequence(newDs);
+        }
       }
 
       if (sequence.getLength() < 1)
@@ -586,21 +603,19 @@ public class EditCommand implements CommandI
    */
   static void paste(Edit command, AlignmentI[] views)
   {
-    StringBuffer tmp;
-    boolean newDSNeeded;
-    boolean newDSWasNeeded;
-    int newstart, newend;
     boolean seqWasDeleted = false;
-    int start = 0, end = 0;
 
     for (int i = 0; i < command.seqs.length; i++)
     {
-      newDSNeeded = false;
-      newDSWasNeeded = command.oldds != null && command.oldds[i] != null;
-      if (command.seqs[i].getLength() < 1)
+      boolean newDSNeeded = false;
+      boolean newDSWasNeeded = command.oldds != null
+              && command.oldds[i] != null;
+      SequenceI sequence = command.seqs[i];
+      if (sequence.getLength() < 1)
       {
-        // ie this sequence was deleted, we need to
-        // readd it to the alignment
+        /*
+         * sequence was deleted; re-add it to the alignment
+         */
         if (command.alIndex[i] < command.al.getHeight())
         {
           List<SequenceI> sequences;
@@ -608,68 +623,78 @@ public class EditCommand implements CommandI
           {
             if (!(command.alIndex[i] < 0))
             {
-              sequences.add(command.alIndex[i], command.seqs[i]);
+              sequences.add(command.alIndex[i], sequence);
             }
           }
         }
         else
         {
-          command.al.addSequence(command.seqs[i]);
+          command.al.addSequence(sequence);
         }
         seqWasDeleted = true;
       }
-      newstart = command.seqs[i].getStart();
-      newend = command.seqs[i].getEnd();
+      int newStart = sequence.getStart();
+      int newEnd = sequence.getEnd();
 
-      tmp = new StringBuffer();
-      tmp.append(command.seqs[i].getSequence());
+      StringBuilder tmp = new StringBuilder();
+      tmp.append(sequence.getSequence());
       // Undo of a delete does not replace original dataset sequence on to
       // alignment sequence.
 
+      int start = 0;
+      int length = 0;
+
       if (command.string != null && command.string[i] != null)
       {
         if (command.position >= tmp.length())
         {
           // This occurs if padding is on, and residues
           // are removed from end of alignment
-          int length = command.position - tmp.length();
-          while (length > 0)
+          int len = command.position - tmp.length();
+          while (len > 0)
           {
             tmp.append(command.gapChar);
-            length--;
+            len--;
           }
         }
         tmp.insert(command.position, command.string[i]);
         for (int s = 0; s < command.string[i].length; s++)
         {
-          if (jalview.schemes.ResidueProperties.aaIndex[command.string[i][s]] != 23)
+          if (!Comparison.isGap(command.string[i][s]))
           {
+            length++;
             if (!newDSNeeded)
             {
               newDSNeeded = true;
-              start = command.seqs[i].findPosition(command.position);
-              end = command.seqs[i]
-                      .findPosition(command.position + command.number);
+              start = sequence.findPosition(command.position);
+              // end = sequence
+              // .findPosition(command.position + command.number);
             }
-            if (command.seqs[i].getStart() == start)
+            if (sequence.getStart() == start)
             {
-              newstart--;
+              newStart--;
             }
             else
             {
-              newend++;
+              newEnd++;
             }
           }
         }
         command.string[i] = null;
       }
 
-      command.seqs[i].setSequence(tmp.toString());
-      command.seqs[i].setStart(newstart);
-      command.seqs[i].setEnd(newend);
+      sequence.setSequence(tmp.toString());
+      sequence.setStart(newStart);
+      sequence.setEnd(newEnd);
+
+      /*
+       * command and Undo share the same dataset sequence if cut was
+       * at start or end of sequence
+       */
+      boolean sameDatasetSequence = false;
       if (newDSNeeded)
       {
-        if (command.seqs[i].getDatasetSequence() != null)
+        if (sequence.getDatasetSequence() != null)
         {
           SequenceI ds;
           if (newDSWasNeeded)
@@ -680,21 +705,29 @@ public class EditCommand implements CommandI
           {
             // make a new DS sequence
             // use new ds mechanism here
-            ds = new Sequence(command.seqs[i].getName(),
-                    jalview.analysis.AlignSeq.extractGaps(
-                            jalview.util.Comparison.GapChars,
-                            command.seqs[i].getSequenceAsString()),
-                    command.seqs[i].getStart(), command.seqs[i].getEnd());
-            ds.setDescription(command.seqs[i].getDescription());
+            String ungapped = AlignSeq.extractGaps(Comparison.GapChars,
+                    sequence.getSequenceAsString());
+            ds = new Sequence(sequence.getName(), ungapped,
+                    sequence.getStart(), sequence.getEnd());
+            ds.setDescription(sequence.getDescription());
           }
           if (command.oldds == null)
           {
             command.oldds = new SequenceI[command.seqs.length];
           }
-          command.oldds[i] = command.seqs[i].getDatasetSequence();
-          command.seqs[i].setDatasetSequence(ds);
+          command.oldds[i] = sequence.getDatasetSequence();
+          sameDatasetSequence = ds == sequence.getDatasetSequence();
+          ds.setSequenceFeatures(sequence.getSequenceFeatures());
+          if (!sameDatasetSequence && command.al.getDataset() != null)
+          {
+            // delete 'undone' sequence from alignment dataset
+            command.al.getDataset()
+                    .deleteSequence(sequence.getDatasetSequence());
+          }
+          sequence.setDatasetSequence(ds);
         }
-        adjustFeatures(command, i, start, end, true);
+        undoCutFeatures(command, command.seqs[i], start, length,
+                sameDatasetSequence);
       }
     }
     adjustAnnotations(command, true, seqWasDeleted, views);
@@ -704,7 +737,7 @@ public class EditCommand implements CommandI
 
   static void replace(Edit command)
   {
-    StringBuffer tmp;
+    StringBuilder tmp;
     String oldstring;
     int start = command.position;
     int end = command.number;
@@ -717,6 +750,7 @@ public class EditCommand implements CommandI
     {
       boolean newDSWasNeeded = command.oldds != null
               && command.oldds[i] != null;
+      boolean newStartEndWasNeeded = command.oldStartEnd!=null && command.oldStartEnd[i]!=null;
 
       /**
        * cut addHistoryItem(new EditCommand("Cut Sequences", EditCommand.CUT,
@@ -729,49 +763,147 @@ public class EditCommand implements CommandI
        * EditCommand.PASTE, sequences, 0, alignment.getWidth(), alignment) );
        * 
        */
+
+      Range beforeEditedPositions = command.seqs[i].findPositions(1, start);
+      Range afterEditedPositions = command.seqs[i]
+              .findPositions(end + 1, command.seqs[i].getLength());
+      
       oldstring = command.seqs[i].getSequenceAsString();
-      tmp = new StringBuffer(oldstring.substring(0, start));
+      tmp = new StringBuilder(oldstring.substring(0, start));
       tmp.append(command.string[i]);
-      String nogaprep = jalview.analysis.AlignSeq.extractGaps(
-              jalview.util.Comparison.GapChars,
+      String nogaprep = AlignSeq.extractGaps(Comparison.GapChars,
               new String(command.string[i]));
-      int ipos = command.seqs[i].findPosition(start)
-              - command.seqs[i].getStart();
-      tmp.append(oldstring.substring(end));
+      if (end < oldstring.length())
+      {
+        tmp.append(oldstring.substring(end));
+      }
+      // stash end prior to updating the sequence object so we can save it if
+      // need be.
+      Range oldstartend = new Range(command.seqs[i].getStart(),
+              command.seqs[i].getEnd());
       command.seqs[i].setSequence(tmp.toString());
-      command.string[i] = oldstring.substring(start, end).toCharArray();
-      String nogapold = jalview.analysis.AlignSeq.extractGaps(
-              jalview.util.Comparison.GapChars,
+      command.string[i] = oldstring
+              .substring(start, Math.min(end, oldstring.length()))
+              .toCharArray();
+      String nogapold = AlignSeq.extractGaps(Comparison.GapChars,
               new String(command.string[i]));
+
       if (!nogaprep.toLowerCase().equals(nogapold.toLowerCase()))
       {
-        if (newDSWasNeeded)
+        // we may already have dataset and limits stashed...
+        if (newDSWasNeeded || newStartEndWasNeeded)
         {
+          if (newDSWasNeeded)
+          {
+          // then just switch the dataset sequence
           SequenceI oldds = command.seqs[i].getDatasetSequence();
           command.seqs[i].setDatasetSequence(command.oldds[i]);
           command.oldds[i] = oldds;
+          }
+          if (newStartEndWasNeeded)
+          {
+            Range newStart = command.oldStartEnd[i];
+            command.oldStartEnd[i] = oldstartend;
+            command.seqs[i].setStart(newStart.getBegin());
+            command.seqs[i].setEnd(newStart.getEnd());
+          }
         }
-        else
+        else         
         {
-          if (command.oldds == null)
+          // decide if we need a new dataset sequence or modify start/end
+          // first edit the original dataset sequence string
+          SequenceI oldds = command.seqs[i].getDatasetSequence();
+          String osp = oldds.getSequenceAsString();
+          int beforeStartOfEdit = -oldds.getStart() + 1
+                  + (beforeEditedPositions == null
+                          ? ((afterEditedPositions != null)
+                                  ? afterEditedPositions.getBegin() - 1
+                                  : oldstartend.getBegin()
+                                          + nogapold.length())
+                          : beforeEditedPositions.getEnd()
+                  );
+          int afterEndOfEdit = -oldds.getStart() + 1
+                  + ((afterEditedPositions == null)
+                  ? oldstartend.getEnd()
+                          : afterEditedPositions.getBegin() - 1);
+          String fullseq = osp.substring(0,
+                  beforeStartOfEdit)
+                  + nogaprep
+                  + osp.substring(afterEndOfEdit);
+
+          // and check if new sequence data is different..
+          if (!fullseq.equalsIgnoreCase(osp))
           {
-            command.oldds = new SequenceI[command.seqs.length];
-          }
-          command.oldds[i] = command.seqs[i].getDatasetSequence();
-          SequenceI newds = new Sequence(
-                  command.seqs[i].getDatasetSequence());
-          String fullseq, osp = newds.getSequenceAsString();
-          fullseq = osp.substring(0, ipos) + nogaprep
-                  + osp.substring(ipos + nogaprep.length());
-          newds.setSequence(fullseq.toUpperCase());
-          // TODO: JAL-1131 ensure newly created dataset sequence is added to
-          // the set of
-          // dataset sequences associated with the alignment.
-          // TODO: JAL-1131 fix up any annotation associated with new dataset
-          // sequence to ensure that original sequence/annotation relationships
-          // are preserved.
-          command.seqs[i].setDatasetSequence(newds);
+            // old ds and edited ds are different, so
+            // create the new dataset sequence
+            SequenceI newds = new Sequence(oldds);
+            newds.setSequence(fullseq);
 
+            if (command.oldds == null)
+            {
+              command.oldds = new SequenceI[command.seqs.length];
+            }
+            command.oldds[i] = command.seqs[i].getDatasetSequence();
+
+            // And preserve start/end for good-measure
+
+            if (command.oldStartEnd == null)
+            {
+              command.oldStartEnd = new Range[command.seqs.length];
+            }
+            command.oldStartEnd[i] = oldstartend;
+            // TODO: JAL-1131 ensure newly created dataset sequence is added to
+            // the set of
+            // dataset sequences associated with the alignment.
+            // TODO: JAL-1131 fix up any annotation associated with new dataset
+            // sequence to ensure that original sequence/annotation
+            // relationships
+            // are preserved.
+            command.seqs[i].setDatasetSequence(newds);
+          }
+          else
+          {
+            if (command.oldStartEnd == null)
+            {
+              command.oldStartEnd = new Range[command.seqs.length];
+            }
+            command.oldStartEnd[i] = new Range(command.seqs[i].getStart(),
+                    command.seqs[i].getEnd());
+            if (beforeEditedPositions != null
+                    && afterEditedPositions == null)
+            {
+              // modification at end
+              command.seqs[i].setEnd(
+                      beforeEditedPositions.getEnd() + nogaprep.length()
+                              - nogapold.length());
+            }
+            else if (afterEditedPositions != null
+                    && beforeEditedPositions == null)
+            {
+              // modification at start
+              command.seqs[i].setStart(
+                      afterEditedPositions.getBegin() - nogaprep.length());
+            }
+            else
+            {
+              // edit covered both start and end. Here we can only guess the
+              // new
+              // start/end
+              String nogapalseq = AlignSeq.extractGaps(Comparison.GapChars,
+                      command.seqs[i].getSequenceAsString().toUpperCase());
+              int newStart = command.seqs[i].getDatasetSequence()
+                      .getSequenceAsString().indexOf(nogapalseq);
+              if (newStart == -1)
+              {
+                throw new Error(
+                        "Implementation Error: could not locate start/end "
+                                + "in dataset sequence after an edit of the sequence string");
+              }
+              int newEnd = newStart + nogapalseq.length() - 1;
+              command.seqs[i].setStart(newStart);
+              command.seqs[i].setEnd(newEnd);
+            }
+          }
         }
       }
       tmp = null;
@@ -787,7 +919,7 @@ public class EditCommand implements CommandI
     if (modifyVisibility && !insert)
     {
       // only occurs if a sequence was added or deleted.
-      command.deletedAnnotationRows = new Hashtable<SequenceI, AlignmentAnnotation[]>();
+      command.deletedAnnotationRows = new Hashtable<>();
     }
     if (command.fullAlignmentHeight)
     {
@@ -799,6 +931,8 @@ public class EditCommand implements CommandI
       AlignmentAnnotation[] tmp;
       for (int s = 0; s < command.seqs.length; s++)
       {
+        command.seqs[s].sequenceChanged();
+
         if (modifyVisibility)
         {
           // Rows are only removed or added to sequence object.
@@ -888,8 +1022,7 @@ public class EditCommand implements CommandI
                 }
                 // and then duplicate added annotation on every other alignment
                 // view
-                for (int vnum = 0; views != null
-                        && vnum < views.length; vnum++)
+                for (int vnum = 0; views != null && vnum < views.length; vnum++)
                 {
                   if (views[vnum] != command.al)
                   {
@@ -944,7 +1077,7 @@ public class EditCommand implements CommandI
 
     if (!insert)
     {
-      command.deletedAnnotations = new Hashtable<String, Annotation[]>();
+      command.deletedAnnotations = new Hashtable<>();
     }
 
     int aSize;
@@ -1105,81 +1238,97 @@ public class EditCommand implements CommandI
     }
   }
 
-  final static void adjustFeatures(Edit command, int index, int i, int j,
-          boolean insert)
+  /**
+   * Restores features to the state before a Cut.
+   * <ul>
+   * <li>re-add any features deleted by the cut</li>
+   * <li>remove any truncated features created by the cut</li>
+   * <li>shift right any features to the right of the cut</li>
+   * </ul>
+   * 
+   * @param command
+   *          the Cut command
+   * @param seq
+   *          the sequence the Cut applied to
+   * @param start
+   *          the start residue position of the cut
+   * @param length
+   *          the number of residues cut
+   * @param sameDatasetSequence
+   *          true if dataset sequence and frame of reference were left
+   *          unchanged by the Cut
+   */
+  final static void undoCutFeatures(Edit command, SequenceI seq,
+          final int start, final int length, boolean sameDatasetSequence)
   {
-    SequenceI seq = command.seqs[index];
     SequenceI sequence = seq.getDatasetSequence();
     if (sequence == null)
     {
       sequence = seq;
     }
 
-    if (insert)
+    /*
+     * shift right features that lie to the right of the restored cut (but not 
+     * if dataset sequence unchanged - so coordinates were changed by Cut)
+     */
+    if (!sameDatasetSequence)
     {
-      if (command.editedFeatures != null
-              && command.editedFeatures.containsKey(seq))
+      /*
+       * shift right all features right of and not 
+       * contiguous with the cut position
+       */
+      seq.getFeatures().shiftFeatures(start + 1, length);
+
+      /*
+       * shift right any features that start at the cut position,
+       * unless they were truncated
+       */
+      List<SequenceFeature> sfs = seq.getFeatures().findFeatures(start,
+              start);
+      for (SequenceFeature sf : sfs)
       {
-        sequence.setSequenceFeatures(command.editedFeatures.get(seq));
+        if (sf.getBegin() == start)
+        {
+          if (!command.truncatedFeatures.containsKey(seq)
+                  || !command.truncatedFeatures.get(seq).contains(sf))
+          {
+            /*
+             * feature was shifted left to cut position (not truncated),
+             * so shift it back right
+             */
+            SequenceFeature shifted = new SequenceFeature(sf, sf.getBegin()
+                    + length, sf.getEnd() + length, sf.getFeatureGroup(),
+                    sf.getScore());
+            seq.addSequenceFeature(shifted);
+            seq.deleteFeature(sf);
+          }
+        }
       }
-
-      return;
-    }
-
-    SequenceFeature[] sf = sequence.getSequenceFeatures();
-
-    if (sf == null)
-    {
-      return;
     }
 
-    SequenceFeature[] oldsf = new SequenceFeature[sf.length];
-
-    int cSize = j - i;
-
-    for (int s = 0; s < sf.length; s++)
+    /*
+     * restore any features that were deleted or truncated
+     */
+    if (command.deletedFeatures != null
+            && command.deletedFeatures.containsKey(seq))
     {
-      SequenceFeature copy = new SequenceFeature(sf[s]);
-
-      oldsf[s] = copy;
-
-      if (sf[s].getEnd() < i)
+      for (SequenceFeature deleted : command.deletedFeatures.get(seq))
       {
-        continue;
-      }
-
-      if (sf[s].getBegin() > j)
-      {
-        sf[s].setBegin(copy.getBegin() - cSize);
-        sf[s].setEnd(copy.getEnd() - cSize);
-        continue;
-      }
-
-      if (sf[s].getBegin() >= i)
-      {
-        sf[s].setBegin(i);
-      }
-
-      if (sf[s].getEnd() < j)
-      {
-        sf[s].setEnd(j - 1);
-      }
-
-      sf[s].setEnd(sf[s].getEnd() - (cSize));
-
-      if (sf[s].getBegin() > sf[s].getEnd())
-      {
-        sequence.deleteFeature(sf[s]);
+        sequence.addSequenceFeature(deleted);
       }
     }
 
-    if (command.editedFeatures == null)
+    /*
+     * delete any truncated features
+     */
+    if (command.truncatedFeatures != null
+            && command.truncatedFeatures.containsKey(seq))
     {
-      command.editedFeatures = new Hashtable<SequenceI, SequenceFeature[]>();
+      for (SequenceFeature amended : command.truncatedFeatures.get(seq))
+      {
+        sequence.deleteFeature(amended);
+      }
     }
-
-    command.editedFeatures.put(seq, oldsf);
-
   }
 
   /**
@@ -1212,7 +1361,7 @@ public class EditCommand implements CommandI
    */
   public Map<SequenceI, SequenceI> priorState(boolean forUndo)
   {
-    Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
+    Map<SequenceI, SequenceI> result = new HashMap<>();
     if (getEdits() == null)
     {
       return result;
@@ -1245,7 +1394,7 @@ public class EditCommand implements CommandI
      * Work backwards through the edit list, deriving the sequences before each
      * was applied. The final result is the sequence set before any edits.
      */
-    Iterator<Edit> editList = new ReverseListIterator<Edit>(getEdits());
+    Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
     while (editList.hasNext())
     {
       Edit oldEdit = editList.next();
@@ -1294,75 +1443,92 @@ public class EditCommand implements CommandI
 
   public class Edit
   {
-    public SequenceI[] oldds;
+    private SequenceI[] oldds;
 
-    boolean fullAlignmentHeight = false;
+    /**
+     * start and end of sequence prior to edit
+     */
+    private Range[] oldStartEnd;
 
-    Hashtable<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
+    private boolean fullAlignmentHeight = false;
 
-    Hashtable<String, Annotation[]> deletedAnnotations;
+    private Map<SequenceI, AlignmentAnnotation[]> deletedAnnotationRows;
 
-    Hashtable<SequenceI, SequenceFeature[]> editedFeatures;
+    private Map<String, Annotation[]> deletedAnnotations;
 
-    AlignmentI al;
+    /*
+     * features deleted by the cut (re-add on Undo)
+     * (including the original of any shortened features)
+     */
+    private Map<SequenceI, List<SequenceFeature>> deletedFeatures;
 
-    Action command;
+    /*
+     * shortened features added by the cut (delete on Undo)
+     */
+    private Map<SequenceI, List<SequenceFeature>> truncatedFeatures;
+
+    private AlignmentI al;
+
+    final private Action command;
 
     char[][] string;
 
     SequenceI[] seqs;
 
-    int[] alIndex;
+    private int[] alIndex;
+
+    private int position;
 
-    int position, number;
+    private int number;
 
-    char gapChar;
+    private char gapChar;
 
-    public Edit(Action command, SequenceI[] seqs, int position, int number,
-            char gapChar)
+    public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            char gap)
     {
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.gapChar = gapChar;
+      this.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      this.gapChar = gap;
     }
 
-    Edit(Action command, SequenceI[] seqs, int position, int number,
-            AlignmentI al)
+    Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            AlignmentI align)
     {
-      this.gapChar = al.getGapCharacter();
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.al = al;
-
-      alIndex = new int[seqs.length];
-      for (int i = 0; i < seqs.length; i++)
+      this(cmd, sqs, pos, count, align.getGapCharacter());
+
+      this.al = align;
+
+      alIndex = new int[sqs.length];
+      for (int i = 0; i < sqs.length; i++)
       {
-        alIndex[i] = al.findIndex(seqs[i]);
+        alIndex[i] = align.findIndex(sqs[i]);
       }
 
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
+      fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
-    Edit(Action command, SequenceI[] seqs, int position, int number,
-            AlignmentI al, String replace)
+    /**
+     * Constructor given a REPLACE command and the replacement string
+     * 
+     * @param cmd
+     * @param sqs
+     * @param pos
+     * @param count
+     * @param align
+     * @param replace
+     */
+    Edit(Action cmd, SequenceI[] sqs, int pos, int count,
+            AlignmentI align, String replace)
     {
-      this.command = command;
-      this.seqs = seqs;
-      this.position = position;
-      this.number = number;
-      this.al = al;
-      this.gapChar = al.getGapCharacter();
-      string = new char[seqs.length][];
-      for (int i = 0; i < seqs.length; i++)
+      this(cmd, sqs, pos, count, align);
+
+      string = new char[sqs.length][];
+      for (int i = 0; i < sqs.length; i++)
       {
         string[i] = replace.toCharArray();
       }
-
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
     }
 
     public SequenceI[] getSequences()
@@ -1406,7 +1572,163 @@ public class EditCommand implements CommandI
     }
     else
     {
-      return new ReverseListIterator<Edit>(getEdits());
+      return new ReverseListIterator<>(getEdits());
+    }
+  }
+
+  /**
+   * Adjusts features for Cut, and saves details of changes made to allow Undo
+   * <ul>
+   * <li>features left of the cut are unchanged</li>
+   * <li>features right of the cut are shifted left</li>
+   * <li>features internal to the cut region are deleted</li>
+   * <li>features that overlap or span the cut are shortened</li>
+   * <li>the originals of any deleted or shortened features are saved, to re-add
+   * on Undo</li>
+   * <li>any added (shortened) features are saved, to delete on Undo</li>
+   * </ul>
+   * 
+   * @param command
+   * @param seq
+   * @param fromPosition
+   * @param toPosition
+   * @param cutIsInternal
+   */
+  protected static void cutFeatures(Edit command, SequenceI seq,
+          int fromPosition, int toPosition, boolean cutIsInternal)
+  {
+    /* 
+     * if the cut is at start or end of sequence
+     * then we don't modify the sequence feature store
+     */
+    if (!cutIsInternal)
+    {
+      return;
+    }
+    List<SequenceFeature> added = new ArrayList<>();
+    List<SequenceFeature> removed = new ArrayList<>();
+  
+    SequenceFeaturesI featureStore = seq.getFeatures();
+    if (toPosition < fromPosition || featureStore == null)
+    {
+      return;
+    }
+  
+    int cutStartPos = fromPosition;
+    int cutEndPos = toPosition;
+    int cutWidth = cutEndPos - cutStartPos + 1;
+  
+    synchronized (featureStore)
+    {
+      /*
+       * get features that overlap the cut region
+       */
+      List<SequenceFeature> toAmend = featureStore.findFeatures(
+              cutStartPos, cutEndPos);
+  
+      /*
+       * add any contact features that span the cut region
+       * (not returned by findFeatures)
+       */
+      for (SequenceFeature contact : featureStore.getContactFeatures())
+      {
+        if (contact.getBegin() < cutStartPos
+                && contact.getEnd() > cutEndPos)
+        {
+          toAmend.add(contact);
+        }
+      }
+
+      /*
+       * adjust start-end of overlapping features;
+       * delete features enclosed by the cut;
+       * delete partially overlapping contact features
+       */
+      for (SequenceFeature sf : toAmend)
+      {
+        int sfBegin = sf.getBegin();
+        int sfEnd = sf.getEnd();
+        int newBegin = sfBegin;
+        int newEnd = sfEnd;
+        boolean toDelete = false;
+        boolean follows = false;
+        
+        if (sfBegin >= cutStartPos && sfEnd <= cutEndPos)
+        {
+          /*
+           * feature lies within cut region - delete it
+           */
+          toDelete = true;
+        }
+        else if (sfBegin < cutStartPos && sfEnd > cutEndPos)
+        {
+          /*
+           * feature spans cut region - left-shift the end
+           */
+          newEnd -= cutWidth;
+        }
+        else if (sfEnd <= cutEndPos)
+        {
+          /*
+           * feature overlaps left of cut region - truncate right
+           */
+          newEnd = cutStartPos - 1;
+          if (sf.isContactFeature())
+          {
+            toDelete = true;
+          }
+        }
+        else if (sfBegin >= cutStartPos)
+        {
+          /*
+           * remaining case - feature overlaps right
+           * truncate left, adjust end of feature
+           */
+          newBegin = cutIsInternal ? cutStartPos : cutEndPos + 1;
+          newEnd = newBegin + sfEnd - cutEndPos - 1;
+          if (sf.isContactFeature())
+          {
+            toDelete = true;
+          }
+        }
+  
+        seq.deleteFeature(sf);
+        if (!follows)
+        {
+          removed.add(sf);
+        }
+        if (!toDelete)
+        {
+          SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+                  sf.getFeatureGroup(), sf.getScore());
+          seq.addSequenceFeature(copy);
+          if (!follows)
+          {
+            added.add(copy);
+          }
+        }
+      }
+  
+      /*
+       * and left shift any features lying to the right of the cut region
+       */
+
+      featureStore.shiftFeatures(cutEndPos + 1, -cutWidth);
+    }
+
+    /*
+     * save deleted and amended features, so that Undo can 
+     * re-add or delete them respectively
+     */
+    if (command.deletedFeatures == null)
+    {
+      command.deletedFeatures = new HashMap<>();
+    }
+    if (command.truncatedFeatures == null)
+    {
+      command.truncatedFeatures = new HashMap<>();
     }
+    command.deletedFeatures.put(seq, removed);
+    command.truncatedFeatures.put(seq, added);
   }
 }
index 24fc181..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,102 +214,87 @@ 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)
   {
-    final int startPosition = sqcol.getStartRes() + 1; // converted to base 1
-    final int endPosition = sqcol.getEndRes() + 1;
+    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();
     int nseq = 0;
     for (SequenceI sq : seqs)
     {
-      boolean sequenceHasFeature = false;
       if (sq != null)
       {
-        SequenceFeature[] sfs = sq.getSequenceFeatures();
-        if (sfs != null)
+        // int ist = sq.findPosition(sqcol.getStartRes());
+        List<SequenceFeature> sfs = sq.findFeatures(startColumn,
+                endColumn, featureType);
+
+        boolean found = false;
+        for (SequenceFeature sf : sfs)
         {
-          int ist = sq.findIndex(sq.getStart());
-          int iend = sq.findIndex(sq.getEnd());
-          if (iend < startPosition || ist > endPosition)
+          if (fr.getColour(sf) == null)
           {
-            // sequence not in region
             continue;
           }
-          for (SequenceFeature sf : sfs)
+          if (!found)
           {
-            // future functionality - featureType == null means mark columns
-            // containing all displayed features
-            if (sf != null && (featureType.equals(sf.getType())))
+            nseq++;
+          }
+          found = true;
+
+          int sfStartCol = sq.findIndex(sf.getBegin());
+          int sfEndCol = sq.findIndex(sf.getEnd());
+
+          if (sf.isContactFeature())
+          {
+            /*
+             * 'contact' feature - check for 'start' or 'end'
+             * position within the selected region
+             */
+            if (sfStartCol >= startColumn && sfStartCol <= endColumn)
+            {
+              bs.set(sfStartCol - 1);
+            }
+            if (sfEndCol >= startColumn && sfEndCol <= endColumn)
             {
-              // optimisation - could consider 'spos,apos' like cursor argument
-              // - findIndex wastes time by starting from first character and
-              // counting
-
-              int sfStartCol = sq.findIndex(sf.getBegin());
-              int sfEndCol = sq.findIndex(sf.getEnd());
-
-              if (sf.isContactFeature())
-              {
-                /*
-                 * 'contact' feature - check for 'start' or 'end'
-                 * position within the selected region
-                 */
-                if (sfStartCol >= startPosition
-                        && sfStartCol <= endPosition)
-                {
-                  bs.set(sfStartCol - 1);
-                  sequenceHasFeature = true;
-                }
-                if (sfEndCol >= startPosition && sfEndCol <= endPosition)
-                {
-                  bs.set(sfEndCol - 1);
-                  sequenceHasFeature = true;
-                }
-                continue;
-              }
-
-              /*
-               * contiguous feature - select feature positions (if any) 
-               * within the selected region
-               */
-              if (sfStartCol > endPosition || sfEndCol < startPosition)
-              {
-                // feature is outside selected region
-                continue;
-              }
-              sequenceHasFeature = true;
-              if (sfStartCol < startPosition)
-              {
-                sfStartCol = startPosition;
-              }
-              if (sfStartCol < ist)
-              {
-                sfStartCol = ist;
-              }
-              if (sfEndCol > endPosition)
-              {
-                sfEndCol = endPosition;
-              }
-              for (; sfStartCol <= sfEndCol; sfStartCol++)
-              {
-                bs.set(sfStartCol - 1); // convert to base 0
-              }
+              bs.set(sfEndCol - 1);
             }
+            continue;
           }
-        }
 
-        if (sequenceHasFeature)
-        {
-          nseq++;
+          /*
+           * contiguous feature - select feature positions (if any) 
+           * within the selected region
+           */
+          if (sfStartCol < startColumn)
+          {
+            sfStartCol = startColumn;
+          }
+          // not sure what the point of this is
+          // if (sfStartCol < ist)
+          // {
+          // sfStartCol = ist;
+          // }
+          if (sfEndCol > endColumn)
+          {
+            sfEndCol = endColumn;
+          }
+          for (; sfStartCol <= sfEndCol; sfStartCol++)
+          {
+            bs.set(sfStartCol - 1); // convert to base 0
+          }
         }
       }
     }
@@ -362,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);
 
   }
 
@@ -376,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;
 
   }
 
@@ -439,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()
@@ -463,7 +439,7 @@ public class AlignViewController implements AlignViewControllerI
       if (!extendCurrent)
       {
         cs.clear();
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(false, false);
       }
     }
     return false;
index 83eeb3d..ec11fc1 100644 (file)
@@ -505,12 +505,11 @@ public class AlignedCodonFrame
          * Read off the mapped nucleotides (converting to position base 0)
          */
         codonPos = MappingUtils.flattenRanges(codonPos);
-        char[] dna = dnaSeq.getSequence();
         int start = dnaSeq.getStart();
-        result.add(
-                new char[]
-                { dna[codonPos[0] - start], dna[codonPos[1] - start],
-                    dna[codonPos[2] - start] });
+        char c1 = dnaSeq.getCharAt(codonPos[0] - start);
+        char c2 = dnaSeq.getCharAt(codonPos[1] - start);
+        char c3 = dnaSeq.getCharAt(codonPos[2] - start);
+        result.add(new char[] { c1, c2, c3 });
       }
     }
     return result.isEmpty() ? null : result;
index 5733719..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!
@@ -1471,8 +1500,8 @@ public class Alignment implements AlignmentI
   {
     // TODO JAL-1270 needs test coverage
     // currently tested for use in jalview.gui.SequenceFetcher
-    boolean samegap = toappend.getGapCharacter() == getGapCharacter();
     char oldc = toappend.getGapCharacter();
+    boolean samegap = oldc == getGapCharacter();
     boolean hashidden = toappend.getHiddenSequences() != null
             && toappend.getHiddenSequences().hiddenSequences != null;
     // get all sequences including any hidden ones
@@ -1490,14 +1519,7 @@ public class Alignment implements AlignmentI
         {
           if (!samegap)
           {
-            char[] oldseq = addedsq.getSequence();
-            for (int c = 0; c < oldseq.length; c++)
-            {
-              if (oldseq[c] == oldc)
-              {
-                oldseq[c] = gapCharacter;
-              }
-            }
+            addedsq.replace(oldc, gapCharacter);
           }
           toappendsq.add(addedsq);
         }
@@ -1609,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)
     {
@@ -1624,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
@@ -1920,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 6bbd566..ee9389c 100755 (executable)
@@ -24,6 +24,8 @@ import jalview.analysis.Rna;
 import jalview.analysis.SecStrConsensus.SimpleBP;
 import jalview.analysis.WUSSParseException;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -96,14 +98,13 @@ public class AlignmentAnnotation
    * Updates the _rnasecstr field Determines the positions that base pair and
    * the positions of helices based on secondary structure from a Stockholm file
    * 
-   * @param RNAannot
+   * @param rnaAnnotation
    */
-  private void _updateRnaSecStr(CharSequence RNAannot)
+  private void _updateRnaSecStr(CharSequence rnaAnnotation)
   {
     try
     {
-      bps = Rna.getModeleBP(RNAannot);
-      _rnasecstr = Rna.getBasePairs(bps);
+      _rnasecstr = Rna.getHelixMap(rnaAnnotation);
       invalidrnastruc = -1;
     } catch (WUSSParseException px)
     {
@@ -114,8 +115,6 @@ public class AlignmentAnnotation
     {
       return;
     }
-    Rna.HelixMap(_rnasecstr);
-    // setRNAStruc(RNAannot);
 
     if (_rnasecstr != null && _rnasecstr.length > 0)
     {
@@ -166,6 +165,64 @@ public class AlignmentAnnotation
   }
 
   /**
+   * Get the RNA Secondary Structure SequenceFeature Array if present
+   */
+  public SequenceFeature[] getRnaSecondaryStructure()
+  {
+    return this._rnasecstr;
+  }
+
+  /**
+   * Check the RNA Secondary Structure is equivalent to one in given
+   * AlignmentAnnotation param
+   */
+  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that)
+  {
+    return rnaSecondaryStructureEquivalent(that, true);
+  }
+
+  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that, boolean compareType)
+  {
+    SequenceFeature[] thisSfArray = this.getRnaSecondaryStructure();
+    SequenceFeature[] thatSfArray = that.getRnaSecondaryStructure();
+    if (thisSfArray == null || thatSfArray == null)
+    {
+      return thisSfArray == null && thatSfArray == null;
+    }
+    if (thisSfArray.length != thatSfArray.length)
+    {
+      return false;
+    }
+    Arrays.sort(thisSfArray, new SFSortByEnd()); // probably already sorted
+                                                   // like this
+    Arrays.sort(thatSfArray, new SFSortByEnd()); // probably already sorted
+                                                   // like this
+    for (int i=0; i < thisSfArray.length; i++) {
+      SequenceFeature thisSf = thisSfArray[i];
+      SequenceFeature thatSf = thatSfArray[i];
+      if (compareType) {
+        if (thisSf.getType() == null || thatSf.getType() == null) {
+          if (thisSf.getType() == null && thatSf.getType() == null) {
+            continue;
+          } else {
+            return false;
+          }
+        }
+        if (! thisSf.getType().equals(thatSf.getType())) {
+          return false;
+        }
+      }
+      if (!(thisSf.getBegin() == thatSf.getBegin()
+              && thisSf.getEnd() == thatSf.getEnd()))
+      {
+        return false;
+      }
+    }
+    return true;
+
+  }
+
+  /**
    * map of positions in the associated annotation
    */
   private Map<Integer, Annotation> sequenceMapping;
@@ -244,19 +301,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"))
@@ -273,12 +317,6 @@ public class AlignmentAnnotation
     }
   }
 
-  // JBPNote: what does this do ?
-  public void ConcenStru(CharSequence RNAannot) throws WUSSParseException
-  {
-    bps = Rna.getModeleBP(RNAannot);
-  }
-
   /**
    * Creates a new AlignmentAnnotation object.
    * 
@@ -315,6 +353,7 @@ public class AlignmentAnnotation
     char firstChar = 0;
     for (int i = 0; i < annotations.length; i++)
     {
+      // DEBUG System.out.println(i + ": " + annotations[i]);
       if (annotations[i] == null)
       {
         continue;
@@ -322,12 +361,15 @@ public class AlignmentAnnotation
       if (annotations[i].secondaryStructure == 'H'
               || annotations[i].secondaryStructure == 'E')
       {
+        // DEBUG System.out.println( "/H|E/ '" +
+        // annotations[i].secondaryStructure + "'");
         hasIcons |= true;
       }
       else
       // Check for RNA secondary structure
       {
-        // System.out.println(annotations[i].secondaryStructure);
+        // DEBUG System.out.println( "/else/ '" +
+        // annotations[i].secondaryStructure + "'");
         // TODO: 2.8.2 should this ss symbol validation check be a function in
         // RNA/ResidueProperties ?
         if (annotations[i].secondaryStructure == '('
@@ -338,10 +380,12 @@ public class AlignmentAnnotation
                 || annotations[i].secondaryStructure == 'B'
                 || annotations[i].secondaryStructure == 'C'
                 || annotations[i].secondaryStructure == 'D'
-                || annotations[i].secondaryStructure == 'E'
+                // || annotations[i].secondaryStructure == 'E' // ambiguous on
+                // its own -- already checked above
                 || annotations[i].secondaryStructure == 'F'
                 || annotations[i].secondaryStructure == 'G'
-                || annotations[i].secondaryStructure == 'H'
+                // || annotations[i].secondaryStructure == 'H' // ambiguous on
+                // its own -- already checked above
                 || annotations[i].secondaryStructure == 'I'
                 || annotations[i].secondaryStructure == 'J'
                 || annotations[i].secondaryStructure == 'K'
@@ -388,7 +432,7 @@ public class AlignmentAnnotation
         // &&
         // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
                 firstChar != ' ' && firstChar != '$' && firstChar != 0xCE
-                && firstChar != '(' && firstChar != '[' && firstChar != '>'
+                && firstChar != '(' && firstChar != '[' && firstChar != '<'
                 && firstChar != '{' && firstChar != 'A' && firstChar != 'B'
                 && firstChar != 'C' && firstChar != 'D' && firstChar != 'E'
                 && firstChar != 'F' && firstChar != 'G' && firstChar != 'H'
@@ -687,7 +731,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());
@@ -723,7 +767,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())
@@ -803,7 +847,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())
         {
@@ -930,7 +974,7 @@ public class AlignmentAnnotation
     {
       return;
     }
-    sequenceMapping = new HashMap<Integer, Annotation>();
+    sequenceMapping = new HashMap<>();
 
     int seqPos;
 
@@ -1145,7 +1189,7 @@ public class AlignmentAnnotation
     {
       return;
     }
-    hidden.makeVisibleAnnotation(this);
+    makeVisibleAnnotation(hidden);
   }
 
   public void setPadGaps(boolean padgaps, char gapchar)
@@ -1211,7 +1255,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
@@ -1257,7 +1301,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)
@@ -1310,7 +1354,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())
       {
@@ -1368,7 +1412,7 @@ public class AlignmentAnnotation
   {
     if (properties == null)
     {
-      properties = new HashMap<String, String>();
+      properties = new HashMap<>();
     }
     properties.put(property, value);
   }
@@ -1493,4 +1537,181 @@ 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 ae29417..f6919cd 100755 (executable)
@@ -210,7 +210,10 @@ public class Annotation
     return ((value == 0f)
             && ((description == null) || (description.trim().length() == 0))
             && ((displayCharacter == null)
-                    || (displayCharacter.trim().length() == 0))
+                    || (displayCharacter.trim().length() == 0)
+                    || (displayCharacter.equals(" ."))) // RNA Stockholm blank
+                                                        // displayCharacter can
+                                                        // end up like this
             && (secondaryStructure == '\0' || (secondaryStructure == ' '))
             && colour == null);
   }
index 53bffa9..c33abb3 100755 (executable)
@@ -70,7 +70,7 @@ public class BinarySequence extends Sequence
     int nores = (isNa) ? ResidueProperties.maxNucleotideIndex
             : ResidueProperties.maxProteinIndex;
 
-    dbinary = new double[getSequence().length * nores];
+    dbinary = new double[getLength() * nores];
 
     return nores;
   }
@@ -88,7 +88,7 @@ public class BinarySequence extends Sequence
   {
     int nores = initMatrixGetNoRes();
     final int[] sindex = getSymbolmatrix();
-    for (int i = 0; i < getSequence().length; i++)
+    for (int i = 0; i < getLength(); i++)
     {
       int aanum = nores - 1;
 
@@ -132,7 +132,7 @@ public class BinarySequence extends Sequence
   {
     int nores = initMatrixGetNoRes();
 
-    for (int i = 0, iSize = getSequence().length; i < iSize; i++)
+    for (int i = 0, iSize = getLength(); i < iSize; i++)
     {
       int aanum = nores - 1;
 
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);
       }
similarity index 86%
rename from src/jalview/io/ClansFile.java
rename to src/jalview/datamodel/ContiguousI.java
index d0b1c72..a9b1372 100644 (file)
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.io;
+package jalview.datamodel;
 
-/**
- * Read or write a CLANS style score matrix file.
- */
-
-public class ClansFile extends FileParse
+public interface ContiguousI
 {
+  int getBegin(); // todo want long for genomic positions?
 
+  int getEnd();
 }
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 fe396ce..b5184fb 100644 (file)
@@ -46,7 +46,7 @@ public class Mapping
     /*
      * The characters of the aligned sequence e.g. "-cGT-ACgTG-"
      */
-    private final char[] alignedSeq;
+    private final SequenceI alignedSeq;
 
     /*
      * the sequence start residue
@@ -102,7 +102,7 @@ public class Mapping
      */
     public AlignedCodonIterator(SequenceI seq, char gapChar)
     {
-      this.alignedSeq = seq.getSequence();
+      this.alignedSeq = seq;
       this.start = seq.getStart();
       this.gap = gapChar;
       fromRanges = map.getFromRanges().iterator();
@@ -176,7 +176,7 @@ public class Mapping
       if (toPosition <= currentToRange[1])
       {
         SequenceI seq = Mapping.this.to;
-        char pep = seq.getSequence()[toPosition - seq.getStart()];
+        char pep = seq.getCharAt(toPosition - seq.getStart());
         toPosition++;
         return String.valueOf(pep);
       }
@@ -257,9 +257,10 @@ public class Mapping
        * allow for offset e.g. treat pos 8 as 2 if sequence starts at 7
        */
       int truePos = sequencePos - (start - 1);
-      while (alignedBases < truePos && alignedColumn < alignedSeq.length)
+      int length = alignedSeq.getLength();
+      while (alignedBases < truePos && alignedColumn < length)
       {
-        char c = alignedSeq[alignedColumn++];
+        char c = alignedSeq.getCharAt(alignedColumn++);
         if (c != gap && !Comparison.isGap(c))
         {
           alignedBases++;
@@ -530,9 +531,8 @@ public class Mapping
         SequenceFeature[] vf = new SequenceFeature[frange.length / 2];
         for (int i = 0, v = 0; i < frange.length; i += 2, v++)
         {
-          vf[v] = new SequenceFeature(f);
-          vf[v].setBegin(frange[i]);
-          vf[v].setEnd(frange[i + 1]);
+          vf[v] = new SequenceFeature(f, frange[i], frange[i + 1],
+                  f.getFeatureGroup(), f.getScore());
           if (frange.length > 2)
           {
             vf[v].setDescription(f.getDescription() + "\nPart " + (v + 1));
@@ -541,27 +541,7 @@ public class Mapping
         return vf;
       }
     }
-    if (false) // else
-    {
-      int[] word = getWord(f.getBegin());
-      if (word[0] < word[1])
-      {
-        f.setBegin(word[0]);
-      }
-      else
-      {
-        f.setBegin(word[1]);
-      }
-      word = getWord(f.getEnd());
-      if (word[0] > word[1])
-      {
-        f.setEnd(word[0]);
-      }
-      else
-      {
-        f.setEnd(word[1]);
-      }
-    }
+
     // give up and just return the feature.
     return new SequenceFeature[] { f };
   }
@@ -713,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
diff --git a/src/jalview/datamodel/Range.java b/src/jalview/datamodel/Range.java
new file mode 100644 (file)
index 0000000..8b6f617
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+/**
+ * An immutable data bean that models a start-end range
+ */
+public class Range implements ContiguousI
+{
+  public final int start;
+
+  public final int end;
+
+  @Override
+  public int getBegin()
+  {
+    return start;
+  }
+
+  @Override
+  public int getEnd()
+  {
+    return end;
+  }
+
+  public Range(int i, int j)
+  {
+    start = i;
+    end = j;
+  }
+
+  @Override
+  public String toString()
+  {
+    return String.valueOf(start) + "-" + String.valueOf(end);
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return start * 31 + end;
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj instanceof Range)
+    {
+      Range r = (Range) obj;
+      return (start == r.start && end == r.end);
+    }
+    return false;
+  }
+}
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 a270e37..d1e3fcc 100755 (executable)
@@ -34,7 +34,7 @@ import java.util.List;
 public class SearchResults implements SearchResultsI
 {
 
-  private List<SearchResultMatchI> matches = new ArrayList<SearchResultMatchI>();
+  private List<SearchResultMatchI> matches = new ArrayList<>();
 
   /**
    * One match consists of a sequence reference, start and end positions.
@@ -42,17 +42,17 @@ public class SearchResults implements SearchResultsI
    */
   public class Match implements SearchResultMatchI
   {
-    SequenceI sequence;
+    final SequenceI sequence;
 
     /**
      * Start position of match in sequence (base 1)
      */
-    int start;
+    final int start;
 
     /**
      * End position (inclusive) (base 1)
      */
-    int end;
+    final int end;
 
     /**
      * create a Match on a range of sequence. Match always holds region in
@@ -133,11 +133,6 @@ public class SearchResults implements SearchResultsI
       return sb.toString();
     }
 
-    public void setSequence(SequenceI seq)
-    {
-      this.sequence = seq;
-    }
-
     /**
      * Hashcode is the hashcode of the matched sequence plus a hash of start and
      * end positions. Match objects that pass the test for equals are guaranteed
@@ -219,20 +214,15 @@ public class SearchResults implements SearchResultsI
       m = (Match) _m;
 
       mfound = false;
-      if (m.sequence == sequence)
-      {
-        mfound = true;
-        // locate aligned position
-        matchStart = sequence.findIndex(m.start) - 1;
-        matchEnd = sequence.findIndex(m.end) - 1;
-      }
-      else if (m.sequence == sequence.getDatasetSequence())
+      if (m.sequence == sequence
+              || m.sequence == sequence.getDatasetSequence())
       {
         mfound = true;
-        // locate region in local context
         matchStart = sequence.findIndex(m.start) - 1;
-        matchEnd = sequence.findIndex(m.end) - 1;
+        matchEnd = m.start == m.end ? matchStart : sequence
+                .findIndex(m.end) - 1;
       }
+
       if (mfound)
       {
         if (matchStart <= end && matchEnd >= start)
@@ -277,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)
@@ -363,4 +356,10 @@ public class SearchResults implements SearchResultsI
     SearchResultsI sr = (SearchResultsI) obj;
     return matches.equals(sr.getResults());
   }
+
+  @Override
+  public void addSearchResults(SearchResultsI toAdd)
+  {
+    matches.addAll(toAdd.getResults());
+  }
 }
index 52a0467..c3dc0e8 100644 (file)
@@ -44,6 +44,13 @@ public interface SearchResultsI
   SearchResultMatchI addResult(SequenceI seq, int start, int end);
 
   /**
+   * adds all match results in the argument to this set
+   * 
+   * @param toAdd
+   */
+  void addSearchResults(SearchResultsI toAdd);
+
+  /**
    * Answers true if the search results include the given sequence (or its
    * dataset sequence), else false
    * 
index 0103237..f555855 100755 (executable)
@@ -22,6 +22,8 @@ package jalview.datamodel;
 
 import jalview.analysis.AlignSeq;
 import jalview.api.DBRefEntryI;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.datamodel.features.SequenceFeaturesI;
 import jalview.util.Comparison;
 import jalview.util.DBRefUtils;
 import jalview.util.MapList;
@@ -32,17 +34,16 @@ 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 fr.orsay.lri.varna.models.rna.RNA;
 
 /**
  * 
- * Implements the SequenceI interface for a char[] based sequence object.
- * 
- * @author $author$
- * @version $Revision$
+ * Implements the SequenceI interface for a char[] based sequence object
  */
 public class Sequence extends ASequence implements SequenceI
 {
@@ -74,15 +75,22 @@ public class Sequence extends ASequence implements SequenceI
    */
   Vector<AlignmentAnnotation> annotation;
 
-  /**
-   * The index of the sequence in a MSA
+  private SequenceFeaturesI sequenceFeatureStore;
+
+  /*
+   * A cursor holding the approximate current view position to the sequence,
+   * as determined by findIndex or findPosition or findPositions.
+   * Using a cursor as a hint allows these methods to be more performant for
+   * large sequences.
    */
-  int index = -1;
+  private SequenceCursor cursor;
 
-  /**
-   * array of sequence features - may not be null for a valid sequence object
+  /*
+   * A number that should be incremented whenever the sequence is edited.
+   * If the value matches the cursor token, then we can trust the cursor,
+   * if not then it should be recomputed. 
    */
-  public SequenceFeature[] sequenceFeatures;
+  private int changeCount;
 
   /**
    * Creates a new Sequence object.
@@ -99,11 +107,13 @@ public class Sequence extends ASequence implements SequenceI
    */
   public Sequence(String name, String sequence, int start, int end)
   {
+    this();
     initSeqAndName(name, sequence.toCharArray(), start, end);
   }
 
   public Sequence(String name, char[] sequence, int start, int end)
   {
+    this();
     initSeqAndName(name, sequence, start, end);
   }
 
@@ -127,11 +137,10 @@ public class Sequence extends ASequence implements SequenceI
     checkValidRange();
   }
 
-  com.stevesoft.pat.Regex limitrx = new com.stevesoft.pat.Regex(
-          "[/][0-9]{1,}[-][0-9]{1,}$");
-
-  com.stevesoft.pat.Regex endrx = new com.stevesoft.pat.Regex("[0-9]{1,}$");
-
+  /**
+   * 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)
@@ -140,17 +149,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 :
@@ -159,7 +188,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++;
         }
@@ -178,6 +207,14 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
+   * default constructor
+   */
+  private Sequence()
+  {
+    sequenceFeatureStore = new SequenceFeatures();
+  }
+
+  /**
    * Creates a new Sequence object.
    * 
    * @param name
@@ -216,8 +253,8 @@ public class Sequence extends ASequence implements SequenceI
    */
   public Sequence(SequenceI seq, AlignmentAnnotation[] alAnnotation)
   {
+    this();
     initSeqFrom(seq, alAnnotation);
-
   }
 
   /**
@@ -233,33 +270,38 @@ public class Sequence extends ASequence implements SequenceI
   protected void initSeqFrom(SequenceI seq,
           AlignmentAnnotation[] alAnnotation)
   {
-    {
-      char[] oseq = seq.getSequence();
-      initSeqAndName(seq.getName(), Arrays.copyOf(oseq, oseq.length),
-              seq.getStart(), seq.getEnd());
-    }
+    char[] oseq = seq.getSequence(); // returns a copy of the array
+    initSeqAndName(seq.getName(), oseq, seq.getStart(), seq.getEnd());
+
     description = seq.getDescription();
     if (seq != datasetSequence)
     {
       setDatasetSequence(seq.getDatasetSequence());
     }
-    if (datasetSequence == null && seq.getDBRefs() != null)
+    
+    /*
+     * only copy DBRefs and seqfeatures if we really are a dataset sequence
+     */
+    if (datasetSequence == null)
     {
-      // only copy DBRefs and seqfeatures if we really are a dataset sequence
-      DBRefEntry[] dbr = seq.getDBRefs();
-      for (int i = 0; i < dbr.length; i++)
+      if (seq.getDBRefs() != null)
       {
-        addDBRef(new DBRefEntry(dbr[i]));
-      }
-      if (seq.getSequenceFeatures() != null)
-      {
-        SequenceFeature[] sf = seq.getSequenceFeatures();
-        for (int i = 0; i < sf.length; i++)
+        DBRefEntry[] dbr = seq.getDBRefs();
+        for (int i = 0; i < dbr.length; i++)
         {
-          addSequenceFeature(new SequenceFeature(sf[i]));
+          addDBRef(new DBRefEntry(dbr[i]));
         }
       }
+
+      /*
+       * make copies of any sequence features
+       */
+      for (SequenceFeature sf : seq.getSequenceFeatures())
+      {
+        addSequenceFeature(new SequenceFeature(sf));
+      }
     }
+
     if (seq.getAnnotation() != null)
     {
       AlignmentAnnotation[] sqann = seq.getAnnotation();
@@ -296,123 +338,67 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   @Override
-  public void setSequenceFeatures(SequenceFeature[] features)
+  public void setSequenceFeatures(List<SequenceFeature> features)
   {
-    if (datasetSequence == null)
-    {
-      sequenceFeatures = features;
-    }
-    else
+    if (datasetSequence != null)
     {
-      if (datasetSequence.getSequenceFeatures() != features
-              && datasetSequence.getSequenceFeatures() != null
-              && datasetSequence.getSequenceFeatures().length > 0)
-      {
-        new Exception(
-                "Warning: JAL-2046 side effect ? Possible implementation error: overwriting dataset sequence features by setting sequence features on alignment")
-                        .printStackTrace();
-      }
       datasetSequence.setSequenceFeatures(features);
+      return;
     }
+    sequenceFeatureStore = new SequenceFeatures(features);
   }
 
   @Override
   public synchronized boolean addSequenceFeature(SequenceFeature sf)
   {
-    if (sequenceFeatures == null && datasetSequence != null)
+    if (sf.getType() == null)
     {
-      return datasetSequence.addSequenceFeature(sf);
-    }
-    if (sequenceFeatures == null)
-    {
-      sequenceFeatures = new SequenceFeature[0];
+      System.err.println("SequenceFeature type may not be null: "
+              + sf.toString());
+      return false;
     }
 
-    for (int i = 0; i < sequenceFeatures.length; i++)
+    if (datasetSequence != null)
     {
-      if (sequenceFeatures[i].equals(sf))
-      {
-        return false;
-      }
+      return datasetSequence.addSequenceFeature(sf);
     }
 
-    SequenceFeature[] temp = new SequenceFeature[sequenceFeatures.length
-            + 1];
-    System.arraycopy(sequenceFeatures, 0, temp, 0, sequenceFeatures.length);
-    temp[sequenceFeatures.length] = sf;
-
-    sequenceFeatures = temp;
-    return true;
+    return sequenceFeatureStore.add(sf);
   }
 
   @Override
   public void deleteFeature(SequenceFeature sf)
   {
-    if (sequenceFeatures == null)
-    {
-      if (datasetSequence != null)
-      {
-        datasetSequence.deleteFeature(sf);
-      }
-      return;
-    }
-
-    int index = 0;
-    for (index = 0; index < sequenceFeatures.length; index++)
-    {
-      if (sequenceFeatures[index].equals(sf))
-      {
-        break;
-      }
-    }
-
-    if (index == sequenceFeatures.length)
-    {
-      return;
-    }
-
-    int sfLength = sequenceFeatures.length;
-    if (sfLength < 2)
+    if (datasetSequence != null)
     {
-      sequenceFeatures = null;
+      datasetSequence.deleteFeature(sf);
     }
     else
     {
-      SequenceFeature[] temp = new SequenceFeature[sfLength - 1];
-      System.arraycopy(sequenceFeatures, 0, temp, 0, index);
-
-      if (index < sfLength)
-      {
-        System.arraycopy(sequenceFeatures, index + 1, temp, index,
-                sequenceFeatures.length - index - 1);
-      }
-
-      sequenceFeatures = temp;
+      sequenceFeatureStore.delete(sf);
     }
   }
 
   /**
-   * Returns the sequence features (if any), looking first on the sequence, then
-   * on its dataset sequence, and so on until a non-null value is found (or
-   * none). This supports retrieval of sequence features stored on the sequence
-   * (as in the applet) or on the dataset sequence (as in the Desktop version).
+   * {@inheritDoc}
    * 
    * @return
    */
   @Override
-  public SequenceFeature[] getSequenceFeatures()
+  public List<SequenceFeature> getSequenceFeatures()
   {
-    SequenceFeature[] features = sequenceFeatures;
-
-    SequenceI seq = this;
-    int count = 0; // failsafe against loop in sequence.datasetsequence...
-    while (features == null && seq.getDatasetSequence() != null
-            && count++ < 10)
+    if (datasetSequence != null)
     {
-      seq = seq.getDatasetSequence();
-      features = ((Sequence) seq).sequenceFeatures;
+      return datasetSequence.getSequenceFeatures();
     }
-    return features;
+    return sequenceFeatureStore.getAllFeatures();
+  }
+
+  @Override
+  public SequenceFeaturesI getFeatures()
+  {
+    return datasetSequence != null ? datasetSequence.getFeatures()
+            : sequenceFeatureStore;
   }
 
   @Override
@@ -420,7 +406,7 @@ public class Sequence extends ASequence implements SequenceI
   {
     if (pdbIds == null)
     {
-      pdbIds = new Vector<PDBEntry>();
+      pdbIds = new Vector<>();
       pdbIds.add(entry);
       return true;
     }
@@ -456,7 +442,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;
   }
 
   /**
@@ -477,15 +463,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();
   }
 
@@ -568,6 +554,7 @@ public class Sequence extends ASequence implements SequenceI
   {
     this.sequence = seq.toCharArray();
     checkValidRange();
+    sequenceChanged();
   }
 
   @Override
@@ -585,7 +572,9 @@ public class Sequence extends ASequence implements SequenceI
   @Override
   public char[] getSequence()
   {
-    return sequence;
+    // return sequence;
+    return sequence == null ? null : Arrays.copyOf(sequence,
+            sequence.length);
   }
 
   /*
@@ -666,10 +655,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)
@@ -677,10 +666,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()
@@ -688,58 +734,382 @@ public class Sequence extends ASequence implements SequenceI
     return this.description;
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see jalview.datamodel.SequenceI#findIndex(int)
+  /**
+   * {@inheritDoc}
    */
   @Override
   public int findIndex(int pos)
   {
-    // returns the alignment position for a residue
+    /*
+     * use a valid, hopefully nearby, cursor if available
+     */
+    if (isValidCursor(cursor))
+    {
+      return findIndex(pos, cursor);
+    }
+
     int j = start;
     int i = 0;
-    // Rely on end being at least as long as the length of the sequence.
+    int startColumn = 0;
+
+    /*
+     * traverse sequence from the start counting gaps; make a note of
+     * the column of the first residue to save in the cursor
+     */
     while ((i < sequence.length) && (j <= end) && (j <= pos))
     {
-      if (!jalview.util.Comparison.isGap(sequence[i]))
+      if (!Comparison.isGap(sequence[i]))
       {
+        if (j == start)
+        {
+          startColumn = i;
+        }
         j++;
       }
-
       i++;
     }
 
-    if ((j == end) && (j < pos))
+    if (j == end && j < pos)
     {
       return end + 1;
     }
-    else
+
+    updateCursor(pos, i, startColumn);
+    return i;
+  }
+
+  /**
+   * Updates the cursor to the latest found residue and column position
+   * 
+   * @param residuePos
+   *          (start..)
+   * @param column
+   *          (1..)
+   * @param startColumn
+   *          column position of the first sequence residue
+   */
+  protected void updateCursor(int residuePos, int column, int startColumn)
+  {
+    /*
+     * preserve end residue column provided cursor was valid
+     */
+    int endColumn = isValidCursor(cursor) ? cursor.lastColumnPosition : 0;
+
+    if (residuePos == this.end)
+    {
+      endColumn = column;
+    }
+
+    cursor = new SequenceCursor(this, residuePos, column, startColumn,
+            endColumn, this.changeCount);
+  }
+
+  /**
+   * Answers the aligned column position (1..) for the given residue position
+   * (start..) given a 'hint' of a residue/column location in the neighbourhood.
+   * The hint may be left of, at, or to the right of the required position.
+   * 
+   * @param pos
+   * @param curs
+   * @return
+   */
+  protected int findIndex(final int pos, SequenceCursor curs)
+  {
+    if (!isValidCursor(curs))
+    {
+      /*
+       * wrong or invalidated cursor, compute de novo
+       */
+      return findIndex(pos);
+    }
+
+    if (curs.residuePosition == pos)
+    {
+      return curs.columnPosition;
+    }
+
+    /*
+     * move left or right to find pos from hint.position
+     */
+    int col = curs.columnPosition - 1; // convert from base 1 to base 0
+    int newPos = curs.residuePosition;
+    int delta = newPos > pos ? -1 : 1;
+
+    while (newPos != pos)
     {
-      return i;
+      col += delta; // shift one column left or right
+      if (col < 0)
+      {
+        break;
+      }
+      if (col == sequence.length)
+      {
+        col--; // return last column if we failed to reach pos
+        break;
+      }
+      if (!Comparison.isGap(sequence[col]))
+      {
+        newPos += delta;
+      }
     }
+
+    col++; // convert back to base 1
+
+    /*
+     * only update cursor if we found the target position
+     */
+    if (newPos == pos)
+    {
+      updateCursor(pos, col, curs.firstColumnPosition);
+    }
+
+    return col;
   }
 
+  /**
+   * {@inheritDoc}
+   */
   @Override
-  public int findPosition(int i)
+  public int findPosition(final int column)
   {
+    /*
+     * use a valid, hopefully nearby, cursor if available
+     */
+    if (isValidCursor(cursor))
+    {
+      return findPosition(column + 1, cursor);
+    }
+    
+    // TODO recode this more naturally i.e. count residues only
+    // as they are found, not 'in anticipation'
+
+    /*
+     * traverse the sequence counting gaps; note the column position
+     * of the first residue, to save in the cursor
+     */
+    int firstResidueColumn = 0;
+    int lastPosFound = 0;
+    int lastPosFoundColumn = 0;
+    int seqlen = sequence.length;
+
+    if (seqlen > 0 && !Comparison.isGap(sequence[0]))
+    {
+      lastPosFound = start;
+      lastPosFoundColumn = 0;
+    }
+
     int j = 0;
     int pos = start;
-    int seqlen = sequence.length;
-    while ((j < i) && (j < seqlen))
+
+    while (j < column && j < seqlen)
     {
-      if (!jalview.util.Comparison.isGap(sequence[j]))
+      if (!Comparison.isGap(sequence[j]))
       {
+        lastPosFound = pos;
+        lastPosFoundColumn = j;
+        if (pos == this.start)
+        {
+          firstResidueColumn = j;
+        }
         pos++;
       }
-
       j++;
     }
+    if (j < seqlen && !Comparison.isGap(sequence[j]))
+    {
+      lastPosFound = pos;
+      lastPosFoundColumn = j;
+      if (pos == this.start)
+      {
+        firstResidueColumn = j;
+      }
+    }
+
+    /*
+     * update the cursor to the last residue position found (if any)
+     * (converting column position to base 1)
+     */
+    if (lastPosFound != 0)
+    {
+      updateCursor(lastPosFound, lastPosFoundColumn + 1,
+              firstResidueColumn + 1);
+    }
 
     return pos;
   }
 
   /**
+   * Answers true if the given cursor is not null, is for this sequence object,
+   * and has a token value that matches this object's changeCount, else false.
+   * This allows us to ignore a cursor as 'stale' if the sequence has been
+   * modified since the cursor was created.
+   * 
+   * @param curs
+   * @return
+   */
+  protected boolean isValidCursor(SequenceCursor curs)
+  {
+    if (curs == null || curs.sequence != this || curs.token != changeCount)
+    {
+      return false;
+    }
+    /*
+     * sanity check against range
+     */
+    if (curs.columnPosition < 0 || curs.columnPosition > sequence.length)
+    {
+      return false;
+    }
+    if (curs.residuePosition < start || curs.residuePosition > end)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Answers the sequence position (start..) for the given aligned column
+   * position (1..), given a hint of a cursor in the neighbourhood. The cursor
+   * may lie left of, at, or to the right of the column position.
+   * 
+   * @param col
+   * @param curs
+   * @return
+   */
+  protected int findPosition(final int col, SequenceCursor curs)
+  {
+    if (!isValidCursor(curs))
+    {
+      /*
+       * wrong or invalidated cursor, compute de novo
+       */
+      return findPosition(col - 1);// ugh back to base 0
+    }
+
+    if (curs.columnPosition == col)
+    {
+      cursor = curs; // in case this method becomes public
+      return curs.residuePosition; // easy case :-)
+    }
+
+    if (curs.lastColumnPosition > 0 && curs.lastColumnPosition < col)
+    {
+      /*
+       * sequence lies entirely to the left of col
+       * - return last residue + 1
+       */
+      return end + 1;
+    }
+
+    if (curs.firstColumnPosition > 0 && curs.firstColumnPosition > col)
+    {
+      /*
+       * sequence lies entirely to the right of col
+       * - return first residue
+       */
+      return start;
+    }
+
+    // todo could choose closest to col out of column,
+    // firstColumnPosition, lastColumnPosition as a start point
+
+    /*
+     * move left or right to find pos from cursor position
+     */
+    int firstResidueColumn = curs.firstColumnPosition;
+    int column = curs.columnPosition - 1; // to base 0
+    int newPos = curs.residuePosition;
+    int delta = curs.columnPosition > col ? -1 : 1;
+    boolean gapped = false;
+    int lastFoundPosition = curs.residuePosition;
+    int lastFoundPositionColumn = curs.columnPosition;
+
+    while (column != col - 1)
+    {
+      column += delta; // shift one column left or right
+      if (column < 0 || column == sequence.length)
+      {
+        break;
+      }
+      gapped = Comparison.isGap(sequence[column]);
+      if (!gapped)
+      {
+        newPos += delta;
+        lastFoundPosition = newPos;
+        lastFoundPositionColumn = column + 1;
+        if (lastFoundPosition == this.start)
+        {
+          firstResidueColumn = column + 1;
+        }
+      }
+    }
+
+    if (cursor == null || lastFoundPosition != cursor.residuePosition)
+    {
+      updateCursor(lastFoundPosition, lastFoundPositionColumn,
+              firstResidueColumn);
+    }
+
+    /*
+     * hack to give position to the right if on a gap
+     * or beyond the length of the sequence (see JAL-2562)
+     */
+    if (delta > 0 && (gapped || column >= sequence.length))
+    {
+      newPos++;
+    }
+
+    return newPos;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Range findPositions(int fromColumn, int toColumn)
+  {
+    if (toColumn < fromColumn || fromColumn < 1)
+    {
+      return null;
+    }
+
+    /*
+     * find the first non-gapped position, if any
+     */
+    int firstPosition = 0;
+    int col = fromColumn - 1;
+    int length = sequence.length;
+    while (col < length && col < toColumn)
+    {
+      if (!Comparison.isGap(sequence[col]))
+      {
+        firstPosition = findPosition(col++);
+        break;
+      }
+      col++;
+    }
+
+    if (firstPosition == 0)
+    {
+      return null;
+    }
+
+    /*
+     * find the last non-gapped position
+     */
+    int lastPosition = firstPosition;
+    while (col < length && col < toColumn)
+    {
+      if (!Comparison.isGap(sequence[col++]))
+      {
+        lastPosition++;
+      }
+    }
+
+    return new Range(firstPosition, lastPosition);
+  }
+
+  /**
    * Returns an int array where indices correspond to each residue in the
    * sequence and the element value gives its position in the alignment
    * 
@@ -768,6 +1138,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()
   {
@@ -791,7 +1182,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;
@@ -857,7 +1248,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)
@@ -869,66 +1260,81 @@ 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;
-    for (int s = i; s < j; s++)
+
+    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 < sequence.length; 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)
+          if (endIndex < j)
           {
-            sindex = findIndex(start) - 1;
-            scalc = true;
-          }
-          if (sindex == s)
-          {
-            // 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;
     sequence = tmp;
+    sequenceChanged();
   }
 
   @Override
@@ -959,6 +1365,7 @@ public class Sequence extends ASequence implements SequenceI
     }
 
     sequence = tmp;
+    sequenceChanged();
   }
 
   @Override
@@ -1087,7 +1494,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))
     {
@@ -1154,7 +1561,7 @@ public class Sequence extends ASequence implements SequenceI
 
   private boolean _isNa;
 
-  private long _seqhash = 0;
+  private int _seqhash = 0;
 
   /**
    * Answers false if the sequence is more than 85% nucleotide (ACGTU), else
@@ -1194,8 +1601,8 @@ public class Sequence extends ASequence implements SequenceI
 
       dsseq.setDescription(description);
       // move features and database references onto dataset sequence
-      dsseq.sequenceFeatures = sequenceFeatures;
-      sequenceFeatures = null;
+      dsseq.sequenceFeatureStore = sequenceFeatureStore;
+      sequenceFeatureStore = null;
       dsseq.dbrefs = dbrefs;
       dbrefs = null;
       // TODO: search and replace any references to this sequence with
@@ -1254,11 +1661,11 @@ public class Sequence extends ASequence implements SequenceI
       return null;
     }
 
-    Vector subset = new Vector();
-    Enumeration e = annotation.elements();
+    Vector<AlignmentAnnotation> subset = new Vector<>();
+    Enumeration<AlignmentAnnotation> e = annotation.elements();
     while (e.hasMoreElements())
     {
-      AlignmentAnnotation ann = (AlignmentAnnotation) e.nextElement();
+      AlignmentAnnotation ann = e.nextElement();
       if (ann.label != null && ann.label.equals(label))
       {
         subset.addElement(ann);
@@ -1273,7 +1680,7 @@ public class Sequence extends ASequence implements SequenceI
     e = subset.elements();
     while (e.hasMoreElements())
     {
-      anns[i++] = (AlignmentAnnotation) e.nextElement();
+      anns[i++] = e.nextElement();
     }
     subset.removeAllElements();
     return anns;
@@ -1326,13 +1733,12 @@ public class Sequence extends ASequence implements SequenceI
     if (entry.getSequenceFeatures() != null)
     {
 
-      SequenceFeature[] sfs = entry.getSequenceFeatures();
-      for (int si = 0; si < sfs.length; si++)
+      List<SequenceFeature> sfs = entry.getSequenceFeatures();
+      for (SequenceFeature feature : sfs)
       {
-        SequenceFeature sf[] = (mp != null) ? mp.locateFeature(sfs[si])
-                : new SequenceFeature[]
-                { new SequenceFeature(sfs[si]) };
-        if (sf != null && sf.length > 0)
+       SequenceFeature sf[] = (mp != null) ? mp.locateFeature(feature)
+                : new SequenceFeature[] { new SequenceFeature(feature) };
+        if (sf != null)
         {
           for (int sfi = 0; sfi < sf.length; sfi++)
           {
@@ -1345,10 +1751,10 @@ public class Sequence extends ASequence implements SequenceI
     // transfer PDB entries
     if (entry.getAllPDBEntries() != null)
     {
-      Enumeration e = entry.getAllPDBEntries().elements();
+      Enumeration<PDBEntry> e = entry.getAllPDBEntries().elements();
       while (e.hasMoreElements())
       {
-        PDBEntry pdb = (PDBEntry) e.nextElement();
+        PDBEntry pdb = e.nextElement();
         addPDBId(pdb);
       }
     }
@@ -1373,30 +1779,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)
   {
@@ -1413,7 +1795,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)
@@ -1469,7 +1851,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)
       {
@@ -1516,4 +1898,177 @@ public class Sequence extends ASequence implements SequenceI
     }
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeatures(int fromColumn, int toColumn,
+          String... types)
+  {
+    int startPos = findPosition(fromColumn - 1); // convert base 1 to base 0
+    int endPos = fromColumn == toColumn ? startPos
+            : findPosition(toColumn - 1);
+
+    List<SequenceFeature> result = getFeatures().findFeatures(startPos,
+            endPos, types);
+    if (datasetSequence != null)
+    {
+      result = datasetSequence.getFeatures().findFeatures(startPos, endPos,
+              types);
+    }
+    else
+    {
+      result = sequenceFeatureStore.findFeatures(startPos, endPos, types);
+    }
+
+    /*
+     * if end column is gapped, endPos may be to the right, 
+     * and we may have included adjacent or enclosing features;
+     * remove any that are not enclosing, non-contact features
+     */
+    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())
+      {
+        SequenceFeature sf = it.next();
+        int sfBegin = sf.getBegin();
+        int sfEnd = sf.getEnd();
+        int featureStartColumn = findIndex(sfBegin);
+        if (featureStartColumn > toColumn)
+        {
+          it.remove();
+        }
+        else if (featureStartColumn < fromColumn)
+        {
+          int featureEndColumn = sfEnd == sfBegin ? featureStartColumn
+                  : findIndex(sfEnd);
+          if (featureEndColumn < fromColumn)
+          {
+            it.remove();
+          }
+          else if (featureEndColumn > toColumn && sf.isContactFeature())
+          {
+            /*
+             * remove an enclosing feature if it is a contact feature
+             */
+            it.remove();
+          }
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * Invalidates any stale cursors (forcing recalculation) by incrementing the
+   * token that has to match the one presented by the cursor
+   */
+  @Override
+  public void sequenceChanged()
+  {
+    changeCount++;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int replace(char c1, char c2)
+  {
+    if (c1 == c2)
+    {
+      return 0;
+    }
+    int count = 0;
+    synchronized (sequence)
+    {
+      for (int c = 0; c < sequence.length; c++)
+      {
+        if (sequence[c] == c1)
+        {
+          sequence[c] = c2;
+          count++;
+        }
+      }
+    }
+    if (count > 0)
+    {
+      sequenceChanged();
+    }
+
+    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;
+  }
 }
diff --git a/src/jalview/datamodel/SequenceCursor.java b/src/jalview/datamodel/SequenceCursor.java
new file mode 100644 (file)
index 0000000..24752bf
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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;
+
+/**
+ * An immutable object representing one or more residue and corresponding
+ * alignment column positions for a sequence
+ */
+public class SequenceCursor
+{
+  /**
+   * the aligned sequence this cursor applies to
+   */
+  public final SequenceI sequence;
+
+  /**
+   * residue position in sequence (start...), 0 if undefined
+   */
+  public final int residuePosition;
+
+  /**
+   * column position (1...) corresponding to residuePosition, or 0 if undefined
+   */
+  public final int columnPosition;
+
+  /**
+   * column position (1...) of first residue in the sequence, or 0 if undefined
+   */
+  public final int firstColumnPosition;
+
+  /**
+   * column position (1...) of last residue in the sequence, or 0 if undefined
+   */
+  public final int lastColumnPosition;
+
+  /**
+   * a token which may be used to check whether this cursor is still valid for
+   * its sequence (allowing it to be ignored if the sequence has changed)
+   */
+  public final int token;
+
+  /**
+   * Constructor
+   * 
+   * @param seq
+   *          sequence this cursor applies to
+   * @param resPos
+   *          residue position in sequence (start..)
+   * @param column
+   *          column position in alignment (1..)
+   * @param tok
+   *          a token that may be validated by the sequence to check the cursor
+   *          is not stale
+   */
+  public SequenceCursor(SequenceI seq, int resPos, int column, int tok)
+  {
+    this(seq, resPos, column, 0, 0, tok);
+  }
+
+  /**
+   * Constructor
+   * 
+   * @param seq
+   *          sequence this cursor applies to
+   * @param resPos
+   *          residue position in sequence (start..)
+   * @param column
+   *          column position in alignment (1..)
+   * @param firstResCol
+   *          column position of the first residue in the sequence (1..), or 0
+   *          if not known
+   * @param lastResCol
+   *          column position of the last residue in the sequence (1..), or 0 if
+   *          not known
+   * @param tok
+   *          a token that may be validated by the sequence to check the cursor
+   *          is not stale
+   */
+  public SequenceCursor(SequenceI seq, int resPos, int column, int firstResCol,
+          int lastResCol, int tok)
+  {
+    sequence = seq;
+    residuePosition = resPos;
+    columnPosition = column;
+    firstColumnPosition = firstResCol;
+    lastColumnPosition = lastResCol;
+    token = tok;
+  }
+
+  @Override
+  public int hashCode()
+  {
+    int hash = 31 * residuePosition;
+    hash = 31 * hash + columnPosition;
+    hash = 31 * hash + token;
+    if (sequence != null)
+    {
+      hash += sequence.hashCode();
+    }
+    return hash;
+  }
+
+  /**
+   * Two cursors are equal if they refer to the same sequence object and have
+   * the same residue position, column position and token value
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (!(obj instanceof SequenceCursor))
+    {
+      return false;
+    }
+    SequenceCursor sc = (SequenceCursor) obj;
+    return sequence == sc.sequence && residuePosition == sc.residuePosition
+            && columnPosition == sc.columnPosition && token == sc.token;
+  }
+
+  @Override
+  public String toString()
+  {
+    String name = sequence == null ? "" : sequence.getName();
+    return String.format("%s:Pos%d:Col%d:startCol%d:endCol%d:tok%d", name,
+            residuePosition, columnPosition, firstColumnPosition,
+            lastColumnPosition, token);
+  }
+}
index 71732aa..7052f34 100755 (executable)
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureAttributes;
+import jalview.datamodel.features.FeatureLocationI;
+import jalview.datamodel.features.FeatureSourceI;
+import jalview.datamodel.features.FeatureSources;
+import jalview.util.StringUtils;
+
+import java.util.Comparator;
 import java.util.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
+public class SequenceFeature implements FeatureLocationI
 {
+  /*
+   * score value if none is set; preferably Float.Nan, but see
+   * JAL-2060 and JAL-2554 for a couple of blockers to that
+   */
+  private static final float NO_SCORE = 0f;
+
   private static final String STATUS = "status";
 
   private static final String STRAND = "STRAND";
@@ -42,19 +58,30 @@ public class SequenceFeature
   // 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
    */
   private static final String ATTRIBUTES = "ATTRIBUTES";
 
-  public int begin;
+  /*
+   * type, begin, end, featureGroup, score and contactFeature are final 
+   * to ensure that the integrity of SequenceFeatures data store 
+   * can't be broken by direct update of these fields
+   */
+  public final String type;
+
+  public final int begin;
+
+  public final int end;
 
-  public int end;
+  public final String featureGroup;
 
-  public float score;
+  public final float score;
 
-  public String type;
+  private final boolean contactFeature;
 
   public String description;
 
@@ -66,13 +93,11 @@ public class SequenceFeature
 
   public Vector<String> links;
 
-  // Feature group can be set from a features file
-  // as a group of features between STARTGROUP and ENDGROUP markers
-  public String featureGroup;
-
-  public SequenceFeature()
-  {
-  }
+  /*
+   * 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
@@ -83,96 +108,101 @@ public class SequenceFeature
    */
   public SequenceFeature(SequenceFeature cpy)
   {
-    if (cpy != null)
-    {
-      begin = cpy.begin;
-      end = cpy.end;
-      score = cpy.score;
-      if (cpy.type != null)
-      {
-        type = new String(cpy.type);
-      }
-      if (cpy.description != null)
-      {
-        description = new String(cpy.description);
-      }
-      if (cpy.featureGroup != null)
-      {
-        featureGroup = new String(cpy.featureGroup);
-      }
-      if (cpy.otherDetails != null)
-      {
-        try
-        {
-          otherDetails = (Map<String, Object>) ((HashMap<String, Object>) cpy.otherDetails)
-                  .clone();
-        } catch (Exception e)
-        {
-          // ignore
-        }
-      }
-      if (cpy.links != null && cpy.links.size() > 0)
-      {
-        links = new Vector<String>();
-        for (int i = 0, iSize = cpy.links.size(); i < iSize; i++)
-        {
-          links.addElement(cpy.links.elementAt(i));
-        }
-      }
-    }
+    this(cpy, cpy.getBegin(), cpy.getEnd(), cpy.getFeatureGroup(), cpy
+            .getScore());
   }
 
   /**
-   * Constructor including a Status value
+   * Constructor
    * 
-   * @param type
-   * @param desc
-   * @param status
-   * @param begin
-   * @param end
-   * @param featureGroup
+   * @param theType
+   * @param theDesc
+   * @param theBegin
+   * @param theEnd
+   * @param group
    */
-  public SequenceFeature(String type, String desc, String status, int begin,
-          int end, String featureGroup)
+  public SequenceFeature(String theType, String theDesc, int theBegin,
+          int theEnd, String group)
   {
-    this(type, desc, begin, end, featureGroup);
-    setStatus(status);
+    this(theType, theDesc, theBegin, theEnd, NO_SCORE, group);
   }
 
   /**
-   * Constructor
+   * Constructor including a score value
    * 
-   * @param type
-   * @param desc
-   * @param begin
-   * @param end
-   * @param featureGroup
+   * @param theType
+   * @param theDesc
+   * @param theBegin
+   * @param theEnd
+   * @param theScore
+   * @param group
    */
-  SequenceFeature(String type, String desc, int begin, int end,
-          String featureGroup)
+  public SequenceFeature(String theType, String theDesc, int theBegin,
+          int theEnd, float theScore, String group)
   {
-    this.type = type;
-    this.description = desc;
-    this.begin = begin;
-    this.end = end;
-    this.featureGroup = featureGroup;
+    this.type = theType;
+    this.description = theDesc;
+    this.begin = theBegin;
+    this.end = theEnd;
+    this.featureGroup = group;
+    this.score = theScore;
+
+    /*
+     * for now, only "Disulfide/disulphide bond" is treated as a contact feature
+     */
+    this.contactFeature = "disulfide bond".equalsIgnoreCase(type)
+            || "disulphide bond".equalsIgnoreCase(type);
   }
 
   /**
-   * Constructor including a score value
+   * A copy constructor that allows the value of final fields to be 'modified'
+   * 
+   * @param sf
+   * @param newType
+   * @param newBegin
+   * @param newEnd
+   * @param newGroup
+   * @param newScore
+   */
+  public SequenceFeature(SequenceFeature sf, String newType, int newBegin,
+          int newEnd, String newGroup, float newScore)
+  {
+    this(newType, sf.getDescription(), newBegin, newEnd, newScore,
+            newGroup);
+
+    this.source = sf.source;
+
+    if (sf.otherDetails != null)
+    {
+      otherDetails = new HashMap<>();
+      for (Entry<String, Object> entry : sf.otherDetails.entrySet())
+      {
+        otherDetails.put(entry.getKey(), entry.getValue());
+      }
+    }
+    if (sf.links != null && sf.links.size() > 0)
+    {
+      links = new Vector<>();
+      for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
+      {
+        links.addElement(sf.links.elementAt(i));
+      }
+    }
+  }
+
+  /**
+   * A copy constructor that allows the value of final fields to be 'modified'
    * 
-   * @param type
-   * @param desc
-   * @param begin
-   * @param end
-   * @param score
-   * @param featureGroup
+   * @param sf
+   * @param newBegin
+   * @param newEnd
+   * @param newGroup
+   * @param newScore
    */
-  public SequenceFeature(String type, String desc, int begin, int end,
-          float score, String featureGroup)
+  public SequenceFeature(SequenceFeature sf, int newBegin, int newEnd,
+          String newGroup, float newScore)
   {
-    this(type, desc, begin, end, featureGroup);
-    this.score = score;
+    this(sf, sf.getType(), newBegin, newEnd, newGroup, newScore);
   }
 
   /**
@@ -268,31 +298,23 @@ public class SequenceFeature
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public int getBegin()
   {
     return begin;
   }
 
-  public void setBegin(int start)
-  {
-    this.begin = start;
-  }
-
   /**
    * DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public int getEnd()
   {
     return end;
   }
 
-  public void setEnd(int end)
-  {
-    this.end = end;
-  }
-
   /**
    * DOCUMENT ME!
    * 
@@ -303,11 +325,6 @@ public class SequenceFeature
     return type;
   }
 
-  public void setType(String type)
-  {
-    this.type = type;
-  }
-
   /**
    * DOCUMENT ME!
    * 
@@ -328,19 +345,17 @@ public class SequenceFeature
     return featureGroup;
   }
 
-  public void setFeatureGroup(String featureGroup)
-  {
-    this.featureGroup = featureGroup;
-  }
-
   public void addLink(String labelLink)
   {
     if (links == null)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
     }
 
-    links.insertElementAt(labelLink, 0);
+    if (!links.contains(labelLink))
+    {
+      links.insertElementAt(labelLink, 0);
+    }
   }
 
   public float getScore()
@@ -348,11 +363,6 @@ public class SequenceFeature
     return score;
   }
 
-  public void setScore(float value)
-  {
-    score = value;
-  }
-
   /**
    * Used for getting values which are not in the basic set. eg STRAND, PHASE
    * for GFF file
@@ -373,6 +383,30 @@ public class SequenceFeature
   }
 
   /**
+   * 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
    * 
@@ -401,13 +435,35 @@ public class SequenceFeature
     {
       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.
@@ -432,17 +488,6 @@ public class SequenceFeature
     return (String) getValue(ATTRIBUTES);
   }
 
-  public void setPosition(int pos)
-  {
-    begin = pos;
-    end = pos;
-  }
-
-  public int getPosition()
-  {
-    return begin;
-  }
-
   /**
    * Return 1 for forward strand ('+' in GFF), -1 for reverse strand ('-' in
    * GFF), and 0 for unknown or not (validly) specified
@@ -538,14 +583,175 @@ public class SequenceFeature
    * positions, rather than ends of a range. Such features may be visualised or
    * reported differently to features on a range.
    */
+  @Override
   public boolean isContactFeature()
   {
-    // TODO abstract one day to a FeatureType class
-    if ("disulfide bond".equalsIgnoreCase(type)
-            || "disulphide bond".equalsIgnoreCase(type))
+    return contactFeature;
+  }
+
+  /**
+   * Answers true if the sequence has zero start and end position
+   * 
+   * @return
+   */
+  public boolean isNonPositional()
+  {
+    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;
     }
-    return false;
+
+    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;
+  }
+}
+
+class SFSortByEnd implements Comparator<SequenceFeature>
+{
+  @Override
+  public int compare(SequenceFeature a, SequenceFeature b)
+  {
+    return a.getEnd() - b.getEnd();
+  }
+}
+
+class SFSortByBegin implements Comparator<SequenceFeature>
+{
+  @Override
+  public int compare(SequenceFeature a, SequenceFeature b)
+  {
+    return a.getBegin() - b.getBegin();
   }
 }
index deabf46..fc8ac49 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;
 
@@ -1196,9 +1197,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
         {
@@ -1324,39 +1326,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);
   }
 
   /**
@@ -1367,17 +1346,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 12ddf60..48615f0 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;
 
@@ -117,9 +121,9 @@ public interface SequenceI extends ASequenceI
   public String getSequenceAsString(int start, int end);
 
   /**
-   * Get the sequence as a character array
+   * Answers a copy of the sequence as a character array
    * 
-   * @return seqeunce and any gaps
+   * @return
    */
   public char[] getSequence();
 
@@ -175,7 +179,7 @@ public interface SequenceI extends ASequenceI
   public String getDescription();
 
   /**
-   * Return the alignment column for a sequence position
+   * Return the alignment column (from 1..) for a sequence position
    * 
    * @param pos
    *          lying from start to end
@@ -190,17 +194,31 @@ 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);
 
   /**
+   * Returns the sequence positions for first and last residues lying within the
+   * given column positions [fromColum,toColumn] (where columns are numbered
+   * from 1), or null if no residues are included in the range
+   * 
+   * @param fromColum
+   *          - first column base 1
+   * @param toColumn
+   *          - last column, base 1
+   * @return
+   */
+  public Range findPositions(int fromColum, int toColumn);
+
+  /**
    * Returns an int array where indices correspond to each residue in the
    * sequence and the element value gives its position in the alignment
    * 
@@ -210,6 +228,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.
@@ -261,22 +286,28 @@ public interface SequenceI extends ASequenceI
   public void insertCharAt(int position, int count, char ch);
 
   /**
-   * Gets array holding sequence features associated with this sequence. The
-   * array may be held by the sequence's dataset sequence if that is defined.
+   * Answers a list of all sequence features associated with this sequence. The
+   * list may be held by the sequence's dataset sequence if that is defined.
    * 
-   * @return hard reference to array
+   * @return
    */
-  public SequenceFeature[] getSequenceFeatures();
+  public List<SequenceFeature> getSequenceFeatures();
 
   /**
-   * Replaces the array of sequence features associated with this sequence with
-   * a new array reference. If this sequence has a dataset sequence, then this
-   * method will update the dataset sequence's feature array
+   * Answers the object holding features for the sequence
+   * 
+   * @return
+   */
+  SequenceFeaturesI getFeatures();
+
+  /**
+   * Replaces the sequence features associated with this sequence with the given
+   * features. If this sequence has a dataset sequence, then this method will
+   * update the dataset sequence's features instead.
    * 
    * @param features
-   *          New array of sequence features
    */
-  public void setSequenceFeatures(SequenceFeature[] features);
+  public void setSequenceFeatures(List<SequenceFeature> features);
 
   /**
    * DOCUMENT ME!
@@ -341,7 +372,7 @@ public interface SequenceI extends ASequenceI
 
   /**
    * Adds the given sequence feature and returns true, or returns false if it is
-   * already present on the sequence
+   * already present on the sequence, or if the feature type is null.
    * 
    * @param sf
    * @return
@@ -431,17 +462,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
    */
 
@@ -479,9 +499,81 @@ public interface SequenceI extends ASequenceI
   public List<DBRefEntry> getPrimaryDBRefs();
 
   /**
+   * Returns a (possibly empty) list of sequence features that overlap the given
+   * alignment column range, optionally restricted to one or more specified
+   * feature types. If the range is all gaps, then features which enclose it are
+   * included (but not contact features).
+   * 
+   * @param fromCol
+   *          start column of range inclusive (1..)
+   * @param toCol
+   *          end column of range inclusive (1..)
+   * @param types
+   *          optional feature types to restrict results to
+   * @return
+   */
+  List<SequenceFeature> findFeatures(int fromCol, int toCol, String... types);
+
+  /**
+   * Method to call to indicate that the sequence (characters or alignment/gaps)
+   * has been modified. Provided to allow any cursors on residue/column
+   * positions to be invalidated.
+   */
+  void sequenceChanged();
+  
+  /**
    * 
    * @return BitSet corresponding to index [0,length) where Comparison.isGap()
    *         returns true.
    */
   BitSet getInsertionsAsBits();
+
+  /**
+   * Replaces every occurrence of c1 in the sequence with c2 and returns the
+   * number of characters changed
+   * 
+   * @param c1
+   * @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..10249f3
--- /dev/null
@@ -0,0 +1,374 @@
+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
+     * descriptions, and the min/max of any numerical values
+     * 
+     * @param desc
+     * @param value
+     */
+    void addInstance(String desc, String value)
+    {
+      addDescription(desc);
+
+      if (value != null)
+      {
+        value = value.trim();
+
+        /*
+         * Parse numeric value unless we have previously
+         * seen text data for this attribute type
+         */
+        if (type == null || type == Datatype.Number)
+        {
+          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)
+          {
+            /*
+             * non-numeric data: treat attribute as Character (or Mixed)
+             */
+            type = (type == null || type == Datatype.Character)
+                    ? Datatype.Character
+                    : Datatype.Mixed;
+            min = 0f;
+            max = 0f;
+            hasValue = false;
+          }
+        }
+      }
+    }
+
+    /**
+     * 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 with a mixture of text and
+   * numeric values are considered text (do not return 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;
+  }
+}
similarity index 66%
rename from src/jalview/io/MatrixFile.java
rename to src/jalview/datamodel/features/FeatureLocationI.java
index 418eea2..378b8db 100644 (file)
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.io;
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
 
 /**
- * IO for asymmetric matrix with arbitrary dimension with labels, as displayed
- * by PCA viewer. Form is: tab separated entity defs header line TITLE\ttitle
- * DESC\tdesc PROPERTY\t<id or empty for whole matrix>\tname\ttype\tvalue
- * ROW\tRow i label (ID)/tPrinciple text/tprinciple description/t...
- * COLUMN\t(similar, optional).. .. <float>\t<float>...(column-wise data for row
- * i)
+ * An extension of ContiguousI that allows start/end values to be interpreted
+ * instead as two contact positions
  */
-
-public class MatrixFile extends FileParse
+public interface FeatureLocationI extends ContiguousI
 {
-
+  boolean isContactFeature();
 }
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);
+  }
+}
diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java
new file mode 100644 (file)
index 0000000..951b7a5
--- /dev/null
@@ -0,0 +1,1089 @@
+/*
+ * 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;
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A data store for a set of sequence features that supports efficient lookup of
+ * features overlapping a given range. Intended for (but not limited to) storage
+ * of features for one sequence and feature type.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureStore
+{
+  /**
+   * a class providing criteria for performing a binary search of a list
+   */
+  abstract static class SearchCriterion
+  {
+    /**
+     * Answers true if the entry passes the search criterion test
+     * 
+     * @param entry
+     * @return
+     */
+    abstract boolean compare(SequenceFeature entry);
+
+    /**
+     * serves a search condition for finding the first feature whose start
+     * position follows a given target location
+     * 
+     * @param target
+     * @return
+     */
+    static SearchCriterion byStart(final long target)
+    {
+      return new SearchCriterion() {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return entry.getBegin() >= target;
+        }
+      };
+    }
+
+    /**
+     * serves a search condition for finding the first feature whose end
+     * position is at or follows a given target location
+     * 
+     * @param target
+     * @return
+     */
+    static SearchCriterion byEnd(final long target)
+    {
+      return new SearchCriterion()
+      {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return entry.getEnd() >= target;
+        }
+      };
+    }
+
+    /**
+     * serves a search condition for finding the first feature which follows the
+     * given range as determined by a supplied comparator
+     * 
+     * @param target
+     * @return
+     */
+    static SearchCriterion byFeature(final ContiguousI to,
+            final Comparator<ContiguousI> rc)
+    {
+      return new SearchCriterion()
+      {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return rc.compare(entry, to) >= 0;
+        }
+      };
+    }
+  }
+
+  /*
+   * Non-positional features have no (zero) start/end position.
+   * Kept as a separate list in case this criterion changes in future.
+   */
+  List<SequenceFeature> nonPositionalFeatures;
+
+  /*
+   * An ordered list of features, with the promise that no feature in the list 
+   * properly contains any other. This constraint allows bounded linear search
+   * of the list for features overlapping a region.
+   * Contact features are not included in this list.
+   */
+  List<SequenceFeature> nonNestedFeatures;
+
+  /*
+   * contact features ordered by first contact position
+   */
+  List<SequenceFeature> contactFeatureStarts;
+
+  /*
+   * contact features ordered by second contact position
+   */
+  List<SequenceFeature> contactFeatureEnds;
+
+  /*
+   * Nested Containment List is used to hold any features that are nested 
+   * within (properly contained by) any other feature. This is a recursive tree
+   * which supports depth-first scan for features overlapping a range.
+   * It is used here as a 'catch-all' fallback for features that cannot be put
+   * into a simple ordered list without invalidating the search methods.
+   */
+  NCList<SequenceFeature> nestedFeatures;
+
+  /*
+   * Feature groups represented in stored positional features 
+   * (possibly including null)
+   */
+  Set<String> positionalFeatureGroups;
+
+  /*
+   * Feature groups represented in stored non-positional features 
+   * (possibly including null)
+   */
+  Set<String> nonPositionalFeatureGroups;
+
+  /*
+   * the total length of all positional features; contact features count 1 to
+   * the total and 1 to size(), consistent with an average 'feature length' of 1
+   */
+  int totalExtent;
+
+  float positionalMinScore;
+
+  float positionalMaxScore;
+
+  float nonPositionalMinScore;
+
+  float nonPositionalMaxScore;
+
+  /**
+   * Constructor
+   */
+  public FeatureStore()
+  {
+    nonNestedFeatures = new ArrayList<SequenceFeature>();
+    positionalFeatureGroups = new HashSet<String>();
+    nonPositionalFeatureGroups = new HashSet<String>();
+    positionalMinScore = Float.NaN;
+    positionalMaxScore = Float.NaN;
+    nonPositionalMinScore = Float.NaN;
+    nonPositionalMaxScore = Float.NaN;
+
+    // we only construct nonPositionalFeatures, contactFeatures
+    // or the NCList if we need to
+  }
+
+  /**
+   * Adds one sequence feature to the store, and returns true, unless the
+   * feature is already contained in the store, in which case this method
+   * returns false. Containment is determined by SequenceFeature.equals()
+   * comparison.
+   * 
+   * @param feature
+   */
+  public boolean addFeature(SequenceFeature feature)
+  {
+    if (contains(feature))
+    {
+      return false;
+    }
+
+    /*
+     * keep a record of feature groups
+     */
+    if (!feature.isNonPositional())
+    {
+      positionalFeatureGroups.add(feature.getFeatureGroup());
+    }
+
+    boolean added = false;
+
+    if (feature.isContactFeature())
+    {
+      added = addContactFeature(feature);
+    }
+    else if (feature.isNonPositional())
+    {
+      added = addNonPositionalFeature(feature);
+    }
+    else
+    {
+      added = addNonNestedFeature(feature);
+      if (!added)
+      {
+        /*
+         * detected a nested feature - put it in the NCList structure
+         */
+        added = addNestedFeature(feature);
+      }
+    }
+
+    if (added)
+    {
+      /*
+       * record the total extent of positional features, to make
+       * getTotalFeatureLength possible; we count the length of a 
+       * contact feature as 1
+       */
+      totalExtent += getFeatureLength(feature);
+
+      /*
+       * record the minimum and maximum score for positional
+       * and non-positional features
+       */
+      float score = feature.getScore();
+      if (!Float.isNaN(score))
+      {
+        if (feature.isNonPositional())
+        {
+          nonPositionalMinScore = min(nonPositionalMinScore, score);
+          nonPositionalMaxScore = max(nonPositionalMaxScore, score);
+        }
+        else
+        {
+          positionalMinScore = min(positionalMinScore, score);
+          positionalMaxScore = max(positionalMaxScore, score);
+        }
+      }
+    }
+
+    return added;
+  }
+
+  /**
+   * Answers true if this store contains the given feature (testing by
+   * SequenceFeature.equals), else false
+   * 
+   * @param feature
+   * @return
+   */
+  public boolean contains(SequenceFeature feature)
+  {
+    if (feature.isNonPositional())
+    {
+      return nonPositionalFeatures == null ? false : nonPositionalFeatures
+              .contains(feature);
+    }
+
+    if (feature.isContactFeature())
+    {
+      return contactFeatureStarts == null ? false : listContains(
+              contactFeatureStarts, feature);
+    }
+
+    if (listContains(nonNestedFeatures, feature))
+    {
+      return true;
+    }
+
+    return nestedFeatures == null ? false : nestedFeatures
+            .contains(feature);
+  }
+
+  /**
+   * Answers the 'length' of the feature, counting 0 for non-positional features
+   * and 1 for contact features
+   * 
+   * @param feature
+   * @return
+   */
+  protected static int getFeatureLength(SequenceFeature feature)
+  {
+    if (feature.isNonPositional())
+    {
+      return 0;
+    }
+    if (feature.isContactFeature())
+    {
+      return 1;
+    }
+    return 1 + feature.getEnd() - feature.getBegin();
+  }
+
+  /**
+   * Adds the feature to the list of non-positional features (with lazy
+   * instantiation of the list if it is null), and returns true. The feature
+   * group is added to the set of distinct feature groups for non-positional
+   * features. This method allows duplicate features, so test before calling to
+   * prevent this.
+   * 
+   * @param feature
+   */
+  protected boolean addNonPositionalFeature(SequenceFeature feature)
+  {
+    if (nonPositionalFeatures == null)
+    {
+      nonPositionalFeatures = new ArrayList<SequenceFeature>();
+    }
+
+    nonPositionalFeatures.add(feature);
+
+    nonPositionalFeatureGroups.add(feature.getFeatureGroup());
+
+    return true;
+  }
+
+  /**
+   * Adds one feature to the NCList that can manage nested features (creating
+   * the NCList if necessary), and returns true. If the feature is already
+   * stored in the NCList (by equality test), then it is not added, and this
+   * method returns false.
+   */
+  protected synchronized boolean addNestedFeature(SequenceFeature feature)
+  {
+    if (nestedFeatures == null)
+    {
+      nestedFeatures = new NCList<>(feature);
+      return true;
+    }
+    return nestedFeatures.add(feature, false);
+  }
+
+  /**
+   * Add a feature to the list of non-nested features, maintaining the ordering
+   * of the list. A check is made for whether the feature is nested in (properly
+   * contained by) an existing feature. If there is no nesting, the feature is
+   * added to the list and the method returns true. If nesting is found, the
+   * feature is not added and the method returns false.
+   * 
+   * @param feature
+   * @return
+   */
+  protected boolean addNonNestedFeature(SequenceFeature feature)
+  {
+    synchronized (nonNestedFeatures)
+    {
+      /*
+       * find the first stored feature which doesn't precede the new one
+       */
+      int insertPosition = binarySearch(nonNestedFeatures,
+              SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION));
+
+      /*
+       * fail if we detect feature enclosure - of the new feature by
+       * the one preceding it, or of the next feature by the new one
+       */
+      if (insertPosition > 0)
+      {
+        if (encloses(nonNestedFeatures.get(insertPosition - 1), feature))
+        {
+          return false;
+        }
+      }
+      if (insertPosition < nonNestedFeatures.size())
+      {
+        if (encloses(feature, nonNestedFeatures.get(insertPosition)))
+        {
+          return false;
+        }
+      }
+
+      /*
+       * checks passed - add the feature
+       */
+      nonNestedFeatures.add(insertPosition, feature);
+
+      return true;
+    }
+  }
+
+  /**
+   * Answers true if range1 properly encloses range2, else false
+   * 
+   * @param range1
+   * @param range2
+   * @return
+   */
+  protected boolean encloses(ContiguousI range1, ContiguousI range2)
+  {
+    int begin1 = range1.getBegin();
+    int begin2 = range2.getBegin();
+    int end1 = range1.getEnd();
+    int end2 = range2.getEnd();
+    if (begin1 == begin2 && end1 > end2)
+    {
+      return true;
+    }
+    if (begin1 < begin2 && end1 >= end2)
+    {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Add a contact feature to the lists that hold them ordered by start (first
+   * contact) and by end (second contact) position, ensuring the lists remain
+   * ordered, and returns true. This method allows duplicate features to be
+   * added, so test before calling to avoid this.
+   * 
+   * @param feature
+   * @return
+   */
+  protected synchronized boolean addContactFeature(SequenceFeature feature)
+  {
+    if (contactFeatureStarts == null)
+    {
+      contactFeatureStarts = new ArrayList<SequenceFeature>();
+    }
+    if (contactFeatureEnds == null)
+    {
+      contactFeatureEnds = new ArrayList<SequenceFeature>();
+    }
+
+    /*
+     * binary search the sorted list to find the insertion point
+     */
+    int insertPosition = binarySearch(contactFeatureStarts,
+            SearchCriterion.byFeature(feature,
+                    RangeComparator.BY_START_POSITION));
+    contactFeatureStarts.add(insertPosition, feature);
+    // and resort to mak siccar...just in case insertion point not quite right
+    Collections.sort(contactFeatureStarts, RangeComparator.BY_START_POSITION);
+
+    insertPosition = binarySearch(contactFeatureStarts,
+            SearchCriterion.byFeature(feature,
+                    RangeComparator.BY_END_POSITION));
+    contactFeatureEnds.add(feature);
+    Collections.sort(contactFeatureEnds, RangeComparator.BY_END_POSITION);
+
+    return true;
+  }
+
+  /**
+   * Answers true if the list contains the feature, else false. This method is
+   * optimised for the condition that the list is sorted on feature start
+   * position ascending, and will give unreliable results if this does not hold.
+   * 
+   * @param features
+   * @param feature
+   * @return
+   */
+  protected static boolean listContains(List<SequenceFeature> features,
+          SequenceFeature feature)
+  {
+    if (features == null || feature == null)
+    {
+      return false;
+    }
+
+    /*
+     * locate the first entry in the list which does not precede the feature
+     */
+    int pos = binarySearch(features,
+            SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION));
+    int len = features.size();
+    while (pos < len)
+    {
+      SequenceFeature sf = features.get(pos);
+      if (sf.getBegin() > feature.getBegin())
+      {
+        return false; // no match found
+      }
+      if (sf.equals(feature))
+      {
+        return true;
+      }
+      pos++;
+    }
+    return false;
+  }
+
+  /**
+   * Returns a (possibly empty) list of features whose extent overlaps the given
+   * range. The returned list is not ordered. Contact features are included if
+   * either of the contact points lies within the range.
+   * 
+   * @param start
+   *          start position of overlap range (inclusive)
+   * @param end
+   *          end position of overlap range (inclusive)
+   * @return
+   */
+  public List<SequenceFeature> findOverlappingFeatures(long start, long end)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    findNonNestedFeatures(start, end, result);
+
+    findContactFeatures(start, end, result);
+
+    if (nestedFeatures != null)
+    {
+      result.addAll(nestedFeatures.findOverlaps(start, end));
+    }
+
+    return result;
+  }
+
+  /**
+   * Adds contact features to the result list where either the second or the
+   * first contact position lies within the target range
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findContactFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    if (contactFeatureStarts != null)
+    {
+      findContactStartFeatures(from, to, result);
+    }
+    if (contactFeatureEnds != null)
+    {
+      findContactEndFeatures(from, to, result);
+    }
+  }
+
+  /**
+   * Adds to the result list any contact features whose end (second contact
+   * point), but not start (first contact point), lies in the query from-to
+   * range
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findContactEndFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    /*
+     * find the first contact feature (if any) that does not lie 
+     * entirely before the target range
+     */
+    int startPosition = binarySearch(contactFeatureEnds,
+            SearchCriterion.byEnd(from));
+    for (; startPosition < contactFeatureEnds.size(); startPosition++)
+    {
+      SequenceFeature sf = contactFeatureEnds.get(startPosition);
+      if (!sf.isContactFeature())
+      {
+        System.err.println("Error! non-contact feature type "
+                + sf.getType() + " in contact features list");
+        continue;
+      }
+
+      int begin = sf.getBegin();
+      if (begin >= from && begin <= to)
+      {
+        /*
+         * this feature's first contact position lies in the search range
+         * so we don't include it in results a second time
+         */
+        continue;
+      }
+
+      int end = sf.getEnd();
+      if (end >= from && end <= to)
+      {
+        result.add(sf);
+      }
+      if (end > to)
+      {
+        break;
+      }
+    }
+  }
+
+  /**
+   * Adds non-nested features to the result list that lie within the target
+   * range. Non-positional features (start=end=0), contact features and nested
+   * features are excluded.
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findNonNestedFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    /*
+     * find the first feature whose end position is
+     * after the target range start
+     */
+    int startIndex = binarySearch(nonNestedFeatures,
+            SearchCriterion.byEnd(from));
+
+    final int startIndex1 = startIndex;
+    int i = startIndex1;
+    while (i < nonNestedFeatures.size())
+    {
+      SequenceFeature sf = nonNestedFeatures.get(i);
+      if (sf.getBegin() > to)
+      {
+        break;
+      }
+      if (sf.getBegin() <= to && sf.getEnd() >= from)
+      {
+        result.add(sf);
+      }
+      i++;
+    }
+  }
+
+  /**
+   * Adds contact features whose start position lies in the from-to range to the
+   * result list
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findContactStartFeatures(long from, long to,
+          List<SequenceFeature> result)
+  {
+    int startPosition = binarySearch(contactFeatureStarts,
+            SearchCriterion.byStart(from));
+
+    for (; startPosition < contactFeatureStarts.size(); startPosition++)
+    {
+      SequenceFeature sf = contactFeatureStarts.get(startPosition);
+      if (!sf.isContactFeature())
+      {
+        System.err.println("Error! non-contact feature type "
+                + sf.getType() + " in contact features list");
+        continue;
+      }
+      int begin = sf.getBegin();
+      if (begin >= from && begin <= to)
+      {
+        result.add(sf);
+      }
+    }
+  }
+
+  /**
+   * Answers a list of all positional features stored, in no guaranteed order
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getPositionalFeatures()
+  {
+    /*
+     * add non-nested features (may be all features for many cases)
+     */
+    List<SequenceFeature> result = new ArrayList<>();
+    result.addAll(nonNestedFeatures);
+
+    /*
+     * add any contact features - from the list by start position
+     */
+    if (contactFeatureStarts != null)
+    {
+      result.addAll(contactFeatureStarts);
+    }
+
+    /*
+     * add any nested features
+     */
+    if (nestedFeatures != null)
+    {
+      result.addAll(nestedFeatures.getEntries());
+    }
+
+    return result;
+  }
+
+  /**
+   * Answers a list of all contact features. If there are none, returns an
+   * immutable empty list.
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getContactFeatures()
+  {
+    if (contactFeatureStarts == null)
+    {
+      return Collections.emptyList();
+    }
+    return new ArrayList<>(contactFeatureStarts);
+  }
+
+  /**
+   * Answers a list of all non-positional features. If there are none, returns
+   * an immutable empty list.
+   * 
+   * @return
+   */
+  public List<SequenceFeature> getNonPositionalFeatures()
+  {
+    if (nonPositionalFeatures == null)
+    {
+      return Collections.emptyList();
+    }
+    return new ArrayList<>(nonPositionalFeatures);
+  }
+
+  /**
+   * Deletes the given feature from the store, returning true if it was found
+   * (and deleted), else false. This method makes no assumption that the feature
+   * is in the 'expected' place in the store, in case it has been modified since
+   * it was added.
+   * 
+   * @param sf
+   */
+  public synchronized boolean delete(SequenceFeature sf)
+  {
+    /*
+     * try the non-nested positional features first
+     */
+    boolean removed = nonNestedFeatures.remove(sf);
+
+    /*
+     * if not found, try contact positions (and if found, delete
+     * from both lists of contact positions)
+     */
+    if (!removed && contactFeatureStarts != null)
+    {
+      removed = contactFeatureStarts.remove(sf);
+      if (removed)
+      {
+        contactFeatureEnds.remove(sf);
+      }
+    }
+
+    boolean removedNonPositional = false;
+
+    /*
+     * if not found, try non-positional features
+     */
+    if (!removed && nonPositionalFeatures != null)
+    {
+      removedNonPositional = nonPositionalFeatures.remove(sf);
+      removed = removedNonPositional;
+    }
+
+    /*
+     * if not found, try nested features
+     */
+    if (!removed && nestedFeatures != null)
+    {
+      removed = nestedFeatures.delete(sf);
+    }
+
+    if (removed)
+    {
+      rescanAfterDelete();
+    }
+
+    return removed;
+  }
+
+  /**
+   * Rescan all features to recompute any cached values after an entry has been
+   * deleted. This is expected to be an infrequent event, so performance here is
+   * not critical.
+   */
+  protected synchronized void rescanAfterDelete()
+  {
+    positionalFeatureGroups.clear();
+    nonPositionalFeatureGroups.clear();
+    totalExtent = 0;
+    positionalMinScore = Float.NaN;
+    positionalMaxScore = Float.NaN;
+    nonPositionalMinScore = Float.NaN;
+    nonPositionalMaxScore = Float.NaN;
+
+    /*
+     * scan non-positional features for groups and scores
+     */
+    for (SequenceFeature sf : getNonPositionalFeatures())
+    {
+      nonPositionalFeatureGroups.add(sf.getFeatureGroup());
+      float score = sf.getScore();
+      nonPositionalMinScore = min(nonPositionalMinScore, score);
+      nonPositionalMaxScore = max(nonPositionalMaxScore, score);
+    }
+
+    /*
+     * scan positional features for groups, scores and extents
+     */
+    for (SequenceFeature sf : getPositionalFeatures())
+    {
+      positionalFeatureGroups.add(sf.getFeatureGroup());
+      float score = sf.getScore();
+      positionalMinScore = min(positionalMinScore, score);
+      positionalMaxScore = max(positionalMaxScore, score);
+      totalExtent += getFeatureLength(sf);
+    }
+  }
+
+  /**
+   * A helper method to return the minimum of two floats, where a non-NaN value
+   * is treated as 'less than' a NaN value (unlike Math.min which does the
+   * opposite)
+   * 
+   * @param f1
+   * @param f2
+   */
+  protected static float min(float f1, float f2)
+  {
+    if (Float.isNaN(f1))
+    {
+      return Float.isNaN(f2) ? f1 : f2;
+    }
+    else
+    {
+      return Float.isNaN(f2) ? f1 : Math.min(f1, f2);
+    }
+  }
+
+  /**
+   * A helper method to return the maximum of two floats, where a non-NaN value
+   * is treated as 'greater than' a NaN value (unlike Math.max which does the
+   * opposite)
+   * 
+   * @param f1
+   * @param f2
+   */
+  protected static float max(float f1, float f2)
+  {
+    if (Float.isNaN(f1))
+    {
+      return Float.isNaN(f2) ? f1 : f2;
+    }
+    else
+    {
+      return Float.isNaN(f2) ? f1 : Math.max(f1, f2);
+    }
+  }
+
+  /**
+   * Answers true if this store has no features, else false
+   * 
+   * @return
+   */
+  public boolean isEmpty()
+  {
+    boolean hasFeatures = !nonNestedFeatures.isEmpty()
+            || (contactFeatureStarts != null && !contactFeatureStarts
+                    .isEmpty())
+            || (nonPositionalFeatures != null && !nonPositionalFeatures
+                    .isEmpty())
+            || (nestedFeatures != null && nestedFeatures.size() > 0);
+
+    return !hasFeatures;
+  }
+
+  /**
+   * Answers the set of distinct feature groups stored, possibly including null,
+   * as an unmodifiable view of the set. The parameter determines whether the
+   * groups for positional or for non-positional features are returned.
+   * 
+   * @param positionalFeatures
+   * @return
+   */
+  public Set<String> getFeatureGroups(boolean positionalFeatures)
+  {
+    if (positionalFeatures)
+    {
+      return Collections.unmodifiableSet(positionalFeatureGroups);
+    }
+    else
+    {
+      return nonPositionalFeatureGroups == null ? Collections
+              .<String> emptySet() : Collections
+              .unmodifiableSet(nonPositionalFeatureGroups);
+    }
+  }
+
+  /**
+   * Performs a binary search of the (sorted) list to find the index of the
+   * first entry which returns true for the given comparator function. Returns
+   * the length of the list if there is no such entry.
+   * 
+   * @param features
+   * @param sc
+   * @return
+   */
+  protected static int binarySearch(List<SequenceFeature> features,
+          SearchCriterion sc)
+  {
+    int start = 0;
+    int end = features.size() - 1;
+    int matched = features.size();
+
+    while (start <= end)
+    {
+      int mid = (start + end) / 2;
+      SequenceFeature entry = features.get(mid);
+      boolean compare = sc.compare(entry);
+      if (compare)
+      {
+        matched = mid;
+        end = mid - 1;
+      }
+      else
+      {
+        start = mid + 1;
+      }
+    }
+
+    return matched;
+  }
+
+  /**
+   * Answers the number of positional (or non-positional) features stored.
+   * Contact features count as 1.
+   * 
+   * @param positional
+   * @return
+   */
+  public int getFeatureCount(boolean positional)
+  {
+    if (!positional)
+    {
+      return nonPositionalFeatures == null ? 0 : nonPositionalFeatures
+              .size();
+    }
+
+    int size = nonNestedFeatures.size();
+
+    if (contactFeatureStarts != null)
+    {
+      // note a contact feature (start/end) counts as one
+      size += contactFeatureStarts.size();
+    }
+
+    if (nestedFeatures != null)
+    {
+      size += nestedFeatures.size();
+    }
+
+    return size;
+  }
+
+  /**
+   * Answers the total length of positional features (or zero if there are
+   * none). Contact features contribute a value of 1 to the total.
+   * 
+   * @return
+   */
+  public int getTotalFeatureLength()
+  {
+    return totalExtent;
+  }
+
+  /**
+   * Answers the minimum score held for positional or non-positional features.
+   * This may be Float.NaN if there are no features, are none has a non-NaN
+   * score.
+   * 
+   * @param positional
+   * @return
+   */
+  public float getMinimumScore(boolean positional)
+  {
+    return positional ? positionalMinScore : nonPositionalMinScore;
+  }
+
+  /**
+   * Answers the maximum score held for positional or non-positional features.
+   * This may be Float.NaN if there are no features, are none has a non-NaN
+   * score.
+   * 
+   * @param positional
+   * @return
+   */
+  public float getMaximumScore(boolean positional)
+  {
+    return positional ? positionalMaxScore : nonPositionalMaxScore;
+  }
+
+  /**
+   * Answers a list of all either positional or non-positional features whose
+   * feature group matches the given group (which may be null)
+   * 
+   * @param positional
+   * @param group
+   * @return
+   */
+  public List<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    /*
+     * if we know features don't include the target group, no need
+     * to inspect them for matches
+     */
+    if (positional && !positionalFeatureGroups.contains(group)
+            || !positional && !nonPositionalFeatureGroups.contains(group))
+    {
+      return result;
+    }
+
+    List<SequenceFeature> sfs = positional ? getPositionalFeatures()
+            : getNonPositionalFeatures();
+    for (SequenceFeature sf : sfs)
+    {
+      String featureGroup = sf.getFeatureGroup();
+      if (group == null && featureGroup == null || group != null
+              && group.equals(featureGroup))
+      {
+        result.add(sf);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Adds the shift amount to the start and end of all positional features whose
+   * start position is at or after fromPosition. Returns true if at least one
+   * feature was shifted, else false.
+   * 
+   * @param fromPosition
+   * @param shiftBy
+   * @return
+   */
+  public synchronized boolean shiftFeatures(int fromPosition, int shiftBy)
+  {
+    /*
+     * Because begin and end are final fields (to ensure the data store's
+     * integrity), we have to delete each feature and re-add it as amended.
+     * (Although a simple shift of all values would preserve data integrity!)
+     */
+    boolean modified = false;
+    for (SequenceFeature sf : getPositionalFeatures())
+    {
+      if (sf.getBegin() >= fromPosition)
+      {
+        modified = true;
+        int newBegin = sf.getBegin() + shiftBy;
+        int newEnd = sf.getEnd() + shiftBy;
+
+        /*
+         * sanity check: don't shift left of the first residue
+         */
+        if (newEnd > 0)
+        {
+          newBegin = Math.max(1, newBegin);
+          SequenceFeature sf2 = new SequenceFeature(sf, newBegin, newEnd,
+                  sf.getFeatureGroup(), sf.getScore());
+          addFeature(sf2);
+        }
+        delete(sf);
+      }
+    }
+    return modified;
+  }
+}
diff --git a/src/jalview/datamodel/features/NCList.java b/src/jalview/datamodel/features/NCList.java
new file mode 100644 (file)
index 0000000..ae58a69
--- /dev/null
@@ -0,0 +1,646 @@
+/*
+ * 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;
+import jalview.datamodel.Range;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An adapted implementation of NCList as described in the paper
+ * 
+ * <pre>
+ * Nested Containment List (NCList): a new algorithm for accelerating
+ * interval query of genome alignment and interval databases
+ * - Alexander V. Alekseyenko, Christopher J. Lee
+ * https://doi.org/10.1093/bioinformatics/btl647
+ * </pre>
+ */
+public class NCList<T extends ContiguousI>
+{
+  /*
+   * the number of ranges represented
+   */
+  private int size;
+
+  /*
+   * a list, in start position order, of sublists of ranges ordered so 
+   * that each contains (or is the same as) the one that follows it
+   */
+  private List<NCNode<T>> subranges;
+
+  /**
+   * Constructor given a list of things that are each located on a contiguous
+   * interval. Note that the constructor may reorder the list.
+   * <p>
+   * We assume here that for each range, start &lt;= end. Behaviour for reverse
+   * ordered ranges is undefined.
+   * 
+   * @param ranges
+   */
+  public NCList(List<T> ranges)
+  {
+    this();
+    build(ranges);
+  }
+
+  /**
+   * Sort and group ranges into sublists where each sublist represents a region
+   * and its contained subregions
+   * 
+   * @param ranges
+   */
+  protected void build(List<T> ranges)
+  {
+    /*
+     * sort by start ascending so that contained intervals 
+     * follow their containing interval
+     */
+    Collections.sort(ranges, RangeComparator.BY_START_POSITION);
+
+    List<Range> sublists = buildSubranges(ranges);
+
+    /*
+     * convert each subrange to an NCNode consisting of a range and
+     * (possibly) its contained NCList
+     */
+    for (Range sublist : sublists)
+    {
+      subranges.add(new NCNode<T>(ranges.subList(sublist.start,
+              sublist.end + 1)));
+    }
+
+    size = ranges.size();
+  }
+
+  public NCList(T entry)
+  {
+    this();
+    subranges.add(new NCNode<>(entry));
+    size = 1;
+  }
+
+  public NCList()
+  {
+    subranges = new ArrayList<NCNode<T>>();
+  }
+
+  /**
+   * Traverses the sorted ranges to identify sublists, within which each
+   * interval contains the one that follows it
+   * 
+   * @param ranges
+   * @return
+   */
+  protected List<Range> buildSubranges(List<T> ranges)
+  {
+    List<Range> sublists = new ArrayList<>();
+    
+    if (ranges.isEmpty())
+    {
+      return sublists;
+    }
+
+    int listStartIndex = 0;
+    long lastEndPos = Long.MAX_VALUE;
+
+    for (int i = 0; i < ranges.size(); i++)
+    {
+      ContiguousI nextInterval = ranges.get(i);
+      long nextStart = nextInterval.getBegin();
+      long nextEnd = nextInterval.getEnd();
+      if (nextStart > lastEndPos || nextEnd > lastEndPos)
+      {
+        /*
+         * this interval is not contained in the preceding one 
+         * close off the last sublist
+         */
+        sublists.add(new Range(listStartIndex, i - 1));
+        listStartIndex = i;
+      }
+      lastEndPos = nextEnd;
+    }
+
+    sublists.add(new Range(listStartIndex, ranges.size() - 1));
+    return sublists;
+  }
+
+  /**
+   * Adds one entry to the stored set (with duplicates allowed)
+   * 
+   * @param entry
+   */
+  public void add(T entry)
+  {
+    add(entry, true);
+  }
+
+  /**
+   * Adds one entry to the stored set, and returns true, unless allowDuplicates
+   * is set to false and it is already contained (by object equality test), in
+   * which case it is not added and this method returns false.
+   * 
+   * @param entry
+   * @param allowDuplicates
+   * @return
+   */
+  public synchronized boolean add(T entry, boolean allowDuplicates)
+  {
+    if (!allowDuplicates && contains(entry))
+    {
+      return false;
+    }
+
+    size++;
+    long start = entry.getBegin();
+    long end = entry.getEnd();
+
+    /*
+     * cases:
+     * - precedes all subranges: add as NCNode on front of list
+     * - follows all subranges: add as NCNode on end of list
+     * - enclosed by a subrange - add recursively to subrange
+     * - encloses one or more subranges - push them inside it
+     * - none of the above - add as a new node and resort nodes list (?)
+     */
+
+    /*
+     * find the first subrange whose end does not precede entry's start
+     */
+    int candidateIndex = findFirstOverlap(start);
+    if (candidateIndex == -1)
+    {
+      /*
+       * all subranges precede this one - add it on the end
+       */
+      subranges.add(new NCNode<>(entry));
+      return true;
+    }
+
+    /*
+     * search for maximal span of subranges i-k that the new entry
+     * encloses; or a subrange that encloses the new entry
+     */
+    boolean enclosing = false;
+    int firstEnclosed = 0;
+    int lastEnclosed = 0;
+    boolean overlapping = false;
+
+    for (int j = candidateIndex; j < subranges.size(); j++)
+    {
+      NCNode<T> subrange = subranges.get(j);
+
+      if (end < subrange.getBegin() && !overlapping && !enclosing)
+      {
+        /*
+         * new entry lies between subranges j-1 j
+         */
+        subranges.add(j, new NCNode<>(entry));
+        return true;
+      }
+
+      if (subrange.getBegin() <= start && subrange.getEnd() >= end)
+      {
+        /*
+         * push new entry inside this subrange as it encloses it
+         */
+        subrange.add(entry);
+        return true;
+      }
+      
+      if (start <= subrange.getBegin())
+      {
+        if (end >= subrange.getEnd())
+        {
+          /*
+           * new entry encloses this subrange (and possibly preceding ones);
+           * continue to find the maximal list it encloses
+           */
+          if (!enclosing)
+          {
+            firstEnclosed = j;
+          }
+          lastEnclosed = j;
+          enclosing = true;
+          continue;
+        }
+        else
+        {
+          /*
+           * entry spans from before this subrange to inside it
+           */
+          if (enclosing)
+          {
+            /*
+             * entry encloses one or more preceding subranges
+             */
+            addEnclosingRange(entry, firstEnclosed, lastEnclosed);
+            return true;
+          }
+          else
+          {
+            /*
+             * entry spans two subranges but doesn't enclose any
+             * so just add it 
+             */
+            subranges.add(j, new NCNode<>(entry));
+            return true;
+          }
+        }
+      }
+      else
+      {
+        overlapping = true;
+      }
+    }
+
+    /*
+     * drops through to here if new range encloses all others
+     * or overlaps the last one
+     */
+    if (enclosing)
+    {
+      addEnclosingRange(entry, firstEnclosed, lastEnclosed);
+    }
+    else
+    {
+      subranges.add(new NCNode<>(entry));
+    }
+
+    return true;
+  }
+  
+  /**
+   * Answers true if this NCList contains the given entry (by object equality
+   * test), else false
+   * 
+   * @param entry
+   * @return
+   */
+  public boolean contains(T entry)
+  {
+    /*
+     * find the first sublist that might overlap, i.e. 
+     * the first whose end position is >= from
+     */
+    int candidateIndex = findFirstOverlap(entry.getBegin());
+
+    if (candidateIndex == -1)
+    {
+      return false;
+    }
+
+    int to = entry.getEnd();
+
+    for (int i = candidateIndex; i < subranges.size(); i++)
+    {
+      NCNode<T> candidate = subranges.get(i);
+      if (candidate.getBegin() > to)
+      {
+        /*
+         * we are past the end of our target range
+         */
+        break;
+      }
+      if (candidate.contains(entry))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Update the tree so that the range of the new entry encloses subranges i to
+   * j (inclusive). That is, replace subranges i-j (inclusive) with a new
+   * subrange that contains them.
+   * 
+   * @param entry
+   * @param i
+   * @param j
+   */
+  protected synchronized void addEnclosingRange(T entry, final int i,
+          final int j)
+  {
+    NCList<T> newNCList = new NCList<>();
+    newNCList.addNodes(subranges.subList(i, j + 1));
+    NCNode<T> newNode = new NCNode<>(entry, newNCList);
+    for (int k = j; k >= i; k--)
+    {
+      subranges.remove(k);
+    }
+    subranges.add(i, newNode);
+  }
+
+  protected void addNodes(List<NCNode<T>> nodes)
+  {
+    for (NCNode<T> node : nodes)
+    {
+      subranges.add(node);
+      size += node.size();
+    }
+  }
+
+  /**
+   * Returns a (possibly empty) list of items whose extent overlaps the given
+   * range
+   * 
+   * @param from
+   *          start of overlap range (inclusive)
+   * @param to
+   *          end of overlap range (inclusive)
+   * @return
+   */
+  public List<T> findOverlaps(long from, long to)
+  {
+    List<T> result = new ArrayList<>();
+
+    findOverlaps(from, to, result);
+    
+    return result;
+  }
+
+  /**
+   * Recursively searches the NCList adding any items that overlap the from-to
+   * range to the result list
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  protected void findOverlaps(long from, long to, List<T> result)
+  {
+    /*
+     * find the first sublist that might overlap, i.e. 
+     * the first whose end position is >= from
+     */
+    int candidateIndex = findFirstOverlap(from);
+
+    if (candidateIndex == -1)
+    {
+      return;
+    }
+
+    for (int i = candidateIndex; i < subranges.size(); i++)
+    {
+      NCNode<T> candidate = subranges.get(i);
+      if (candidate.getBegin() > to)
+      {
+        /*
+         * we are past the end of our target range
+         */
+        break;
+      }
+      candidate.findOverlaps(from, to, result);
+    }
+
+  }
+
+  /**
+   * Search subranges for the first one whose end position is not before the
+   * target range's start position, i.e. the first one that may overlap the
+   * target range. Returns the index in the list of the first such range found,
+   * or -1 if none found.
+   * 
+   * @param from
+   * @return
+   */
+  protected int findFirstOverlap(long from)
+  {
+    /*
+     * The NCList paper describes binary search for this step,
+     * but this not implemented here as (a) I haven't understood it yet
+     * and (b) it seems to imply complications for adding to an NCList
+     */
+
+    int i = 0;
+    if (subranges != null)
+    {
+      for (NCNode<T> subrange : subranges)
+      {
+        if (subrange.getEnd() >= from)
+        {
+          return i;
+        }
+        i++;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Formats the tree as a bracketed list e.g.
+   * 
+   * <pre>
+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * </pre>
+   */
+  @Override
+  public String toString()
+  {
+    return subranges.toString();
+  }
+
+  /**
+   * Returns a string representation of the data where containment is shown by
+   * indentation on new lines
+   * 
+   * @return
+   */
+  public String prettyPrint()
+  {
+    StringBuilder sb = new StringBuilder(512);
+    int offset = 0;
+    int indent = 2;
+    prettyPrint(sb, offset, indent);
+    sb.append(System.lineSeparator());
+    return sb.toString();
+  }
+
+  /**
+   * @param sb
+   * @param offset
+   * @param indent
+   */
+  void prettyPrint(StringBuilder sb, int offset, int indent)
+  {
+    boolean first = true;
+    for (NCNode<T> subrange : subranges)
+    {
+      if (!first)
+      {
+        sb.append(System.lineSeparator());
+      }
+      first = false;
+      subrange.prettyPrint(sb, offset, indent);
+    }
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList, else false.
+   * 
+   * @return
+   */
+  public boolean isValid()
+  {
+    return isValid(Integer.MIN_VALUE, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList bounded within the given start-end range, else false.
+   * <p>
+   * Each subrange must lie within start-end (inclusive). Subranges must be
+   * ordered by start position ascending.
+   * <p>
+   * 
+   * @param start
+   * @param end
+   * @return
+   */
+  boolean isValid(final int start, final int end)
+  {
+    int lastStart = start;
+    for (NCNode<T> subrange : subranges)
+    {
+      if (subrange.getBegin() < lastStart)
+      {
+        System.err.println("error in NCList: range " + subrange.toString()
+                + " starts before " + lastStart);
+        return false;
+      }
+      if (subrange.getEnd() > end)
+      {
+        System.err.println("error in NCList: range " + subrange.toString()
+                + " ends after " + end);
+        return false;
+      }
+      lastStart = subrange.getBegin();
+
+      if (!subrange.isValid())
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Answers the lowest start position enclosed by the ranges
+   * 
+   * @return
+   */
+  public int getStart()
+  {
+    return subranges.isEmpty() ? 0 : subranges.get(0).getBegin();
+  }
+
+  /**
+   * Returns the number of ranges held (deep count)
+   * 
+   * @return
+   */
+  public int size()
+  {
+    return size;
+  }
+
+  /**
+   * Returns a list of all entries stored
+   * 
+   * @return
+   */
+  public List<T> getEntries()
+  {
+    List<T> result = new ArrayList<>();
+    getEntries(result);
+    return result;
+  }
+
+  /**
+   * Adds all contained entries to the given list
+   * 
+   * @param result
+   */
+  void getEntries(List<T> result)
+  {
+    for (NCNode<T> subrange : subranges)
+    {
+      subrange.getEntries(result);
+    }
+  }
+
+  /**
+   * Deletes the given entry from the store, returning true if it was found (and
+   * deleted), else false. This method makes no assumption that the entry is in
+   * the 'expected' place in the store, in case it has been modified since it
+   * was added. Only the first 'same object' match is deleted, not 'equal' or
+   * multiple objects.
+   * 
+   * @param entry
+   */
+  public synchronized boolean delete(T entry)
+  {
+    if (entry == null)
+    {
+      return false;
+    }
+    for (int i = 0; i < subranges.size(); i++)
+    {
+      NCNode<T> subrange = subranges.get(i);
+      NCList<T> subRegions = subrange.getSubRegions();
+
+      if (subrange.getRegion() == entry)
+      {
+        /*
+         * if the subrange is rooted on this entry, promote its
+         * subregions (if any) to replace the subrange here;
+         * NB have to resort subranges after doing this since e.g.
+         * [10-30 [12-20 [16-18], 13-19]]
+         * after deleting 12-20, 16-18 is promoted to sibling of 13-19
+         * but should follow it in the list of subranges of 10-30 
+         */
+        subranges.remove(i);
+        if (subRegions != null)
+        {
+          subranges.addAll(subRegions.subranges);
+          Collections.sort(subranges, RangeComparator.BY_START_POSITION);
+        }
+        size--;
+        return true;
+      }
+      else
+      {
+        if (subRegions != null && subRegions.delete(entry))
+        {
+          size--;
+          subrange.deleteSubRegionsIfEmpty();
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/src/jalview/datamodel/features/NCNode.java b/src/jalview/datamodel/features/NCNode.java
new file mode 100644 (file)
index 0000000..b991750
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Each node of the NCList tree consists of a range, and (optionally) the NCList
+ * of ranges it encloses
+ *
+ * @param <V>
+ */
+class NCNode<V extends ContiguousI> implements ContiguousI
+{
+  /*
+   * deep size (number of ranges included)
+   */
+  private int size;
+
+  private V region;
+
+  /*
+   * null, or an object holding contained subregions of this nodes region
+   */
+  private NCList<V> subregions;
+
+  /**
+   * Constructor given a list of ranges
+   * 
+   * @param ranges
+   */
+  NCNode(List<V> ranges)
+  {
+    build(ranges);
+  }
+
+  /**
+   * Constructor given a single range
+   * 
+   * @param range
+   */
+  NCNode(V range)
+  {
+    List<V> ranges = new ArrayList<>();
+    ranges.add(range);
+    build(ranges);
+  }
+
+  NCNode(V entry, NCList<V> newNCList)
+  {
+    region = entry;
+    subregions = newNCList;
+    size = 1 + newNCList.size();
+  }
+
+  /**
+   * @param ranges
+   */
+  protected void build(List<V> ranges)
+  {
+    size = ranges.size();
+
+    if (!ranges.isEmpty())
+    {
+      region = ranges.get(0);
+    }
+    if (ranges.size() > 1)
+    {
+      subregions = new NCList<V>(ranges.subList(1, ranges.size()));
+    }
+  }
+
+  @Override
+  public int getBegin()
+  {
+    return region.getBegin();
+  }
+
+  @Override
+  public int getEnd()
+  {
+    return region.getEnd();
+  }
+
+  /**
+   * Formats the node as a bracketed list e.g.
+   * 
+   * <pre>
+   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
+   * </pre>
+   */
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(10 * size);
+    sb.append(region.getBegin()).append("-").append(region.getEnd());
+    if (subregions != null)
+    {
+      sb.append(" ").append(subregions.toString());
+    }
+    return sb.toString();
+  }
+
+  void prettyPrint(StringBuilder sb, int offset, int indent) {
+    for (int i = 0 ; i < offset ; i++) {
+      sb.append(" ");
+    }
+    sb.append(region.getBegin()).append("-").append(region.getEnd());
+    if (subregions != null)
+    {
+      sb.append(System.lineSeparator());
+      subregions.prettyPrint(sb, offset + 2, indent);
+    }
+  }
+  /**
+   * Add any ranges that overlap the from-to range to the result list
+   * 
+   * @param from
+   * @param to
+   * @param result
+   */
+  void findOverlaps(long from, long to, List<V> result)
+  {
+    if (region.getBegin() <= to && region.getEnd() >= from)
+    {
+      result.add(region);
+    }
+    if (subregions != null)
+    {
+      subregions.findOverlaps(from, to, result);
+    }
+  }
+
+  /**
+   * Add one range to this subrange
+   * 
+   * @param entry
+   */
+  synchronized void add(V entry)
+  {
+    if (entry.getBegin() < region.getBegin() || entry.getEnd() > region.getEnd()) {
+      throw new IllegalArgumentException(String.format(
+              "adding improper subrange %d-%d to range %d-%d",
+              entry.getBegin(), entry.getEnd(), region.getBegin(),
+              region.getEnd()));
+    }
+    if (subregions == null)
+    {
+      subregions = new NCList<V>(entry);
+    }
+    else
+    {
+      subregions.add(entry);
+    }
+    size++;
+  }
+
+  /**
+   * Answers true if the data held satisfy the rules of construction of an
+   * NCList, else false.
+   * 
+   * @return
+   */
+  boolean isValid()
+  {
+    /*
+     * we don't handle reverse ranges
+     */
+    if (region != null && region.getBegin() > region.getEnd())
+    {
+      return false;
+    }
+    if (subregions == null)
+    {
+      return true;
+    }
+    return subregions.isValid(getBegin(), getEnd());
+  }
+
+  /**
+   * Adds all contained entries to the given list
+   * 
+   * @param entries
+   */
+  void getEntries(List<V> entries)
+  {
+    entries.add(region);
+    if (subregions != null)
+    {
+      subregions.getEntries(entries);
+    }
+  }
+
+  /**
+   * Answers true if this object contains the given entry (by object equals
+   * test), else false
+   * 
+   * @param entry
+   * @return
+   */
+  boolean contains(V entry)
+  {
+    if (entry == null)
+    {
+      return false;
+    }
+    if (entry.equals(region))
+    {
+      return true;
+    }
+    return subregions == null ? false : subregions.contains(entry);
+  }
+
+  /**
+   * Answers the 'root' region modelled by this object
+   * 
+   * @return
+   */
+  V getRegion()
+  {
+    return region;
+  }
+
+  /**
+   * Answers the (possibly null) contained regions within this object
+   * 
+   * @return
+   */
+  NCList<V> getSubRegions()
+  {
+    return subregions;
+  }
+
+  /**
+   * Nulls the subregion reference if it is empty (after a delete entry
+   * operation)
+   */
+  void deleteSubRegionsIfEmpty()
+  {
+    if (subregions != null && subregions.size() == 0)
+    {
+      subregions = null;
+    }
+  }
+
+  /**
+   * Answers the (deep) size of this node i.e. the number of ranges it models
+   * 
+   * @return
+   */
+  int size()
+  {
+    return size;
+  }
+}
diff --git a/src/jalview/datamodel/features/RangeComparator.java b/src/jalview/datamodel/features/RangeComparator.java
new file mode 100644 (file)
index 0000000..b7d702d
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import java.util.Comparator;
+
+/**
+ * A comparator that orders ranges by either start position or end position
+ * ascending. If the position matches, ordering is resolved by end position (or
+ * start position).
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class RangeComparator implements Comparator<ContiguousI>
+{
+  public static final Comparator<ContiguousI> BY_START_POSITION = new RangeComparator(
+          true);
+
+  public static final Comparator<ContiguousI> BY_END_POSITION = new RangeComparator(
+          false);
+
+  boolean byStart;
+
+  /**
+   * Constructor
+   * 
+   * @param byStartPosition
+   *          if true, order based on start position, if false by end position
+   */
+  RangeComparator(boolean byStartPosition)
+  {
+    byStart = byStartPosition;
+  }
+
+  @Override
+  public int compare(ContiguousI o1, ContiguousI o2)
+  {
+    int len1 = o1.getEnd() - o1.getBegin();
+    int len2 = o2.getEnd() - o2.getBegin();
+
+    if (byStart)
+    {
+      return compare(o1.getBegin(), o2.getBegin(), len1, len2);
+    }
+    else
+    {
+      return compare(o1.getEnd(), o2.getEnd(), len1, len2);
+    }
+  }
+
+  /**
+   * Compares two ranges for ordering
+   * 
+   * @param pos1
+   *          first range positional ordering criterion
+   * @param pos2
+   *          second range positional ordering criterion
+   * @param len1
+   *          first range length ordering criterion
+   * @param len2
+   *          second range length ordering criterion
+   * @return
+   */
+  public int compare(long pos1, long pos2, int len1, int len2)
+  {
+    int order = Long.compare(pos1, pos2);
+    if (order == 0)
+    {
+      /*
+       * if tied on position order, longer length sorts to left
+       * i.e. the negation of normal ordering by length
+       */
+      order = -Integer.compare(len1, len2);
+    }
+    return order;
+  }
+}
diff --git a/src/jalview/datamodel/features/SequenceFeatures.java b/src/jalview/datamodel/features/SequenceFeatures.java
new file mode 100644 (file)
index 0000000..8f965b4
--- /dev/null
@@ -0,0 +1,493 @@
+/*
+ * 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;
+import jalview.datamodel.SequenceFeature;
+import jalview.io.gff.SequenceOntologyFactory;
+import jalview.io.gff.SequenceOntologyI;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * A class that stores sequence features in a way that supports efficient
+ * querying by type and location (overlap). Intended for (but not limited to)
+ * storage of features for one sequence.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class SequenceFeatures implements SequenceFeaturesI
+{
+  /**
+   * a comparator for sorting features by start position ascending
+   */
+  private static Comparator<ContiguousI> FORWARD_STRAND = new Comparator<ContiguousI>()
+  {
+    @Override
+    public int compare(ContiguousI o1, ContiguousI o2)
+    {
+      return Integer.compare(o1.getBegin(), o2.getBegin());
+    }
+  };
+
+  /**
+   * a comparator for sorting features by end position descending
+   */
+  private static Comparator<ContiguousI> REVERSE_STRAND = new Comparator<ContiguousI>()
+  {
+    @Override
+    public int compare(ContiguousI o1, ContiguousI o2)
+    {
+      return Integer.compare(o2.getEnd(), o1.getEnd());
+    }
+  };
+
+  /*
+   * map from feature type to structured store of features for that type
+   * null types are permitted (but not a good idea!)
+   */
+  private Map<String, FeatureStore> featureStore;
+
+  /**
+   * Constructor
+   */
+  public SequenceFeatures()
+  {
+    /*
+     * use a TreeMap so that features are returned in alphabetical order of type
+     * ? wrap as a synchronized map for add and delete operations
+     */
+    // featureStore = Collections
+    // .synchronizedSortedMap(new TreeMap<String, FeatureStore>());
+    featureStore = new TreeMap<>();
+  }
+
+  /**
+   * Constructor given a list of features
+   */
+  public SequenceFeatures(List<SequenceFeature> features)
+  {
+    this();
+    if (features != null)
+    {
+      for (SequenceFeature feature : features)
+      {
+        add(feature);
+      }
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean add(SequenceFeature sf)
+  {
+    String type = sf.getType();
+    if (type == null)
+    {
+      System.err.println("Feature type may not be null: " + sf.toString());
+      return false;
+    }
+
+    if (featureStore.get(type) == null)
+    {
+      featureStore.put(type, new FeatureStore());
+    }
+    return featureStore.get(type).addFeature(sf);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeatures(int from, int to,
+          String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.findOverlappingFeatures(from, to));
+    }
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getAllFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    result.addAll(getPositionalFeatures(type));
+
+    result.addAll(getNonPositionalFeatures());
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getFeaturesByOntology(String... ontologyTerm)
+  {
+    if (ontologyTerm == null || ontologyTerm.length == 0)
+    {
+      return new ArrayList<>();
+    }
+
+    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()]));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getFeatureCount(boolean positional, String... type)
+  {
+    int result = 0;
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result += featureSet.getFeatureCount(positional);
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getTotalFeatureLength(String... type)
+  {
+    int result = 0;
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result += featureSet.getTotalFeatureLength();
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getPositionalFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.getPositionalFeatures());
+    }
+    return result;
+  }
+
+  /**
+   * A convenience method that converts a vararg for feature types to an
+   * Iterable over matched feature sets in key order
+   * 
+   * @param type
+   * @return
+   */
+  protected Iterable<FeatureStore> varargToTypes(String... type)
+  {
+    if (type == null || type.length == 0)
+    {
+      /*
+       * no vararg parameter supplied - return all
+       */
+      return featureStore.values();
+    }
+
+    List<FeatureStore> types = new ArrayList<>();
+    List<String> args = Arrays.asList(type);
+    for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
+    {
+      if (args.contains(featureType.getKey()))
+      {
+        types.add(featureType.getValue());
+      }
+    }
+    return types;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getContactFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.getContactFeatures());
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getNonPositionalFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      result.addAll(featureSet.getNonPositionalFeatures());
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean delete(SequenceFeature sf)
+  {
+    for (FeatureStore featureSet : featureStore.values())
+    {
+      if (featureSet.delete(sf))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean hasFeatures()
+  {
+    for (FeatureStore featureSet : featureStore.values())
+    {
+      if (!featureSet.isEmpty())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureGroups(boolean positionalFeatures,
+          String... type)
+  {
+    Set<String> groups = new HashSet<>();
+
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      groups.addAll(featureSet.getFeatureGroups(positionalFeatures));
+    }
+
+    return groups;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureTypesForGroups(boolean positionalFeatures,
+          String... groups)
+  {
+    Set<String> result = new HashSet<>();
+
+    for (Entry<String, FeatureStore> featureType : featureStore.entrySet())
+    {
+      Set<String> featureGroups = featureType.getValue().getFeatureGroups(
+              positionalFeatures);
+      for (String group : groups)
+      {
+        if (featureGroups.contains(group))
+        {
+          /*
+           * yes this feature type includes one of the query groups
+           */
+          result.add(featureType.getKey());
+          break;
+        }
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureTypes(String... soTerm)
+  {
+    Set<String> types = new HashSet<>();
+    for (Entry<String, FeatureStore> entry : featureStore.entrySet())
+    {
+      String type = entry.getKey();
+      if (!entry.getValue().isEmpty() && isOntologyTerm(type, soTerm))
+      {
+        types.add(type);
+      }
+    }
+    return types;
+  }
+
+  /**
+   * Answers true if the given type matches one of the specified terms (or is a
+   * sub-type of one in the Sequence Ontology), or if no terms are supplied.
+   * Answers false if filter terms are specified and the given term does not
+   * match any of them.
+   * 
+   * @param type
+   * @param soTerm
+   * @return
+   */
+  protected boolean isOntologyTerm(String type, String... soTerm)
+  {
+    if (soTerm == null || soTerm.length == 0)
+    {
+      return true;
+    }
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    for (String term : soTerm)
+    {
+      if (type.equals(term) || so.isA(type, term))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public float getMinimumScore(String type, boolean positional)
+  {
+    return featureStore.containsKey(type) ? featureStore.get(type)
+            .getMinimumScore(positional) : Float.NaN;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public float getMaximumScore(String type, boolean positional)
+  {
+    return featureStore.containsKey(type) ? featureStore.get(type)
+            .getMaximumScore(positional) : Float.NaN;
+  }
+
+  /**
+   * A convenience method to sort features by start position ascending (if on
+   * forward strand), or end position descending (if on reverse strand)
+   * 
+   * @param features
+   * @param forwardStrand
+   */
+  public static void sortFeatures(List<SequenceFeature> features,
+          final boolean forwardStrand)
+  {
+    Collections.sort(features, forwardStrand ? FORWARD_STRAND
+            : REVERSE_STRAND);
+  }
+
+  /**
+   * {@inheritDoc} This method is 'semi-optimised': it only inspects features
+   * for types that include the specified group, but has to inspect every
+   * feature of those types for matching feature group. This is efficient unless
+   * a sequence has features that share the same type but are in different
+   * groups - an unlikely case.
+   * <p>
+   * For example, if RESNUM feature is created with group = PDBID, then features
+   * would only be retrieved for those sequences associated with the target
+   * PDBID (group).
+   */
+  @Override
+  public List<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group, String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+    for (FeatureStore featureSet : varargToTypes(type))
+    {
+      if (featureSet.getFeatureGroups(positional).contains(group))
+      {
+        result.addAll(featureSet.getFeaturesForGroup(positional, group));
+      }
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean shiftFeatures(int fromPosition, int shiftBy)
+  {
+    boolean modified = false;
+    for (FeatureStore fs : featureStore.values())
+    {
+      modified |= fs.shiftFeatures(fromPosition, shiftBy);
+    }
+    return modified;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void deleteAll()
+  {
+    featureStore.clear();
+  }
+}
diff --git a/src/jalview/datamodel/features/SequenceFeaturesI.java b/src/jalview/datamodel/features/SequenceFeaturesI.java
new file mode 100644 (file)
index 0000000..ca0283e
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Set;
+
+public interface SequenceFeaturesI
+{
+
+  /**
+   * Adds one sequence feature to the store, and returns true, unless the
+   * feature is already contained in the store, in which case this method
+   * returns false. Containment is determined by SequenceFeature.equals()
+   * comparison. Answers false, and does not add the feature, if feature type is
+   * null.
+   * 
+   * @param sf
+   */
+  boolean add(SequenceFeature sf);
+
+  /**
+   * Returns a (possibly empty) list of features, optionally restricted to
+   * specified types, which overlap the given (inclusive) sequence position
+   * range
+   * 
+   * @param from
+   * @param to
+   * @param type
+   * @return
+   */
+  List<SequenceFeature> findFeatures(int from, int to,
+          String... type);
+
+  /**
+   * Answers a list of all features stored, in no particular guaranteed order.
+   * Positional features may optionally be restricted to specified types, but
+   * all non-positional features (if any) are always returned.
+   * <p>
+   * To filter non-positional features by type, use
+   * getNonPositionalFeatures(type).
+   * 
+   * @param type
+   * @return
+   */
+  List<SequenceFeature> getAllFeatures(String... type);
+
+  /**
+   * Answers a list of all positional (or non-positional) features which are in
+   * the specified feature group, optionally restricted to features of specified
+   * types.
+   * 
+   * @param positional
+   *          if true returns positional features, else non-positional features
+   * @param group
+   *          the feature group to be matched (which may be null)
+   * @param type
+   *          optional feature types to filter by
+   * @return
+   */
+  List<SequenceFeature> getFeaturesForGroup(boolean positional,
+          String group, String... type);
+
+  /**
+   * Answers a list of all features stored, whose type either matches, or is a
+   * specialisation (in the Sequence Ontology) of, one of the given terms.
+   * Results are returned in no particular order.
+   * 
+   * @param ontologyTerm
+   * @return
+   */
+  List<SequenceFeature> getFeaturesByOntology(String... ontologyTerm);
+
+  /**
+   * Answers the number of (positional or non-positional) features, optionally
+   * restricted to specified feature types. Contact features are counted as 1.
+   * 
+   * @param positional
+   * @param type
+   * @return
+   */
+  int getFeatureCount(boolean positional, String... type);
+
+  /**
+   * Answers the total length of positional features, optionally restricted to
+   * specified feature types. Contact features are counted as length 1.
+   * 
+   * @param type
+   * @return
+   */
+  int getTotalFeatureLength(String... type);
+
+  /**
+   * Answers a list of all positional features, optionally restricted to
+   * specified types, in no particular guaranteed order
+   * 
+   * @param type
+   * @return
+   */
+  List<SequenceFeature> getPositionalFeatures(
+          String... type);
+
+  /**
+   * Answers a list of all contact features, optionally restricted to specified
+   * types, in no particular guaranteed order
+   * 
+   * @return
+   */
+  List<SequenceFeature> getContactFeatures(String... type);
+
+  /**
+   * Answers a list of all non-positional features, optionally restricted to
+   * specified types, in no particular guaranteed order
+   * 
+   * @param type
+   *          if no type is specified, all are returned
+   * @return
+   */
+  List<SequenceFeature> getNonPositionalFeatures(
+          String... type);
+
+  /**
+   * Deletes the given feature from the store, returning true if it was found
+   * (and deleted), else false. This method makes no assumption that the feature
+   * is in the 'expected' place in the store, in case it has been modified since
+   * it was added.
+   * 
+   * @param sf
+   */
+  boolean delete(SequenceFeature sf);
+
+  /**
+   * Answers true if this store contains at least one feature, else false
+   * 
+   * @return
+   */
+  boolean hasFeatures();
+
+  /**
+   * Returns a set of the distinct feature groups present in the collection. The
+   * set may include null. The boolean parameter determines whether the groups
+   * for positional or for non-positional features are returned. The optional
+   * type parameter may be used to restrict to groups for specified feature
+   * types.
+   * 
+   * @param positionalFeatures
+   * @param type
+   * @return
+   */
+  Set<String> getFeatureGroups(boolean positionalFeatures,
+          String... type);
+
+  /**
+   * Answers the set of distinct feature types for which there is at least one
+   * feature with one of the given feature group(s). The boolean parameter
+   * determines whether the groups for positional or for non-positional features
+   * are returned.
+   * 
+   * @param positionalFeatures
+   * @param groups
+   * @return
+   */
+  Set<String> getFeatureTypesForGroups(
+          boolean positionalFeatures, String... groups);
+
+  /**
+   * Answers a set of the distinct feature types for which a feature is stored.
+   * The types may optionally be restricted to those which match, or are a
+   * subtype of, given sequence ontology terms
+   * 
+   * @return
+   */
+  Set<String> getFeatureTypes(String... soTerm);
+
+  /**
+   * Answers the minimum score held for positional or non-positional features
+   * for the specified type. This may be Float.NaN if there are no features, or
+   * none has a non-NaN score.
+   * 
+   * @param type
+   * @param positional
+   * @return
+   */
+  float getMinimumScore(String type, boolean positional);
+
+  /**
+   * Answers the maximum score held for positional or non-positional features
+   * for the specified type. This may be Float.NaN if there are no features, or
+   * none has a non-NaN score.
+   * 
+   * @param type
+   * @param positional
+   * @return
+   */
+  float getMaximumScore(String type, boolean positional);
+
+  /**
+   * Adds the shift amount to the start and end of all positional features whose
+   * start position is at or after fromPosition. Returns true if at least one
+   * feature was shifted, else false.
+   * 
+   * @param fromPosition
+   * @param shiftBy
+   */
+  boolean shiftFeatures(int fromPosition, int shiftBy);
+
+  /**
+   * Deletes all positional and non-positional features
+   */
+  void deleteAll();
+}
index 2de100b..bbe6a20 100644 (file)
@@ -371,8 +371,8 @@ public class EmblEntry
         System.err.println(
                 "Implementation Notice: EMBLCDS records not properly supported yet - Making up the CDNA region of this sequence... may be incorrect ("
                         + sourceDb + ":" + getAccession() + ")");
-        if (translationLength
-                * 3 == (1 - codonStart + dna.getSequence().length))
+        int dnaLength = dna.getLength();
+        if (translationLength * 3 == (1 - codonStart + dnaLength))
         {
           System.err.println(
                   "Not allowing for additional stop codon at end of cDNA fragment... !");
@@ -383,8 +383,7 @@ public class EmblEntry
                   new int[]
                   { 1, translationLength }, 3, 1);
         }
-        if ((translationLength + 1)
-                * 3 == (1 - codonStart + dna.getSequence().length))
+        if ((translationLength + 1) * 3 == (1 - codonStart + dnaLength))
         {
           System.err.println(
                   "Allowing for additional stop codon at end of cDNA fragment... will probably cause an error in VAMSAs!");
@@ -456,13 +455,27 @@ public class EmblEntry
       /*
        * add cds features to dna sequence
        */
-      for (int xint = 0; exons != null && xint < exons.length; xint += 2)
+      String cds = feature.getName(); // "CDS"
+      for (int xint = 0; exons != null && xint < exons.length - 1; xint += 2)
       {
-        SequenceFeature sf = makeCdsFeature(exons, xint, proteinName,
-                proteinId, vals, codonStart);
-        sf.setType(feature.getName()); // "CDS"
+        int exonStart = exons[xint];
+        int exonEnd = exons[xint + 1];
+        int begin = Math.min(exonStart, exonEnd);
+        int end = Math.max(exonStart, exonEnd);
+        int exonNumber = xint / 2 + 1;
+        String desc = String.format("Exon %d for protein '%s' EMBLCDS:%s",
+                exonNumber, proteinName, proteinId);
+
+        SequenceFeature sf = makeCdsFeature(cds, desc, begin, end,
+                sourceDb, vals);
+
         sf.setEnaLocation(feature.getLocation());
-        sf.setFeatureGroup(sourceDb);
+        boolean forwardStrand = exonStart <= exonEnd;
+        sf.setStrand(forwardStrand ? "+" : "-");
+        sf.setPhase(String.valueOf(codonStart - 1));
+        sf.setValue(FeatureProperties.EXONPOS, exonNumber);
+        sf.setValue(FeatureProperties.EXONPRODUCT, proteinName);
+
         dna.addSequenceFeature(sf);
       }
     }
@@ -577,33 +590,24 @@ public class EmblEntry
   /**
    * Helper method to construct a SequenceFeature for one cds range
    * 
-   * @param exons
-   *          array of cds [start, end, ...] positions
-   * @param exonStartIndex
-   *          offset into the exons array
-   * @param proteinName
-   * @param proteinAccessionId
+   * @param type
+   *          feature type ("CDS")
+   * @param desc
+   *          description
+   * @param begin
+   *          start position
+   * @param end
+   *          end position
+   * @param group
+   *          feature group
    * @param vals
    *          map of 'miscellaneous values' for feature
-   * @param codonStart
-   *          codon start position for CDS (1/2/3, normally 1)
    * @return
    */
-  protected SequenceFeature makeCdsFeature(int[] exons, int exonStartIndex,
-          String proteinName, String proteinAccessionId,
-          Map<String, String> vals, int codonStart)
-  {
-    int exonNumber = exonStartIndex / 2 + 1;
-    SequenceFeature sf = new SequenceFeature();
-    sf.setBegin(Math.min(exons[exonStartIndex], exons[exonStartIndex + 1]));
-    sf.setEnd(Math.max(exons[exonStartIndex], exons[exonStartIndex + 1]));
-    sf.setDescription(String.format("Exon %d for protein '%s' EMBLCDS:%s",
-            exonNumber, proteinName, proteinAccessionId));
-    sf.setPhase(String.valueOf(codonStart - 1));
-    sf.setStrand(
-            exons[exonStartIndex] <= exons[exonStartIndex + 1] ? "+" : "-");
-    sf.setValue(FeatureProperties.EXONPOS, exonNumber);
-    sf.setValue(FeatureProperties.EXONPRODUCT, proteinName);
+  protected SequenceFeature makeCdsFeature(String type, String desc,
+          int begin, int end, String group, Map<String, String> vals)
+  {
+    SequenceFeature sf = new SequenceFeature(type, desc, begin, end, group);
     if (!vals.isEmpty())
     {
       StringBuilder sb = new StringBuilder();
similarity index 90%
rename from src/jalview/datamodel/UniprotEntry.java
rename to src/jalview/datamodel/xdb/uniprot/UniprotEntry.java
index 4cf0f13..a3537c9 100755 (executable)
@@ -18,7 +18,9 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
+
+import jalview.datamodel.PDBEntry;
 
 import java.util.Vector;
 
@@ -36,7 +38,7 @@ public class UniprotEntry
 
   Vector<String> accession;
 
-  Vector<SequenceFeature> feature;
+  Vector<UniprotFeature> feature;
 
   Vector<PDBEntry> dbrefs;
 
@@ -47,12 +49,12 @@ public class UniprotEntry
     accession = items;
   }
 
-  public void setFeature(Vector<SequenceFeature> items)
+  public void setFeature(Vector<UniprotFeature> items)
   {
     feature = items;
   }
 
-  public Vector<SequenceFeature> getFeature()
+  public Vector<UniprotFeature> getFeature()
   {
     return feature;
   }
diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java b/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java
new file mode 100644 (file)
index 0000000..8bd5652
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+import java.util.Vector;
+
+/**
+ * A data model class for binding from Uniprot XML via uniprot_mapping.xml
+ */
+public class UniprotFeature
+{
+  private String type;
+
+  private String description = null;
+
+  private String original = null;
+
+  private Vector<String> variation = null;
+
+  private String status;
+
+  private int begin;
+
+  private int end;
+
+  public String getType()
+  {
+    return type;
+  }
+
+  public void setType(String t)
+  {
+    this.type = t;
+  }
+
+  public String getDescription()
+  {
+    return description;
+  }
+
+  public void setDescription(String d)
+  {
+    this.description = d;
+  }
+
+  public String getStatus()
+  {
+    return status;
+  }
+
+  public void setStatus(String s)
+  {
+    this.status = s;
+  }
+
+  public int getBegin()
+  {
+    return begin;
+  }
+
+  public void setBegin(int b)
+  {
+    this.begin = b;
+  }
+
+  public int getEnd()
+  {
+    return end;
+  }
+
+  public void setEnd(int e)
+  {
+    this.end = e;
+  }
+
+  public int getPosition()
+  {
+    return begin;
+  }
+
+  public void setPosition(int p)
+  {
+    this.begin = p;
+    this.end = p;
+  }
+
+  public String getOriginal()
+  {
+    return original;
+  }
+
+  public void setOriginal(String original)
+  {
+    this.original = original;
+  }
+
+  public Vector<String> getVariation()
+  {
+    return variation;
+  }
+
+  public void setVariation(Vector<String> variant)
+  {
+    this.variation = variant;
+  }
+}
similarity index 96%
rename from src/jalview/datamodel/UniprotFile.java
rename to src/jalview/datamodel/xdb/uniprot/UniprotFile.java
index f0e38d8..9cc0391 100755 (executable)
@@ -18,7 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
 
 import java.util.Vector;
 
@@ -18,7 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
 
 import java.util.Vector;
 
similarity index 97%
rename from src/jalview/datamodel/UniprotSequence.java
rename to src/jalview/datamodel/xdb/uniprot/UniprotSequence.java
index 1150f1e..bdba73f 100755 (executable)
@@ -18,7 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.datamodel;
+package jalview.datamodel.xdb.uniprot;
 
 /**
  * Data model for the sequence returned by a Uniprot query
index dc000c6..7384327 100644 (file)
 package jalview.ext.ensembl;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import com.stevesoft.pat.Regex;
 
 /**
@@ -109,23 +113,37 @@ public class EnsemblCdna extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true if the sequence feature type is 'exon' (or a subtype of exon
-   * in the Sequence Ontology), and the Parent of the feature is the transcript
-   * we are retrieving
+   * Answers a list of sequence features (if any) whose type is 'exon' (or a
+   * subtype of exon in the Sequence Ontology), and whose Parent is the
+   * transcript we are retrieving
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
-            SequenceOntologyI.EXON))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.EXON);
+    for (SequenceFeature sf : sfs)
     {
       String parentFeature = (String) sf.getValue(PARENT);
       if (("transcript:" + accId).equals(parentFeature))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+
+    return result;
+  }
+
+  /**
+   * Parameter object_type=Transcaript added to ensure cdna and not peptide is
+   * returned (JAL-2529)
+   */
+  @Override
+  protected String getObjectType()
+  {
+    return OBJECT_TYPE_TRANSCRIPT;
   }
 
 }
index 8b2550d..8a71b64 100644 (file)
@@ -102,23 +102,26 @@ public class EnsemblCds extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true if the sequence feature type is 'CDS' (or a subtype of CDS in
-   * the Sequence Ontology), and the Parent of the feature is the transcript we
-   * are retrieving
+   * Answers a list of sequence features (if any) whose type is 'CDS' (or a
+   * subtype of CDS in the Sequence Ontology), and whose Parent is the
+   * transcript we are retrieving
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
-            SequenceOntologyI.CDS))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.CDS);
+    for (SequenceFeature sf : sfs)
     {
       String parentFeature = (String) sf.getValue(PARENT);
       if (("transcript:" + accId).equals(parentFeature))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+    return result;
   }
 
   /**
@@ -130,7 +133,7 @@ public class EnsemblCds extends EnsemblSeqProxy
   protected List<int[]> getCdsRanges(SequenceI dnaSeq)
   {
     int len = dnaSeq.getLength();
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     ranges.add(new int[] { 1, len });
     return ranges;
   }
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..582eac6 100644 (file)
@@ -22,9 +22,11 @@ package jalview.ext.ensembl;
 
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.io.DataSourceType;
 import jalview.io.FeaturesFile;
 import jalview.io.FileParse;
 
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -82,14 +84,15 @@ 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())
+    BufferedReader fp = getSequenceReader(queries);
+    if (fp == null)
     {
       return null;
     }
-    FeaturesFile fr = new FeaturesFile(fp);
+    FeaturesFile fr = new FeaturesFile(
+            new FileParse(fp, null, DataSourceType.URL));
     return new Alignment(fr.getSeqsAsArray());
   }
 
@@ -109,9 +112,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)
     {
@@ -132,13 +143,13 @@ class EnsemblFeatures extends EnsemblRestClient
    * describes the required encoding of the response.
    */
   @Override
-  protected String getRequestMimeType(boolean multipleIds)
+  protected String getRequestMimeType()
   {
     return "text/x-gff3";
   }
 
   /**
-   * Returns the MIME type for GFF3.
+   * Returns the MIME type for GFF3
    */
   @Override
   protected String getResponseMimeType()
index edeeedd..36b19e2 100644 (file)
@@ -23,9 +23,12 @@ 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;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.FeatureColour;
@@ -97,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
@@ -143,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)
@@ -160,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);
           }
         }
       }
@@ -228,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.
@@ -283,22 +312,20 @@ public class EnsemblGene extends EnsemblSeqProxy
    */
   protected void clearGeneFeatures(SequenceI gene)
   {
-    SequenceFeature[] sfs = gene.getSequenceFeatures();
-    if (sfs != null)
+    /*
+     * Note we include NMD_transcript_variant here because it behaves like 
+     * 'transcript' in Ensembl, although strictly speaking it is not 
+     * (it is a sub-type of sequence_variant)    
+     */
+    String[] soTerms = new String[] {
+        SequenceOntologyI.NMD_TRANSCRIPT_VARIANT,
+        SequenceOntologyI.TRANSCRIPT, SequenceOntologyI.EXON,
+        SequenceOntologyI.CDS };
+    List<SequenceFeature> sfs = gene.getFeatures().getFeaturesByOntology(
+            soTerms);
+    for (SequenceFeature sf : sfs)
     {
-      SequenceOntologyI so = SequenceOntologyFactory.getInstance();
-      List<SequenceFeature> filtered = new ArrayList<SequenceFeature>();
-      for (SequenceFeature sf : sfs)
-      {
-        String type = sf.getType();
-        if (!isTranscript(type) && !so.isA(type, SequenceOntologyI.EXON)
-                && !so.isA(type, SequenceOntologyI.CDS))
-        {
-          filtered.add(sf);
-        }
-      }
-      gene.setSequenceFeatures(
-              filtered.toArray(new SequenceFeature[filtered.size()]));
+      gene.deleteFeature(sf);
     }
   }
 
@@ -348,11 +375,12 @@ public class EnsemblGene extends EnsemblSeqProxy
     {
       splices = findFeatures(gene, SequenceOntologyI.CDS, parentId);
     }
+    SequenceFeatures.sortFeatures(splices, true);
 
     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.getSequenceFeatures(),
+    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,23 +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;
-    SequenceFeature[] sfs = geneSequence.getSequenceFeatures();
 
-    if (sfs != null)
+    List<SequenceFeature> sfs = geneSequence.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.TRANSCRIPT);
+    sfs.addAll(geneSequence.getFeatures().getPositionalFeatures(
+            SequenceOntologyI.NMD_TRANSCRIPT_VARIANT));
+
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
+      String parent = (String) sf.getValue(PARENT);
+      if (parentIdentifier.equalsIgnoreCase(parent))
       {
-        if (isTranscript(sf.getType()))
-        {
-          String parent = (String) sf.getValue(PARENT);
-          if (parentIdentifier.equals(parent))
-          {
-            transcriptFeatures.add(sf);
-          }
-        }
+        transcriptFeatures.add(sf);
       }
     }
 
@@ -478,22 +548,27 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true for a feature of type 'gene' (or a sub-type of gene in the
-   * Sequence Ontology), whose ID is the accession we are retrieving
+   * Answers a list of sequence features (if any) whose type is 'gene' (or a
+   * subtype of gene in the Sequence Ontology), and whose ID is the accession we
+   * are retrieving
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
-            SequenceOntologyI.GENE))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.GENE);
+    for (SequenceFeature sf : sfs)
     {
-      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;
+        result.add(sf);
       }
     }
-    return false;
+    return result;
   }
 
   /**
@@ -515,7 +590,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;
       }
@@ -524,17 +599,6 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
-   * Answers false. This allows an optimisation - a single 'gene' feature is all
-   * that is needed to identify the positions of the gene on the genomic
-   * sequence.
-   */
-  @Override
-  protected boolean isSpliceable()
-  {
-    return false;
-  }
-
-  /**
    * Override to do nothing as Ensembl doesn't return a protein sequence for a
    * gene identifier
    */
index 458a233..6684e20 100644 (file)
 package jalview.ext.ensembl;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.io.gff.SequenceOntologyI;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A client to fetch genomic sequence from Ensembl
@@ -94,22 +99,32 @@ public class EnsemblGenome extends EnsemblSeqProxy
   }
 
   /**
-   * Answers true if the sequence feature type is 'transcript' (or a subtype of
-   * transcript in the Sequence Ontology), and the ID of the feature is the
-   * transcript we are retrieving
+   * Answers a list of sequence features (if any) whose type is 'transcript' (or
+   * a subtype of transcript in the Sequence Ontology), and whose ID is the
+   * accession we are retrieving.
+   * <p>
+   * Note we also include features of type "NMD_transcript_variant", although
+   * not strictly 'transcript' in the SO, as they used in Ensembl as if they
+   * were.
    */
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    if (isTranscript(sf.getType()))
+    List<SequenceFeature> result = new ArrayList<>();
+    List<SequenceFeature> sfs = seq.getFeatures().getFeaturesByOntology(
+            SequenceOntologyI.TRANSCRIPT,
+            SequenceOntologyI.NMD_TRANSCRIPT_VARIANT);
+    for (SequenceFeature sf : sfs)
     {
-      String id = (String) sf.getValue(ID);
+      // NB features as gff use 'ID'; rest services return as 'id'
+      String id = (String) sf.getValue("ID");
       if (("transcript:" + accId).equals(id))
       {
-        return true;
+        result.add(sf);
       }
     }
-    return false;
+    return result;
   }
 
 }
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..fa24f1e 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;
+  }
+
+  /**
+   * 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..c6b794a 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);
@@ -103,41 +117,117 @@ public class EnsemblLookup extends EnsemblRestClient
     return true;
   }
 
-  @Override
-  protected String getRequestMimeType(boolean multipleIds)
+  /**
+   * Returns the gene id related to the given identifier (which may be for a
+   * gene, transcript or protein), or null if none is found
+   * 
+   * @param identifier
+   * @return
+   */
+  public String getGeneId(String identifier)
   {
-    return "application/json";
+    return getGeneId(identifier, null);
   }
 
-  @Override
-  protected String getResponseMimeType()
+  /**
+   * Returns the gene id related to the given identifier (which may be for a
+   * gene, transcript or protein), or null if none is found
+   * 
+   * @param identifier
+   * @param objectType
+   * @return
+   */
+  public String getGeneId(String identifier, String objectType)
   {
-    return "application/json";
+    return parseGeneId(getResult(identifier, objectType));
   }
 
   /**
-   * Calls the Ensembl lookup REST endpoint and retrieves the 'Parent' for the
+   * 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)
+  {
+    if (val == null)
+    {
+      return null;
+    }
+    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 +245,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..f01bd4f
--- /dev/null
@@ -0,0 +1,413 @@
+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 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);
+        if (br != 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..0280f16 100644 (file)
@@ -22,7 +22,9 @@ package jalview.ext.ensembl;
 
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import com.stevesoft.pat.Regex;
@@ -108,10 +110,10 @@ public class EnsemblProtein extends EnsemblSeqProxy
   }
 
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    // not applicable - protein sequence is not a 'subset' of genomic sequence
-    return false;
+    return new ArrayList<>();
   }
 
   @Override
index ad6c70c..9e01cc4 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.ext.ensembl;
 
-import jalview.io.DataSourceType;
-import jalview.io.FileParse;
 import jalview.util.StringUtils;
 
 import java.io.BufferedReader;
@@ -31,6 +29,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 +41,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,43 +52,39 @@ 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 = "7.0";
 
-  private static final String LATEST_ENSEMBL_REST_VERSION = "5.0";
+  private static final String LATEST_ENSEMBL_REST_VERSION = "7.0";
 
   private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
 
-  private static Map<String, EnsemblInfo> domainData;
-
-  // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
-  private static final String PING_URL = "http://rest.ensembl.org/info/ping.json";
+  private static Map<String, EnsemblData> domainData;
 
   private final static long AVAILABILITY_RETEST_INTERVAL = 10000L; // 10 seconds
 
   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 +94,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 +121,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()
   {
@@ -180,22 +151,28 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
   protected abstract boolean useGetRequest();
 
   /**
-   * Return the desired value for the Content-Type request header
-   * 
-   * @param multipleIds
+   * Returns the desired value for the Content-Type request header. Default is
+   * application/json, override if required to vary this.
    * 
    * @return
    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
    */
-  protected abstract String getRequestMimeType(boolean multipleIds);
+  protected String getRequestMimeType()
+  {
+    return "application/json";
+  }
 
   /**
-   * Return the desired value for the Accept request header
+   * Return the desired value for the Accept request header. Default is
+   * application/json, override if required to vary this.
    * 
    * @return
    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
    */
-  protected abstract String getResponseMimeType();
+  protected String getResponseMimeType()
+  {
+    return "application/json";
+  }
 
   /**
    * Checks Ensembl's REST 'ping' endpoint, and returns true if response
@@ -204,21 +181,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 +208,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)
@@ -244,25 +226,20 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
   }
 
   /**
-   * returns a reader to a Fasta response from the Ensembl sequence endpoint
+   * Returns a reader to a (Json) response from the Ensembl sequence endpoint.
+   * If the request failed the return value may be null.
    * 
    * @param ids
    * @return
    * @throws IOException
    */
-  protected FileParse getSequenceReader(List<String> ids) throws IOException
+  protected BufferedReader getSequenceReader(List<String> ids)
+          throws IOException
   {
     URL url = getUrl(ids);
 
     BufferedReader reader = getHttpResponse(url, ids);
-    if (reader == null)
-    {
-      // request failed
-      return null;
-    }
-    FileParse fp = new FileParse(reader, url.toString(),
-            DataSourceType.URL);
-    return fp;
+    return reader;
   }
 
   /**
@@ -281,7 +258,8 @@ 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. Returns null if
+   * the HTTP response code was not 200.
    * 
    * @param url
    * @param ids
@@ -290,12 +268,60 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    *          in milliseconds
    * @return
    * @throws IOException
-   *           if response code was not 200, or other I/O error
    */
   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();
 
     /*
@@ -305,8 +331,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
     boolean multipleIds = ids != null && ids.size() > 1;
     connection.setRequestMethod(
             multipleIds ? HttpMethod.POST : HttpMethod.GET);
-    connection.setRequestProperty("Content-Type",
-            getRequestMimeType(multipleIds));
+    connection.setRequestProperty("Content-Type", getRequestMimeType());
     connection.setRequestProperty("Accept", getResponseMimeType());
 
     connection.setUseCaches(false);
@@ -320,77 +345,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 +391,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 +465,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 +535,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 16b858a..7b448fd 100644 (file)
@@ -28,27 +28,32 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.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;
 import jalview.util.DBRefUtils;
+import jalview.util.IntRangeComparator;
 import jalview.util.MapList;
-import jalview.util.RangeComparator;
 
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
 /**
  * Base class for Ensembl sequence fetchers
  * 
@@ -57,12 +62,6 @@ import java.util.List;
  */
 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";
@@ -207,7 +206,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());
@@ -223,7 +223,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))
         {
 
@@ -389,50 +389,44 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       inProgress = false;
       throw new JalviewException("ENSEMBL Rest API not available.");
     }
-    FileParse fp = getSequenceReader(ids);
-    if (fp == null)
+    BufferedReader br = getSequenceReader(ids);
+    if (br == null)
     {
       return alignment;
     }
 
-    FastaFile fr = new FastaFile(fp);
-    if (fr.hasWarningMessage())
+    List<SequenceI> seqs = parseSequenceJson(br);
+
+    if (seqs.isEmpty())
     {
-      System.out.println(
-              String.format("Warning when retrieving %d ids %s\n%s",
-                      ids.size(), ids.toString(), fr.getWarningMessage()));
+      throw new IOException("No data returned for " + ids);
     }
-    else if (fr.getSeqs().size() != ids.size())
+
+    if (seqs.size() != ids.size())
     {
       System.out.println(String.format(
               "Only retrieved %d sequences for %d query strings",
-              fr.getSeqs().size(), ids.size()));
-    }
-
-    if (fr.getSeqs().size() == 1 && fr.getSeqs().get(0).getLength() == 0)
-    {
-      /*
-       * POST request has returned an empty FASTA file e.g. for invalid id
-       */
-      throw new IOException("No data returned for " + ids);
+              seqs.size(), ids.size()));
     }
 
-    if (fr.getSeqs().size() > 0)
+    if (!seqs.isEmpty())
     {
-      AlignmentI seqal = new Alignment(fr.getSeqsAsArray());
-      for (SequenceI sq : seqal.getSequences())
+      AlignmentI seqal = new Alignment(
+              seqs.toArray(new SequenceI[seqs.size()]));
+      for (SequenceI seq : seqs)
       {
-        if (sq.getDescription() == null)
+        if (seq.getDescription() == null)
         {
-          sq.setDescription(getDbName());
+          seq.setDescription(getDbName());
         }
-        String name = sq.getName();
+        String name = seq.getName();
         if (ids.contains(name)
                 || ids.contains(name.replace("ENSP", "ENST")))
         {
-          DBRefEntry dbref = DBRefUtils.parseToDbRef(sq, getDbSource(),
+          // TODO JAL-3077 use true accession version in dbref
+          DBRefEntry dbref = DBRefUtils.parseToDbRef(seq, getDbSource(),
                   getEnsemblDataVersion(), name);
-          sq.addDBRef(dbref);
+          seq.addDBRef(dbref);
         }
       }
       if (alignment == null)
@@ -448,6 +442,49 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
+   * Parses a JSON response for a single sequence ID query
+   * 
+   * @param br
+   * @return a single jalview.datamodel.Sequence
+   * @see http://rest.ensembl.org/documentation/info/sequence_id
+   */
+  protected List<SequenceI> parseSequenceJson(BufferedReader br)
+  {
+    JSONParser jp = new JSONParser();
+    List<SequenceI> result = new ArrayList<>();
+    try
+    {
+      /*
+       * for now, assumes only one sequence returned; refactor if needed
+       * in future to handle a JSONArray with more than one
+       */
+      final JSONObject val = (JSONObject) jp.parse(br);
+      Object s = val.get("desc");
+      String desc = s == null ? null : s.toString();
+      s = val.get("id");
+      String id = s == null ? null : s.toString();
+      s = val.get("seq");
+      String seq = s == null ? null : s.toString();
+      Sequence sequence = new Sequence(id, seq);
+      if (desc != null)
+      {
+        sequence.setDescription(desc);
+      }
+      // todo JAL-3077 make a DBRefEntry with true accession version
+      // s = val.get("version");
+      // String version = s == null ? "0" : s.toString();
+      // DBRefEntry dbref = new DBRefEntry(getDbSource(), version, id);
+      // sequence.addDBRef(dbref);
+      result.add(sequence);
+    } catch (ParseException | IOException e)
+    {
+      System.err.println("Error processing JSON response: " + e.toString());
+      // ignore
+    }
+    return result;
+  }
+
+  /**
    * Returns the URL for the REST call
    * 
    * @return
@@ -468,13 +505,31 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     }
     // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
     urlstring.append("?type=").append(getSourceEnsemblType().getType());
-    urlstring.append(("&Accept=text/x-fasta"));
+    urlstring.append(("&Accept=application/json"));
+    urlstring.append(("&Content-Type=application/json"));
+
+    String objectType = getObjectType();
+    if (objectType != null)
+    {
+      urlstring.append("&").append(OBJECT_TYPE).append("=")
+              .append(objectType);
+    }
 
     URL url = new URL(urlstring.toString());
     return url;
   }
 
   /**
+   * Override this method to specify object_type request parameter
+   * 
+   * @return
+   */
+  protected String getObjectType()
+  {
+    return null;
+  }
+
+  /**
    * A sequence/id POST request currently allows up to 50 queries
    * 
    * @see http://rest.ensembl.org/documentation/info/sequence_id_post
@@ -491,18 +546,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     return false;
   }
 
-  @Override
-  protected String getRequestMimeType(boolean multipleIds)
-  {
-    return multipleIds ? "application/json" : "text/x-fasta";
-  }
-
-  @Override
-  protected String getResponseMimeType()
-  {
-    return "text/x-fasta";
-  }
-
   /**
    * 
    * @return the configured sequence return type for this source
@@ -538,8 +581,9 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
           String accId, int start)
   {
-    SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
-    if (sfs == null)
+    List<SequenceFeature> sfs = getIdentifyingFeatures(sourceSequence,
+            accId);
+    if (sfs.isEmpty())
     {
       return null;
     }
@@ -548,54 +592,38 @@ 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;
 
     for (SequenceFeature sf : sfs)
     {
+      int strand = sf.getStrand();
+      strand = strand == 0 ? 1 : strand; // treat unknown as forward
+
+      if (directionSet && strand != direction)
+      {
+        // abort - mix of forward and backward
+        System.err
+                .println("Error: forward and backward strand for " + accId);
+        return null;
+      }
+      direction = strand;
+      directionSet = true;
+
       /*
-       * accept the target feature type or a specialisation of it
-       * (e.g. coding_exon for exon)
+       * add to CDS ranges, semi-sorted forwards/backwards
        */
-      if (identifiesSequence(sf, accId))
+      if (strand < 0)
       {
-        int strand = sf.getStrand();
-        strand = strand == 0 ? 1 : strand; // treat unknown as forward
-
-        if (directionSet && strand != direction)
-        {
-          // abort - mix of forward and backward
-          System.err.println(
-                  "Error: forward and backward strand for " + accId);
-          return null;
-        }
-        direction = strand;
-        directionSet = true;
-
-        /*
-         * add to CDS ranges, semi-sorted forwards/backwards
-         */
-        if (strand < 0)
-        {
-          regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
-        }
-        else
-        {
-          regions.add(new int[] { sf.getBegin(), sf.getEnd() });
-        }
-        mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
-
-        if (!isSpliceable())
-        {
-          /*
-           * 'gene' sequence is contiguous so we can stop as soon as its
-           * identifying feature has been found
-           */
-          break;
-        }
+        regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
+      }
+      else
+      {
+        regions.add(new int[] { sf.getBegin(), sf.getEnd() });
       }
+      mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
     }
 
     if (regions.isEmpty())
@@ -609,7 +637,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
      * a final sort is needed since Ensembl returns CDS sorted within source
      * (havana / ensembl_havana)
      */
-    Collections.sort(regions, new RangeComparator(direction == 1));
+    Collections.sort(regions, direction == 1 ? IntRangeComparator.ASCENDING
+            : IntRangeComparator.DESCENDING);
 
     List<int[]> to = Arrays
             .asList(new int[]
@@ -619,26 +648,18 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
-   * Answers true if the sequence being retrieved may occupy discontiguous
-   * regions on the genomic sequence.
-   */
-  protected boolean isSpliceable()
-  {
-    return true;
-  }
-
-  /**
-   * Returns true if the sequence feature marks positions of the genomic
+   * Answers a list of sequence features that mark positions of the genomic
    * sequence feature which are within the sequence being retrieved. For
    * example, an 'exon' feature whose parent is the target transcript marks the
-   * cdna positions of the transcript.
+   * cdna positions of the transcript. For a gene sequence, this is trivially
+   * just the 'gene' feature with matching gene id.
    * 
-   * @param sf
+   * @param seq
    * @param accId
    * @return
    */
-  protected abstract boolean identifiesSequence(SequenceFeature sf,
-          String accId);
+  protected abstract List<SequenceFeature> getIdentifyingFeatures(
+          SequenceI seq, String accId);
 
   /**
    * Transfers the sequence feature to the target sequence, locating its start
@@ -661,13 +682,15 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
     if (mappedRange != null)
     {
-      SequenceFeature copy = new SequenceFeature(sf);
-      copy.setBegin(Math.min(mappedRange[0], mappedRange[1]));
-      copy.setEnd(Math.max(mappedRange[0], mappedRange[1]));
-      if (".".equals(copy.getFeatureGroup()))
+      String group = sf.getFeatureGroup();
+      if (".".equals(group))
       {
-        copy.setFeatureGroup(getDbSource());
+        group = getDbSource();
       }
+      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);
 
       /*
@@ -690,7 +713,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;
@@ -701,7 +724,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);
 
     /*
@@ -711,7 +734,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);
     }
   }
@@ -765,8 +789,9 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       return false;
     }
 
-    // long start = System.currentTimeMillis();
-    SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
+//    long start = System.currentTimeMillis();
+    List<SequenceFeature> sfs = sourceSequence.getFeatures()
+            .getPositionalFeatures();
     MapList mapping = getGenomicRangesFromFeatures(sourceSequence,
             accessionId, targetSequence.getStart());
     if (mapping == null)
@@ -776,10 +801,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
     boolean result = transferFeatures(sfs, targetSequence, mapping,
             accessionId);
-    // System.out.println("transferFeatures (" + (sfs.length) + " --> "
-    // + targetSequence.getSequenceFeatures().length + ") to "
-    // + targetSequence.getName()
-    // + " took " + (System.currentTimeMillis() - start) + "ms");
+//    System.out.println("transferFeatures (" + (sfs.size()) + " --> "
+//            + targetSequence.getFeatures().getFeatureCount(true) + ") to "
+//            + targetSequence.getName() + " took "
+//            + (System.currentTimeMillis() - start) + "ms");
     return result;
   }
 
@@ -788,13 +813,13 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
    * converted using the mapping. Features which do not overlap are ignored.
    * Features whose parent is not the specified identifier are also ignored.
    * 
-   * @param features
+   * @param sfs
    * @param targetSequence
    * @param mapping
    * @param parentId
    * @return
    */
-  protected boolean transferFeatures(SequenceFeature[] features,
+  protected boolean transferFeatures(List<SequenceFeature> sfs,
           SequenceI targetSequence, MapList mapping, String parentId)
   {
     final boolean forwardStrand = mapping.isFromForwardStrand();
@@ -804,10 +829,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
      * position descending if reverse strand) so as to add them in
      * 'forwards' order to the target sequence
      */
-    sortFeatures(features, forwardStrand);
+    SequenceFeatures.sortFeatures(sfs, forwardStrand);
 
     boolean transferred = false;
-    for (SequenceFeature sf : features)
+    for (SequenceFeature sf : sfs)
     {
       if (retainFeature(sf, parentId))
       {
@@ -819,33 +844,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
-   * Sort features by start position ascending (if on forward strand), or end
-   * position descending (if on reverse strand)
-   * 
-   * @param features
-   * @param forwardStrand
-   */
-  protected static void sortFeatures(SequenceFeature[] features,
-          final boolean forwardStrand)
-  {
-    Arrays.sort(features, new Comparator<SequenceFeature>()
-    {
-      @Override
-      public int compare(SequenceFeature o1, SequenceFeature o2)
-      {
-        if (forwardStrand)
-        {
-          return Integer.compare(o1.getBegin(), o2.getBegin());
-        }
-        else
-        {
-          return Integer.compare(o2.getEnd(), o1.getEnd());
-        }
-      }
-    });
-  }
-
-  /**
    * Answers true if the feature type is one we want to keep for the sequence.
    * Some features are only retrieved in order to identify the sequence range,
    * and may then be discarded as redundant information (e.g. "CDS" feature for
@@ -870,7 +868,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;
@@ -887,35 +886,30 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
 
   /**
    * Returns a (possibly empty) list of features on the sequence which have the
-   * specified sequence ontology type (or a sub-type of it), and the given
+   * specified sequence ontology term (or a sub-type of it), and the given
    * identifier as parent
    * 
    * @param sequence
-   * @param type
+   * @param term
    * @param parentId
    * @return
    */
   protected List<SequenceFeature> findFeatures(SequenceI sequence,
-          String type, String parentId)
+          String term, String parentId)
   {
-    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> result = new ArrayList<>();
 
-    SequenceFeature[] sfs = sequence.getSequenceFeatures();
-    if (sfs != null)
+    List<SequenceFeature> sfs = sequence.getFeatures()
+            .getFeaturesByOntology(term);
+    for (SequenceFeature sf : sfs)
     {
-      SequenceOntologyI so = SequenceOntologyFactory.getInstance();
-      for (SequenceFeature sf : sfs)
+      String parent = (String) sf.getValue(PARENT);
+      if (parent != null && parent.equalsIgnoreCase(parentId))
       {
-        if (so.isA(sf.getType(), type))
-        {
-          String parent = (String) sf.getValue(PARENT);
-          if (parent.equals(parentId))
-          {
-            result.add(sf);
-          }
-        }
+        result.add(sf);
       }
     }
+
     return result;
   }
 
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..77768a6 100644 (file)
@@ -88,18 +88,6 @@ class EnsemblXref extends EnsemblRestClient
     return true;
   }
 
-  @Override
-  protected String getRequestMimeType(boolean multipleIds)
-  {
-    return "application/json";
-  }
-
-  @Override
-  protected String getResponseMimeType()
-  {
-    return "application/json";
-  }
-
   /**
    * Calls the Ensembl xrefs REST endpoint and retrieves any cross-references
    * ("primary_id") for the given identifier (Ensembl accession id) and database
@@ -113,8 +101,8 @@ class EnsemblXref extends EnsemblRestClient
    */
   public List<DBRefEntry> getCrossReferences(String identifier)
   {
-    List<DBRefEntry> result = new ArrayList<DBRefEntry>();
-    List<String> ids = new ArrayList<String>();
+    List<DBRefEntry> result = new ArrayList<>();
+    List<String> ids = new ArrayList<>();
     ids.add(identifier);
 
     BufferedReader br = null;
@@ -124,8 +112,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
@@ -160,7 +151,7 @@ class EnsemblXref extends EnsemblRestClient
           throws IOException
   {
     JSONParser jp = new JSONParser();
-    List<DBRefEntry> result = new ArrayList<DBRefEntry>();
+    List<DBRefEntry> result = new ArrayList<>();
     try
     {
       JSONArray responses = (JSONArray) jp.parse(br);
@@ -168,16 +159,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 +199,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..a5b1110 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,11 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return chainNames;
   }
 
+  protected IProgressIndicator getIProgressIndicator()
+  {
+    return null;
+  }
+
   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 ddf3b1a..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);
         }
@@ -355,10 +365,10 @@ public class JmolParser extends StructureFile implements JmolStatusListener
           SequenceI sq, char[] secstr, char[] secstrcode, String chainId,
           int firstResNum)
   {
-    char[] seq = sq.getSequence();
+    int length = sq.getLength();
     boolean ssFound = false;
-    Annotation asecstr[] = new Annotation[seq.length + firstResNum - 1];
-    for (int p = 0; p < seq.length; p++)
+    Annotation asecstr[] = new Annotation[length + firstResNum - 1];
+    for (int p = 0; p < length; p++)
     {
       if (secstr[p] >= 'A' && secstr[p] <= 'z')
       {
index 8c0ea66..39d6704 100644 (file)
@@ -20,7 +20,7 @@
  */
 package jalview.ext.rbvi.chimera;
 
-import jalview.util.RangeComparator;
+import jalview.util.IntRangeComparator;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -120,14 +120,14 @@ public class AtomSpecModel
 
       for (String chain : modelData.keySet())
       {
-        chain = chain.trim();
+        chain = " ".equals(chain) ? chain : chain.trim();
 
         List<int[]> rangeList = modelData.get(chain);
 
         /*
          * sort ranges into ascending start position order
          */
-        Collections.sort(rangeList, new RangeComparator(true));
+        Collections.sort(rangeList, IntRangeComparator.ASCENDING);
 
         int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
         int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
@@ -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 dd91087..dad8511 100644 (file)
@@ -411,12 +411,8 @@ public class ChimeraCommands
           StructureMapping mapping, SequenceI seq,
           Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
   {
-    SequenceFeature[] sfs = seq.getSequenceFeatures();
-    if (sfs == null)
-    {
-      return;
-    }
-
+    List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
+            visibleFeatures.toArray(new String[visibleFeatures.size()]));
     for (SequenceFeature sf : sfs)
     {
       String type = sf.getType();
@@ -427,7 +423,7 @@ public class ChimeraCommands
        */
       boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
               .equals(sf.getFeatureGroup());
-      if (isFromViewer || !visibleFeatures.contains(type))
+      if (isFromViewer)
       {
         continue;
       }
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 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 c78abdc..94b38ed 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());
   }
 
@@ -2444,12 +2431,11 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     viewport.setSelectionGroup(null);
     viewport.getColumnSelection().clear();
     viewport.setSelectionGroup(null);
-    alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null);
     alignPanel.getIdPanel().getIdCanvas().searchResults = null;
     // 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();
   }
@@ -2480,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();
   }
@@ -2489,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();
   }
 
@@ -2549,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
       {
@@ -2614,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());
 
@@ -2653,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());
@@ -2714,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)
     {
       /*
@@ -2869,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);
   }
 
   /*
@@ -2916,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);
   }
 
   /**
@@ -2945,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();
   }
 
@@ -3049,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();
   }
 
@@ -3065,7 +3051,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   {
     viewport.showAllHiddenColumns();
     viewport.showAllHiddenSeqs();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
     viewport.sendSelection();
   }
 
@@ -3073,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();
   }
 
@@ -3094,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);
   }
 
   /**
@@ -3107,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);
   }
 
   /**
@@ -3120,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);
   }
 
   /**
@@ -3133,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);
   }
 
   /**
@@ -3146,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);
   }
 
   /**
@@ -3159,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;
@@ -3197,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);
   }
 
   /**
@@ -3273,6 +3260,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                 alignPanel.setOverviewPanel(null);
               };
             });
+    if (getKeyListeners().length > 0)
+    {
+      frame.addKeyListener(getKeyListeners()[0]);
+    }
 
     alignPanel.setOverviewPanel(overview);
   }
@@ -3354,7 +3345,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
     viewport.setGlobalColourScheme(cs);
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
   }
 
   /**
@@ -3439,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);
   }
 
   /**
@@ -3455,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);
   }
 
   /**
@@ -3471,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);
   }
 
   /**
@@ -3488,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);
   }
 
   /**
@@ -3645,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);
       }
     });
   }
@@ -3674,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);
       }
     });
   }
@@ -3789,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;
   }
 
@@ -4260,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();
   }
 
@@ -4387,7 +4378,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // Java's Transferable for native dnd
     evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
     Transferable t = evt.getTransferable();
-    List<String> files = new ArrayList<>();
+    final AlignFrame thisaf = this;
+    final List<String> files = new ArrayList<>();
     List<DataSourceType> protocols = new ArrayList<>();
 
     try
@@ -4399,133 +4391,160 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
     if (files != null)
     {
-      try
+      new Thread(new Runnable()
       {
-        // check to see if any of these files have names matching sequences in
-        // the alignment
-        SequenceIdMatcher idm = new SequenceIdMatcher(
-                viewport.getAlignment().getSequencesArray());
-        /**
-         * Object[] { String,SequenceI}
-         */
-        ArrayList<Object[]> filesmatched = new ArrayList<>();
-        ArrayList<String> filesnotmatched = new ArrayList<>();
-        for (int i = 0; i < files.size(); i++)
+        @Override
+        public void run()
         {
-          String file = files.get(i).toString();
-          String pdbfn = "";
-          DataSourceType protocol = FormatAdapter.checkProtocol(file);
-          if (protocol == DataSourceType.FILE)
-          {
-            File fl = new File(file);
-            pdbfn = fl.getName();
-          }
-          else if (protocol == DataSourceType.URL)
-          {
-            URL url = new URL(file);
-            pdbfn = url.getFile();
-          }
-          if (pdbfn.length() > 0)
+          try
           {
-            // attempt to find a match in the alignment
-            SequenceI[] mtch = idm.findAllIdMatches(pdbfn);
-            int l = 0, c = pdbfn.indexOf(".");
-            while (mtch == null && c != -1)
+            // check to see if any of these files have names matching sequences
+            // in
+            // the alignment
+            SequenceIdMatcher idm = new SequenceIdMatcher(
+                    viewport.getAlignment().getSequencesArray());
+            /**
+             * Object[] { String,SequenceI}
+             */
+            ArrayList<Object[]> filesmatched = new ArrayList<>();
+            ArrayList<String> filesnotmatched = new ArrayList<>();
+            for (int i = 0; i < files.size(); i++)
             {
-              do
+              String file = files.get(i).toString();
+              String pdbfn = "";
+              DataSourceType protocol = FormatAdapter.checkProtocol(file);
+              if (protocol == DataSourceType.FILE)
               {
-                l = c;
-              } while ((c = pdbfn.indexOf(".", l)) > l);
-              if (l > -1)
+                File fl = new File(file);
+                pdbfn = fl.getName();
+              }
+              else if (protocol == DataSourceType.URL)
               {
-                pdbfn = pdbfn.substring(0, l);
+                URL url = new URL(file);
+                pdbfn = url.getFile();
+              }
+              if (pdbfn.length() > 0)
+              {
+                // attempt to find a match in the alignment
+                SequenceI[] mtch = idm.findAllIdMatches(pdbfn);
+                int l = 0, c = pdbfn.indexOf(".");
+                while (mtch == null && c != -1)
+                {
+                  do
+                  {
+                    l = c;
+                  } while ((c = pdbfn.indexOf(".", l)) > l);
+                  if (l > -1)
+                  {
+                    pdbfn = pdbfn.substring(0, l);
+                  }
+                  mtch = idm.findAllIdMatches(pdbfn);
+                }
+                if (mtch != null)
+                {
+                  FileFormatI type = null;
+                  try
+                  {
+                    type = new IdentifyFile().identify(file, protocol);
+                  } catch (Exception ex)
+                  {
+                    type = null;
+                  }
+                  if (type != null && type.isStructureFile())
+                  {
+                    filesmatched.add(new Object[] { file, protocol, mtch });
+                    continue;
+                  }
+                }
+                // File wasn't named like one of the sequences or wasn't a PDB
+                // file.
+                filesnotmatched.add(file);
               }
-              mtch = idm.findAllIdMatches(pdbfn);
             }
-            if (mtch != null)
+            int assocfiles = 0;
+            if (filesmatched.size() > 0)
             {
-              FileFormatI type = null;
-              try
+              boolean autoAssociate = Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
+              if (!autoAssociate)
               {
-                type = new IdentifyFile().identify(file, protocol);
-              } catch (Exception ex)
+                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)
               {
-                type = null;
+                for (Object[] fm : filesmatched)
+                {
+                  // try and associate
+                  // TODO: may want to set a standard ID naming formalism for
+                  // associating PDB files which have no IDs.
+                  for (SequenceI toassoc : (SequenceI[]) fm[2])
+                  {
+                    PDBEntry pe = new AssociatePdbFileWithSeq()
+                            .associatePdbWithSeq((String) fm[0],
+                                    (DataSourceType) fm[1], toassoc, false,
+                                    Desktop.instance);
+                    if (pe != null)
+                    {
+                      System.err.println("Associated file : "
+                              + ((String) fm[0]) + " with "
+                              + toassoc.getDisplayId(true));
+                      assocfiles++;
+                    }
+                  }
+                  // TODO: do we need to update overview ? only if features are
+                  // shown I guess
+                  alignPanel.paintAlignment(true, false);
+                }
               }
-              if (type != null && type.isStructureFile())
+              else
               {
-                filesmatched.add(new Object[] { file, protocol, mtch });
-                continue;
+                /*
+                 * add declined structures as sequences
+                 */
+                for (Object[] o : filesmatched)
+                {
+                  filesnotmatched.add((String) o[0]);
+                }
               }
             }
-            // File wasn't named like one of the sequences or wasn't a PDB file.
-            filesnotmatched.add(file);
-          }
-        }
-        int assocfiles = 0;
-        if (filesmatched.size() > 0)
-        {
-          if (Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false)
-                  || JvOptionPane.showConfirmDialog(this,
-                          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)
-
-          {
-            for (Object[] fm : filesmatched)
+            if (filesnotmatched.size() > 0)
             {
-              // try and associate
-              // TODO: may want to set a standard ID naming formalism for
-              // associating PDB files which have no IDs.
-              for (SequenceI toassoc : (SequenceI[]) fm[2])
+              if (assocfiles > 0 && (Cache.getDefault(
+                      "AUTOASSOCIATE_PDBANDSEQS_IGNOREOTHERS", false)
+                      || JvOptionPane.showConfirmDialog(thisaf,
+                              "<html>" + MessageManager.formatMessage(
+                                      "label.ignore_unmatched_dropped_files_info",
+                                      new Object[]
+                                      { Integer.valueOf(
+                                              filesnotmatched.size())
+                                              .toString() })
+                                      + "</html>",
+                              MessageManager.getString(
+                                      "label.ignore_unmatched_dropped_files"),
+                              JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION))
               {
-                PDBEntry pe = new AssociatePdbFileWithSeq()
-                        .associatePdbWithSeq((String) fm[0],
-                                (DataSourceType) fm[1], toassoc, false,
-                                Desktop.instance);
-                if (pe != null)
-                {
-                  System.err.println("Associated file : " + ((String) fm[0])
-                          + " with " + toassoc.getDisplayId(true));
-                  assocfiles++;
-                }
+                return;
               }
-              alignPanel.paintAlignment(true);
+              for (String fn : filesnotmatched)
+              {
+                loadJalviewDataFile(fn, null, null, null);
+              }
+
             }
-          }
-        }
-        if (filesnotmatched.size() > 0)
-        {
-          if (assocfiles > 0 && (Cache.getDefault(
-                  "AUTOASSOCIATE_PDBANDSEQS_IGNOREOTHERS", false)
-                  || JvOptionPane.showConfirmDialog(this,
-                          "<html>" + MessageManager.formatMessage(
-                                  "label.ignore_unmatched_dropped_files_info",
-                                  new Object[]
-                                  { Integer.valueOf(filesnotmatched.size())
-                                          .toString() })
-                                  + "</html>",
-                          MessageManager.getString(
-                                  "label.ignore_unmatched_dropped_files"),
-                          JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION))
-          {
-            return;
-          }
-          for (String fn : filesnotmatched)
+          } catch (Exception ex)
           {
-            loadJalviewDataFile(fn, null, null, null);
+            ex.printStackTrace();
           }
-
         }
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
-      }
+      }).start();
     }
   }
 
@@ -4627,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))
@@ -4639,7 +4654,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           {
             if (parseFeaturesFile(file, sourceType))
             {
-              alignPanel.paintAlignment(true);
+              alignPanel.paintAlignment(true, true);
             }
           }
           else
@@ -4654,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)
     {
@@ -4853,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());
       };
     });
@@ -5179,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);
   }
 
   /*
@@ -5268,7 +5284,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     {
       PaintRefresher.Refresh(this, viewport.getSequenceSetId());
       alignPanel.updateAnnotation();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
   }
 
@@ -5280,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);
     }
   }
 
@@ -5289,6 +5305,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   {
     if (avc.createGroup())
     {
+      if (applyAutoAnnotationSettings.isSelected())
+      {
+        alignPanel.updateAnnotation(true, false);
+      }
       alignPanel.alignmentChanged();
     }
   }
@@ -5370,7 +5390,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);
   }
 
   /**
@@ -5573,6 +5593,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 c22a37d..7e77bec 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;
@@ -448,27 +443,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
@@ -476,10 +450,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();
@@ -489,8 +463,8 @@ public class AlignViewport extends AlignmentViewport
     {
       end = alignment.getWidth();
     }
-    viscontigs = alignment.getHiddenColumns().getVisibleContigs(start, end);
-    return viscontigs;
+    return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
+            false));
   }
 
   /**
@@ -606,58 +580,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()
   {
@@ -679,7 +601,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)
   {
@@ -1110,5 +1032,4 @@ public class AlignViewport extends AlignmentViewport
     }
     fr.setTransparency(featureSettings.getTransparency());
   }
-
 }
index ba9fc11..60ef480 100644 (file)
@@ -30,12 +30,13 @@ import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.io.HTMLOutput;
 import jalview.jbgui.GAlignmentPanel;
 import jalview.math.AlignmentDimension;
 import jalview.schemes.ResidueProperties;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.Comparison;
 import jalview.util.MessageManager;
-import jalview.util.Platform;
 import jalview.viewmodel.ViewportListenerI;
 import jalview.viewmodel.ViewportRanges;
 
@@ -46,7 +47,7 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
-import java.awt.Insets;
+import java.awt.Graphics2D;
 import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
 import java.awt.event.ComponentAdapter;
@@ -74,8 +75,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
 {
   public AlignViewport av;
 
-  ViewportRanges vpRanges;
-
   OverviewPanel overviewPanel;
 
   private SeqPanel seqPanel;
@@ -95,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;
@@ -122,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));
 
@@ -154,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
         {
@@ -167,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);
         }
       }
 
@@ -212,7 +208,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
     alignFrame.updateEditMenuBar();
 
-    paintAlignment(true);
+    // no idea if we need to update structure
+    paintAlignment(true, true);
 
   }
 
@@ -241,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();
   }
 
@@ -341,19 +333,11 @@ public class AlignmentPanel extends GAlignmentPanel implements
    */
   public void highlightSearchResults(SearchResultsI results)
   {
-    scrollToPosition(results);
-    getSeqPanel().seqCanvas.highlightSearchResults(results);
-  }
+    boolean scrolled = scrollToPosition(results, 0, true, false);
 
-  /**
-   * Scroll the view to show the position of the highlighted region in results
-   * (if any) and redraw the overview
-   * 
-   * @param results
-   */
-  public boolean scrollToPosition(SearchResultsI results)
-  {
-    return scrollToPosition(results, 0, true, false);
+    boolean noFastPaint = scrolled && av.getWrapAlignment();
+
+    getSeqPanel().seqCanvas.highlightSearchResults(results, noFastPaint);
   }
 
   /**
@@ -371,8 +355,10 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   /**
-   * Scroll the view to show the position of the highlighted region in results
-   * (if any)
+   * Scrolls the view (if necessary) to show the position of the first
+   * highlighted region in results (if any). Answers true if the view was
+   * scrolled, or false if no matched region was found, or it is already
+   * visible.
    * 
    * @param results
    * @param verticalOffset
@@ -382,116 +368,118 @@ public class AlignmentPanel extends GAlignmentPanel implements
    *          - when set, the overview will be recalculated (takes longer)
    * @param centre
    *          if true, try to centre the search results horizontally in the view
-   * @return false if results were not found
+   * @return
    */
-  public boolean scrollToPosition(SearchResultsI results,
+  protected boolean scrollToPosition(SearchResultsI results,
           int verticalOffset, boolean redrawOverview, boolean centre)
   {
     int startv, endv, starts, ends;
-    // TODO: properly locate search results in view when large numbers of hidden
-    // columns exist before highlighted region
-    // do we need to scroll the panel?
-    // TODO: tons of nullpointerexceptions raised here.
-    if (results != null && results.getSize() > 0 && av != null
-            && av.getAlignment() != null)
-    {
-      int seqIndex = av.getAlignment().findIndex(results);
-      if (seqIndex == -1)
-      {
-        return false;
-      }
-      SequenceI seq = av.getAlignment().getSequenceAt(seqIndex);
+    ViewportRanges ranges = av.getRanges();
 
-      int[] r = results.getResults(seq, 0, av.getAlignment().getWidth());
-      if (r == null)
-      {
-        return false;
-      }
-      int start = r[0];
-      int end = r[1];
+    if (results == null || results.isEmpty() || av == null
+            || av.getAlignment() == null)
+    {
+      return false;
+    }
+    int seqIndex = av.getAlignment().findIndex(results);
+    if (seqIndex == -1)
+    {
+      return false;
+    }
+    SequenceI seq = av.getAlignment().getSequenceAt(seqIndex);
 
-      /*
-       * To centre results, scroll to positions half the visible width
-       * left/right of the start/end positions
-       */
-      if (centre)
-      {
-        int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2
-                - 1;
-        start = Math.max(start - offset, 0);
-        end = end + offset - 1;
-      }
-      if (start < 0)
-      {
-        return false;
-      }
-      if (end == seq.getEnd())
-      {
-        return false;
-      }
-      if (av.hasHiddenColumns())
+    int[] r = results.getResults(seq, 0, av.getAlignment().getWidth());
+    if (r == null)
+    {
+      return false;
+    }
+    int start = r[0];
+    int end = r[1];
+
+    /*
+     * To centre results, scroll to positions half the visible width
+     * left/right of the start/end positions
+     */
+    if (centre)
+    {
+      int offset = (ranges.getEndRes() - ranges.getStartRes() + 1) / 2 - 1;
+      start = Math.max(start - offset, 0);
+      end = end + offset - 1;
+    }
+    if (start < 0)
+    {
+      return false;
+    }
+    if (end == seq.getEnd())
+    {
+      return false;
+    }
+
+    if (av.hasHiddenColumns())
+    {
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      start = hidden.absoluteToVisibleColumn(start);
+      end = hidden.absoluteToVisibleColumn(end);
+      if (start == end)
       {
-        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-        start = hidden.findColumnPosition(start);
-        end = hidden.findColumnPosition(end);
-        if (start == end)
+        if (!hidden.isVisible(r[0]))
         {
-          if (!hidden.isVisible(r[0]))
-          {
-            // don't scroll - position isn't visible
-            return false;
-          }
+          // don't scroll - position isn't visible
+          return false;
         }
       }
+    }
 
-      /*
-       * allow for offset of target sequence (actually scroll to one above it)
-       */
-      seqIndex = Math.max(0, seqIndex - verticalOffset);
+    /*
+     * allow for offset of target sequence (actually scroll to one above it)
+     */
+    seqIndex = Math.max(0, seqIndex - verticalOffset);
+    boolean scrollNeeded = true;
 
-      if (!av.getWrapAlignment())
+    if (!av.getWrapAlignment())
+    {
+      if ((startv = ranges.getStartRes()) >= start)
       {
-        if ((startv = vpRanges.getStartRes()) >= start)
-        {
-          /*
-           * Scroll left to make start of search results visible
-           */
-          setScrollValues(start, seqIndex);
-        }
-        else if ((endv = vpRanges.getEndRes()) <= end)
-        {
-          /*
-           * Scroll right to make end of search results visible
-           */
-          setScrollValues(startv + end - endv, seqIndex);
-        }
-        else if ((starts = vpRanges.getStartSeq()) > seqIndex)
-        {
-          /*
-           * Scroll up to make start of search results visible
-           */
-          setScrollValues(vpRanges.getStartRes(), seqIndex);
-        }
-        else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
-        {
-          /*
-           * Scroll down to make end of search results visible
-           */
-          setScrollValues(vpRanges.getStartRes(),
-                  starts + seqIndex - ends + 1);
-        }
         /*
-         * Else results are already visible - no need to scroll
+         * Scroll left to make start of search results visible
          */
+        setScrollValues(start, seqIndex);
       }
-      else
+      else if ((endv = ranges.getEndRes()) <= end)
+      {
+        /*
+         * Scroll right to make end of search results visible
+         */
+        setScrollValues(startv + end - endv, seqIndex);
+      }
+      else if ((starts = ranges.getStartSeq()) > seqIndex)
       {
-        vpRanges.scrollToWrappedVisible(start);
+        /*
+         * Scroll up to make start of search results visible
+         */
+        setScrollValues(ranges.getStartRes(), seqIndex);
       }
+      else if ((ends = ranges.getEndSeq()) <= seqIndex)
+      {
+        /*
+         * Scroll down to make end of search results visible
+         */
+        setScrollValues(ranges.getStartRes(), starts + seqIndex - ends
+                + 1);
+      }
+      /*
+       * Else results are already visible - no need to scroll
+       */
+      scrollNeeded = false;
+    }
+    else
+    {
+      scrollNeeded = ranges.scrollToWrappedVisible(start);
     }
 
-    paintAlignment(redrawOverview);
-    return true;
+    paintAlignment(redrawOverview, false);
+
+    return scrollNeeded;
   }
 
   /**
@@ -546,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);
   }
 
   /**
@@ -556,40 +546,10 @@ public class AlignmentPanel extends GAlignmentPanel implements
   protected void validateAnnotationDimensions(boolean adjustPanelHeight)
   {
     int annotationHeight = getAnnotationPanel().adjustPanelHeight();
+    annotationHeight = getAnnotationPanel()
+            .adjustForAlignFrame(adjustPanelHeight, annotationHeight);
 
-    if (adjustPanelHeight)
-    {
-      int rowHeight = av.getCharHeight();
-      int alignmentHeight = rowHeight * av.getAlignment().getHeight();
-
-      /*
-       * 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.
-       */
-      int stuff = Platform.isAMac() ? 80 : 100;
-      Insets insets = alignFrame.getInsets();
-      int availableHeight = alignFrame.getHeight() - stuff - insets.top
-              - insets.bottom;
-
-      /*
-       * If not enough vertical space, maximize annotation height while keeping
-       * at least two rows of alignment visible
-       */
-      if (annotationHeight + alignmentHeight > availableHeight)
-      {
-        annotationHeight = Math.min(annotationHeight,
-                availableHeight - 2 * rowHeight);
-      }
-    }
-    else
-    {
-      // maintain same window layout whilst updating sliders
-      annotationHeight = annotationScroller.getSize().height;
-    }
     hscroll.addNotify();
-
     annotationScroller.setPreferredSize(
             new Dimension(annotationScroller.getWidth(), annotationHeight));
 
@@ -613,7 +573,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);
@@ -627,6 +588,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     {
       annotationScroller.setVisible(true);
       annotationSpaceFillerHolder.setVisible(true);
+      validateAnnotationDimensions(false);
     }
 
     int canvasWidth = getSeqPanel().seqCanvas.getWidth();
@@ -636,16 +598,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);
       }
     }
 
@@ -686,7 +648,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();
@@ -744,10 +706,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();
 
@@ -758,12 +722,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();
 
@@ -774,12 +738,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
       {
         return;
       }
-      vpRanges.setViewportStartAndHeight(y, height);
-    }
-    if (!fastPaint)
-    {
-      repaint();
+      ranges.setViewportStartAndHeight(y, height);
     }
+    repaint();
   }
 
   /**
@@ -794,6 +755,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();
@@ -803,8 +766,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;
@@ -814,9 +777,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
@@ -837,20 +800,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());
@@ -858,10 +821,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)
       {
@@ -879,17 +844,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());
   }
 
   /**
@@ -901,8 +869,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)
@@ -947,30 +916,16 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param pg
-   *          DOCUMENT ME!
-   * @param pwidth
-   *          DOCUMENT ME!
-   * @param pheight
-   *          DOCUMENT ME!
-   * @param pi
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   * 
-   * @throws PrinterException
-   *           DOCUMENT ME!
-   */
-  /**
    * Draws the alignment image, including sequence ids, sequences, and
    * annotation labels and annotations if shown, on either one or two Graphics
-   * context.
+   * contexts.
    * 
    * @param pageWidth
+   *          in pixels
    * @param pageHeight
-   * @param pi
+   *          in pixels
+   * @param pageIndex
+   *          (0, 1, ...)
    * @param idGraphics
    *          the graphics context for sequence ids and annotation labels
    * @param alignmentGraphics
@@ -979,7 +934,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
    * @return
    * @throws PrinterException
    */
-  public int printUnwrapped(int pageWidth, int pageHeight, int pi,
+  public int printUnwrapped(int pageWidth, int pageHeight, int pageIndex,
           Graphics idGraphics, Graphics alignmentGraphics)
           throws PrinterException
   {
@@ -993,8 +948,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
             : idWidth;
 
     FontMetrics fm = getFontMetrics(av.getFont());
-    int charHeight = av.getCharHeight();
-    int scaleHeight = charHeight + fm.getDescent();
+    final int charHeight = av.getCharHeight();
+    final int scaleHeight = charHeight + fm.getDescent();
 
     idGraphics.setColor(Color.white);
     idGraphics.fillRect(0, 0, pageWidth, pageHeight);
@@ -1003,29 +958,20 @@ public class AlignmentPanel extends GAlignmentPanel implements
     /*
      * How many sequences and residues can we fit on a printable page?
      */
-    int totalRes = (pageWidth - idWidth) / av.getCharWidth();
+    final int totalRes = (pageWidth - idWidth) / av.getCharWidth();
 
-    int totalSeq = (pageHeight - scaleHeight) / charHeight - 1;
+    final int totalSeq = (pageHeight - scaleHeight) / charHeight - 1;
 
-    int alignmentWidth = av.getAlignment().getWidth();
-    int pagesWide = (alignmentWidth / totalRes) + 1;
+    final int alignmentWidth = av.getAlignment().getWidth();
+    final int pagesWide = (alignmentWidth / totalRes) + 1;
 
-    final int startRes = (pi % pagesWide) * totalRes;
-    int endRes = (startRes + totalRes) - 1;
-
-    if (endRes > (alignmentWidth - 1))
-    {
-      endRes = alignmentWidth - 1;
-    }
+    final int startRes = (pageIndex % pagesWide) * totalRes;
+    final int endRes = Math.min(startRes + totalRes - 1,
+            alignmentWidth - 1);
 
-    final int startSeq = (pi / pagesWide) * totalSeq;
-    int endSeq = startSeq + totalSeq;
-
-    int alignmentHeight = av.getAlignment().getHeight();
-    if (endSeq > alignmentHeight)
-    {
-      endSeq = alignmentHeight;
-    }
+    final int startSeq = (pageIndex / pagesWide) * totalSeq;
+    final int alignmentHeight = av.getAlignment().getHeight();
+    final int endSeq = Math.min(startSeq + totalSeq, alignmentHeight);
 
     int pagesHigh = ((alignmentHeight / totalSeq) + 1) * pageHeight;
 
@@ -1036,7 +982,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
     pagesHigh /= pageHeight;
 
-    if (pi >= (pagesWide * pagesHigh))
+    if (pageIndex >= (pagesWide * pagesHigh))
     {
       return Printable.NO_SUCH_PAGE;
     }
@@ -1055,47 +1001,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
      * then reset to top left (0, 0)
      */
     idGraphics.translate(0, scaleHeight);
-    idGraphics.setFont(getIdPanel().getIdCanvas().getIdfont());
-    Color currentColor = null;
-    Color currentTextColor = null;
+    IdCanvas idCanvas = getIdPanel().getIdCanvas();
+    List<SequenceI> selection = av.getSelectionGroup() == null ? null
+            : av.getSelectionGroup().getSequences(null);
+    idCanvas.drawIds((Graphics2D) idGraphics, av, startSeq, endSeq - 1,
+            selection);
 
-    SequenceI seq;
-    for (int i = startSeq; i < endSeq; i++)
-    {
-      seq = av.getAlignment().getSequenceAt(i);
-      if ((av.getSelectionGroup() != null)
-              && av.getSelectionGroup().getSequences(null).contains(seq))
-      {
-        /*
-         * gray out ids of sequences in selection group (if any)
-         */
-        currentColor = Color.gray;
-        currentTextColor = Color.black;
-      }
-      else
-      {
-        currentColor = av.getSequenceColour(seq);
-        currentTextColor = Color.black;
-      }
-
-      idGraphics.setColor(currentColor);
-      idGraphics.fillRect(0, (i - startSeq) * charHeight, idWidth,
-              charHeight);
-
-      idGraphics.setColor(currentTextColor);
-
-      int xPos = 0;
-      String displayId = seq.getDisplayId(av.getShowJVSuffix());
-      if (av.isRightAlignIds())
-      {
-        fm = idGraphics.getFontMetrics();
-        xPos = idWidth - fm.stringWidth(displayId) - 4;
-      }
-
-      idGraphics.drawString(displayId, xPos,
-              (((i - startSeq) * charHeight) + charHeight)
-                      - (charHeight / 5));
-    }
     idGraphics.setFont(av.getFont());
     idGraphics.translate(0, -scaleHeight);
 
@@ -1105,7 +1016,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
      */
     alignmentGraphics.translate(alignmentGraphicsOffset, scaleHeight);
     getSeqPanel().seqCanvas.drawPanelForPrinting(alignmentGraphics, startRes,
-            endRes, startSeq, endSeq);
+            endRes, startSeq, endSeq - 1);
     alignmentGraphics.translate(-alignmentGraphicsOffset, 0);
 
     if (av.isShowAnnotation() && (endSeq == alignmentHeight))
@@ -1135,31 +1046,27 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   /**
-   * DOCUMENT ME!
+   * Prints one page of an alignment in wrapped mode. Returns
+   * Printable.PAGE_EXISTS (0) if a page was drawn, or Printable.NO_SUCH_PAGE if
+   * no page could be drawn (page number out of range).
    * 
-   * @param pg
-   *          DOCUMENT ME!
-   * @param pwidth
-   *          DOCUMENT ME!
-   * @param pheight
-   *          DOCUMENT ME!
-   * @param pi
-   *          DOCUMENT ME!
+   * @param pageWidth
+   * @param pageHeight
+   * @param pageNumber
+   *          (0, 1, ...)
+   * @param g
    * 
-   * @return DOCUMENT ME!
+   * @return
    * 
    * @throws PrinterException
-   *           DOCUMENT ME!
    */
-  public int printWrappedAlignment(int pwidth, int pheight, int pi,
-          Graphics pg) throws PrinterException
+  public int printWrappedAlignment(int pageWidth, int pageHeight, int pageNumber,
+          Graphics g) throws PrinterException
   {
     int annotationHeight = 0;
-    AnnotationLabels labels = null;
     if (av.isShowAnnotation())
     {
       annotationHeight = getAnnotationPanel().adjustPanelHeight();
-      labels = new AnnotationLabels(av);
     }
 
     int hgap = av.getCharHeight();
@@ -1177,68 +1084,43 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+              .absoluteToVisibleColumn(maxwidth) - 1;
     }
 
     int resWidth = getSeqPanel().seqCanvas
-            .getWrappedCanvasWidth(pwidth - idWidth);
+            .getWrappedCanvasWidth(pageWidth - idWidth);
 
     int totalHeight = cHeight * (maxwidth / resWidth + 1);
 
-    pg.setColor(Color.white);
-    pg.fillRect(0, 0, pwidth, pheight);
-    pg.setFont(av.getFont());
-
-    // //////////////
-    // Draw the ids
-    pg.setColor(Color.black);
+    g.setColor(Color.white);
+    g.fillRect(0, 0, pageWidth, pageHeight);
+    g.setFont(av.getFont());
+    g.setColor(Color.black);
 
-    pg.translate(0, -pi * pheight);
-
-    pg.setClip(0, pi * pheight, pwidth, pheight);
-
-    int ypos = hgap;
-
-    do
-    {
-      for (int i = 0; i < av.getAlignment().getHeight(); i++)
-      {
-        pg.setFont(getIdPanel().getIdCanvas().getIdfont());
-        SequenceI s = av.getAlignment().getSequenceAt(i);
-        String string = s.getDisplayId(av.getShowJVSuffix());
-        int xPos = 0;
-        if (av.isRightAlignIds())
-        {
-          FontMetrics fm = pg.getFontMetrics();
-          xPos = idWidth - fm.stringWidth(string) - 4;
-        }
-        pg.drawString(string, xPos,
-                ((i * av.getCharHeight()) + ypos + av.getCharHeight())
-                        - (av.getCharHeight() / 5));
-      }
-      if (labels != null)
-      {
-        pg.translate(-3, ypos
-                + (av.getAlignment().getHeight() * av.getCharHeight()));
+    /*
+     * method: print the whole wrapped alignment, but with a clip region that
+     * is restricted to the requested page; this supports selective print of 
+     * single  pages or ranges, (at the cost of some repeated processing in 
+     * the 'normal' case, when all pages are printed)
+     */
+    g.translate(0, -pageNumber * pageHeight);
 
-        pg.setFont(av.getFont());
-        labels.drawComponent(pg, idWidth);
-        pg.translate(+3, -ypos
-                - (av.getAlignment().getHeight() * av.getCharHeight()));
-      }
+    g.setClip(0, pageNumber * pageHeight, pageWidth, pageHeight);
 
-      ypos += cHeight;
-    } while (ypos < totalHeight);
+    /*
+     * draw sequence ids and annotation labels (if shown)
+     */
+    IdCanvas idCanvas = getIdPanel().getIdCanvas();
+    idCanvas.drawIdsWrapped((Graphics2D) g, av, 0, totalHeight);
 
-    pg.translate(idWidth, 0);
+    g.translate(idWidth, 0);
 
-    getSeqPanel().seqCanvas.drawWrappedPanelForPrinting(pg, pwidth - idWidth,
+    getSeqPanel().seqCanvas.drawWrappedPanelForPrinting(g, pageWidth - idWidth,
             totalHeight, 0);
 
-    if ((pi * pheight) < totalHeight)
+    if ((pageNumber * pageHeight) < totalHeight)
     {
       return Printable.PAGE_EXISTS;
-
     }
     else
     {
@@ -1369,7 +1251,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())
@@ -1438,35 +1320,33 @@ public class AlignmentPanel extends GAlignmentPanel implements
     {
       try
       {
-        int s, sSize = av.getAlignment().getHeight(), res,
-                alwidth = av.getAlignment().getWidth(), g, gSize, f, fSize,
-                sy;
+        int sSize = av.getAlignment().getHeight();
+        int alwidth = av.getAlignment().getWidth();
         PrintWriter out = new PrintWriter(new FileWriter(imgMapFile));
-        out.println(jalview.io.HTMLOutput.getImageMapHTML());
+        out.println(HTMLOutput.getImageMapHTML());
         out.println("<img src=\"" + imageName
                 + "\" border=\"0\" usemap=\"#Map\" >"
                 + "<map name=\"Map\">");
 
-        for (s = 0; s < sSize; s++)
+        for (int s = 0; s < sSize; s++)
         {
-          sy = s * av.getCharHeight() + scaleHeight;
+          int sy = s * av.getCharHeight() + scaleHeight;
 
           SequenceI seq = av.getAlignment().getSequenceAt(s);
-          SequenceFeature[] features = seq.getSequenceFeatures();
           SequenceGroup[] groups = av.getAlignment().findAllGroups(seq);
-          for (res = 0; res < alwidth; res++)
+          for (int column = 0; column < alwidth; column++)
           {
-            StringBuilder text = new StringBuilder();
+            StringBuilder text = new StringBuilder(512);
             String triplet = null;
             if (av.getAlignment().isNucleotide())
             {
-              triplet = ResidueProperties.nucleotideName
-                      .get(seq.getCharAt(res) + "");
+              triplet = ResidueProperties.nucleotideName.get(seq
+                      .getCharAt(column) + "");
             }
             else
             {
-              triplet = ResidueProperties.aa2Triplet
-                      .get(seq.getCharAt(res) + "");
+              triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(column)
+                      + "");
             }
 
             if (triplet == null)
@@ -1474,84 +1354,73 @@ public class AlignmentPanel extends GAlignmentPanel implements
               continue;
             }
 
-            int alIndex = seq.findPosition(res);
-            gSize = groups.length;
-            for (g = 0; g < gSize; g++)
+            int seqPos = seq.findPosition(column);
+            int gSize = groups.length;
+            for (int g = 0; g < gSize; g++)
             {
               if (text.length() < 1)
               {
                 text.append("<area shape=\"rect\" coords=\"")
-                        .append((idWidth + res * av.getCharWidth()))
+                        .append((idWidth + column * av.getCharWidth()))
                         .append(",").append(sy).append(",")
-                        .append((idWidth + (res + 1) * av.getCharWidth()))
+                        .append((idWidth + (column + 1) * av.getCharWidth()))
                         .append(",").append((av.getCharHeight() + sy))
                         .append("\"").append(" onMouseOver=\"toolTip('")
-                        .append(alIndex).append(" ").append(triplet);
+                        .append(seqPos).append(" ").append(triplet);
               }
 
-              if (groups[g].getStartRes() < res
-                      && groups[g].getEndRes() > res)
+              if (groups[g].getStartRes() < column
+                      && groups[g].getEndRes() > column)
               {
                 text.append("<br><em>").append(groups[g].getName())
                         .append("</em>");
               }
             }
 
-            if (features != null)
+            if (text.length() < 1)
             {
-              if (text.length() < 1)
-              {
-                text.append("<area shape=\"rect\" coords=\"")
-                        .append((idWidth + res * av.getCharWidth()))
-                        .append(",").append(sy).append(",")
-                        .append((idWidth + (res + 1) * av.getCharWidth()))
-                        .append(",").append((av.getCharHeight() + sy))
-                        .append("\"").append(" onMouseOver=\"toolTip('")
-                        .append(alIndex).append(" ").append(triplet);
-              }
-              fSize = features.length;
-              for (f = 0; f < fSize; f++)
+              text.append("<area shape=\"rect\" coords=\"")
+                      .append((idWidth + column * av.getCharWidth()))
+                      .append(",").append(sy).append(",")
+                      .append((idWidth + (column + 1) * av.getCharWidth()))
+                      .append(",").append((av.getCharHeight() + sy))
+                      .append("\"").append(" onMouseOver=\"toolTip('")
+                      .append(seqPos).append(" ").append(triplet);
+            }
+            if (!Comparison.isGap(seq.getCharAt(column)))
+            {
+              List<SequenceFeature> features = seq.findFeatures(column, column);
+              for (SequenceFeature sf : features)
               {
-
-                if ((features[f].getBegin() <= seq.findPosition(res))
-                        && (features[f].getEnd() >= seq.findPosition(res)))
+                if (sf.isContactFeature())
                 {
-                  if (features[f].isContactFeature())
-                  {
-                    if (features[f].getBegin() == seq.findPosition(res)
-                            || features[f].getEnd() == seq
-                                    .findPosition(res))
-                    {
-                      text.append("<br>").append(features[f].getType())
-                              .append(" ").append(features[f].getBegin())
-                              .append(":").append(features[f].getEnd());
-                    }
-                  }
-                  else
+                  text.append("<br>").append(sf.getType()).append(" ")
+                          .append(sf.getBegin()).append(":")
+                          .append(sf.getEnd());
+                }
+                else
+                {
+                  text.append("<br>");
+                  text.append(sf.getType());
+                  String description = sf.getDescription();
+                  if (description != null
+                          && !sf.getType().equals(description))
                   {
-                    text.append("<br>");
-                    text.append(features[f].getType());
-                    if (features[f].getDescription() != null && !features[f]
-                            .getType().equals(features[f].getDescription()))
-                    {
-                      text.append(" ").append(features[f].getDescription());
-                    }
-
-                    if (features[f].getValue("status") != null)
-                    {
-                      text.append(" (")
-                              .append(features[f].getValue("status"))
-                              .append(")");
-                    }
+                    description = description.replace("\"", "&quot;");
+                    text.append(" ").append(description);
                   }
                 }
-
+                String status = sf.getStatus();
+                if (status != null && !"".equals(status))
+                {
+                  text.append(" (").append(status).append(")");
+                }
+              }
+              if (text.length() > 1)
+              {
+                text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
+                out.println(text.toString());
               }
-            }
-            if (text.length() > 1)
-            {
-              text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
-              out.println(text.toString());
             }
           }
         }
@@ -1600,7 +1469,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;
@@ -1626,13 +1495,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);
@@ -1655,9 +1525,15 @@ public class AlignmentPanel extends GAlignmentPanel implements
    */
   protected void closeChildFrames()
   {
+    if (overviewPanel != null)
+    {
+      overviewPanel.dispose();
+      overviewPanel = null;
+    }
     if (calculationDialog != null)
     {
       calculationDialog.closeFrame();
+      calculationDialog = null;
     }
   }
 
@@ -1820,37 +1696,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
    * @param verticalOffset
    *          the number of visible sequences to show above the mapped region
    */
-  public void scrollToCentre(SearchResultsI sr, int verticalOffset)
+  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);
   }
 
@@ -1887,7 +1734,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (adjustHeight)
     {
       // sort, repaint, update overview
-      paintAlignment(true);
+      paintAlignment(true, false);
     }
     else
     {
@@ -1904,8 +1751,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 beb77ca..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,9 @@ 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;
 
 import javax.swing.BorderFactory;
@@ -56,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();
@@ -83,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"));
@@ -132,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)
@@ -147,70 +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())
     {
-      Map<String, FeatureColourI> displayedFeatureColours = ap
-              .getFeatureRenderer().getDisplayedFeatureCols();
-      FeaturesFile formatter = new FeaturesFile();
-      SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
-      Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
-              .getDisplayedFeatureCols();
-      boolean includeNonPositional = ap.av.isShowNPFeats();
-      if (GFFFormat.isSelected())
-      {
-        text = new FeaturesFile().printGffFormat(
-                ap.av.getAlignment().getDataset().getSequencesArray(),
-                displayedFeatureColours, true, ap.av.isShowNPFeats());
-        text = formatter.printGffFormat(sequences, featureColours, true,
-                includeNonPositional);
-      }
-      else
-      {
-        text = new FeaturesFile().printJalviewFormat(
-                ap.av.getAlignment().getDataset().getSequencesArray(),
-                displayedFeatureColours, true, ap.av.isShowNPFeats()); // ap.av.featuresDisplayed);
-        text = formatter.printJalviewFormat(sequences, featureColours, true,
-                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",
@@ -219,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(
@@ -230,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
     {
@@ -253,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"));
@@ -262,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"));
@@ -271,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..50971c7 100755 (executable)
@@ -30,6 +30,7 @@ import jalview.renderer.AwtRenderPanelI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
 import jalview.viewmodel.ViewportListenerI;
 import jalview.viewmodel.ViewportRanges;
 
@@ -175,11 +176,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);
       }
@@ -205,7 +207,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
   @Override
   public Dimension getPreferredScrollableViewportSize()
   {
-    return getPreferredSize();
+    Dimension ps = getPreferredSize();
+    return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
   }
 
   @Override
@@ -670,7 +673,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       }
       graphStretchY = evt.getY();
       adjustPanelHeight();
-      ap.paintAlignment(true);
+      ap.paintAlignment(false, false);
     }
     else
     {
@@ -724,7 +727,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 +785,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 +911,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 +968,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 +1001,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 +1019,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 +1188,59 @@ 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();
+    }
+  }
+
+  /**
+   * computes the visible height of the annotation panel
+   * 
+   * @param adjustPanelHeight
+   *          - when false, just adjust existing height according to other
+   *          windows
+   * @param annotationHeight
+   * @return height to use for the ScrollerPreferredVisibleSize
+   */
+  public int adjustForAlignFrame(boolean adjustPanelHeight,
+          int annotationHeight)
+  {
+    /*
+     * Estimate available height in the AlignFrame for alignment +
+     * annotations. Deduct an estimate for title bar, menu bar, scale panel,
+     * hscroll, status bar, insets. 
+     */
+    int stuff = (ap.getViewName() != null ? 30 : 0)
+            + (Platform.isAMac() ? 120 : 140);
+    int availableHeight = ap.alignFrame.getHeight() - stuff;
+    int rowHeight = av.getCharHeight();
+
+    if (adjustPanelHeight)
+    {
+      int alignmentHeight = rowHeight * av.getAlignment().getHeight();
+
+      /*
+       * If not enough vertical space, maximize annotation height while keeping
+       * at least two rows of alignment visible
+       */
+      if (annotationHeight + alignmentHeight > availableHeight)
+      {
+        annotationHeight = Math.min(annotationHeight,
+                availableHeight - 2 * rowHeight);
+      }
+    }
+    else
+    {
+      // maintain same window layout whilst updating sliders
+      annotationHeight = Math.min(ap.annotationScroller.getSize().height,
+              availableHeight - 2 * rowHeight);
+    }
+    return annotationHeight;
   }
 }
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);
index 1bbe8d8..3a64716 100644 (file)
@@ -120,6 +120,15 @@ public class AppVarna extends JInternalFrame
       }
     }
 
+    /**
+     * highlight a region from start to end (inclusive) on rna
+     * 
+     * @param rna
+     * @param start
+     *          - first base pair index (from 0)
+     * @param end
+     *          - last base pair index (from 0)
+     */
     public void highlightRegion(RNA rna, int start, int end)
     {
       clearLastSelection();
@@ -397,7 +406,8 @@ public class AppVarna extends JInternalFrame
     RnaModel rnaModel = models.get(rna);
     if (rnaModel.seq == sequence)
     {
-      int highlightPos = rnaModel.gapped ? index : position - 1;
+      int highlightPos = rnaModel.gapped ? index
+              : position - sequence.getStart();
       mouseOverHighlighter.highlightRegion(rna, highlightPos, highlightPos);
       vab.updateSelectedRNA(rna);
     }
@@ -418,15 +428,28 @@ public class AppVarna extends JInternalFrame
     {
       return;
     }
-    if (seqsel != null && seqsel.getSize() > 0)
+
+    RnaModel rnaModel = models.get(rna);
+
+    if (seqsel != null && seqsel.getSize() > 0
+            && seqsel.contains(rnaModel.seq))
     {
       int start = seqsel.getStartRes(), end = seqsel.getEndRes();
-      ShiftList shift = offsets.get(rna);
-      if (shift != null)
+      if (rnaModel.gapped)
       {
-        start = shift.shift(start);
-        end = shift.shift(end);
+        ShiftList shift = offsets.get(rna);
+        if (shift != null)
+        {
+          start = shift.shift(start);
+          end = shift.shift(end);
+        }
       }
+      else
+      {
+        start = rnaModel.seq.findPosition(start) - rnaModel.seq.getStart();
+        end = rnaModel.seq.findPosition(end) - rnaModel.seq.getStart();
+      }
+
       selectionHighlighter.highlightRegion(rna, start, end);
       selectionHighlighter.getLastHighlight()
               .setOutlineColor(seqsel.getOutlineColour());
@@ -628,11 +651,10 @@ public class AppVarna extends JInternalFrame
     ShiftList offset = new ShiftList();
     int ofstart = -1;
     int sleng = seq.getLength();
-    char[] seqChars = seq.getSequence();
 
     for (int i = 0; i < sleng; i++)
     {
-      if (Comparison.isGap(seqChars[i]))
+      if (Comparison.isGap(seq.getCharAt(i)))
       {
         if (ofstart == -1)
         {
diff --git a/src/jalview/gui/AquaInternalFrameManager.java b/src/jalview/gui/AquaInternalFrameManager.java
new file mode 100644 (file)
index 0000000..8ef204c
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * 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);
+      }
+
+      // add or relocate to top of stack
+      if (fChildFrames.indexOf(f) != -1)
+      {
+        fChildFrames.remove(f);
+      }
+      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 7b8ade6..2a96daf 100644 (file)
@@ -298,8 +298,9 @@ public class CutAndPasteTransfer extends GCutAndPasteTransfer
                   AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
           af.getViewport().setShowSequenceFeatures(showSeqFeatures);
           af.getViewport().setFeaturesDisplayed(fd);
-          ColourSchemeI cs = ColourSchemeMapper
-                  .getJalviewColourScheme(colourSchemeName, al);
+          af.setMenusForViewport();
+          ColourSchemeI cs = ColourSchemeMapper.getJalviewColourScheme(
+                  colourSchemeName, al);
           if (cs != null)
           {
             af.changeColour(cs);
diff --git a/src/jalview/gui/DasSourceBrowser.java b/src/jalview/gui/DasSourceBrowser.java
deleted file mode 100644 (file)
index 8570ac3..0000000
+++ /dev/null
@@ -1,864 +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.jbgui.GDasSourceBrowser;
-import jalview.util.MessageManager;
-import jalview.util.TableSorter;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-
-import java.awt.BorderLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Properties;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
-import javax.swing.JCheckBox;
-import javax.swing.JLabel;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JTextField;
-import javax.swing.ListSelectionModel;
-import javax.swing.SwingUtilities;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
-import javax.swing.table.AbstractTableModel;
-
-import org.biodas.jdas.schema.sources.CAPABILITY;
-import org.biodas.jdas.schema.sources.COORDINATES;
-import org.biodas.jdas.schema.sources.PROP;
-import org.biodas.jdas.schema.sources.VERSION;
-
-public class DasSourceBrowser extends GDasSourceBrowser
-        implements Runnable, ListSelectionListener
-{
-  DasSourceRegistryI sourceRegistry = null;
-
-  Vector<String> selectedSources;
-
-  public DasSourceBrowser(FeatureSettings featureSettings)
-  {
-    fs = featureSettings;
-    // TODO DasSourceRegistryProvider API
-    sourceRegistry = jalview.bin.Cache.getDasSourceRegistry();
-    String registry = sourceRegistry.getDasRegistryURL();
-
-    registryURL.setText(registry);
-
-    setSelectedFromProperties();
-
-    displayFullDetails(null);
-    table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-
-    filter1.addListSelectionListener(this);
-    filter2.addListSelectionListener(this);
-    filter3.addListSelectionListener(this);
-
-    // Ask to be notified of selection changes.
-    ListSelectionModel rowSM = table.getSelectionModel();
-    rowSM.addListSelectionListener(new ListSelectionListener()
-    {
-      @Override
-      public void valueChanged(ListSelectionEvent e)
-      {
-        ListSelectionModel lsm = (ListSelectionModel) e.getSource();
-        if (!lsm.isSelectionEmpty())
-        {
-          int selectedRow = lsm.getMinSelectionIndex();
-          displayFullDetails(table.getValueAt(selectedRow, 0).toString());
-        }
-      }
-    });
-
-    table.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mouseClicked(MouseEvent evt)
-      {
-        if (evt.getClickCount() == 2 || evt.isPopupTrigger())
-        {
-          editRemoveLocalSource(evt);
-        }
-      }
-    });
-
-    if (sourceRegistry.getSources() != null)
-    {
-      init();
-    }
-  }
-
-  FeatureSettings fs = null;
-
-  private boolean loadingDasSources;
-
-  public DasSourceBrowser()
-  {
-    this(null);
-  }
-
-  @Override
-  public void paintComponent(java.awt.Graphics g)
-  {
-    if (sourceRegistry == null)
-    {
-      Thread worker = new Thread(this);
-      worker.start();
-    }
-  }
-
-  void init()
-  {
-    List<jalviewSourceI> sources = sourceRegistry.getSources();
-    int dSize = sources.size();
-    Object[][] data = new Object[dSize][2];
-    for (int i = 0; i < dSize; i++)
-    {
-      data[i][0] = sources.get(i).getTitle(); // what's equivalent of nickname
-      data[i][1] = new Boolean(
-              selectedSources.contains(sources.get(i).getTitle()));
-    }
-
-    refreshTableData(data);
-    setCapabilities(sourceRegistry);
-
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        TableSorter sorter = (TableSorter) table.getModel();
-        sorter.setSortingStatus(1, TableSorter.DESCENDING);
-        sorter.setSortingStatus(1, TableSorter.NOT_SORTED);
-      }
-    });
-
-    progressBar.setIndeterminate(false);
-    progressBar.setVisible(false);
-    addLocal.setVisible(true);
-    refresh.setVisible(true);
-  }
-
-  public void refreshTableData(Object[][] data)
-  {
-    TableSorter sorter = new TableSorter(new DASTableModel(data));
-    sorter.setTableHeader(table.getTableHeader());
-    table.setModel(sorter);
-  }
-
-  void displayFullDetails(String nickName)
-  {
-
-    StringBuffer text = new StringBuffer(
-            "<HTML><font size=\"2\" face=\"Verdana, Arial, Helvetica, sans-serif\">");
-
-    if (nickName == null)
-    {
-      fullDetails.setText(text + MessageManager
-              .getString("label.select_das_service_from_table"));
-      return;
-    }
-
-    int dSize = sourceRegistry.getSources().size();
-    for (jalviewSourceI ds : sourceRegistry.getSources())
-    {
-      if (!ds.getTitle().equals(nickName))
-      {
-        continue;
-      }
-
-      VERSION latest = ds.getVersion();
-      text.append(
-              "<font color=\"#0000FF\">Id:</font> " + ds.getUri() + "<br>");
-      text.append("<font color=\"#0000FF\">Nickname:</font> "
-              + ds.getTitle() + "<br>");
-
-      text.append("<font color=\"#0000FF\">URL:</font> <a href=\""
-              + ds.getSourceURL() + "\">" + ds.getSourceURL() + "</a>"
-              + "<br>");
-      if (!ds.isLocal())
-      {
-        if (ds.getDocHref() != null && ds.getDocHref().length() > 0)
-        {
-          text.append("<font color=\"#0000FF\">Site:</font> <a href=\""
-                  + ds.getDocHref() + "\">" + ds.getDocHref() + "</a>"
-                  + "<br>");
-        }
-
-        text.append("<font color=\"#0000FF\">Description:</font> "
-                + ds.getDescription() + "<br>");
-
-        text.append(
-                "<font color=\"#0000FF\">Admin Email:</font> <a href=\"mailto:"
-                        + ds.getEmail() + "\">" + ds.getEmail() + "</a>"
-                        + "<br>");
-
-        text.append("<font color=\"#0000FF\">Registered at:</font> "
-                + latest.getCreated() + "<br>");
-
-        // TODO: Identify last successful test date
-        // text.append("<font color=\"#0000FF\">Last successful test:</font> "
-        // + latest.dasSources[i].getLeaseDate() + "<br>");
-      }
-      else
-      {
-        text.append("Source was added manually.<br/>");
-      }
-      text.append("<font color=\"#0000FF\">Labels:</font> ");
-      boolean b = false;
-      for (PROP labl : latest.getPROP())
-      {
-        if (labl.getName().equalsIgnoreCase("LABEL"))
-        {
-          if (b)
-          {
-            text.append(",");
-          }
-          text.append(" ");
-
-          text.append(labl.getValue());
-          b = true;
-        }
-        ;
-      }
-      text.append("<br>");
-
-      text.append("<font color=\"#0000FF\">Capabilities:</font> ");
-      CAPABILITY[] scap = latest.getCAPABILITY().toArray(new CAPABILITY[0]);
-      for (int j = 0; j < scap.length; j++)
-      {
-        text.append(scap[j].getType());
-        if (j < scap.length - 1)
-        {
-          text.append(", ");
-        }
-      }
-      text.append("<br>");
-
-      text.append("<font color=\"#0000FF\">Coordinates:</font>");
-      int i = 1;
-      for (COORDINATES dcs : latest.getCOORDINATES())
-      {
-        text.append("<br/>" + i++ + ". ");
-        text.append(dcs.getAuthority() + " : " + dcs.getSource());
-        if (dcs.getTaxid() != null && dcs.getTaxid().trim().length() > 0)
-        {
-          text.append(" [TaxId:" + dcs.getTaxid() + "]");
-        }
-        if (dcs.getVersion() != null
-                && dcs.getVersion().trim().length() > 0)
-        {
-          {
-            text.append(" {v. " + dcs.getVersion() + "}");
-          }
-        }
-        text.append(" (<a href=\"" + dcs.getUri() + "\">" + dcs.getUri()
-                + "</a>)");
-      }
-      text.append("</font></html>");
-
-      break;
-    }
-
-    fullDetails.setText(text.toString());
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        fullDetailsScrollpane.getVerticalScrollBar().setValue(0);
-      }
-    });
-  }
-
-  @Override
-  public void run()
-  {
-    loadingDasSources = true;
-
-    addLocal.setVisible(false);
-    refresh.setVisible(false);
-    progressBar.setVisible(true);
-    progressBar.setIndeterminate(true);
-    setParentGuiEnabled(false);
-    // Refresh the source list.
-    sourceRegistry.refreshSources();
-
-    init();
-
-    setParentGuiEnabled(true);
-    loadingDasSources = false;
-
-  }
-
-  private void setParentGuiEnabled(boolean b)
-  {
-    if (fs != null)
-    {
-      fs.fetchDAS.setEnabled(b);
-      fs.saveDAS.setEnabled(b);
-    }
-  }
-
-  public Vector<jalviewSourceI> getSelectedSources()
-  {
-    // wait around if we're still loading.
-    while (sourceRegistry == null)
-    {
-      if (!loadingDasSources)
-      {
-        new Thread(this).start();
-        try
-        {
-          Thread.sleep(5);
-        } catch (Exception e)
-        {
-        }
-        ;
-        while (loadingDasSources)
-        {
-          try
-          {
-            Thread.sleep(5);
-          } catch (Exception e)
-          {
-          }
-          ;
-        }
-        ;
-      }
-    }
-
-    Vector<jalviewSourceI> selected = new Vector<jalviewSourceI>();
-    for (String source : selectedSources)
-    {
-      jalviewSourceI srce = sourceRegistry.getSource(source);
-      if (srce != null)
-      {
-        selected.addElement(srce);
-      }
-    }
-    return selected;
-  }
-
-  @Override
-  public void refresh_actionPerformed(ActionEvent e)
-  {
-    saveProperties(jalview.bin.Cache.applicationProperties);
-
-    Thread worker = new Thread(this);
-    worker.start();
-  }
-
-  private void setCapabilities(DasSourceRegistryI sourceRegistry2)
-  {
-    Vector<String> authority = new Vector<String>();
-    Vector<String> type = new Vector<String>();
-    Vector<String> label = new Vector<String>();
-    Vector<String> taxIds = new Vector<String>();
-    authority.add("Any");
-    type.add("Any");
-    label.add("Any");
-
-    for (jalviewSourceI ds : sourceRegistry2.getSources())
-    {
-      VERSION latest = ds.getVersion();
-
-      for (COORDINATES cs : latest.getCOORDINATES())
-      {
-        if (!type.contains(cs.getSource()))
-        {
-          type.add(cs.getSource()); // source==category
-        }
-
-        if (!authority.contains(cs.getAuthority()))
-        {
-          authority.add(cs.getAuthority());
-        }
-      }
-
-      for (PROP slabel : latest.getPROP())
-      {
-        if (slabel.getName().equalsIgnoreCase("LABEL")
-                && !label.contains(slabel.getValue()))
-        {
-          label.add(slabel.getValue());
-        }
-      }
-
-    }
-
-    filter1.setListData(authority);
-    filter2.setListData(type);
-    filter3.setListData(label);
-    // filter4 taxIds
-
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        filter1.setSelectedIndex(0);
-        filter2.setSelectedIndex(0);
-        filter3.setSelectedIndex(0);
-      }
-    });
-  }
-
-  @Override
-  public void amendLocal(boolean newSource)
-  {
-    String url = "http://localhost:8080/", nickname = "";
-    boolean seqsrc = false;
-    if (!newSource)
-    {
-      int selectedRow = table.getSelectionModel().getMinSelectionIndex();
-      nickname = table.getValueAt(selectedRow, 0).toString();
-      jalviewSourceI source = sourceRegistry.getSource(nickname);
-      url = source.getUri();
-      seqsrc = source.isSequenceSource();
-    }
-
-    JTextField nametf = new JTextField(nickname, 40);
-    JTextField urltf = new JTextField(url, 40);
-    JCheckBox seqs = new JCheckBox(
-            MessageManager.getString("label.sequence_source"));
-    seqs.setSelected(seqsrc);
-    JPanel panel = new JPanel(new BorderLayout());
-    JPanel pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.name:")),
-            BorderLayout.CENTER);
-    pane12.add(nametf, BorderLayout.EAST);
-    panel.add(pane12, BorderLayout.NORTH);
-    pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.url:")),
-            BorderLayout.NORTH);
-    pane12.add(seqs, BorderLayout.SOUTH);
-    pane12.add(urltf, BorderLayout.EAST);
-    panel.add(pane12, BorderLayout.SOUTH);
-
-    int reply = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-            panel, MessageManager.getString("label.enter_local_das_source"),
-            JvOptionPane.OK_CANCEL_OPTION);
-
-    if (reply != JvOptionPane.OK_OPTION)
-    {
-      return;
-    }
-
-    if (!urltf.getText().endsWith("/"))
-    {
-      urltf.setText(urltf.getText() + "/");
-    }
-
-    jalviewSourceI local = sourceRegistry.createLocalSource(urltf.getText(),
-            nametf.getText(), seqs.isSelected(), true);
-    List sources = sourceRegistry.getSources();
-    int osize = sources.size();
-    int size = osize + (newSource ? 1 : 0);
-
-    Object[][] data = new Object[size][2];
-    DASTableModel dtm = (table != null)
-            ? (DASTableModel) ((TableSorter) table.getModel())
-                    .getTableModel()
-            : null;
-    for (int i = 0; i < osize; i++)
-    {
-      String osrc = (dtm == null || i >= osize) ? null
-              : (String) dtm.getValueAt(i, 0);
-      if (!newSource && osrc != null
-              && dtm.getValueAt(i, 0).equals(nickname))
-      {
-        data[i][0] = local.getTitle();
-        data[i][1] = new Boolean(true);
-      }
-      else
-      {
-        data[i][0] = osrc;
-        data[i][1] = new Boolean(selectedSources.contains(osrc));
-      }
-    }
-    // Always add a new source at the end
-    if (newSource)
-    {
-      data[osize][0] = local.getTitle();
-      data[osize][1] = new Boolean(true);
-      selectedSources.add(local.getTitle());
-    }
-
-    refreshTableData(data);
-
-    SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        scrollPane.getVerticalScrollBar()
-                .setValue(scrollPane.getVerticalScrollBar().getMaximum());
-      }
-    });
-
-    displayFullDetails(local.getTitle());
-  }
-
-  public void editRemoveLocalSource(MouseEvent evt)
-  {
-    int selectedRow = table.getSelectionModel().getMinSelectionIndex();
-    if (selectedRow == -1)
-    {
-      return;
-    }
-
-    String nickname = table.getValueAt(selectedRow, 0).toString();
-
-    if (!sourceRegistry.getSource(nickname).isLocal())
-    {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString(
-                      "label.you_can_only_edit_or_remove_local_das_sources"),
-              MessageManager.getString("label.public_das_source"),
-              JvOptionPane.WARNING_MESSAGE);
-      return;
-    }
-
-    Object[] options = { "Edit", "Remove", "Cancel" };
-    int choice = JvOptionPane.showInternalOptionDialog(Desktop.desktop,
-            "Do you want to edit or remove " + nickname + "?",
-            "Edit / Remove Local DAS Source",
-            JvOptionPane.YES_NO_CANCEL_OPTION,
-            JvOptionPane.QUESTION_MESSAGE, null, options, options[2]);
-
-    switch (choice)
-    {
-    case 0:
-      amendLocal(false);
-      break;
-    case 1:
-      sourceRegistry.removeLocalSource(sourceRegistry.getSource(nickname));
-      selectedSources.remove(nickname);
-      Object[][] data = new Object[sourceRegistry.getSources().size()][2];
-      int index = 0, l = table.getRowCount();
-
-      for (int i = 0; i < l; i++)
-      {
-        String nm;
-        if ((nm = (String) table.getValueAt(i, 0)).equals(nickname))
-        {
-          continue;
-        }
-        else
-        {
-          data[index][0] = nm;
-          data[index][1] = new Boolean(selectedSources.contains(nm));
-          index++;
-        }
-      }
-      refreshTableData(data);
-      SwingUtilities.invokeLater(new Runnable()
-      {
-        @Override
-        public void run()
-        {
-          scrollPane.getVerticalScrollBar()
-                  .setValue(scrollPane.getVerticalScrollBar().getMaximum());
-        }
-      });
-
-      break;
-    }
-  }
-
-  @Override
-  public void valueChanged(ListSelectionEvent evt)
-  {
-    // Called when the MainTable selection changes
-    if (evt.getValueIsAdjusting())
-    {
-      return;
-    }
-
-    displayFullDetails(null);
-
-    // Filter the displayed data sources
-
-    ArrayList names = new ArrayList();
-    ArrayList selected = new ArrayList();
-
-    // The features filter is not visible, but we must still
-    // filter the das source list here.
-    // July 2006 - only 6 sources fo not serve features
-    Object[] dummyFeatureList = new Object[] { "features" };
-    List<jalviewSourceI> srcs = sourceRegistry.getSources();
-    for (jalviewSourceI ds : srcs)
-    {
-
-      VERSION v = ds.getVersion();
-      List<COORDINATES> coords = v.getCOORDINATES();
-      if (ds.isLocal() || ((coords == null || coords.size() == 0)
-              && filter1.getSelectedIndex() == 0
-              && filter2.getSelectedIndex() == 0
-              && filter3.getSelectedIndex() == 0))
-      {
-        // THIS IS A FIX FOR LOCAL SOURCES WHICH DO NOT
-        // HAVE COORDINATE SYSTEMS, INFO WHICH AT PRESENT
-        // IS ADDED FROM THE REGISTRY
-        names.add(ds.getTitle());
-        selected.add(new Boolean(selectedSources.contains(ds.getTitle())));
-        continue;
-      }
-
-      if (!selectedInList(dummyFeatureList, ds.getCapabilityList(v))
-              || !selectedInList(filter3.getSelectedValues(),
-                      ds.getLabelsFor(v)))
-      {
-        continue;
-      }
-
-      for (int j = 0; j < coords.size(); j++)
-      {
-        if (selectedInList(filter1.getSelectedValues(),
-                new String[]
-                { coords.get(j).getAuthority() })
-                && selectedInList(filter2.getSelectedValues(), new String[]
-                { coords.get(j).getSource() }))
-        {
-          names.add(ds.getTitle());
-          selected.add(
-                  new Boolean(selectedSources.contains(ds.getTitle())));
-          break;
-        }
-      }
-    }
-
-    int dSize = names.size();
-    Object[][] data = new Object[dSize][2];
-    for (int d = 0; d < dSize; d++)
-    {
-      data[d][0] = names.get(d);
-      data[d][1] = selected.get(d);
-    }
-
-    refreshTableData(data);
-  }
-
-  private boolean selectedInList(Object[] selection, String[] items)
-  {
-    for (int i = 0; i < selection.length; i++)
-    {
-      if (selection[i].equals("Any"))
-      {
-        return true;
-      }
-      if (items == null || items.length == 0)
-      {
-        return false;
-      }
-      String sel = (items[0].startsWith("das1:") ? "das1:" : "")
-              + selection[i];
-      for (int j = 0; j < items.length; j++)
-      {
-        if (sel.equals(items[j]))
-        {
-          return true;
-        }
-      }
-    }
-
-    return false;
-  }
-
-  void setSelectedFromProperties()
-  {
-    String active = jalview.bin.Cache.getDefault("DAS_ACTIVE_SOURCE",
-            "uniprot");
-    StringTokenizer st = new StringTokenizer(active, "\t");
-    selectedSources = new Vector();
-    while (st.hasMoreTokens())
-    {
-      selectedSources.addElement(st.nextToken());
-    }
-  }
-
-  @Override
-  public void reset_actionPerformed(ActionEvent e)
-  {
-    registryURL.setText(sourceRegistry.getDasRegistryURL());
-  }
-
-  /**
-   * set the DAS source settings in the given jalview properties.
-   * 
-   * @param properties
-   */
-  public void saveProperties(Properties properties)
-  {
-    if (registryURL.getText() == null || registryURL.getText().length() < 1)
-    {
-      properties.remove(jalview.bin.Cache.DAS_REGISTRY_URL);
-    }
-    else
-    {
-      properties.setProperty(jalview.bin.Cache.DAS_REGISTRY_URL,
-              registryURL.getText());
-    }
-
-    StringBuffer sb = new StringBuffer();
-    for (int r = 0; r < table.getModel().getRowCount(); r++)
-    {
-      if (((Boolean) table.getValueAt(r, 1)).booleanValue())
-      {
-        sb.append(table.getValueAt(r, 0) + "\t");
-      }
-    }
-
-    properties.setProperty(jalview.bin.Cache.DAS_ACTIVE_SOURCE,
-            sb.toString());
-
-    String sourceprop = sourceRegistry.getLocalSourceString();
-    properties.setProperty(jalview.bin.Cache.DAS_LOCAL_SOURCE, sourceprop);
-  }
-
-  class DASTableModel extends AbstractTableModel
-  {
-
-    public DASTableModel(Object[][] data)
-    {
-      this.data = data;
-    }
-
-    private String[] columnNames = new String[] {
-        MessageManager.getString("label.nickname"),
-        MessageManager.getString("label.use_source") };
-
-    private Object[][] data;
-
-    @Override
-    public int getColumnCount()
-    {
-      return columnNames.length;
-    }
-
-    @Override
-    public int getRowCount()
-    {
-      return data.length;
-    }
-
-    @Override
-    public String getColumnName(int col)
-    {
-      return columnNames[col];
-    }
-
-    @Override
-    public Object getValueAt(int row, int col)
-    {
-      return data[row][col];
-    }
-
-    /*
-     * JTable uses this method to determine the default renderer/ editor for
-     * each cell. If we didn't implement this method, then the last column would
-     * contain text ("true"/"false"), rather than a check box.
-     */
-    @Override
-    public Class getColumnClass(int c)
-    {
-      return getValueAt(0, c).getClass();
-    }
-
-    /*
-     * Don't need to implement this method unless your table's editable.
-     */
-    @Override
-    public boolean isCellEditable(int row, int col)
-    {
-      // Note that the data/cell address is constant,
-      // no matter where the cell appears onscreen.
-      return col == 1;
-
-    }
-
-    /*
-     * Don't need to implement this method unless your table's data can change.
-     */
-    @Override
-    public void setValueAt(Object value, int row, int col)
-    {
-      data[row][col] = value;
-      fireTableCellUpdated(row, col);
-
-      String name = getValueAt(row, 0).toString();
-      boolean selected = ((Boolean) value).booleanValue();
-
-      if (selectedSources.contains(name) && !selected)
-      {
-        selectedSources.remove(name);
-      }
-
-      if (!selectedSources.contains(name) && selected)
-      {
-        selectedSources.add(name);
-      }
-    }
-  }
-
-  public void initDasSources()
-  {
-
-    Thread thr = new Thread(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        // this actually initialises the das source list
-        paintComponent(null); // yuk
-      }
-    });
-    thr.start();
-    while (loadingDasSources || sourceRegistry == null)
-    {
-      try
-      {
-        Thread.sleep(10);
-      } catch (Exception e)
-      {
-      }
-      ;
-    }
-  }
-
-  /**
-   * disable or enable the buttons on the source browser
-   * 
-   * @param b
-   */
-  public void setGuiEnabled(boolean b)
-  {
-    refresh.setEnabled(b);
-    addLocal.setEnabled(b);
-  }
-}
index c8d900d..8d9e366 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()
@@ -529,7 +519,7 @@ public class Desktop extends jalview.jbgui.GDesktop
   {
     final Desktop me = this;
     // Thread off the news reader, in case there are connection problems.
-    addDialogThread(new Runnable()
+    new Thread(new Runnable()
     {
       @Override
       public void run()
@@ -540,13 +530,13 @@ public class Desktop extends jalview.jbgui.GDesktop
         showNews.setVisible(true);
         Cache.log.debug("Completed news thread.");
       }
-    });
+    }).start();
   }
 
   public void getIdentifiersOrgData()
   {
     // Thread off the identifiers fetcher
-    addDialogThread(new Runnable()
+    new Thread(new Runnable()
     {
       @Override
       public void run()
@@ -563,7 +553,8 @@ public class Desktop extends jalview.jbgui.GDesktop
                   + e.getMessage());
         }
       }
-    });
+    }).start();
+    ;
   }
 
   @Override
@@ -866,13 +857,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 +878,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 +908,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 +927,8 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     });
 
+    setKeyBindings(frame);
+
     desktop.add(frame);
 
     windowMenu.add(menuItem);
@@ -966,6 +948,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 +1029,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 +1433,6 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       ssm.resetAll();
     }
-    System.gc();
   }
 
   @Override
@@ -1720,7 +1737,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 +1829,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 +1864,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 +2377,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 +2471,7 @@ public class Desktop extends jalview.jbgui.GDesktop
         Thread worker = new Thread(this);
         worker.start();
       }
+      repaint();
     }
 
     public boolean isShowMemoryUsage()
@@ -2517,14 +2535,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 +2555,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 +2600,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 +2743,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 +3337,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 +3425,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;
+          Cache.log.debug("Adding missing FILE protocol for "
+                  + files.get(protocols.size()));
+          protocols.add(DataSourceType.FILE);
         }
-        java.net.URI uri = new java.net.URI(s);
-        if (uri.getScheme().toLowerCase().startsWith("http"))
+        for (java.util.StringTokenizer st = new java.util.StringTokenizer(
+                data, "\r\n"); st.hasMoreTokens();)
         {
-          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());
+          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 +3545,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 396df04..0000000
+++ /dev/null
@@ -1,622 +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.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();
-      }
-    });
-    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 358798d..46f574e 100644 (file)
@@ -41,6 +41,7 @@ import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
@@ -179,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)
       {
@@ -204,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);
+            }
+          });
         }
       }
     });
@@ -234,19 +233,26 @@ public class FeatureRenderer
     if (!create && features.size() > 1)
     {
       /*
-       * more than one feature at selected position - add a drop-down
-       * to choose the feature to amend
+       * more than one feature at selected position - 
+       * add a drop-down to choose the feature to amend
+       * space pad text if necessary to make entries distinct
        */
       gridPanel = new JPanel(new GridLayout(4, 1));
       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)
       {
-        String text = sf.getType() + "/" + sf.getBegin() + "-" + sf.getEnd()
-                + " (" + sf.getFeatureGroup() + ")";
+        String text = String.format("%s/%d-%d (%s)", sf.getType(),
+                sf.getBegin(), sf.getEnd(), sf.getFeatureGroup());
+        while (added.contains(text))
+        {
+          text += " ";
+        }
         overlaps.addItem(text);
+        added.add(text);
       }
       choosePanel.add(overlaps);
 
@@ -270,9 +276,8 @@ public class FeatureRenderer
             highlight.addResult(sequences.get(0), sf.getBegin(),
                     sf.getEnd());
 
-            alignPanel.getSeqPanel().seqCanvas
-                    .highlightSearchResults(highlight);
-
+            alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(
+                    highlight, false);
           }
           FeatureColourI col = getFeatureStyle(name.getText());
           if (col == null)
@@ -386,7 +391,10 @@ public class FeatureRenderer
 
     FeaturesFile ffile = new FeaturesFile();
 
-    String enteredType = name.getText().trim();
+    final String enteredType = name.getText().trim();
+    final String enteredGroup = group.getText().trim();
+    final String enteredDescription = description.getText().replaceAll("\n", " ");
+
     if (reply == JvOptionPane.OK_OPTION && enteredType.length() > 0)
     {
       /*
@@ -395,7 +403,7 @@ public class FeatureRenderer
       if (useLastDefaults)
       {
         lastFeatureAdded = enteredType;
-        lastFeatureGroupAdded = group.getText().trim();
+        lastFeatureGroupAdded = enteredGroup;
         // TODO: determine if the null feature group is valid
         if (lastFeatureGroupAdded.length() < 1)
         {
@@ -421,26 +429,37 @@ public class FeatureRenderer
       {
         /*
          * YES_OPTION corresponds to the Amend button
-         * need to refresh Feature Settings if type, group or colour changed
+         * need to refresh Feature Settings if type, group or colour changed;
+         * note we don't force the feature to be visible - the user has been
+         * warned if a hidden feature type or group was entered
          */
-        sf.type = enteredType;
-        sf.featureGroup = group.getText().trim();
-        sf.description = description.getText().replaceAll("\n", " ");
-        boolean refreshSettings = (!featureType.equals(sf.type)
-                || !featureGroup.equals(sf.featureGroup));
+        boolean refreshSettings = (!featureType.equals(enteredType) || !featureGroup
+                .equals(enteredGroup));
         refreshSettings |= (fcol != oldcol);
-
-        setColour(sf.type, fcol);
-
+        setColour(enteredType, fcol);
+        int newBegin = sf.begin;
+        int newEnd = sf.end;
         try
         {
-          sf.begin = ((Integer) start.getValue()).intValue();
-          sf.end = ((Integer) end.getValue()).intValue();
+          newBegin = ((Integer) start.getValue()).intValue();
+          newEnd = ((Integer) end.getValue()).intValue();
         } catch (NumberFormatException ex)
         {
+          // JSpinner doesn't accept invalid format data :-)
         }
 
-        ffile.parseDescriptionHTML(sf, false);
+        /*
+         * replace the feature by deleting it and adding a new one
+         * (to ensure integrity of SequenceFeatures data store)
+         */
+        sequences.get(0).deleteFeature(sf);
+        SequenceFeature newSf = new SequenceFeature(sf, enteredType,
+                newBegin, newEnd, enteredGroup, sf.getScore());
+        newSf.setDescription(enteredDescription);
+        ffile.parseDescriptionHTML(newSf, false);
+        // amend features dialog only updates one sequence at a time
+        sequences.get(0).addSequenceFeature(newSf);
+
         if (refreshSettings)
         {
           featuresAdded();
@@ -455,19 +474,18 @@ public class FeatureRenderer
         for (int i = 0; i < sequences.size(); i++)
         {
           SequenceFeature sf = features.get(i);
-          sf.type = enteredType;
-          // fix for JAL-1538 - always set feature group here
-          sf.featureGroup = group.getText().trim();
-          sf.description = description.getText().replaceAll("\n", " ");
-          sequences.get(i).addSequenceFeature(sf);
-          ffile.parseDescriptionHTML(sf, false);
+          SequenceFeature sf2 = new SequenceFeature(enteredType,
+                  enteredDescription, sf.getBegin(), sf.getEnd(),
+                  enteredGroup);
+          ffile.parseDescriptionHTML(sf2, false);
+          sequences.get(i).addSequenceFeature(sf2);
         }
 
         setColour(enteredType, fcol);
 
         featuresAdded();
 
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(true, true);
 
         return true;
       }
@@ -477,7 +495,7 @@ public class FeatureRenderer
       }
     }
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
 
     return true;
   }
index 109d0b6..1358c8f 100644 (file)
@@ -22,20 +22,21 @@ package jalview.gui;
 
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
-import jalview.bin.Cache;
-import jalview.datamodel.SequenceFeature;
+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.ws.dbsources.das.api.jalviewSourceI;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -44,6 +45,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,13 +63,14 @@ 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;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.Vector;
 
 import javax.help.HelpSetException;
 import javax.swing.AbstractCellEditor;
@@ -86,36 +89,52 @@ 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;
-import javax.swing.SwingUtilities;
 import javax.swing.event.ChangeEvent;
 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");
 
-  jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher;
+  /*
+   * 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;
 
-  JPanel settingsPane = new JPanel();
+  private static final int COLUMN_COUNT = 4;
 
-  JPanel dasSettingsPane = new JPanel();
+  private static final int MIN_WIDTH = 400;
+
+  private static final int MIN_HEIGHT = 400;
 
   final FeatureRenderer fr;
 
   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 +145,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 +200,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 +250,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 +263,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 +274,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());
         }
       }
     });
@@ -248,9 +314,6 @@ public class FeatureSettings extends JPanel
     // MessageManager.getString("label.feature_settings_click_drag")));
     scrollPane.setViewportView(table);
 
-    dassourceBrowser = new DasSourceBrowser(this);
-    dasSettingsPane.add(dassourceBrowser, BorderLayout.CENTER);
-
     if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
     {
       fr.findAllFeatures(true); // display everything!
@@ -267,8 +330,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 +344,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));
 
@@ -299,13 +362,13 @@ public class FeatureSettings extends JPanel
                       javax.swing.event.InternalFrameEvent evt)
               {
                 fr.removePropertyChangeListener(change);
-                dassourceBrowser.fs = null;
               };
             });
     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 +408,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,56 +521,22 @@ public class FeatureSettings extends JPanel
     men.show(table, x, y);
   }
 
-  /**
-   * true when Feature Settings are updating from feature renderer
-   */
-  private boolean handlingUpdate = false;
-
-  /**
-   * contains a float[3] for each feature type string. created by setTableData
-   */
-  Map<String, float[]> typeWidth = null;
-
   @Override
   synchronized public void discoverAllFeatureData()
   {
-    Vector<String> allFeatures = new Vector<String>();
-    Vector<String> allGroups = new Vector<String>();
-    SequenceFeature[] tmpfeatures;
-    String group;
-    for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
-    {
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+    Set<String> allGroups = new HashSet<>();
+    AlignmentI alignment = af.getViewport().getAlignment();
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+    for (int i = 0; i < alignment.getHeight(); i++)
+    {
+      SequenceI seq = alignment.getSequenceAt(i);
+      for (String group : seq.getFeatures().getFeatureGroups(true))
       {
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
-        {
-          index++;
-          continue;
-        }
-
-        if (tmpfeatures[index].getFeatureGroup() != null)
-        {
-          group = tmpfeatures[index].featureGroup;
-          if (!allGroups.contains(group))
-          {
-            allGroups.addElement(group);
-            checkGroupState(group);
-          }
-        }
-
-        if (!allFeatures.contains(tmpfeatures[index].getType()))
+        if (group != null && !allGroups.contains(group))
         {
-          allFeatures.addElement(tmpfeatures[index].getType());
+          allGroups.add(group);
+          checkGroupState(group);
         }
-        index++;
       }
     }
 
@@ -552,102 +567,84 @@ 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 == true)
+    if (resettingTable)
     {
       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
-    float[] avWidth = null;
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector<String> visibleChecks = new Vector<String>();
-    Set<String> foundGroups = new HashSet<String>();
-
-    // Find out which features should be visible depending on which groups
-    // are selected / deselected
-    // and recompute average width ordering
+
+    Set<String> displayableTypes = new HashSet<>();
+    Set<String> foundGroups = new HashSet<>();
+
+    /*
+     * determine which feature types may be visible depending on 
+     * which groups are selected, and recompute average width data
+     */
     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
     {
 
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+      SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+      /*
+       * get the sequence's groups for positional features
+       * and keep track of which groups are visible
+       */
+      Set<String> groups = seq.getFeatures().getFeatureGroups(true);
+      Set<String> visibleGroups = new HashSet<>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-        foundGroups.add(group);
-
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
-        {
-          index++;
-          continue;
-        }
-
         if (group == null || checkGroupState(group))
         {
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
-        }
-        if (!typeWidth.containsKey(tmpfeatures[index].getType()))
-        {
-          typeWidth.put(tmpfeatures[index].getType(),
-                  avWidth = new float[3]);
-        }
-        else
-        {
-          avWidth = typeWidth.get(tmpfeatures[index].getType());
-        }
-        avWidth[0]++;
-        if (tmpfeatures[index].getBegin() > tmpfeatures[index].getEnd())
-        {
-          avWidth[1] += 1 + tmpfeatures[index].getBegin()
-                  - tmpfeatures[index].getEnd();
+          visibleGroups.add(group);
         }
-        else
+      }
+      foundGroups.addAll(groups);
+
+      /*
+       * get distinct feature types for visible groups
+       * record distinct visible types, and their count and total length
+       */
+      Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
+              visibleGroups.toArray(new String[visibleGroups.size()]));
+      for (String type : types)
+      {
+        displayableTypes.add(type);
+        float[] avWidth = typeWidth.get(type);
+        if (avWidth == null)
         {
-          avWidth[1] += 1 + tmpfeatures[index].getEnd()
-                  - tmpfeatures[index].getBegin();
+          avWidth = new float[2];
+          typeWidth.put(type, avWidth);
         }
-        index++;
+        // todo this could include features with a non-visible group
+        // - do we greatly care?
+        // todo should we include non-displayable features here, and only
+        // update when features are added?
+        avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
+        avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
       }
     }
 
-    int fSize = visibleChecks.size();
-    Object[][] data = new Object[fSize][3];
+    Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
     int dataIndex = 0;
 
     if (fr.hasRenderOrder())
@@ -663,48 +660,57 @@ public class FeatureSettings extends JPanel
       List<String> frl = fr.getRenderOrder();
       for (int ro = frl.size() - 1; ro > -1; ro--)
       {
-        type = frl.get(ro);
+        String type = frl.get(ro);
 
-        if (!visibleChecks.contains(type))
+        if (!displayableTypes.contains(type))
         {
           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++;
-        visibleChecks.removeElement(type);
+        displayableTypes.remove(type);
       }
     }
 
-    fSize = visibleChecks.size();
-    for (int i = 0; i < fSize; i++)
+    /*
+     * process any extra features belonging only to 
+     * a group which was just selected
+     */
+    while (!displayableTypes.isEmpty())
     {
-      // These must be extra features belonging to the group
-      // which was just selected
-      type = visibleChecks.elementAt(i).toString();
-      data[dataIndex][0] = type;
+      String type = displayableTypes.iterator().next();
+      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
@@ -725,8 +731,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>
@@ -742,27 +748,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;
               }
             }
@@ -775,10 +781,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;
@@ -788,8 +796,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
    */
@@ -832,10 +840,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"));
@@ -846,88 +858,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)
+      }
+
+      /*
+       * update feature settings table
+       */
+      if (table != null)
       {
-        System.out.println("Error loading User Colour File\n" + ex);
+        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"));
@@ -937,68 +939,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()
@@ -1011,17 +1032,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
       {
@@ -1038,16 +1058,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)
       {
@@ -1081,97 +1102,80 @@ 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();
+  /**
+   * 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;
+  }
 
-  BorderLayout borderLayout2 = new BorderLayout();
+  private void jbInit() throws Exception
+  {
+    this.setLayout(new BorderLayout());
 
-  BorderLayout borderLayout3 = new BorderLayout();
+    JPanel settingsPane = new JPanel();
+    settingsPane.setLayout(new BorderLayout());
 
-  JPanel bigPanel = new JPanel();
+    JPanel bigPanel = new JPanel();
+    bigPanel.setLayout(new BorderLayout());
 
-  BorderLayout borderLayout4 = new BorderLayout();
+    groupPanel = new JPanel();
+    bigPanel.add(groupPanel, BorderLayout.NORTH);
 
-  JButton invert = new JButton();
+    JButton invert = new JButton(
+            MessageManager.getString("label.invert_selection"));
+    invert.setFont(JvSwingUtils.getLabelFont());
+    invert.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        invertSelection();
+      }
+    });
 
-  JPanel buttonPanel = new JPanel();
+    JButton optimizeOrder = new JButton(
+            MessageManager.getString("label.optimise_order"));
+    optimizeOrder.setFont(JvSwingUtils.getLabelFont());
+    optimizeOrder.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        orderByAvWidth();
+      }
+    });
 
-  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();
-
-  JButton sortByDens = new JButton();
-
-  JButton help = new JButton();
-
-  JPanel transbuttons = new JPanel(new GridLayout(5, 1));
-
-  private void jbInit() throws Exception
-  {
-    this.setLayout(borderLayout1);
-    settingsPane.setLayout(borderLayout2);
-    dasSettingsPane.setLayout(borderLayout3);
-    bigPanel.setLayout(borderLayout4);
-
-    groupPanel = new JPanel();
-    bigPanel.add(groupPanel, BorderLayout.NORTH);
-
-    invert.setFont(JvSwingUtils.getLabelFont());
-    invert.setText(MessageManager.getString("label.invert_selection"));
-    invert.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        invertSelection();
-      }
-    });
-    optimizeOrder.setFont(JvSwingUtils.getLabelFont());
-    optimizeOrder.setText(MessageManager.getString("label.optimise_order"));
-    optimizeOrder.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        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
@@ -1180,9 +1184,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
@@ -1191,8 +1195,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
@@ -1223,20 +1228,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
@@ -1245,8 +1253,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
@@ -1255,8 +1267,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
@@ -1270,227 +1286,39 @@ 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);
+        }
       }
     });
 
     transparency.setMaximum(70);
     transparency.setToolTipText(
             MessageManager.getString("label.transparency_tip"));
-    fetchDAS.setText(MessageManager.getString("label.fetch_das_features"));
-    fetchDAS.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        fetchDAS_actionPerformed(e);
-      }
-    });
-    saveDAS.setText(MessageManager.getString("action.save_as_default"));
-    saveDAS.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        saveDAS_actionPerformed(e);
-      }
-    });
-    dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
-    dasSettingsPane.setBorder(null);
-    cancelDAS.setEnabled(false);
-    cancelDAS.setText(MessageManager.getString("action.cancel_fetch"));
-    cancelDAS.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        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);
-    dasButtonPanel.add(fetchDAS);
-    dasButtonPanel.add(cancelDAS);
-    dasButtonPanel.add(saveDAS);
-    settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER);
-    settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH);
-  }
-
-  public void fetchDAS_actionPerformed(ActionEvent e)
-  {
-    fetchDAS.setEnabled(false);
-    cancelDAS.setEnabled(true);
-    dassourceBrowser.setGuiEnabled(false);
-    Vector<jalviewSourceI> selectedSources = dassourceBrowser
-            .getSelectedSources();
-    doDasFeatureFetch(selectedSources, true, true);
-  }
-
-  /**
-   * get the features from selectedSources for all or the current selection
-   * 
-   * @param selectedSources
-   * @param checkDbRefs
-   * @param promptFetchDbRefs
-   */
-  private void doDasFeatureFetch(List<jalviewSourceI> selectedSources,
-          boolean checkDbRefs, boolean promptFetchDbRefs)
-  {
-    SequenceI[] dataset, seqs;
-    int iSize;
-    AlignmentViewport vp = af.getViewport();
-    if (vp.getSelectionGroup() != null
-            && vp.getSelectionGroup().getSize() > 0)
-    {
-      iSize = vp.getSelectionGroup().getSize();
-      dataset = new SequenceI[iSize];
-      seqs = vp.getSelectionGroup().getSequencesInOrder(vp.getAlignment());
-    }
-    else
-    {
-      iSize = vp.getAlignment().getHeight();
-      seqs = vp.getAlignment().getSequencesArray();
-    }
-
-    dataset = new SequenceI[iSize];
-    for (int i = 0; i < iSize; i++)
-    {
-      dataset[i] = seqs[i].getDatasetSequence();
-    }
-
-    cancelDAS.setEnabled(true);
-    dasFeatureFetcher = new jalview.ws.DasSequenceFeatureFetcher(dataset,
-            this, selectedSources, checkDbRefs, promptFetchDbRefs);
-    af.getViewport().setShowSequenceFeatures(true);
-    af.showSeqFeatures.setSelected(true);
-  }
-
-  /**
-   * blocking call to initialise the das source browser
-   */
-  public void initDasSources()
-  {
-    dassourceBrowser.initDasSources();
-  }
-
-  /**
-   * examine the current list of das sources and return any matching the given
-   * nicknames in sources
-   * 
-   * @param sources
-   *          Vector of Strings to resolve to DAS source nicknames.
-   * @return sources that are present in source list.
-   */
-  public List<jalviewSourceI> resolveSourceNicknames(Vector<String> sources)
-  {
-    return dassourceBrowser.sourceRegistry.resolveSourceNicknames(sources);
-  }
-
-  /**
-   * get currently selected das sources. ensure you have called initDasSources
-   * before calling this.
-   * 
-   * @return vector of selected das source nicknames
-   */
-  public Vector<jalviewSourceI> getSelectedSources()
-  {
-    return dassourceBrowser.getSelectedSources();
-  }
-
-  /**
-   * properly initialise DAS fetcher and then initiate a new thread to fetch
-   * features from the named sources (rather than any turned on by default)
-   * 
-   * @param sources
-   * @param block
-   *          if true then runs in same thread, otherwise passes to the Swing
-   *          executor
-   */
-  public void fetchDasFeatures(Vector<String> sources, boolean block)
-  {
-    initDasSources();
-    List<jalviewSourceI> resolved = dassourceBrowser.sourceRegistry
-            .resolveSourceNicknames(sources);
-    if (resolved.size() == 0)
-    {
-      resolved = dassourceBrowser.getSelectedSources();
-    }
-    if (resolved.size() > 0)
-    {
-      final List<jalviewSourceI> dassources = resolved;
-      fetchDAS.setEnabled(false);
-      // cancelDAS.setEnabled(true); doDasFetch does this.
-      Runnable fetcher = new Runnable()
-      {
-
-        @Override
-        public void run()
-        {
-          doDasFeatureFetch(dassources, true, false);
-
-        }
-      };
-      if (block)
-      {
-        fetcher.run();
-      }
-      else
-      {
-        SwingUtilities.invokeLater(fetcher);
-      }
-    }
-  }
-
-  public void saveDAS_actionPerformed(ActionEvent e)
-  {
-    dassourceBrowser
-            .saveProperties(jalview.bin.Cache.applicationProperties);
-  }
-
-  public void complete()
-  {
-    fetchDAS.setEnabled(true);
-    cancelDAS.setEnabled(false);
-    dassourceBrowser.setGuiEnabled(true);
-
-  }
-
-  public void cancelDAS_actionPerformed(ActionEvent e)
-  {
-    if (dasFeatureFetcher != null)
-    {
-      dasFeatureFetcher.cancel();
-    }
-    complete();
-  }
-
-  public void noDasSourceActive()
-  {
-    complete();
-    JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-            MessageManager.getString("label.no_das_sources_selected_warn"),
-            MessageManager.getString("label.no_das_sources_selected_title"),
-            JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
+    bigPanel.add(scrollPane, BorderLayout.CENTER);
+    settingsPane.add(bigPanel, BorderLayout.CENTER);
+    settingsPane.add(buttonPanel, BorderLayout.SOUTH);
+    this.add(settingsPane);
   }
 
   // ///////////////////////////////////////////////////////////////////////
@@ -1498,18 +1326,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;
@@ -1549,10 +1378,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
@@ -1591,12 +1424,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())
@@ -1604,14 +1432,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)
       {
@@ -1636,6 +1462,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
    * 
@@ -1662,28 +1536,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
@@ -1699,17 +1588,259 @@ 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);
+          /**
+           * @j2sNative
+           */
+          {
+            chooser.setRequestFocusEnabled(true);
+            chooser.requestFocus();
+          }
+          chooser.addActionListener(this);
+          // Make the renderer reappear.
+          fireEditingStopped();
+        }
       }
       else
       {
-        comp.setToolTipText(tt + " " + comp.getToolTipText());
+        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
+      {
+        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();
+        }
+        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;
     }
   }
 }
@@ -1795,124 +1926,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..55bc519
--- /dev/null
@@ -0,0 +1,1743 @@
+/*
+ * 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.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();
+
+  /**
+   * colours and filters are shown in tabbed view or single content pane
+   */
+  JPanel coloursPanel, filtersPanel;
+
+  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;
+
+  private JPanel chooseFiltersPanel;
+
+  /**
+   * Constructor
+   * 
+   * @param frender
+   * @param theType
+   */
+  public FeatureTypeSettings(FeatureRenderer frender, 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, false, title, 500, 500);
+    
+    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())
+      {
+        singleColour.setBackground(fc.getColour());
+        singleColour.setForeground(fc.getColour());
+        simpleColour.setSelected(true);
+      }
+
+      /*
+       * 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());
+
+    /*
+     * an ActionListener that applies colour changes
+     */
+    changeColourAction = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        colourChanged(true);
+      }
+    };
+
+    /*
+     * first panel/tab: colour options
+     */
+    JPanel coloursPanel = initialiseColoursPanel();
+    this.add(coloursPanel, BorderLayout.NORTH);
+
+    /*
+     * second panel/tab: filter options
+     */
+    JPanel filtersPanel = initialiseFiltersPanel();
+    this.add(filtersPanel, BorderLayout.CENTER);
+
+    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 last plain colour;
+     * make min colour a pale version of max colour
+     */
+    FeatureColourI fc = fr.getFeatureColours().get(featureType);
+    Color bg = fc.getColour() == null ? Color.BLACK : fc.getColour();
+    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.setBackground(Color.white);
+    colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
+    JvSwingUtils.createTitledBorder(colourByPanel,
+            MessageManager.getString("action.colour"), true);
+
+    /*
+     * simple colour radio button and colour picker
+     */
+    JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    simpleColourPanel.setBackground(Color.white);
+    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)
+        {
+          colourChanged(true);
+        }
+      }
+    });
+
+    singleColour.setFont(JvSwingUtils.getLabelFont());
+    singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    singleColour.setPreferredSize(new Dimension(40, 20));
+    if (originalColour.isGraduatedColour())
+    {
+      singleColour.setBackground(originalColour.getMaxColour());
+      singleColour.setForeground(originalColour.getMaxColour());
+    }
+    else
+    {
+      singleColour.setBackground(originalColour.getColour());
+      singleColour.setForeground(originalColour.getColour());
+    }
+    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 = singleColour.getBackground();
+      FeatureColourI fc = new FeatureColour(c);
+      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);
+    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);
+      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
+     */
+    setNumericHints(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, retrieves the min-max range
+   * for the value (score or attribute), and sets it as a tooltip on the value
+   * field. If the field is currently empty, then pre-populates it with
+   * <ul>
+   * <li>the minimum value, if condition is > or >=</li>
+   * <li>the maximum value, if condition is < or <=</li>
+   * </ul>
+   * 
+   * @param attName
+   * @param selectedCondition
+   * @param patternField
+   */
+  private void setNumericHints(String attName, Condition selectedCondition,
+          JTextField patternField)
+  {
+    patternField.setToolTipText("");
+
+    if (selectedCondition.isNumeric())
+    {
+      float[] minMax = getMinMax(attName);
+      if (minMax != null)
+      {
+        String minFormatted = DECFMT_2_2.format(minMax[0]);
+        String maxFormatted = DECFMT_2_2.format(minMax[1]);
+        String tip = String.format("(%s - %s)", minFormatted, maxFormatted);
+        patternField.setToolTipText(tip);
+        if (patternField.getText().isEmpty())
+        {
+          if (selectedCondition == Condition.GE
+                  || selectedCondition == Condition.GT)
+          {
+            patternField.setText(minFormatted);
+          }
+          else
+          {
+            if (selectedCondition == Condition.LE
+                    || selectedCondition == Condition.LT)
+            {
+              patternField.setText(maxFormatted);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * 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.Number)
+              || (!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);
+    }
+
+    /*
+     * 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("");
+      }
+    }
+
+    /*
+     * restore the listener
+     */
+    condCombo.addItemListener(listener);
+  }
+
+  /**
+   * 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();
+
+    setNumericHints(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;
+  }
+
+  /**
+   * 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 f526592..84540f4 100755 (executable)
@@ -223,8 +223,9 @@ public class Finder extends GFinder
     for (SearchResultMatchI match : searchResults.getResults())
     {
       seqs.add(match.getSequence().getDatasetSequence());
-      features.add(new SequenceFeature(searchString, desc, null,
-              match.getStart(), match.getEnd(), desc));
+      features.add(new SequenceFeature(searchString, desc,
+              match
+              .getStart(), match.getEnd(), desc));
     }
 
     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
index 6cddcca..c66f304 100755 (executable)
@@ -134,7 +134,7 @@ public class FontChooser extends GFontChooser
       fontAsCdna.setSelected(ap.av.isProteinFontAsCdna());
     }
 
-    if (tp != null)
+    if (isTreeFont())
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("action.change_font_tree_panel"),
@@ -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
@@ -229,13 +229,17 @@ public class FontChooser extends GFontChooser
   @Override
   protected void cancel_actionPerformed()
   {
-    if (ap != null)
+    if (isTreeFont())
+    {
+      tp.setTreeFont(oldFont);
+    }
+    else if (ap != null)
     {
       ap.av.setFont(oldFont, true);
       ap.av.setScaleProteinAsCdna(oldProteinScale);
       ap.av.setProteinFontAsCdna(oldMirrorFont);
       ap.av.antiAlias = oldSmoothFont;
-      ap.paintAlignment(true);
+      ap.fontChanged();
 
       if (scaleAsCdna.isVisible() && scaleAsCdna.isEnabled())
       {
@@ -250,10 +254,6 @@ public class FontChooser extends GFontChooser
         splitFrame.repaint();
       }
     }
-    else if (tp != null)
-    {
-      tp.setTreeFont(oldFont);
-    }
 
     try
     {
@@ -263,6 +263,11 @@ public class FontChooser extends GFontChooser
     }
   }
 
+  private boolean isTreeFont()
+  {
+    return tp != null;
+  }
+
   /**
    * DOCUMENT ME!
    */
@@ -317,7 +322,7 @@ public class FontChooser extends GFontChooser
       }
       return;
     }
-    if (tp != null)
+    if (isTreeFont())
     {
       tp.setTreeFont(newFont);
     }
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..cf88c90 100755 (executable)
@@ -63,10 +63,6 @@ public class IdCanvas extends JPanel implements ViewportListenerI
 
   List<SequenceI> searchResults;
 
-  FontMetrics fm;
-
-  AnnotationLabels labels = null;
-
   AnnotationPanel ap;
 
   private Font idfont;
@@ -83,12 +79,12 @@ public class IdCanvas extends JPanel implements ViewportListenerI
     this.av = av;
     PaintRefresher.Register(this, av.getSequenceSetId());
     av.getRanges().addPropertyChangeListener(this);
-  }
+    }
 
   /**
    * DOCUMENT ME!
    * 
-   * @param gg
+   * @param g
    *          DOCUMENT ME!
    * @param hiddenRows
    *          true - check and display hidden row marker if need be
@@ -101,7 +97,7 @@ public class IdCanvas extends JPanel implements ViewportListenerI
    * @param ypos
    *          DOCUMENT ME!
    */
-  public void drawIdString(Graphics2D gg, boolean hiddenRows, SequenceI s,
+  public void drawIdString(Graphics2D g, boolean hiddenRows, SequenceI s,
           int i, int starty, int ypos)
   {
     int xPos = 0;
@@ -110,39 +106,40 @@ public class IdCanvas extends JPanel implements ViewportListenerI
 
     if ((searchResults != null) && searchResults.contains(s))
     {
-      gg.setColor(Color.black);
-      gg.fillRect(0, ((i - starty) * charHeight) + ypos, getWidth(),
+      g.setColor(Color.black);
+      g.fillRect(0, ((i - starty) * charHeight) + ypos, getWidth(),
               charHeight);
-      gg.setColor(Color.white);
+      g.setColor(Color.white);
     }
     else if ((av.getSelectionGroup() != null)
             && av.getSelectionGroup().getSequences(null).contains(s))
     {
-      gg.setColor(Color.lightGray);
-      gg.fillRect(0, ((i - starty) * charHeight) + ypos, getWidth(),
+      g.setColor(Color.lightGray);
+      g.fillRect(0, ((i - starty) * charHeight) + ypos, getWidth(),
               charHeight);
-      gg.setColor(Color.white);
+      g.setColor(Color.white);
     }
     else
     {
-      gg.setColor(av.getSequenceColour(s));
-      gg.fillRect(0, ((i - starty) * charHeight) + ypos, getWidth(),
+      g.setColor(av.getSequenceColour(s));
+      g.fillRect(0, ((i - starty) * charHeight) + ypos, getWidth(),
               charHeight);
-      gg.setColor(Color.black);
+      g.setColor(Color.black);
     }
 
     if (av.isRightAlignIds())
     {
+      FontMetrics fm = g.getFontMetrics();
       xPos = panelWidth
               - fm.stringWidth(s.getDisplayId(av.getShowJVSuffix())) - 4;
     }
 
-    gg.drawString(s.getDisplayId(av.getShowJVSuffix()), xPos,
+    g.drawString(s.getDisplayId(av.getShowJVSuffix()), xPos,
             (((i - starty + 1) * charHeight) + ypos) - (charHeight / 5));
 
     if (hiddenRows)
     {
-      drawMarker(i, starty, ypos);
+      drawMarker(g, av, i, starty, ypos);
     }
 
   }
@@ -199,12 +196,16 @@ public class IdCanvas extends JPanel implements ViewportListenerI
 
     gg.translate(0, transY);
 
-    drawIds(ss, es);
+    drawIds(gg, av, ss, es, searchResults);
 
     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,192 +217,202 @@ 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());
-
+    
+    drawIds(gg, av, av.getRanges().getStartSeq(), av.getRanges().getEndSeq(), searchResults);
+    
     g.drawImage(image, 0, 0, this);
   }
 
   /**
-   * DOCUMENT ME!
+   * Draws sequence ids from sequence index startSeq to endSeq (inclusive), with
+   * the font and other display settings configured on the viewport. Ids of
+   * sequences included in the selection are coloured grey, otherwise the
+   * current id colour for the sequence id is used.
    * 
-   * @param starty
-   *          DOCUMENT ME!
-   * @param endy
-   *          DOCUMENT ME!
+   * @param g
+   * @param alignViewport
+   * @param startSeq
+   * @param endSeq
+   * @param selection
    */
-  void drawIds(int starty, int endy)
+  void drawIds(Graphics2D g, AlignViewport alignViewport, final int startSeq,
+          final int endSeq, List<SequenceI> selection)
   {
-    if (av.isSeqNameItalics())
+    Font font = alignViewport.getFont();
+    if (alignViewport.isSeqNameItalics())
     {
-      setIdfont(new Font(av.getFont().getName(), Font.ITALIC,
-              av.getFont().getSize()));
+      setIdfont(new Font(font.getName(), Font.ITALIC,
+              font.getSize()));
     }
     else
     {
-      setIdfont(av.getFont());
+      setIdfont(font);
     }
 
-    gg.setFont(getIdfont());
-    fm = gg.getFontMetrics();
+    g.setFont(getIdfont());
+    FontMetrics fm = g.getFontMetrics();
 
-    if (av.antiAlias)
+    if (alignViewport.antiAlias)
     {
-      gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
               RenderingHints.VALUE_ANTIALIAS_ON);
     }
 
     Color currentColor = Color.white;
     Color currentTextColor = Color.black;
 
-    boolean hasHiddenRows = av.hasHiddenRows();
+    boolean hasHiddenRows = alignViewport.hasHiddenRows();
 
-    if (av.getWrapAlignment())
+    if (alignViewport.getWrapAlignment())
     {
-      drawIdsWrapped(starty, hasHiddenRows);
+      drawIdsWrapped(g, alignViewport, startSeq, getHeight());
       return;
     }
 
-    // No need to hang on to labels if we're not wrapped
-    labels = null;
-
     // Now draw the id strings
     int panelWidth = getWidth();
     int xPos = 0;
 
-    SequenceI sequence;
     // Now draw the id strings
-    for (int i = starty; i <= endy; i++)
+    for (int i = startSeq; i <= endSeq; i++)
     {
-      sequence = av.getAlignment().getSequenceAt(i);
+      SequenceI sequence = alignViewport.getAlignment().getSequenceAt(i);
 
       if (sequence == null)
       {
         continue;
       }
 
-      if (hasHiddenRows || av.isDisplayReferenceSeq())
+      if (hasHiddenRows || alignViewport.isDisplayReferenceSeq())
       {
-        setHiddenFont(sequence);
+        g.setFont(getHiddenFont(sequence, alignViewport));
       }
 
       // Selected sequence colours
-      if ((searchResults != null) && searchResults.contains(sequence))
+      if (selection != null && selection.contains(sequence))
       {
         currentColor = Color.black;
         currentTextColor = Color.white;
       }
-      else if ((av.getSelectionGroup() != null) && av.getSelectionGroup()
-              .getSequences(null).contains(sequence))
+      else if ((alignViewport.getSelectionGroup() != null) && alignViewport
+              .getSelectionGroup().getSequences(null).contains(sequence))
       {
         currentColor = Color.lightGray;
         currentTextColor = Color.black;
       }
       else
       {
-        currentColor = av.getSequenceColour(sequence);
+        currentColor = alignViewport.getSequenceColour(sequence);
         currentTextColor = Color.black;
       }
 
-      gg.setColor(currentColor);
+      g.setColor(currentColor);
 
-      gg.fillRect(0, (i - starty) * av.getCharHeight(), getWidth(),
-              av.getCharHeight());
+      int charHeight = alignViewport.getCharHeight();
+      g.fillRect(0, (i - startSeq) * charHeight,
+              getWidth(), charHeight);
 
-      gg.setColor(currentTextColor);
+      g.setColor(currentTextColor);
 
-      String string = sequence.getDisplayId(av.getShowJVSuffix());
+      String string = sequence
+              .getDisplayId(alignViewport.getShowJVSuffix());
 
-      if (av.isRightAlignIds())
+      if (alignViewport.isRightAlignIds())
       {
         xPos = panelWidth - fm.stringWidth(string) - 4;
       }
 
-      gg.drawString(string, xPos,
-              (((i - starty) * av.getCharHeight()) + av.getCharHeight())
-                      - (av.getCharHeight() / 5));
+      g.drawString(string, xPos, (((i - startSeq) * charHeight) + charHeight)
+              - (charHeight / 5));
 
       if (hasHiddenRows)
       {
-        drawMarker(i, starty, 0);
+        drawMarker(g, alignViewport, i, startSeq, 0);
       }
     }
   }
 
   /**
-   * Draws sequence ids in wrapped mode
+   * Draws sequence ids, and annotation labels if annotations are shown, in
+   * wrapped mode
    * 
-   * @param starty
-   * @param hasHiddenRows
+   * @param g
+   * @param alignViewport
+   * @param startSeq
    */
-  protected void drawIdsWrapped(int starty, boolean hasHiddenRows)
+  void drawIdsWrapped(Graphics2D g, AlignViewport alignViewport,
+          int startSeq, int pageHeight)
   {
-    int maxwidth = av.getAlignment().getWidth();
-    int alheight = av.getAlignment().getHeight();
+    int alignmentWidth = alignViewport.getAlignment().getWidth();
+    final int alheight = alignViewport.getAlignment().getHeight();
 
-    if (av.hasHiddenColumns())
+    if (alignViewport.hasHiddenColumns())
     {
-      maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+      alignmentWidth = alignViewport.getAlignment().getHiddenColumns()
+              .absoluteToVisibleColumn(alignmentWidth) - 1;
     }
 
     int annotationHeight = 0;
 
-    if (av.isShowAnnotation())
+    AnnotationLabels labels = null;
+    if (alignViewport.isShowAnnotation())
     {
       if (ap == null)
       {
-        ap = new AnnotationPanel(av);
+        ap = new AnnotationPanel(alignViewport);
       }
-
       annotationHeight = ap.adjustPanelHeight();
-      if (labels == null)
-      {
-        labels = new AnnotationLabels(av);
-      }
+      labels = new AnnotationLabels(alignViewport);
     }
 
-    int hgap = av.getCharHeight();
-    if (av.getScaleAboveWrapped())
+    final int charHeight = alignViewport.getCharHeight();
+    int hgap = charHeight;
+    if (alignViewport.getScaleAboveWrapped())
     {
-      hgap += av.getCharHeight();
+      hgap += charHeight;
     }
 
-    int cHeight = alheight * av.getCharHeight() + hgap + annotationHeight;
+    /*
+     * height of alignment + gap + annotations (if shown)
+     */
+    int cHeight = alheight * charHeight + hgap
+            + annotationHeight;
 
-    ViewportRanges ranges = av.getRanges();
+    ViewportRanges ranges = alignViewport.getRanges();
 
     int rowSize = ranges.getViewportWidth();
 
@@ -409,49 +420,58 @@ public class IdCanvas extends JPanel implements ViewportListenerI
      * draw repeating sequence ids until out of sequence data or
      * out of visible space, whichever comes first
      */
+    boolean hasHiddenRows = alignViewport.hasHiddenRows();
     int ypos = hgap;
-    int row = ranges.getStartRes();
-    while ((ypos <= getHeight()) && (row < maxwidth))
+    int rowStartRes = ranges.getStartRes();
+    while ((ypos <= pageHeight) && (rowStartRes < alignmentWidth))
     {
-      for (int i = starty; i < alheight; i++)
+      for (int i = startSeq; i < alheight; i++)
       {
-        SequenceI s = av.getAlignment().getSequenceAt(i);
-        if (hasHiddenRows || av.isDisplayReferenceSeq())
+        SequenceI s = alignViewport.getAlignment().getSequenceAt(i);
+        if (hasHiddenRows || alignViewport.isDisplayReferenceSeq())
         {
-          setHiddenFont(s);
+          g.setFont(getHiddenFont(s, alignViewport));
         }
         else
         {
-          gg.setFont(getIdfont());
+          g.setFont(getIdfont());
         }
-
-        drawIdString(gg, hasHiddenRows, s, i, 0, ypos);
+        drawIdString(g, hasHiddenRows, s, i, 0, ypos);
       }
 
-      if (labels != null && av.isShowAnnotation())
+      if (labels != null && alignViewport.isShowAnnotation())
       {
-        gg.translate(0, ypos + (alheight * av.getCharHeight()));
-        labels.drawComponent(gg, getWidth());
-        gg.translate(0, -ypos - (alheight * av.getCharHeight()));
+        g.translate(0, ypos + (alheight * charHeight));
+        labels.drawComponent(g, getWidth());
+        g.translate(0, -ypos - (alheight * charHeight));
       }
 
       ypos += cHeight;
-      row += rowSize;
+      rowStartRes += rowSize;
     }
   }
 
-  void drawMarker(int i, int starty, int yoffset)
+  /**
+   * Draws a marker (a blue right-pointing triangle) between sequences to
+   * indicate hidden sequences.
+   * 
+   * @param g
+   * @param alignViewport
+   * @param seqIndex
+   * @param starty
+   * @param yoffset
+   */
+  void drawMarker(Graphics2D g, AlignViewport alignViewport, int seqIndex, int starty, int yoffset)
   {
-
-    SequenceI[] hseqs = av.getAlignment()
+    SequenceI[] hseqs = alignViewport.getAlignment()
             .getHiddenSequences().hiddenSequences;
     // Use this method here instead of calling hiddenSeq adjust
     // 3 times.
     int hSize = hseqs.length;
 
-    int hiddenIndex = i;
-    int lastIndex = i - 1;
-    int nextIndex = i + 1;
+    int hiddenIndex = seqIndex;
+    int lastIndex = seqIndex - 1;
+    int nextIndex = seqIndex + 1;
 
     for (int j = 0; j < hSize; j++)
     {
@@ -472,52 +492,56 @@ public class IdCanvas extends JPanel implements ViewportListenerI
       }
     }
 
+    /*
+     * are we below or above the hidden sequences?
+     */
     boolean below = (hiddenIndex > lastIndex + 1);
     boolean above = (nextIndex > hiddenIndex + 1);
 
-    gg.setColor(Color.blue);
+    g.setColor(Color.blue);
+    int charHeight = av.getCharHeight();
+
+    /*
+     * vertices of the triangle, below or above hidden seqs
+     */
+    int[] xPoints = new int[]
+    { getWidth() - charHeight,
+        getWidth() - charHeight, getWidth() };
+    int yShift = seqIndex - starty;
+
     if (below)
     {
-      gg.fillPolygon(
-              new int[]
-              { getWidth() - av.getCharHeight(),
-                  getWidth() - av.getCharHeight(), getWidth() },
-              new int[]
-              { (i - starty) * av.getCharHeight() + yoffset,
-                  (i - starty) * av.getCharHeight() + yoffset
-                          + av.getCharHeight() / 4,
-                  (i - starty) * av.getCharHeight() + yoffset },
-              3);
+      int[] yPoints = new int[] { yShift * charHeight + yoffset,
+          yShift * charHeight + yoffset + charHeight / 4,
+          yShift * charHeight + yoffset };
+      g.fillPolygon(xPoints, yPoints, 3);
     }
     if (above)
     {
-      gg.fillPolygon(
-              new int[]
-              { getWidth() - av.getCharHeight(),
-                  getWidth() - av.getCharHeight(), getWidth() },
-              new int[]
-              { (i - starty + 1) * av.getCharHeight() + yoffset,
-                  (i - starty + 1) * av.getCharHeight() + yoffset
-                          - av.getCharHeight() / 4,
-                  (i - starty + 1) * av.getCharHeight() + yoffset },
-              3);
-
+      yShift++;
+      int[] yPoints = new int[] { yShift * charHeight + yoffset,
+          yShift * charHeight + yoffset - charHeight / 4,
+          yShift * charHeight + yoffset };
+      g.fillPolygon(xPoints, yPoints, 3);
     }
   }
 
-  void setHiddenFont(SequenceI seq)
+  /**
+   * Answers the standard sequence id font, or a bold font if the sequence is
+   * set as reference or a hidden group representative
+   * 
+   * @param seq
+   * @param alignViewport
+   * @return
+   */
+  private Font getHiddenFont(SequenceI seq, AlignViewport alignViewport)
   {
-    Font bold = new Font(av.getFont().getName(), Font.BOLD,
-            av.getFont().getSize());
-
     if (av.isReferenceSeq(seq) || av.isHiddenRepSequence(seq))
     {
-      gg.setFont(bold);
-    }
-    else
-    {
-      gg.setFont(getIdfont());
+      return new Font(av.getFont().getName(), Font.BOLD,
+              av.getFont().getSize());
     }
+    return getIdfont();
   }
 
   /**
@@ -564,5 +588,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 4ccfb2f..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);
   }
 
   /**
@@ -325,28 +325,25 @@ public class IdPanel extends JPanel
   {
     int seq2 = alignPanel.getSeqPanel().findSeq(e);
     Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
-    // build a new links menu based on the current links + any non-positional
-    // features
+
+    /*
+     *  build a new links menu based on the current links
+     *  and any non-positional features
+     */
     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-    SequenceFeature sfs[] = sq == null ? null : sq.getSequenceFeatures();
-    if (sfs != null)
+    List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
+    for (SequenceFeature sf : features)
     {
-      for (SequenceFeature sf : sfs)
+      if (sf.links != null)
       {
-        if (sf.begin == sf.end && sf.begin == 0)
+        for (String link : sf.links)
         {
-          if (sf.links != null && sf.links.size() > 0)
-          {
-            for (int l = 0, lSize = sf.links.size(); l < lSize; l++)
-            {
-              nlinks.add(sf.links.elementAt(l));
-            }
-          }
+          nlinks.add(link);
         }
       }
     }
 
-    PopupMenu pop = new PopupMenu(alignPanel, sq, nlinks,
+    PopupMenu pop = new PopupMenu(alignPanel, sq, features,
             Preferences.getGroupURLLinks());
     pop.show(this, e.getX(), e.getY());
   }
@@ -511,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..8d62433 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.gui;
 
-import jalview.bin.Cache;
 import jalview.util.MessageManager;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
@@ -42,7 +41,6 @@ import java.util.List;
 import java.util.Vector;
 
 import javax.swing.JButton;
-import javax.swing.JFrame;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
@@ -101,10 +99,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 +368,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 +487,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 +504,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)
   {
@@ -518,54 +516,6 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
     lstners.remove(actionListener);
   }
 
-  public static void main(String args[])
-  {
-    Cache.getDasSourceRegistry();
-    JDatabaseTree jdt = new JDatabaseTree(new jalview.ws.SequenceFetcher());
-    JFrame foo = new JFrame();
-    foo.setLayout(new BorderLayout());
-    foo.add(jdt.getDatabaseSelectorButton(), BorderLayout.CENTER);
-    foo.pack();
-    foo.setVisible(true);
-    int nultimes = 5;
-    final Thread us = Thread.currentThread();
-    jdt.addActionListener(new ActionListener()
-    {
-
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        us.interrupt();
-      }
-    });
-    do
-    {
-      try
-      {
-        Thread.sleep(50);
-      } catch (InterruptedException x)
-      {
-        nultimes--;
-        if (!jdt.hasSelection())
-        {
-          System.out.println("No Selection");
-        }
-        else
-        {
-          System.out.println("Selection: " + jdt.getSelectedItem());
-          int s = 1;
-          for (DbSourceProxy pr : jdt.getSelectedSources())
-          {
-            System.out.println("Source " + s++ + ": " + pr.getDbName()
-                    + " (" + pr.getDbSource() + ") Version "
-                    + pr.getDbVersion() + ". Test:\t" + pr.getTestQuery());
-          }
-          System.out.println("Test queries: " + jdt.getExampleQueries());
-        }
-      }
-    } while (nultimes > 0 && foo.isVisible());
-    foo.setVisible(false);
-  }
 
   @Override
   public void keyPressed(KeyEvent arg0)
@@ -596,4 +546,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 4c2b8b6..9285754 100644 (file)
@@ -32,10 +32,15 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.GraphLine;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.RnaViewerModel;
+import jalview.datamodel.SequenceFeature;
 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;
@@ -47,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;
@@ -59,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;
@@ -74,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;
@@ -82,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;
@@ -114,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;
@@ -215,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)
@@ -882,48 +869,61 @@ public class Jalview2XML
 
       // TODO: omit sequence features from each alignment view's XML dump if we
       // are storing dataset
-      if (jds.getSequenceFeatures() != null)
+      List<jalview.datamodel.SequenceFeature> sfs = jds
+              .getSequenceFeatures();
+      for (SequenceFeature sf : sfs)
       {
-        jalview.datamodel.SequenceFeature[] sf = jds.getSequenceFeatures();
-        int index = 0;
-        while (index < sf.length)
-        {
-          Features features = new Features();
+        Features features = new Features();
 
-          features.setBegin(sf[index].getBegin());
-          features.setEnd(sf[index].getEnd());
-          features.setDescription(sf[index].getDescription());
-          features.setType(sf[index].getType());
-          features.setFeatureGroup(sf[index].getFeatureGroup());
-          features.setScore(sf[index].getScore());
-          if (sf[index].links != null)
+        features.setBegin(sf.getBegin());
+        features.setEnd(sf.getEnd());
+        features.setDescription(sf.getDescription());
+        features.setType(sf.getType());
+        features.setFeatureGroup(sf.getFeatureGroup());
+        features.setScore(sf.getScore());
+        if (sf.links != null)
+        {
+          for (int l = 0; l < sf.links.size(); l++)
           {
-            for (int l = 0; l < sf[index].links.size(); l++)
-            {
-              OtherData keyValue = new OtherData();
-              keyValue.setKey("LINK_" + l);
-              keyValue.setValue(sf[index].links.elementAt(l).toString());
-              features.addOtherData(keyValue);
-            }
+            OtherData keyValue = new OtherData();
+            keyValue.setKey("LINK_" + l);
+            keyValue.setValue(sf.links.elementAt(l).toString());
+            features.addOtherData(keyValue);
           }
-          if (sf[index].otherDetails != null)
+        }
+        if (sf.otherDetails != null)
+        {
+          /*
+           * save feature attributes, which may be simple strings or
+           * map valued (have sub-attributes)
+           */
+          for (Entry<String, Object> entry : sf.otherDetails.entrySet())
           {
-            String key;
-            Iterator<String> keys = sf[index].otherDetails.keySet()
-                    .iterator();
-            while (keys.hasNext())
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            if (value instanceof Map<?, ?>)
             {
-              key = keys.next();
-              OtherData keyValue = new OtherData();
-              keyValue.setKey(key);
-              keyValue.setValue(sf[index].otherDetails.get(key).toString());
-              features.addOtherData(keyValue);
+              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);
             }
           }
-
-          jseq.addFeatures(features);
-          index++;
         }
+
+        jseq.addFeatures(features);
       }
 
       if (jdatasq.getAllPDBEntries() != null)
@@ -1088,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
@@ -1106,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);
 
@@ -1345,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());
@@ -1365,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));
@@ -1378,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)
             {
@@ -1390,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())
         {
@@ -1402,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);
@@ -1421,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]);
@@ -2299,6 +2329,7 @@ public class Jalview2XML
 
       jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
       af = loadJalviewAlign(jprovider);
+      af.setMenusForViewport();
 
     } catch (MalformedURLException e)
     {
@@ -2988,25 +3019,51 @@ public class Jalview2XML
           Features[] features = jseqs[i].getFeatures();
           for (int f = 0; f < features.length; f++)
           {
-            jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature(
-                    features[f].getType(), features[f].getDescription(),
-                    features[f].getStatus(), features[f].getBegin(),
-                    features[f].getEnd(), features[f].getFeatureGroup());
+            SequenceFeature sf = new SequenceFeature(features[f].getType(),
+                    features[f].getDescription(), features[f].getBegin(),
+                    features[f].getEnd(), features[f].getScore(),
+                    features[f].getFeatureGroup());
+            sf.setStatus(features[f].getStatus());
 
-            sf.setScore(features[f].getScore());
+            /*
+             * 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);
           }
@@ -4254,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
@@ -4581,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()
@@ -4595,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());
@@ -4627,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<>();
@@ -4660,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)
@@ -5346,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();
@@ -5387,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);
@@ -5643,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 e94ee0e..331e738 100755 (executable)
@@ -36,6 +36,7 @@ import jalview.binding.Tree;
 import jalview.binding.UserColours;
 import jalview.binding.Viewport;
 import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceFeature;
 import jalview.io.FileFormat;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
@@ -226,11 +227,10 @@ public class Jalview2XML_V1
         Features[] features = JSEQ[i].getFeatures();
         for (int f = 0; f < features.length; f++)
         {
-          jalview.datamodel.SequenceFeature sf = new jalview.datamodel.SequenceFeature(
-                  features[f].getType(), features[f].getDescription(),
-                  features[f].getStatus(), features[f].getBegin(),
+          SequenceFeature sf = new SequenceFeature(features[f].getType(),
+                  features[f].getDescription(), features[f].getBegin(),
                   features[f].getEnd(), null);
-
+          sf.setStatus(features[f].getStatus());
           al.getSequenceAt(i).getDatasetSequence().addSequenceFeature(sf);
         }
       }
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 2ef71cc..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());
@@ -1946,8 +2030,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       if (start <= end)
       {
         seqs.add(sg.getSequenceAt(i).getDatasetSequence());
-        features.add(
-                new SequenceFeature(null, null, null, start, end, null));
+        features.add(new SequenceFeature(null, null, start, end, null));
       }
     }
 
@@ -1960,7 +2043,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
               .amendFeatures(seqs, features, true, ap))
       {
         ap.alignFrame.setShowSeqFeatures(true);
-        ap.highlightSearchResults(null);
+        ap.av.setSearchResults(null); // clear highlighting
+        ap.repaint(); // draw new/amended features
       }
     }
   }
index 6635dbe..7d02fac 100755 (executable)
@@ -168,8 +168,6 @@ public class Preferences extends GPreferences
 
   JInternalFrame frame;
 
-  DasSourceBrowser dasSource;
-
   private WsPreferences wsPrefs;
 
   private OptionsParam promptEachTimeOpt = new OptionsParam(
@@ -190,8 +188,6 @@ public class Preferences extends GPreferences
     super();
     frame = new JInternalFrame();
     frame.setContentPane(this);
-    dasSource = new DasSourceBrowser();
-    dasTab.add(dasSource, BorderLayout.CENTER);
     wsPrefs = new WsPreferences();
     wsTab.add(wsPrefs, BorderLayout.CENTER);
     int width = 500, height = 450;
@@ -525,8 +521,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 +780,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());
 
     /*
@@ -795,7 +793,6 @@ public class Preferences extends GPreferences
     Cache.applicationProperties.setProperty("PAD_GAPS",
             Boolean.toString(padGaps.isSelected()));
 
-    dasSource.saveProperties(Cache.applicationProperties);
     wsPrefs.updateAndRefreshWsMenuConfig(false);
     Cache.saveProperties();
     Desktop.instance.doConfigureStructurePrefs();
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 ef2392c..6abee08 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();
   }
 
@@ -283,7 +284,7 @@ public class ScalePanel extends JPanel
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     if (res >= av.getAlignment().getWidth())
@@ -299,7 +300,7 @@ public class ScalePanel extends JPanel
       }
       else
       {
-        ap.paintAlignment(false);
+        ap.paintAlignment(false, false);
       }
       return;
     }
@@ -318,7 +319,7 @@ public class ScalePanel extends JPanel
       }
     }
     stretchingGroup = false;
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -338,7 +339,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);
@@ -348,7 +349,7 @@ public class ScalePanel extends JPanel
     {
       stretchingGroup = true;
       cs.stretchGroup(res, sg, min, max);
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -394,7 +395,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(
@@ -411,6 +412,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
@@ -459,7 +462,7 @@ public class ScalePanel extends JPanel
         {
           if (hidden.isVisible(sel))
           {
-            sel = hidden.findColumnPosition(sel);
+            sel = hidden.absoluteToVisibleColumn(sel);
           }
           else
           {
@@ -489,23 +492,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);
         }
       }
     }
@@ -551,10 +549,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 c0dd1b0..5c404f0 100755 (executable)
@@ -25,12 +25,13 @@ 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;
 
-import java.awt.AlphaComposite;
 import java.awt.BasicStroke;
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -41,56 +42,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
 {
-  final FeatureRenderer fr;
+  private static final String ZEROS = "0000000000";
 
-  final SequenceRenderer seqRdr;
+  final FeatureRenderer fr;
 
   BufferedImage img;
 
-  Graphics2D gg;
-
   AlignViewport av;
 
-  boolean fastPaint = false;
+  int cursorX = 0;
+
+  int cursorY = 0;
 
-  int LABEL_WEST;
+  private final SequenceRenderer seqRdr;
 
-  int LABEL_EAST;
+  private boolean fastPaint = false;
 
-  int cursorX = 0;
+  private boolean fastpainting = false;
 
-  int cursorY = 0;
+  private AnnotationPanel annotations;
 
-  int charHeight = 0;
+  /*
+   * measurements for drawing a wrapped alignment
+   */
+  private int labelWidthEast; // label right width in pixels if shown
+
+  private int labelWidthWest; // label left width in pixels if shown
+
+  private int wrappedSpaceAboveAlignment; // gap between widths
 
-  int charWidth = 0;
+  private int wrappedRepeatHeightPx; // height in pixels of wrapped width
 
-  boolean fastpainting = false;
+  private int wrappedVisibleWidths; // number of wrapped widths displayed
 
-  AnnotationPanel annotations;
+  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());
@@ -110,29 +117,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)
@@ -147,137 +161,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 = LABEL_WEST - 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)
   {
@@ -287,123 +283,121 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     }
     fastpainting = true;
     fastPaint = true;
-    updateViewport();
-
-    ViewportRanges ranges = av.getRanges();
-    int sr = ranges.getStartRes();
-    int er = ranges.getEndRes();
-    int ss = ranges.getStartSeq();
-    int es = 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 = (er - sr - horizontal) * charWidth;
-      sr = er - horizontal;
-    }
-    else if (horizontal < 0)
-    {
-      er = sr - horizontal;
-    }
-    else if (vertical > 0) // scroll down
+    try
     {
-      ss = es - 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 (ss < ranges.getStartSeq())
-      { // ie scrolling too fast, more than a page at a time
-        ss = 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)
-    {
-      es = ss - vertical;
 
-      if (es > ranges.getEndSeq())
+      if (vertical > 0) // scroll down
       {
-        es = ranges.getEndSeq();
+        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 = startSeq - vertical;
 
-    gg.translate(transX, transY);
-    drawPanel(gg, sr, er, ss, es, 0);
-    gg.translate(-transX, -transY);
+        if (endSeq > ranges.getEndSeq())
+        {
+          endSeq = ranges.getEndSeq();
+        }
+      }
 
-    repaint();
-    fastpainting = false;
-  }
+      gg.translate(transX, transY);
+      drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
+      gg.translate(-transX, -transY);
 
-  /**
-   * Definitions of startx and endx (hopefully): SMJS This is what I'm working
-   * towards! startx is the first residue (starting at 0) to display. endx is
-   * the last residue to display (starting at 0). starty is the first sequence
-   * to display (starting at 0). endy is the last sequence to display (starting
-   * at 0). NOTE 1: The av limits are set in setFont in this class and in the
-   * adjustment listener in SeqPanel when the scrollbars move.
-   */
+      // 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;
+    }
+  }
 
-  // Set this to false to force a full panel paint
   @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)))
     {
-      BufferedImage lcimg = buildLocalImage(selectImage);
-      g.drawImage(lcimg, 0, 0, this);
+      g.drawImage(img, 0, 0, this);
+
+      drawSelectionGroup((Graphics2D) g, ranges.getStartRes(),
+              ranges.getEndRes(), ranges.getStartSeq(), ranges.getEndSeq());
+
       fastPaint = false;
     }
-    else if ((width > 0) && (height > 0))
+    else if (width > 0 && height > 0)
     {
-      // img is a cached version of the last view we drew, if any
-      // if we have no img or the size has changed, make a new one
+      /*
+       * img is a cached version of the last view we drew, if any
+       * if we have no img or the size has changed, make a new one
+       */
       if (img == null || width != img.getWidth()
               || height != img.getHeight())
       {
-        img = setupImage();
-        if (img == null)
-        {
-          return;
-        }
+        img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
         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());
@@ -414,12 +408,19 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
                 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);
+      drawSelectionGroup(gg, ranges.getStartRes(),
+              ranges.getEndRes(), ranges.getStartSeq(), ranges.getEndSeq());
+
+      g.drawImage(img, 0, 0, this);
     }
-  }
 
+    if (av.cursorMode)
+    {
+      drawCursor(g, ranges.getStartRes(), ranges.getEndRes(),
+              ranges.getStartSeq(), ranges.getEndSeq());
+    }
+  }
+  
   /**
    * Draw an alignment panel for printing
    * 
@@ -439,14 +440,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   {
     drawPanel(g1, startRes, endRes, startSeq, endSeq, 0);
 
-    BufferedImage selectImage = drawSelectionGroup(startRes, endRes,
+    drawSelectionGroup((Graphics2D) g1, startRes, endRes,
             startSeq, endSeq);
-    if (selectImage != null)
-    {
-      ((Graphics2D) g1).setComposite(AlphaComposite
-              .getInstance(AlphaComposite.SRC_OVER));
-      g1.drawImage(selectImage, 0, 0, this);
-    }
   }
 
   /**
@@ -464,299 +459,375 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   public void drawWrappedPanelForPrinting(Graphics g, int canvasWidth,
           int canvasHeight, int startRes)
   {
-    SequenceGroup group = av.getSelectionGroup();
-
     drawWrappedPanel(g, canvasWidth, canvasHeight, startRes);
 
+    SequenceGroup group = av.getSelectionGroup();
     if (group != null)
     {
-      BufferedImage selectImage = null;
-      try
-      {
-        selectImage = new BufferedImage(canvasWidth, canvasHeight,
-                BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
-      } catch (OutOfMemoryError er)
-      {
-        System.gc();
-        System.err.println("Print image OutOfMemory Error.\n" + er);
-        new OOMWarning("Creating wrapped alignment image for printing", er);
-      }
-      if (selectImage != null)
-      {
-        Graphics2D g2 = selectImage.createGraphics();
-        setupSelectionGroup(g2, selectImage);
-        drawWrappedSelection(g2, group, canvasWidth, canvasHeight,
+      drawWrappedSelection((Graphics2D) g, group, canvasWidth, canvasHeight,
                 startRes);
-
-        g2.setComposite(
-                AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
-        g.drawImage(selectImage, 0, 0, this);
-        g2.dispose();
-      }
     }
   }
 
-  /*
-   * Make a local image by combining the cached image img
-   * with any selection
+  /**
+   * Returns the visible width of the canvas in residues, after allowing for
+   * East or West scales (if shown)
+   * 
+   * @param canvasWidth
+   *          the width in pixels (possibly including scales)
+   * 
+   * @return
    */
-  private BufferedImage buildLocalImage(BufferedImage selectImage)
+  public int getWrappedCanvasWidth(int canvasWidth)
   {
-    // clone the cached image
-    BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
-            img.getType());
-    Graphics2D g2d = lcimg.createGraphics();
-    g2d.drawImage(img, 0, 0, null);
+    int charWidth = av.getCharWidth();
+
+    FontMetrics fm = getFontMetrics(av.getFont());
 
-    // overlay selection group on lcimg
-    if (selectImage != null)
+    int labelWidth = 0;
+    
+    if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
     {
-      g2d.setComposite(
-              AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
-      g2d.drawImage(selectImage, 0, 0, this);
+      labelWidth = getLabelWidth(fm);
     }
-    g2d.dispose();
 
-    return lcimg;
+    labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
+
+    labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
+
+    return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
   }
 
-  /*
-   * Set up a buffered image of the correct height and size for the sequence canvas
+  /**
+   * 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
    */
-  private BufferedImage setupImage()
+  protected int getLabelWidth(FontMetrics fm)
   {
-    BufferedImage lcimg = null;
-
-    int width = getWidth();
-    int height = getHeight();
-
-    width -= (width % charWidth);
-    height -= (height % charHeight);
-
-    if ((width < 1) || (height < 1))
+    /*
+     * find the biggest sequence end position we need to show
+     * (note this is not necessarily the sequence length)
+     */
+    int maxWidth = 0;
+    AlignmentI alignment = av.getAlignment();
+    for (int i = 0; i < alignment.getHeight(); i++)
     {
-      return null;
+      maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
     }
 
-    try
-    {
-      lcimg = new BufferedImage(width, height,
-              BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
-    } catch (OutOfMemoryError er)
+    int length = 0;
+    for (int i = maxWidth; i > 0; i /= 10)
     {
-      System.gc();
-      System.err.println(
-              "Group image OutOfMemory Redraw Error.\n" + er);
-      new OOMWarning("Creating alignment image for display", er);
-
-      return null;
+      length++;
     }
 
-    return lcimg;
+    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 cwidth
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
+   * @param g
+   * @param canvasWidth
+   *          available width in pixels
+   * @param canvasHeight
+   *          available height in pixels
+   * @param startColumn
+   *          the first column (0...) of the alignment to draw
    */
-  public int getWrappedCanvasWidth(int cwidth)
+  public void drawWrappedPanel(Graphics g, int canvasWidth,
+          int canvasHeight, final int startColumn)
   {
-    FontMetrics fm = getFontMetrics(av.getFont());
-
-    LABEL_EAST = 0;
-    LABEL_WEST = 0;
+    int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
 
-    if (av.getScaleRightWrapped())
-    {
-      LABEL_EAST = fm.stringWidth(getMask());
-    }
+    av.setWrappedWidth(wrappedWidthInResidues);
 
-    if (av.getScaleLeftWrapped())
+    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 (excluding 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))
     {
-      LABEL_WEST = fm.stringWidth(getMask());
+      int endColumn = Math
+              .min(maxWidth, start + wrappedWidthInResidues - 1);
+      drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
+      ypos += wrappedRepeatHeightPx;
+      start += wrappedWidthInResidues;
+      currentWidth++;
     }
 
-    return (cwidth - LABEL_EAST - LABEL_WEST) / charWidth;
+    drawWrappedDecorators(g, startColumn);
   }
 
   /**
-   * Generates a string of zeroes.
+   * 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>
    * 
-   * @return String
+   * @param canvasWidth
+   * @param canvasHeight
+   * @return the number of residue columns in each width
    */
-  String getMask()
+  protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
   {
-    String mask = "00";
-    int maxWidth = 0;
-    int tmp;
-    for (int i = 0; i < av.getAlignment().getHeight(); i++)
+    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.getAlignment().getHeight()
+            * 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))
     {
-      tmp = av.getAlignment().getSequenceAt(i).getEnd();
-      if (tmp > maxWidth)
-      {
-        maxWidth = tmp;
-      }
+      wrappedVisibleWidths++;
     }
 
-    for (int i = maxWidth; i > 0; i /= 10)
+    /*
+     * compute width in residues; this also sets East and West label widths
+     */
+    int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth);
+
+    /*
+     *  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)
     {
-      mask += "0";
+      maxWidths++;
     }
-    return mask;
+    wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
+
+    return wrappedWidthInResidues;
   }
 
   /**
-   * DOCUMENT ME!
+   * Draws one width of a wrapped alignment, including sequences and
+   * annnotations, if shown, but not scales or hidden column markers
    * 
    * @param g
-   *          DOCUMENT ME!
-   * @param canvasWidth
-   *          DOCUMENT ME!
+   * @param ypos
+   * @param startColumn
+   * @param endColumn
    * @param canvasHeight
-   *          DOCUMENT ME!
-   * @param startRes
-   *          DOCUMENT ME!
    */
-  private void drawWrappedPanel(Graphics g, int canvasWidth,
-          int canvasHeight, int startRes)
+  protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
+          int endColumn, int canvasHeight)
   {
-    updateViewport();
-    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())
+    ViewportRanges ranges = av.getRanges();
+    int viewportWidth = ranges.getViewportWidth();
+
+    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)
     {
-      LABEL_WEST = fm.stringWidth(getMask());
+      g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
     }
-
-    int hgap = charHeight;
-    if (av.getScaleAboveWrapped())
+    else
     {
-      hgap += charHeight;
+      g.setClip(0, (int) clip.getBounds().getY(),
+              viewportWidth * charWidth, (int) clip.getBounds().getHeight());
     }
 
-    int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
-    int cHeight = av.getAlignment().getHeight() * charHeight;
-
-    av.setWrappedWidth(cWidth);
+    /*
+     * 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);
 
-    av.getRanges().setViewportStartAndWidth(startRes, cWidth);
+    drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
+            ypos);
 
-    int endx;
-    int ypos = hgap;
-    int maxwidth = av.getAlignment().getWidth();
+    int cHeight = av.getAlignment().getHeight() * av.getCharHeight();
 
-    if (av.hasHiddenColumns())
+    if (av.isShowAnnotation())
     {
-      maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth);
+      g.translate(0, cHeight + ypos + 3);
+      if (annotations == null)
+      {
+        annotations = new AnnotationPanel(av);
+      }
+
+      annotations.renderer.drawComponent(annotations, av, g, -1,
+              startColumn, endx + 1);
+      g.translate(0, -cHeight - ypos - 3);
     }
+    g.setClip(clip);
+    g.translate(-xOffset, 0);
+  }
 
-    while ((ypos <= canvasHeight) && (startRes < maxwidth))
-    {
-      endx = startRes + cWidth - 1;
+  /**
+   * 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();
 
-      if (endx > maxwidth)
-      {
-        endx = maxwidth;
-      }
+    g.setFont(av.getFont());
+    g.setColor(Color.black);
 
-      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 - LABEL_EAST, 0);
-        drawEastScale(g, startRes, endx, ypos);
-        g.translate(-(canvasWidth - LABEL_EAST), 0);
+        int x = labelWidthWest + viewportWidth * charWidth;
+        g.translate(x, 0);
+        drawVerticalScale(g, startCol, endColumn, ypos, false);
+        g.translate(-x, 0);
       }
 
-      g.translate(LABEL_WEST, 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(-LABEL_WEST, 0);
-
-      ypos += cHeight + getAnnotationHeight() + 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);
     }
   }
 
@@ -767,6 +838,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())
@@ -774,7 +848,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       hgap += charHeight;
     }
 
-    int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / charWidth;
+    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
+            / charWidth;
     int cHeight = av.getAlignment().getHeight() * charHeight;
 
     int startx = startRes;
@@ -785,11 +860,15 @@ 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
     // each block as if it were a block from an unwrapped alignment
+    g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
+            BasicStroke.JOIN_ROUND, 3f, new float[]
+            { 5f, 3f }, 0f));
+    g.setColor(Color.RED);
     while ((ypos <= canvasHeight) && (startx < maxwidth))
     {
       // set end value to be start + width, or maxwidth, whichever is smaller
@@ -800,13 +879,13 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
         endx = maxwidth;
       }
 
-      g.translate(LABEL_WEST, 0);
+      g.translate(labelWidthWest, 0);
 
       drawUnwrappedSelection(g, group, startx, endx, 0,
               av.getAlignment().getHeight() - 1,
               ypos);
 
-      g.translate(-LABEL_WEST, 0);
+      g.translate(-labelWidthWest, 0);
 
       // update vertical offset
       ypos += cHeight + getAnnotationHeight() + hgap;
@@ -814,6 +893,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       // update horizontal offset
       startx += cWidth;
     }
+    g.setStroke(new BasicStroke());
   }
 
   int getAnnotationHeight()
@@ -831,90 +911,101 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     return annotations.adjustPanelHeight();
   }
 
-  /*
-   * Draw an alignment panel for printing
+  /**
+   * Draws the visible region of the alignment on the graphics context. If there
+   * are hidden column markers in the visible region, then each sub-region
+   * between the markers is drawn separately, followed by the hidden column
+   * marker.
    * 
    * @param g1
-   *          Graphics object to draw with
+   *          the graphics context, positioned at the first residue to be drawn
    * @param startRes
-   *          start residue of print area
+   *          offset of the first column to draw (0..)
    * @param endRes
-   *          end residue of print area
+   *          offset of the last column to draw (0..)
    * @param startSeq
-   *          start sequence of print area
+   *          offset of the first sequence to draw (0..)
    * @param endSeq
-   *          end sequence of print area
-   * @param offset
-   *          vertical offset
+   *          offset of the last sequence to draw (0..)
+   * @param yOffset
+   *          vertical offset at which to draw (for wrapped alignments)
    */
-  private void drawPanel(Graphics g1, int startRes, int endRes,
-          int startSeq, int endSeq, int offset)
+  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, offset);
+      draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
     }
     else
     {
       int screenY = 0;
-      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];
-
-        if (hideStart <= blockStart)
-        {
-          blockStart += (hideEnd - hideStart) + 1;
-          continue;
-        }
-
-        blockEnd = hideStart - 1;
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      VisibleContigsIterator regions = hidden
+              .getVisContigsIterator(startRes, endRes + 1, true);
 
+      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
+         */
         g1.translate(screenY * charWidth, 0);
 
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
+        draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
 
-        if (av.getShowHiddenMarkers())
+        /*
+         * 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) * charWidth - 1,
-                  0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
-                  (endSeq - startSeq + 1) * charHeight + offset);
+                  0 + yOffset, (blockEnd - blockStart + 1) * charWidth - 1,
+                  (endSeq - startSeq + 1) * charHeight + yOffset);
         }
 
         g1.translate(-screenY * charWidth, 0);
         screenY += blockEnd - blockStart + 1;
-        blockStart = hideEnd + 1;
-
-        if (screenY > (endRes - startRes))
-        {
-          // already rendered last block
-          return;
-        }
-      }
-
-      if (screenY <= (endRes - startRes))
-      {
-        // remaining visible region to render
-        blockEnd = blockStart + (endRes - startRes) - screenY;
-        g1.translate(screenY * charWidth, 0);
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
-
-        g1.translate(-screenY * charWidth, 0);
-      }
-    }
+      }
+    }
 
   }
 
+  /**
+   * Draws a region of the visible alignment
+   * 
+   * @param g1
+   * @param startRes
+   *          offset of the first column in the visible region (0..)
+   * @param endRes
+   *          offset of the last column in the visible region (0..)
+   * @param startSeq
+   *          offset of the first sequence in the visible region (0..)
+   * @param endSeq
+   *          offset of the last sequence in the visible region (0..)
+   * @param yOffset
+   *          vertical offset at which to draw (for wrapped alignments)
+   */
   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());
 
@@ -940,30 +1031,25 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
                 offset + ((i - startSeq) * charHeight), false);
       }
 
-      // / Highlight search Results once all sequences have been drawn
-      // ////////////////////////////////////////////////////////
+      /*
+       * highlight search Results once sequence has been drawn
+       */
       if (av.hasSearchResults())
       {
-        int[] visibleResults = av.getSearchResults().getResults(nextSeq,
-                startRes, endRes);
+        SearchResultsI searchResults = av.getSearchResults();
+        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
@@ -974,14 +1060,21 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
   }
 
+  /**
+   * Draws the outlines of any groups defined on the alignment (excluding the
+   * current selection group, if any)
+   * 
+   * @param g1
+   * @param startRes
+   * @param endRes
+   * @param startSeq
+   * @param endSeq
+   * @param offset
+   */
   void drawGroupsBoundaries(Graphics g1, int startRes, int endRes,
           int startSeq, int endSeq, int offset)
   {
     Graphics2D g = (Graphics2D) g1;
-    //
-    // ///////////////////////////////////
-    // Now outline any areas if necessary
-    // ///////////////////////////////////
 
     SequenceGroup group = null;
     int groupIndex = -1;
@@ -994,18 +1087,15 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
     if (group != null)
     {
-      g.setStroke(new BasicStroke());
-      g.setColor(group.getOutlineColour());
-      
       do
       {
+        g.setColor(group.getOutlineColour());
+
         drawPartialGroupOutline(g, group, startRes, endRes, startSeq,
                 endSeq, offset);
 
         groupIndex++;
 
-        g.setStroke(new BasicStroke());
-
         if (groupIndex >= av.getAlignment().getGroups().size())
         {
           break;
@@ -1019,33 +1109,28 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
   }
 
-
-  /*
-   * Draw the selection group as a separate image and overlay
+  /**
+   * Draws the outline of the current selection group (if any)
+   * 
+   * @param g
+   * @param startRes
+   * @param endRes
+   * @param startSeq
+   * @param endSeq
    */
-  private BufferedImage drawSelectionGroup(int startRes, int endRes,
+  private void drawSelectionGroup(Graphics2D g, int startRes, int endRes,
           int startSeq, int endSeq)
   {
-    // get a new image of the correct size
-    BufferedImage selectionImage = setupImage();
-
-    if (selectionImage == null)
-    {
-      return null;
-    }
-
     SequenceGroup group = av.getSelectionGroup();
     if (group == null)
     {
-      // nothing to draw
-      return null;
+      return;
     }
 
-    // set up drawing colour
-    Graphics2D g = (Graphics2D) selectionImage.getGraphics();
-
-    setupSelectionGroup(g, selectionImage);
-
+    g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
+            BasicStroke.JOIN_ROUND, 3f, new float[]
+            { 5f, 3f }, 0f));
+    g.setColor(Color.RED);
     if (!av.getWrapAlignment())
     {
       drawUnwrappedSelection(g, group, startRes, endRes, startSeq, endSeq,
@@ -1056,42 +1141,120 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       drawWrappedSelection(g, group, getWidth(), getHeight(),
               av.getRanges().getStartRes());
     }
-
-    g.dispose();
-    return selectionImage;
+    g.setStroke(new BasicStroke());
   }
 
-  /*
-   * Set up graphics for selection group
+  /**
+   * 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 setupSelectionGroup(Graphics2D g,
-          BufferedImage selectionImage)
+  private void drawCursor(Graphics g, int startRes, int endRes,
+          int startSeq,
+          int endSeq)
   {
-    // set background to transparent
-    g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
-    g.fillRect(0, 0, selectionImage.getWidth(), selectionImage.getHeight());
+    // convert the cursorY into a position on the visible alignment
+    int cursor_ypos = cursorY;
 
-    // set up foreground to draw red dashed line
-    g.setComposite(AlphaComposite.Src);
-    g.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
-            BasicStroke.JOIN_ROUND, 3f, new float[]
-    { 5f, 3f }, 0f));
-    g.setColor(Color.RED);
+    // 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());
+        }
+      }
+    }
   }
 
-  /*
+
+  /**
    * Draw a selection group over an unwrapped alignment
-   * @param g graphics object to draw with
-   * @param group selection group
-   * @param startRes start residue of area to draw
-   * @param endRes end residue of area to draw
-   * @param startSeq start sequence of area to draw
-   * @param endSeq end sequence of area to draw
-   * @param offset vertical offset (used when called from wrapped alignment code)
+   * 
+   * @param g
+   *          graphics object to draw with
+   * @param group
+   *          selection group
+   * @param startRes
+   *          start residue of area to draw
+   * @param endRes
+   *          end residue of area to draw
+   * @param startSeq
+   *          start sequence of area to draw
+   * @param endSeq
+   *          end sequence of area to draw
+   * @param offset
+   *          vertical offset (used when called from wrapped alignment code)
    */
   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,
@@ -1101,22 +1264,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,
@@ -1124,35 +1282,27 @@ 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);
       }
     }
   }
 
-  /*
-   * Draw the selection group as a separate image and overlay
+  /**
+   * Draws part of a selection group outline
+   * 
+   * @param g
+   * @param group
+   * @param startRes
+   * @param endRes
+   * @param startSeq
+   * @param endSeq
+   * @param verticalOffset
    */
   private void drawPartialGroupOutline(Graphics2D g, SequenceGroup group,
           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;
@@ -1160,156 +1310,305 @@ 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)
-          {
-            g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
-          }
-
-          if (sx < 0)
-          {
-            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)
+          if ((bottom == -1)
+                  && !seqs.contains(av.getAlignment().getSequenceAt(i + 1)))
           {
-            g.drawLine(sx, top, sx + xwidth, top);
-            top = -1;
+            bottom = sy + charHeight;
           }
 
-          // draw horizontal line at bottom of group
-          if (bottom != -1)
+          if (!inGroup)
           {
-            g.drawLine(sx, bottom, sx + xwidth, bottom);
-            bottom = -1;
+            if (((top == -1) && (i == 0)) || !seqs
+                    .contains(av.getAlignment().getSequenceAt(i - 1)))
+            {
+              top = sy;
+            }
+
+            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);
+        drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
+        drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
+      }
     }
+  }
 
-    if (inGroup)
+  /**
+   * 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)
     {
-      sy = verticalOffset + ((i - startSeq) * charHeight);
-      if (sx >= 0 && sx < visWidth)
-      {
-        g.drawLine(sx, oldY, sx, sy);
-      }
+      width += startx;
+      startx = 0;
+    }
 
-      if (sx + xwidth < visWidth)
-      {
-        g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
-      }
+    // don't let width extend beyond current block, or group extent
+    // fixes JAL-2672
+    if (startx + width >= visWidth)
+    {
+      width = visWidth - startx;
+    }
 
-      if (sx < 0)
-      {
-        xwidth += sx;
-        sx = 0;
-      }
+    if (top != -1)
+    {
+      g.drawLine(startx, top, startx + width, top);
+    }
+
+    if (bottom != -1)
+    {
+      g.drawLine(startx, bottom - 1, startx + width, 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);
+    }
 
-      if (sx + xwidth > visWidth)
+    // if end position is visible, draw vertical line to right of
+    // group
+    if (sx + xwidth < visWidth)
+    {
+      g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
+    }
+  }
+  
+  /**
+   * Highlights search results in the visible region by rendering as white text
+   * on a black background. Any previous highlighting is removed. Answers true
+   * if any highlight was left on the visible alignment (so status bar should be
+   * set to match), else false.
+   * <p>
+   * Currently fastPaint is not implemented for wrapped alignments. If a wrapped
+   * alignment had to be scrolled to show the highlighted region, then it should
+   * be fully redrawn, otherwise a fast paint can be performed. This argument
+   * could be removed if fast paint of scrolled wrapped alignment is coded in
+   * future (JAL-2609).
+   * 
+   * @param results
+   * @param noFastPaint
+   * @return
+   */
+  public boolean highlightSearchResults(SearchResultsI results,
+          boolean noFastPaint)
+  {
+    if (fastpainting)
+    {
+      return false;
+    }
+    boolean wrapped = av.getWrapAlignment();
+    try
+    {
+      fastPaint = !noFastPaint;
+      fastpainting = fastPaint;
+
+      /*
+       * to avoid redrawing the whole visible region, we instead
+       * redraw just the minimal regions to remove previous highlights
+       * and add new ones
+       */
+      SearchResultsI previous = av.getSearchResults();
+      av.setSearchResults(results);
+      boolean redrawn = false;
+      boolean drawn = false;
+      if (wrapped)
       {
-        xwidth = visWidth;
+        redrawn = drawMappedPositionsWrapped(previous);
+        drawn = drawMappedPositionsWrapped(results);
+        redrawn |= drawn;
       }
-      else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
+      else
       {
-        xwidth = (endRes - startRes + 1) * charWidth;
+        redrawn = drawMappedPositions(previous);
+        drawn = drawMappedPositions(results);
+        redrawn |= drawn;
       }
 
-      if (top != -1)
+      /*
+       * if highlights were either removed or added, repaint
+       */
+      if (redrawn)
       {
-        g.drawLine(sx, top, sx + xwidth, top);
-        top = -1;
+        repaint();
       }
 
-      if (bottom != -1)
-      {
-        g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
-        bottom = -1;
-      }
+      /*
+       * return true only if highlights were added
+       */
+      return drawn;
 
-      inGroup = false;
+    } finally
+    {
+      fastpainting = false;
     }
   }
-  
+
   /**
-   * DOCUMENT ME!
+   * Redraws the minimal rectangle in the visible region (if any) that includes
+   * mapped positions of the given search results. Whether or not positions are
+   * highlighted depends on the SearchResults set on the Viewport. This allows
+   * this method to be called to either clear or set highlighting. Answers true
+   * if any positions were drawn (in which case a repaint is still required),
+   * else false.
    * 
    * @param results
-   *          DOCUMENT ME!
+   * @return
    */
-  public void highlightSearchResults(SearchResultsI results)
+  protected boolean drawMappedPositions(SearchResultsI results)
   {
-    img = null;
+    if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
+    {
+      return false;
+    }
+
+    /*
+     * calculate the minimal rectangle to redraw that 
+     * includes both new and existing search results
+     */
+    int firstSeq = Integer.MAX_VALUE;
+    int lastSeq = -1;
+    int firstCol = Integer.MAX_VALUE;
+    int lastCol = -1;
+    boolean matchFound = false;
+
+    ViewportRanges ranges = av.getRanges();
+    int firstVisibleColumn = ranges.getStartRes();
+    int lastVisibleColumn = ranges.getEndRes();
+    AlignmentI alignment = av.getAlignment();
+    if (av.hasHiddenColumns())
+    {
+      firstVisibleColumn = alignment.getHiddenColumns()
+              .visibleToAbsoluteColumn(firstVisibleColumn);
+      lastVisibleColumn = alignment.getHiddenColumns()
+              .visibleToAbsoluteColumn(lastVisibleColumn);
+    }
+
+    for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+            .getEndSeq(); seqNo++)
+    {
+      SequenceI seq = alignment.getSequenceAt(seqNo);
+
+      int[] visibleResults = results.getResults(seq, firstVisibleColumn,
+              lastVisibleColumn);
+      if (visibleResults != null)
+      {
+        for (int i = 0; i < visibleResults.length - 1; i += 2)
+        {
+          int firstMatchedColumn = visibleResults[i];
+          int lastMatchedColumn = visibleResults[i + 1];
+          if (firstMatchedColumn <= lastVisibleColumn
+                  && lastMatchedColumn >= firstVisibleColumn)
+          {
+            /*
+             * found a search results match in the visible region - 
+             * remember the first and last sequence matched, and the first
+             * and last visible columns in the matched positions
+             */
+            matchFound = true;
+            firstSeq = Math.min(firstSeq, seqNo);
+            lastSeq = Math.max(lastSeq, seqNo);
+            firstMatchedColumn = Math.max(firstMatchedColumn,
+                    firstVisibleColumn);
+            lastMatchedColumn = Math.min(lastMatchedColumn,
+                    lastVisibleColumn);
+            firstCol = Math.min(firstCol, firstMatchedColumn);
+            lastCol = Math.max(lastCol, lastMatchedColumn);
+          }
+        }
+      }
+    }
 
-    av.setSearchResults(results);
+    if (matchFound)
+    {
+      if (av.hasHiddenColumns())
+      {
+        firstCol = alignment.getHiddenColumns()
+                .absoluteToVisibleColumn(firstCol);
+        lastCol = alignment.getHiddenColumns().absoluteToVisibleColumn(lastCol);
+      }
+      int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
+      int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
+      gg.translate(transX, transY);
+      drawPanel(gg, firstCol, lastCol, firstSeq, lastSeq, 0);
+      gg.translate(-transX, -transY);
+    }
 
-    repaint();
+    return matchFound;
   }
 
   @Override
@@ -1321,34 +1620,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
@@ -1356,14 +1663,423 @@ 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
+   * this method to be called either to clear highlights (passing the previous
+   * search results), or to draw new highlights.
+   * 
+   * @param results
+   * @return
+   */
+  protected boolean drawMappedPositionsWrapped(SearchResultsI results)
+  {
+    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 = wrappedRepeatHeightPx;
+
+    ViewportRanges ranges = av.getRanges();
+    int canvasHeight = getHeight();
+    int repeats = canvasHeight / wrappedHeight;
+    if (canvasHeight / wrappedHeight > 0)
+    {
+      repeats++;
+    }
+
+    int firstVisibleColumn = ranges.getStartRes();
+    int lastVisibleColumn = ranges.getStartRes() + repeats
+            * ranges.getViewportWidth() - 1;
+
+    AlignmentI alignment = av.getAlignment();
+    if (av.hasHiddenColumns())
+    {
+      firstVisibleColumn = alignment.getHiddenColumns()
+              .visibleToAbsoluteColumn(firstVisibleColumn);
+      lastVisibleColumn = alignment.getHiddenColumns()
+              .visibleToAbsoluteColumn(lastVisibleColumn);
+    }
+
+    int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
+
+    for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
+            .getEndSeq(); seqNo++)
+    {
+      SequenceI seq = alignment.getSequenceAt(seqNo);
+
+      int[] visibleResults = results.getResults(seq, firstVisibleColumn,
+              lastVisibleColumn);
+      if (visibleResults != null)
+      {
+        for (int i = 0; i < visibleResults.length - 1; i += 2)
+        {
+          int firstMatchedColumn = visibleResults[i];
+          int lastMatchedColumn = visibleResults[i + 1];
+          if (firstMatchedColumn <= lastVisibleColumn
+                  && lastMatchedColumn >= firstVisibleColumn)
+          {
+            /*
+             * found a search results match in the visible region
+             */
+            firstMatchedColumn = Math.max(firstMatchedColumn,
+                    firstVisibleColumn);
+            lastMatchedColumn = Math.min(lastMatchedColumn,
+                    lastVisibleColumn);
+
+            /*
+             * draw each mapped position separately (as contiguous positions may
+             * wrap across lines)
+             */
+            for (int mappedPos = firstMatchedColumn; mappedPos <= lastMatchedColumn; mappedPos++)
+            {
+              int displayColumn = mappedPos;
+              if (av.hasHiddenColumns())
+              {
+                displayColumn = alignment.getHiddenColumns()
+                        .absoluteToVisibleColumn(displayColumn);
+              }
+
+              /*
+               * transX: offset from left edge of canvas to residue position
+               */
+              int transX = labelWidthWest
+                      + ((displayColumn - ranges.getStartRes()) % wrappedWidth)
+                      * av.getCharWidth();
+
+              /*
+               * transY: offset from top edge of canvas to residue position
+               */
+              int transY = gapHeight;
+              transY += (displayColumn - ranges.getStartRes())
+                      / wrappedWidth * wrappedHeight;
+              transY += (seqNo - ranges.getStartSeq()) * av.getCharHeight();
+
+              /*
+               * yOffset is from graphics origin to start of visible region
+               */
+              int yOffset = 0;// (displayColumn / wrappedWidth) * wrappedHeight;
+              if (transY < getHeight())
+              {
+                matchFound = true;
+                gg.translate(transX, transY);
+                drawPanel(gg, displayColumn, displayColumn, seqNo, seqNo,
+                        yOffset);
+                gg.translate(-transX, -transY);
+              }
+            }
+          }
+        }
+      }
+    }
+  
+    return matchFound;
+  }
+
+  /**
+   * Answers the width in pixels of the left scale labels (0 if not shown)
+   * 
+   * @return
+   */
+  int getLabelWidthWest()
+  {
+    return labelWidthWest;
   }
 }
index c3aa15f..8b2e7bc 100644 (file)
@@ -59,10 +59,8 @@ 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;
-import java.util.ListIterator;
 
 import javax.swing.JPanel;
 import javax.swing.SwingUtilities;
@@ -77,14 +75,23 @@ 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;
 
+  /*
+   * last column position for mouseMoved event
+   */
+  private int lastMouseColumn;
+
+  /*
+   * last sequence offset for mouseMoved event
+   */
+  private int lastMouseSeq;
+
   protected int lastres;
 
   protected int startseq;
@@ -139,38 +146,39 @@ 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);
     }
+
+    lastMouseColumn = -1;
+    lastMouseSeq = -1;
   }
 
   int startWrapBlock = -1;
@@ -203,8 +211,8 @@ public class SeqPanel extends JPanel
               + hgap + seqCanvas.getAnnotationHeight();
 
       int y = evt.getY();
-      y -= hgap;
-      x = Math.max(0, x - seqCanvas.LABEL_WEST);
+      y = Math.max(0, y - hgap);
+      x = Math.max(0, x - seqCanvas.getLabelWidthWest());
 
       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
       if (cwidth < 1)
@@ -238,7 +246,7 @@ public class SeqPanel extends JPanel
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     return res;
@@ -308,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()
@@ -327,7 +335,7 @@ public class SeqPanel extends JPanel
     {
       seqCanvas.cursorX = getKeyboardNo1() - 1;
       seqCanvas.cursorY = getKeyboardNo2() - 1;
-      scrollToVisible();
+      scrollToVisible(true);
     }
   }
 
@@ -336,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)
@@ -351,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
@@ -364,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)
     {
@@ -388,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);
@@ -472,7 +525,7 @@ public class SeqPanel extends JPanel
       av.setSelectionGroup(sg);
     }
 
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -669,6 +722,8 @@ public class SeqPanel extends JPanel
     }
     lastSearchResults = results;
 
+    boolean wasScrolled = false;
+
     if (av.isFollowHighlight())
     {
       // don't allow highlight of protein/cDNA to also scroll a complementary
@@ -676,14 +731,19 @@ public class SeqPanel extends JPanel
       // over residue to change abruptly, causing highlighted residue in panel 2
       // to change, causing a scroll in panel 1 etc)
       ap.setToScrollComplementPanel(false);
-      if (ap.scrollToPosition(results, false))
+      wasScrolled = ap.scrollToPosition(results, false);
+      if (wasScrolled)
       {
         seqCanvas.revalidate();
       }
       ap.setToScrollComplementPanel(true);
     }
-    setStatusMessage(results);
-    seqCanvas.highlightSearchResults(results);
+
+    boolean noFastPaint = wasScrolled && av.getWrapAlignment();
+    if (seqCanvas.highlightSearchResults(results, noFastPaint))
+    {
+      setStatusMessage(results);
+    }
   }
 
   @Override
@@ -700,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)
@@ -716,11 +778,22 @@ 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;
       return;
     }
+    if (column == lastMouseColumn && seq == lastMouseSeq)
+    {
+      /*
+       * just a pixel move without change of residue
+       */
+      return;
+    }
+    lastMouseColumn = column;
+    lastMouseSeq = seq;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
@@ -771,13 +844,9 @@ public class SeqPanel extends JPanel
     if (av.isShowSequenceFeatures())
     {
       List<SequenceFeature> features = ap.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(), pos);
-      if (isGapped)
-      {
-        removeAdjacentFeatures(features, column + 1, sequence);
-      }
+              .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>
     {
@@ -786,45 +855,18 @@ public class SeqPanel extends JPanel
     }
     else
     {
-      if (lastTooltip == null
-              || !lastTooltip.equals(tooltipText.toString()))
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
       {
-        String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
-                tooltipText.toString());
-        // String formatedTooltipText = tooltipText.toString();
-        setToolTipText(formatedTooltipText);
-        lastTooltip = tooltipText.toString();
+        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+        tooltipText.append("...");
       }
-
-    }
-
-  }
-
-  /**
-   * Removes from the list of features any that start after, or end before, the
-   * given column position. This allows us to retain only those features
-   * adjacent to a gapped position that straddle the position. Contact features
-   * that 'straddle' the position are also removed, since they are not 'at' the
-   * position.
-   * 
-   * @param features
-   * @param column
-   *          alignment column (1..)
-   * @param sequence
-   */
-  protected void removeAdjacentFeatures(List<SequenceFeature> features,
-          final int column, SequenceI sequence)
-  {
-    // TODO should this be an AlignViewController method (and reused by applet)?
-    ListIterator<SequenceFeature> it = features.listIterator();
-    while (it.hasNext())
-    {
-      SequenceFeature sf = it.next();
-      if (sf.isContactFeature()
-              || sequence.findIndex(sf.getBegin()) > column
-              || sequence.findIndex(sf.getEnd()) < column)
+      String textString = tooltipText.toString();
+      if (lastTooltip == null || !lastTooltip.equals(textString))
       {
-        it.remove();
+        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
+                textString);
+        setToolTipText(formattedTooltipText);
+        lastTooltip = textString;
       }
     }
   }
@@ -859,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
@@ -883,19 +926,48 @@ public class SeqPanel extends JPanel
    *          aligned sequence object
    * @param column
    *          alignment column
-   * @param seq
+   * @param seqIndex
    *          index of sequence in alignment
    * @return sequence position of residue at column, or adjacent residue if at a
    *         gap
    */
-  int setStatusMessage(SequenceI sequence, final int column, int seq)
+  int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
+  {
+    char sequenceChar = sequence.getCharAt(column);
+    int pos = sequence.findPosition(column);
+    setStatusMessage(sequence, seqIndex, sequenceChar, pos);
+
+    return pos;
+  }
+
+  /**
+   * Builds the status message for the current cursor location and writes it to
+   * the status bar, for example
+   * 
+   * <pre>
+   * Sequence 3 ID: FER1_SOLLC
+   * Sequence 5 ID: FER1_PEA Residue: THR (4)
+   * Sequence 5 ID: FER1_PEA Residue: B (3)
+   * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
+   * </pre>
+   * 
+   * @param sequence
+   * @param seqIndex
+   *          sequence position in the alignment (1..)
+   * @param sequenceChar
+   *          the character under the cursor
+   * @param residuePos
+   *          the sequence residue position (if not over a gap)
+   */
+  protected void setStatusMessage(SequenceI sequence, int seqIndex,
+          char sequenceChar, int residuePos)
   {
     StringBuilder text = new StringBuilder(32);
 
     /*
      * Sequence number (if known), and sequence name.
      */
-    String seqno = seq == -1 ? "" : " " + (seq + 1);
+    String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
     text.append("Sequence").append(seqno).append(" ID: ")
             .append(sequence.getName());
 
@@ -904,13 +976,12 @@ public class SeqPanel extends JPanel
     /*
      * Try to translate the display character to residue name (null for gap).
      */
-    final String displayChar = String.valueOf(sequence.getCharAt(column));
-    boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
-    int pos = sequence.findPosition(column);
+    boolean isGapped = Comparison.isGap(sequenceChar);
 
     if (!isGapped)
     {
       boolean nucleotide = av.getAlignment().isNucleotide();
+      String displayChar = String.valueOf(sequenceChar);
       if (nucleotide)
       {
         residue = ResidueProperties.nucleotideName.get(displayChar);
@@ -924,11 +995,9 @@ public class SeqPanel extends JPanel
       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
               .append(": ").append(residue == null ? displayChar : residue);
 
-      text.append(" (").append(Integer.toString(pos)).append(")");
+      text.append(" (").append(Integer.toString(residuePos)).append(")");
     }
     ap.alignFrame.statusBar.setText(text.toString());
-
-    return pos;
   }
 
   /**
@@ -956,12 +1025,9 @@ public class SeqPanel extends JPanel
 
       if (seq == ds)
       {
-        /*
-         * Convert position in sequence (base 1) to sequence character array
-         * index (base 0)
-         */
-        int start = m.getStart() - m.getSequence().getStart();
-        setStatusMessage(seq, start, sequenceIndex);
+        int start = m.getStart();
+        setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
+                start);
         return;
       }
     }
@@ -1041,7 +1107,7 @@ public class SeqPanel extends JPanel
         }
         if (newWidth > 0)
         {
-          ap.paintAlignment(false);
+          ap.paintAlignment(false, false);
           if (copyChanges)
           {
             /*
@@ -1206,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))
@@ -1258,7 +1324,7 @@ public class SeqPanel extends JPanel
         // Find the next gap before the end
         // of the visible region boundary
         boolean blank = false;
-        for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
+        for (; fixedRight > lastres; fixedRight--)
         {
           blank = true;
 
@@ -1284,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;
@@ -1568,19 +1635,13 @@ public class SeqPanel extends JPanel
       }
 
       int column = findColumn(evt);
-      boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
 
       /*
        * find features at the position (if not gapped), or straddling
        * the position (if at a gap)
        */
       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(),
-                      sequence.findPosition(column));
-      if (isGapped)
-      {
-        removeAdjacentFeatures(features, column, sequence);
-      }
+              .findFeaturesAtColumn(sequence, column + 1);
 
       if (!features.isEmpty())
       {
@@ -1588,9 +1649,9 @@ public class SeqPanel extends JPanel
          * highlight the first feature at the position on the alignment
          */
         SearchResultsI highlight = new SearchResults();
-        highlight.addResult(sequence, features.get(0).getBegin(),
-                features.get(0).getEnd());
-        seqCanvas.highlightSearchResults(highlight);
+        highlight.addResult(sequence, features.get(0).getBegin(), features
+                .get(0).getEnd());
+        seqCanvas.highlightSearchResults(highlight, false);
 
         /*
          * open the Amend Features dialog; clear highlighting afterwards,
@@ -1599,7 +1660,8 @@ public class SeqPanel extends JPanel
         List<SequenceI> seqs = Collections.singletonList(sequence);
         seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
                 ap);
-        seqCanvas.highlightSearchResults(null);
+        av.setSearchResults(null); // clear highlighting
+        seqCanvas.repaint(); // draw new/amended features
       }
     }
   }
@@ -1608,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);
   }
 
   /**
@@ -1645,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;
 
@@ -1768,25 +1837,13 @@ public class SeqPanel extends JPanel
    */
   void showPopupMenu(MouseEvent evt)
   {
-    final int res = findColumn(evt);
+    final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
-            .findFeaturesAtRes(sequence.getDatasetSequence(),
-                    sequence.findPosition(res));
-    List<String> links = new ArrayList<>();
-    for (SequenceFeature sf : allFeatures)
-    {
-      if (sf.links != null)
-      {
-        for (String link : sf.links)
-        {
-          links.add(link);
-        }
-      }
-    }
+    List<SequenceFeature> features = ap.getFeatureRenderer()
+            .findFeaturesAtColumn(sequence, column + 1);
 
-    PopupMenu pop = new PopupMenu(ap, null, links);
+    PopupMenu pop = new PopupMenu(ap, null, features);
     pop.show(this, evt.getX(), evt.getY());
   }
 
@@ -1811,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)
     {
@@ -1831,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;
@@ -1886,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)
@@ -1894,7 +1953,7 @@ public class SeqPanel extends JPanel
       if (res < (stretchGroup.getEndRes() + 1))
       {
         stretchGroup.setStartRes(res);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -1928,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
       {
@@ -1938,7 +1997,7 @@ public class SeqPanel extends JPanel
         }
 
         stretchGroup.addSequence(nextSeq, false);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -2233,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 804d1a5..8754fbb 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;
@@ -33,7 +34,6 @@ import jalview.io.gff.SequenceOntologyI;
 import jalview.util.DBRefUtils;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
 import java.awt.BorderLayout;
@@ -78,6 +78,8 @@ public class SequenceFetcher extends JPanel implements Runnable
 
   JButton close = new JButton();
 
+  JButton back = new JButton();
+
   JPanel jPanel1 = new JPanel();
 
   JTextArea textArea = new JTextArea();
@@ -108,10 +110,6 @@ public class SequenceFetcher extends JPanel implements Runnable
 
   private static jalview.ws.SequenceFetcher sfetch = null;
 
-  private static long lastDasSourceRegistry = -3;
-
-  private static DasSourceRegistryI dasRegistry = null;
-
   private static boolean _initingFetcher = false;
 
   private static Thread initingThread = null;
@@ -162,11 +160,7 @@ public class SequenceFetcher extends JPanel implements Runnable
                 Thread.currentThread().hashCode());
       }
     }
-    if (sfetch == null || dasRegistry != Cache.getDasSourceRegistry()
-            || lastDasSourceRegistry != (Cache.getDasSourceRegistry()
-                    .getDasRegistryURL()
-                    + Cache.getDasSourceRegistry().getLocalSourceString())
-                            .hashCode())
+    if (sfetch == null)
     {
       _initingFetcher = true;
       initingThread = Thread.currentThread();
@@ -180,16 +174,12 @@ public class SequenceFetcher extends JPanel implements Runnable
                         "status.init_sequence_database_fetchers"),
                 Thread.currentThread().hashCode());
       }
-      dasRegistry = Cache.getDasSourceRegistry();
-      dasRegistry.refreshSources();
 
       jalview.ws.SequenceFetcher sf = new jalview.ws.SequenceFetcher();
       if (guiWindow != null)
       {
         guiWindow.setProgressBar(null, Thread.currentThread().hashCode());
       }
-      lastDasSourceRegistry = (dasRegistry.getDasRegistryURL()
-              + dasRegistry.getLocalSourceString()).hashCode();
       sfetch = sf;
       _initingFetcher = false;
       initingThread = null;
@@ -270,7 +260,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 +373,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 +426,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 +435,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 +461,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();
@@ -540,15 +551,10 @@ public class SequenceFetcher extends JPanel implements Runnable
       dbeg.setText(MessageManager.formatMessage("label.example_query_param",
               new String[]
               { eq }));
+      // TODO this should be a property of the SequenceFetcher whether commas are and
+      // colons are allowed in the IDs...
+
       boolean enablePunct = !(eq != null && eq.indexOf(",") > -1);
-      for (DbSourceProxy dbs : database.getSelectedSources())
-      {
-        if (dbs instanceof jalview.ws.dbsources.das.datamodel.DasSequenceSource)
-        {
-          enablePunct = false;
-          break;
-        }
-      }
       replacePunctuation.setEnabled(enablePunct);
 
     } catch (Exception ex)
@@ -582,6 +588,10 @@ public class SequenceFetcher extends JPanel implements Runnable
     try
     {
       frame.setClosed(true);
+      if (parentFTSframe!=null)
+      {
+        parentFTSframe.btn_cancel_ActionPerformed();
+      }
     } catch (Exception ex)
     {
     }
@@ -594,7 +604,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 +616,7 @@ public class SequenceFetcher extends JPanel implements Runnable
     textArea.setEnabled(true);
     ok.setEnabled(true);
     close.setEnabled(true);
+    back.setEnabled(parentFTSframe != null);
   }
 
   @Override
@@ -645,10 +656,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 +677,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 +843,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();
@@ -1004,15 +1015,11 @@ public class SequenceFetcher extends JPanel implements Runnable
         {
           for (SequenceI sq : alsqs)
           {
-            if ((sfs = sq.getSequenceFeatures()) != null)
+            if (sq.getFeatures().hasFeatures())
             {
-              if (sfs.length > 0)
-              {
-                af.setShowSeqFeatures(true);
-                break;
-              }
+              af.setShowSeqFeatures(true);
+              break;
             }
-
           }
         }
 
@@ -1091,4 +1098,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 7dc1a99..fa30e13 100755 (executable)
@@ -962,6 +962,15 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
           }
         }
         colourGroups(groups);
+
+        /*
+         * clear partition (don't show vertical line) if
+         * it is to the right of all nodes
+         */
+        if (groups.isEmpty())
+        {
+          threshold = 0f;
+        }
       }
 
       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
@@ -981,7 +990,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
 
       Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
 
-      Vector<SequenceI> sequences = new Vector<SequenceI>();
+      Vector<SequenceI> sequences = new Vector<>();
 
       for (int j = 0; j < l.size(); j++)
       {
index 80f0c73..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;
   }
 
@@ -793,19 +823,17 @@ public class TreePanel extends GTreePanel
             }
             if (newname == null)
             {
-              SequenceFeature sf[] = sq.getSequenceFeatures();
-              for (int i = 0; sf != null && i < sf.length; i++)
+              List<SequenceFeature> features = sq.getFeatures()
+                      .getPositionalFeatures(labelClass);
+              for (SequenceFeature feature : features)
               {
-                if (sf[i].getType().equals(labelClass))
+                if (newname == null)
+                {
+                  newname = feature.getDescription();
+                }
+                else
                 {
-                  if (newname == null)
-                  {
-                    newname = new String(sf[i].getDescription());
-                  }
-                  else
-                  {
-                    newname = newname + "; " + sf[i].getDescription();
-                  }
+                  newname = newname + "; " + feature.getDescription();
                 }
               }
             }
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 6317e83..1b93892 100755 (executable)
@@ -246,10 +246,7 @@ public class BLCFile extends AlignFile
 
       out.append(newline);
 
-      if (s[i].getSequence().length > max)
-      {
-        max = s[i].getSequence().length;
-      }
+      max = Math.max(max, s[i].getLength());
 
       i++;
     }
index 6c35ca1..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";
@@ -209,10 +211,7 @@ public class ClustalFile extends AlignFile
     {
       String tmp = printId(s[i], jvsuffix);
 
-      if (s[i].getSequence().length > max)
-      {
-        max = s[i].getSequence().length;
-      }
+      max = Math.max(max, s[i].getLength());
 
       if (tmp.length() > maxid)
       {
@@ -241,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;
 
-        if ((end < s[j].getSequence().length)
-                && (start < s[j].getSequence().length))
+        int length = s[j].getLength();
+        if ((chunkEnd < length) && (chunkStart < length))
         {
-          out.append(s[j].getSequenceAsString(start, end));
+          out.append(s[j].getSequenceAsString(chunkStart, chunkEnd));
         }
         else
         {
-          if (start < s[j].getSequence().length)
+          if (chunkStart < length)
           {
-            out.append(s[j].getSequenceAsString().substring(start));
+            out.append(s[j].getSequenceAsString().substring(chunkStart));
           }
         }
 
index 4be8d0e..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;
@@ -44,8 +46,8 @@ import java.awt.Color;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -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;
@@ -94,14 +104,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   /**
    * Constructor which does not parse the file immediately
    * 
-   * @param inFile
+   * @param file
    * @param paste
    * @throws IOException
    */
-  public FeaturesFile(String inFile, DataSourceType paste)
+  public FeaturesFile(String file, DataSourceType paste)
           throws IOException
   {
-    super(false, inFile, paste);
+    super(false, file, paste);
   }
 
   /**
@@ -117,14 +127,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * Constructor that optionally parses the file immediately
    * 
    * @param parseImmediately
-   * @param inFile
+   * @param file
    * @param type
    * @throws IOException
    */
-  public FeaturesFile(boolean parseImmediately, String inFile,
+  public FeaturesFile(boolean parseImmediately, String file,
           DataSourceType type) throws IOException
   {
-    super(parseImmediately, inFile, type);
+    super(parseImmediately, file, type);
   }
 
   /**
@@ -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
@@ -281,7 +319,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
      */
     for (SequenceI newseq : newseqs)
     {
-      if (newseq.getSequenceFeatures() != null)
+      if (newseq.getFeatures().hasFeatures())
       {
         align.addSequence(newseq);
       }
@@ -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.
@@ -359,20 +434,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       Color colour = ColorUtils.createColourFromName(ft);
       featureColours.put(ft, new FeatureColour(colour));
     }
-    SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos, endPos,
-            featureGroup);
+    SequenceFeature sf = null;
     if (gffColumns.length > 6)
     {
       float score = Float.NaN;
       try
       {
         score = new Float(gffColumns[6]).floatValue();
-        // update colourgradient bounds if allowed to
       } catch (NumberFormatException ex)
       {
-        // leave as NaN
+        sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
       }
-      sf.setScore(score);
+      sf = new SequenceFeature(ft, desc, startPos, endPos, score,
+              featureGroup);
+    }
+    else
+    {
+      sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
     }
 
     parseDescriptionHTML(sf, removeHTML);
@@ -472,217 +550,260 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
     ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks(
             sf.getDescription(), removeHTML, newline);
 
-    sf.description = (removeHTML) ? parsed.getNonHtmlContent()
-            : sf.description;
+    if (removeHTML)
+    {
+      sf.setDescription(parsed.getNonHtmlContent());
+    }
+
     for (String link : parsed.getLinks())
     {
       sf.addLink(link);
     }
-
   }
 
   /**
-   * generate a features file for seqs includes non-pos features by default.
-   * 
-   * @param sequences
-   *          source of sequence features
-   * @param visible
-   *          hash of feature types and colours
-   * @return features file contents
-   */
-  public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible)
-  {
-    return printJalviewFormat(sequences, visible, true, true);
-  }
-
-  /**
-   * generate a features file for seqs with colours from visible (if any)
+   * 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
-   *          hash of Colours for each feature type
-   * @param visOnly
-   *          when true only feature types in 'visible' will be output
-   * @param nonpos
-   *          indicates if non-positional features should be output (regardless
-   *          of group or type)
-   * @return features file contents
+   *          map of colour for each visible feature type
+   * @param featureFilters
+   * @param visibleFeatureGroups
+   * @param includeNonPositional
+   *          if true, include non-positional features (regardless of group or
+   *          type)
+   * @return
    */
   public String printJalviewFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean visOnly,
-          boolean nonpos)
+          Map<String, FeatureColourI> visible,
+          Map<String, FeatureMatcherSetI> featureFilters,
+          List<String> visibleFeatureGroups, boolean includeNonPositional)
   {
-    StringBuilder out = new StringBuilder(256);
-    boolean featuresGen = false;
-    if (visOnly && !nonpos && (visible == null || visible.size() < 1))
+    if (!includeNonPositional && (visible == null || visible.isEmpty()))
     {
       // no point continuing.
       return "No Features Visible";
     }
 
-    if (visible != null && visOnly)
+    /*
+     * write out feature colours (if we know them)
+     */
+    // TODO: decide if feature links should also be written here ?
+    StringBuilder out = new StringBuilder(256);
+    if (visible != null)
     {
-      // write feature colours only if we're given them and we are generating
-      // viewed features
-      // TODO: decide if feature links should also be written here ?
-      Iterator<String> en = visible.keySet().iterator();
-      while (en.hasNext())
+      for (Entry<String, FeatureColourI> featureColour : visible.entrySet())
       {
-        String featureType = en.next().toString();
-        FeatureColourI colour = visible.get(featureType);
-        out.append(colour.toJalviewFormat(featureType)).append(newline);
+        FeatureColourI colour = featureColour.getValue();
+        out.append(colour.toJalviewFormat(featureColour.getKey())).append(
+                newline);
       }
     }
 
-    // Work out which groups are both present and visible
-    List<String> groups = new ArrayList<String>();
-    int groupIndex = 0;
-    boolean isnonpos = false;
+    String[] types = visible == null ? new String[0] : visible.keySet()
+            .toArray(new String[visible.keySet().size()]);
+
+    /*
+     * feature filters if any
+     */
+    outputFeatureFilters(out, visible, featureFilters);
 
-    SequenceFeature[] features;
-    for (int i = 0; i < sequences.length; i++)
+    /*
+     * 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<>(visibleFeatureGroups);
+    sortedGroups.remove(null);
+    sortedGroups.remove("");
+    Collections.sort(sortedGroups);
+    sortedGroups.add(null);
+    sortedGroups.add("");
+
+    boolean foundSome = false;
+
+    /*
+     * first output any non-positional features
+     */
+    if (includeNonPositional)
     {
-      features = sequences[i].getSequenceFeatures();
-      if (features != null)
+      for (int i = 0; i < sequences.length; i++)
       {
-        for (int j = 0; j < features.length; j++)
+        String sequenceName = sequences[i].getName();
+        for (SequenceFeature feature : sequences[i].getFeatures()
+                .getNonPositionalFeatures())
         {
-          isnonpos = features[j].begin == 0 && features[j].end == 0;
-          if ((!nonpos && isnonpos) || (!isnonpos && visOnly
-                  && !visible.containsKey(features[j].type)))
-          {
-            continue;
-          }
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, feature));
+        }
+      }
+    }
 
-          if (features[j].featureGroup != null
-                  && !groups.contains(features[j].featureGroup))
-          {
-            groups.add(features[j].featureGroup);
-          }
+    /*
+     * 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);
+    }
 
-    String group = null;
-    do
+  }
+
+  /**
+   * 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)
     {
-      if (groups.size() > 0 && groupIndex < groups.size())
+      boolean isNamedGroup = (group != null && !"".equals(group));
+      if (isNamedGroup)
       {
-        group = groups.get(groupIndex);
         out.append(newline);
-        out.append("STARTGROUP").append(TAB);
+        out.append(STARTGROUP).append(TAB);
         out.append(group);
         out.append(newline);
       }
-      else
-      {
-        group = null;
-      }
 
+      /*
+       * output positional features within groups
+       */
       for (int i = 0; i < sequences.length; i++)
       {
-        features = sequences[i].getSequenceFeatures();
-        if (features != null)
+        String sequenceName = sequences[i].getName();
+        List<SequenceFeature> features = new ArrayList<>();
+        if (featureTypes.length > 0)
         {
-          for (SequenceFeature sequenceFeature : features)
-          {
-            isnonpos = sequenceFeature.begin == 0
-                    && sequenceFeature.end == 0;
-            if ((!nonpos && isnonpos) || (!isnonpos && visOnly
-                    && !visible.containsKey(sequenceFeature.type)))
-            {
-              // skip if feature is nonpos and we ignore them or if we only
-              // output visible and it isn't non-pos and it's not visible
-              continue;
-            }
-
-            if (group != null && (sequenceFeature.featureGroup == null
-                    || !sequenceFeature.featureGroup.equals(group)))
-            {
-              continue;
-            }
+          features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
+                  true, group, featureTypes));
+        }
 
-            if (group == null && sequenceFeature.featureGroup != null)
-            {
-              continue;
-            }
-            // we have features to output
-            featuresGen = true;
-            if (sequenceFeature.description == null
-                    || sequenceFeature.description.equals(""))
-            {
-              out.append(sequenceFeature.type).append(TAB);
-            }
-            else
-            {
-              if (sequenceFeature.links != null && sequenceFeature
-                      .getDescription().indexOf("<html>") == -1)
-              {
-                out.append("<html>");
-              }
-
-              out.append(sequenceFeature.description);
-              if (sequenceFeature.links != null)
-              {
-                for (int l = 0; l < sequenceFeature.links.size(); l++)
-                {
-                  String label = sequenceFeature.links.elementAt(l);
-                  String href = label.substring(label.indexOf("|") + 1);
-                  label = label.substring(0, label.indexOf("|"));
-
-                  if (sequenceFeature.description.indexOf(href) == -1)
-                  {
-                    out.append(
-                            " <a href=\"" + href + "\">" + label + "</a>");
-                  }
-                }
-
-                if (sequenceFeature.getDescription()
-                        .indexOf("</html>") == -1)
-                {
-                  out.append("</html>");
-                }
-              }
-
-              out.append(TAB);
-            }
-            out.append(sequences[i].getName());
-            out.append("\t-1\t");
-            out.append(sequenceFeature.begin);
-            out.append(TAB);
-            out.append(sequenceFeature.end);
-            out.append(TAB);
-            out.append(sequenceFeature.type);
-            if (!Float.isNaN(sequenceFeature.score))
-            {
-              out.append(TAB);
-              out.append(sequenceFeature.score);
-            }
-            out.append(newline);
-          }
+        for (SequenceFeature sequenceFeature : features)
+        {
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, sequenceFeature));
         }
       }
 
-      if (group != null)
+      if (isNamedGroup)
       {
-        out.append("ENDGROUP").append(TAB);
+        out.append(ENDGROUP).append(TAB);
         out.append(group);
         out.append(newline);
-        groupIndex++;
       }
-      else
+    }
+    return foundSome;
+  }
+
+  /**
+   * @param out
+   * @param sequenceName
+   * @param sequenceFeature
+   */
+  protected String formatJalviewFeature(
+          String sequenceName, SequenceFeature sequenceFeature)
+  {
+    StringBuilder out = new StringBuilder(64);
+    if (sequenceFeature.description == null
+            || sequenceFeature.description.equals(""))
+    {
+      out.append(sequenceFeature.type).append(TAB);
+    }
+    else
+    {
+      if (sequenceFeature.links != null
+              && sequenceFeature.getDescription().indexOf("<html>") == -1)
       {
-        break;
+        out.append("<html>");
       }
 
-    } while (groupIndex < groups.size() + 1);
+      out.append(sequenceFeature.description);
+      if (sequenceFeature.links != null)
+      {
+        for (int l = 0; l < sequenceFeature.links.size(); l++)
+        {
+          String label = sequenceFeature.links.elementAt(l);
+          String href = label.substring(label.indexOf("|") + 1);
+          label = label.substring(0, label.indexOf("|"));
+
+          if (sequenceFeature.description.indexOf(href) == -1)
+          {
+            out.append(" <a href=\"" + href + "\">" + label + "</a>");
+          }
+        }
 
-    if (!featuresGen)
+        if (sequenceFeature.getDescription().indexOf("</html>") == -1)
+        {
+          out.append("</html>");
+        }
+      }
+
+      out.append(TAB);
+    }
+    out.append(sequenceName);
+    out.append("\t-1\t");
+    out.append(sequenceFeature.begin);
+    out.append(TAB);
+    out.append(sequenceFeature.end);
+    out.append(TAB);
+    out.append(sequenceFeature.type);
+    if (!Float.isNaN(sequenceFeature.score))
     {
-      return "No Features Visible";
+      out.append(TAB);
+      out.append(sequenceFeature.score);
     }
+    out.append(newline);
 
     return out.toString();
   }
@@ -711,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)
     {
@@ -740,102 +861,90 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns features output in GFF2 format, including hidden and non-positional
-   * features
-   * 
-   * @param sequences
-   *          the sequences whose features are to be output
-   * @param visible
-   *          a map whose keys are the type names of visible features
-   * @return
-   */
-  public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible)
-  {
-    return printGffFormat(sequences, visible, true, true);
-  }
-
-  /**
    * Returns features output in GFF2 format
    * 
    * @param sequences
    *          the sequences whose features are to be output
    * @param visible
    *          a map whose keys are the type names of visible features
-   * @param outputVisibleOnly
+   * @param visibleFeatureGroups
    * @param includeNonPositionalFeatures
    * @return
    */
   public String printGffFormat(SequenceI[] sequences,
-          Map<String, FeatureColourI> visible, boolean outputVisibleOnly,
+          Map<String, FeatureColourI> visible,
+          List<String> visibleFeatureGroups,
           boolean includeNonPositionalFeatures)
   {
     StringBuilder out = new StringBuilder(256);
-    int version = gffVersion == 0 ? 2 : gffVersion;
-    out.append(String.format("%s %d\n", GFF_VERSION, version));
-    String source;
-    boolean isnonpos;
+
+    out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
+
+    if (!includeNonPositionalFeatures
+            && (visible == null || visible.isEmpty()))
+    {
+      return out.toString();
+    }
+
+    String[] types = visible == null ? new String[0] : visible.keySet()
+            .toArray(
+            new String[visible.keySet().size()]);
+
     for (SequenceI seq : sequences)
     {
-      SequenceFeature[] features = seq.getSequenceFeatures();
-      if (features != null)
+      List<SequenceFeature> features = new ArrayList<>();
+      if (includeNonPositionalFeatures)
       {
-        for (SequenceFeature sf : features)
-        {
-          isnonpos = sf.begin == 0 && sf.end == 0;
-          if (!includeNonPositionalFeatures && isnonpos)
-          {
-            /*
-             * ignore non-positional features if not wanted
-             */
-            continue;
-          }
-          // TODO why the test !isnonpos here?
-          // what about not visible non-positional features?
-          if (!isnonpos && outputVisibleOnly
-                  && !visible.containsKey(sf.type))
-          {
-            /*
-             * ignore not visible features if not wanted
-             */
-            continue;
-          }
+        features.addAll(seq.getFeatures().getNonPositionalFeatures());
+      }
+      if (visible != null && !visible.isEmpty())
+      {
+        features.addAll(seq.getFeatures().getPositionalFeatures(types));
+      }
 
-          source = sf.featureGroup;
-          if (source == null)
-          {
-            source = sf.getDescription();
-          }
+      for (SequenceFeature sf : features)
+      {
+        String source = sf.featureGroup;
+        if (!sf.isNonPositional() && source != null
+                && !visibleFeatureGroups.contains(source))
+        {
+          // group is not visible
+          continue;
+        }
 
-          out.append(seq.getName());
-          out.append(TAB);
-          out.append(source);
-          out.append(TAB);
-          out.append(sf.type);
-          out.append(TAB);
-          out.append(sf.begin);
-          out.append(TAB);
-          out.append(sf.end);
-          out.append(TAB);
-          out.append(sf.score);
-          out.append(TAB);
-
-          int strand = sf.getStrand();
-          out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
-          out.append(TAB);
-
-          String phase = sf.getPhase();
-          out.append(phase == null ? "." : phase);
-
-          // miscellaneous key-values (GFF column 9)
-          String attributes = sf.getAttributes();
-          if (attributes != null)
-          {
-            out.append(TAB).append(attributes);
-          }
+        if (source == null)
+        {
+          source = sf.getDescription();
+        }
 
-          out.append(newline);
+        out.append(seq.getName());
+        out.append(TAB);
+        out.append(source);
+        out.append(TAB);
+        out.append(sf.type);
+        out.append(TAB);
+        out.append(sf.begin);
+        out.append(TAB);
+        out.append(sf.end);
+        out.append(TAB);
+        out.append(sf.score);
+        out.append(TAB);
+
+        int strand = sf.getStrand();
+        out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
+        out.append(TAB);
+
+        String phase = sf.getPhase();
+        out.append(phase == null ? "." : phase);
+
+        // miscellaneous key-values (GFF column 9)
+        String attributes = sf.getAttributes();
+        if (attributes != null)
+        {
+          out.append(TAB).append(attributes);
         }
+
+        out.append(newline);
       }
     }
 
@@ -1096,10 +1205,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       // rename sequences if GFF handler requested this
       // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ?
-      SequenceFeature[] sfs = seq.getSequenceFeatures();
-      if (sfs != null)
+      List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures();
+      if (!sfs.isEmpty())
       {
-        String newName = (String) sfs[0].getValue(GffHelperI.RENAME_TOKEN);
+        String newName = (String) sfs.get(0).getValue(
+                GffHelperI.RENAME_TOKEN);
         if (newName != null)
         {
           seq.setName(newName);
index 4b33dbf..e94e1ce 100644 (file)
@@ -347,7 +347,7 @@ public enum FileFormat implements FileFormatI
       return true;
     }
   },
-  Jalview("Jalview", "jar,jvp", true, true)
+  Jalview("Jalview", "jvp, jar", true, true)
   {
     @Override
     public AlignmentFileReaderI getReader(FileParse source)
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 7ad8fcd..ff959b0 100755 (executable)
@@ -275,6 +275,11 @@ public class IdentifyFile
           // read as a FASTA (probably)
           break;
         }
+        if (data.indexOf("{\"") > -1)
+        {
+          reply = FileFormat.Json;
+          break;
+        }
         int lessThan = data.indexOf("<");
         if ((lessThan > -1)) // possible Markup Language data i.e HTML,
                              // RNAML, XML
@@ -292,11 +297,6 @@ public class IdentifyFile
           }
         }
 
-        if (data.indexOf("{\"") > -1)
-        {
-          reply = FileFormat.Json;
-          break;
-        }
         if ((data.length() < 1) || (data.indexOf("#") == 0))
         {
           lineswereskipped = true;
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 5d9c804..fda22dc 100644 (file)
@@ -51,6 +51,7 @@ import jalview.schemes.ColourSchemeProperty;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.ResidueColourScheme;
 import jalview.util.ColorUtils;
+import jalview.util.Format;
 import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 
 import java.awt.Color;
@@ -227,8 +228,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
 
       if (exportSettings.isExportFeatures())
       {
-        jsonAlignmentPojo
-                .setSeqFeatures(sequenceFeatureToJsonPojo(sqs, fr));
+        jsonAlignmentPojo.setSeqFeatures(sequenceFeatureToJsonPojo(sqs));
       }
 
       if (exportSettings.isExportGroups() && seqGroups != null
@@ -308,8 +308,8 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
     return hiddenSections;
   }
 
-  public List<SequenceFeaturesPojo> sequenceFeatureToJsonPojo(
-          SequenceI[] sqs, FeatureRenderer fr)
+  protected List<SequenceFeaturesPojo> sequenceFeatureToJsonPojo(
+          SequenceI[] sqs)
   {
     displayedFeatures = (fr == null) ? null : fr.getFeaturesDisplayed();
     List<SequenceFeaturesPojo> sequenceFeaturesPojo = new ArrayList<>();
@@ -320,42 +320,38 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
 
     FeatureColourFinder finder = new FeatureColourFinder(fr);
 
+    String[] visibleFeatureTypes = displayedFeatures == null ? null
+            : displayedFeatures.getVisibleFeatures().toArray(
+                    new String[displayedFeatures.getVisibleFeatureCount()]);
+
     for (SequenceI seq : sqs)
     {
-      SequenceI dataSetSequence = seq.getDatasetSequence();
-      SequenceFeature[] seqFeatures = (dataSetSequence == null) ? null
-              : seq.getDatasetSequence().getSequenceFeatures();
-
-      seqFeatures = (seqFeatures == null) ? seq.getSequenceFeatures()
-              : seqFeatures;
-      if (seqFeatures == null)
-      {
-        continue;
-      }
-
+      /*
+       * get all features currently visible (and any non-positional features)
+       */
+      List<SequenceFeature> seqFeatures = seq.getFeatures().getAllFeatures(
+              visibleFeatureTypes);
       for (SequenceFeature sf : seqFeatures)
       {
-        if (displayedFeatures != null
-                && displayedFeatures.isVisible(sf.getType()))
-        {
-          SequenceFeaturesPojo jsonFeature = new SequenceFeaturesPojo(
-                  String.valueOf(seq.hashCode()));
-
-          String featureColour = (fr == null) ? null
-                  : jalview.util.Format.getHexString(
-                          finder.findFeatureColour(Color.white, seq,
-                                  seq.findIndex(sf.getBegin())));
-          jsonFeature.setXstart(seq.findIndex(sf.getBegin()) - 1);
-          jsonFeature.setXend(seq.findIndex(sf.getEnd()));
-          jsonFeature.setType(sf.getType());
-          jsonFeature.setDescription(sf.getDescription());
-          jsonFeature.setLinks(sf.links);
-          jsonFeature.setOtherDetails(sf.otherDetails);
-          jsonFeature.setScore(sf.getScore());
-          jsonFeature.setFillColor(featureColour);
-          jsonFeature.setFeatureGroup(sf.getFeatureGroup());
-          sequenceFeaturesPojo.add(jsonFeature);
-        }
+        SequenceFeaturesPojo jsonFeature = new SequenceFeaturesPojo(
+                String.valueOf(seq.hashCode()));
+
+        String featureColour = (fr == null) ? null : Format
+                .getHexString(finder.findFeatureColour(Color.white, seq,
+                        seq.findIndex(sf.getBegin())));
+        int xStart = sf.getBegin() == 0 ? 0
+                : seq.findIndex(sf.getBegin()) - 1;
+        int xEnd = sf.getEnd() == 0 ? 0 : seq.findIndex(sf.getEnd());
+        jsonFeature.setXstart(xStart);
+        jsonFeature.setXend(xEnd);
+        jsonFeature.setType(sf.getType());
+        jsonFeature.setDescription(sf.getDescription());
+        jsonFeature.setLinks(sf.links);
+        jsonFeature.setOtherDetails(sf.otherDetails);
+        jsonFeature.setScore(sf.getScore());
+        jsonFeature.setFillColor(featureColour);
+        jsonFeature.setFeatureGroup(sf.getFeatureGroup());
+        sequenceFeaturesPojo.add(jsonFeature);
       }
     }
     return sequenceFeaturesPojo;
@@ -687,12 +683,23 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
         Long end = (Long) jsonFeature.get("xEnd");
         String type = (String) jsonFeature.get("type");
         String featureGrp = (String) jsonFeature.get("featureGroup");
-        String descripiton = (String) jsonFeature.get("description");
+        String description = (String) jsonFeature.get("description");
         String seqRef = (String) jsonFeature.get("sequenceRef");
         Float score = Float.valueOf(jsonFeature.get("score").toString());
 
         Sequence seq = seqMap.get(seqRef);
-        SequenceFeature sequenceFeature = new SequenceFeature();
+
+        /*
+         * begin/end of 0 is for a non-positional feature
+         */
+        int featureBegin = begin.intValue() == 0 ? 0 : seq
+                .findPosition(begin.intValue());
+        int featureEnd = end.intValue() == 0 ? 0 : seq.findPosition(end
+                .intValue()) - 1;
+
+        SequenceFeature sequenceFeature = new SequenceFeature(type,
+                description, featureBegin, featureEnd, score, featureGrp);
+
         JSONArray linksJsonArray = (JSONArray) jsonFeature.get("links");
         if (linksJsonArray != null && linksJsonArray.size() > 0)
         {
@@ -703,12 +710,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
             sequenceFeature.addLink(link);
           }
         }
-        sequenceFeature.setFeatureGroup(featureGrp);
-        sequenceFeature.setScore(score);
-        sequenceFeature.setDescription(descripiton);
-        sequenceFeature.setType(type);
-        sequenceFeature.setBegin(seq.findPosition(begin.intValue()));
-        sequenceFeature.setEnd(seq.findPosition(end.intValue()) - 1);
+
         seq.addSequenceFeature(sequenceFeature);
         displayedFeatures.setVisible(type);
       }
index c49e34f..7a21c16 100755 (executable)
@@ -68,8 +68,8 @@ public class JalviewFileChooser extends JFileChooser
   public static JalviewFileChooser forRead(String directory,
           String selected)
   {
-    List<String> extensions = new ArrayList<String>();
-    List<String> descs = new ArrayList<String>();
+    List<String> extensions = new ArrayList<>();
+    List<String> descs = new ArrayList<>();
     for (FileFormatI format : FileFormats.getInstance().getFormats())
     {
       if (format.isReadable())
@@ -96,8 +96,8 @@ public class JalviewFileChooser extends JFileChooser
   {
     // TODO in Java 8, forRead and forWrite can be a single method
     // with a lambda expression parameter for isReadable/isWritable
-    List<String> extensions = new ArrayList<String>();
-    List<String> descs = new ArrayList<String>();
+    List<String> extensions = new ArrayList<>();
+    List<String> descs = new ArrayList<>();
     for (FileFormatI format : FileFormats.getInstance().getFormats())
     {
       if (format.isWritable())
@@ -142,7 +142,7 @@ public class JalviewFileChooser extends JFileChooser
     super(safePath(dir));
     if (extensions.length == descs.length)
     {
-      List<String[]> formats = new ArrayList<String[]>();
+      List<String[]> formats = new ArrayList<>();
       for (int i = 0; i < extensions.length; i++)
       {
         formats.add(new String[] { extensions[i], descs[i] });
@@ -278,6 +278,19 @@ public class JalviewFileChooser extends JFileChooser
     return null;
   }
 
+  File ourselectedFile = null;
+
+  @Override
+  public File getSelectedFile()
+  {
+    File selfile = super.getSelectedFile();
+    if (selfile == null && ourselectedFile != null)
+    {
+      return ourselectedFile;
+    }
+    return selfile;
+  }
+
   @Override
   public int showSaveDialog(Component parent) throws HeadlessException
   {
@@ -285,23 +298,48 @@ public class JalviewFileChooser extends JFileChooser
 
     setDialogType(SAVE_DIALOG);
 
+    this.setSelectedFile(null);
     int ret = showDialog(parent, MessageManager.getString("action.save"));
+    ourselectedFile = getSelectedFile();
 
+    if (getSelectedFile() == null)
+    {
+      // Workaround for Java 9,10 on OSX - no selected file, but there is a
+      // filename typed in
+      try
+      {
+        String filename = ((BasicFileChooserUI) getUI()).getFileName();
+        if (filename != null && filename.length() > 0)
+        {
+          ourselectedFile = new File(getCurrentDirectory(), filename);
+        }
+      } catch (Throwable x)
+      {
+        System.err.println(
+                "Unexpected exception when trying to get filename.");
+        x.printStackTrace();
+      }
+    }
+    if (ourselectedFile == null)
+    {
+      return JalviewFileChooser.CANCEL_OPTION;
+    }
     if (getFileFilter() instanceof JalviewFileFilter)
     {
       JalviewFileFilter jvf = (JalviewFileFilter) getFileFilter();
 
-      if (!jvf.accept(getSelectedFile()))
+      if (!jvf.accept(ourselectedFile))
       {
-        String withExtension = getSelectedFile() + "."
+        String withExtension = getSelectedFile().getName() + "."
                 + jvf.getAcceptableExtension();
-        setSelectedFile(new File(withExtension));
+        ourselectedFile = (new File(getCurrentDirectory(), withExtension));
+        setSelectedFile(ourselectedFile);
       }
     }
     // TODO: ENSURE THAT FILES SAVED WITH A ':' IN THE NAME ARE REFUSED AND THE
     // USER PROMPTED FOR A NEW FILENAME
     if ((ret == JalviewFileChooser.APPROVE_OPTION)
-            && getSelectedFile().exists())
+            && ourselectedFile.exists())
     {
       int confirm = JvOptionPane.showConfirmDialog(parent,
               MessageManager.getString("label.overwrite_existing_file"),
index d59e88a..21f5b0f 100755 (executable)
 package jalview.io;
 
 import java.io.File;
-import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.util.StringTokenizer;
 
 import javax.swing.filechooser.FileFilter;
@@ -31,7 +33,7 @@ public class JalviewFileFilter extends FileFilter
 {
   public static Hashtable suffixHash = new Hashtable();
 
-  private Hashtable filters = null;
+  private Map<String, JalviewFileFilter> filters = null;
 
   private String description = "no description";
 
@@ -72,10 +74,11 @@ public class JalviewFileFilter extends FileFilter
 
   public String getAcceptableExtension()
   {
-    return filters.keys().nextElement().toString();
+    return filters.keySet().iterator().next().toString();
   }
 
   // takes account of the fact that database is a directory
+  @Override
   public boolean accept(File f)
   {
     if (f != null)
@@ -87,7 +90,7 @@ public class JalviewFileFilter extends FileFilter
         return true;
       }
 
-      if ((extension != null) && (filters.get(getExtension(f)) != null))
+      if ((extension != null) && (filters.get(extension) != null))
       {
         return true;
       }
@@ -118,13 +121,14 @@ public class JalviewFileFilter extends FileFilter
   {
     if (filters == null)
     {
-      filters = new Hashtable(5);
+      filters = new LinkedHashMap<>(5);
     }
 
     filters.put(extension.toLowerCase(), this);
     fullDescription = null;
   }
 
+  @Override
   public String getDescription()
   {
     if (fullDescription == null)
@@ -135,15 +139,15 @@ public class JalviewFileFilter extends FileFilter
                 : (description + " (");
 
         // build the description from the extension list
-        Enumeration extensions = filters.keys();
+        Iterator<String> extensions = filters.keySet().iterator();
 
         if (extensions != null)
         {
-          fullDescription += ("." + (String) extensions.nextElement());
+          fullDescription += ("." + extensions.next());
 
-          while (extensions.hasMoreElements())
+          while (extensions.hasNext())
           {
-            fullDescription += (", " + (String) extensions.nextElement());
+            fullDescription += (", " + extensions.next());
           }
         }
 
index b2fe587..18114f3 100755 (executable)
@@ -37,14 +37,15 @@ public class JalviewFileView extends FileView
 
   private void loadExtensions()
   {
-    extensions = new HashMap<String, String>();
+    extensions = new HashMap<>();
     for (FileFormatI ff : FileFormats.getInstance().getFormats())
     {
       String desc = ff.getName() + " file";
       String exts = ff.getExtensions();
       for (String ext : exts.split(","))
       {
-        extensions.put(ext.trim().toLowerCase(),
+        ext = ext.trim().toLowerCase();
+        extensions.put(ext,
                 desc + ("jar".equals(ext) ? " (old)" : ""));
       }
     }
@@ -124,7 +125,7 @@ public class JalviewFileView extends FileView
     {
       if (icons == null)
       {
-        icons = new HashMap<String, ImageIcon>();
+        icons = new HashMap<>();
       }
       if (!icons.containsKey(filePath))
       {
index 55138e7..c9f1fcf 100755 (executable)
@@ -59,7 +59,7 @@ public class JnetAnnotationMaker
     // in the future we could search for the query
     // sequence in the alignment before calling this function.
     SequenceI seqRef = al.getSequenceAt(firstSeq);
-    int width = preds[0].getSequence().length;
+    int width = preds[0].getLength();
     int[] gapmap = al.getSequenceAt(firstSeq).gapMap();
     if ((delMap != null && delMap.length > width)
             || (delMap == null && gapmap.length != width))
index 4dbf950..df2bed2 100755 (executable)
@@ -294,7 +294,7 @@ public class MSFfile extends AlignFile
     }
 
     long maxNB = 0;
-    out.append("   MSF: " + s[0].getSequence().length + "   Type: "
+    out.append("   MSF: " + s[0].getLength() + "   Type: "
             + (is_NA ? "N" : "P") + "    Check:  " + (bigChecksum % 10000)
             + "   ..");
     out.append(newline);
@@ -310,9 +310,9 @@ public class MSFfile extends AlignFile
 
       nameBlock[i] = new String("  Name: " + printId(s[i], jvSuffix) + " ");
 
-      idBlock[i] = new String("Len: "
-              + maxLenpad.form(s[i].getSequence().length) + "  Check: "
-              + maxChkpad.form(checksums[i]) + "  Weight: 1.00" + newline);
+      idBlock[i] = new String("Len: " + maxLenpad.form(s[i].getLength())
+              + "  Check: " + maxChkpad.form(checksums[i])
+              + "  Weight: 1.00" + newline);
 
       if (s[i].getName().length() > maxid)
       {
@@ -369,8 +369,9 @@ public class MSFfile extends AlignFile
           int start = (i * 50) + (k * 10);
           int end = start + 10;
 
-          if ((end < s[j].getSequence().length)
-                  && (start < s[j].getSequence().length))
+          int length = s[j].getLength();
+          if ((end < length)
+                  && (start < length))
           {
             out.append(s[j].getSequence(start, end));
 
@@ -385,7 +386,7 @@ public class MSFfile extends AlignFile
           }
           else
           {
-            if (start < s[j].getSequence().length)
+            if (start < length)
             {
               out.append(s[j].getSequenceAsString().substring(start));
               out.append(newline);
index 68aeb68..6b9dc3f 100755 (executable)
@@ -157,10 +157,7 @@ public class PfamFile extends AlignFile
     {
       String tmp = printId(s[i], jvsuffix);
 
-      if (s[i].getSequence().length > max)
-      {
-        max = s[i].getSequence().length;
-      }
+      max = Math.max(max, s[i].getLength());
 
       if (tmp.length() > maxid)
       {
index 2a961a2..539826f 100644 (file)
@@ -248,7 +248,7 @@ public class PhylipFile extends AlignFile
     sb.append(" ");
     // if there are no sequences, then define the number of characters as 0
     sb.append(
-            (sqs.length > 0) ? Integer.toString(sqs[0].getSequence().length)
+(sqs.length > 0) ? Integer.toString(sqs[0].getLength())
                     : "0")
             .append(newline);
 
@@ -280,13 +280,13 @@ public class PhylipFile extends AlignFile
       // sequential has the entire sequence following the name
       if (sequential)
       {
-        sb.append(s.getSequence());
+        sb.append(s.getSequenceAsString());
       }
       else
       {
         // Jalview ensures all sequences are of same length so no need
         // to keep track of min/max length
-        sequenceLength = s.getSequence().length;
+        sequenceLength = s.getLength();
         // interleaved breaks the sequence into chunks for
         // interleavedColumns characters
         sb.append(s.getSequence(0,
index 7b5ce0d..f7f718b 100755 (executable)
@@ -92,7 +92,7 @@ public class PileUpfile extends MSFfile
       i++;
     }
 
-    out.append("   MSF: " + s[0].getSequence().length
+    out.append("   MSF: " + s[0].getLength()
             + "   Type: P    Check:  " + bigChecksum % 10000 + "   ..");
     out.append(newline);
     out.append(newline);
@@ -151,8 +151,8 @@ public class PileUpfile extends MSFfile
           int start = (i * 50) + (k * 10);
           int end = start + 10;
 
-          if ((end < s[j].getSequence().length)
-                  && (start < s[j].getSequence().length))
+          int length = s[j].getLength();
+          if ((end < length) && (start < length))
           {
             out.append(s[j].getSequence(start, end));
 
@@ -167,7 +167,7 @@ public class PileUpfile extends MSFfile
           }
           else
           {
-            if (start < s[j].getSequence().length)
+            if (start < length)
             {
               out.append(s[j].getSequenceAsString().substring(start));
               out.append(newline);
index 613c01f..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;
@@ -57,7 +59,8 @@ public class SequenceAnnotationReport
   final String linkImageURL;
 
   /*
-   * Comparator to order DBRefEntry by Source + accession id (case-insensitive)
+   * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
+   * with 'Primary' sources placed before others, and 'chromosome' first of all
    */
   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
   {
@@ -65,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);
@@ -77,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;
     }
@@ -105,9 +116,9 @@ public class SequenceAnnotationReport
     }
   };
 
-  public SequenceAnnotationReport(String linkImageURL)
+  public SequenceAnnotationReport(String linkURL)
   {
-    this.linkImageURL = linkImageURL;
+    this.linkImageURL = linkURL;
   }
 
   /**
@@ -119,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);
       }
     }
   }
@@ -139,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())
     {
@@ -152,99 +163,92 @@ public class SequenceAnnotationReport
         sb.append(feature.getType()).append(" ").append(feature.getBegin())
                 .append(":").append(feature.getEnd());
       }
+      return;
+    }
+
+    if (sb.length() > 6)
+    {
+      sb.append("<br>");
     }
-    else
+    // TODO: remove this hack to display link only features
+    boolean linkOnly = feature.getValue("linkonly") != null;
+    if (!linkOnly)
     {
-      if (sb.length() > 6)
+      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
    * 
@@ -267,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();
           }
         }
@@ -297,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())
     {
@@ -312,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);
   }
 
   /**
@@ -330,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>");
@@ -353,108 +358,128 @@ public class SequenceAnnotationReport
     {
       ds = ds.getDatasetSequence();
     }
+
+    if (showDbRefs)
+    {
+      maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
+    }
+
+    /*
+     * add non-positional features if wanted
+     */
+    if (showNpFeats)
+    {
+      for (SequenceFeature sf : sequence.getFeatures()
+              .getNonPositionalFeatures())
+      {
+        int sz = -sb.length();
+        appendFeature(sb, 0, fr, sf);
+        sz += sb.length();
+        maxWidth = Math.max(maxWidth, sz);
+      }
+    }
+    sb.append("</i>");
+    return maxWidth;
+  }
+
+  /**
+   * A helper method that appends any DBRefs, returning the maximum line length
+   * added
+   * 
+   * @param sb
+   * @param ds
+   * @param summary
+   * @return
+   */
+  protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
+          boolean summary)
+  {
     DBRefEntry[] dbrefs = ds.getDBRefs();
-    if (showDbRefs && dbrefs != null)
+    if (dbrefs == null)
+    {
+      return 0;
+    }
+
+    // note this sorts the refs held on the sequence!
+    Arrays.sort(dbrefs, comparator);
+    boolean ellipsis = false;
+    String source = null;
+    String lastSource = null;
+    int countForSource = 0;
+    int sourceCount = 0;
+    boolean moreSources = false;
+    int maxLineLength = 0;
+    int lineLength = 0;
+
+    for (DBRefEntry ref : dbrefs)
     {
-      // note this sorts the refs held on the sequence!
-      Arrays.sort(dbrefs, comparator);
-      boolean ellipsis = false;
-      String source = null;
-      String lastSource = null;
-      int countForSource = 0;
-      int sourceCount = 0;
-      boolean moreSources = false;
-      int lineLength = 0;
-
-      for (DBRefEntry ref : dbrefs)
+      source = ref.getSource();
+      if (source == null)
       {
-        source = ref.getSource();
-        if (source == null)
-        {
-          // shouldn't happen
-          continue;
-        }
-        boolean sourceChanged = !source.equals(lastSource);
-        if (sourceChanged)
-        {
-          lineLength = 0;
-          countForSource = 0;
-          sourceCount++;
-        }
-        if (sourceCount > MAX_SOURCES && summary)
-        {
-          ellipsis = true;
-          moreSources = true;
-          break;
-        }
-        lastSource = source;
-        countForSource++;
-        if (countForSource == 1 || !summary)
-        {
-          sb.append("<br>");
-        }
-        if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
-        {
-          String accessionId = ref.getAccessionId();
-          lineLength += accessionId.length() + 1;
-          if (countForSource > 1 && summary)
-          {
-            sb.append(", ").append(accessionId);
-            lineLength++;
-          }
-          else
-          {
-            sb.append(source).append(" ").append(accessionId);
-            lineLength += source.length();
-          }
-          maxWidth = Math.max(maxWidth, lineLength);
-        }
-        if (countForSource == MAX_REFS_PER_SOURCE && summary)
-        {
-          sb.append(COMMA).append(ELLIPSIS);
-          ellipsis = true;
-        }
+        // shouldn't happen
+        continue;
       }
-      if (moreSources)
+      boolean sourceChanged = !source.equals(lastSource);
+      if (sourceChanged)
       {
-        sb.append("<br>").append(ELLIPSIS).append(COMMA).append(source)
-                .append(COMMA).append(ELLIPSIS);
+        lineLength = 0;
+        countForSource = 0;
+        sourceCount++;
       }
-      if (ellipsis)
+      if (sourceCount > MAX_SOURCES && summary)
       {
-        sb.append("<br>(");
-        sb.append(MessageManager.getString("label.output_seq_details"));
-        sb.append(")");
+        ellipsis = true;
+        moreSources = true;
+        break;
       }
-    }
-
-    /*
-     * add non-positional features if wanted
-     */
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-    if (showNpFeats && features != null)
-    {
-      for (int i = 0; i < features.length; i++)
+      lastSource = source;
+      countForSource++;
+      if (countForSource == 1 || !summary)
+      {
+        sb.append("<br>");
+      }
+      if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
       {
-        if (features[i].begin == 0 && features[i].end == 0)
+        String accessionId = ref.getAccessionId();
+        lineLength += accessionId.length() + 1;
+        if (countForSource > 1 && summary)
         {
-          int sz = -sb.length();
-          appendFeature(sb, 0, minmax, features[i]);
-          sz += sb.length();
-          maxWidth = Math.max(maxWidth, sz);
+          sb.append(", ").append(accessionId);
+          lineLength++;
         }
+        else
+        {
+          sb.append(source).append(" ").append(accessionId);
+          lineLength += source.length();
+        }
+        maxLineLength = Math.max(maxLineLength, lineLength);
+      }
+      if (countForSource == MAX_REFS_PER_SOURCE && summary)
+      {
+        sb.append(COMMA).append(ELLIPSIS);
+        ellipsis = true;
       }
     }
-    sb.append("</i>");
-    return maxWidth;
+    if (moreSources)
+    {
+      sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
+    }
+    if (ellipsis)
+    {
+      sb.append("<br>(");
+      sb.append(MessageManager.getString("label.output_seq_details"));
+      sb.append(")");
+    }
+
+    return maxLineLength;
   }
 
   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 e22ee0a..0e73af1 100644 (file)
@@ -74,6 +74,8 @@ import fr.orsay.lri.varna.models.rna.RNA;
  */
 public class StockholmFile extends AlignFile
 {
+  private static final String ANNOTATION = "annotation";
+
   private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
 
   private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
@@ -81,6 +83,14 @@ public class StockholmFile extends AlignFile
   public static final Regex DETECT_BRACKETS = new Regex(
           "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 
+  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
+  public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
+
+  // use the following regex to decide an annotations (whole) line is NOT an RNA
+  // SS (it contains only E,H,e,h and other non-brace/non-alpha chars)
+  private static final Regex NOT_RNASS = new Regex(
+          "^[^<>[\\](){}A-DF-Za-df-z]*$");
+
   StringBuffer out; // output buffer
 
   AlignmentI al;
@@ -195,7 +205,7 @@ public class StockholmFile extends AlignFile
     String version;
     // String id;
     Hashtable seqAnn = new Hashtable(); // Sequence related annotations
-    LinkedHashMap<String, String> seqs = new LinkedHashMap<String, String>();
+    LinkedHashMap<String, String> seqs = new LinkedHashMap<>();
     Regex p, r, rend, s, x;
     // Temporary line for processing RNA annotation
     // String RNAannot = "";
@@ -391,7 +401,7 @@ public class StockholmFile extends AlignFile
               while (j.hasMoreElements())
               {
                 String desc = j.nextElement().toString();
-                if ("annotations".equals(desc) && annotsAdded)
+                if (ANNOTATION.equals(desc) && annotsAdded)
                 {
                   // don't add features if we already added an annotation row
                   continue;
@@ -411,7 +421,7 @@ public class StockholmFile extends AlignFile
                     int new_pos = posmap[k]; // look up nearest seqeunce
                     // position to this column
                     SequenceFeature feat = new SequenceFeature(type, desc,
-                            new_pos, new_pos, 0f, null);
+                            new_pos, new_pos, null);
 
                     seqO.addSequenceFeature(feat);
                   }
@@ -634,7 +644,7 @@ public class StockholmFile extends AlignFile
               content = new Hashtable();
               features.put(this.id2type(type), content);
             }
-            String ns = (String) content.get("annotation");
+            String ns = (String) content.get(ANNOTATION);
 
             if (ns == null)
             {
@@ -642,7 +652,7 @@ public class StockholmFile extends AlignFile
             }
             // finally, append the annotation line
             ns += seq;
-            content.put("annotation", ns);
+            content.put(ANNOTATION, ns);
             // // end of wrapped annotation block.
             // // Now a new row is created with the current set of data
 
@@ -656,7 +666,7 @@ public class StockholmFile extends AlignFile
               strucAnn = new Hashtable();
             }
 
-            Vector<AlignmentAnnotation> newStruc = new Vector<AlignmentAnnotation>();
+            Vector<AlignmentAnnotation> newStruc = new Vector<>();
             parseAnnotationRow(newStruc, type, ns);
             for (AlignmentAnnotation alan : newStruc)
             {
@@ -708,7 +718,7 @@ public class StockholmFile extends AlignFile
   private void guessDatabaseFor(Sequence seqO, String dbr, String dbsource)
   {
     DBRefEntry dbrf = null;
-    List<DBRefEntry> dbrs = new ArrayList<DBRefEntry>();
+    List<DBRefEntry> dbrs = new ArrayList<>();
     String seqdb = "Unknown", sdbac = "" + dbr;
     int st = -1, en = -1, p;
     if ((st = sdbac.indexOf("/")) > -1)
@@ -822,9 +832,14 @@ public class StockholmFile extends AlignFile
     }
     boolean ss = false, posterior = false;
     type = id2type(type);
+
+    boolean isrnass = false;
     if (type.equalsIgnoreCase("secondary structure"))
     {
       ss = true;
+      isrnass = !NOT_RNASS.search(annots); // sorry about the double negative
+                                           // here (it's easier for dealing with
+                                           // other non-alpha-non-brace chars)
     }
     if (type.equalsIgnoreCase("posterior probability"))
     {
@@ -842,7 +857,7 @@ public class StockholmFile extends AlignFile
       {
         // if (" .-_".indexOf(pos) == -1)
         {
-          if (DETECT_BRACKETS.search(pos))
+          if (isrnass && RNASS_BRACKETS.indexOf(pos) >= 0)
           {
             ann.secondaryStructure = Rna.getRNASecStrucState(pos).charAt(0);
             ann.displayCharacter = "" + pos.charAt(0);
@@ -930,10 +945,7 @@ public class StockholmFile extends AlignFile
     while ((in < s.length) && (s[in] != null))
     {
       String tmp = printId(s[in], jvSuffix);
-      if (s[in].getSequence().length > max)
-      {
-        max = s[in].getSequence().length;
-      }
+      max = Math.max(max, s[in].getLength());
 
       if (tmp.length() > maxid)
       {
@@ -1115,22 +1127,36 @@ public class StockholmFile extends AlignFile
     String ch = (annot == null)
             ? ((sequenceI == null) ? "-"
                     : Character.toString(sequenceI.getCharAt(k)))
-            : annot.displayCharacter;
+            : (annot.displayCharacter == null
+                    ? String.valueOf(annot.secondaryStructure)
+                    : annot.displayCharacter);
+    if (ch == null)
+    {
+      ch = " ";
+    }
     if (key != null && key.equals("SS"))
     {
+      char ssannotchar = ' ';
+      boolean charset = false;
       if (annot == null)
       {
         // sensible gap character
-        return ' ';
+        ssannotchar = ' ';
+        charset = true;
       }
       else
       {
         // valid secondary structure AND no alternative label (e.g. ' B')
         if (annot.secondaryStructure > ' ' && ch.length() < 2)
         {
-          return annot.secondaryStructure;
+          ssannotchar = annot.secondaryStructure;
+          charset = true;
         }
       }
+      if (charset)
+      {
+        return (ssannotchar == ' ' && isrna) ? '.' : ssannotchar;
+      }
     }
 
     if (ch.length() == 0)
@@ -1145,7 +1171,9 @@ public class StockholmFile extends AlignFile
     {
       seq = ch.charAt(1);
     }
-    return seq;
+
+    return (seq == ' ' && key != null && key.equals("SS") && isrna) ? '.'
+            : seq;
   }
 
   public String print()
index 40bb8be..3025907 100644 (file)
@@ -397,8 +397,10 @@ public abstract class StructureFile extends AlignFile
 
   public static boolean isRNA(SequenceI seq)
   {
-    for (char c : seq.getSequence())
+    int length = seq.getLength();
+    for (int i = 0; i < length; i++)
     {
+      char c = seq.getCharAt(i);
       if ((c != 'A') && (c != 'C') && (c != 'G') && (c != 'U'))
       {
         return false;
index 6e7df71..d526a31 100755 (executable)
@@ -184,7 +184,7 @@ public class WSWUBlastClient
         }
       }
     }
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, false);
 
   }
 
index 3d0daed..beef3e7 100644 (file)
@@ -23,11 +23,10 @@ package jalview.io.cache;
 import jalview.bin.Cache;
 import jalview.util.MessageManager;
 
-import java.awt.Color;
-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;
@@ -35,46 +34,69 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
-import javax.swing.BorderFactory;
 import javax.swing.JComboBox;
-import javax.swing.JLabel;
 import javax.swing.JMenuItem;
-import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
-import javax.swing.JTextField;
 import javax.swing.SwingUtilities;
-import javax.swing.text.AttributeSet;
-import javax.swing.text.BadLocationException;
-import javax.swing.text.PlainDocument;
 
 public class JvCacheableInputBox<E> extends JComboBox<String>
 {
 
   private static final long serialVersionUID = 5774610435079326695L;
 
-  private static final int INPUT_LIMIT = 2;
-
   private static final int LEFT_BOARDER_WIDTH = 16;
 
   private String cacheKey;
 
   private AppCache appCache;
 
-  private JPanel pnlDefaultCache = new JPanel();
-
-  private JLabel lblDefaultCacheSize = new JLabel();
-
-  private JTextField txtDefaultCacheSize = new JTextField();
-
   private JPopupMenu popup = new JPopupMenu();
 
   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();
@@ -84,8 +106,8 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
   }
 
   /**
-   * Method for initialising cache items for a given cache key and populating
-   * the in-memory cache with persisted cache items
+   * Method for initialising cache items for a given cache key and populating the
+   * in-memory cache with persisted cache items
    * 
    * @param cacheKey
    */
@@ -105,7 +127,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
             .getAllCachedItemsFor(cacheKey);
     if (foundCacheItems == null)
     {
-      foundCacheItems = new LinkedHashSet<String>();
+      foundCacheItems = new LinkedHashSet<>();
     }
     // populate memory cache
     for (String cacheItem : persistedCacheItems)
@@ -120,48 +142,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
    */
   private void initCachePopupMenu()
   {
-    pnlDefaultCache.setBackground(Color.WHITE);
-    // pad panel so as to align with other menu items
-    pnlDefaultCache.setBorder(
-            BorderFactory.createEmptyBorder(0, LEFT_BOARDER_WIDTH, 0, 0));
-    txtDefaultCacheSize.setPreferredSize(new Dimension(45, 20));
-    txtDefaultCacheSize.setFont(new java.awt.Font("Verdana", 0, 12));
-    lblDefaultCacheSize
-            .setText(MessageManager.getString("label.default_cache_size"));
-    lblDefaultCacheSize.setFont(new java.awt.Font("Verdana", 0, 12));
-    // Force input to accept only Integer entries up to length - INPUT_LIMIT
-    txtDefaultCacheSize.setDocument(new PlainDocument()
-    {
-      private static final long serialVersionUID = 1L;
-
-      @Override
-      public void insertString(int offs, String str, AttributeSet a)
-              throws BadLocationException
-      {
-        if (getLength() + str.length() <= INPUT_LIMIT && isInteger(str))
-        {
-          super.insertString(offs, str, a);
-        }
-      }
-    });
-    txtDefaultCacheSize.addKeyListener(new java.awt.event.KeyAdapter()
-    {
-      @Override
-      public void keyPressed(KeyEvent e)
-      {
-        if (e.getKeyCode() == KeyEvent.VK_ENTER)
-        {
-          e.consume();
-          updateCache();
-          closePopup();
-        }
-      }
-    });
-
-    txtDefaultCacheSize.setText(appCache.getCacheLimit(cacheKey));
-    pnlDefaultCache.add(lblDefaultCacheSize);
     menuItemClearCache.setFont(new java.awt.Font("Verdana", 0, 12));
-    pnlDefaultCache.add(txtDefaultCacheSize);
     menuItemClearCache
             .setText(MessageManager.getString("action.clear_cached_items"));
     menuItemClearCache.addActionListener(new ActionListener()
@@ -176,18 +157,11 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
       }
     });
 
-    popup.insert(pnlDefaultCache, 0);
     popup.add(menuItemClearCache);
     setComponentPopupMenu(popup);
     add(popup);
   }
 
-  private void closePopup()
-  {
-    popup.setVisible(false);
-    popup.transferFocus();
-  }
-
   /**
    * Answers true if input text is an integer
    * 
@@ -216,10 +190,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
       @Override
       public void run()
       {
-        int userLimit = txtDefaultCacheSize.getText().trim().isEmpty()
-                ? Integer.valueOf(AppCache.DEFAULT_LIMIT)
-                : Integer.valueOf(txtDefaultCacheSize.getText());
-        int cacheLimit = appCache.updateCacheLimit(cacheKey, userLimit);
+        int cacheLimit = Integer.parseInt(appCache.getCacheLimit(cacheKey));
         String userInput = getUserInput();
         if (userInput != null && !userInput.isEmpty())
         {
@@ -238,7 +209,7 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
           removeAllItems();
         }
         Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
-        List<String> reversedCacheItems = new ArrayList<String>();
+        List<String> reversedCacheItems = new ArrayList<>();
         reversedCacheItems.addAll(cacheItems);
         cacheItems = null;
         Collections.reverse(reversedCacheItems);
@@ -287,10 +258,6 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
   public void persistCache()
   {
     appCache.persistCache(cacheKey);
-    int userLimit = txtDefaultCacheSize.getText().trim().isEmpty()
-            ? Integer.valueOf(AppCache.DEFAULT_LIMIT)
-            : Integer.valueOf(txtDefaultCacheSize.getText());
-    appCache.updateCacheLimit(cacheKey, userLimit);
   }
 
   /**
index 91add1a..da0c245 100644 (file)
@@ -353,12 +353,16 @@ public class ExonerateHelper extends Gff2Helper
     return false;
   }
 
+  /**
+   * An override to set feature group to "exonerate" instead of the default GFF
+   * source value (column 2)
+   */
   @Override
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> set)
   {
-    SequenceFeature sf = super.buildSequenceFeature(gff, set);
-    sf.setFeatureGroup("exonerate");
+    SequenceFeature sf = super.buildSequenceFeature(gff, TYPE_COL,
+            "exonerate", set);
 
     return sf;
   }
index 594040a..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";
@@ -311,10 +313,9 @@ public class Gff3Helper extends GffHelperBase
          * give the mapped sequence a copy of the sequence feature, with 
          * start/end range adjusted 
          */
-        SequenceFeature sf2 = new SequenceFeature(sf);
-        sf2.setBegin(1);
         int sequenceFeatureLength = 1 + sf.getEnd() - sf.getBegin();
-        sf2.setEnd(sequenceFeatureLength);
+        SequenceFeature sf2 = new SequenceFeature(sf, 1,
+                sequenceFeatureLength, sf.getFeatureGroup(), sf.getScore());
         mappedSequence.addSequenceFeature(sf2);
 
         /*
@@ -363,9 +364,11 @@ public class Gff3Helper extends GffHelperBase
    */
   @Override
   protected SequenceFeature buildSequenceFeature(String[] gff,
+          int typeColumn, String group,
           Map<String, List<String>> attributes)
   {
-    SequenceFeature sf = super.buildSequenceFeature(gff, attributes);
+    SequenceFeature sf = super.buildSequenceFeature(gff, typeColumn, group,
+            attributes);
     String desc = getDescription(sf, attributes);
     if (desc != null)
     {
@@ -398,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 301aaec..1d4d3ac 100644 (file)
@@ -338,6 +338,19 @@ public abstract class GffHelperBase implements GffHelperI
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> attributes)
   {
+    return buildSequenceFeature(gff, TYPE_COL, gff[SOURCE_COL], attributes);
+  }
+
+  /**
+   * @param gff
+   * @param typeColumn
+   * @param group
+   * @param attributes
+   * @return
+   */
+  protected SequenceFeature buildSequenceFeature(String[] gff,
+          int typeColumn, String group, Map<String, List<String>> attributes)
+  {
     try
     {
       int start = Integer.parseInt(gff[START_COL]);
@@ -356,8 +369,8 @@ public abstract class GffHelperBase implements GffHelperI
         // e.g. '.' - leave as zero
       }
 
-      SequenceFeature sf = new SequenceFeature(gff[TYPE_COL],
-              gff[SOURCE_COL], start, end, score, gff[SOURCE_COL]);
+      SequenceFeature sf = new SequenceFeature(gff[typeColumn],
+              gff[SOURCE_COL], start, end, score, group);
 
       sf.setStrand(gff[STRAND_COL]);
 
index c05593f..948cdd2 100644 (file)
@@ -73,13 +73,19 @@ public class InterProScanHelper extends Gff3Helper
   }
 
   /**
-  * 
-  */
+   * An override that
+   * <ul>
+   * <li>uses Source (column 2) as feature type instead of the default column 3</li>
+   * <li>sets "InterProScan" as the feature group</li>
+   * <li>extracts "signature_desc" attribute as the feature description</li>
+   * </ul>
+   */
   @Override
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> attributes)
   {
-    SequenceFeature sf = super.buildSequenceFeature(gff, attributes);
+    SequenceFeature sf = super.buildSequenceFeature(gff, SOURCE_COL,
+            INTER_PRO_SCAN, attributes);
 
     /*
      * signature_desc is a more informative source of description
@@ -91,13 +97,6 @@ public class InterProScanHelper extends Gff3Helper
       sf.setDescription(description);
     }
 
-    /*
-     * Set sequence feature group as 'InterProScan', and type as the source
-     * database for this match (e.g. 'Pfam')
-     */
-    sf.setType(gff[SOURCE_COL]);
-    sf.setFeatureGroup(INTER_PRO_SCAN);
-
     return sf;
   }
 
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]);
index 462f5af..a399c04 100644 (file)
 package jalview.io.vamsas;
 
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.io.VamsasAppDatastore;
 
+import java.util.List;
+
 import uk.ac.vamsas.objects.core.DataSet;
 import uk.ac.vamsas.objects.core.DbRef;
 import uk.ac.vamsas.objects.core.Sequence;
@@ -45,12 +48,12 @@ public class Datasetsequence extends DatastoreItem
   // private AlignmentI jvdset;
 
   public Datasetsequence(VamsasAppDatastore vamsasAppDatastore,
-          SequenceI sq, String dict, DataSet dataset)
+          SequenceI sq, String theDict, DataSet theDataset)
   {
     super(vamsasAppDatastore, sq, uk.ac.vamsas.objects.core.Sequence.class);
-    this.dataset = dataset;
+    this.dataset = theDataset;
     // this.jvdset = jvdset;
-    this.dict = dict;
+    this.dict = theDict;
     doSync();
   }
 
@@ -61,6 +64,7 @@ public class Datasetsequence extends DatastoreItem
     doJvUpdate();
   }
 
+  @Override
   public void addFromDocument()
   {
     Sequence vseq = (Sequence) vobj;
@@ -73,6 +77,7 @@ public class Datasetsequence extends DatastoreItem
     modified = true;
   }
 
+  @Override
   public void updateFromDoc()
   {
     Sequence sq = (Sequence) vobj;
@@ -129,25 +134,21 @@ public class Datasetsequence extends DatastoreItem
    */
   private boolean updateSqFeatures()
   {
-    boolean modified = false;
+    boolean changed = false;
     SequenceI sq = (SequenceI) jvobj;
 
     // add or update any new features/references on dataset sequence
-    if (sq.getSequenceFeatures() != null)
+    List<SequenceFeature> sfs = sq.getSequenceFeatures();
+    for (SequenceFeature sf : sfs)
     {
-      int sfSize = sq.getSequenceFeatures().length;
-
-      for (int sf = 0; sf < sfSize; sf++)
-      {
-        modified |= new jalview.io.vamsas.Sequencefeature(datastore,
-                (jalview.datamodel.SequenceFeature) sq
-                        .getSequenceFeatures()[sf],
-                dataset, (Sequence) vobj).docWasUpdated();
-      }
+      changed |= new jalview.io.vamsas.Sequencefeature(datastore, sf,
+              dataset, (Sequence) vobj).docWasUpdated();
     }
-    return modified;
+
+    return changed;
   }
 
+  @Override
   public void addToDocument()
   {
     SequenceI sq = (SequenceI) jvobj;
@@ -218,6 +219,7 @@ public class Datasetsequence extends DatastoreItem
     return modifiedtheseq;
   }
 
+  @Override
   public void conflict()
   {
     log.warn(
@@ -228,6 +230,7 @@ public class Datasetsequence extends DatastoreItem
 
   boolean modified = false;
 
+  @Override
   public void updateToDoc()
   {
     SequenceI sq = (SequenceI) jvobj;
index 31d64f5..74f73d4 100644 (file)
@@ -281,9 +281,39 @@ public class Sequencefeature extends Rangetype
   private SequenceFeature getJalviewSeqFeature(RangeAnnotation dseta)
   {
     int[] se = getBounds(dseta);
-    SequenceFeature sf = new jalview.datamodel.SequenceFeature(
-            dseta.getType(), dseta.getDescription(), dseta.getStatus(),
-            se[0], se[1], dseta.getGroup());
+
+    /*
+     * try to identify feature score
+     */
+    boolean scoreFound = false;
+    float theScore = 0f;
+    String featureType = dseta.getType();
+    if (dseta.getScoreCount() > 0)
+    {
+      Enumeration scr = dseta.enumerateScore();
+      while (scr.hasMoreElements())
+      {
+        Score score = (Score) scr.nextElement();
+        if (score.getName().equals(featureType))
+        {
+          theScore = score.getContent();
+          scoreFound = true;
+        }
+      }
+    }
+
+    SequenceFeature sf = null;
+    if (scoreFound)
+    {
+      sf = new SequenceFeature(featureType, dseta.getDescription(), se[0],
+              se[1], theScore, dseta.getGroup());
+    }
+    else
+    {
+      sf = new SequenceFeature(featureType, dseta.getDescription(), se[0],
+              se[1], dseta.getGroup());
+    }
+    sf.setStatus(dseta.getStatus());
     if (dseta.getLinkCount() > 0)
     {
       Link[] links = dseta.getLink();
@@ -299,11 +329,7 @@ public class Sequencefeature extends Rangetype
       while (scr.hasMoreElements())
       {
         Score score = (Score) scr.nextElement();
-        if (score.getName().equals(sf.getType()))
-        {
-          sf.setScore(score.getContent());
-        }
-        else
+        if (!score.getName().equals(sf.getType()))
         {
           sf.setValue(score.getName(), "" + score.getContent());
         }
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..6807382 100755 (executable)
@@ -264,10 +264,6 @@ public class GPreferences extends JPanel
 
   protected JCheckBox sortByTree = new JCheckBox();
 
-  /*
-   * DAS Settings tab
-   */
-  protected JPanel dasTab = new JPanel();
 
   /*
    * Web Services tab
@@ -326,12 +322,6 @@ public class GPreferences extends JPanel
             MessageManager.getString("label.editing"));
 
     /*
-     * See DasSourceBrowser for the real work of configuring this tab.
-     */
-    dasTab.setLayout(new BorderLayout());
-    tabbedPane.add(dasTab, MessageManager.getString("label.das_settings"));
-
-    /*
      * See WsPreferences for the real work of configuring this tab.
      */
     wsTab.setLayout(new BorderLayout());
@@ -524,7 +514,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 +1198,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 +1504,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 00cbd2c..cfe2735 100644 (file)
@@ -75,7 +75,7 @@ public class FeatureColourFinder
    * @param defaultColour
    * @param seq
    * @param column
-   *          alignment column position (base zero)
+   *          alignment column position (0..)
    * @return
    */
   public Color findFeatureColour(Color defaultColour, SequenceI seq,
@@ -101,7 +101,7 @@ public class FeatureColourFinder
       }
     }
 
-    Color c = featureRenderer.findFeatureColour(seq, column, g);
+    Color c = featureRenderer.findFeatureColour(seq, column + 1, g);
     if (c == null)
     {
       return defaultColour;
index 9f57510..795cd36 100644 (file)
@@ -21,6 +21,8 @@
 package jalview.renderer.seqfeatures;
 
 import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.Range;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.util.Comparison;
@@ -31,6 +33,7 @@ import java.awt.Color;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
+import java.util.List;
 
 public class FeatureRenderer extends FeatureRendererModel
 {
@@ -213,18 +216,12 @@ public class FeatureRenderer extends FeatureRendererModel
       return null;
     }
 
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
-    {
-      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
-       * - normally white, but can be customised otherwise
+       * - normally white, but can be customised
        */
       return null;
     }
@@ -235,7 +232,7 @@ public class FeatureRenderer extends FeatureRendererModel
       /*
        * simple case - just find the topmost rendered visible feature colour
        */
-      renderedColour = findFeatureColour(seq, seq.findPosition(column));
+      renderedColour = findFeatureColour(seq, column);
     }
     else
     {
@@ -272,8 +269,11 @@ public class FeatureRenderer extends FeatureRendererModel
           final SequenceI seq, int start, int end, int y1,
           boolean colourOnly)
   {
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
+    /*
+     * if columns are all gapped, or sequence has no features, nothing to do
+     */
+    Range visiblePositions = seq.findPositions(start+1, end+1);
+    if (visiblePositions == null || !seq.getFeatures().hasFeatures())
     {
       return null;
     }
@@ -287,10 +287,6 @@ public class FeatureRenderer extends FeatureRendererModel
               transparency));
     }
 
-    int startPos = seq.findPosition(start);
-    int endPos = seq.findPosition(end);
-
-    int sfSize = sequenceFeatures.length;
     Color drawnColour = null;
 
     /*
@@ -304,54 +300,60 @@ public class FeatureRenderer extends FeatureRendererModel
         continue;
       }
 
-      // loop through all features in sequence to find
-      // current feature to render
-      for (int sfindex = 0; sfindex < sfSize; sfindex++)
+      FeatureColourI fc = getFeatureStyle(type);
+      List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
+              visiblePositions.getBegin(), visiblePositions.getEnd(), type);
+
+      if (fc.isSimpleColour())
       {
-        final SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
-        if (!sequenceFeature.type.equals(type))
-        {
-          continue;
-        }
+        filterFeaturesForDisplay(overlaps);
+      }
 
-        /*
-         * a feature type may be flagged as shown but the group 
-         * an instance of it belongs to may be hidden
-         */
-        if (featureGroupNotShown(sequenceFeature))
+      for (SequenceFeature sf : overlaps)
+      {
+        Color featureColour = getColor(sf, fc);
+        if (featureColour == null)
         {
+          /*
+           * feature excluded by visibility settings, filters, or colour threshold
+           */
           continue;
         }
 
         /*
-         * check feature overlaps the target range
-         * TODO: efficient retrieval of features overlapping a range
+         * if feature starts/ends outside the visible range,
+         * restrict to visible positions (or if a contact feature,
+         * to a single position)
          */
-        if (sequenceFeature.getBegin() > endPos
-                || sequenceFeature.getEnd() < startPos)
+        int visibleStart = sf.getBegin();
+        if (visibleStart < visiblePositions.getBegin())
         {
-          continue;
+          visibleStart = sf.isContactFeature() ? sf.getEnd()
+                  : visiblePositions.getBegin();
         }
-
-        Color featureColour = getColour(sequenceFeature);
-        if (featureColour == null)
+        int visibleEnd = sf.getEnd();
+        if (visibleEnd > visiblePositions.getEnd())
         {
-          // score feature outwith threshold for colouring
-          continue;
+          visibleEnd = sf.isContactFeature() ? sf.getBegin()
+                  : visiblePositions.getEnd();
         }
 
-        boolean isContactFeature = sequenceFeature.isContactFeature();
+        int featureStartCol = seq.findIndex(visibleStart);
+        int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
+                .findIndex(visibleEnd);
+
+        // Color featureColour = getColour(sequenceFeature);
+
+        boolean isContactFeature = sf.isContactFeature();
 
         if (isContactFeature)
         {
-          boolean drawn = renderFeature(g, seq,
-                  seq.findIndex(sequenceFeature.begin) - 1,
-                  seq.findIndex(sequenceFeature.begin) - 1, featureColour,
-                  start, end, y1, colourOnly);
-          drawn |= renderFeature(g, seq,
-                  seq.findIndex(sequenceFeature.end) - 1,
-                  seq.findIndex(sequenceFeature.end) - 1, featureColour,
-                  start, end, y1, colourOnly);
+          boolean drawn = renderFeature(g, seq, featureStartCol - 1,
+                  featureStartCol - 1, featureColour, start, end, y1,
+                  colourOnly);
+          drawn |= renderFeature(g, seq, featureEndCol - 1,
+                  featureEndCol - 1, featureColour, start, end, y1,
+                  colourOnly);
           if (drawn)
           {
             drawnColour = featureColour;
@@ -359,6 +361,10 @@ public class FeatureRenderer extends FeatureRendererModel
         }
         else
         {
+          /*
+           * showing feature score by height of colour
+           * is not implemented as a selectable option 
+           *
           if (av.isShowSequenceFeaturesHeight()
                   && !Float.isNaN(sequenceFeature.score))
           {
@@ -374,15 +380,16 @@ public class FeatureRenderer extends FeatureRendererModel
           }
           else
           {
+          */
             boolean drawn = renderFeature(g, seq,
-                    seq.findIndex(sequenceFeature.begin) - 1,
-                    seq.findIndex(sequenceFeature.end) - 1, featureColour,
+                    featureStartCol - 1,
+                    featureEndCol - 1, featureColour,
                     start, end, y1, colourOnly);
             if (drawn)
             {
               drawnColour = featureColour;
             }
-          }
+          /*}*/
         }
       }
     }
@@ -400,23 +407,6 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
-   * Answers true if the feature belongs to a feature group which is not
-   * currently displayed, else false
-   * 
-   * @param sequenceFeature
-   * @return
-   */
-  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();
-  }
-
-  /**
    * Called when alignment in associated view has new/modified features to
    * discover and display.
    * 
@@ -428,23 +418,23 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
-   * Returns the sequence feature colour rendered at the given sequence
-   * position, or null if none found. The feature of highest render order (i.e.
-   * on top) is found, subject to both feature type and feature group being
-   * visible, and its colour returned.
+   * Returns the sequence feature colour rendered at the given column position,
+   * or null if none found. The feature of highest render order (i.e. on top) is
+   * found, subject to both feature type and feature group being visible, and
+   * its colour returned. This method is suitable when no feature transparency
+   * applied (only the topmost visible feature colour is rendered).
+   * <p>
+   * Note this method does not check for a gap in the column so would return the
+   * colour for features enclosing a gapped column. Check for gap before calling
+   * if different behaviour is wanted.
    * 
    * @param seq
-   * @param pos
+   * @param column
+   *          (1..)
    * @return
    */
-  Color findFeatureColour(SequenceI seq, int pos)
+  Color findFeatureColour(SequenceI seq, int column)
   {
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
-    {
-      return null;
-    }
-
     /*
      * check for new feature added while processing
      */
@@ -463,33 +453,17 @@ public class FeatureRenderer extends FeatureRendererModel
         continue;
       }
 
-      for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++)
+      List<SequenceFeature> overlaps = seq.findFeatures(column, column,
+              type);
+      for (SequenceFeature sequenceFeature : overlaps)
       {
-        SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
-        if (!sequenceFeature.type.equals(type))
+        if (!featureGroupNotShown(sequenceFeature))
         {
-          continue;
-        }
-
-        if (featureGroupNotShown(sequenceFeature))
-        {
-          continue;
-        }
-
-        /*
-         * check the column position is within the feature range
-         * (or is one of the two contact positions for a contact feature)
-         */
-        boolean featureIsAtPosition = sequenceFeature.begin <= pos
-                && sequenceFeature.end >= pos;
-        if (sequenceFeature.isContactFeature())
-        {
-          featureIsAtPosition = sequenceFeature.begin == pos
-                  || sequenceFeature.end == pos;
-        }
-        if (featureIsAtPosition)
-        {
-          return getColour(sequenceFeature);
+          Color col = getColour(sequenceFeature);
+          if (col != null)
+          {
+            return col;
+          }
         }
       }
     }
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 ccbad44..ec13343 100755 (executable)
@@ -107,19 +107,18 @@ public class ClustalxColourScheme extends ResidueColourScheme
 
     for (SequenceI sq : seqs)
     {
-      char[] seq = sq.getSequence();
-
-      int end_j = seq.length - 1;
+      int end_j = sq.getLength() - 1;
+      int length = sq.getLength();
 
       for (int i = 0; i <= end_j; i++)
       {
-        if ((seq.length - 1) < i)
+        if (length - 1 < i)
         {
           res = 23;
         }
         else
         {
-          res = ResidueProperties.aaIndex[seq[i]];
+          res = ResidueProperties.aaIndex[sq.getCharAt(i)];
         }
         cons2[i][res]++;
       }
index a40a690..51e7645 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 '|'
@@ -183,8 +321,8 @@ public class FeatureColour implements FeatureColourI
       } catch (Exception e)
       {
         throw new IllegalArgumentException(
-                "Couldn't parse the minimum value for graduated colour ("
-                        + descriptor + ")");
+                "Couldn't parse the minimum value for graduated colour ('"
+                        + minval + "')");
       }
       try
       {
@@ -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())
@@ -550,15 +712,40 @@ public class FeatureColour implements FeatureColourI
       return getColour();
     }
 
-    // todo should we check for above/below threshold here?
-    if (range == 0.0)
+    /*
+     * graduated colour case, optionally with threshold
+     * 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)
     {
-      return getMaxColour();
+      try
+      {
+        String attVal = feature.getValueAsString(attributeName);
+        scr = Float.valueOf(attVal);
+      } catch (Throwable e)
+      {
+        scr = Float.NaN;
+      }
     }
-    float scr = feature.getScore();
     if (Float.isNaN(scr))
     {
-      return getMinColour();
+      return noColour;
+    }
+
+    if (isAboveThreshold() && scr <= threshold)
+    {
+      return null;
+    }
+
+    if (isBelowThreshold() && scr >= threshold)
+    {
+      return null;
+    }
+    if (range == 0.0)
+    {
+      return getMaxColour();
     }
     float scl = (scr - base) / range;
     if (isHighToLow)
@@ -601,44 +788,6 @@ public class FeatureColour implements FeatureColourI
     return (isHighToLow) ? (base + range) : base;
   }
 
-  /**
-   * Answers true if the feature has a simple colour, or is coloured by label,
-   * or has a graduated colour and the score of this feature instance is within
-   * the range to render (if any), i.e. does not lie below or above any
-   * threshold set.
-   * 
-   * @param feature
-   * @return
-   */
-  @Override
-  public boolean isColored(SequenceFeature feature)
-  {
-    if (isColourByLabel() || !isGraduatedColour())
-    {
-      return true;
-    }
-
-    float val = feature.getScore();
-    if (Float.isNaN(val))
-    {
-      return true;
-    }
-    if (Float.isNaN(this.threshold))
-    {
-      return true;
-    }
-
-    if (isAboveThreshold() && val <= threshold)
-    {
-      return false;
-    }
-    if (isBelowThreshold() && val >= threshold)
-    {
-      return false;
-    }
-    return true;
-  }
-
   @Override
   public boolean isSimpleColour()
   {
@@ -662,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())
@@ -685,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
         {
@@ -701,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 17d3a70..d4fc233 100644 (file)
@@ -285,35 +285,10 @@ public class Comparison
     {
       return false;
     }
-    char[][] letters = new char[seqs.length][];
-    for (int i = 0; i < seqs.length; i++)
-    {
-      if (seqs[i] != null)
-      {
-        char[] sequence = seqs[i].getSequence();
-        if (sequence != null)
-        {
-          letters[i] = sequence;
-        }
-      }
-    }
-
-    return areNucleotide(letters);
-  }
 
-  /**
-   * Answers true if more than 85% of the sequence residues (ignoring gaps) are
-   * A, G, C, T or U, else false. This is just a heuristic guess and may give a
-   * wrong answer (as AGCT are also amino acid codes).
-   * 
-   * @param letters
-   * @return
-   */
-  static final boolean areNucleotide(char[][] letters)
-  {
     int ntCount = 0;
     int aaCount = 0;
-    for (char[] seq : letters)
+    for (SequenceI seq : seqs)
     {
       if (seq == null)
       {
@@ -321,8 +296,10 @@ public class Comparison
       }
       // TODO could possibly make an informed guess just from the first sequence
       // to save a lengthy calculation
-      for (char c : seq)
+      int len = seq.getLength();
+      for (int i = 0; i < len; i++)
       {
+        char c = seq.getCharAt(i);
         if (isNucleotide(c))
         {
           ntCount++;
similarity index 81%
rename from src/jalview/util/RangeComparator.java
rename to src/jalview/util/IntRangeComparator.java
index 0a3ddd8..a0a29f2 100644 (file)
@@ -26,11 +26,17 @@ import java.util.Comparator;
  * A comparator to order [from, to] ranges into ascending or descending order of
  * their start position
  */
-public class RangeComparator implements Comparator<int[]>
+public class IntRangeComparator implements Comparator<int[]>
 {
+  public static final Comparator<int[]> ASCENDING = new IntRangeComparator(
+          true);
+
+  public static final Comparator<int[]> DESCENDING = new IntRangeComparator(
+          false);
+
   boolean forwards;
 
-  public RangeComparator(boolean forward)
+  IntRangeComparator(boolean forward)
   {
     forwards = forward;
   }
index 4658724..9f28ee1 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;
   }
 
@@ -318,6 +327,13 @@ public class MapList
     fromHighest = Integer.MIN_VALUE;
     for (int[] range : fromRange)
     {
+      if (range.length != 2)
+      {
+        // throw new IllegalArgumentException(range);
+        System.err.println(
+                "Invalid format for fromRange " + Arrays.toString(range)
+                + " may cause errors");
+      }
       fromLowest = Math.min(fromLowest, Math.min(range[0], range[1]));
       fromHighest = Math.max(fromHighest, Math.max(range[0], range[1]));
     }
@@ -326,6 +342,13 @@ public class MapList
     toHighest = Integer.MIN_VALUE;
     for (int[] range : toRange)
     {
+      if (range.length != 2)
+      {
+        // throw new IllegalArgumentException(range);
+        System.err.println("Invalid format for toRange "
+                + Arrays.toString(range)
+                + " may cause errors");
+      }
       toLowest = Math.min(toLowest, Math.min(range[0], range[1]));
       toHighest = Math.max(toHighest, Math.max(range[0], range[1]));
     }
@@ -347,7 +370,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 +826,7 @@ public class MapList
     {
       return null;
     }
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     if (fs <= fe)
     {
       intv = fs;
@@ -1094,8 +1117,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 +1168,72 @@ 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 || transferred.length % 2 != 0)
+      {
+        return null;
+      }
+
+      /*
+       *  convert [start1, end1, start2, end2, ...] 
+       *  to [[start1, end1], [start2, end2], ...]
+       */
+      for (int i = 0; i < transferred.length;)
+      {
+        toRanges.add(new int[] { transferred[i], transferred[i + 1] });
+        i += 2;
+      }
+    }
+
+    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 d6ece8d..e5cfaee 100644 (file)
@@ -57,10 +57,20 @@ public class UrlConstants
   public static final String DEFAULT_STRING = DEFAULT_LABEL
           + "|https://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
 
+  private static final String COLON = ":";
+
   /*
    * not instantiable
    */
   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 f8115a1..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;
   }
@@ -456,27 +539,63 @@ public class ViewportRanges extends ViewportProperties
   }
 
   /**
-   * Scroll a wrapped alignment so that the specified residue is visible. Fires
-   * a property change event.
+   * Scroll a wrapped alignment so that the specified residue is in the first
+   * repeat of the wrapped view. Fires a property change event. Answers true if
+   * the startRes changed, else false.
+   * 
+   * @param res
+   *          residue position to scroll to NB visible position not absolute
+   *          alignment position
+   * @return
+   */
+  public boolean scrollToWrappedVisible(int res)
+  {
+    int newStartRes = calcWrappedStartResidue(res);
+    if (newStartRes == startRes)
+    {
+      return false;
+    }
+    setStartRes(newStartRes);
+
+    return true;
+  }
+
+  /**
+   * Calculate wrapped start residue from visible start residue
    * 
    * @param res
-   *          residue position to scroll to
+   *          visible start residue
+   * @return left column of panel res will be located in
    */
-  public void scrollToWrappedVisible(int res)
+  private int calcWrappedStartResidue(int res)
   {
-    // get the start residue of the wrapped row which res is in
-    // and set that as our start residue
+    int oldStartRes = startRes;
     int width = getViewportWidth();
-    setStartRes((res / width) * width);
+
+    boolean up = res < oldStartRes;
+    int widthsToScroll = Math.abs((res - oldStartRes) / width);
+    if (up)
+    {
+      widthsToScroll++;
+    }
+
+    int residuesToScroll = width * widthsToScroll;
+    int newStartRes = up ? oldStartRes - residuesToScroll : oldStartRes
+            + residuesToScroll;
+    if (newStartRes < 0)
+    {
+      newStartRes = 0;
+    }
+    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)
   {
@@ -488,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))
       {
@@ -507,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 81d1b7e..553f813 100644 (file)
@@ -26,6 +26,8 @@ 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;
 import jalview.util.ColorUtils;
@@ -47,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<>();
+
+  /*
+   * visibility flag for each feature group
+   */
+  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
 
-  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<String, Boolean>();
+  /*
+   * filters for each feature type
+   */
+  protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
 
   protected String[] renderOrder;
 
@@ -99,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())
@@ -116,11 +152,10 @@ public abstract class FeatureRendererModel
           synchronized (fd)
           {
             fd.clear();
-            java.util.Iterator<String> fdisp = _fr.getFeaturesDisplayed()
-                    .getVisibleFeatures();
-            while (fdisp.hasNext())
+            for (String type : _fr.getFeaturesDisplayed()
+                    .getVisibleFeatures())
             {
-              fd.setVisible(fdisp.next());
+              fd.setVisible(type);
             }
           }
         }
@@ -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()
   {
@@ -265,49 +300,44 @@ public abstract class FeatureRendererModel
   }
 
   @Override
-  public List<SequenceFeature> findFeaturesAtRes(SequenceI sequence,
-          int res)
+  public List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
   {
-    ArrayList<SequenceFeature> tmp = new ArrayList<SequenceFeature>();
-    SequenceFeature[] features = sequence.getSequenceFeatures();
-
-    if (features != null)
+    /*
+     * 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<>();
+    if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
     {
-      for (int i = 0; i < features.length; i++)
-      {
-        if (!av.areFeaturesDisplayed() || !av.getFeaturesDisplayed()
-                .isVisible(features[i].getType()))
-        {
-          continue;
-        }
+      return result;
+    }
 
-        if (features[i].featureGroup != null && featureGroups != null
-                && featureGroups.containsKey(features[i].featureGroup)
-                && !featureGroups.get(features[i].featureGroup)
-                        .booleanValue())
-        {
-          continue;
-        }
+    Set<String> visibleFeatures = getFeaturesDisplayed()
+            .getVisibleFeatures();
+    String[] visibleTypes = visibleFeatures
+            .toArray(new String[visibleFeatures.size()]);
+    List<SequenceFeature> features = sequence.findFeatures(column, column,
+            visibleTypes);
 
-        // check if start/end are at res, and if not a contact feature, that res
-        // lies between start and end
-        if ((features[i].getBegin() == res || features[i].getEnd() == res)
-                || (!features[i].isContactFeature()
-                        && (features[i].getBegin() < res)
-                        && (features[i].getEnd() >= res)))
-        {
-          tmp.add(features[i]);
-        }
+    /*
+     * 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) && getColour(sf) != null)
+      {
+        result.add(sf);
       }
     }
-    return tmp;
+    return result;
   }
 
   /**
    * Searches alignment for all features and updates colours
    * 
    * @param newMadeVisible
-   *          if true newly added feature types will be rendered immediatly
+   *          if true newly added feature types will be rendered immediately
    *          TODO: check to see if this method should actually be proxied so
    *          repaint events can be propagated by the renderer code
    */
@@ -329,8 +359,7 @@ public abstract class FeatureRendererModel
     }
     FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
 
-    ArrayList<String> allfeatures = new ArrayList<String>();
-    ArrayList<String> oldfeatures = new ArrayList<String>();
+    Set<String> oldfeatures = new HashSet<>();
     if (renderOrder != null)
     {
       for (int i = 0; i < renderOrder.length; i++)
@@ -341,107 +370,110 @@ public abstract class FeatureRendererModel
         }
       }
     }
-    if (minmax == null)
-    {
-      minmax = new Hashtable<String, float[][]>();
-    }
 
-    Set<String> oldGroups = new HashSet<String>(featureGroups.keySet());
     AlignmentI alignment = av.getAlignment();
+    List<String> allfeatures = new ArrayList<>();
+
     for (int i = 0; i < alignment.getHeight(); i++)
     {
       SequenceI asq = alignment.getSequenceAt(i);
-      SequenceFeature[] features = asq.getSequenceFeatures();
-
-      if (features == null)
+      for (String group : asq.getFeatures().getFeatureGroups(true))
       {
-        continue;
-      }
-
-      int index = 0;
-      while (index < features.length)
-      {
-        String fgrp = features[index].getFeatureGroup();
-        oldGroups.remove(fgrp);
-        if (!featuresDisplayed.isRegistered(features[index].getType()))
+        boolean groupDisplayed = true;
+        if (group != null)
         {
-          if (fgrp != null)
+          if (featureGroups.containsKey(group))
           {
-            Boolean groupDisplayed = featureGroups.get(fgrp);
-            if (groupDisplayed == null)
-            {
-              groupDisplayed = Boolean.valueOf(newMadeVisible);
-              featureGroups.put(fgrp, groupDisplayed);
-            }
-            if (!groupDisplayed.booleanValue())
-            {
-              index++;
-              continue;
-            }
+            groupDisplayed = featureGroups.get(group);
           }
-          if (!(features[index].begin == 0 && features[index].end == 0))
+          else
           {
-            // If beginning and end are 0, the feature is for the whole sequence
-            // and we don't want to render the feature in the normal way
-
-            if (newMadeVisible
-                    && !oldfeatures.contains(features[index].getType()))
-            {
-              // this is a new feature type on the alignment. Mark it for
-              // display.
-              featuresDisplayed.setVisible(features[index].getType());
-              setOrder(features[index].getType(), 0);
-            }
+            groupDisplayed = newMadeVisible;
+            featureGroups.put(group, groupDisplayed);
           }
         }
-        if (!allfeatures.contains(features[index].getType()))
-        {
-          allfeatures.add(features[index].getType());
-        }
-        if (!Float.isNaN(features[index].score))
+        if (groupDisplayed)
         {
-          int nonpos = features[index].getBegin() >= 1 ? 0 : 1;
-          float[][] mm = minmax.get(features[index].getType());
-          if (mm == null)
-          {
-            mm = new float[][] { null, null };
-            minmax.put(features[index].getType(), mm);
-          }
-          if (mm[nonpos] == null)
-          {
-            mm[nonpos] = new float[] { features[index].score,
-                features[index].score };
-
-          }
-          else
+          Set<String> types = asq.getFeatures().getFeatureTypesForGroups(
+                  true, group);
+          for (String type : types)
           {
-            if (mm[nonpos][0] > features[index].score)
-            {
-              mm[nonpos][0] = features[index].score;
-            }
-            if (mm[nonpos][1] < features[index].score)
+            if (!allfeatures.contains(type)) // or use HashSet and no test?
             {
-              mm[nonpos][1] = features[index].score;
+              allfeatures.add(type);
             }
+            updateMinMax(asq, type, true); // todo: for all features?
           }
         }
-        index++;
       }
     }
 
-    /*
-     * oldGroups now consists of groups that no longer 
-     * have any feature in them - remove these
-     */
-    for (String grp : oldGroups)
+    // uncomment to add new features in alphebetical order (but JAL-2575)
+    // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER);
+    if (newMadeVisible)
     {
-      featureGroups.remove(grp);
+      for (String type : allfeatures)
+      {
+        if (!oldfeatures.contains(type))
+        {
+          featuresDisplayed.setVisible(type);
+          setOrder(type, 0);
+        }
+      }
     }
 
     updateRenderOrder(allfeatures);
     findingFeatures = false;
   }
 
+  /**
+   * Updates the global (alignment) min and max values for a feature type from
+   * the score for a sequence, if the score is not NaN. Values are stored
+   * separately for positional and non-positional features.
+   * 
+   * @param seq
+   * @param featureType
+   * @param positional
+   */
+  protected void updateMinMax(SequenceI seq, String featureType,
+          boolean positional)
+  {
+    float min = seq.getFeatures().getMinimumScore(featureType, positional);
+    if (Float.isNaN(min))
+    {
+      return;
+    }
+
+    float max = seq.getFeatures().getMaximumScore(featureType, positional);
+
+    /*
+     * stored values are 
+     * { {positionalMin, positionalMax}, {nonPositionalMin, nonPositionalMax} }
+     */
+    if (minmax == null)
+    {
+      minmax = new Hashtable<>();
+    }
+    synchronized (minmax)
+    {
+      float[][] mm = minmax.get(featureType);
+      int index = positional ? 0 : 1;
+      if (mm == null)
+      {
+        mm = new float[][] { null, null };
+        minmax.put(featureType, mm);
+      }
+      if (mm[index] == null)
+      {
+        mm[index] = new float[] { min, max };
+      }
+      else
+      {
+        mm[index][0] = Math.min(mm[index][0], min);
+        mm[index][1] = Math.max(mm[index][1], max);
+      }
+    }
+  }
   protected Boolean firing = Boolean.FALSE;
 
   /**
@@ -457,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);
@@ -484,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]);
                 }
@@ -514,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]);
           }
@@ -564,33 +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.
-   * 
-   * @param feature
-   * @return
-   */
+  @Override
   public Color getColour(SequenceFeature feature)
   {
     FeatureColourI fc = getFeatureStyle(feature.getType());
-    return fc.isColored(feature) ? fc.getColor(feature) : null;
-  }
-
-  /**
-   * Answers true unless the feature has a score value which lies outside a
-   * minimum or maximum threshold configured for colouring. This method does not
-   * check feature type or group visibility.
-   * 
-   * @param sequenceFeature
-   * @return
-   */
-  protected boolean showFeature(SequenceFeature sequenceFeature)
-  {
-    FeatureColourI fc = getFeatureStyle(sequenceFeature.type);
-    return fc.isColored(sequenceFeature);
+    return getColor(feature, fc);
   }
 
   /**
@@ -602,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
@@ -637,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;
@@ -671,31 +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
+   * 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;
@@ -728,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);
         }
@@ -855,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())
       {
@@ -901,16 +916,15 @@ 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;
     }
-    Iterator<String> features = getViewport().getFeaturesDisplayed()
+    Set<String> features = getViewport().getFeaturesDisplayed()
             .getVisibleFeatures();
-    while (features.hasNext())
+    for (String feature : features)
     {
-      String feature = features.next();
       fcols.put(feature, getFeatureStyle(feature));
     }
     return fcols;
@@ -930,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)
     {
@@ -951,26 +965,189 @@ public abstract class FeatureRendererModel
   @Override
   public List<String> getDisplayedFeatureGroups()
   {
-    List<String> _gps = new ArrayList<String>();
-    boolean valid = false;
+    List<String> _gps = new ArrayList<>();
     for (String gp : getFeatureGroups())
     {
       if (checkGroupVisibility(gp, false))
       {
-        valid = true;
         _gps.add(gp);
       }
-      if (!valid)
+    }
+    return _gps;
+  }
+
+  /**
+   * Answers true if the feature belongs to a feature group which is not
+   * currently displayed, else false
+   * 
+   * @param sequenceFeature
+   * @return
+   */
+  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();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
+          int resNo)
+  {
+    List<SequenceFeature> result = new ArrayList<>();
+    if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
+    {
+      return result;
+    }
+
+    /*
+     * include features at the position provided their feature type is 
+     * displayed, and feature group is null or the empty string
+     * or marked for display
+     */
+    Set<String> visibleFeatures = getFeaturesDisplayed()
+            .getVisibleFeatures();
+    String[] visibleTypes = visibleFeatures
+            .toArray(new String[visibleFeatures.size()]);
+    List<SequenceFeature> features = sequence.getFeatures().findFeatures(
+            resNo, resNo, visibleTypes);
+  
+    for (SequenceFeature sf : features)
+    {
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
       {
-        return null;
+        result.add(sf);
       }
-      else
+    }
+    return result;
+  }
+
+  /**
+   * 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
+   */
+  public void filterFeaturesForDisplay(List<SequenceFeature> features)
+  {
+    /*
+     * 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);
+    SequenceFeature lastFeature = null;
+
+    Iterator<SequenceFeature> it = features.iterator();
+    while (it.hasNext())
+    {
+      SequenceFeature sf = it.next();
+
+      /*
+       * a feature is redundant for rendering purposes if it has the
+       * same extent as another (so would just redraw the same colour);
+       * (checking type and isContactFeature as a fail-safe here, although
+       * currently they are guaranteed to match in this context)
+       */
+      if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+              && sf.getEnd() == lastFeature.getEnd()
+              && sf.isContactFeature() == lastFeature.isContactFeature()
+              && sf.getType().equals(lastFeature.getType()))
       {
-        // gps = new String[_gps.size()];
-        // _gps.toArray(gps);
+        it.remove();
       }
+      lastFeature = sf;
     }
-    return _gps;
+  }
+
+  @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 4c7e3c4..f44a2d1 100644 (file)
@@ -23,22 +23,21 @@ package jalview.viewmodel.seqfeatures;
 import jalview.api.FeaturesDisplayedI;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Set;
 
 public class FeaturesDisplayed implements FeaturesDisplayedI
 {
-  private HashSet<String> featuresDisplayed = new HashSet<String>();
+  private Set<String> featuresDisplayed = new HashSet<String>();
 
-  private HashSet<String> featuresRegistered = new HashSet<String>();
+  private Set<String> featuresRegistered = new HashSet<String>();
 
   public FeaturesDisplayed(FeaturesDisplayedI featuresDisplayed2)
   {
-    Iterator<String> fdisp = featuresDisplayed2.getVisibleFeatures();
-    String ftype;
-    while (fdisp.hasNext())
+    Set<String> fdisp = featuresDisplayed2.getVisibleFeatures();
+    for (String ftype : fdisp)
     {
-      ftype = fdisp.next();
       featuresDisplayed.add(ftype);
       featuresRegistered.add(ftype);
     }
@@ -46,13 +45,12 @@ public class FeaturesDisplayed implements FeaturesDisplayedI
 
   public FeaturesDisplayed()
   {
-    // TODO Auto-generated constructor stub
   }
 
   @Override
-  public Iterator<String> getVisibleFeatures()
+  public Set<String> getVisibleFeatures()
   {
-    return featuresDisplayed.iterator();
+    return Collections.unmodifiableSet(featuresDisplayed);
   }
 
   @Override
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 2422748..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);
     }
 
   }
@@ -229,6 +229,7 @@ class ColumnCounterSetWorker extends AlignCalcWorker
    * 
    * @param alignment
    * @param col
+   *          (0..)
    * @param row
    * @param fr
    */
@@ -249,14 +250,12 @@ class ColumnCounterSetWorker extends AlignCalcWorker
     {
       return null;
     }
-    int pos = seq.findPosition(col);
 
     /*
      * compute a count for any displayed features at residue
      */
-    // NB have to adjust pos if using AlignmentView.getVisibleAlignment
     // see JAL-2075
-    List<SequenceFeature> features = fr.findFeaturesAtRes(seq, pos);
+    List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, col + 1);
     int[] count = this.counter.count(String.valueOf(res), features);
     return count;
   }
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 ea6c5f2..41e4d0d 100644 (file)
@@ -26,18 +26,14 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.Mapping;
-import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.CutAndPasteTransfer;
-import jalview.gui.DasSourceBrowser;
 import jalview.gui.Desktop;
 import jalview.gui.FeatureSettings;
 import jalview.gui.IProgressIndicator;
 import jalview.gui.OOMWarning;
 import jalview.util.DBRefUtils;
 import jalview.util.MessageManager;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-import jalview.ws.dbsources.das.datamodel.DasSequenceSource;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
 import java.util.ArrayList;
@@ -62,6 +58,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();
@@ -118,7 +116,7 @@ public class DBRefFetcher implements Runnable
           DbSourceProxy[] sources, FeatureSettings featureSettings,
           boolean isNucleotide)
   {
-    listeners = new ArrayList<FetchFinishedListenerI>();
+    listeners = new ArrayList<>();
     this.progressWindow = progressIndicatorFrame;
     alseqs = new SequenceI[seqs.length];
     SequenceI[] ds = new SequenceI[seqs.length];
@@ -140,7 +138,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);
@@ -164,23 +162,7 @@ public class DBRefFetcher implements Runnable
   {
     // af.featureSettings_actionPerformed(null);
     String[] defdb = null;
-    List<DbSourceProxy> selsources = new ArrayList<DbSourceProxy>();
-    Vector<jalviewSourceI> dasselsrc = (featureSettings != null)
-            ? featureSettings.getSelectedSources()
-            : new DasSourceBrowser().getSelectedSources();
-
-    for (jalviewSourceI src : dasselsrc)
-    {
-      List<DbSourceProxy> sp = src.getSequenceSourceProxies();
-      if (sp != null)
-      {
-        selsources.addAll(sp);
-        if (sp.size() > 1)
-        {
-          Cache.log.debug("Added many Db Sources for :" + src.getTitle());
-        }
-      }
-    }
+    List<DbSourceProxy> selsources = new ArrayList<>();
     // select appropriate databases based on alignFrame context.
     if (forNucleotide)
     {
@@ -190,7 +172,7 @@ public class DBRefFetcher implements Runnable
     {
       defdb = DBRefSource.PROTEINDBS;
     }
-    List<DbSourceProxy> srces = new ArrayList<DbSourceProxy>();
+    List<DbSourceProxy> srces = new ArrayList<>();
     for (String ddb : defdb)
     {
       List<DbSourceProxy> srcesfordb = sfetcher.getSourceProxy(ddb);
@@ -234,30 +216,6 @@ public class DBRefFetcher implements Runnable
   }
 
   /**
-   * retrieve all the das sequence sources and add them to the list of db
-   * sources to retrieve from
-   */
-  public void appendAllDasSources()
-  {
-    if (dbSources == null)
-    {
-      dbSources = new DbSourceProxy[0];
-    }
-    // append additional sources
-    DbSourceProxy[] otherdb = sfetcher
-            .getDbSourceProxyInstances(DasSequenceSource.class);
-    if (otherdb != null && otherdb.length > 0)
-    {
-      DbSourceProxy[] newsrc = new DbSourceProxy[dbSources.length
-              + otherdb.length];
-      System.arraycopy(dbSources, 0, newsrc, 0, dbSources.length);
-      System.arraycopy(otherdb, 0, newsrc, dbSources.length,
-              otherdb.length);
-      dbSources = newsrc;
-    }
-  }
-
-  /**
    * start the fetcher thread
    * 
    * @param waitTillFinished
@@ -310,14 +268,14 @@ public class DBRefFetcher implements Runnable
       }
       else if (seqs == null)
       {
-        seqs = new Vector<SequenceI>();
+        seqs = new Vector<>();
         seqs.addElement(seq);
       }
 
     }
     else
     {
-      seqs = new Vector<SequenceI>();
+      seqs = new Vector<>();
       seqs.addElement(seq);
     }
 
@@ -356,9 +314,9 @@ public class DBRefFetcher implements Runnable
       e.printStackTrace();
     }
 
-    Vector<SequenceI> sdataset = new Vector<SequenceI>(
+    Vector<SequenceI> sdataset = new Vector<>(
             Arrays.asList(dataset));
-    List<String> warningMessages = new ArrayList<String>();
+    List<String> warningMessages = new ArrayList<>();
 
     int db = 0;
     while (sdataset.size() > 0 && db < dbSources.length)
@@ -370,8 +328,8 @@ public class DBRefFetcher implements Runnable
       SequenceI[] currSeqs = new SequenceI[sdataset.size()];
       sdataset.copyInto(currSeqs);// seqs that are to be validated against
       // dbSources[db]
-      Vector<String> queries = new Vector<String>(); // generated queries curSeq
-      seqRefs = new Hashtable<String, Vector<SequenceI>>();
+      Vector<String> queries = new Vector<>(); // generated queries curSeq
+      seqRefs = new Hashtable<>();
 
       int seqIndex = 0;
 
@@ -573,7 +531,7 @@ public class DBRefFetcher implements Runnable
     {
       // Work out which sequences this sequence matches,
       // taking into account all accessionIds and names in the file
-      Vector<SequenceI> sequenceMatches = new Vector<SequenceI>();
+      Vector<SequenceI> sequenceMatches = new Vector<>();
       // look for corresponding accession ids
       DBRefEntry[] entryRefs = DBRefUtils
               .selectRefs(retrievedSeq.getDBRefs(), new String[]
@@ -706,28 +664,14 @@ public class DBRefFetcher implements Runnable
 
           if (updateRefFrame)
           {
-            SequenceFeature[] sfs = sequence.getSequenceFeatures();
-            if (sfs != null)
+            /*
+             * relocate existing sequence features by offset
+             */
+            int startShift = absStart - sequenceStart + 1;
+            if (startShift != 0)
             {
-              /*
-               * relocate existing sequence features by offset
-               */
-              int start = sequenceStart;
-              int end = sequence.getEnd();
-              int startShift = 1 - absStart - start;
-
-              if (startShift != 0)
-              {
-                for (SequenceFeature sf : sfs)
-                {
-                  if (sf.getBegin() >= start && sf.getEnd() <= end)
-                  {
-                    sf.setBegin(sf.getBegin() + startShift);
-                    sf.setEnd(sf.getEnd() + startShift);
-                    modified = true;
-                  }
-                }
-              }
+              modified |= sequence.getFeatures().shiftFeatures(1,
+                      startShift);
             }
           }
         }
@@ -840,7 +784,7 @@ public class DBRefFetcher implements Runnable
    */
   private SequenceI[] recoverDbSequences(SequenceI[] sequencesArray)
   {
-    Vector<SequenceI> nseq = new Vector<SequenceI>();
+    Vector<SequenceI> nseq = new Vector<>();
     for (int i = 0; sequencesArray != null
             && i < sequencesArray.length; i++)
     {
diff --git a/src/jalview/ws/DasSequenceFeatureFetcher.java b/src/jalview/ws/DasSequenceFeatureFetcher.java
deleted file mode 100644 (file)
index 0a61dff..0000000
+++ /dev/null
@@ -1,925 +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.ws;
-
-import jalview.bin.Cache;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.DBRefSource;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.gui.AlignFrame;
-import jalview.gui.Desktop;
-import jalview.gui.FeatureSettings;
-import jalview.gui.JvOptionPane;
-import jalview.util.DBRefUtils;
-import jalview.util.MessageManager;
-import jalview.util.UrlLink;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
-import org.biodas.jdas.client.FeaturesClient;
-import org.biodas.jdas.client.adapters.features.DasGFFAdapter;
-import org.biodas.jdas.client.adapters.features.DasGFFAdapter.GFFAdapter;
-import org.biodas.jdas.client.threads.FeaturesClientMultipleSources;
-import org.biodas.jdas.schema.features.ERRORSEGMENT;
-import org.biodas.jdas.schema.features.FEATURE;
-import org.biodas.jdas.schema.features.LINK;
-import org.biodas.jdas.schema.features.SEGMENT;
-import org.biodas.jdas.schema.features.TYPE;
-import org.biodas.jdas.schema.features.UNKNOWNFEATURE;
-import org.biodas.jdas.schema.features.UNKNOWNSEGMENT;
-import org.biodas.jdas.schema.sources.COORDINATES;
-
-/**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
- */
-public class DasSequenceFeatureFetcher
-{
-  SequenceI[] sequences;
-
-  AlignFrame af;
-
-  FeatureSettings fsettings;
-
-  StringBuffer sbuffer = new StringBuffer();
-
-  List<jalviewSourceI> selectedSources;
-
-  boolean cancelled = false;
-
-  private void debug(String mesg)
-  {
-    debug(mesg, null);
-  }
-
-  private void debug(String mesg, Exception e)
-  {
-    if (Cache.log != null)
-    {
-      Cache.log.debug(mesg, e);
-    }
-    else
-    {
-      System.err.println(mesg);
-      if (e != null)
-      {
-        e.printStackTrace();
-      }
-    }
-  }
-
-  long startTime;
-
-  private DasSourceRegistryI sourceRegistry;
-
-  private boolean useJDASMultiThread = true;
-
-  /**
-   * Creates a new SequenceFeatureFetcher object. Uses default
-   * 
-   * @param align
-   *          DOCUMENT ME!
-   * @param ap
-   *          DOCUMENT ME!
-   */
-  public DasSequenceFeatureFetcher(SequenceI[] sequences,
-          FeatureSettings fsettings, Vector selectedSources)
-  {
-    this(sequences, fsettings, selectedSources, true, true, true);
-  }
-
-  public DasSequenceFeatureFetcher(SequenceI[] oursequences,
-          FeatureSettings fsettings, List<jalviewSourceI> selectedSources2,
-          boolean checkDbrefs, boolean promptFetchDbrefs)
-  {
-    this(oursequences, fsettings, selectedSources2, checkDbrefs,
-            promptFetchDbrefs, true);
-  }
-
-  public DasSequenceFeatureFetcher(SequenceI[] oursequences,
-          FeatureSettings fsettings, List<jalviewSourceI> selectedSources2,
-          boolean checkDbrefs, boolean promptFetchDbrefs,
-          boolean useJDasMultiThread)
-  {
-    this.useJDASMultiThread = useJDasMultiThread;
-    this.selectedSources = new ArrayList<jalviewSourceI>();
-    // filter both sequences and sources to eliminate duplicates
-    for (jalviewSourceI src : selectedSources2)
-    {
-      if (!selectedSources.contains(src))
-      {
-        selectedSources.add(src);
-      }
-      ;
-    }
-    Vector sqs = new Vector();
-    for (int i = 0; i < oursequences.length; i++)
-    {
-      if (!sqs.contains(oursequences[i]))
-      {
-        sqs.addElement(oursequences[i]);
-      }
-    }
-    sequences = new SequenceI[sqs.size()];
-    for (int i = 0; i < sequences.length; i++)
-    {
-      sequences[i] = (SequenceI) sqs.elementAt(i);
-    }
-    if (fsettings != null)
-    {
-      this.fsettings = fsettings;
-      this.af = fsettings.af;
-      af.setShowSeqFeatures(true);
-    }
-    int uniprotCount = 0;
-    for (jalviewSourceI source : selectedSources)
-    {
-      for (COORDINATES coords : source.getVersion().getCOORDINATES())
-      {
-        // TODO: match UniProt coord system canonically (?) - does
-        // UniProt==uniprot==UNIPROT ?
-        if (coords.getAuthority().toLowerCase().equals("uniprot"))
-        {
-          uniprotCount++;
-          break;
-        }
-      }
-    }
-
-    int refCount = 0;
-    for (int i = 0; i < sequences.length; i++)
-    {
-      DBRefEntry[] dbref = sequences[i].getDBRefs();
-      if (dbref != null)
-      {
-        for (int j = 0; j < dbref.length; j++)
-        {
-          if (dbref[j].getSource().equals(DBRefSource.UNIPROT))
-          {
-            refCount++;
-            break;
-          }
-        }
-      }
-    }
-
-    if (checkDbrefs && refCount < sequences.length && uniprotCount > 0)
-    {
-
-      int reply = JvOptionPane.YES_OPTION;
-      if (promptFetchDbrefs)
-      {
-        reply = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-                MessageManager.getString(
-                        "info.you_want_jalview_to_find_uniprot_accessions"),
-                MessageManager
-                        .getString("label.find_uniprot_accession_ids"),
-                JvOptionPane.YES_NO_OPTION, JvOptionPane.QUESTION_MESSAGE);
-      }
-
-      if (reply == JvOptionPane.YES_OPTION)
-      {
-        Thread thread = new Thread(new FetchDBRefs());
-        thread.start();
-      }
-      else
-      {
-        _startFetching();
-      }
-    }
-    else
-    {
-      _startFetching();
-    }
-
-  }
-
-  private void _startFetching()
-  {
-    running = true;
-    new Thread(new FetchSeqFeatures()).start();
-  }
-
-  class FetchSeqFeatures implements Runnable
-  {
-    @Override
-    public void run()
-    {
-      startFetching();
-      setGuiFetchComplete();
-    }
-  }
-
-  class FetchDBRefs implements Runnable
-  {
-    @Override
-    public void run()
-    {
-      running = true;
-      boolean isNucleotide = af.getViewport().getAlignment().isNucleotide();
-      new DBRefFetcher(sequences, af, null, af.featureSettings,
-              isNucleotide).fetchDBRefs(true);
-
-      startFetching();
-      setGuiFetchComplete();
-    }
-  }
-
-  /**
-   * Spawns Fetcher threads to add features to sequences in the dataset
-   */
-  void startFetching()
-  {
-    running = true;
-    cancelled = false;
-    startTime = System.currentTimeMillis();
-    if (af != null)
-    {
-      af.setProgressBar(MessageManager.getString(
-              "status.fetching_das_sequence_features"), startTime);
-    }
-    if (sourceRegistry == null)
-    {
-      sourceRegistry = Cache.getDasSourceRegistry();
-    }
-    if (selectedSources == null || selectedSources.size() == 0)
-    {
-      try
-      {
-        jalviewSourceI[] sources = sourceRegistry.getSources()
-                .toArray(new jalviewSourceI[0]);
-        String active = Cache.getDefault("DAS_ACTIVE_SOURCE", "uniprot");
-        StringTokenizer st = new StringTokenizer(active, "\t");
-        selectedSources = new Vector();
-        String token;
-        while (st.hasMoreTokens())
-        {
-          token = st.nextToken();
-          for (int i = 0; i < sources.length; i++)
-          {
-            if (sources[i].getTitle().equals(token))
-            {
-              selectedSources.add(sources[i]);
-              break;
-            }
-          }
-        }
-      } catch (Exception ex)
-      {
-        debug("Exception whilst setting default feature sources from registry and local preferences.",
-                ex);
-      }
-    }
-
-    if (selectedSources == null || selectedSources.size() == 0)
-    {
-      System.out.println("No DAS Sources active");
-      cancelled = true;
-      setGuiNoDassourceActive();
-      return;
-    }
-
-    sourcesRemaining = selectedSources.size();
-    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>>();
-    for (jalviewSourceI _sr : selectedSources)
-    {
-
-      Map<String, SequenceI> slist = new HashMap<String, SequenceI>();
-      List<DBRefEntry> idob = new ArrayList<DBRefEntry>();
-      List<String> qset = new ArrayList<String>();
-
-      for (SequenceI seq : sequences)
-      {
-        Object[] idset = nextSequence(_sr, seq);
-        if (idset != null)
-        {
-          List<DBRefEntry> _idob = (List<DBRefEntry>) idset[0];
-          List<String> _qset = (List<String>) idset[1];
-          if (_idob.size() > 0)
-          {
-            // add sequence's ref for each id derived from it
-            // (space inefficient, but most unambiguous)
-            // could replace with hash with _qset values as keys.
-            Iterator<DBRefEntry> dbobj = _idob.iterator();
-            for (String q : _qset)
-            {
-              SequenceI osq = slist.get(q);
-              DBRefEntry dr = dbobj.next();
-              if (osq != null && osq != seq)
-              {
-                // skip - non-canonical query
-              }
-              else
-              {
-                idob.add(dr);
-                qset.add(q);
-                slist.put(q, seq);
-              }
-            }
-          }
-        }
-      }
-      if (idob.size() > 0)
-      {
-        srcobj.add(_sr);
-        src.add(_sr.getSourceURL());
-        ids.add(qset);
-        idobj.add(idob);
-        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>>();
-    if (!useJDASMultiThread)
-    {
-      Iterator<String> sources = src.iterator();
-      // iterate over each query for each source and do each one individually
-      for (List<String> idl : ids)
-      {
-        String source = sources.next();
-        FeaturesClient featuresc = new FeaturesClient(
-                sourceRegistry.getSessionHandler()
-                        .getConnectionPropertyProviderFor(source));
-        for (String id : idl)
-        {
-          List<String> qid = Arrays.asList(new String[] { id });
-          try
-          {
-            DasGFFAdapter dga = featuresc.fetchData(source, qid);
-            Map<List<String>, DasGFFAdapter> ers = results.get(source);
-            if (ers == null)
-            {
-              results.put(source,
-                      ers = new HashMap<List<String>, DasGFFAdapter>());
-            }
-            ers.put(qid, dga);
-          } catch (Exception ex)
-          {
-            Map<List<String>, Exception> ers = errors.get(source);
-            if (ers == null)
-            {
-              errors.put(source,
-                      ers = new HashMap<List<String>, Exception>());
-            }
-            ers.put(qid, ex);
-          }
-        }
-      }
-    }
-    else
-    {
-      // pass them all at once
-      fc.fetchData(src, ids, false, results, errors);
-      fc.shutDown();
-      while (!fc.isTerminated())
-      {
-        try
-        {
-          Thread.sleep(200);
-        } catch (InterruptedException x)
-        {
-
-        }
-      }
-    }
-    Iterator<List<String>> idset = ids.iterator();
-    Iterator<List<DBRefEntry>> idobjset = idobj.iterator();
-    Iterator<Map<String, SequenceI>> seqset = sqset.iterator();
-    for (jalviewSourceI source : srcobj)
-    {
-      processResponse(seqset.next(), source, idset.next(), idobjset.next(),
-              results.get(source.getSourceURL()),
-              errors.get(source.getSourceURL()));
-    }
-  }
-
-  private void processResponse(Map<String, SequenceI> sequencemap,
-          jalviewSourceI jvsource, List<String> ids, List<DBRefEntry> idobj,
-          Map<List<String>, DasGFFAdapter> results,
-          Map<List<String>, Exception> errors)
-  {
-    Set<SequenceI> sequences = new HashSet<SequenceI>();
-    String source = jvsource.getSourceURL();
-    // process features
-    DasGFFAdapter result = (results == null) ? null : results.get(ids);
-    Exception error = (errors == null) ? null : errors.get(ids);
-    if (result == null)
-    {
-      debug("das source " + source + " could not be contacted. "
-              + (error == null ? "" : error.toString()));
-    }
-    else
-    {
-
-      GFFAdapter gff = result.getGFF();
-      List<SEGMENT> segments = gff.getSegments();
-      List<ERRORSEGMENT> errorsegs = gff.getErrorSegments();
-      List<UNKNOWNFEATURE> unkfeats = gff.getUnknownFeatures();
-      List<UNKNOWNSEGMENT> unksegs = gff.getUnknownSegments();
-      debug("das source " + source + " returned " + gff.getTotal()
-              + " responses. " + (errorsegs != null ? errorsegs.size() : 0)
-              + " were incorrect segment queries, "
-              + (unkfeats != null ? unkfeats.size() : 0)
-              + " were unknown features "
-              + (unksegs != null ? unksegs.size() : 0)
-              + " were unknown segments and "
-              + (segments != null ? segments.size() : 0)
-              + " were segment responses.");
-      Iterator<DBRefEntry> dbr = idobj.iterator();
-      if (segments != null)
-      {
-        for (SEGMENT seg : segments)
-        {
-          String id = seg.getId();
-          if (ids.indexOf(id) == -1)
-          {
-            id = id.toUpperCase();
-          }
-          DBRefEntry dbref = idobj.get(ids.indexOf(id));
-          SequenceI sequence = sequencemap.get(id);
-          boolean added = false;
-          sequences.add(sequence);
-
-          for (FEATURE feat : seg.getFEATURE())
-          {
-            // standard DAS feature-> jalview sequence feature transformation
-            SequenceFeature f = newSequenceFeature(feat,
-                    jvsource.getTitle());
-            if (!parseSeqFeature(sequence, f, feat, jvsource))
-            {
-              if (dbref.getMap() != null && f.getBegin() > 0
-                      && f.getEnd() > 0)
-              {
-                debug("mapping from " + f.getBegin() + " - " + f.getEnd());
-                SequenceFeature vf[] = null;
-
-                try
-                {
-                  vf = dbref.getMap().locateFeature(f);
-                } catch (Exception ex)
-                {
-                  Cache.log.warn(
-                          "Error in 'experimental' mapping of features. Please try to reproduce and then report info to jalview-discuss@jalview.org.");
-                  Cache.log.warn("Mapping feature from " + f.getBegin()
-                          + " to " + f.getEnd() + " in dbref "
-                          + dbref.getAccessionId() + " in "
-                          + dbref.getSource());
-                  Cache.log.warn("using das Source " + source);
-                  Cache.log.warn("Exception", ex);
-                }
-
-                if (vf != null)
-                {
-                  for (int v = 0; v < vf.length; v++)
-                  {
-                    debug("mapping to " + v + ": " + vf[v].getBegin()
-                            + " - " + vf[v].getEnd());
-                    sequence.addSequenceFeature(vf[v]);
-                  }
-                }
-              }
-              else
-              {
-                sequence.addSequenceFeature(f);
-              }
-            }
-          }
-        }
-        featuresAdded(sequences);
-      }
-      else
-      {
-        // System.out.println("No features found for " + seq.getName()
-        // + " from: " + e.getDasSource().getNickname());
-      }
-    }
-  }
-
-  private void setGuiNoDassourceActive()
-  {
-
-    if (af != null)
-    {
-      af.setProgressBar(
-              MessageManager.getString("status.no_das_sources_active"),
-              startTime);
-    }
-    if (getFeatSettings() != null)
-    {
-      fsettings.noDasSourceActive();
-    }
-  }
-
-  /**
-   * Update our fsettings dialog reference if we didn't have one when we were
-   * first initialised.
-   * 
-   * @return fsettings
-   */
-  private FeatureSettings getFeatSettings()
-  {
-    if (fsettings == null)
-    {
-      if (af != null)
-      {
-        fsettings = af.featureSettings;
-      }
-    }
-    return fsettings;
-  }
-
-  public void cancel()
-  {
-    if (af != null)
-    {
-      af.setProgressBar(MessageManager.getString(
-              "status.das_feature_fetching_cancelled"), startTime);
-    }
-    cancelled = true;
-  }
-
-  int sourcesRemaining = 0;
-
-  private boolean running = false;
-
-  private void setGuiFetchComplete()
-  {
-    running = false;
-    if (!cancelled && af != null)
-    {
-      // only update the progress bar if we've completed the fetch normally
-      af.setProgressBar(MessageManager.getString(
-              "status.das_feature_fetching_complete"), startTime);
-    }
-
-    if (af != null && af.featureSettings != null)
-    {
-      af.featureSettings.discoverAllFeatureData();
-    }
-
-    if (getFeatSettings() != null)
-    {
-      fsettings.complete();
-    }
-  }
-
-  void featuresAdded(Set<SequenceI> seqs)
-  {
-    if (af == null)
-    {
-      // no gui to update with features.
-      return;
-    }
-    af.getFeatureRenderer().featuresAdded();
-
-    int start = af.getViewport().getRanges().getStartSeq();
-    int end = af.getViewport().getRanges().getEndSeq();
-    int index;
-    for (index = start; index < end; index++)
-    {
-      for (SequenceI seq : seqs)
-      {
-        if (seq == af.getViewport().getAlignment().getSequenceAt(index)
-                .getDatasetSequence())
-        {
-          af.alignPanel.paintAlignment(true);
-          index = end;
-          break;
-        }
-      }
-    }
-  }
-
-  Object[] nextSequence(jalviewSourceI dasSource, SequenceI seq)
-  {
-    if (cancelled)
-    {
-      return null;
-    }
-    DBRefEntry[] uprefs = DBRefUtils.selectRefs(seq.getDBRefs(),
-            new String[]
-            {
-                // jalview.datamodel.DBRefSource.PDB,
-                DBRefSource.UNIPROT,
-            // jalview.datamodel.DBRefSource.EMBL - not tested on any EMBL coord
-            // sys sources
-            });
-    // 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>();
-    boolean dasCoordSysFound = false;
-
-    if (uprefs != null)
-    {
-      // do any of these ids match the source's coordinate system ?
-      for (int j = 0; !dasCoordSysFound && j < uprefs.length; j++)
-      {
-
-        for (COORDINATES csys : dasSource.getVersion().getCOORDINATES())
-        {
-          if (DBRefUtils.isDasCoordinateSystem(csys.getAuthority(),
-                  uprefs[j]))
-          {
-            debug("Launched fetcher for coordinate system "
-                    + csys.getAuthority());
-            // Will have to pass any mapping information to the fetcher
-            // - the start/end for the DBRefEntry may not be the same as the
-            // sequence's start/end
-
-            System.out.println(
-                    seq.getName() + " " + (seq.getDatasetSequence() == null)
-                            + " " + csys.getUri());
-
-            dasCoordSysFound = true; // break's out of the loop
-            ids.add(uprefs[j]);
-            qstring.add(uprefs[j].getAccessionId());
-          }
-          else
-          {
-            System.out.println("IGNORE " + csys.getAuthority());
-          }
-        }
-      }
-    }
-
-    if (!dasCoordSysFound)
-    {
-      String id = null;
-      // try and use the name as the sequence id
-      if (seq.getName().indexOf("|") > -1)
-      {
-        id = seq.getName().substring(seq.getName().lastIndexOf("|") + 1);
-        if (id.trim().length() < 4)
-        {
-          // hack - we regard a significant ID as being at least 4
-          // non-whitespace characters
-          id = seq.getName().substring(0, seq.getName().lastIndexOf("|"));
-          if (id.indexOf("|") > -1)
-          {
-            id = id.substring(id.lastIndexOf("|") + 1);
-          }
-        }
-      }
-      else
-      {
-        id = seq.getName();
-      }
-      if (id != null)
-      {
-        DBRefEntry dbre = new DBRefEntry();
-        dbre.setAccessionId(id);
-        // Should try to call a general feature fetcher that
-        // queries many sources with name to discover applicable ID references
-        ids.add(dbre);
-        qstring.add(dbre.getAccessionId());
-      }
-    }
-
-    return new Object[] { ids, qstring };
-  }
-
-  /**
-   * examine the given sequence feature to determine if it should actually be
-   * turned into sequence annotation or database cross references rather than a
-   * simple sequence feature.
-   * 
-   * @param seq
-   *          the sequence to annotate
-   * @param f
-   *          the jalview sequence feature generated from the DAS feature
-   * @param map
-   *          the sequence feature attributes
-   * @param source
-   *          the source that emitted the feature
-   * @return true if feature was consumed as another kind of annotation.
-   */
-  protected boolean parseSeqFeature(SequenceI seq, SequenceFeature f,
-          FEATURE feature, jalviewSourceI source)
-  {
-    SequenceI mseq = seq;
-    while (seq.getDatasetSequence() != null)
-    {
-      seq = seq.getDatasetSequence();
-    }
-    if (f.getType() != null)
-    {
-      String type = f.getType();
-      if (type.equalsIgnoreCase("protein_name"))
-      {
-        // parse name onto the alignment sequence or the dataset sequence.
-        if (seq.getDescription() == null
-                || seq.getDescription().trim().length() == 0)
-        {
-          // could look at the note series to pick out the first long name, for
-          // the moment just use the whole description string
-          seq.setDescription(f.getDescription());
-        }
-        if (mseq.getDescription() == null
-                || mseq.getDescription().trim().length() == 0)
-        {
-          // could look at the note series to pick out the first long name, for
-          // the moment just use the whole description string
-          mseq.setDescription(f.getDescription());
-        }
-        return true;
-      }
-      // check if source has biosapiens or other sequence ontology label
-      if (type.equalsIgnoreCase("DBXREF") || type.equalsIgnoreCase("DBREF"))
-      {
-        // try to parse the accession out
-
-        DBRefEntry dbr = new DBRefEntry();
-        dbr.setVersion(source.getTitle());
-        StringTokenizer st = new StringTokenizer(f.getDescription(), ":");
-        if (st.hasMoreTokens())
-        {
-          dbr.setSource(st.nextToken());
-        }
-        if (st.hasMoreTokens())
-        {
-          dbr.setAccessionId(st.nextToken());
-        }
-        seq.addDBRef(dbr);
-
-        if (f.links != null && f.links.size() > 0)
-        {
-          // feature is also appended to enable links to be seen.
-          // TODO: consider extending dbrefs to have their own links ?
-          // TODO: new feature: extract dbref links from DAS servers and add the
-          // URL pattern to the list of DB name associated links in the user's
-          // preferences ?
-          // for the moment - just fix up the existing feature so it displays
-          // correctly.
-          // f.setType(dbr.getSource());
-          // f.setDescription();
-          f.setValue("linkonly", Boolean.TRUE);
-          // f.setDescription("");
-          Vector newlinks = new Vector();
-          Enumeration it = f.links.elements();
-          while (it.hasMoreElements())
-          {
-            String elm;
-            UrlLink urllink = new UrlLink(elm = (String) it.nextElement());
-            if (urllink.isValid())
-            {
-              urllink.setLabel(f.getDescription());
-              newlinks.addElement(urllink.toString());
-            }
-            else
-            {
-              // couldn't parse the link properly. Keep it anyway - just in
-              // case.
-              debug("couldn't parse link string - " + elm);
-              newlinks.addElement(elm);
-            }
-          }
-          f.links = newlinks;
-          seq.addSequenceFeature(f);
-        }
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * creates a jalview sequence feature from a das feature document
-   * 
-   * @param feat
-   * @return sequence feature object created using dasfeature information
-   */
-  SequenceFeature newSequenceFeature(FEATURE feat, String nickname)
-  {
-    if (feat == null)
-    {
-      return null;
-    }
-    try
-    {
-      /**
-       * Different qNames for a DAS Feature - are string keys to the HashMaps in
-       * features "METHOD") || qName.equals("TYPE") || qName.equals("START") ||
-       * qName.equals("END") || qName.equals("NOTE") || qName.equals("LINK") ||
-       * qName.equals("SCORE")
-       */
-      String desc = new String();
-      if (feat.getNOTE() != null)
-      {
-        for (String note : feat.getNOTE())
-        {
-          desc += note;
-        }
-      }
-
-      int start = 0, end = 0;
-      float score = 0f;
-
-      try
-      {
-        start = Integer.parseInt(feat.getSTART().toString());
-      } catch (Exception ex)
-      {
-      }
-      try
-      {
-        end = Integer.parseInt(feat.getEND().toString());
-      } catch (Exception ex)
-      {
-      }
-      try
-      {
-        Object scr = feat.getSCORE();
-        if (scr != null)
-        {
-          score = (float) Double.parseDouble(scr.toString());
-
-        }
-      } catch (Exception ex)
-      {
-      }
-
-      SequenceFeature f = new SequenceFeature(getTypeString(feat.getTYPE()),
-              desc, start, end, score, nickname);
-
-      if (feat.getLINK() != null)
-      {
-        for (LINK link : feat.getLINK())
-        {
-          // Do not put feature extent in link text for non-positional features
-          if (f.begin == 0 && f.end == 0)
-          {
-            f.addLink(f.getType() + " " + link.getContent() + "|"
-                    + link.getHref());
-          }
-          else
-          {
-            f.addLink(f.getType() + " " + f.begin + "_" + f.end + " "
-                    + link.getContent() + "|" + link.getHref());
-          }
-        }
-      }
-
-      return f;
-    } catch (Exception e)
-    {
-      System.out.println("ERRR " + e);
-      e.printStackTrace();
-      System.out.println("############");
-      debug("Failed to parse " + feat.toString(), e);
-      return null;
-    }
-  }
-
-  private String getTypeString(TYPE type)
-  {
-    return type.getContent();
-  }
-
-  public boolean isRunning()
-  {
-    return running;
-  }
-
-}
index a0b77de..29d4ec7 100644 (file)
@@ -29,12 +29,10 @@ import jalview.ws.dbsources.PfamFull;
 import jalview.ws.dbsources.PfamSeed;
 import jalview.ws.dbsources.RfamSeed;
 import jalview.ws.dbsources.Uniprot;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
 import jalview.ws.seqfetcher.ASequenceFetcher;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
 import java.util.ArrayList;
-import java.util.List;
 
 /**
  * This implements the run-time discovery of sequence database clients.
@@ -50,11 +48,6 @@ public class SequenceFetcher extends ASequenceFetcher
    */
   public SequenceFetcher()
   {
-    this(true);
-  }
-
-  public SequenceFetcher(boolean addDas)
-  {
     addDBRefSourceImpl(EnsemblGene.class);
     addDBRefSourceImpl(EnsemblGenomes.class);
     addDBRefSourceImpl(EmblSource.class);
@@ -64,26 +57,19 @@ public class SequenceFetcher extends ASequenceFetcher
     addDBRefSourceImpl(PfamFull.class);
     addDBRefSourceImpl(PfamSeed.class);
     addDBRefSourceImpl(RfamSeed.class);
-
-    if (addDas)
-    {
-      registerDasSequenceSources();
-    }
   }
 
   /**
-   * return an ordered list of database sources where non-das database classes
-   * appear before das database classes
+   * return an ordered list of database sources excluding alignment only databases
    */
   public String[] getOrderedSupportedSources()
   {
     String[] srcs = this.getSupportedDb();
-    ArrayList<String> dassrc = new ArrayList<String>(),
-            nondas = new ArrayList<String>();
+    ArrayList<String> src = new ArrayList<>();
+
     for (int i = 0; i < srcs.length; i++)
     {
-      boolean das = false, skip = false;
-      String nm;
+      boolean skip = false;
       for (DbSourceProxy dbs : getSourceProxy(srcs[i]))
       {
         // Skip the alignment databases for the moment - they're not useful for
@@ -92,86 +78,28 @@ public class SequenceFetcher extends ASequenceFetcher
         {
           skip = true;
         }
-        else
-        {
-          nm = dbs.getDbName();
-          if (getSourceProxy(
-                  srcs[i]) instanceof jalview.ws.dbsources.das.datamodel.DasSequenceSource)
-          {
-            if (nm.startsWith("das:"))
-            {
-              nm = nm.substring(4);
-              das = true;
-            }
-            break;
-          }
-        }
       }
       if (skip)
       {
         continue;
       }
-      if (das)
       {
-        dassrc.add(srcs[i]);
-      }
-      else
-      {
-        nondas.add(srcs[i]);
+        src.add(srcs[i]);
       }
     }
-    String[] tosort = nondas.toArray(new String[0]),
-            sorted = nondas.toArray(new String[0]);
+    String[] tosort = src.toArray(new String[0]),
+            sorted = src.toArray(new String[0]);
     for (int j = 0, jSize = sorted.length; j < jSize; j++)
     {
       tosort[j] = tosort[j].toLowerCase();
     }
     jalview.util.QuickSort.sort(tosort, sorted);
     // construct array with all sources listed
-
-    srcs = new String[sorted.length + dassrc.size()];
     int i = 0;
     for (int j = sorted.length - 1; j >= 0; j--, i++)
     {
       srcs[i] = sorted[j];
-      sorted[j] = null;
-    }
-
-    sorted = dassrc.toArray(new String[0]);
-    tosort = dassrc.toArray(new String[0]);
-    for (int j = 0, jSize = sorted.length; j < jSize; j++)
-    {
-      tosort[j] = tosort[j].toLowerCase();
-    }
-    jalview.util.QuickSort.sort(tosort, sorted);
-    for (int j = sorted.length - 1; j >= 0; j--, i++)
-    {
-      srcs[i] = sorted[j];
     }
     return srcs;
   }
-
-  /**
-   * query the currently defined DAS source registry for sequence sources and
-   * add a DasSequenceSource instance for each source to the SequenceFetcher
-   * source list.
-   */
-  public void registerDasSequenceSources()
-  {
-    // TODO: define a context as a registry provider (either desktop,
-    // jalview.bin.cache, or something else).
-    for (jalviewSourceI source : jalview.bin.Cache.getDasSourceRegistry()
-            .getSources())
-    {
-      if (source.isSequenceSource())
-      {
-        List<DbSourceProxy> dassources = source.getSequenceSourceProxies();
-        for (DbSourceProxy seqsrc : dassources)
-        {
-          addDbRefSourceImpl(seqsrc);
-        }
-      }
-    }
-  }
-
 }
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 6c26f45..167cd97 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;
@@ -28,8 +29,11 @@ import jalview.datamodel.PDBEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.UniprotEntry;
-import jalview.datamodel.UniprotFile;
+import jalview.datamodel.xdb.uniprot.UniprotEntry;
+import jalview.datamodel.xdb.uniprot.UniprotFeature;
+import jalview.datamodel.xdb.uniprot.UniprotFile;
+import jalview.schemes.ResidueProperties;
+import jalview.util.StringUtils;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
 
 import java.io.InputStream;
@@ -38,6 +42,7 @@ import java.io.Reader;
 import java.net.URL;
 import java.net.URLConnection;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Vector;
 
 import org.exolab.castor.mapping.Mapping;
@@ -51,6 +56,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 = "|";
 
   /*
@@ -66,6 +73,11 @@ public class Uniprot extends DbSourceProxyImpl
     super();
   }
 
+  private String getDomain()
+  {
+    return Cache.getDefault("UNIPROT_DOMAIN", DEFAULT_UNIPROT_DOMAIN);
+  }
+
   /*
    * (non-Javadoc)
    * 
@@ -162,7 +174,7 @@ public class Uniprot extends DbSourceProxyImpl
               "(UNIPROT\\|?|UNIPROT_|UNIREF\\d+_|UNIREF\\d+\\|?)", "");
       AlignmentI al = null;
 
-      String downloadstring = "http://www.uniprot.org/uniprot/" + queries
+      String downloadstring = getDomain() + "/uniprot/" + queries
               + ".xml";
       URL url = null;
       URLConnection urlconn = null;
@@ -261,16 +273,17 @@ public class Uniprot extends DbSourceProxyImpl
 
         }
       }
-
     }
 
     sequence.setPDBId(onlyPdbEntries);
     if (entry.getFeature() != null)
     {
-      for (SequenceFeature sf : entry.getFeature())
+      for (UniprotFeature uf : entry.getFeature())
       {
-        sf.setFeatureGroup("Uniprot");
-        sequence.addSequenceFeature(sf);
+        SequenceFeature copy = new SequenceFeature(uf.getType(),
+                getDescription(uf), uf.getBegin(), uf.getEnd(), "Uniprot");
+        copy.setStatus(uf.getStatus());
+        sequence.addSequenceFeature(copy);
       }
     }
     for (DBRefEntry dbr : dbRefs)
@@ -281,6 +294,94 @@ public class Uniprot extends DbSourceProxyImpl
   }
 
   /**
+   * Constructs a feature description from the description and (optionally)
+   * original and variant fields of the Uniprot XML feature
+   * 
+   * @param uf
+   * @return
+   */
+  protected static String getDescription(UniprotFeature uf)
+  {
+    String orig = uf.getOriginal();
+    List<String> variants = uf.getVariation();
+    StringBuilder sb = new StringBuilder();
+
+    /*
+     * append variant in standard format if present
+     * e.g. p.Arg59Lys
+     * multiple variants are split over lines using <br>
+     */
+    boolean asHtml = false;
+    if (orig != null && !orig.isEmpty() && variants != null
+            && !variants.isEmpty())
+    {
+      int p = 0;
+      for (String var : variants)
+      {
+        // TODO proper HGVS nomenclature for delins structural variations
+        // http://varnomen.hgvs.org/recommendations/protein/variant/delins/
+        // for now we are pragmatic - any orig/variant sequence longer than
+        // three characters is shown with single-character notation rather than
+        // three-letter notation
+        sb.append("p.");
+        if (orig.length() < 4)
+        {
+          for (int c = 0, clen = orig.length(); c < clen; c++)
+          {
+            char origchar = orig.charAt(c);
+            String orig3 = ResidueProperties.aa2Triplet.get("" + origchar);
+            sb.append(orig3 == null ? origchar
+                    : StringUtils.toSentenceCase(orig3));
+          }
+        }
+        else
+        {
+          sb.append(orig);
+        }
+
+        sb.append(Integer.toString(uf.getPosition()));
+
+        if (var.length() < 4)
+        {
+          for (int c = 0, clen = var.length(); c < clen; c++)
+          {
+            char varchar = var.charAt(c);
+            String var3 = ResidueProperties.aa2Triplet.get("" + varchar);
+
+            sb.append(var3 != null ? StringUtils.toSentenceCase(var3)
+                    : "" + varchar);
+          }
+        }
+        else
+        {
+          sb.append(var);
+        }
+        if (++p != variants.size())
+        {
+          sb.append("<br/>&nbsp;&nbsp;");
+          asHtml = true;
+        }
+        else
+        {
+          sb.append(" ");
+        }
+      }
+    }
+    String description = uf.getDescription();
+    if (description != null)
+    {
+      sb.append(description);
+    }
+    if (asHtml)
+    {
+      sb.insert(0, "<html>");
+      sb.append("</html>");
+    }
+
+    return sb.toString();
+  }
+
+  /**
    * 
    * @param entry
    *          UniportEntry
@@ -308,23 +409,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 "";
   }
diff --git a/src/jalview/ws/dbsources/das/api/DasSourceRegistryI.java b/src/jalview/ws/dbsources/das/api/DasSourceRegistryI.java
deleted file mode 100644 (file)
index 55c50b2..0000000
+++ /dev/null
@@ -1,57 +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.ws.dbsources.das.api;
-
-import java.util.List;
-
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-
-/**
- * API for a registry that provides datasources that jalview can access
- * 
- * @author jprocter
- * 
- */
-public interface DasSourceRegistryI
-{
-
-  List<jalviewSourceI> getSources();
-
-  String getDasRegistryURL();
-
-  jalviewSourceI getSource(String nickname);
-
-  // TODO: re JAL-424 - introduce form where local source is queried for
-  // metadata, rather than have it all provided by caller.
-  jalviewSourceI createLocalSource(String uri, String name,
-          boolean sequence, boolean features);
-
-  boolean removeLocalSource(jalviewSourceI source);
-
-  void refreshSources();
-
-  String getLocalSourceString();
-
-  List<jalviewSourceI> resolveSourceNicknames(List<String> sources);
-
-  // TODO: refactor to jDAS specific interface
-  MultipleConnectionPropertyProviderI getSessionHandler();
-}
diff --git a/src/jalview/ws/dbsources/das/api/jalviewSourceI.java b/src/jalview/ws/dbsources/das/api/jalviewSourceI.java
deleted file mode 100644 (file)
index bc3c8ea..0000000
+++ /dev/null
@@ -1,87 +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.ws.dbsources.das.api;
-
-import jalview.ws.seqfetcher.DbSourceProxy;
-
-import java.util.List;
-
-import org.biodas.jdas.schema.sources.MAINTAINER;
-import org.biodas.jdas.schema.sources.VERSION;
-
-public interface jalviewSourceI
-{
-
-  String getTitle();
-
-  VERSION getVersion();
-
-  String getDocHref();
-
-  String getDescription();
-
-  String getUri();
-
-  MAINTAINER getMAINTAINER();
-
-  String getEmail();
-
-  boolean isLocal();
-
-  boolean isSequenceSource();
-
-  String[] getCapabilityList(VERSION v);
-
-  String[] getLabelsFor(VERSION v);
-
-  /**
-   * 
-   * @return null if not a sequence source, otherwise a series of database
-   *         sources that can be used to retrieve sequence data for particular
-   *         database coordinate systems
-   */
-  List<DbSourceProxy> getSequenceSourceProxies();
-
-  boolean isFeatureSource();
-
-  /**
-   * returns the base URL for the latest version of a source's DAS endpoint set
-   * 
-   * @return
-   */
-  String getSourceURL();
-
-  /**
-   * test to see if this source's latest version is older than the given source
-   * 
-   * @param jalviewSourceI
-   * @return true if newer than given source
-   */
-  boolean isNewerThan(jalviewSourceI jalviewSourceI);
-
-  /**
-   * test if the source is a reference source for the authority
-   * 
-   * @return
-   */
-  boolean isReferenceSource();
-
-}
diff --git a/src/jalview/ws/dbsources/das/datamodel/DasSequenceSource.java b/src/jalview/ws/dbsources/das/datamodel/DasSequenceSource.java
deleted file mode 100644 (file)
index 84f6d4d..0000000
+++ /dev/null
@@ -1,354 +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.ws.dbsources.das.datamodel;
-
-import jalview.bin.Cache;
-import jalview.datamodel.Alignment;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceI;
-import jalview.util.MessageManager;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-import jalview.ws.seqfetcher.DbSourceProxy;
-import jalview.ws.seqfetcher.DbSourceProxyImpl;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
-import org.biodas.jdas.client.SequenceClient;
-import org.biodas.jdas.client.adapters.sequence.DasSequenceAdapter;
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-import org.biodas.jdas.client.threads.SequenceClientMultipleSources;
-import org.biodas.jdas.schema.sequence.SEQUENCE;
-import org.biodas.jdas.schema.sources.COORDINATES;
-import org.biodas.jdas.schema.sources.SOURCE;
-import org.biodas.jdas.schema.sources.VERSION;
-
-import com.stevesoft.pat.Regex;
-
-/**
- * an instance of this class is created for each unique DAS Sequence source (ie
- * one capable of handling the 'sequence' for a particular MapMaster)
- * 
- * @author JimP
- * 
- */
-public class DasSequenceSource extends DbSourceProxyImpl
-        implements DbSourceProxy
-{
-  private jalviewSourceI jsrc;
-
-  protected SOURCE source = null;
-
-  protected VERSION version = null;
-
-  protected COORDINATES coordsys = null;
-
-  protected String dbname = "DASCS";
-
-  protected String dbrefname = "das:source";
-
-  protected MultipleConnectionPropertyProviderI connprops = null;
-
-  /**
-   * DAS sources are tier 1 - if we have a direct DB connection then we should
-   * prefer it
-   */
-  private int tier = 1;
-
-  /**
-   * create a new DbSource proxy for a DAS 1 source
-   * 
-   * @param dbnbame
-   *          Human Readable Name to use when fetching from this source
-   * @param dbrefname
-   *          DbRefName for DbRefs attached to sequences retrieved from this
-   *          source
-   * @param source
-   *          Das1Source
-   * @param coordsys
-   *          specific coordinate system to use for this source
-   * @throws Exception
-   *           if source is not capable of the 'sequence' command
-   */
-  public DasSequenceSource(String dbname, String dbrefname, SOURCE source,
-          VERSION version, COORDINATES coordsys,
-          MultipleConnectionPropertyProviderI connprops) throws Exception
-  {
-    if (!(jsrc = new JalviewSource(source, connprops, false))
-            .isSequenceSource())
-    {
-      throw new Exception(MessageManager.formatMessage(
-              "exception.das_source_doesnt_support_sequence_command",
-              new String[]
-              { source.getTitle() }));
-    }
-    this.tier = 1 + ((jsrc.isLocal() || jsrc.isReferenceSource()) ? 0 : 1);
-    this.source = source;
-    this.dbname = dbname;
-    this.dbrefname = dbrefname.toUpperCase();
-    if (coordsys != null)
-    {
-      this.coordsys = coordsys;
-    }
-    this.connprops = connprops;
-  }
-
-  public String getAccessionSeparator()
-  {
-    return "\t";
-  }
-
-  public Regex getAccessionValidator()
-  {
-    /** ? * */
-    return Regex.perlCode("m/([^:]+)(:\\d+,\\d+)?/");
-  }
-
-  public String getDbName()
-  {
-    // TODO: map to
-    return dbname + " (DAS)";
-  }
-
-  public String getDbSource()
-  {
-    return dbrefname;
-  }
-
-  public String getDbVersion()
-  {
-    return coordsys != null ? coordsys.getVersion() : "";
-  }
-
-  public AlignmentI getSequenceRecords(String queries) throws Exception
-  {
-    StringTokenizer st = new StringTokenizer(queries, "\t");
-    List<String> toks = new ArrayList<String>(),
-            src = new ArrayList<String>(), acIds = new ArrayList<String>();
-    while (st.hasMoreTokens())
-    {
-      String t;
-      toks.add(t = st.nextToken());
-      acIds.add(t.replaceAll(":[0-9,]+", ""));
-    }
-    src.add(jsrc.getSourceURL());
-    Map<String, Map<List<String>, DasSequenceAdapter>> resultset = new HashMap<String, Map<List<String>, DasSequenceAdapter>>();
-    Map<String, Map<List<String>, Exception>> errors = new HashMap<String, Map<List<String>, Exception>>();
-
-    // First try multiple sources
-    boolean multiple = true, retry = false;
-    do
-    {
-      if (!multiple)
-      {
-        retry = false;
-        // slow, fetch one at a time.
-        for (String sr : src)
-        {
-          System.err.println(
-                  "Retrieving IDs individually from das source: " + sr);
-          org.biodas.jdas.client.SequenceClient sq = new SequenceClient(
-                  connprops.getConnectionPropertyProviderFor(sr));
-          for (String q : toks)
-          {
-            List<String> qset = Arrays.asList(new String[] { q });
-            try
-            {
-              DasSequenceAdapter s = sq.fetchData(sr, qset);
-              Map<List<String>, DasSequenceAdapter> dss = resultset.get(sr);
-              if (dss == null)
-              {
-                resultset.put(sr,
-                        dss = new HashMap<List<String>, DasSequenceAdapter>());
-              }
-              dss.put(qset, s);
-            } catch (Exception x)
-            {
-              Map<List<String>, Exception> ers = errors.get(sr);
-              if (ers == null)
-              {
-                errors.put(sr,
-                        ers = new HashMap<List<String>, Exception>());
-              }
-              ers.put(qset, x);
-            }
-          }
-        }
-      }
-      else
-      {
-        SequenceClientMultipleSources sclient;
-        sclient = new SequenceClientMultipleSources();
-        sclient.fetchData(src, toks, resultset, errors);
-        sclient.shutDown();
-        while (!sclient.isTerminated())
-        {
-          try
-          {
-            Thread.sleep(200);
-
-          } catch (InterruptedException x)
-          {
-          }
-        }
-        if (resultset.isEmpty() && !errors.isEmpty())
-        {
-          retry = true;
-          multiple = false;
-        }
-      }
-    } while (retry);
-
-    if (resultset.isEmpty())
-    {
-      System.err.println("Sequence Query to " + jsrc.getTitle() + " with '"
-              + queries + "' returned no sequences.");
-      return null;
-    }
-    else
-    {
-      Vector<SequenceI> seqs = null;
-      for (Map.Entry<String, Map<List<String>, DasSequenceAdapter>> resset : resultset
-              .entrySet())
-      {
-        for (Map.Entry<List<String>, DasSequenceAdapter> result : resset
-                .getValue().entrySet())
-        {
-          DasSequenceAdapter dasseqresp = result.getValue();
-          List<String> accessions = result.getKey();
-          for (SEQUENCE e : dasseqresp.getSequence())
-          {
-            String lbl = e.getId();
-
-            if (acIds.indexOf(lbl) == -1)
-            {
-              System.err.println(
-                      "Warning - received sequence event for strange accession code ("
-                              + lbl + ")");
-            }
-            else
-            {
-              if (seqs == null)
-              {
-                if (e.getContent().length() == 0)
-                {
-                  System.err.println(
-                          "Empty sequence returned for accession code ("
-                                  + lbl + ") from " + resset.getKey()
-                                  + " (source is " + getDbName());
-                  continue;
-                }
-              }
-              seqs = new java.util.Vector<SequenceI>();
-              // JDAS returns a sequence complete with any newlines and spaces
-              // in the XML
-              Sequence sq = new Sequence(lbl,
-                      e.getContent().replaceAll("\\s+", ""));
-              sq.setStart(e.getStart().intValue());
-              sq.addDBRef(new DBRefEntry(getDbSource(),
-                      getDbVersion() + ":" + e.getVersion(), lbl));
-              seqs.addElement(sq);
-            }
-          }
-        }
-      }
-
-      if (seqs == null || seqs.size() == 0)
-        return null;
-      SequenceI[] sqs = new SequenceI[seqs.size()];
-      for (int i = 0, iSize = seqs.size(); i < iSize; i++)
-      {
-        sqs[i] = (SequenceI) seqs.elementAt(i);
-      }
-      Alignment al = new Alignment(sqs);
-      if (jsrc.isFeatureSource())
-      {
-        java.util.Vector<jalviewSourceI> srcs = new java.util.Vector<jalviewSourceI>();
-        srcs.addElement(jsrc);
-        try
-        {
-          jalview.ws.DasSequenceFeatureFetcher dssf = new jalview.ws.DasSequenceFeatureFetcher(
-                  sqs, null, srcs, false, false, multiple);
-          while (dssf.isRunning())
-          {
-            try
-            {
-              Thread.sleep(200);
-            } catch (InterruptedException x)
-            {
-
-            }
-          }
-
-        } catch (Exception x)
-        {
-          Cache.log.error(
-                  "Couldn't retrieve features for sequence from its source.",
-                  x);
-        }
-      }
-
-      return al;
-    }
-  }
-
-  public String getTestQuery()
-  {
-    return coordsys == null ? "" : coordsys.getTestRange();
-  }
-
-  public boolean isValidReference(String accession)
-  {
-    // TODO try to validate an accession against source
-    // We don't really know how to do this without querying source
-
-    return true;
-  }
-
-  /**
-   * @return the source
-   */
-  public SOURCE getSource()
-  {
-    return source;
-  }
-
-  /**
-   * @return the coordsys
-   */
-  public COORDINATES getCoordsys()
-  {
-    return coordsys;
-  }
-
-  @Override
-  public int getTier()
-  {
-    return tier;
-  }
-}
diff --git a/src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java b/src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java
deleted file mode 100644 (file)
index de7f380..0000000
+++ /dev/null
@@ -1,484 +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.ws.dbsources.das.datamodel;
-
-import jalview.bin.Cache;
-import jalview.ws.dbsources.das.api.DasSourceRegistryI;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.StringTokenizer;
-
-import org.biodas.jdas.client.ConnectionPropertyProviderI;
-import org.biodas.jdas.client.SourcesClient;
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-import org.biodas.jdas.dassources.Capabilities;
-import org.biodas.jdas.schema.sources.CAPABILITY;
-import org.biodas.jdas.schema.sources.SOURCE;
-import org.biodas.jdas.schema.sources.SOURCES;
-import org.biodas.jdas.schema.sources.VERSION;
-
-/**
- *
- */
-public class DasSourceRegistry
-        implements DasSourceRegistryI, MultipleConnectionPropertyProviderI
-{
-  // private org.biodas.jdas.schema.sources.SOURCE[] dasSources = null;
-  private List<jalviewSourceI> dasSources = null;
-
-  private Hashtable<String, jalviewSourceI> sourceNames = null;
-
-  private Hashtable<String, jalviewSourceI> localSources = null;
-
-  // public static String DEFAULT_REGISTRY = "http://www.dasregistry.org/das/";
-  public static String DEFAULT_REGISTRY = "http://www.ebi.ac.uk/das-srv/registry/das/";
-
-  /**
-   * true if thread is running and we are talking to DAS registry service
-   */
-  private boolean loadingDasSources = false;
-
-  public boolean isLoadingDasSources()
-  {
-    return loadingDasSources;
-  }
-
-  @Override
-  public String getDasRegistryURL()
-  {
-    String registry = jalview.bin.Cache.getDefault("DAS_REGISTRY_URL",
-            DEFAULT_REGISTRY);
-
-    if (registry.indexOf("/registry/das1/sources/") > -1)
-    {
-      jalview.bin.Cache.setProperty(jalview.bin.Cache.DAS_REGISTRY_URL,
-              DEFAULT_REGISTRY);
-      registry = DEFAULT_REGISTRY;
-    }
-    if (registry.lastIndexOf("sources.xml") == registry.length() - 11)
-    {
-      // no trailing sources.xml document for registry in JDAS
-      jalview.bin.Cache.setProperty(jalview.bin.Cache.DAS_REGISTRY_URL,
-              registry = registry.substring(0,
-                      registry.lastIndexOf("sources.xml")));
-    }
-    return registry;
-  }
-
-  /**
-   * query the default DAS Source Registry for sources. Uses value of jalview
-   * property DAS_REGISTRY_URL and the DasSourceBrowser.DEFAULT_REGISTRY if that
-   * doesn't exist.
-   * 
-   * @return list of sources
-   */
-  private List<jalviewSourceI> getDASSources()
-  {
-
-    return getDASSources(getDasRegistryURL(), this);
-  }
-
-  /**
-   * query the given URL for DasSources.
-   * 
-   * @param registryURL
-   *          return sources from registryURL
-   */
-  private static List<jalviewSourceI> getDASSources(String registryURL,
-          MultipleConnectionPropertyProviderI registry)
-  {
-    try
-    {
-      URL url = new URL(registryURL);
-      org.biodas.jdas.client.SourcesClientInterface client = new SourcesClient();
-
-      SOURCES sources = client.fetchDataRegistry(registryURL, null, null,
-              null, null, null, null);
-
-      List<SOURCE> dassources = sources.getSOURCE();
-      ArrayList<jalviewSourceI> dsrc = new ArrayList<jalviewSourceI>();
-      HashMap<String, Integer> latests = new HashMap<String, Integer>();
-      Integer latest;
-      for (SOURCE src : dassources)
-      {
-        JalviewSource jsrc = new JalviewSource(src, registry, false);
-        latest = latests.get(jsrc.getSourceURL());
-        if (latest != null)
-        {
-          if (jsrc.isNewerThan(dsrc.get(latest.intValue())))
-          {
-            dsrc.set(latest.intValue(), jsrc);
-          }
-          else
-          {
-            System.out.println(
-                    "Debug: Ignored older source " + jsrc.getTitle());
-          }
-        }
-        else
-        {
-          latests.put(jsrc.getSourceURL(), Integer.valueOf(dsrc.size()));
-          dsrc.add(jsrc);
-        }
-      }
-      return dsrc;
-    } catch (Exception ex)
-    {
-      System.out.println(
-              "DAS1 registry at " + registryURL + " no longer exists");
-      return new ArrayList<jalviewSourceI>();
-    }
-  }
-
-  public void run()
-  {
-    getSources();
-  }
-
-  @Override
-  public List<jalviewSourceI> getSources()
-  {
-    if (dasSources == null)
-    {
-      dasSources = getDASSources();
-    }
-    return appendLocalSources();
-  }
-
-  /**
-   * generate Sources from the local das source list
-   * 
-   */
-  private void addLocalDasSources()
-  {
-    if (localSources == null)
-    {
-      // get local sources from properties and initialise the local source list
-      String local = jalview.bin.Cache.getProperty("DAS_LOCAL_SOURCE");
-
-      if (local != null)
-      {
-        int n = 1;
-        StringTokenizer st = new StringTokenizer(local, "\t");
-        while (st.hasMoreTokens())
-        {
-          String token = st.nextToken();
-          int bar = token.indexOf("|");
-          if (bar == -1)
-          {
-            System.err.println(
-                    "Warning: DAS user local source appears to have no nickname (expected a '|' followed by nickname)\nOffending definition: '"
-                            + token + "'");
-          }
-          String url = token.substring(bar + 1);
-          boolean features = true, sequence = false;
-          if (url.startsWith("sequence:"))
-          {
-            url = url.substring(9);
-            // this source also serves sequences as well as features
-            sequence = true;
-          }
-          try
-          {
-            if (bar > -1)
-            {
-              createLocalSource(url, token.substring(0, bar), sequence,
-                      features);
-            }
-            else
-            {
-              createLocalSource(url, "User Source" + n, sequence, features);
-            }
-          } catch (Exception q)
-          {
-            System.err.println(
-                    "Unexpected exception when creating local source from '"
-                            + token + "'");
-            q.printStackTrace();
-          }
-          n++;
-        }
-      }
-    }
-  }
-
-  private List<jalviewSourceI> appendLocalSources()
-  {
-    List<jalviewSourceI> srclist = new ArrayList<jalviewSourceI>();
-    addLocalDasSources();
-    sourceNames = new Hashtable<String, jalviewSourceI>();
-    if (dasSources != null)
-    {
-      for (jalviewSourceI src : dasSources)
-      {
-        sourceNames.put(src.getTitle(), src);
-        srclist.add(src);
-      }
-    }
-
-    if (localSources == null)
-    {
-      return srclist;
-    }
-    Enumeration en = localSources.keys();
-    while (en.hasMoreElements())
-    {
-      String key = en.nextElement().toString();
-      jalviewSourceI jvsrc = localSources.get(key);
-      sourceNames.put(key, jvsrc);
-      srclist.add(jvsrc);
-    }
-    return srclist;
-  }
-
-  /*
-  * 
-  */
-
-  @Override
-  public jalviewSourceI createLocalSource(String url, String name,
-          boolean sequence, boolean features)
-  {
-    SOURCE local = _createLocalSource(url, name, sequence, features);
-
-    if (localSources == null)
-    {
-      localSources = new Hashtable<String, jalviewSourceI>();
-    }
-    jalviewSourceI src = new JalviewSource(local, this, true);
-    localSources.put(local.getTitle(), src);
-    return src;
-  }
-
-  private SOURCE _createLocalSource(String url, String name,
-          boolean sequence, boolean features)
-  {
-    SOURCE local = new SOURCE();
-
-    local.setUri(url);
-    local.setTitle(name);
-    local.setVERSION(new ArrayList<VERSION>());
-    VERSION v = new VERSION();
-    List<CAPABILITY> cp = new ArrayList<CAPABILITY>();
-    if (sequence)
-    {
-      /*
-       * Could try and synthesize a coordinate system for the source if needbe
-       * COORDINATES coord = new COORDINATES(); coord.setAuthority("NCBI");
-       * coord.setSource("Chromosome"); coord.setTaxid("9606");
-       * coord.setVersion("35"); version.getCOORDINATES().add(coord);
-       */
-      CAPABILITY cap = new CAPABILITY();
-      cap.setType("das1:" + Capabilities.SEQUENCE.getName());
-      cap.setQueryUri(url + "/sequence");
-      cp.add(cap);
-    }
-    if (features)
-    {
-      CAPABILITY cap = new CAPABILITY();
-      cap.setType("das1:" + Capabilities.FEATURES.getName());
-      cap.setQueryUri(url + "/features");
-      cp.add(cap);
-    }
-
-    v.getCAPABILITY().addAll(cp);
-    local.getVERSION().add(v);
-
-    return local;
-  }
-
-  @Override
-  public jalviewSourceI getSource(String nickname)
-  {
-    return sourceNames.get(nickname);
-  }
-
-  @Override
-  public boolean removeLocalSource(jalviewSourceI source)
-  {
-    if (localSources.containsValue(source))
-    {
-      localSources.remove(source.getTitle());
-      sourceNames.remove(source.getTitle());
-      dasSources.remove(source);
-      jalview.bin.Cache.setProperty("DAS_LOCAL_SOURCE",
-              getLocalSourceString());
-
-      return true;
-    }
-    return false;
-  }
-
-  @Override
-  public void refreshSources()
-  {
-    dasSources = null;
-    sourceNames = null;
-    run();
-  }
-
-  @Override
-  public List<jalviewSourceI> resolveSourceNicknames(List<String> sources)
-  {
-    ArrayList<jalviewSourceI> resolved = new ArrayList<jalviewSourceI>();
-    if (sourceNames != null)
-    {
-      for (String src : sources)
-      {
-        jalviewSourceI dsrc = sourceNames.get(src);
-        if (dsrc != null)
-        {
-          resolved.add(dsrc);
-        }
-      }
-    }
-    return resolved;
-  }
-
-  @Override
-  public String getLocalSourceString()
-  {
-    if (localSources != null)
-    {
-      StringBuffer sb = new StringBuffer();
-      Enumeration en = localSources.keys();
-      while (en.hasMoreElements())
-      {
-        String token = en.nextElement().toString();
-        jalviewSourceI srco = localSources.get(token);
-        sb.append(token + "|" + (srco.isSequenceSource() ? "sequence:" : "")
-                + srco.getUri() + "\t");
-      }
-      return sb.toString();
-    }
-    return "";
-  }
-
-  private static final Hashtable<URL, String> authStash;
-  static
-  {
-    authStash = new Hashtable<URL, String>();
-
-    try
-    {
-      // TODO: allow same credentials for https and http
-      authStash.put(
-              new URL("http://www.compbio.dundee.ac.uk/geneweb/das/myseq/"),
-              "Basic SmltOm1pSg==");
-    } catch (MalformedURLException e)
-    {
-      // TODO Auto-generated catch block
-      e.printStackTrace();
-    }
-  }
-
-  @Override
-  public MultipleConnectionPropertyProviderI getSessionHandler()
-  {
-    return this;
-  }
-
-  @Override
-  public ConnectionPropertyProviderI getConnectionPropertyProviderFor(
-          String arg0)
-  {
-
-    final ConnectionPropertyProviderI conprov = new ConnectionPropertyProviderI()
-    {
-      boolean authed = false;
-
-      @Override
-      public void setConnectionProperties(HttpURLConnection connection)
-      {
-        String auth = authStash.get(connection.getURL());
-        if (auth != null && auth.length() > 0)
-        {
-          connection.setRequestProperty("Authorisation", auth);
-          authed = true;
-        }
-        else
-        {
-          authed = false;
-        }
-      }
-
-      @Override
-      public boolean getResponseProperties(HttpURLConnection connection)
-      {
-        String auth = authStash.get(connection.getURL());
-        if (auth != null && auth.length() == 0)
-        {
-          // don't attempt to check if we authed or not - user entered empty
-          // password
-          return false;
-        }
-        if (!authed)
-        {
-          if (auth != null)
-          {
-            // try and pass credentials.
-            return true;
-          }
-          // see if we should try and create a new auth record.
-          String ameth = connection.getHeaderField("X-DAS-AuthMethods");
-          Cache.log.debug("Could authenticate to " + connection.getURL()
-                  + " with : " + ameth);
-          // TODO: search auth string and raise login box - return if auth was
-          // provided
-          return false;
-        }
-        else
-        {
-          // check to see if auth was successful
-          String asuc = connection
-                  .getHeaderField("X-DAS_AuthenticatedUser");
-          if (asuc != null && asuc.trim().length() > 0)
-          {
-            // authentication was successful
-            Cache.log.debug("Authenticated successfully to "
-                    + connection.getURL().toString());
-            return false;
-          }
-          // it wasn't - so we should tell the user it failed and ask if they
-          // want to attempt authentication again.
-          authStash.remove(connection.getURL());
-          // open a new login/password dialog with cancel button
-          // set new authStash content with password and return true
-          return true; //
-          // User cancelled auth - so put empty string in stash to indicate we
-          // don't want to auth with this server.
-          // authStash.put(connection.getURL(), "");
-          // return false;
-        }
-      }
-    };
-    return conprov;
-  }
-
-}
diff --git a/src/jalview/ws/dbsources/das/datamodel/JalviewSource.java b/src/jalview/ws/dbsources/das/datamodel/JalviewSource.java
deleted file mode 100644 (file)
index 07ba027..0000000
+++ /dev/null
@@ -1,384 +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.ws.dbsources.das.datamodel;
-
-import jalview.util.MessageManager;
-import jalview.ws.dbsources.das.api.jalviewSourceI;
-import jalview.ws.seqfetcher.DbSourceProxy;
-
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-
-import org.biodas.jdas.client.threads.MultipleConnectionPropertyProviderI;
-import org.biodas.jdas.dassources.Capabilities;
-import org.biodas.jdas.dassources.utils.DasTimeFormat;
-import org.biodas.jdas.schema.sources.CAPABILITY;
-import org.biodas.jdas.schema.sources.COORDINATES;
-import org.biodas.jdas.schema.sources.MAINTAINER;
-import org.biodas.jdas.schema.sources.PROP;
-import org.biodas.jdas.schema.sources.SOURCE;
-import org.biodas.jdas.schema.sources.VERSION;
-
-public class JalviewSource implements jalviewSourceI
-{
-  SOURCE source;
-
-  MultipleConnectionPropertyProviderI connprov;
-
-  public JalviewSource(SOURCE local2,
-          MultipleConnectionPropertyProviderI connprov, boolean local)
-  {
-    this.connprov = connprov;
-    this.local = local;
-    source = local2;
-  }
-
-  @Override
-  public String getTitle()
-  {
-    return source.getTitle();
-  }
-
-  @Override
-  public VERSION getVersion()
-  {
-
-    return getVersionFor(source);
-  }
-
-  @Override
-  public String getDocHref()
-  {
-    return source.getDocHref();
-  }
-
-  @Override
-  public String getDescription()
-  {
-    return source.getDescription();
-  }
-
-  @Override
-  public String getUri()
-  {
-    return source.getUri();
-  }
-
-  @Override
-  public MAINTAINER getMAINTAINER()
-  {
-    return source.getMAINTAINER();
-  }
-
-  @Override
-  public String getEmail()
-  {
-    return (local) ? null : source.getMAINTAINER().getEmail();
-  }
-
-  boolean local = false;
-
-  @Override
-  public boolean isLocal()
-  {
-    return local;
-  }
-
-  @Override
-  public boolean isSequenceSource()
-  {
-    String seqcap = "das1:" + Capabilities.SEQUENCE.getName();
-    for (String cp : getCapabilityList(getVersionFor(source)))
-    {
-      if (cp.equals(seqcap))
-      {
-        return true;
-
-      }
-    }
-    return false;
-  }
-
-  @Override
-  public boolean isFeatureSource()
-  {
-    String seqcap = "das1:" + Capabilities.FEATURES.getName();
-    for (String cp : getCapabilityList(getVersionFor(source)))
-    {
-      if (cp.equals(seqcap))
-      {
-        return true;
-
-      }
-    }
-    return false;
-  }
-
-  private VERSION getVersionFor(SOURCE ds)
-  {
-    VERSION latest = null;
-    for (VERSION v : ds.getVERSION())
-    {
-      if (latest == null
-              || isLaterThan(latest.getCreated(), v.getCreated()))
-      {
-        // TODO: das 1.6 - should just get the first version - ignore other
-        // versions since not specified how to construct URL from version's URI
-        // + source URI
-        latest = v;
-      }
-    }
-    return latest;
-  }
-
-  /**
-   * compare date strings. null or unparseable dates are assumed to be oldest
-   * 
-   * @param ref
-   * @param newer
-   * @return true iff ref comes before newer
-   */
-  private boolean isLaterThan(String ref, String newer)
-  {
-    Date refdate = null, newdate = null;
-    if (ref != null && ref.trim().length() > 0)
-    {
-      try
-      {
-        refdate = DasTimeFormat.fromDASString(ref.trim());
-
-      } catch (ParseException x)
-      {
-      }
-    }
-    if (newer != null && newer.trim().length() > 0)
-    {
-      try
-      {
-        newdate = DasTimeFormat.fromDASString(newer);
-      } catch (ParseException e)
-      {
-      }
-    }
-    if (refdate != null)
-    {
-      if (newdate != null)
-      {
-        return refdate.before(newdate);
-      }
-      return false;
-    }
-    if (newdate != null)
-    {
-      return true;
-    }
-    // assume first instance of source is newest in list. - TODO: check if
-    // natural ordering of source versions is newest first or oldest first
-    return false;
-  }
-
-  public String[] getLabelsFor(VERSION v)
-  {
-    ArrayList<String> labels = new ArrayList<String>();
-    for (PROP p : v.getPROP())
-    {
-      if (p.getName().equalsIgnoreCase("LABEL"))
-      {
-        labels.add(p.getValue());
-      }
-    }
-    return labels.toArray(new String[0]);
-  }
-
-  private CAPABILITY getCapability(Capabilities capability)
-  {
-    for (CAPABILITY p : getVersion().getCAPABILITY())
-    {
-      if (p.getType().equalsIgnoreCase(capability.getName()) || p.getType()
-              .equalsIgnoreCase("das1:" + capability.getName()))
-      {
-        return p;
-      }
-    }
-    return null;
-  }
-
-  public String[] getCapabilityList(VERSION v)
-  {
-
-    ArrayList<String> labels = new ArrayList<String>();
-    for (CAPABILITY p : v.getCAPABILITY())
-    {
-      // TODO: work out what to do with namespace prefix
-      // does SEQUENCE == das1:SEQUENCE and das2:SEQUENCE ?
-      // for moment, just show all capabilities...
-      if (p.getType().startsWith("das1:"))
-      {
-        labels.add(p.getType());
-      }
-    }
-    return labels.toArray(new String[0]);
-  }
-
-  @Override
-  public List<DbSourceProxy> getSequenceSourceProxies()
-  {
-    if (!isSequenceSource())
-    {
-      return null;
-    }
-    ArrayList<DbSourceProxy> seqsources = new ArrayList<DbSourceProxy>();
-    if (!local)
-    {
-      VERSION v = getVersion();
-      Map<String, COORDINATES> latestc = new Hashtable<String, COORDINATES>();
-      for (COORDINATES cs : v.getCOORDINATES())
-      {
-        COORDINATES ltst = latestc.get(cs.getUri());
-        if (ltst == null || ltst.getVersion() == null
-                || (ltst.getVersion() != null && cs.getVersion() != null
-                        && isLaterThan(ltst.getVersion(), cs.getVersion())))
-        {
-          latestc.put(cs.getUri(), cs);
-        }
-      }
-      for (COORDINATES cs : latestc.values())
-      {
-        DasSequenceSource ds;
-        /*
-         * if (css == null || css.length == 0) { // TODO: query das source
-         * directly to identify coordinate system... or // have to make up a
-         * coordinate system css = new DasCoordinateSystem[] { new
-         * DasCoordinateSystem() }; css[0].setName(d1s.getNickname());
-         * css[0].setUniqueId(d1s.getNickname()); } for (int c = 0; c <
-         * css.length; c++) {
-         */
-        try
-        {
-          seqsources.add(ds = new DasSequenceSource(
-                  getTitle() + " (" + cs.getAuthority() + " "
-                          + cs.getSource()
-                          + (cs.getVersion() != null ? " " + cs.getVersion()
-                                  : "")
-                          + ")",
-                  cs.getAuthority(), source, v, cs, connprov));
-          if (seqsources.size() > 1)
-          {
-            System.err.println("Added another sequence DB source for "
-                    + getTitle() + " (" + ds.getDbName() + ")");
-          }
-        } catch (Exception e)
-        {
-          System.err.println("Ignoring sequence coord system " + cs + " ("
-                  + cs.getContent() + ") for source " + getTitle()
-                  + "- threw exception when constructing fetcher.\n");
-          e.printStackTrace();
-        }
-      }
-    }
-    else
-    {
-      try
-      {
-        seqsources.add(new DasSequenceSource(getTitle(), getTitle(), source,
-                getVersion(), null, connprov));
-      } catch (Exception e)
-      {
-        // TODO Auto-generated catch block
-        e.printStackTrace();
-      }
-
-    }
-    if (seqsources.size() > 1)
-    {
-      // sort by name
-      DbSourceProxy[] tsort = seqsources.toArray(new DasSequenceSource[0]);
-      String[] nm = new String[tsort.length];
-      for (int i = 0; i < nm.length; i++)
-      {
-        nm[i] = tsort[i].getDbName().toLowerCase();
-      }
-      jalview.util.QuickSort.sort(nm, tsort);
-      seqsources.clear();
-      for (DbSourceProxy ssrc : tsort)
-      {
-        seqsources.add(ssrc);
-      }
-    }
-    return seqsources;
-  }
-
-  @Override
-  public String getSourceURL()
-  {
-    try
-    {
-      // kind of dumb, since
-      // org.biodas.jdas.dassources.utils.VersionAdapter.getSourceUriFromQueryUri()
-      // does this,
-      // but this way, we can access non DAS 1.6 compliant sources (which have
-      // to have a URL like <sourcename>/das/ and cause a validation exception)
-
-      for (CAPABILITY cap : getVersion().getCAPABILITY())
-      {
-        String capname = cap.getType()
-                .substring(cap.getType().indexOf(":") + 1);
-        int p = cap.getQueryUri().lastIndexOf(capname);
-        if (p < -1)
-        {
-          throw new Exception(MessageManager.formatMessage(
-                  "exception.invalid_das_source", new String[]
-                  { source.getUri() }));
-        }
-        if (cap.getQueryUri().charAt(p) == '/')
-        {
-          p--;
-        }
-        return cap.getQueryUri().substring(0, p);
-      }
-    } catch (Exception x)
-    {
-      System.err.println("Serious: Couldn't get the URL for source "
-              + source.getTitle());
-      x.printStackTrace();
-    }
-    return null;
-  }
-
-  @Override
-  public boolean isNewerThan(jalviewSourceI other)
-  {
-    return isLaterThan(getVersion().getCreated(),
-            other.getVersion().getCreated());
-  }
-
-  @Override
-  public boolean isReferenceSource()
-  {
-    // TODO check source object for indication that we are the primary for a DAS
-    // coordinate system
-    return false;
-  }
-}
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 fb6ffc4..9a2316c 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.ws.jws2;
 
-import jalview.api.AlignCalcWorkerI;
 import jalview.api.FeatureColourI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
@@ -63,9 +62,9 @@ public class AADisorderClient extends JabawsCalcWorker
   AlignFrame af;
 
   public AADisorderClient(Jws2Instance sh, AlignFrame alignFrame,
-          WsParamSetI preset, List<Argument> paramset)
+          WsParamSetI thePreset, List<Argument> paramset)
   {
-    super(sh, alignFrame, preset, paramset);
+    super(sh, alignFrame, thePreset, paramset);
     af = alignFrame;
     typeName = sh.action;
     methodName = sh.serviceType;
@@ -100,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" });
@@ -123,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 });
@@ -136,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>());
@@ -149,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 });
@@ -157,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 });
@@ -174,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
        */
@@ -239,13 +238,13 @@ public class AADisorderClient extends JabawsCalcWorker
                 }
                 if (vals.hasNext())
                 {
-                  sf = new SequenceFeature(type[0], type[1], base + rn.from,
-                          base + rn.to, val = vals.next().floatValue(),
-                          methodName);
+                  val = vals.next().floatValue();
+                  sf = new SequenceFeature(type[0], type[1],
+                          base + rn.from, base + rn.to, val, methodName);
                 }
                 else
                 {
-                  sf = new SequenceFeature(type[0], type[1], null,
+                  sf = new SequenceFeature(type[0], type[1],
                           base + rn.from, base + rn.to, methodName);
                 }
                 dseq.addSequenceFeature(sf);
@@ -359,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)
         {
@@ -367,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 e03cd64..55ca3ff 100644 (file)
@@ -65,7 +65,7 @@ public class SeqVector extends InputType
       {
         idvector.append(sep);
       }
-      idvector.append(seq.getSequence());
+      idvector.append(seq.getSequenceAsString());
     }
     return new StringBody(idvector.toString());
   }
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 7132939..533c0af 100644 (file)
@@ -36,6 +36,7 @@ import jalview.schemes.TaylorColourScheme;
 import jalview.structure.StructureImportSettings;
 
 import java.awt.Color;
+import java.util.List;
 import java.util.Vector;
 
 import org.testng.annotations.BeforeClass;
@@ -90,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));
@@ -233,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"));
@@ -258,19 +259,19 @@ public class PDBChainTest
     /*
      * check sequence features
      */
-    SequenceFeature[] sfs = c.sequence.getSequenceFeatures();
-    assertEquals(3, sfs.length);
-    assertEquals("RESNUM", sfs[0].type);
-    assertEquals("MET:4 1gaqA", sfs[0].description);
-    assertEquals(4, sfs[0].begin);
-    assertEquals(4, sfs[0].end);
-    assertEquals("RESNUM", sfs[0].type);
-    assertEquals("LYS:5 1gaqA", sfs[1].description);
-    assertEquals(5, sfs[1].begin);
-    assertEquals(5, sfs[1].end);
-    assertEquals("LEU:6 1gaqA", sfs[2].description);
-    assertEquals(6, sfs[2].begin);
-    assertEquals(6, sfs[2].end);
+    List<SequenceFeature> sfs = c.sequence.getSequenceFeatures();
+    assertEquals(3, sfs.size());
+    assertEquals("RESNUM", sfs.get(0).type);
+    assertEquals("MET:4 1gaqA", sfs.get(0).description);
+    assertEquals(4, sfs.get(0).begin);
+    assertEquals(4, sfs.get(0).end);
+    assertEquals("RESNUM", sfs.get(0).type);
+    assertEquals("LYS:5 1gaqA", sfs.get(1).description);
+    assertEquals(5, sfs.get(1).begin);
+    assertEquals(5, sfs.get(1).end);
+    assertEquals("LEU:6 1gaqA", sfs.get(2).description);
+    assertEquals(6, sfs.get(2).begin);
+    assertEquals(6, sfs.get(2).end);
   }
 
   private Atom makeAtom(int resnum, String name, String resname)
@@ -291,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;
@@ -306,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"));
@@ -319,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);
@@ -327,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);
   }
 
   /**
@@ -343,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"));
@@ -374,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"));
@@ -405,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 088611e..3b9be23 100644 (file)
@@ -31,10 +31,9 @@ public class AlignmentSorterTest
     /*
      * sort with no score features does nothing
      */
-    PA.setValue(AlignmentSorter.class, "lastSortByFeatureScore", null);
+    PA.setValue(AlignmentSorter.class, "sortByFeatureCriteria", null);
 
-    AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(),
-            al,
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
             AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(0), seq1);
     assertSame(al.getSequenceAt(1), seq2);
@@ -65,9 +64,9 @@ public class AlignmentSorterTest
      * sort by ascending score, no filter on feature type or group
      * NB sort order for the same feature set (none) gets toggled, so descending
      */
-    PA.setValue(AlignmentSorter.class, "sortByFeatureScoreAscending", true);
-    AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(),
-            al, AlignmentSorter.FEATURE_SCORE);
+    PA.setValue(AlignmentSorter.class, "sortByFeatureAscending", true);
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(3), seq3); // -0.5
     assertSame(al.getSequenceAt(2), seq2); // 2.5
     assertSame(al.getSequenceAt(1), seq1); // 3.0
@@ -76,8 +75,8 @@ public class AlignmentSorterTest
     /*
      * repeat sort toggles order - now ascending
      */
-    AlignmentSorter.sortByFeature((String) null, null, 0, al.getWidth(),
-            al, AlignmentSorter.FEATURE_SCORE);
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(0), seq3); // -0.5
     assertSame(al.getSequenceAt(1), seq2); // 2.5
     assertSame(al.getSequenceAt(2), seq1); // 3.0
@@ -116,7 +115,7 @@ public class AlignmentSorterTest
      */
     // fails because seq1.findPosition(4) returns 4
     // although residue 4 is in column 5! - JAL-2544
-    AlignmentSorter.sortByFeature((String) null, null, 0, 4, al,
+    AlignmentSorter.sortByFeature(null, null, 0, 4, al,
             AlignmentSorter.FEATURE_SCORE);
     assertSame(al.getSequenceAt(0), seq3); // -4
     assertSame(al.getSequenceAt(1), seq1); // 2.0
index bada3ca..a7a7d34 100644 (file)
@@ -34,18 +34,21 @@ 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;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 import jalview.io.AppletFormatAdapter;
 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;
 
@@ -62,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()
@@ -70,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()
   {
@@ -262,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
@@ -506,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);
 
@@ -604,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
@@ -696,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
@@ -773,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
@@ -847,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)
@@ -1043,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:
@@ -1105,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) 
@@ -1179,12 +1217,12 @@ public class AlignmentUtilsTests
     /*
      * check cds2 acquired a variant feature in position 5
      */
-    SequenceFeature[] sfs = cds2Dss.getSequenceFeatures();
+    List<SequenceFeature> sfs = cds2Dss.getSequenceFeatures();
     assertNotNull(sfs);
-    assertEquals(1, sfs.length);
-    assertEquals("variant", sfs[0].type);
-    assertEquals(5, sfs[0].begin);
-    assertEquals(5, sfs[0].end);
+    assertEquals(1, sfs.size());
+    assertEquals("variant", sfs.get(0).type);
+    assertEquals(5, sfs.get(0).begin);
+    assertEquals(5, sfs.get(0).end);
   }
 
   /**
@@ -1489,39 +1527,39 @@ public class AlignmentUtilsTests
      * that partially overlap 5' or 3' (start or end) of target sequence
      */
     AlignmentUtils.transferFeatures(dna, cds, map, null);
-    SequenceFeature[] sfs = cds.getSequenceFeatures();
-    assertEquals(6, sfs.length);
+    List<SequenceFeature> sfs = cds.getSequenceFeatures();
+    assertEquals(6, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    SequenceFeature sf = sfs.get(0);
     assertEquals("type2", sf.getType());
     assertEquals("desc2", sf.getDescription());
     assertEquals(2f, sf.getScore());
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
 
-    sf = sfs[1];
+    sf = sfs.get(1);
     assertEquals("type3", sf.getType());
     assertEquals("desc3", sf.getDescription());
     assertEquals(3f, sf.getScore());
     assertEquals(1, sf.getBegin());
     assertEquals(3, sf.getEnd());
 
-    sf = sfs[2];
+    sf = sfs.get(2);
     assertEquals("type4", sf.getType());
     assertEquals(2, sf.getBegin());
     assertEquals(5, sf.getEnd());
 
-    sf = sfs[3];
+    sf = sfs.get(3);
     assertEquals("type5", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(6, sf.getEnd());
 
-    sf = sfs[4];
+    sf = sfs.get(4);
     assertEquals("type8", sf.getType());
     assertEquals(6, sf.getBegin());
     assertEquals(6, sf.getEnd());
 
-    sf = sfs[5];
+    sf = sfs.get(5);
     assertEquals("type9", sf.getType());
     assertEquals(6, sf.getBegin());
     assertEquals(6, sf.getEnd());
@@ -1551,10 +1589,10 @@ public class AlignmentUtilsTests
 
     // desc4 and desc8 are the 'omit these' varargs
     AlignmentUtils.transferFeatures(dna, cds, map, null, "type4", "type8");
-    SequenceFeature[] sfs = cds.getSequenceFeatures();
-    assertEquals(1, sfs.length);
+    List<SequenceFeature> sfs = cds.getSequenceFeatures();
+    assertEquals(1, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    SequenceFeature sf = sfs.get(0);
     assertEquals("type5", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(6, sf.getEnd());
@@ -1584,10 +1622,10 @@ public class AlignmentUtilsTests
 
     // "type5" is the 'select this type' argument
     AlignmentUtils.transferFeatures(dna, cds, map, "type5");
-    SequenceFeature[] sfs = cds.getSequenceFeatures();
-    assertEquals(1, sfs.length);
+    List<SequenceFeature> sfs = cds.getSequenceFeatures();
+    assertEquals(1, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    SequenceFeature sf = sfs.get(0);
     assertEquals("type5", sf.getType());
     assertEquals(1, sf.getBegin());
     assertEquals(6, sf.getEnd());
@@ -1746,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);
 
@@ -1895,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");
@@ -1984,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
@@ -1999,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;
@@ -2042,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);
 
     /*
@@ -2056,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);
 
     /*
@@ -2066,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);
 
@@ -2074,16 +2127,58 @@ 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
      */
-    SequenceFeature[] sfs = peptide.getSequenceFeatures();
-    assertEquals(5, sfs.length);
+    List<SequenceFeature> sfs = peptide.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertEquals(8, sfs.size());
 
-    SequenceFeature sf = sfs[0];
+    /*
+     * 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("var5", sf.getValue("ID"));
+    assertEquals("Benign", sf.getValue("clinical_significance"));
+    assertEquals("ID=var5;clinical_significance=Benign",
+            sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "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"));
+    assertEquals("ID=var2;clinical_significance=Dodgy", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "p.Lys1Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2",
+            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"));
@@ -2095,56 +2190,79 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
-    sf = sfs[1];
+    // AAA -> TAA -> stop codon
+    sf = sfs.get(3);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
-    assertEquals("p.Lys1Gln", sf.getDescription());
-    assertEquals("var2", sf.getValue("ID"));
-    assertEquals("Dodgy", sf.getValue("clinical_significance"));
-    assertEquals("ID=var2;clinical_significance=Dodgy", sf.getAttributes());
+    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(
-            "p.Lys1Gln var2|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var2",
+            "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3",
             sf.links.get(0));
     assertEquals(dbSnp, sf.getFeatureGroup());
 
-    sf = sfs[2];
+    // AAA -> AAG synonymous
+    sf = sfs.get(4);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
-    assertEquals("p.Lys1Asn", sf.getDescription());
+    assertEquals("synonymous_variant", sf.getType());
+    assertEquals("aaA/aaG", sf.getDescription());
     assertEquals("var4", sf.getValue("ID"));
-    assertEquals("Benign", sf.getValue("clinical_significance"));
-    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
+    assertEquals("None", sf.getValue("clinical_significance"));
+    assertEquals("ID=var4;clinical_significance=None", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
             sf.links.get(0));
-    assertEquals(ensembl, sf.getFeatureGroup());
+    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());
 
-    // var5 generates two distinct protein variant features
-    sf = sfs[3];
+    // 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("p.Pro3His", sf.getDescription());
-    assertEquals("var6", sf.getValue("ID"));
+    assertEquals("nonsynonymous_variant", sf.getType());
+    assertEquals("p.Pro3Arg", sf.getDescription());
+    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.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
 
-    sf = sfs[4];
+    // CCC -> CAC -> P/H
+    sf = sfs.get(7);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
-    assertEquals("p.Pro3Arg", sf.getDescription());
-    assertEquals("var6", sf.getValue("ID"));
+    assertEquals("nonsynonymous_variant", sf.getType());
+    assertEquals("p.Pro3His", sf.getDescription());
+    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.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
   }
@@ -2265,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);
 
     /*
@@ -2297,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);
 
     /*
@@ -2526,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(2, toMap.getToRanges().size());
+    assertEquals(2, toMap.getToRanges().get(0).length);
+    assertEquals(158, toMap.getToRanges().get(0)[0]);
+    assertEquals(164, toMap.getToRanges().get(0)[1]);
+    assertEquals(210, toMap.getToRanges().get(1)[0]);
+    assertEquals(214, toMap.getToRanges().get(1)[1]);
+    // 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..b3c78be 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:
@@ -426,7 +427,7 @@ public class CrossRefTest
      * argument false suppresses adding DAS sources
      * todo: define an interface type SequenceFetcherI and mock that
      */
-    SequenceFetcher mockFetcher = new SequenceFetcher(false)
+    SequenceFetcher mockFetcher = new SequenceFetcher()
     {
       @Override
       public boolean isFetchable(String source)
@@ -504,7 +505,7 @@ public class CrossRefTest
      * argument false suppresses adding DAS sources
      * todo: define an interface type SequenceFetcherI and mock that
      */
-    SequenceFetcher mockFetcher = new SequenceFetcher(false)
+    SequenceFetcher mockFetcher = new SequenceFetcher()
     {
       @Override
       public boolean isFetchable(String source)
@@ -650,7 +651,7 @@ public class CrossRefTest
      * passed in calls to getSequences() - important to verify that
      * duplicate sequence fetches are not requested
      */
-    SequenceFetcher mockFetcher = new SequenceFetcher(false)
+    SequenceFetcher mockFetcher = new SequenceFetcher()
     {
       int call = 0;
 
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 814d2d4..1faf3f2 100644 (file)
@@ -27,9 +27,10 @@ import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
 
 import jalview.analysis.SecStrConsensus.SimpleBP;
+import jalview.datamodel.SequenceFeature;
 import jalview.gui.JvOptionPane;
 
-import java.util.Vector;
+import java.util.List;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -48,7 +49,7 @@ public class RnaTest
   public void testGetSimpleBPs() throws WUSSParseException
   {
     String rna = "([{})]"; // JAL-1081 example
-    Vector<SimpleBP> bps = Rna.getSimpleBPs(rna);
+    List<SimpleBP> bps = Rna.getSimpleBPs(rna);
     assertEquals(3, bps.size());
 
     /*
@@ -313,4 +314,54 @@ public class RnaTest
               .valueOf((char) i) + " "));
     }
   }
+
+  @Test(groups = "Functional")
+  public void testGetHelixMap_oneHelix() throws WUSSParseException
+  {
+    String rna = ".(..[{.<..>}..].)";
+    SequenceFeature[] sfs = Rna.getHelixMap(rna);
+    assertEquals(4, sfs.length);
+
+    /*
+     * pairs are added in the order in which the closing bracket is found
+     * (see testGetSimpleBPs)
+     */
+    assertEquals(7, sfs[0].getBegin());
+    assertEquals(10, sfs[0].getEnd());
+    assertEquals("0", sfs[0].getFeatureGroup());
+    assertEquals(5, sfs[1].getBegin());
+    assertEquals(11, sfs[1].getEnd());
+    assertEquals("0", sfs[1].getFeatureGroup());
+    assertEquals(4, sfs[2].getBegin());
+    assertEquals(14, sfs[2].getEnd());
+    assertEquals("0", sfs[2].getFeatureGroup());
+    assertEquals(1, sfs[3].getBegin());
+    assertEquals(16, sfs[3].getEnd());
+    assertEquals("0", sfs[3].getFeatureGroup());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetHelixMap_twoHelices() throws WUSSParseException
+  {
+    String rna = ".([.)]..{.<}.>";
+    SequenceFeature[] sfs = Rna.getHelixMap(rna);
+    assertEquals(4, sfs.length);
+  
+    /*
+     * pairs are added in the order in which the closing bracket is found
+     * (see testGetSimpleBPs)
+     */
+    assertEquals(1, sfs[0].getBegin());
+    assertEquals(4, sfs[0].getEnd());
+    assertEquals("0", sfs[0].getFeatureGroup());
+    assertEquals(2, sfs[1].getBegin());
+    assertEquals(5, sfs[1].getEnd());
+    assertEquals("0", sfs[1].getFeatureGroup());
+    assertEquals(8, sfs[2].getBegin());
+    assertEquals(11, sfs[2].getEnd());
+    assertEquals("1", sfs[2].getFeatureGroup());
+    assertEquals(10, sfs[3].getBegin());
+    assertEquals(13, sfs[3].getEnd());
+    assertEquals("1", sfs[3].getFeatureGroup());
+  }
 }
index 11cb10c..9839ba0 100644 (file)
@@ -62,26 +62,25 @@ public class SeqsetUtilsTest
     AlignmentI al = new Alignment(sqset);
     al.setDataset(null);
     AlignmentI ds = al.getDataset();
-    SequenceFeature sf1 = new SequenceFeature("f1", "foo", "bleh", 2, 3,
-            "far"), sf2 = new SequenceFeature("f2", "foo", "bleh", 2, 3,
-            "far");
+    SequenceFeature sf1 = new SequenceFeature("f1", "foo", 2, 3, "far");
+    SequenceFeature sf2 = new SequenceFeature("f2", "foo", 2, 3, "far");
     ds.getSequenceAt(0).addSequenceFeature(sf1);
     Hashtable unq = SeqsetUtils.uniquify(sqset, true);
     SequenceI[] sqset2 = new SequenceI[] {
         new Sequence(sqset[0].getName(), sqset[0].getSequenceAsString()),
         new Sequence(sqset[1].getName(), sqset[1].getSequenceAsString()) };
-    Assert.assertTrue(sqset[0].getSequenceFeatures()[0] == sf1);
-    Assert.assertEquals(sqset2[0].getSequenceFeatures(), null);
+    Assert.assertSame(sqset[0].getSequenceFeatures().get(0), sf1);
+    Assert.assertTrue(sqset2[0].getSequenceFeatures().isEmpty());
     ds.getSequenceAt(0).addSequenceFeature(sf2);
-    Assert.assertEquals(sqset[0].getSequenceFeatures().length, 2);
+    Assert.assertEquals(sqset[0].getSequenceFeatures().size(), 2);
     SeqsetUtils.deuniquify(unq, sqset2);
     // explicitly test that original sequence features still exist because they
     // are on the shared dataset sequence
-    Assert.assertEquals(sqset[0].getSequenceFeatures().length, 2);
-    Assert.assertEquals(sqset2[0].getSequenceFeatures().length, 2);
-    Assert.assertTrue(sqset[0].getSequenceFeatures()[0] == sqset2[0]
-            .getSequenceFeatures()[0]);
-    Assert.assertTrue(sqset[0].getSequenceFeatures()[1] == sqset2[0]
-            .getSequenceFeatures()[1]);
+    Assert.assertEquals(sqset[0].getSequenceFeatures().size(), 2);
+    Assert.assertEquals(sqset2[0].getSequenceFeatures().size(), 2);
+    Assert.assertSame(sqset[0].getSequenceFeatures().get(0), sqset2[0]
+            .getSequenceFeatures().get(0));
+    Assert.assertSame(sqset[0].getSequenceFeatures().get(1), sqset2[0]
+            .getSequenceFeatures().get(1));
   }
 }
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 0577fae..16ca70d 100644 (file)
@@ -23,6 +23,7 @@ package jalview.analysis.scoremodels;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
+import jalview.api.analysis.ScoreModelI;
 import jalview.api.analysis.SimilarityParamsI;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
@@ -84,18 +85,18 @@ public class FeatureDistanceModelTest
       SequenceI ds = al.getSequenceAt(i).getDatasetSequence();
       if (sf1[i * 2] > 0)
       {
-        ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", "sf1",
-                sf1[i * 2], sf1[i * 2 + 1], "sf1"));
+        ds.addSequenceFeature(new SequenceFeature("sf1", "sf1", sf1[i * 2],
+                sf1[i * 2 + 1], "sf1"));
       }
       if (sf2[i * 2] > 0)
       {
-        ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", "sf2",
-                sf2[i * 2], sf2[i * 2 + 1], "sf2"));
+        ds.addSequenceFeature(new SequenceFeature("sf2", "sf2", sf2[i * 2],
+                sf2[i * 2 + 1], "sf2"));
       }
       if (sf3[i * 2] > 0)
       {
-        ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", "sf3",
-                sf3[i * 2], sf3[i * 2 + 1], "sf3"));
+        ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", sf3[i * 2],
+                sf3[i * 2 + 1], "sf3"));
       }
     }
     alf.setShowSeqFeatures(true);
@@ -113,12 +114,12 @@ public class FeatureDistanceModelTest
   public void testFeatureScoreModel() throws Exception
   {
     AlignFrame alf = getTestAlignmentFrame();
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
 
-    MatrixI dm = fsm.findDistances(
+    MatrixI dm = sm.findDistances(
             alf.getViewport().getAlignmentView(true),
             SimilarityParams.Jalview);
     assertEquals(dm.getValue(0, 2), 0d,
@@ -133,11 +134,11 @@ public class FeatureDistanceModelTest
     AlignFrame alf = getTestAlignmentFrame();
     // hiding first two columns shouldn't affect the tree
     alf.getViewport().hideColumns(0, 1);
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
-    MatrixI dm = fsm.findDistances(
+    MatrixI dm = sm.findDistances(
             alf.getViewport().getAlignmentView(true),
             SimilarityParams.Jalview);
     assertEquals(dm.getValue(0, 2), 0d,
@@ -153,11 +154,12 @@ public class FeatureDistanceModelTest
     // hide columns and check tree changes
     alf.getViewport().hideColumns(3, 4);
     alf.getViewport().hideColumns(0, 1);
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    // getName() can become static in Java 8
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
-    MatrixI dm = fsm.findDistances(
+    MatrixI dm = sm.findDistances(
             alf.getViewport().getAlignmentView(true),
             SimilarityParams.Jalview);
     assertEquals(
@@ -197,22 +199,22 @@ public class FeatureDistanceModelTest
     Assert.assertEquals(af.getFeatureRenderer().getDisplayedFeatureTypes()
             .size(), 1, "Should be just one feature type displayed");
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 1)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 1)
             .size(), 0);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 2)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 2)
             .size(), 1);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 3)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 3)
             .size(), 0);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 4)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 4)
             .size(), 0);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 5)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 5)
             .size(), 1);
     // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 6)
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 6)
             .size(), 0);
   }
 
@@ -252,13 +254,15 @@ public class FeatureDistanceModelTest
     alf.setShowSeqFeatures(true);
     alf.getFeatureRenderer().findAllFeatures(true);
 
-    FeatureDistanceModel fsm = new FeatureDistanceModel();
-    assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
     alf.selectAllSequenceMenuItem_actionPerformed(null);
 
-    MatrixI distances = fsm.findDistances(alf.getViewport()
-            .getAlignmentView(true), SimilarityParams.Jalview);
+    AlignmentView alignmentView = alf.getViewport()
+            .getAlignmentView(true);
+    MatrixI distances = sm.findDistances(alignmentView,
+            SimilarityParams.Jalview);
     assertEquals(distances.width(), 2);
     assertEquals(distances.height(), 2);
     assertEquals(distances.getValue(0, 0), 0d);
@@ -279,9 +283,10 @@ public class FeatureDistanceModelTest
     AlignViewport viewport = af.getViewport();
     AlignmentView view = viewport.getAlignmentView(false);
 
-    FeatureDistanceModel sm = new FeatureDistanceModel();
-    sm.configureFromAlignmentView(af.alignPanel);
-  
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            af.alignPanel);
+
     /*
      * feature distance model always normalises by region width
      * gap-gap is always included (but scores zero)
index 69a3ef7..3ac8656 100644 (file)
@@ -35,6 +35,9 @@ import org.testng.annotations.BeforeTest;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ScanResult;
+
 public class CommandLineOperations
 {
 
@@ -45,7 +48,7 @@ public class CommandLineOperations
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  private static final int TEST_TIMEOUT = 4500; // Note longer timeout needed on
+  private static final int TEST_TIMEOUT = 9000; // Note longer timeout needed on
                                                 // full test run than on
                                                 // individual tests
 
@@ -55,7 +58,7 @@ public class CommandLineOperations
 
   private static final int MINFILESIZE_BIG = 4096;
 
-  private ArrayList<String> successfulCMDs = new ArrayList<String>();
+  private ArrayList<String> successfulCMDs = new ArrayList<>();
 
   /***
    * from
@@ -113,13 +116,39 @@ public class CommandLineOperations
     }
   }
 
+  private static ClassGraph scanner = null;
+
+  private static String classpath = null;
+
+  public synchronized static String getClassPath()
+  {
+    if (scanner == null)
+    {
+      scanner = new ClassGraph();
+      ScanResult scan = scanner.scan();
+      classpath = scan.getClasspath();
+    }
+    while (classpath == null)
+    {
+      try
+      {
+        Thread.sleep(10);
+      } catch (InterruptedException x)
+      {
+
+      }
+    }
+    return classpath;
+  }
   private Worker jalviewDesktopRunner(boolean withAwt, String cmd,
           int timeout)
   {
+    // Note: JAL-3065 - don't include quotes for lib/* because the arguments are
+    // not expanded by the shell
+    String classpath = getClassPath();
     String _cmd = "java "
             + (withAwt ? "-Djava.awt.headless=true" : "")
-            + " -Djava.ext.dirs=./lib -classpath ./classes jalview.bin.Jalview ";
-    System.out.println("CMD [" + cmd + "]");
+            + " -classpath " + classpath + " jalview.bin.Jalview ";
     Process ls2_proc = null;
     Worker worker = null;
     try
index 3223042..2160657 100644 (file)
@@ -22,17 +22,24 @@ package jalview.commands;
 
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Map;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -45,13 +52,15 @@ import org.testng.annotations.Test;
  */
 public class EditCommandTest
 {
-
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
+  private static Comparator<SequenceFeature> BY_DESCRIPTION = new Comparator<SequenceFeature>()
   {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
+
+    @Override
+    public int compare(SequenceFeature o1, SequenceFeature o2)
+    {
+      return o1.getDescription().compareTo(o2.getDescription());
+    }
+  };
 
   private EditCommand testee;
 
@@ -59,6 +68,22 @@ public class EditCommandTest
 
   private Alignment al;
 
+  /*
+   * compute n(n+1)/2 e.g. 
+   * func(5) = 5 + 4 + 3 + 2 + 1 = 15
+   */
+  private static int func(int i)
+  {
+    return i * (i + 1) / 2;
+  }
+
+  @BeforeClass(alwaysRun = true)
+  public void setUpJvOptionPane()
+  {
+    JvOptionPane.setInteractiveMode(false);
+    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+  }
+
   @BeforeMethod(alwaysRun = true)
   public void setUp()
   {
@@ -128,18 +153,23 @@ public class EditCommandTest
   }
 
   /**
-   * Test a Paste action, where this adds sequences to an alignment.
+   * Test a Paste action, followed by Undo and Redo
    */
   @Test(groups = { "Functional" }, enabled = false)
-  // TODO fix so it works
-  public void testPaste_addToAlignment()
+  public void testPaste_undo_redo()
   {
+    // TODO code this test properly, bearing in mind that:
+    // Paste action requires something on the clipboard (Cut/Copy)
+    // - EditCommand.paste doesn't add sequences to the alignment
+    // ... that is done in AlignFrame.paste()
+    // ... unless as a Redo
+    // ...
+
     SequenceI[] newSeqs = new SequenceI[2];
     newSeqs[0] = new Sequence("newseq0", "ACEFKL");
     newSeqs[1] = new Sequence("newseq1", "JWMPDH");
 
-    Edit ec = testee.new Edit(Action.PASTE, newSeqs, 0, al.getWidth(), al);
-    EditCommand.paste(ec, new AlignmentI[] { al });
+    new EditCommand("Paste", Action.PASTE, newSeqs, 0, al.getWidth(), al);
     assertEquals(6, al.getSequences().size());
     assertEquals("1234567890", seqs[3].getSequenceAsString());
     assertEquals("ACEFKL", seqs[4].getSequenceAsString());
@@ -249,12 +279,123 @@ public class EditCommandTest
   {
     // seem to need a dataset sequence on the edited sequence here
     seqs[1].createDatasetSequence();
-    new EditCommand("", Action.REPLACE, "ZXY", new SequenceI[] { seqs[1] },
+    assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
+    // NB command.number holds end position for a Replace command
+    new EditCommand("", Action.REPLACE, "Z-xY", new SequenceI[] { seqs[1] },
             4, 8, al);
     assertEquals("abcdefghjk", seqs[0].getSequenceAsString());
+    assertEquals("fghjZ-xYopq", seqs[1].getSequenceAsString());
+    assertEquals("fghjZxYopq",
+            seqs[1].getDatasetSequence().getSequenceAsString());
     assertEquals("qrstuvwxyz", seqs[2].getSequenceAsString());
     assertEquals("1234567890", seqs[3].getSequenceAsString());
-    seqs[1] = new Sequence("seq1", "fghjZXYnopq");
+  }
+
+  /**
+   * Test the replace command (used to manually edit a sequence)
+   */
+  @Test(groups = { "Functional" })
+  public void testReplace_withGaps()
+  {
+    SequenceI seq = new Sequence("seq", "ABC--DEF");
+    seq.createDatasetSequence();
+    assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(6, seq.getEnd());
+
+    /*
+     * replace C- with XYZ
+     * NB arg4 = start column of selection for edit (base 0)
+     * arg5 = column after end of selection for edit
+     */
+    EditCommand edit = new EditCommand("", Action.REPLACE, "xyZ",
+            new SequenceI[]
+            { seq }, 2,
+            4, al);
+    assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(8, seq.getEnd());
+    assertEquals("ABxyZDEF",
+            seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, seq.getDatasetSequence().getEnd());
+
+    /*
+     * undo the edit
+     */
+    AlignmentI[] views = new AlignmentI[]
+    { new Alignment(new SequenceI[] { seq }) };
+    edit.undoCommand(views);
+
+    assertEquals("ABC--DEF", seq.getSequenceAsString());
+    assertEquals("ABCDEF", seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(6, seq.getEnd());
+    assertEquals(6, seq.getDatasetSequence().getEnd());
+
+    /*
+     * redo the edit
+     */
+    edit.doCommand(views);
+
+    assertEquals("ABxyZ-DEF", seq.getSequenceAsString());
+    assertEquals(1, seq.getStart());
+    assertEquals(8, seq.getEnd());
+    assertEquals("ABxyZDEF",
+            seq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, seq.getDatasetSequence().getEnd());
+
+  }
+
+  /**
+   * Test replace command when it doesn't cause a sequence edit (see comment in
+   */
+  @Test(groups = { "Functional" })
+  public void testReplaceFirstResiduesWithGaps()
+  {
+    // test replace when gaps are inserted at start. Start/end should change
+    // w.r.t. original edited sequence.
+    SequenceI dsseq = seqs[1].getDatasetSequence();
+    EditCommand edit = new EditCommand("", Action.REPLACE, "----",
+            new SequenceI[]
+            { seqs[1] }, 0, 4, al);
+
+    // trimmed start
+    assertEquals("----klmnopq", seqs[1].getSequenceAsString());
+    // and ds is preserved
+    assertTrue(dsseq == seqs[1].getDatasetSequence());
+    // and it is unchanged
+    assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
+    // and that alignment sequence start has been adjusted
+    assertEquals(5, seqs[1].getStart());
+    assertEquals(11, seqs[1].getEnd());
+
+    AlignmentI[] views = new AlignmentI[] { new Alignment(seqs) };
+    // and undo
+    edit.undoCommand(views);
+
+    // dataset sequence unchanged
+    assertTrue(dsseq == seqs[1].getDatasetSequence());
+    // restore sequence
+    assertEquals("fghjklmnopq", seqs[1].getSequenceAsString());
+    // and start/end numbering also restored
+    assertEquals(1, seqs[1].getStart());
+    assertEquals(11, seqs[1].getEnd());
+
+    // now redo
+    edit.undoCommand(views);
+
+    // and repeat asserts for the original edit
+
+    // trimmed start
+    assertEquals("----klmnopq", seqs[1].getSequenceAsString());
+    // and ds is preserved
+    assertTrue(dsseq == seqs[1].getDatasetSequence());
+    // and it is unchanged
+    assertEquals("fghjklmnopq", dsseq.getSequenceAsString());
+    // and that alignment sequence start has been adjusted
+    assertEquals(5, seqs[1].getStart());
+    assertEquals(11, seqs[1].getEnd());
+
   }
 
   /**
@@ -639,4 +780,384 @@ public class EditCommandTest
     assertEquals(ds2, unwound.get(ds2).getDatasetSequence());
     assertEquals(ds3, unwound.get(ds3).getDatasetSequence());
   }
+
+  /**
+   * Test a cut action's relocation of sequence features
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures()
+  {
+    /*
+     * create sequence features before, after and overlapping
+     * a cut of columns/residues 4-7
+     */
+    SequenceI seq0 = seqs[0]; // abcdefghjk/1-10
+    seq0.addSequenceFeature(new SequenceFeature("before", "", 1, 3, 0f,
+            null));
+    seq0.addSequenceFeature(new SequenceFeature("overlap left", "", 2, 6,
+            0f, null));
+    seq0.addSequenceFeature(new SequenceFeature("internal", "", 5, 6, 0f,
+            null));
+    seq0.addSequenceFeature(new SequenceFeature("overlap right", "", 7, 8,
+            0f, null));
+    seq0.addSequenceFeature(new SequenceFeature("after", "", 8, 10, 0f,
+            null));
+
+    /*
+     * add some contact features
+     */
+    SequenceFeature internalContact = new SequenceFeature("disulphide bond", "", 5,
+            6, 0f, null);
+    seq0.addSequenceFeature(internalContact); // should get deleted
+    SequenceFeature overlapLeftContact = new SequenceFeature(
+            "disulphide bond", "", 2, 6, 0f, null);
+    seq0.addSequenceFeature(overlapLeftContact); // should get deleted
+    SequenceFeature overlapRightContact = new SequenceFeature(
+            "disulphide bond", "", 5, 8, 0f, null);
+    seq0.addSequenceFeature(overlapRightContact); // should get deleted
+    SequenceFeature spanningContact = new SequenceFeature(
+            "disulphide bond", "", 2, 9, 0f, null);
+    seq0.addSequenceFeature(spanningContact); // should get shortened 3'
+
+    /*
+     * cut columns 3-6 (base 0), residues d-g 4-7
+     */
+    Edit ec = testee.new Edit(Action.CUT, seqs, 3, 4, al); // cols 3-6 base 0
+    EditCommand.cut(ec, new AlignmentI[] { al });
+
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sfs, true);
+
+    assertEquals(5, sfs.size()); // features internal to cut were deleted
+    SequenceFeature sf = sfs.get(0);
+    assertEquals("before", sf.getType());
+    assertEquals(1, sf.getBegin());
+    assertEquals(3, sf.getEnd());
+    sf = sfs.get(1);
+    assertEquals("disulphide bond", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(5, sf.getEnd()); // truncated by cut
+    sf = sfs.get(2);
+    assertEquals("overlap left", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(3, sf.getEnd()); // truncated by cut
+    sf = sfs.get(3);
+    assertEquals("after", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(6, sf.getEnd()); // shifted left by cut
+    sf = sfs.get(4);
+    assertEquals("overlap right", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(4, sf.getEnd()); // truncated by cut
+  }
+
+  /**
+   * Test a cut action's relocation of sequence features, with full coverage of
+   * all possible feature and cut locations for a 5-position ungapped sequence
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures_exhaustive()
+  {
+    /*
+     * create a sequence features on each subrange of 1-5
+     */
+    SequenceI seq0 = new Sequence("seq", "ABCDE");
+    int start = 8;
+    int end = 12;
+    seq0.setStart(start);
+    seq0.setEnd(end);
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    alignment.setDataset(null);
+
+    /*
+     * create a new alignment with shared dataset sequence
+     */
+    AlignmentI copy = new Alignment(
+            new SequenceI[]
+            { alignment.getDataset().getSequenceAt(0).deriveSequence() });
+    SequenceI copySeq0 = copy.getSequenceAt(0);
+
+    for (int from = start; from <= end; from++)
+    {
+      for (int to = from; to <= end; to++)
+      {
+        String desc = String.format("%d-%d", from, to);
+        SequenceFeature sf = new SequenceFeature("test", desc, from, to,
+                0f, null);
+        sf.setValue("from", Integer.valueOf(from));
+        sf.setValue("to", Integer.valueOf(to));
+        seq0.addSequenceFeature(sf);
+      }
+    }
+    // sanity check
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(func(5), sfs.size());
+    assertEquals(sfs, copySeq0.getSequenceFeatures());
+    String copySequenceFeatures = copySeq0.getSequenceFeatures().toString();
+
+    /*
+     * now perform all possible cuts of subranges of columns 1-5
+     * and validate the resulting remaining sequence features!
+     */
+    SequenceI[] sqs = new SequenceI[] { seq0 };
+
+    for (int from = 0; from < seq0.getLength(); from++)
+    {
+      for (int to = from; to < seq0.getLength(); to++)
+      {
+        EditCommand ec = new EditCommand("Cut", Action.CUT, sqs, from, (to
+                - from + 1), alignment);
+        final String msg = String.format("Cut %d-%d ", from + 1, to + 1);
+        boolean newDatasetSequence = copySeq0.getDatasetSequence() != seq0
+                .getDatasetSequence();
+
+        verifyCut(seq0, from, to, msg, start);
+
+        /*
+         * verify copy alignment dataset sequence unaffected
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+
+        /*
+         * verify any new dataset sequence was added to the
+         * alignment dataset
+         */
+        assertEquals("Wrong Dataset size after " + msg,
+                newDatasetSequence ? 2 : 1,
+                alignment.getDataset().getHeight());
+
+        /*
+         * undo and verify all restored
+         */
+        AlignmentI[] views = new AlignmentI[] { alignment };
+        ec.undoCommand(views);
+        sfs = seq0.getSequenceFeatures();
+        assertEquals("After undo of " + msg, func(5), sfs.size());
+        verifyUndo(from, to, sfs);
+
+        /*
+         * verify copy alignment dataset sequence still unaffected
+         * and alignment dataset has shrunk (if it was added to)
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+        assertEquals("Wrong Dataset size after Undo of " + msg, 1,
+                alignment.getDataset().getHeight());
+
+        /*
+         * redo and verify
+         */
+        ec.doCommand(views);
+        verifyCut(seq0, from, to, msg, start);
+
+        /*
+         * verify copy alignment dataset sequence unaffected
+         * and any new dataset sequence readded to alignment dataset
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+        assertEquals("Wrong Dataset size after Redo of " + msg,
+                newDatasetSequence ? 2 : 1,
+                alignment.getDataset().getHeight());
+
+        /*
+         * undo ready for next cut
+         */
+        ec.undoCommand(views);
+
+        /*
+         * final verify that copy alignment dataset sequence is still unaffected
+         * and that alignment dataset has shrunk
+         */
+        assertEquals("Original dataset sequence was modified",
+                copySequenceFeatures,
+                copySeq0.getSequenceFeatures().toString());
+        assertEquals("Wrong Dataset size after final Undo of " + msg, 1,
+                alignment.getDataset().getHeight());
+      }
+    }
+  }
+
+  /**
+   * Verify by inspection that the sequence features left on the sequence after
+   * a cut match the expected results. The trick to this is that we can parse
+   * each feature's original start-end positions from its description.
+   * 
+   * @param seq0
+   * @param from
+   * @param to
+   * @param msg
+   * @param seqStart
+   */
+  protected void verifyCut(SequenceI seq0, int from, int to,
+          final String msg, int seqStart)
+  {
+    List<SequenceFeature> sfs;
+    sfs = seq0.getSequenceFeatures();
+
+    Collections.sort(sfs, BY_DESCRIPTION);
+
+    /*
+     * confirm the number of features has reduced by the
+     * number of features within the cut region i.e. by
+     * func(length of cut); exception is a cut at start or end of sequence, 
+     * which retains the original coordinates, dataset sequence 
+     * and all its features
+     */
+    boolean datasetRetained = from == 0 || to == 4;
+    if (datasetRetained)
+    {
+      // dataset and all features retained
+      assertEquals(msg, func(5), sfs.size());
+    }
+    else if (to - from == 4)
+    {
+      // all columns were cut
+      assertTrue(sfs.isEmpty());
+    }
+    else
+    {
+      // failure in checkFeatureRelocation is more informative!
+      assertEquals(msg + "wrong number of features left", func(5)
+              - func(to - from + 1), sfs.size());
+    }
+
+    /*
+     * inspect individual features
+     */
+    for (SequenceFeature sf : sfs)
+    {
+      verifyFeatureRelocation(sf, from + 1, to + 1, !datasetRetained,
+              seqStart);
+    }
+  }
+
+  /**
+   * Check that after Undo, every feature has start/end that match its original
+   * "start" and "end" properties
+   * 
+   * @param from
+   * @param to
+   * @param sfs
+   */
+  protected void verifyUndo(int from, int to, List<SequenceFeature> sfs)
+  {
+    for (SequenceFeature sf : sfs)
+    {
+      final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+      final int oldTo = ((Integer) sf.getValue("to")).intValue();
+      String msg = String.format(
+              "Undo cut of [%d-%d], feature at [%d-%d] ", from + 1, to + 1,
+              oldFrom, oldTo);
+      assertEquals(msg + "start", oldFrom, sf.getBegin());
+      assertEquals(msg + "end", oldTo, sf.getEnd());
+    }
+  }
+
+  /**
+   * Helper method to check a feature has been correctly relocated after a cut
+   * 
+   * @param sf
+   * @param from
+   *          start of cut (first residue cut 1..)
+   * @param to
+   *          end of cut (last residue cut 1..)
+   * @param newDataset
+   * @param seqStart
+   */
+  private void verifyFeatureRelocation(SequenceFeature sf, int from, int to,
+ boolean newDataset, int seqStart)
+  {
+    // TODO handle the gapped sequence case as well
+    int cutSize = to - from + 1;
+    final int oldFrom = ((Integer) sf.getValue("from")).intValue();
+    final int oldTo = ((Integer) sf.getValue("to")).intValue();
+    final int oldFromPosition = oldFrom - seqStart + 1; // 1..
+    final int oldToPosition = oldTo - seqStart + 1; // 1..
+
+    String msg = String.format(
+            "Feature %s relocated to %d-%d after cut of %d-%d",
+            sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
+    if (!newDataset)
+    {
+      // dataset retained with all features unchanged
+      assertEquals("0: " + msg, oldFrom, sf.getBegin());
+      assertEquals("0: " + msg, oldTo, sf.getEnd());
+    }
+    else if (oldToPosition < from)
+    {
+      // before cut region so unchanged
+      assertEquals("1: " + msg, oldFrom, sf.getBegin());
+      assertEquals("2: " + msg, oldTo, sf.getEnd());
+    }
+    else if (oldFromPosition > to)
+    {
+      // follows cut region - shift by size of cut
+      assertEquals("3: " + msg, newDataset ? oldFrom - cutSize : oldFrom,
+              sf.getBegin());
+      assertEquals("4: " + msg, newDataset ? oldTo - cutSize : oldTo,
+              sf.getEnd());
+    }
+    else if (oldFromPosition < from && oldToPosition > to)
+    {
+      // feature encloses cut region - shrink it right
+      assertEquals("5: " + msg, oldFrom, sf.getBegin());
+      assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFromPosition < from)
+    {
+      // feature overlaps left side of cut region - truncated right
+      assertEquals("7: " + msg, from - 1 + seqStart - 1, sf.getEnd());
+    }
+    else if (oldToPosition > to)
+    {
+      // feature overlaps right side of cut region - truncated left
+      assertEquals("8: " + msg, newDataset ? from + seqStart - 1 : to + 1,
+              sf.getBegin());
+      assertEquals("9: " + msg, newDataset ? from + oldTo - to - 1 : oldTo,
+              sf.getEnd());
+    }
+    else
+    {
+      // feature internal to cut - should have been deleted!
+      Assert.fail(msg + " - should have been deleted");
+    }
+  }
+
+  /**
+   * Test a cut action's relocation of sequence features
+   */
+  @Test(groups = { "Functional" })
+  public void testCut_withFeatures5prime()
+  {
+    SequenceI seq0 = new Sequence("seq/8-11", "A-BCC");
+    seq0.createDatasetSequence();
+    assertEquals(8, seq0.getStart());
+    seq0.addSequenceFeature(new SequenceFeature("", "", 10, 11, 0f,
+            null));
+    SequenceI[] seqsArray = new SequenceI[] { seq0 };
+    AlignmentI alignment = new Alignment(seqsArray);
+
+    /*
+     * cut columns of A-B; same dataset sequence is retained, aligned sequence
+     * start becomes 10
+     */
+    Edit ec = testee.new Edit(Action.CUT, seqsArray, 0, 3, alignment);
+    EditCommand.cut(ec, new AlignmentI[] { alignment });
+  
+    /*
+     * feature on CC(10-11) should still be on CC(10-11)
+     */
+    assertSame(seq0, alignment.getSequenceAt(0));
+    assertEquals(10, seq0.getStart());
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
+    assertEquals(10, sf.getBegin());
+    assertEquals(11, sf.getEnd());
+  }
 }
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 1cfa771..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();
@@ -1300,4 +1314,174 @@ public class AlignmentTest
     AlignmentI alignment = new Alignment(new SequenceI[] { seq });
     alignment.setDataset(alignment);
   }
+
+  @Test(groups = "Functional")
+  public void testAppend()
+  {
+    SequenceI seq = new Sequence("seq1", "FRMLPSRT-A--L-");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq });
+    alignment.setGapCharacter('-');
+    SequenceI seq2 = new Sequence("seq1", "KP..L.FQII.");
+    AlignmentI alignment2 = new Alignment(new SequenceI[] { seq2 });
+    alignment2.setGapCharacter('.');
+
+    alignment.append(alignment2);
+
+    assertEquals('-', alignment.getGapCharacter());
+    assertSame(seq, alignment.getSequenceAt(0));
+    assertEquals("KP--L-FQII-", alignment.getSequenceAt(1)
+            .getSequenceAsString());
+
+    // 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 ab25aa6..89169d6 100644 (file)
@@ -121,7 +121,7 @@ public class SeqCigarTest
     /*
      * TODO: can we add assertions to the sysouts that follow?
      */
-    System.out.println("Original sequence align:\n" + sub_gapped_s
+    System.out.println("\nOriginal sequence align:\n" + sub_gapped_s
             + "\nReconstructed window from 8 to 48\n" + "XXXXXXXX"
             + sub_se_gp.getSequenceString('-') + "..." + "\nCigar String:"
             + sub_se_gp.getCigarstring() + "\n");
@@ -193,7 +193,8 @@ public class SeqCigarTest
     SequenceI gen_sgapped_s = gen_sgapped.getSeq('-');
     // assertEquals("Couldn't reconstruct sequence", s_gapped.getSequence(),
     // gen_sgapped_s);
-    if (!gen_sgapped_s.getSequence().equals(s_gapped.getSequence()))
+    if (!gen_sgapped_s.getSequenceAsString().equals(
+            s_gapped.getSequenceAsString()))
     {
       // TODO: investigate errors reported here, to allow full conversion to
       // passing JUnit assertion form
index 2da8918..c955979 100644 (file)
@@ -42,7 +42,7 @@ public class SequenceFeatureTest
   }
 
   @Test(groups = { "Functional" })
-  public void testCopyConstructor()
+  public void testCopyConstructors()
   {
     SequenceFeature sf1 = new SequenceFeature("type", "desc", 22, 33,
             12.5f, "group");
@@ -56,10 +56,41 @@ public class SequenceFeatureTest
     assertEquals("desc", sf2.getDescription());
     assertEquals(22, sf2.getBegin());
     assertEquals(33, sf2.getEnd());
+    assertEquals(12.5f, sf2.getScore());
     assertEquals("+", sf2.getValue("STRAND"));
     assertEquals("Testing", sf2.getValue("Note"));
     // shallow clone of otherDetails map - contains the same object values!
     assertSame(count, sf2.getValue("Count"));
+
+    /*
+     * copy constructor modifying begin/end/group/score
+     */
+    SequenceFeature sf3 = new SequenceFeature(sf1, 11, 14, "group2", 17.4f);
+    assertEquals("type", sf3.getType());
+    assertEquals("desc", sf3.getDescription());
+    assertEquals(11, sf3.getBegin());
+    assertEquals(14, sf3.getEnd());
+    assertEquals(17.4f, sf3.getScore());
+    assertEquals("+", sf3.getValue("STRAND"));
+    assertEquals("Testing", sf3.getValue("Note"));
+    // shallow clone of otherDetails map - contains the same object values!
+    assertSame(count, sf3.getValue("Count"));
+
+    /*
+     * copy constructor modifying type/begin/end/group/score
+     */
+    SequenceFeature sf4 = new SequenceFeature(sf1, "Disulfide bond", 12,
+            15, "group3", -9.1f);
+    assertEquals("Disulfide bond", sf4.getType());
+    assertTrue(sf4.isContactFeature());
+    assertEquals("desc", sf4.getDescription());
+    assertEquals(12, sf4.getBegin());
+    assertEquals(15, sf4.getEnd());
+    assertEquals(-9.1f, sf4.getScore());
+    assertEquals("+", sf4.getValue("STRAND"));
+    assertEquals("Testing", sf4.getValue("Note"));
+    // shallow clone of otherDetails map - contains the same object values!
+    assertSame(count, sf4.getValue("Count"));
   }
 
   /**
@@ -123,51 +154,61 @@ public class SequenceFeatureTest
     assertEquals(sf1.hashCode(), sf2.hashCode());
 
     // changing type breaks equals:
-    String restores = sf2.getType();
-    sf2.setType("Type");
-    assertFalse(sf1.equals(sf2));
-    sf2.setType(restores);
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 22, 33,
+            12.5f, "group");
+    SequenceFeature sf4 = new SequenceFeature("Type", "desc", 22, 33,
+            12.5f, "group");
+    assertFalse(sf3.equals(sf4));
 
     // changing description breaks equals:
-    restores = sf2.getDescription();
+    String restores = sf2.getDescription();
     sf2.setDescription("Desc");
     assertFalse(sf1.equals(sf2));
     sf2.setDescription(restores);
 
     // changing score breaks equals:
     float restoref = sf2.getScore();
-    sf2.setScore(12.4f);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), 10f);
     assertFalse(sf1.equals(sf2));
-    sf2.setScore(restoref);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), restoref);
 
     // NaN doesn't match a number
     restoref = sf2.getScore();
-    sf2.setScore(Float.NaN);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), Float.NaN);
     assertFalse(sf1.equals(sf2));
 
     // NaN matches NaN
-    sf1.setScore(Float.NaN);
+    sf1 = new SequenceFeature(sf1, sf1.getBegin(), sf1.getEnd(),
+            sf1.getFeatureGroup(), Float.NaN);
     assertTrue(sf1.equals(sf2));
-    sf1.setScore(restoref);
-    sf2.setScore(restoref);
+    sf1 = new SequenceFeature(sf1, sf1.getBegin(), sf1.getEnd(),
+            sf1.getFeatureGroup(), restoref);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(),
+            sf2.getFeatureGroup(), restoref);
 
     // changing start position breaks equals:
     int restorei = sf2.getBegin();
-    sf2.setBegin(21);
+    sf2 = new SequenceFeature(sf2, 21, sf2.getEnd(), sf2.getFeatureGroup(), sf2.getScore());
     assertFalse(sf1.equals(sf2));
-    sf2.setBegin(restorei);
+    sf2 = new SequenceFeature(sf2, restorei, sf2.getEnd(),
+            sf2.getFeatureGroup(), sf2.getScore());
 
     // changing end position breaks equals:
     restorei = sf2.getEnd();
-    sf2.setEnd(32);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), 32,
+            sf2.getFeatureGroup(), sf2.getScore());
     assertFalse(sf1.equals(sf2));
-    sf2.setEnd(restorei);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), restorei,
+            sf2.getFeatureGroup(), sf2.getScore());
 
     // changing feature group breaks equals:
     restores = sf2.getFeatureGroup();
-    sf2.setFeatureGroup("Group");
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), "Group", sf2.getScore());
     assertFalse(sf1.equals(sf2));
-    sf2.setFeatureGroup(restores);
+    sf2 = new SequenceFeature(sf2, sf2.getBegin(), sf2.getEnd(), restores, sf2.getScore());
 
     // changing ID breaks equals:
     restores = (String) sf2.getValue("ID");
@@ -215,17 +256,64 @@ public class SequenceFeatureTest
     SequenceFeature sf = new SequenceFeature("type", "desc", 22, 33, 12.5f,
             "group");
     assertFalse(sf.isContactFeature());
-    sf.setType("");
+    sf = new SequenceFeature("", "desc", 22, 33, 12.5f, "group");
     assertFalse(sf.isContactFeature());
-    sf.setType(null);
+    sf = new SequenceFeature(null, "desc", 22, 33, 12.5f, "group");
     assertFalse(sf.isContactFeature());
-    sf.setType("Disulfide Bond");
+    sf = new SequenceFeature("Disulfide Bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
-    sf.setType("disulfide bond");
+    sf = new SequenceFeature("disulfide bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
-    sf.setType("Disulphide Bond");
+    sf = new SequenceFeature("Disulphide Bond", "desc", 22, 33, 12.5f,
+            "group");
     assertTrue(sf.isContactFeature());
-    sf.setType("disulphide bond");
+    sf = new SequenceFeature("disulphide bond", "desc", 22, 33, 12.5f,
+            "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 a52f0a2..9629b6f 100644 (file)
@@ -23,11 +23,14 @@ package jalview.datamodel;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertNotSame;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
-import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import jalview.analysis.AlignmentGenerator;
+import jalview.commands.EditCommand;
+import jalview.commands.EditCommand.Action;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.gui.JvOptionPane;
 import jalview.util.MapList;
@@ -36,6 +39,7 @@ 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;
 
@@ -44,6 +48,8 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class SequenceTest
 {
 
@@ -102,15 +108,6 @@ public class SequenceTest
     // change sequence, should trigger an update of cached result
     sq.setSequence("ASDFASDFADSF");
     assertTrue(sq.isProtein());
-    /*
-     * in situ change of sequence doesn't change hashcode :-O
-     * (sequence should not expose internal implementation)
-     */
-    for (int i = 0; i < sq.getSequence().length; i++)
-    {
-      sq.getSequence()[i] = "acgtu".charAt(i % 5);
-    }
-    assertTrue(sq.isProtein()); // but it isn't
   }
 
   @Test(groups = { "Functional" })
@@ -235,82 +232,454 @@ public class SequenceTest
   @Test(groups = { "Functional" })
   public void testFindIndex()
   {
+    /* 
+     * call sequenceChanged() after each test to invalidate any cursor,
+     * forcing the 1-arg findIndex to be executed
+     */
     SequenceI sq = new Sequence("test", "ABCDEF");
     assertEquals(0, sq.findIndex(0));
+    sq.sequenceChanged();
     assertEquals(1, sq.findIndex(1));
+    sq.sequenceChanged();
     assertEquals(5, sq.findIndex(5));
+    sq.sequenceChanged();
     assertEquals(6, sq.findIndex(6));
+    sq.sequenceChanged();
     assertEquals(6, sq.findIndex(9));
 
-    sq = new Sequence("test", "-A--B-C-D-E-F--");
-    assertEquals(2, sq.findIndex(1));
-    assertEquals(5, sq.findIndex(2));
-    assertEquals(7, sq.findIndex(3));
+    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));
+    sq.sequenceChanged();
+    assertEquals(7, sq.findIndex(10));
 
     // before start returns 0
+    sq.sequenceChanged();
     assertEquals(0, sq.findIndex(0));
+    sq.sequenceChanged();
     assertEquals(0, sq.findIndex(-1));
 
     // 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));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPositions()
+  {
+    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
+
+    /*
+     * invalid inputs
+     */
+    assertNull(sq.findPositions(6, 5));
+    assertNull(sq.findPositions(0, 5));
+    assertNull(sq.findPositions(-1, 5));
+
+    /*
+     * all gapped ranges
+     */
+    assertNull(sq.findPositions(1, 1)); // 1-based columns
+    assertNull(sq.findPositions(5, 5));
+    assertNull(sq.findPositions(5, 6));
+    assertNull(sq.findPositions(5, 7));
+
+    /*
+     * all ungapped ranges
+     */
+    assertEquals(new Range(8, 8), sq.findPositions(2, 2)); // A
+    assertEquals(new Range(8, 9), sq.findPositions(2, 3)); // AB
+    assertEquals(new Range(8, 10), sq.findPositions(2, 4)); // ABC
+    assertEquals(new Range(9, 10), sq.findPositions(3, 4)); // BC
+
+    /*
+     * gap to ungapped range
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 4)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(6, 9)); // DE
+
+    /*
+     * ungapped to gapped range
+     */
+    assertEquals(new Range(10, 10), sq.findPositions(4, 5)); // C
+    assertEquals(new Range(9, 13), sq.findPositions(3, 11)); // BCDEF
+
+    /*
+     * ungapped to ungapped enclosing gaps
+     */
+    assertEquals(new Range(10, 11), sq.findPositions(4, 8)); // CD
+    assertEquals(new Range(8, 13), sq.findPositions(2, 11)); // ABCDEF
+
+    /*
+     * gapped to gapped enclosing ungapped
+     */
+    assertEquals(new Range(8, 10), sq.findPositions(1, 5)); // ABC
+    assertEquals(new Range(11, 12), sq.findPositions(5, 10)); // DE
+    assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
+    assertEquals(new Range(8, 13), sq.findPositions(1, 99));
   }
 
   /**
-   * Tests for the method that returns a dataset sequence position (base 1) for
+   * Tests for the method that returns a dataset sequence position (start..) for
    * an aligned column position (base 0).
    */
   @Test(groups = { "Functional" })
   public void testFindPosition()
   {
-    SequenceI sq = new Sequence("test", "ABCDEF");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(6, sq.findPosition(5));
+    /* 
+     * call sequenceChanged() after each test to invalidate any cursor,
+     * forcing the 1-arg findPosition to be executed
+     */
+    SequenceI sq = new Sequence("test/8-13", "ABCDEF");
+    assertEquals(8, sq.findPosition(0));
+    // Sequence should now hold a cursor at [8, 0]
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    int token = (int) PA.getValue(sq, "changeCount");
+    assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+
+    sq.sequenceChanged();
+
+    /*
+     * find F13 at column offset 5, cursor should update to [13, 6]
+     * endColumn is found and saved in cursor
+     */
+    assertEquals(13, sq.findPosition(5));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(++token, (int) PA.getValue(sq, "changeCount"));
+    assertEquals(new SequenceCursor(sq, 13, 6, token), cursor);
+    assertEquals("test:Pos13:Col6:startCol1:endCol6:tok1",
+            PA.getValue(sq, "cursor").toString());
+
     // assertEquals(-1, seq.findPosition(6)); // fails
 
-    sq = new Sequence("test", "AB-C-D--");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(2, sq.findPosition(1));
+    sq = new Sequence("test/8-11", "AB-C-D--");
+    token = (int) PA.getValue(sq, "changeCount"); // 0
+    assertEquals(8, sq.findPosition(0));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 8, 1, token), cursor);
+    assertEquals("test:Pos8:Col1:startCol1:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(9, sq.findPosition(1));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok1",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
     // gap position 'finds' residue to the right (not the left as per javadoc)
-    assertEquals(3, sq.findPosition(2));
-    assertEquals(3, sq.findPosition(3));
-    assertEquals(4, sq.findPosition(4));
-    assertEquals(4, sq.findPosition(5));
+    // cursor is set to the last residue position found [B 2]
+    assertEquals(10, sq.findPosition(2));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 2, ++token), cursor);
+    assertEquals("test:Pos9:Col2:startCol1:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(3));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    // column[4] is the gap after C - returns D11
+    // cursor is set to [C 4]
+    assertEquals(11, sq.findPosition(4));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 4, ++token), cursor);
+    assertEquals("test:Pos10:Col4:startCol1:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(5)); // D
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+    // lastCol has been found and saved in the cursor
+    assertEquals("test:Pos11:Col6:startCol1:endCol6:tok5",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
     // returns 1 more than sequence length if off the end ?!?
-    assertEquals(5, sq.findPosition(6));
-    assertEquals(5, sq.findPosition(7));
+    assertEquals(12, sq.findPosition(6));
 
-    sq = new Sequence("test", "--AB-C-DEF--");
-    assertEquals(1, sq.findPosition(0));
-    assertEquals(1, sq.findPosition(1));
-    assertEquals(1, sq.findPosition(2));
-    assertEquals(2, sq.findPosition(3));
-    assertEquals(3, sq.findPosition(4));
-    assertEquals(3, sq.findPosition(5));
-    assertEquals(4, sq.findPosition(6));
-    assertEquals(4, sq.findPosition(7));
-    assertEquals(5, sq.findPosition(8));
-    assertEquals(6, sq.findPosition(9));
-    assertEquals(7, sq.findPosition(10));
-    assertEquals(7, sq.findPosition(11));
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(7));
+
+    /*
+     * first findPosition should also set firstResCol in cursor
+     */
+    sq = new Sequence("test/8-13", "--AB-C-DEF--");
+    assertEquals(8, sq.findPosition(0));
+    assertNull(PA.getValue(sq, "cursor"));
+
+    sq.sequenceChanged();
+    assertEquals(8, sq.findPosition(1));
+    assertNull(PA.getValue(sq, "cursor"));
+
+    sq.sequenceChanged();
+    assertEquals(8, sq.findPosition(2));
+    assertEquals("test:Pos8:Col3:startCol3:endCol0:tok2",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(9, sq.findPosition(3));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok3",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    // column[4] is a gap, returns next residue pos (C10)
+    // cursor is set to last residue found [B]
+    assertEquals(10, sq.findPosition(4));
+    assertEquals("test:Pos9:Col4:startCol3:endCol0:tok4",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(10, sq.findPosition(5));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok5",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    // column[6] is a gap, returns next residue pos (D11)
+    // cursor is set to last residue found [C]
+    assertEquals(11, sq.findPosition(6));
+    assertEquals("test:Pos10:Col6:startCol3:endCol0:tok6",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(11, sq.findPosition(7));
+    assertEquals("test:Pos11:Col8:startCol3:endCol0:tok7",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8));
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok8",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * when the last residue column is found, it is set in the cursor
+     */
+    sq.sequenceChanged();
+    assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok9",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(10));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok10",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * findPosition for column beyond sequence length
+     * returns 1 more than last residue position
+     */
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(11));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok11",
+            PA.getValue(sq, "cursor").toString());
+
+    sq.sequenceChanged();
+    assertEquals(14, sq.findPosition(99));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok12",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * gapped sequence ending in non-gap
+     */
+    sq = new Sequence("test/8-13", "--AB-C-DEF");
+    assertEquals(13, sq.findPosition(9));
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+    sq.sequenceChanged();
+    assertEquals(12, sq.findPosition(8)); // E12
+    // sequenceChanged() invalidates cursor.lastResidueColumn
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos12:Col9:startCol3:endCol0:tok1",
+            cursor.toString());
+    // findPosition with cursor accepts base 1 column values
+    assertEquals(13, ((Sequence) sq).findPosition(10, cursor));
+    assertEquals(13, sq.findPosition(9)); // F13
+    // lastResidueColumn has now been found and saved in cursor
+    assertEquals("test:Pos13:Col10:startCol3:endCol10:tok1",
+            PA.getValue(sq, "cursor").toString());
   }
 
   @Test(groups = { "Functional" })
   public void testDeleteChars()
   {
+    /*
+     * internal delete
+     */
     SequenceI sq = new Sequence("test", "ABCDEF");
+    assertNull(PA.getValue(sq, "datasetSequence"));
     assertEquals(1, sq.getStart());
     assertEquals(6, sq.getEnd());
     sq.deleteChars(2, 3);
     assertEquals("ABDEF", sq.getSequenceAsString());
     assertEquals(1, sq.getStart());
     assertEquals(5, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
 
+    /*
+     * delete at start
+     */
     sq = new Sequence("test", "ABCDEF");
     sq.deleteChars(0, 2);
     assertEquals("CDEF", sq.getSequenceAsString());
     assertEquals(3, sq.getStart());
     assertEquals(6, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+
+    sq = new Sequence("test", "ABCDE");
+    sq.deleteChars(0, 3);
+    assertEquals("DE", sq.getSequenceAsString());
+    assertEquals(4, sq.getStart());
+    assertEquals(5, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+
+    /*
+     * delete at end
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.deleteChars(4, 6);
+    assertEquals("ABCD", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(4, sq.getEnd());
+    assertNull(PA.getValue(sq, "datasetSequence"));
+
+    /*
+     * delete more positions than there are
+     */
+    sq = new Sequence("test/8-11", "ABCD");
+    sq.deleteChars(0, 99);
+    assertEquals("", sq.getSequenceAsString());
+    assertEquals(12, sq.getStart()); // = findPosition(99) ?!?
+    assertEquals(11, sq.getEnd());
+
+    sq = new Sequence("test/8-11", "----");
+    sq.deleteChars(0, 99); // ArrayIndexOutOfBoundsException <= 2.10.2
+    assertEquals("", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(11, sq.getEnd());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDeleteChars_withDbRefsAndFeatures()
+  {
+    /*
+     * internal delete - new dataset sequence created
+     * gets a copy of any dbrefs
+     */
+    SequenceI sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    DBRefEntry dbr1 = new DBRefEntry("Uniprot", "0", "a123");
+    sq.addDBRef(dbr1);
+    Object ds = PA.getValue(sq, "datasetSequence");
+    assertNotNull(ds);
+    assertEquals(1, sq.getStart());
+    assertEquals(6, sq.getEnd());
+    sq.deleteChars(2, 3);
+    assertEquals("ABDEF", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(5, sq.getEnd());
+    Object newDs = PA.getValue(sq, "datasetSequence");
+    assertNotNull(newDs);
+    assertNotSame(ds, newDs);
+    assertNotNull(sq.getDBRefs());
+    assertEquals(1, sq.getDBRefs().length);
+    assertNotSame(dbr1, sq.getDBRefs()[0]);
+    assertEquals(dbr1, sq.getDBRefs()[0]);
+
+    /*
+     * internal delete with sequence features
+     * (failure case for JAL-2541)
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            "CathGroup");
+    sq.addSequenceFeature(sf1);
+    ds = PA.getValue(sq, "datasetSequence");
+    assertNotNull(ds);
+    assertEquals(1, sq.getStart());
+    assertEquals(6, sq.getEnd());
+    sq.deleteChars(2, 4);
+    assertEquals("ABEF", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(4, sq.getEnd());
+    newDs = PA.getValue(sq, "datasetSequence");
+    assertNotNull(newDs);
+    assertNotSame(ds, newDs);
+    List<SequenceFeature> sfs = sq.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    assertNotSame(sf1, sfs.get(0));
+    assertEquals(sf1, sfs.get(0));
+
+    /*
+     * delete at start - no new dataset sequence created
+     * any sequence features remain as before
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    ds = PA.getValue(sq, "datasetSequence");
+    sf1 = new SequenceFeature("Cath", "desc", 2, 4, 2f, "CathGroup");
+    sq.addSequenceFeature(sf1);
+    sq.deleteChars(0, 2);
+    assertEquals("CDEF", sq.getSequenceAsString());
+    assertEquals(3, sq.getStart());
+    assertEquals(6, sq.getEnd());
+    assertSame(ds, PA.getValue(sq, "datasetSequence"));
+    sfs = sq.getSequenceFeatures();
+    assertNotNull(sfs);
+    assertEquals(1, sfs.size());
+    assertSame(sf1, sfs.get(0));
+
+    /*
+     * delete at end - no new dataset sequence created
+     * any dbrefs remain as before
+     */
+    sq = new Sequence("test", "ABCDEF");
+    sq.createDatasetSequence();
+    ds = PA.getValue(sq, "datasetSequence");
+    dbr1 = new DBRefEntry("Uniprot", "0", "a123");
+    sq.addDBRef(dbr1);
+    sq.deleteChars(4, 6);
+    assertEquals("ABCD", sq.getSequenceAsString());
+    assertEquals(1, sq.getStart());
+    assertEquals(4, sq.getEnd());
+    assertSame(ds, PA.getValue(sq, "datasetSequence"));
+    assertNotNull(sq.getDBRefs());
+    assertEquals(1, sq.getDBRefs().length);
+    assertSame(dbr1, sq.getDBRefs()[0]);
   }
 
   @Test(groups = { "Functional" })
@@ -348,16 +717,16 @@ public class SequenceTest
     SequenceI sq = new Sequence("test", "GATCAT");
     sq.createDatasetSequence();
 
-    assertNull(sq.getSequenceFeatures());
+    assertTrue(sq.getSequenceFeatures().isEmpty());
 
     /*
      * SequenceFeature on sequence
      */
-    SequenceFeature sf = new SequenceFeature();
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 4, 2f, null);
     sq.addSequenceFeature(sf);
-    SequenceFeature[] sfs = sq.getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    assertSame(sf, sfs[0]);
+    List<SequenceFeature> sfs = sq.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    assertSame(sf, sfs.get(0));
 
     /*
      * SequenceFeature on sequence and dataset sequence; returns that on
@@ -366,18 +735,19 @@ public class SequenceTest
      * Note JAL-2046: spurious: we have no use case for this at the moment.
      * This test also buggy - as sf2.equals(sf), no new feature is added
      */
-    SequenceFeature sf2 = new SequenceFeature();
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            null);
     sq.getDatasetSequence().addSequenceFeature(sf2);
     sfs = sq.getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    assertSame(sf, sfs[0]);
+    assertEquals(1, sfs.size());
+    assertSame(sf, sfs.get(0));
 
     /*
      * SequenceFeature on dataset sequence only
      * Note JAL-2046: spurious: we have no use case for setting a non-dataset sequence's feature array to null at the moment.
      */
     sq.setSequenceFeatures(null);
-    assertNull(sq.getDatasetSequence().getSequenceFeatures());
+    assertTrue(sq.getDatasetSequence().getSequenceFeatures().isEmpty());
 
     /*
      * Corrupt case - no SequenceFeature, dataset's dataset is the original
@@ -398,7 +768,7 @@ public class SequenceTest
       assertTrue(e.getMessage().toLowerCase()
               .contains("implementation error"));
     }
-    assertNull(sq.getSequenceFeatures());
+    assertTrue(sq.getSequenceFeatures().isEmpty());
   }
 
   /**
@@ -448,11 +818,23 @@ public class SequenceTest
   public void testCreateDatasetSequence()
   {
     SequenceI sq = new Sequence("my", "ASDASD");
+    sq.addSequenceFeature(new SequenceFeature("type", "desc", 1, 10, 1f,
+            "group"));
+    sq.addDBRef(new DBRefEntry("source", "version", "accession"));
     assertNull(sq.getDatasetSequence());
+    assertNotNull(PA.getValue(sq, "sequenceFeatureStore"));
+    assertNotNull(PA.getValue(sq, "dbrefs"));
+
     SequenceI rds = sq.createDatasetSequence();
     assertNotNull(rds);
     assertNull(rds.getDatasetSequence());
-    assertEquals(sq.getDatasetSequence(), rds);
+    assertSame(sq.getDatasetSequence(), rds);
+
+    // sequence features and dbrefs transferred to dataset sequence
+    assertNull(PA.getValue(sq, "sequenceFeatureStore"));
+    assertNull(PA.getValue(sq, "dbrefs"));
+    assertNotNull(PA.getValue(rds, "sequenceFeatureStore"));
+    assertNotNull(PA.getValue(rds, "dbrefs"));
   }
 
   /**
@@ -520,7 +902,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));
@@ -559,12 +941,9 @@ public class SequenceTest
     assertEquals("CD", derived.getSequenceAsString());
     assertSame(sq.getDatasetSequence(), derived.getDatasetSequence());
 
-    assertNull(sq.sequenceFeatures);
-    assertNull(derived.sequenceFeatures);
     // derived sequence should access dataset sequence features
     assertNotNull(sq.getSequenceFeatures());
-    assertArrayEquals(sq.getSequenceFeatures(),
-            derived.getSequenceFeatures());
+    assertEquals(sq.getSequenceFeatures(), derived.getSequenceFeatures());
 
     /*
      *  verify we have primary db refs *just* for PDB IDs with associated
@@ -694,18 +1073,18 @@ public class SequenceTest
     assertEquals(anns[0].score, seq1.getAnnotation()[0].score);
 
     // copy has a copy of the sequence feature:
-    SequenceFeature[] sfs = copy.getSequenceFeatures();
-    assertEquals(1, sfs.length);
+    List<SequenceFeature> sfs = copy.getSequenceFeatures();
+    assertEquals(1, sfs.size());
     if (seq1.getDatasetSequence() != null
             && copy.getDatasetSequence() == seq1.getDatasetSequence())
     {
-      assertTrue(sfs[0] == seq1.getSequenceFeatures()[0]);
+      assertSame(sfs.get(0), seq1.getSequenceFeatures().get(0));
     }
     else
     {
-      assertFalse(sfs[0] == seq1.getSequenceFeatures()[0]);
+      assertNotSame(sfs.get(0), seq1.getSequenceFeatures().get(0));
     }
-    assertTrue(sfs[0].equals(seq1.getSequenceFeatures()[0]));
+    assertEquals(sfs.get(0), seq1.getSequenceFeatures().get(0));
 
     // copy has a copy of the PDB entry
     Vector<PDBEntry> pdbs = copy.getAllPDBEntries();
@@ -724,6 +1103,36 @@ public class SequenceTest
     assertEquals(' ', sq.getCharAt(-1));
   }
 
+  @Test(groups = { "Functional" })
+  public void testAddSequenceFeatures()
+  {
+    SequenceI sq = new Sequence("", "abcde");
+    // type may not be null
+    assertFalse(sq.addSequenceFeature(new SequenceFeature(null, "desc", 4,
+            8, 0f, null)));
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, 0f, null)));
+    // can't add a duplicate feature
+    assertFalse(sq.addSequenceFeature(new SequenceFeature("Cath", "desc",
+            4, 8, 0f, null)));
+    // can add a different feature
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Scop", "desc", 4,
+            8, 0f, null))); // different type
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath",
+            "description", 4, 8, 0f, null)));// different description
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 3,
+            8, 0f, null))); // different start position
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            9, 0f, null))); // different end position
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, 1f, null))); // different score
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, Float.NaN, null))); // score NaN
+    assertTrue(sq.addSequenceFeature(new SequenceFeature("Cath", "desc", 4,
+            8, 0f, "Metal"))); // different group
+    assertEquals(8, sq.getFeatures().getAllFeatures().size());
+  }
+
   /**
    * Tests for adding (or updating) dbrefs
    * 
@@ -1023,4 +1432,724 @@ public class SequenceTest
     seq2.createDatasetSequence();
     seq.setDatasetSequence(seq2);
   }
+
+  @Test(groups = { "Functional" })
+  public void testFindFeatures()
+  {
+    SequenceI sq = new Sequence("test/8-16", "-ABC--DEF--GHI--");
+    sq.createDatasetSequence();
+
+    assertTrue(sq.findFeatures(1, 99).isEmpty());
+
+    // add non-positional feature
+    SequenceFeature sf0 = new SequenceFeature("Cath", "desc", 0, 0, 2f,
+            null);
+    sq.addSequenceFeature(sf0);
+    // add feature on BCD
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 9, 11, 2f,
+            null);
+    sq.addSequenceFeature(sfBCD);
+    // add feature on DE
+    SequenceFeature sfDE = new SequenceFeature("Cath", "desc", 11, 12, 2f,
+            null);
+    sq.addSequenceFeature(sfDE);
+    // add contact feature at [B, H]
+    SequenceFeature sfContactBH = new SequenceFeature("Disulphide bond",
+            "desc", 9, 15, 2f, null);
+    sq.addSequenceFeature(sfContactBH);
+    // add contact feature at [F, G]
+    SequenceFeature sfContactFG = new SequenceFeature("Disulfide Bond",
+            "desc", 13, 14, 2f, null);
+    sq.addSequenceFeature(sfContactFG);
+    // add single position feature at [I]
+    SequenceFeature sfI = new SequenceFeature("Disulfide Bond",
+            "desc", 16, 16, null);
+    sq.addSequenceFeature(sfI);
+
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+
+    // columns 1-6 (-ABC--) includes BCD and B/H feature but not DE
+    found = sq.findFeatures(1, 6);
+    assertEquals(2, found.size());
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfContactBH));
+
+    // columns 5-6 (--) includes (enclosing) BCD but not (contact) B/H feature
+    found = sq.findFeatures(5, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfBCD));
+
+    // columns 7-10 (DEF-) includes BCD, DE, F/G but not B/H feature
+    found = sq.findFeatures(7, 10);
+    assertEquals(3, found.size());
+    assertTrue(found.contains(sfBCD));
+    assertTrue(found.contains(sfDE));
+    assertTrue(found.contains(sfContactFG));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+
+    // columns 14-14 (I) should find variant feature
+    found = sq.findFeatures(14, 14);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfI));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindIndex_withCursor()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+
+    // 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 (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" })
+  public void testFindPosition_withCursor()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+  
+    // find F pos given A - lastCol gets set in cursor
+    assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // find A pos given F - first residue column is saved in cursor
+    assertEquals(8, sq.findPosition(2, new SequenceCursor(sq, 13, 10, 0)));
+    assertEquals("test:Pos8:Col2:startCol2:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+  
+    // find C pos given C (neither startCol nor endCol is set)
+    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 10, 6, 0)));
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // now the grey area - what residue position for a gapped column? JAL-2562
+
+    // find 'residue' for column 3 given cursor for D (so working left)
+    // returns B9; cursor is updated to [B 5]
+    assertEquals(9, sq.findPosition(3, new SequenceCursor(sq, 11, 7, 0)));
+    assertEquals("test:Pos9:Col5:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // find 'residue' for column 8 given cursor for D (so working right)
+    // returns E12; cursor is updated to [D 7]
+    assertEquals(12, sq.findPosition(8, new SequenceCursor(sq, 11, 7, 0)));
+    assertEquals("test:Pos11:Col7:startCol0:endCol0:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    // find 'residue' for column 12 given cursor for B
+    // returns 1 more than last residue position; cursor is updated to [F 10]
+    // lastCol position is saved in cursor
+    assertEquals(14, sq.findPosition(12, new SequenceCursor(sq, 9, 5, 0)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * findPosition for column beyond length of sequence
+     * returns 1 more than the last residue position
+     * cursor is set to last real residue position [F 10]
+     */
+    assertEquals(14, sq.findPosition(99, new SequenceCursor(sq, 8, 2, 0)));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+
+    /*
+     * and the case without a trailing gap
+     */
+    sq = new Sequence("test/8-13", "-A--BCD-EF");
+    // first find C from A
+    assertEquals(10, sq.findPosition(6, new SequenceCursor(sq, 8, 2, 0)));
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos10:Col6:startCol0:endCol0:tok0",
+            cursor.toString());
+    // now 'find' 99 from C
+    // cursor is set to [F 10] and saved lastCol
+    assertEquals(14, sq.findPosition(99, cursor));
+    assertEquals("test:Pos13:Col10:startCol0:endCol10:tok0",
+            PA.getValue(sq, "cursor").toString());
+  }
+
+  @Test
+  public void testIsValidCursor()
+  {
+    Sequence sq = new Sequence("Seq", "ABC--DE-F", 8, 13);
+    assertFalse(sq.isValidCursor(null));
+
+    /*
+     * cursor is valid if it has valid sequence ref and changeCount token
+     * and positions within the range of the sequence
+     */
+    int changeCount = (int) PA.getValue(sq, "changeCount");
+    SequenceCursor cursor = new SequenceCursor(sq, 13, 1, changeCount);
+    assertTrue(sq.isValidCursor(cursor));
+
+    /*
+     * column position outside [0 - length] is rejected
+     */
+    cursor = new SequenceCursor(sq, 13, -1, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 13, 10, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 7, 8, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 14, 2, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+
+    /*
+     * wrong sequence is rejected
+     */
+    cursor = new SequenceCursor(null, 13, 1, changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(new Sequence("Seq", "abc"), 13, 1,
+            changeCount);
+    assertFalse(sq.isValidCursor(cursor));
+
+    /*
+     * wrong token value is rejected
+     */
+    cursor = new SequenceCursor(sq, 13, 1, changeCount + 1);
+    assertFalse(sq.isValidCursor(cursor));
+    cursor = new SequenceCursor(sq, 13, 1, changeCount - 1);
+    assertFalse(sq.isValidCursor(cursor));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindPosition_withCursorAndEdits()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+  
+    // find F pos given A
+    assertEquals(13, sq.findPosition(10, new SequenceCursor(sq, 8, 2, 0)));
+    int token = (int) PA.getValue(sq, "changeCount"); // 0
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, token), cursor);
+
+    /*
+     * setSequence should invalidate the cursor cached by the sequence
+     */
+    sq.setSequence("-A-BCD-EF---"); // one gap removed
+    assertEquals(8, sq.getStart()); // sanity check
+    assertEquals(11, sq.findPosition(5)); // D11
+    // cursor should now be at [D 6]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 11, 6, ++token), cursor);
+    assertEquals(0, cursor.lastColumnPosition); // not yet found
+    assertEquals(13, sq.findPosition(8)); // E13
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(9, cursor.lastColumnPosition); // found
+
+    /*
+     * deleteChars should invalidate the cached cursor
+     */
+    sq.deleteChars(2, 5); // delete -BC
+    assertEquals("-AD-EF---", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart()); // sanity check
+    assertEquals(10, sq.findPosition(4)); // E10
+    // cursor should now be at [E 5]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 10, 5, ++token), cursor);
+
+    /*
+     * Edit to insert gaps should invalidate the cached cursor
+     * insert 2 gaps at column[3] to make -AD---EF---
+     */
+    SequenceI[] seqs = new SequenceI[] { sq };
+    AlignmentI al = new Alignment(seqs);
+    new EditCommand().appendEdit(Action.INSERT_GAP, seqs, 3, 2, al, true);
+    assertEquals("-AD---EF---", sq.getSequenceAsString());
+    assertEquals(10, sq.findPosition(4)); // E10
+    // cursor should now be at [D 3]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 9, 3, ++token), cursor);
+
+    /*
+     * insertCharAt should invalidate the cached cursor
+     * insert CC at column[4] to make -AD-CC--EF---
+     */
+    sq.insertCharAt(4, 2, 'C');
+    assertEquals("-AD-CC--EF---", sq.getSequenceAsString());
+    assertEquals(13, sq.findPosition(9)); // F13
+    // cursor should now be at [F 10]
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(new SequenceCursor(sq, 13, 10, ++token), cursor);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetSequence()
+  {
+    String seqstring = "-A--BCD-EF--";
+    Sequence sq = new Sequence("test/8-13", seqstring);
+    sq.createDatasetSequence();
+    assertTrue(Arrays.equals(sq.getSequence(), seqstring.toCharArray()));
+    assertTrue(Arrays.equals(sq.getDatasetSequence().getSequence(),
+            "ABCDEF".toCharArray()));
+
+    // verify a copy of the sequence array is returned
+    char[] theSeq = (char[]) PA.getValue(sq, "sequence");
+    assertNotSame(theSeq, sq.getSequence());
+    theSeq = (char[]) PA.getValue(sq.getDatasetSequence(), "sequence");
+    assertNotSame(theSeq, sq.getDatasetSequence().getSequence());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testReplace()
+  {
+    String seqstring = "-A--BCD-EF--";
+    SequenceI sq = new Sequence("test/8-13", seqstring);
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('A', 'A')); // same char
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(0, sq.replace('X', 'Y')); // not there
+    assertEquals(seqstring, sq.getSequenceAsString());
+    assertEquals(0, PA.getValue(sq, "changeCount"));
+
+    assertEquals(1, sq.replace('A', 'K'));
+    assertEquals("-K--BCD-EF--", sq.getSequenceAsString());
+    assertEquals(1, PA.getValue(sq, "changeCount"));
+
+    assertEquals(6, sq.replace('-', '.'));
+    assertEquals(".K..BCD.EF..", sq.getSequenceAsString());
+    assertEquals(2, PA.getValue(sq, "changeCount"));
+  }
+
+  @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..0846ec2
--- /dev/null
@@ -0,0 +1,133 @@
+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", "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 });
+    // setting 'mixed' character and numeric values wipes the min/max value
+    sf.setValue("kd", "some text");
+    assertNull(fa.getMinMax("Pfam", "kd"));
+
+    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");
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java
new file mode 100644 (file)
index 0000000..6e7dd02
--- /dev/null
@@ -0,0 +1,938 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.testng.annotations.Test;
+
+public class FeatureStoreTest
+{
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_nonNested()
+  {
+    FeatureStore fs = new FeatureStore();
+    fs.addFeature(new SequenceFeature("", "", 10, 20, Float.NaN,
+            null));
+    // same range different description
+    fs.addFeature(new SequenceFeature("", "desc", 10, 20, Float.NaN, null));
+    fs.addFeature(new SequenceFeature("", "", 15, 25, Float.NaN, null));
+    fs.addFeature(new SequenceFeature("", "", 20, 35, Float.NaN, null));
+
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
+    assertTrue(overlaps.isEmpty());
+
+    overlaps = fs.findOverlappingFeatures(8, 10);
+    assertEquals(overlaps.size(), 2);
+    assertEquals(overlaps.get(0).getEnd(), 20);
+    assertEquals(overlaps.get(1).getEnd(), 20);
+
+    overlaps = fs.findOverlappingFeatures(12, 16);
+    assertEquals(overlaps.size(), 3);
+    assertEquals(overlaps.get(0).getEnd(), 20);
+    assertEquals(overlaps.get(1).getEnd(), 20);
+    assertEquals(overlaps.get(2).getEnd(), 25);
+
+    overlaps = fs.findOverlappingFeatures(33, 33);
+    assertEquals(overlaps.size(), 1);
+    assertEquals(overlaps.get(0).getEnd(), 35);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_nested()
+  {
+    FeatureStore fs = new FeatureStore();
+    SequenceFeature sf1 = addFeature(fs, 10, 50);
+    SequenceFeature sf2 = addFeature(fs, 10, 40);
+    SequenceFeature sf3 = addFeature(fs, 20, 30);
+    // fudge feature at same location but different group (so is added)
+    SequenceFeature sf4 = new SequenceFeature("", "", 20, 30, Float.NaN,
+            "different group");
+    fs.addFeature(sf4);
+    SequenceFeature sf5 = addFeature(fs, 35, 36);
+
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
+    assertTrue(overlaps.isEmpty());
+
+    overlaps = fs.findOverlappingFeatures(10, 15);
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+
+    overlaps = fs.findOverlappingFeatures(45, 60);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf1));
+
+    overlaps = fs.findOverlappingFeatures(32, 38);
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+    assertTrue(overlaps.contains(sf5));
+
+    overlaps = fs.findOverlappingFeatures(15, 25);
+    assertEquals(overlaps.size(), 4);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+    assertTrue(overlaps.contains(sf3));
+    assertTrue(overlaps.contains(sf4));
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_mixed()
+  {
+    FeatureStore fs = new FeatureStore();
+    SequenceFeature sf1 = addFeature(fs, 10, 50);
+    SequenceFeature sf2 = addFeature(fs, 1, 15);
+    SequenceFeature sf3 = addFeature(fs, 20, 30);
+    SequenceFeature sf4 = addFeature(fs, 40, 100);
+    SequenceFeature sf5 = addFeature(fs, 60, 100);
+    SequenceFeature sf6 = addFeature(fs, 70, 70);
+
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(200, 200);
+    assertTrue(overlaps.isEmpty());
+
+    overlaps = fs.findOverlappingFeatures(1, 9);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf2));
+
+    overlaps = fs.findOverlappingFeatures(5, 18);
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+
+    overlaps = fs.findOverlappingFeatures(30, 40);
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf3));
+    assertTrue(overlaps.contains(sf4));
+
+    overlaps = fs.findOverlappingFeatures(80, 90);
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+
+    overlaps = fs.findOverlappingFeatures(68, 70);
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+    assertTrue(overlaps.contains(sf6));
+  }
+
+  /**
+   * Helper method to add a feature of no particular type
+   * 
+   * @param fs
+   * @param from
+   * @param to
+   * @return
+   */
+  SequenceFeature addFeature(FeatureStore fs, int from, int to)
+  {
+    SequenceFeature sf1 = new SequenceFeature("", "", from, to, Float.NaN,
+            null);
+    fs.addFeature(sf1);
+    return sf1;
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures_contactFeatures()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    SequenceFeature sf = new SequenceFeature("disulphide bond", "bond", 10,
+            20, Float.NaN, null);
+    fs.addFeature(sf);
+
+    /*
+     * neither contact point in range
+     */
+    List<SequenceFeature> overlaps = fs.findOverlappingFeatures(1, 9);
+    assertTrue(overlaps.isEmpty());
+
+    /*
+     * neither contact point in range
+     */
+    overlaps = fs.findOverlappingFeatures(11, 19);
+    assertTrue(overlaps.isEmpty());
+
+    /*
+     * first contact point in range
+     */
+    overlaps = fs.findOverlappingFeatures(5, 15);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf));
+
+    /*
+     * second contact point in range
+     */
+    overlaps = fs.findOverlappingFeatures(15, 25);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf));
+
+    /*
+     * both contact points in range
+     */
+    overlaps = fs.findOverlappingFeatures(5, 25);
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf));
+  }
+
+  /**
+   * Tests for the method that returns false for an attempt to add a feature
+   * that would enclose, or be enclosed by, another feature
+   */
+  @Test(groups = "Functional")
+  public void testAddNonNestedFeature()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    String type = "Domain";
+    SequenceFeature sf1 = new SequenceFeature(type, type, 10, 20,
+            Float.NaN, null);
+    assertTrue(fs.addNonNestedFeature(sf1));
+
+    // co-located feature is ok
+    SequenceFeature sf2 = new SequenceFeature(type, type, 10, 20,
+            Float.NaN, null);
+    assertTrue(fs.addNonNestedFeature(sf2));
+
+    // overlap left is ok
+    SequenceFeature sf3 = new SequenceFeature(type, type, 5, 15, Float.NaN,
+            null);
+    assertTrue(fs.addNonNestedFeature(sf3));
+
+    // overlap right is ok
+    SequenceFeature sf4 = new SequenceFeature(type, type, 15, 25,
+            Float.NaN, null);
+    assertTrue(fs.addNonNestedFeature(sf4));
+
+    // add enclosing feature is not ok
+    SequenceFeature sf5 = new SequenceFeature(type, type, 10, 21,
+            Float.NaN, null);
+    assertFalse(fs.addNonNestedFeature(sf5));
+    SequenceFeature sf6 = new SequenceFeature(type, type, 4, 15, Float.NaN,
+            null);
+    assertFalse(fs.addNonNestedFeature(sf6));
+    SequenceFeature sf7 = new SequenceFeature(type, type, 1, 50, Float.NaN,
+            null);
+    assertFalse(fs.addNonNestedFeature(sf7));
+
+    // add enclosed feature is not ok
+    SequenceFeature sf8 = new SequenceFeature(type, type, 10, 19,
+            Float.NaN, null);
+    assertFalse(fs.addNonNestedFeature(sf8));
+    SequenceFeature sf9 = new SequenceFeature(type, type, 16, 25,
+            Float.NaN, null);
+    assertFalse(fs.addNonNestedFeature(sf9));
+    SequenceFeature sf10 = new SequenceFeature(type, type, 7, 7, Float.NaN,
+            null);
+    assertFalse(fs.addNonNestedFeature(sf10));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPositionalFeatures()
+  {
+    FeatureStore store = new FeatureStore();
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.addFeature(sf1);
+    // same range, different description
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20,
+            Float.NaN, null);
+    store.addFeature(sf2);
+    // discontiguous range
+    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40,
+            Float.NaN, null);
+    store.addFeature(sf3);
+    // overlapping range
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35,
+            Float.NaN, null);
+    store.addFeature(sf4);
+    // enclosing range
+    SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50,
+            Float.NaN, null);
+    store.addFeature(sf5);
+    // non-positional feature
+    SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.addFeature(sf6);
+    // contact feature
+    SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.addFeature(sf7);
+
+    List<SequenceFeature> features = store.getPositionalFeatures();
+    assertEquals(features.size(), 6);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertFalse(features.contains(sf6));
+    assertTrue(features.contains(sf7));
+
+    features = store.getNonPositionalFeatures();
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete()
+  {
+    FeatureStore store = new FeatureStore();
+    SequenceFeature sf1 = addFeature(store, 10, 20);
+    assertTrue(store.getPositionalFeatures().contains(sf1));
+
+    /*
+     * simple deletion
+     */
+    assertTrue(store.delete(sf1));
+    assertTrue(store.getPositionalFeatures().isEmpty());
+
+    /*
+     * non-positional feature deletion
+     */
+    SequenceFeature sf2 = addFeature(store, 0, 0);
+    assertFalse(store.getPositionalFeatures().contains(sf2));
+    assertTrue(store.getNonPositionalFeatures().contains(sf2));
+    assertTrue(store.delete(sf2));
+    assertTrue(store.getNonPositionalFeatures().isEmpty());
+
+    /*
+     * contact feature deletion
+     */
+    SequenceFeature sf3 = new SequenceFeature("", "Disulphide Bond", 11,
+            23, Float.NaN, null);
+    store.addFeature(sf3);
+    assertEquals(store.getPositionalFeatures().size(), 1);
+    assertTrue(store.getPositionalFeatures().contains(sf3));
+    assertTrue(store.delete(sf3));
+    assertTrue(store.getPositionalFeatures().isEmpty());
+
+    /*
+     * nested feature deletion
+     */
+    SequenceFeature sf4 = addFeature(store, 20, 30);
+    SequenceFeature sf5 = addFeature(store, 22, 26); // to NCList
+    SequenceFeature sf6 = addFeature(store, 23, 24); // child of sf5
+    SequenceFeature sf7 = addFeature(store, 25, 25); // sibling of sf6
+    SequenceFeature sf8 = addFeature(store, 24, 24); // child of sf6
+    SequenceFeature sf9 = addFeature(store, 23, 23); // child of sf6
+    assertEquals(store.getPositionalFeatures().size(), 6);
+
+    // delete a node with children - they take its place
+    assertTrue(store.delete(sf6)); // sf8, sf9 should become children of sf5
+    assertEquals(store.getPositionalFeatures().size(), 5);
+    assertFalse(store.getPositionalFeatures().contains(sf6));
+
+    // delete a node with no children
+    assertTrue(store.delete(sf7));
+    assertEquals(store.getPositionalFeatures().size(), 4);
+    assertFalse(store.getPositionalFeatures().contains(sf7));
+
+    // delete root of NCList
+    assertTrue(store.delete(sf5));
+    assertEquals(store.getPositionalFeatures().size(), 3);
+    assertFalse(store.getPositionalFeatures().contains(sf5));
+
+    // continue the killing fields
+    assertTrue(store.delete(sf4));
+    assertEquals(store.getPositionalFeatures().size(), 2);
+    assertFalse(store.getPositionalFeatures().contains(sf4));
+
+    assertTrue(store.delete(sf9));
+    assertEquals(store.getPositionalFeatures().size(), 1);
+    assertFalse(store.getPositionalFeatures().contains(sf9));
+
+    assertTrue(store.delete(sf8));
+    assertTrue(store.getPositionalFeatures().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testAddFeature()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, null);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, null);
+
+    assertTrue(fs.addFeature(sf1));
+    assertEquals(fs.getFeatureCount(true), 1); // positional
+    assertEquals(fs.getFeatureCount(false), 0); // non-positional
+
+    /*
+     * re-adding the same or an identical feature should fail
+     */
+    assertFalse(fs.addFeature(sf1));
+    assertEquals(fs.getFeatureCount(true), 1);
+    assertFalse(fs.addFeature(sf2));
+    assertEquals(fs.getFeatureCount(true), 1);
+
+    /*
+     * add non-positional
+     */
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
+            null);
+    assertTrue(fs.addFeature(sf3));
+    assertEquals(fs.getFeatureCount(true), 1); // positional
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
+            null);
+    assertFalse(fs.addFeature(sf4)); // already stored
+    assertEquals(fs.getFeatureCount(true), 1); // positional
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+
+    /*
+     * add contact
+     */
+    SequenceFeature sf5 = new SequenceFeature("Disulfide bond", "", 10, 20,
+            Float.NaN, null);
+    assertTrue(fs.addFeature(sf5));
+    assertEquals(fs.getFeatureCount(true), 2); // positional - add 1 for contact
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "", 10, 20,
+            Float.NaN, null);
+    assertFalse(fs.addFeature(sf6)); // already stored
+    assertEquals(fs.getFeatureCount(true), 2); // no change
+    assertEquals(fs.getFeatureCount(false), 1); // no change
+  }
+
+  @Test(groups = "Functional")
+  public void testIsEmpty()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 0);
+
+    /*
+     * non-nested feature
+     */
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, null);
+    fs.addFeature(sf1);
+    assertFalse(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 1);
+    fs.delete(sf1);
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 0);
+
+    /*
+     * non-positional feature
+     */
+    sf1 = new SequenceFeature("Cath", "", 0, 0, Float.NaN, null);
+    fs.addFeature(sf1);
+    assertFalse(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(false), 1); // non-positional
+    assertEquals(fs.getFeatureCount(true), 0); // positional
+    fs.delete(sf1);
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(false), 0);
+
+    /*
+     * contact feature
+     */
+    sf1 = new SequenceFeature("Disulfide bond", "", 19, 49, Float.NaN, null);
+    fs.addFeature(sf1);
+    assertFalse(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 1);
+    fs.delete(sf1);
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getFeatureCount(true), 0);
+
+    /*
+     * sf2, sf3 added as nested features
+     */
+    sf1 = new SequenceFeature("Cath", "", 19, 49, Float.NaN, null);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 20, 40,
+            Float.NaN, null);
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 25, 35,
+            Float.NaN, null);
+    fs.addFeature(sf1);
+    fs.addFeature(sf2);
+    fs.addFeature(sf3);
+    assertEquals(fs.getFeatureCount(true), 3);
+    assertTrue(fs.delete(sf1));
+    assertEquals(fs.getFeatureCount(true), 2);
+    // FeatureStore should now only contain features in the NCList
+    assertTrue(fs.nonNestedFeatures.isEmpty());
+    assertEquals(fs.nestedFeatures.size(), 2);
+    assertFalse(fs.isEmpty());
+    assertTrue(fs.delete(sf2));
+    assertEquals(fs.getFeatureCount(true), 1);
+    assertFalse(fs.isEmpty());
+    assertTrue(fs.delete(sf3));
+    assertEquals(fs.getFeatureCount(true), 0);
+    assertTrue(fs.isEmpty()); // all gone
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureGroups()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertTrue(fs.getFeatureGroups(true).isEmpty());
+    assertTrue(fs.getFeatureGroups(false).isEmpty());
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f, "group1");
+    fs.addFeature(sf1);
+    Set<String> groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("group1"));
+
+    /*
+     * add another feature of the same group, delete one, delete both
+     */
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "group1");
+    fs.addFeature(sf2);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("group1"));
+    fs.delete(sf2);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("group1"));
+    fs.delete(sf1);
+    groups = fs.getFeatureGroups(true);
+    assertTrue(fs.getFeatureGroups(true).isEmpty());
+
+    SequenceFeature sf3 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "group2");
+    fs.addFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Cath", "desc", 20, 30, 1f, "Group2");
+    fs.addFeature(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 20, 30, 1f, null);
+    fs.addFeature(sf5);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 3);
+    assertTrue(groups.contains("group2"));
+    assertTrue(groups.contains("Group2")); // case sensitive
+    assertTrue(groups.contains(null)); // null allowed
+    assertTrue(fs.getFeatureGroups(false).isEmpty()); // non-positional
+
+    fs.delete(sf3);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 2);
+    assertFalse(groups.contains("group2"));
+    fs.delete(sf4);
+    groups = fs.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertFalse(groups.contains("Group2"));
+    fs.delete(sf5);
+    groups = fs.getFeatureGroups(true);
+    assertTrue(groups.isEmpty());
+
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
+            "CathGroup");
+    fs.addFeature(sf6);
+    groups = fs.getFeatureGroups(false);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("CathGroup"));
+    assertTrue(fs.delete(sf6));
+    assertTrue(fs.getFeatureGroups(false).isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetTotalFeatureLength()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertEquals(fs.getTotalFeatureLength(), 0);
+
+    addFeature(fs, 10, 20); // 11
+    assertEquals(fs.getTotalFeatureLength(), 11);
+    addFeature(fs, 17, 37); // 21
+    SequenceFeature sf1 = addFeature(fs, 14, 74); // 61
+    assertEquals(fs.getTotalFeatureLength(), 93);
+
+    // non-positional features don't count
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
+            "group1");
+    fs.addFeature(sf2);
+    assertEquals(fs.getTotalFeatureLength(), 93);
+
+    // contact features count 1
+    SequenceFeature sf3 = new SequenceFeature("disulphide bond", "desc",
+            15, 35, 1f, "group1");
+    fs.addFeature(sf3);
+    assertEquals(fs.getTotalFeatureLength(), 94);
+
+    assertTrue(fs.delete(sf1));
+    assertEquals(fs.getTotalFeatureLength(), 33);
+    assertFalse(fs.delete(sf1));
+    assertEquals(fs.getTotalFeatureLength(), 33);
+    assertTrue(fs.delete(sf2));
+    assertEquals(fs.getTotalFeatureLength(), 33);
+    assertTrue(fs.delete(sf3));
+    assertEquals(fs.getTotalFeatureLength(), 32);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureLength()
+  {
+    /*
+     * positional feature
+     */
+    SequenceFeature sf1 = new SequenceFeature("Cath", "desc", 10, 20, 1f, "group1");
+    assertEquals(FeatureStore.getFeatureLength(sf1), 11);
+  
+    /*
+     * non-positional feature
+     */
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 0, 0, 1f,
+            "CathGroup");
+    assertEquals(FeatureStore.getFeatureLength(sf2), 0);
+
+    /*
+     * contact feature counts 1
+     */
+    SequenceFeature sf3 = new SequenceFeature("Disulphide Bond", "desc",
+            14, 28, 1f, "AGroup");
+    assertEquals(FeatureStore.getFeatureLength(sf3), 1);
+  }
+
+  @Test(groups = "Functional")
+  public void testMin()
+  {
+    assertEquals(FeatureStore.min(Float.NaN, Float.NaN), Float.NaN);
+    assertEquals(FeatureStore.min(Float.NaN, 2f), 2f);
+    assertEquals(FeatureStore.min(-2f, Float.NaN), -2f);
+    assertEquals(FeatureStore.min(2f, -3f), -3f);
+  }
+
+  @Test(groups = "Functional")
+  public void testMax()
+  {
+    assertEquals(FeatureStore.max(Float.NaN, Float.NaN), Float.NaN);
+    assertEquals(FeatureStore.max(Float.NaN, 2f), 2f);
+    assertEquals(FeatureStore.max(-2f, Float.NaN), -2f);
+    assertEquals(FeatureStore.max(2f, -3f), 2f);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinimumScore_getMaximumScore()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertEquals(fs.getMinimumScore(true), Float.NaN); // positional
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN); // non-positional
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // add features with no score
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 0, 0,
+            Float.NaN, "group");
+    fs.addFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 10, 20,
+            Float.NaN, "group");
+    fs.addFeature(sf2);
+    assertEquals(fs.getMinimumScore(true), Float.NaN);
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // add positional features with score
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 10, 20, 1f,
+            "group");
+    fs.addFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("type", "desc", 12, 16, 4f,
+            "group");
+    fs.addFeature(sf4);
+    assertEquals(fs.getMinimumScore(true), 1f);
+    assertEquals(fs.getMaximumScore(true), 4f);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // add non-positional features with score
+    SequenceFeature sf5 = new SequenceFeature("type", "desc", 0, 0, 11f,
+            "group");
+    fs.addFeature(sf5);
+    SequenceFeature sf6 = new SequenceFeature("type", "desc", 0, 0, -7f,
+            "group");
+    fs.addFeature(sf6);
+    assertEquals(fs.getMinimumScore(true), 1f);
+    assertEquals(fs.getMaximumScore(true), 4f);
+    assertEquals(fs.getMinimumScore(false), -7f);
+    assertEquals(fs.getMaximumScore(false), 11f);
+
+    // delete one positional and one non-positional
+    // min-max should be recomputed
+    assertTrue(fs.delete(sf6));
+    assertTrue(fs.delete(sf3));
+    assertEquals(fs.getMinimumScore(true), 4f);
+    assertEquals(fs.getMaximumScore(true), 4f);
+    assertEquals(fs.getMinimumScore(false), 11f);
+    assertEquals(fs.getMaximumScore(false), 11f);
+
+    // delete remaining features with score
+    assertTrue(fs.delete(sf4));
+    assertTrue(fs.delete(sf5));
+    assertEquals(fs.getMinimumScore(true), Float.NaN);
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+
+    // delete all features
+    assertTrue(fs.delete(sf1));
+    assertTrue(fs.delete(sf2));
+    assertTrue(fs.isEmpty());
+    assertEquals(fs.getMinimumScore(true), Float.NaN);
+    assertEquals(fs.getMaximumScore(true), Float.NaN);
+    assertEquals(fs.getMinimumScore(false), Float.NaN);
+    assertEquals(fs.getMaximumScore(false), Float.NaN);
+  }
+
+  @Test(groups = "Functional")
+  public void testListContains()
+  {
+    assertFalse(FeatureStore.listContains(null, null));
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+    assertFalse(FeatureStore.listContains(features, null));
+
+    SequenceFeature sf1 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
+            "group1");
+    assertFalse(FeatureStore.listContains(null, sf1));
+    assertFalse(FeatureStore.listContains(features, sf1));
+
+    features.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
+            "group1");
+    SequenceFeature sf3 = new SequenceFeature("type1", "desc1", 20, 40, 3f,
+            "group1");
+
+    // sf2.equals(sf1) so contains should return true
+    assertTrue(FeatureStore.listContains(features, sf2));
+    assertFalse(FeatureStore.listContains(features, sf3));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeaturesForGroup()
+  {
+    FeatureStore fs = new FeatureStore();
+
+    /*
+     * with no features
+     */
+    assertTrue(fs.getFeaturesForGroup(true, null).isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, null).isEmpty());
+    assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty());
+
+    /*
+     * sf1: positional feature in the null group
+     */
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            null);
+    fs.addFeature(sf1);
+    assertTrue(fs.getFeaturesForGroup(true, "uniprot").isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, "uniprot").isEmpty());
+    assertTrue(fs.getFeaturesForGroup(false, null).isEmpty());
+    List<SequenceFeature> features = fs.getFeaturesForGroup(true, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+
+    /*
+     * sf2: non-positional feature in the null group
+     * sf3: positional feature in a non-null group
+     * sf4: non-positional feature in a non-null group
+     */
+    SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
+            null);
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            "Uniprot");
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
+            "Rfam");
+    fs.addFeature(sf2);
+    fs.addFeature(sf3);
+    fs.addFeature(sf4);
+
+    features = fs.getFeaturesForGroup(true, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+
+    features = fs.getFeaturesForGroup(false, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+
+    features = fs.getFeaturesForGroup(true, "Uniprot");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+
+    features = fs.getFeaturesForGroup(false, "Rfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf4));
+  }
+
+  @Test(groups = "Functional")
+  public void testShiftFeatures()
+  {
+    FeatureStore fs = new FeatureStore();
+    assertFalse(fs.shiftFeatures(0, 1)); // nothing to do
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
+    fs.addFeature(sf1);
+    // nested feature:
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 8, 14, 0f, null);
+    fs.addFeature(sf2);
+    // contact feature:
+    SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32,
+            0f, null);
+    fs.addFeature(sf3);
+    // non-positional feature:
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f, null);
+    fs.addFeature(sf4);
+
+    /*
+     * shift all features right by 5
+     */
+    assertTrue(fs.shiftFeatures(0, 5));
+
+    // non-positional features untouched:
+    List<SequenceFeature> nonPos = fs.getNonPositionalFeatures();
+    assertEquals(nonPos.size(), 1);
+    assertTrue(nonPos.contains(sf4));
+
+    // positional features are replaced
+    List<SequenceFeature> pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 3);
+    assertFalse(pos.contains(sf1));
+    assertFalse(pos.contains(sf2));
+    assertFalse(pos.contains(sf3));
+    SequenceFeatures.sortFeatures(pos, true); // ascending start pos
+    assertEquals(pos.get(0).getBegin(), 7);
+    assertEquals(pos.get(0).getEnd(), 10);
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 19);
+    assertEquals(pos.get(2).getBegin(), 28);
+    assertEquals(pos.get(2).getEnd(), 37);
+
+    /*
+     * now shift left by 15
+     * feature at [7-10] should be removed
+     * feature at [13-19] should become [1-4] 
+     */
+    assertTrue(fs.shiftFeatures(0, -15));
+    pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 22);
+
+    /*
+     * shift right by 4 from position 2 onwards
+     * feature at [1-4] unchanged, feature at [13-22] shifts
+     */
+    assertTrue(fs.shiftFeatures(2, 4));
+    pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(1).getBegin(), 17);
+    assertEquals(pos.get(1).getEnd(), 26);
+
+    /*
+     * shift right by 4 from position 18 onwards
+     * should be no change
+     */
+    SequenceFeature f1 = pos.get(0);
+    SequenceFeature f2 = pos.get(1);
+    assertFalse(fs.shiftFeatures(18, 4)); // no update
+    pos = fs.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertSame(pos.get(0), f1);
+    assertSame(pos.get(1), f2);
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete_readd()
+  {
+    /*
+     * add a feature and a nested feature
+     */
+    FeatureStore store = new FeatureStore();
+    SequenceFeature sf1 = addFeature(store, 10, 20);
+    // sf2 is nested in sf1 so will be stored in nestedFeatures
+    SequenceFeature sf2 = addFeature(store, 12, 14);
+    List<SequenceFeature> features = store.getPositionalFeatures();
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(store.nonNestedFeatures.contains(sf1));
+    assertTrue(store.nestedFeatures.contains(sf2));
+  
+    /*
+     * delete the first feature
+     */
+    assertTrue(store.delete(sf1));
+    features = store.getPositionalFeatures();
+    assertFalse(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+
+    /*
+     * re-add the 'nested' feature; is it now duplicated?
+     */
+    store.addFeature(sf2);
+    features = store.getPositionalFeatures();
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+  }
+
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    FeatureStore fs = new FeatureStore();
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, "group1");
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 20,
+            Float.NaN, "group2");
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 0, 0, Float.NaN,
+            "group1");
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 0, 0, 0f,
+            "group1");
+    SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "", 5, 15,
+            Float.NaN, "group1");
+    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "", 5, 15,
+            Float.NaN, "group2");
+
+    fs.addFeature(sf1);
+    fs.addFeature(sf3);
+    fs.addFeature(sf5);
+    assertTrue(fs.contains(sf1)); // positional feature
+    assertTrue(fs.contains(new SequenceFeature(sf1))); // identical feature
+    assertFalse(fs.contains(sf2)); // different group
+    assertTrue(fs.contains(sf3)); // non-positional
+    assertTrue(fs.contains(new SequenceFeature(sf3)));
+    assertFalse(fs.contains(sf4)); // different score
+    assertTrue(fs.contains(sf5)); // contact feature
+    assertTrue(fs.contains(new SequenceFeature(sf5)));
+    assertFalse(fs.contains(sf6)); // different group
+
+    /*
+     * add a nested feature
+     */
+    SequenceFeature sf7 = new SequenceFeature("Cath", "", 12, 16,
+            Float.NaN, "group1");
+    fs.addFeature(sf7);
+    assertTrue(fs.contains(sf7));
+    assertTrue(fs.contains(new SequenceFeature(sf7)));
+
+    /*
+     * delete the outer (enclosing, non-nested) feature
+     */
+    fs.delete(sf1);
+    assertFalse(fs.contains(sf1));
+    assertTrue(fs.contains(sf7));
+  }
+}
diff --git a/test/jalview/datamodel/features/NCListTest.java b/test/jalview/datamodel/features/NCListTest.java
new file mode 100644 (file)
index 0000000..2c7f752
--- /dev/null
@@ -0,0 +1,682 @@
+package jalview.datamodel.features;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class NCListTest
+{
+
+  private Random random = new Random(107);
+
+  private Comparator<ContiguousI> sorter = new RangeComparator(true);
+
+  /**
+   * A basic sanity test of the constructor
+   */
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 20));
+    ranges.add(new Range(10, 20));
+    ranges.add(new Range(15, 30));
+    ranges.add(new Range(10, 30));
+    ranges.add(new Range(11, 19));
+    ranges.add(new Range(10, 20));
+    ranges.add(new Range(1, 100));
+
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    String expected = "[1-100 [10-30 [10-20 [10-20 [11-19]]]], 15-30 [20-20]]";
+    assertEquals(ncl.toString(), expected);
+    assertTrue(ncl.isValid());
+
+    Collections.reverse(ranges);
+    ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), expected);
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testFindOverlaps()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    ranges.add(new Range(30, 70));
+    ranges.add(new Range(1, 100));
+    ranges.add(new Range(70, 120));
+  
+    NCList<Range> ncl = new NCList<Range>(ranges);
+
+    List<Range> overlaps = ncl.findOverlaps(121, 122);
+    assertEquals(overlaps.size(), 0);
+
+    overlaps = ncl.findOverlaps(21, 22);
+    assertEquals(overlaps.size(), 2);
+    assertEquals(((ContiguousI) overlaps.get(0)).getBegin(), 1);
+    assertEquals(((ContiguousI) overlaps.get(0)).getEnd(), 100);
+    assertEquals(((ContiguousI) overlaps.get(1)).getBegin(), 20);
+    assertEquals(((ContiguousI) overlaps.get(1)).getEnd(), 50);
+
+    overlaps = ncl.findOverlaps(110, 110);
+    assertEquals(overlaps.size(), 1);
+    assertEquals(((ContiguousI) overlaps.get(0)).getBegin(), 70);
+    assertEquals(((ContiguousI) overlaps.get(0)).getEnd(), 120);
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_onTheEnd()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(60, 70));
+    assertEquals(ncl.toString(), "[20-50, 60-70]");
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_inside()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(30, 40));
+    assertEquals(ncl.toString(), "[20-50 [30-40]]");
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_onTheFront()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(5, 15));
+    assertEquals(ncl.toString(), "[5-15, 20-50]");
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_enclosing()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 50));
+    ranges.add(new Range(30, 60));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-50, 30-60]");
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.getStart(), 20);
+
+    ncl.add(new Range(10, 70));
+    assertEquals(ncl.toString(), "[10-70 [20-50, 30-60]]");
+    assertTrue(ncl.isValid());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_spanning()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(20, 40));
+    ranges.add(new Range(60, 70));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-40, 60-70]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(30, 50));
+    assertEquals(ncl.toString(), "[20-40, 30-50, 60-70]");
+    assertTrue(ncl.isValid());
+
+    ncl.add(new Range(40, 65));
+    assertEquals(ncl.toString(), "[20-40, 30-50, 40-65, 60-70]");
+    assertTrue(ncl.isValid());
+  }
+
+  /**
+   * Provides the scales for pseudo-random NCLists i.e. the range of the maximal
+   * [0-scale] interval to be stored
+   * 
+   * @return
+   */
+  @DataProvider(name = "scalesOfLife")
+  public Object[][] getScales()
+  {
+    return new Object[][] { new Integer[] { 10 }, new Integer[] { 100 } };
+  }
+
+  /**
+   * Do a number of pseudo-random (reproducible) builds of an NCList, to
+   * exercise as many methods of the class as possible while generating the
+   * range of possible structure topologies
+   * <ul>
+   * <li>verify that add adds an entry and increments size</li>
+   * <li>...except where the entry is already contained (by equals test)</li>
+   * <li>verify that the structure is valid at all stages of construction</li>
+   * <li>generate, run and verify a range of overlap queries</li>
+   * <li>tear down the structure by deleting entries, verifying correctness at
+   * each stage</li>
+   * </ul>
+   */
+  @Test(groups = "Functional", dataProvider = "scalesOfLife")
+  public void test_pseudoRandom(Integer scale)
+  {
+    NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>(scale);
+    
+    testAdd_pseudoRandom(scale, ncl, features);
+
+    /*
+     * sort the list of added ranges - this doesn't affect the test,
+     * just makes it easier to inspect the data in the debugger
+     */
+    Collections.sort(features, sorter);
+
+    testFindOverlaps_pseudoRandom(ncl, scale, features);
+
+    testDelete_pseudoRandom(ncl, features);
+  }
+
+  /**
+   * Pick randomly selected entries to delete in turn, checking the NCList size
+   * and validity at each stage, until it is empty
+   * 
+   * @param ncl
+   * @param features
+   */
+  protected void testDelete_pseudoRandom(NCList<SequenceFeature> ncl,
+          List<SequenceFeature> features)
+  {
+    int deleted = 0;
+
+    while (!features.isEmpty())
+    {
+      assertEquals(ncl.size(), features.size());
+      int toDelete = random.nextInt(features.size());
+      SequenceFeature entry = features.get(toDelete);
+      assertTrue(ncl.contains(entry), String.format(
+              "NCList doesn't contain entry [%d] '%s'!", deleted,
+              entry.toString()));
+
+      ncl.delete(entry);
+      assertFalse(ncl.contains(entry), String.format(
+              "NCList still contains deleted entry [%d] '%s'!", deleted,
+              entry.toString()));
+      features.remove(toDelete);
+      deleted++;
+
+      assertTrue(ncl.isValid(), String.format(
+              "NCList invalid after %d deletions, last deleted was '%s'",
+              deleted, entry.toString()));
+
+      /*
+       * brute force check that deleting one entry didn't delete any others
+       */
+      for (int i = 0; i < features.size(); i++)
+      {
+        SequenceFeature sf = features.get(i);
+        assertTrue(ncl.contains(sf), String.format(
+                        "NCList doesn't contain entry [%d] %s after deleting '%s'!",
+                        i, sf.toString(), entry.toString()));
+      }
+    }
+    assertEquals(ncl.size(), 0); // all gone
+  }
+
+  /**
+   * Randomly generate entries and add them to the NCList, checking its validity
+   * and size at each stage. A few entries should be duplicates (by equals test)
+   * so not get added.
+   * 
+   * @param scale
+   * @param ncl
+   * @param features
+   */
+  protected void testAdd_pseudoRandom(Integer scale,
+          NCList<SequenceFeature> ncl,
+          List<SequenceFeature> features)
+  {
+    int count = 0;
+    final int size = 50;
+
+    for (int i = 0; i < size; i++)
+    {
+      int r1 = random.nextInt(scale + 1);
+      int r2 = random.nextInt(scale + 1);
+      int from = Math.min(r1, r2);
+      int to = Math.max(r1, r2);
+
+      /*
+       * choice of two feature values means that occasionally an identical
+       * feature may be generated, in which case it should not be added 
+       */
+      float value = (float) i % 2;
+      SequenceFeature feature = new SequenceFeature("Pfam", "", from, to,
+              value, "group");
+
+      /*
+       * add to NCList - with duplicate entries (by equals) disallowed
+       */
+      ncl.add(feature, false);
+      if (features.contains(feature))
+      {
+        System.out.println("Duplicate feature generated "
+                + feature.toString());
+      }
+      else
+      {
+        features.add(feature);
+        count++;
+      }
+    
+      /*
+       * check list format is valid at each stage of its construction
+       */
+      assertTrue(ncl.isValid(),
+              String.format("Failed for scale = %d, i=%d", scale, i));
+      assertEquals(ncl.size(), count);
+    }
+    // System.out.println(ncl.prettyPrint());
+  }
+
+  /**
+   * A helper method that generates pseudo-random range queries and veries that
+   * findOverlaps returns the correct matches
+   * 
+   * @param ncl
+   *          the NCList to query
+   * @param scale
+   *          ncl maximal range is [0, scale]
+   * @param features
+   *          a list of the ranges stored in ncl
+   */
+  protected void testFindOverlaps_pseudoRandom(NCList<SequenceFeature> ncl,
+          int scale,
+          List<SequenceFeature> features)
+  {
+    int halfScale = scale / 2;
+    int minIterations = 20;
+
+    /*
+     * generates ranges in [-halfScale, scale+halfScale]
+     * - some should be internal to [0, scale] P = 1/4
+     * - some should lie before 0 P = 1/16
+     * - some should lie after scale P = 1/16
+     * - some should overlap left P = 1/4
+     * - some should overlap right P = 1/4
+     * - some should enclose P = 1/8
+     * 
+     * 50 iterations give a 96% probability of including the
+     * unlikeliest case; keep going until we have done all!
+     */
+    boolean inside = false;
+    boolean enclosing = false;
+    boolean before = false;
+    boolean after = false;
+    boolean overlapLeft = false;
+    boolean overlapRight = false;
+    boolean allCasesCovered = false;
+
+    int i = 0;
+    while (i < minIterations || !allCasesCovered)
+    {
+      i++;
+      int r1 = random.nextInt((scale + 1) * 2);
+      int r2 = random.nextInt((scale + 1) * 2);
+      int from = Math.min(r1, r2) - halfScale;
+      int to = Math.max(r1, r2) - halfScale;
+
+      /*
+       * ensure all cases of interest get covered
+       */
+      inside |= from >= 0 && to <= scale;
+      enclosing |= from <= 0 && to >= scale;
+      before |= to < 0;
+      after |= from > scale;
+      overlapLeft |= from < 0 && to >= 0 && to <= scale;
+      overlapRight |= from >= 0 && from <= scale && to > scale;
+      if (!allCasesCovered)
+      {
+        allCasesCovered |= inside && enclosing && before && after
+              && overlapLeft && overlapRight;
+        if (allCasesCovered)
+        {
+          System.out
+                  .println(String
+                          .format("Covered all findOverlaps cases after %d iterations for scale %d",
+                                  i, scale));
+        }
+      }
+
+      verifyFindOverlaps(ncl, from, to, features);
+    }
+  }
+
+  /**
+   * A helper method that verifies that overlaps found by interrogating an
+   * NCList correctly match those found by brute force search
+   * 
+   * @param ncl
+   * @param from
+   * @param to
+   * @param features
+   */
+  protected void verifyFindOverlaps(NCList<SequenceFeature> ncl, int from,
+          int to, List<SequenceFeature> features)
+  {
+    List<SequenceFeature> overlaps = ncl.findOverlaps(from, to);
+
+    /*
+     * check returned entries do indeed overlap from-to range
+     */
+    for (ContiguousI sf : overlaps)
+    {
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
+      assertTrue(begin <= to && end >= from, String.format(
+              "[%d, %d] does not overlap query range [%d, %d]", begin, end,
+              from, to));
+    }
+
+    /*
+     * check overlapping ranges are included in the results
+     * (the test above already shows non-overlapping ranges are not)
+     */
+    for (ContiguousI sf : features)
+    {
+      int begin = sf.getBegin();
+      int end = sf.getEnd();
+      if (begin <= to && end >= from)
+      {
+        boolean found = overlaps.contains(sf);
+        assertTrue(found, String.format(
+                "[%d, %d] missing in query range [%d, %d]", begin, end,
+                from, to));
+      }
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testGetEntries()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(20, 20);
+    Range r2 = new Range(10, 20);
+    Range r3 = new Range(15, 30);
+    Range r4 = new Range(10, 30);
+    Range r5 = new Range(11, 19);
+    Range r6 = new Range(10, 20);
+    ranges.add(r1);
+    ranges.add(r2);
+    ranges.add(r3);
+    ranges.add(r4);
+    ranges.add(r5);
+    ranges.add(r6);
+  
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    Range r7 = new Range(1, 100);
+    ncl.add(r7);
+
+    List<Range> contents = ncl.getEntries();
+    assertEquals(contents.size(), 7);
+    assertTrue(contents.contains(r1));
+    assertTrue(contents.contains(r2));
+    assertTrue(contents.contains(r3));
+    assertTrue(contents.contains(r4));
+    assertTrue(contents.contains(r5));
+    assertTrue(contents.contains(r6));
+    assertTrue(contents.contains(r7));
+
+    ncl = new NCList<Range>();
+    assertTrue(ncl.getEntries().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(20, 30);
+    ranges.add(r1);
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.getEntries().contains(r1));
+
+    Range r2 = new Range(20, 30);
+    assertFalse(ncl.delete(null)); // null argument
+    assertFalse(ncl.delete(r2)); // never added
+    assertTrue(ncl.delete(r1)); // success
+    assertTrue(ncl.getEntries().isEmpty());
+
+    /*
+     * tests where object.equals() == true
+     */
+    NCList<SequenceFeature> features = new NCList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    features.add(sf1);
+    assertEquals(sf1, sf2); // sf1.equals(sf2)
+    assertFalse(features.delete(sf2)); // equality is not enough for deletion
+    assertTrue(features.getEntries().contains(sf1)); // still there!
+    assertTrue(features.delete(sf1));
+    assertTrue(features.getEntries().isEmpty()); // gone now
+
+    /*
+     * test with duplicate objects in NCList
+     */
+    features.add(sf1);
+    features.add(sf1);
+    assertEquals(features.getEntries().size(), 2);
+    assertSame(features.getEntries().get(0), sf1);
+    assertSame(features.getEntries().get(1), sf1);
+    assertTrue(features.delete(sf1)); // first match only is deleted
+    assertTrue(features.contains(sf1));
+    assertEquals(features.size(), 1);
+    assertTrue(features.delete(sf1));
+    assertTrue(features.getEntries().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdd_overlapping()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(40, 50));
+    ranges.add(new Range(20, 30));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[20-30, 40-50]");
+    assertTrue(ncl.isValid());
+  
+    /*
+     * add range overlapping internally
+     */
+    ncl.add(new Range(25, 35));
+    assertEquals(ncl.toString(), "[20-30, 25-35, 40-50]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * add range overlapping last range
+     */
+    ncl.add(new Range(45, 55));
+    assertEquals(ncl.toString(), "[20-30, 25-35, 40-50, 45-55]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * add range overlapping first range
+     */
+    ncl.add(new Range(15, 25));
+    assertEquals(ncl.toString(), "[15-25, 20-30, 25-35, 40-50, 45-55]");
+    assertTrue(ncl.isValid());
+  }
+
+  /**
+   * Test the contains method (which uses object equals test)
+   */
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    NCList<SequenceFeature> ncl = new NCList<SequenceFeature>();
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "anothergroup");
+    ncl.add(sf1);
+
+    assertTrue(ncl.contains(sf1));
+    assertTrue(ncl.contains(sf2)); // sf1.equals(sf2)
+    assertFalse(ncl.contains(sf3)); // !sf1.equals(sf3)
+
+    /*
+     * make some deeper structure in the NCList
+     */
+    SequenceFeature sf4 = new SequenceFeature("type", "desc", 2, 9, 2f,
+            "group");
+    ncl.add(sf4);
+    assertTrue(ncl.contains(sf4));
+    SequenceFeature sf5 = new SequenceFeature("type", "desc", 4, 5, 2f,
+            "group");
+    SequenceFeature sf6 = new SequenceFeature("type", "desc", 6, 8, 2f,
+            "group");
+    ncl.add(sf5);
+    ncl.add(sf6);
+    assertTrue(ncl.contains(sf5));
+    assertTrue(ncl.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testIsValid()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    Range r1 = new Range(40, 50);
+    ranges.add(r1);
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.isValid());
+
+    Range r2 = new Range(42, 44);
+    ncl.add(r2);
+    assertTrue(ncl.isValid());
+    Range r3 = new Range(46, 48);
+    ncl.add(r3);
+    assertTrue(ncl.isValid());
+    Range r4 = new Range(43, 43);
+    ncl.add(r4);
+    assertTrue(ncl.isValid());
+
+    assertEquals(ncl.toString(), "[40-50 [42-44 [43-43], 46-48]]");
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r1, "start", 43);
+    assertFalse(ncl.isValid()); // r2 not inside r1
+    PA.setValue(r1, "start", 40);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r3, "start", 41);
+    assertFalse(ncl.isValid()); // r3 should precede r2
+    PA.setValue(r3, "start", 46);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r4, "start", 41);
+    assertFalse(ncl.isValid()); // r4 not inside r2
+    PA.setValue(r4, "start", 43);
+    assertTrue(ncl.isValid());
+
+    PA.setValue(r4, "start", 44);
+    assertFalse(ncl.isValid()); // r4 has reverse range
+  }
+
+  @Test(groups = "Functional")
+  public void testPrettyPrint()
+  {
+    /*
+     * construct NCList from a list of ranges
+     * they are sorted then assembled into NCList subregions
+     * notice that 42-42 end up inside 41-46
+     */
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(40, 50));
+    ranges.add(new Range(45, 55));
+    ranges.add(new Range(40, 45));
+    ranges.add(new Range(41, 46));
+    ranges.add(new Range(42, 42));
+    ranges.add(new Range(42, 42));
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.toString(),
+            "[40-50 [40-45], 41-46 [42-42 [42-42]], 45-55]");
+    String expected = "40-50\n  40-45\n41-46\n  42-42\n    42-42\n45-55\n";
+    assertEquals(ncl.prettyPrint(), expected);
+
+    /*
+     * repeat but now add ranges one at a time
+     * notice that 42-42 end up inside 40-50 so we get
+     * a different but equal valid NCList structure
+     */
+    ranges.clear();
+    ncl = new NCList<Range>(ranges);
+    ncl.add(new Range(40, 50));
+    ncl.add(new Range(45, 55));
+    ncl.add(new Range(40, 45));
+    ncl.add(new Range(41, 46));
+    ncl.add(new Range(42, 42));
+    ncl.add(new Range(42, 42));
+    assertTrue(ncl.isValid());
+    assertEquals(ncl.toString(),
+            "[40-50 [40-45 [42-42 [42-42]], 41-46], 45-55]");
+    expected = "40-50\n  40-45\n    42-42\n      42-42\n  41-46\n45-55\n";
+    assertEquals(ncl.prettyPrint(), expected);
+  }
+
+  /**
+   * A test that shows different valid trees can be constructed from the same
+   * set of ranges, depending on the order of construction
+   */
+  @Test(groups = "Functional")
+  public void testConstructor_alternativeTrees()
+  {
+    List<Range> ranges = new ArrayList<Range>();
+    ranges.add(new Range(10, 60));
+    ranges.add(new Range(20, 30));
+    ranges.add(new Range(40, 50));
+  
+    /*
+     * constructor with greedy traversal of sorted ranges to build nested
+     * containment lists results in 20-30 inside 10-60, 40-50 a sibling
+     */
+    NCList<Range> ncl = new NCList<Range>(ranges);
+    assertEquals(ncl.toString(), "[10-60 [20-30], 40-50]");
+    assertTrue(ncl.isValid());
+
+    /*
+     * adding ranges one at a time results in 40-50 
+     * a sibling of 20-30 inside 10-60
+     */
+    ncl = new NCList<Range>(new Range(10, 60));
+    ncl.add(new Range(20, 30));
+    ncl.add(new Range(40, 50));
+    assertEquals(ncl.toString(), "[10-60 [20-30, 40-50]]");
+    assertTrue(ncl.isValid());
+  }
+}
diff --git a/test/jalview/datamodel/features/NCNodeTest.java b/test/jalview/datamodel/features/NCNodeTest.java
new file mode 100644 (file)
index 0000000..4713084
--- /dev/null
@@ -0,0 +1,136 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.Range;
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class NCNodeTest
+{
+  @Test(groups = "Functional")
+  public void testAdd()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    assertEquals(node.getBegin(), 10);
+    Range r2 = new Range(10, 15);
+    node.add(r2);
+
+    List<Range> contents = new ArrayList<Range>();
+    node.getEntries(contents);
+    assertEquals(contents.size(), 2);
+    assertTrue(contents.contains(r1));
+    assertTrue(contents.contains(r2));
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testAdd_invalidRangeStart()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    assertEquals(node.getBegin(), 10);
+    Range r2 = new Range(9, 15);
+    node.add(r2);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testAdd_invalidRangeEnd()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    assertEquals(node.getBegin(), 10);
+    Range r2 = new Range(12, 21);
+    node.add(r2);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetEntries()
+  {
+    Range r1 = new Range(10, 20);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    List<Range> entries = new ArrayList<Range>();
+
+    node.getEntries(entries);
+    assertEquals(entries.size(), 1);
+    assertTrue(entries.contains(r1));
+
+    // clearing the returned list does not affect the NCNode
+    entries.clear();
+    node.getEntries(entries);
+    assertEquals(entries.size(), 1);
+    assertTrue(entries.contains(r1));
+
+    Range r2 = new Range(15, 18);
+    node.add(r2);
+    entries.clear();
+    node.getEntries(entries);
+    assertEquals(entries.size(), 2);
+    assertTrue(entries.contains(r1));
+    assertTrue(entries.contains(r2));
+  }
+
+  /**
+   * Tests for the contains method (uses entry.equals() test)
+   */
+  @Test(groups = "Functional")
+  public void testContains()
+  {
+    SequenceFeature sf1 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "group");
+    SequenceFeature sf3 = new SequenceFeature("type", "desc", 1, 10, 2f,
+            "anothergroup");
+    NCNode<SequenceFeature> node = new NCNode<SequenceFeature>(sf1);
+
+    assertFalse(node.contains(null));
+    assertTrue(node.contains(sf1));
+    assertTrue(node.contains(sf2)); // sf1.equals(sf2)
+    assertFalse(node.contains(sf3)); // !sf1.equals(sf3)
+  }
+
+  /**
+   * Test method that checks for valid structure. Valid means that all
+   * subregions (if any) lie within the root range, and that all subregions have
+   * valid structure.
+   */
+  @Test(groups = "Functional")
+  public void testIsValid()
+  {
+    Range r1 = new Range(10, 20);
+    Range r2 = new Range(14, 15);
+    Range r3 = new Range(16, 17);
+    NCNode<Range> node = new NCNode<Range>(r1);
+    node.add(r2);
+    node.add(r3);
+
+    /*
+     * node has root range [10-20] and contains an
+     * NCList of [14-15, 16-17]
+     */
+    assertTrue(node.isValid());
+    PA.setValue(r1, "start", 15);
+    assertFalse(node.isValid()); // r2 not within r1
+    PA.setValue(r1, "start", 10);
+    assertTrue(node.isValid());
+    PA.setValue(r1, "end", 16);
+    assertFalse(node.isValid()); // r3 not within r1
+    PA.setValue(r1, "end", 20);
+    assertTrue(node.isValid());
+    PA.setValue(r3, "start", 12);
+    assertFalse(node.isValid()); // r3 should precede r2
+  }
+}
diff --git a/test/jalview/datamodel/features/RangeComparatorTest.java b/test/jalview/datamodel/features/RangeComparatorTest.java
new file mode 100644 (file)
index 0000000..4849b38
--- /dev/null
@@ -0,0 +1,65 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.ContiguousI;
+import jalview.datamodel.Range;
+
+import java.util.Comparator;
+
+import org.testng.annotations.Test;
+
+public class RangeComparatorTest
+{
+
+  @Test(groups = "Functional")
+  public void testCompare()
+  {
+    RangeComparator comp = new RangeComparator(true);
+
+    // same position, same length
+    assertEquals(comp.compare(10, 10, 20, 20), 0);
+    // same position, len1 > len2
+    assertEquals(comp.compare(10, 10, 20, 19), -1);
+    // same position, len1 < len2
+    assertEquals(comp.compare(10, 10, 20, 21), 1);
+    // pos1 > pos2
+    assertEquals(comp.compare(11, 10, 20, 20), 1);
+    // pos1 < pos2
+    assertEquals(comp.compare(10, 11, 20, 10), -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testCompare_byStart()
+  {
+    Comparator<ContiguousI> comp = RangeComparator.BY_START_POSITION;
+
+    // same start position, same length
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 20)), 0);
+    // same start position, len1 > len2
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 19)), -1);
+    // same start position, len1 < len2
+    assertEquals(comp.compare(new Range(10, 18), new Range(10, 20)), 1);
+    // pos1 > pos2
+    assertEquals(comp.compare(new Range(11, 20), new Range(10, 20)), 1);
+    // pos1 < pos2
+    assertEquals(comp.compare(new Range(10, 20), new Range(11, 20)), -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testCompare_byEnd()
+  {
+    Comparator<ContiguousI> comp = RangeComparator.BY_END_POSITION;
+
+    // same end position, same length
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 20)), 0);
+    // same end position, len1 > len2
+    assertEquals(comp.compare(new Range(10, 20), new Range(11, 20)), -1);
+    // same end position, len1 < len2
+    assertEquals(comp.compare(new Range(11, 20), new Range(10, 20)), 1);
+    // end1 > end2
+    assertEquals(comp.compare(new Range(10, 21), new Range(10, 20)), 1);
+    // end1 < end2
+    assertEquals(comp.compare(new Range(10, 20), new Range(10, 21)), -1);
+  }
+}
diff --git a/test/jalview/datamodel/features/SequenceFeaturesTest.java b/test/jalview/datamodel/features/SequenceFeaturesTest.java
new file mode 100644 (file)
index 0000000..29e76bb
--- /dev/null
@@ -0,0 +1,1278 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class SequenceFeaturesTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertFalse(store.hasFeatures());
+
+    store = new SequenceFeatures((List<SequenceFeature>) null);
+    assertFalse(store.hasFeatures());
+
+    List<SequenceFeature> features = new ArrayList<>();
+    store = new SequenceFeatures(features);
+    assertFalse(store.hasFeatures());
+
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    features.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 15, 18,
+            Float.NaN, null);
+    features.add(sf2); // nested
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc2", 0, 0,
+            Float.NaN, null); // non-positional
+    features.add(sf3);
+    store = new SequenceFeatures(features);
+    assertTrue(store.hasFeatures());
+    assertEquals(2, store.getFeatureCount(true)); // positional
+    assertEquals(1, store.getFeatureCount(false)); // non-positional
+    assertFalse(store.add(sf1)); // already contained
+    assertFalse(store.add(sf2)); // already contained
+    assertFalse(store.add(sf3)); // already contained
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPositionalFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // same range, different description
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc2", 10, 20,
+            Float.NaN, null);
+    store.add(sf2);
+    // discontiguous range
+    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(sf3);
+    // overlapping range
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 15, 35,
+            Float.NaN, null);
+    store.add(sf4);
+    // enclosing range
+    SequenceFeature sf5 = new SequenceFeature("Metal", "desc", 5, 50,
+            Float.NaN, null);
+    store.add(sf5);
+    // non-positional feature
+    SequenceFeature sf6 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf6);
+    // contact feature
+    SequenceFeature sf7 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.add(sf7);
+    // different feature type
+    SequenceFeature sf8 = new SequenceFeature("Pfam", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(sf8);
+    SequenceFeature sf9 = new SequenceFeature("Pfam", "desc", 15, 35,
+            Float.NaN, null);
+    store.add(sf9);
+
+    /*
+     * get all positional features
+     */
+    List<SequenceFeature> features = store.getPositionalFeatures();
+    assertEquals(features.size(), 8);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertFalse(features.contains(sf6)); // non-positional
+    assertTrue(features.contains(sf7));
+    assertTrue(features.contains(sf8));
+    assertTrue(features.contains(sf9));
+
+    /*
+     * get features by type
+     */
+    assertTrue(store.getPositionalFeatures((String) null).isEmpty());
+    assertTrue(store.getPositionalFeatures("Cath").isEmpty());
+    assertTrue(store.getPositionalFeatures("METAL").isEmpty());
+
+    features = store.getPositionalFeatures("Metal");
+    assertEquals(features.size(), 5);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertFalse(features.contains(sf6));
+
+    features = store.getPositionalFeatures("Disulphide bond");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf7));
+
+    features = store.getPositionalFeatures("Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf8));
+    assertTrue(features.contains(sf9));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetContactFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    // non-contact
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // non-positional
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf2);
+    // contact feature
+    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.add(sf3);
+    // repeat for different feature type
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18,
+            45, Float.NaN, null);
+    store.add(sf6);
+  
+    /*
+     * get all contact features
+     */
+    List<SequenceFeature> features = store.getContactFeatures();
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf6));
+  
+    /*
+     * get contact features by type
+     */
+    assertTrue(store.getContactFeatures((String) null).isEmpty());
+    assertTrue(store.getContactFeatures("Cath").isEmpty());
+    assertTrue(store.getContactFeatures("Pfam").isEmpty());
+    assertTrue(store.getContactFeatures("DISULPHIDE BOND").isEmpty());
+  
+    features = store.getContactFeatures("Disulphide bond");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+  
+    features = store.getContactFeatures("Disulfide bond");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf6));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetNonPositionalFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    // positional
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // non-positional
+    SequenceFeature sf2 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf2);
+    // contact feature
+    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc",
+            18, 45, Float.NaN, null);
+    store.add(sf3);
+    // repeat for different feature type
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Disulfide bond", "desc", 18,
+            45, Float.NaN, null);
+    store.add(sf6);
+    // one more non-positional, different description
+    SequenceFeature sf7 = new SequenceFeature("Pfam", "desc2", 0, 0,
+            Float.NaN, null);
+    store.add(sf7);
+  
+    /*
+     * get all non-positional features
+     */
+    List<SequenceFeature> features = store.getNonPositionalFeatures();
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf7));
+  
+    /*
+     * get non-positional features by type
+     */
+    assertTrue(store.getNonPositionalFeatures((String) null).isEmpty());
+    assertTrue(store.getNonPositionalFeatures("Cath").isEmpty());
+    assertTrue(store.getNonPositionalFeatures("PFAM").isEmpty());
+  
+    features = store.getNonPositionalFeatures("Metal");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+  
+    features = store.getNonPositionalFeatures("Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf7));
+  }
+
+  /**
+   * Helper method to add a feature of no particular type
+   * 
+   * @param sf
+   * @param type
+   * @param from
+   * @param to
+   * @return
+   */
+  SequenceFeature addFeature(SequenceFeaturesI sf, String type, int from,
+          int to)
+  {
+    SequenceFeature sf1 = new SequenceFeature(type, "", from, to,
+            Float.NaN,
+            null);
+    sf.add(sf1);
+    return sf1;
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatures()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
+    SequenceFeature sf2 = addFeature(sf, "Pfam", 1, 15);
+    SequenceFeature sf3 = addFeature(sf, "Pfam", 20, 30);
+    SequenceFeature sf4 = addFeature(sf, "Pfam", 40, 100);
+    SequenceFeature sf5 = addFeature(sf, "Pfam", 60, 100);
+    SequenceFeature sf6 = addFeature(sf, "Pfam", 70, 70);
+    SequenceFeature sf7 = addFeature(sf, "Cath", 10, 50);
+    SequenceFeature sf8 = addFeature(sf, "Cath", 1, 15);
+    SequenceFeature sf9 = addFeature(sf, "Cath", 20, 30);
+    SequenceFeature sf10 = addFeature(sf, "Cath", 40, 100);
+    SequenceFeature sf11 = addFeature(sf, "Cath", 60, 100);
+    SequenceFeature sf12 = addFeature(sf, "Cath", 70, 70);
+  
+    List<SequenceFeature> overlaps = sf.findFeatures(200, 200, "Pfam");
+    assertTrue(overlaps.isEmpty());
+  
+    overlaps = sf.findFeatures( 1, 9, "Pfam");
+    assertEquals(overlaps.size(), 1);
+    assertTrue(overlaps.contains(sf2));
+  
+    overlaps = sf.findFeatures( 5, 18, "Pfam");
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf2));
+  
+    overlaps = sf.findFeatures(30, 40, "Pfam");
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf1));
+    assertTrue(overlaps.contains(sf3));
+    assertTrue(overlaps.contains(sf4));
+  
+    overlaps = sf.findFeatures( 80, 90, "Pfam");
+    assertEquals(overlaps.size(), 2);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+  
+    overlaps = sf.findFeatures( 68, 70, "Pfam");
+    assertEquals(overlaps.size(), 3);
+    assertTrue(overlaps.contains(sf4));
+    assertTrue(overlaps.contains(sf5));
+    assertTrue(overlaps.contains(sf6));
+
+    overlaps = sf.findFeatures(16, 69, "Cath");
+    assertEquals(overlaps.size(), 4);
+    assertTrue(overlaps.contains(sf7));
+    assertFalse(overlaps.contains(sf8));
+    assertTrue(overlaps.contains(sf9));
+    assertTrue(overlaps.contains(sf10));
+    assertTrue(overlaps.contains(sf11));
+    assertFalse(overlaps.contains(sf12));
+
+    assertTrue(sf.findFeatures(0, 1000, "Metal").isEmpty());
+
+    overlaps = sf.findFeatures(7, 7, (String) null);
+    assertTrue(overlaps.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testDelete()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
+    assertTrue(sf.getPositionalFeatures().contains(sf1));
+
+    assertFalse(sf.delete(null));
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 10, 15, 0f, null);
+    assertFalse(sf.delete(sf2)); // not added, can't delete it
+    assertTrue(sf.delete(sf1));
+    assertTrue(sf.getPositionalFeatures().isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testHasFeatures()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    assertFalse(sf.hasFeatures());
+
+    SequenceFeature sf1 = addFeature(sf, "Pfam", 10, 50);
+    assertTrue(sf.hasFeatures());
+
+    sf.delete(sf1);
+    assertFalse(sf.hasFeatures());
+  }
+
+  /**
+   * Tests for the method that gets feature groups for positional or
+   * non-positional features
+   */
+  @Test(groups = "Functional")
+  public void testGetFeatureGroups()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    assertTrue(sf.getFeatureGroups(true).isEmpty());
+    assertTrue(sf.getFeatureGroups(false).isEmpty());
+
+    /*
+     * add a non-positional feature (begin/end = 0/0)
+     */
+    SequenceFeature sfx = new SequenceFeature("AType", "Desc", 0, 0, 0f,
+            "AGroup");
+    sf.add(sfx);
+    Set<String> groups = sf.getFeatureGroups(true); // for positional
+    assertTrue(groups.isEmpty());
+    groups = sf.getFeatureGroups(false); // for non-positional
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+    groups = sf.getFeatureGroups(false, "AType");
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+    groups = sf.getFeatureGroups(true, "AnotherType");
+    assertTrue(groups.isEmpty());
+
+    /*
+     * add, then delete, more non-positional features of different types
+     */
+    SequenceFeature sfy = new SequenceFeature("AnotherType", "Desc", 0, 0,
+            0f,
+            "AnotherGroup");
+    sf.add(sfy);
+    SequenceFeature sfz = new SequenceFeature("AThirdType", "Desc", 0, 0,
+            0f,
+            null);
+    sf.add(sfz);
+    groups = sf.getFeatureGroups(false);
+    assertEquals(groups.size(), 3);
+    assertTrue(groups.contains("AGroup"));
+    assertTrue(groups.contains("AnotherGroup"));
+    assertTrue(groups.contains(null)); // null is a possible group
+    sf.delete(sfz);
+    sf.delete(sfy);
+    groups = sf.getFeatureGroups(false);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+
+    /*
+     * add positional features
+     */
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "Desc", 10, 50, 0f,
+            "PfamGroup");
+    sf.add(sf1);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("PfamGroup"));
+    groups = sf.getFeatureGroups(false); // non-positional unchanged
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("AGroup"));
+
+    SequenceFeature sf2 = new SequenceFeature("Cath", "Desc", 10, 50, 0f,
+            null);
+    sf.add(sf2);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("PfamGroup"));
+    assertTrue(groups.contains(null));
+
+    sf.delete(sf1);
+    sf.delete(sf2);
+    assertTrue(sf.getFeatureGroups(true).isEmpty());
+
+    SequenceFeature sf3 = new SequenceFeature("CDS", "", 10, 50, 0f,
+            "Ensembl");
+    sf.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("exon", "", 10, 50, 0f,
+            "Ensembl");
+    sf.add(sf4);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("Ensembl"));
+
+    /*
+     * delete last Ensembl group feature from CDS features
+     * but still have one in exon features
+     */
+    sf.delete(sf3);
+    groups = sf.getFeatureGroups(true);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("Ensembl"));
+
+    /*
+     * delete the last non-positional feature
+     */
+    sf.delete(sfx);
+    groups = sf.getFeatureGroups(false);
+    assertTrue(groups.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureTypesForGroups()
+  {
+    SequenceFeaturesI sf = new SequenceFeatures();
+    assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty());
+  
+    /*
+     * add feature with group = "Uniprot", type = "helix"
+     */
+    String groupUniprot = "Uniprot";
+    SequenceFeature sf1 = new SequenceFeature("helix", "Desc", 10, 50, 0f,
+            groupUniprot);
+    sf.add(sf1);
+    Set<String> groups = sf.getFeatureTypesForGroups(true, groupUniprot);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("helix"));
+    assertTrue(sf.getFeatureTypesForGroups(true, (String) null).isEmpty());
+  
+    /*
+     * add feature with group = "Uniprot", type = "strand"
+     */
+    SequenceFeature sf2 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
+            groupUniprot);
+    sf.add(sf2);
+    groups = sf.getFeatureTypesForGroups(true, groupUniprot);
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("helix"));
+    assertTrue(groups.contains("strand"));
+
+    /*
+     * delete the "strand" Uniprot feature - still have "helix"
+     */
+    sf.delete(sf2);
+    groups = sf.getFeatureTypesForGroups(true, groupUniprot);
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("helix"));
+
+    /*
+     * delete the "helix" Uniprot feature - none left
+     */
+    sf.delete(sf1);
+    assertTrue(sf.getFeatureTypesForGroups(true, groupUniprot).isEmpty());
+
+    /*
+     * add some null group features
+     */
+    SequenceFeature sf3 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
+            null);
+    sf.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("turn", "Desc", 10, 50, 0f,
+            null);
+    sf.add(sf4);
+    groups = sf.getFeatureTypesForGroups(true, (String) null);
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("strand"));
+    assertTrue(groups.contains("turn"));
+
+    /*
+     * add strand/Cath  and turn/Scop and query for one or both groups
+     * (find feature types for groups selected in Feature Settings)
+     */
+    SequenceFeature sf5 = new SequenceFeature("strand", "Desc", 10, 50, 0f,
+            "Cath");
+    sf.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("turn", "Desc", 10, 50, 0f,
+            "Scop");
+    sf.add(sf6);
+    groups = sf.getFeatureTypesForGroups(true, "Cath");
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("strand"));
+    groups = sf.getFeatureTypesForGroups(true, "Scop");
+    assertEquals(groups.size(), 1);
+    assertTrue(groups.contains("turn"));
+    groups = sf.getFeatureTypesForGroups(true, "Cath", "Scop");
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("turn"));
+    assertTrue(groups.contains("strand"));
+    // alternative vararg syntax
+    groups = sf.getFeatureTypesForGroups(true, new String[] { "Cath",
+        "Scop" });
+    assertEquals(groups.size(), 2);
+    assertTrue(groups.contains("turn"));
+    assertTrue(groups.contains("strand"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureTypes()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    Set<String> types = store.getFeatureTypes();
+    assertTrue(types.isEmpty());
+
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 1);
+    assertTrue(types.contains("Metal"));
+
+    // null type is rejected...
+    SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20,
+            Float.NaN, null);
+    assertFalse(store.add(sf2));
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 1);
+    assertFalse(types.contains(null));
+    assertTrue(types.contains("Metal"));
+
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 2);
+    assertTrue(types.contains("Pfam"));
+
+    /*
+     * add contact feature
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 3);
+    assertTrue(types.contains("Disulphide Bond"));
+
+    /*
+     * add another Pfam
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 3); // unchanged
+
+    /*
+     * delete first Pfam - still have one
+     */
+    assertTrue(store.delete(sf3));
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 3);
+    assertTrue(types.contains("Pfam"));
+
+    /*
+     * delete second Pfam - no longer have one
+     */
+    assertTrue(store.delete(sf5));
+    types = store.getFeatureTypes();
+    assertEquals(types.size(), 2);
+    assertFalse(types.contains("Pfam"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureCount()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertEquals(store.getFeatureCount(true), 0);
+    assertEquals(store.getFeatureCount(false), 0);
+  
+    /*
+     * add positional
+     */
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    assertEquals(store.getFeatureCount(true), 1);
+    assertEquals(store.getFeatureCount(false), 0);
+
+    /*
+     * null feature type is rejected
+     */
+    SequenceFeature sf2 = new SequenceFeature(null, "desc", 10, 20,
+            Float.NaN, null);
+    assertFalse(store.add(sf2));
+    assertEquals(store.getFeatureCount(true), 1);
+    assertEquals(store.getFeatureCount(false), 0);
+  
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    assertEquals(store.getFeatureCount(true), 1);
+    assertEquals(store.getFeatureCount(false), 1);
+  
+    /*
+     * add contact feature (counts as 1)
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    assertEquals(store.getFeatureCount(true), 2);
+    assertEquals(store.getFeatureCount(false), 1);
+  
+    /*
+     * add another Pfam but this time as a positional feature
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    assertEquals(store.getFeatureCount(true), 3); // sf1, sf4, sf5
+    assertEquals(store.getFeatureCount(false), 1); // sf3
+    assertEquals(store.getFeatureCount(true, "Pfam"), 1); // positional
+    assertEquals(store.getFeatureCount(false, "Pfam"), 1); // non-positional
+    // search for type==null
+    assertEquals(store.getFeatureCount(true, (String) null), 0);
+    // search with no type specified
+    assertEquals(store.getFeatureCount(true, (String[]) null), 3);
+    assertEquals(store.getFeatureCount(true, "Metal", "Cath"), 1);
+    assertEquals(store.getFeatureCount(true, "Disulphide Bond"), 1);
+    assertEquals(store.getFeatureCount(true, "Metal", "Pfam", null), 2);
+
+    /*
+     * delete first Pfam (non-positional)
+     */
+    assertTrue(store.delete(sf3));
+    assertEquals(store.getFeatureCount(true), 3);
+    assertEquals(store.getFeatureCount(false), 0);
+  
+    /*
+     * delete second Pfam (positional)
+     */
+    assertTrue(store.delete(sf5));
+    assertEquals(store.getFeatureCount(true), 2);
+    assertEquals(store.getFeatureCount(false), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAllFeatures()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    List<SequenceFeature> features = store.getAllFeatures();
+    assertTrue(features.isEmpty());
+  
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+  
+    SequenceFeature sf2 = new SequenceFeature("Metallic", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf2);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf2));
+  
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf3));
+  
+    /*
+     * add contact feature
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 4);
+    assertTrue(features.contains(sf4));
+  
+    /*
+     * add another Pfam
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 5);
+    assertTrue(features.contains(sf5));
+
+    /*
+     * select by type does not apply to non-positional features
+     */
+    features = store.getAllFeatures("Cath");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+
+    features = store.getAllFeatures("Pfam", "Cath", "Metal");
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+  
+    /*
+     * delete first Pfam
+     */
+    assertTrue(store.delete(sf3));
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 4);
+    assertFalse(features.contains(sf3));
+  
+    /*
+     * delete second Pfam
+     */
+    assertTrue(store.delete(sf5));
+    features = store.getAllFeatures();
+    assertEquals(features.size(), 3);
+    assertFalse(features.contains(sf3));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetTotalFeatureLength()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertEquals(store.getTotalFeatureLength(), 0);
+
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 10, 20,
+            Float.NaN, null);
+    assertTrue(store.add(sf1));
+    assertEquals(store.getTotalFeatureLength(), 11);
+    assertEquals(store.getTotalFeatureLength("Metal"), 11);
+    assertEquals(store.getTotalFeatureLength("Plastic"), 0);
+
+    // re-add does nothing!
+    assertFalse(store.add(sf1));
+    assertEquals(store.getTotalFeatureLength(), 11);
+
+    /*
+     * add non-positional feature
+     */
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    assertEquals(store.getTotalFeatureLength(), 11);
+
+    /*
+     * add contact feature - counts 1 to feature length
+     */
+    SequenceFeature sf4 = new SequenceFeature("Disulphide Bond", "desc",
+            10, 20, Float.NaN, null);
+    store.add(sf4);
+    assertEquals(store.getTotalFeatureLength(), 12);
+
+    /*
+     * add another Pfam
+     */
+    SequenceFeature sf5 = new SequenceFeature("Pfam", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf5);
+    assertEquals(store.getTotalFeatureLength(), 23);
+
+    /*
+     * delete features
+     */
+    assertTrue(store.delete(sf3)); // non-positional
+    assertEquals(store.getTotalFeatureLength(), 23); // no change
+
+    assertTrue(store.delete(sf5));
+    assertEquals(store.getTotalFeatureLength(), 12);
+
+    assertTrue(store.delete(sf4)); // contact
+    assertEquals(store.getTotalFeatureLength(), 11);
+
+    assertTrue(store.delete(sf1));
+    assertEquals(store.getTotalFeatureLength(), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinimumScore_getMaximumScore()
+  {
+    SequenceFeatures sf = new SequenceFeatures();
+    SequenceFeature sf1 = new SequenceFeature("Metal", "desc", 0, 0,
+            Float.NaN, "group"); // non-positional, no score
+    sf.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 10, 20,
+            Float.NaN, "group"); // positional, no score
+    sf.add(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Metal", "desc", 10, 20, 1f,
+            "group");
+    sf.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 12, 16, 4f,
+            "group");
+    sf.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 0, 0, 11f,
+            "group");
+    sf.add(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Cath", "desc", 0, 0, -7f,
+            "group");
+    sf.add(sf6);
+
+    assertEquals(sf.getMinimumScore("nosuchtype", true), Float.NaN);
+    assertEquals(sf.getMinimumScore("nosuchtype", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("nosuchtype", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("nosuchtype", false), Float.NaN);
+
+    // positional features min-max:
+    assertEquals(sf.getMinimumScore("Metal", true), 1f);
+    assertEquals(sf.getMaximumScore("Metal", true), 4f);
+    assertEquals(sf.getMinimumScore("Cath", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("Cath", true), Float.NaN);
+
+    // non-positional features min-max:
+    assertEquals(sf.getMinimumScore("Cath", false), -7f);
+    assertEquals(sf.getMaximumScore("Cath", false), 11f);
+    assertEquals(sf.getMinimumScore("Metal", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("Metal", false), Float.NaN);
+
+    // delete features; min-max should get recomputed
+    sf.delete(sf6);
+    assertEquals(sf.getMinimumScore("Cath", false), 11f);
+    assertEquals(sf.getMaximumScore("Cath", false), 11f);
+    sf.delete(sf4);
+    assertEquals(sf.getMinimumScore("Metal", true), 1f);
+    assertEquals(sf.getMaximumScore("Metal", true), 1f);
+    sf.delete(sf5);
+    assertEquals(sf.getMinimumScore("Cath", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("Cath", false), Float.NaN);
+    sf.delete(sf3);
+    assertEquals(sf.getMinimumScore("Metal", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("Metal", true), Float.NaN);
+    sf.delete(sf1);
+    sf.delete(sf2);
+    assertFalse(sf.hasFeatures());
+    assertEquals(sf.getMinimumScore("Cath", false), Float.NaN);
+    assertEquals(sf.getMaximumScore("Cath", false), Float.NaN);
+    assertEquals(sf.getMinimumScore("Metal", true), Float.NaN);
+    assertEquals(sf.getMaximumScore("Metal", true), Float.NaN);
+  }
+
+  @Test(groups = "Functional")
+  public void testVarargsToTypes()
+  {
+    SequenceFeatures sf = new SequenceFeatures();
+    sf.add(new SequenceFeature("Metal", "desc", 0, 0, Float.NaN, "group"));
+    sf.add(new SequenceFeature("Cath", "desc", 10, 20, Float.NaN, "group"));
+
+    /*
+     * no type specified - get all types stored
+     * they are returned in keyset (alphabetical) order
+     */
+    Map<String, FeatureStore> featureStores = (Map<String, FeatureStore>) PA
+            .getValue(sf, "featureStore");
+
+    Iterable<FeatureStore> types = sf.varargToTypes();
+    Iterator<FeatureStore> iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * empty array is the same as no vararg parameter supplied
+     * so treated as all stored types
+     */
+    types = sf.varargToTypes(new String[] {});
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * null type specified; this is passed as vararg
+     * String[1] {null}
+     */
+    types = sf.varargToTypes((String) null);
+    assertFalse(types.iterator().hasNext());
+
+    /*
+     * null types array specified; this is passed as vararg null
+     */
+    types = sf.varargToTypes((String[]) null);
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one type specified
+     */
+    types = sf.varargToTypes("Metal");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two types specified - get sorted alphabetically
+     */
+    types = sf.varargToTypes("Metal", "Cath");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Cath"));
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+
+    /*
+     * null type included - should be ignored
+     */
+    types = sf.varargToTypes("Metal", null, "Helix");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertSame(iterator.next(), featureStores.get("Metal"));
+    assertFalse(iterator.hasNext());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeatureTypes_byOntology()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+  
+    SequenceFeature sf1 = new SequenceFeature("transcript", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+    // mRNA isA mature_transcript isA transcript
+    SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf2);
+    // just to prove non-positional feature types are included
+    SequenceFeature sf3 = new SequenceFeature("mRNA", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("CDS", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf4);
+
+    Set<String> types = store.getFeatureTypes("transcript");
+    assertEquals(types.size(), 2);
+    assertTrue(types.contains("transcript"));
+    assertTrue(types.contains("mRNA"));
+
+    // matches include arguments whether SO terms or not
+    types = store.getFeatureTypes("transcript", "CDS");
+    assertEquals(types.size(), 3);
+    assertTrue(types.contains("transcript"));
+    assertTrue(types.contains("mRNA"));
+    assertTrue(types.contains("CDS"));
+
+    types = store.getFeatureTypes("exon");
+    assertTrue(types.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeaturesByOntology()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    List<SequenceFeature> features = store.getFeaturesByOntology();
+    assertTrue(features.isEmpty());
+    assertTrue(store.getFeaturesByOntology(new String[] {}).isEmpty());
+    assertTrue(store.getFeaturesByOntology((String[]) null).isEmpty());
+  
+    SequenceFeature transcriptFeature = new SequenceFeature("transcript", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(transcriptFeature);
+
+    /*
+     * mRNA is a sub-type of transcript; added here 'as if' non-positional
+     * just to show that non-positional features are included in results
+     */
+    SequenceFeature mrnaFeature = new SequenceFeature("mRNA", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(mrnaFeature);
+
+    SequenceFeature pfamFeature = new SequenceFeature("Pfam", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(pfamFeature);
+
+    /*
+     * "transcript" matches both itself and the sub-term "mRNA"
+     */
+    features = store.getFeaturesByOntology("transcript");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(transcriptFeature));
+    assertTrue(features.contains(mrnaFeature));
+
+    /*
+     * "mRNA" matches itself but not parent term "transcript"
+     */
+    features = store.getFeaturesByOntology("mRNA");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(mrnaFeature));
+
+    /*
+     * "pfam" is not an SO term but is included as an exact match
+     */
+    features = store.getFeaturesByOntology("mRNA", "Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(mrnaFeature));
+    assertTrue(features.contains(pfamFeature));
+
+    features = store.getFeaturesByOntology("sequence_variant");
+    assertTrue(features.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testSortFeatures()
+  {
+    List<SequenceFeature> sfs = new ArrayList<>();
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 80,
+            Float.NaN, null);
+    sfs.add(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Rfam", "desc", 40, 50,
+            Float.NaN, null);
+    sfs.add(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Rfam", "desc", 50, 60,
+            Float.NaN, null);
+    sfs.add(sf3);
+
+    // sort by end position descending
+    SequenceFeatures.sortFeatures(sfs, false);
+    assertSame(sfs.get(0), sf1);
+    assertSame(sfs.get(1), sf3);
+    assertSame(sfs.get(2), sf2);
+
+    // sort by start position ascending
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertSame(sfs.get(0), sf1);
+    assertSame(sfs.get(1), sf2);
+    assertSame(sfs.get(2), sf3);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetFeaturesForGroup()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+
+    List<SequenceFeature> features = store.getFeaturesForGroup(true, null);
+    assertTrue(features.isEmpty());
+    assertTrue(store.getFeaturesForGroup(false, null).isEmpty());
+    assertTrue(store.getFeaturesForGroup(true, "Uniprot").isEmpty());
+    assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty());
+
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            null);
+    SequenceFeature sf2 = new SequenceFeature("Pfam", "desc", 0, 0, 0f,
+            null);
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 4, 10, 0f,
+            "Uniprot");
+    SequenceFeature sf4 = new SequenceFeature("Metal", "desc", 0, 0, 0f,
+            "Rfam");
+    SequenceFeature sf5 = new SequenceFeature("Cath", "desc", 5, 15, 0f,
+            null);
+    store.add(sf1);
+    store.add(sf2);
+    store.add(sf3);
+    store.add(sf4);
+    store.add(sf5);
+
+    // positional features for null group, any type
+    features = store.getFeaturesForGroup(true, null);
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf5));
+
+    // positional features for null group, specified type
+    features = store.getFeaturesForGroup(true, null, new String[] { "Pfam",
+        "Xfam" });
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf1));
+    features = store.getFeaturesForGroup(true, null, new String[] { "Pfam",
+        "Xfam", "Cath" });
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf5));
+
+    // positional features for non-null group, any type
+    features = store.getFeaturesForGroup(true, "Uniprot");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+    assertTrue(store.getFeaturesForGroup(true, "Rfam").isEmpty());
+
+    // positional features for non-null group, specified type
+    features = store.getFeaturesForGroup(true, "Uniprot", "Pfam", "Xfam",
+            "Rfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf3));
+    assertTrue(store.getFeaturesForGroup(true, "Uniprot", "Cath").isEmpty());
+
+    // non-positional features for null group, any type
+    features = store.getFeaturesForGroup(false, null);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+
+    // non-positional features for null group, specified type
+    features = store.getFeaturesForGroup(false, null, "Pfam", "Xfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+    assertTrue(store.getFeaturesForGroup(false, null, "Cath").isEmpty());
+
+    // non-positional features for non-null group, any type
+    features = store.getFeaturesForGroup(false, "Rfam");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf4));
+    assertTrue(store.getFeaturesForGroup(false, "Uniprot").isEmpty());
+
+    // non-positional features for non-null group, specified type
+    features = store.getFeaturesForGroup(false, "Rfam", "Pfam", "Metal");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf4));
+    assertTrue(store.getFeaturesForGroup(false, "Rfam", "Cath", "Pfam")
+            .isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testShiftFeatures()
+  {
+    SequenceFeatures store = new SequenceFeatures();
+    assertFalse(store.shiftFeatures(0, 1));
+
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 2, 5, 0f, null);
+    store.add(sf1);
+    // nested feature:
+    SequenceFeature sf2 = new SequenceFeature("Metal", "", 8, 14, 0f, null);
+    store.add(sf2);
+    // contact feature:
+    SequenceFeature sf3 = new SequenceFeature("Disulfide bond", "", 23, 32,
+            0f, null);
+    store.add(sf3);
+    // non-positional feature:
+    SequenceFeature sf4 = new SequenceFeature("Pfam", "", 0, 0, 0f, null);
+    store.add(sf4);
+  
+    /*
+     * shift features right by 5
+     */
+    assertTrue(store.shiftFeatures(0, 5));
+  
+    // non-positional features untouched:
+    List<SequenceFeature> nonPos = store.getNonPositionalFeatures();
+    assertEquals(nonPos.size(), 1);
+    assertTrue(nonPos.contains(sf4));
+  
+    // positional features are replaced
+    List<SequenceFeature> pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 3);
+    assertFalse(pos.contains(sf1));
+    assertFalse(pos.contains(sf2));
+    assertFalse(pos.contains(sf3));
+    SequenceFeatures.sortFeatures(pos, true); // ascending start pos
+    assertEquals(pos.get(0).getBegin(), 7);
+    assertEquals(pos.get(0).getEnd(), 10);
+    assertEquals(pos.get(0).getType(), "Cath");
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 19);
+    assertEquals(pos.get(1).getType(), "Metal");
+    assertEquals(pos.get(2).getBegin(), 28);
+    assertEquals(pos.get(2).getEnd(), 37);
+    assertEquals(pos.get(2).getType(), "Disulfide bond");
+  
+    /*
+     * now shift left by 15
+     * feature at [7-10] should be removed
+     * feature at [13-19] should become [1-4] 
+     */
+    assertTrue(store.shiftFeatures(0, -15));
+    pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(0).getType(), "Metal");
+    assertEquals(pos.get(1).getBegin(), 13);
+    assertEquals(pos.get(1).getEnd(), 22);
+    assertEquals(pos.get(1).getType(), "Disulfide bond");
+
+    /*
+     * shift right by 4 from column 2
+     * feature at [1-4] should be unchanged
+     * feature at [13-22] should become [17-26] 
+     */
+    assertTrue(store.shiftFeatures(2, 4));
+    pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertEquals(pos.get(0).getBegin(), 1);
+    assertEquals(pos.get(0).getEnd(), 4);
+    assertEquals(pos.get(0).getType(), "Metal");
+    assertEquals(pos.get(1).getBegin(), 17);
+    assertEquals(pos.get(1).getEnd(), 26);
+    assertEquals(pos.get(1).getType(), "Disulfide bond");
+
+    /*
+     * shift right from column 18
+     * should be no updates
+     */
+    SequenceFeature f1 = pos.get(0);
+    SequenceFeature f2 = pos.get(1);
+    assertFalse(store.shiftFeatures(18, 6));
+    pos = store.getPositionalFeatures();
+    assertEquals(pos.size(), 2);
+    SequenceFeatures.sortFeatures(pos, true);
+    assertSame(pos.get(0), f1);
+    assertSame(pos.get(1), f2);
+  }
+
+  @Test(groups = "Functional")
+  public void testIsOntologyTerm()
+  {
+    SequenceFeatures store = new SequenceFeatures();
+    assertTrue(store.isOntologyTerm("gobbledygook"));
+    assertTrue(store.isOntologyTerm("transcript", "transcript"));
+    assertTrue(store.isOntologyTerm("mRNA", "transcript"));
+    assertFalse(store.isOntologyTerm("transcript", "mRNA"));
+    assertTrue(store.isOntologyTerm("junk", "transcript", "junk"));
+    assertTrue(store.isOntologyTerm("junk", new String[] {}));
+    assertTrue(store.isOntologyTerm("junk", (String[]) null));
+  }
+
+  @Test(groups = "Functional")
+  public void testDeleteAll()
+  {
+    SequenceFeaturesI store = new SequenceFeatures();
+    assertFalse(store.hasFeatures());
+    store.deleteAll();
+    assertFalse(store.hasFeatures());
+    store.add(new SequenceFeature("Cath", "Desc", 12, 20, 0f, "Group"));
+    store.add(new SequenceFeature("Pfam", "Desc", 6, 12, 2f, "Group2"));
+    assertTrue(store.hasFeatures());
+    store.deleteAll();
+    assertFalse(store.hasFeatures());
+  }
+}
index fb0204b..c9d8deb 100644 (file)
@@ -25,6 +25,7 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -212,20 +213,25 @@ public class EnsemblCdnaTest
             20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("aberrant_processed_transcript");
+    sf = new SequenceFeature("aberrant_processed_transcript", "", 20000,
+            20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500,
+            0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
     // other feature with no parent is retained
-    sf.setType("sequence_variant");
+    sf = new SequenceFeature("sequence_variant", "", 20000, 20500, 0f, null);
     assertTrue(testee.retainFeature(sf, accId));
 
     // other feature with desired parent is retained
     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));
@@ -236,34 +242,51 @@ public class EnsemblCdnaTest
    * accession id as parent
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblCdna testee = new EnsemblCdna();
+    SequenceI seq = new Sequence(accId, "MKLNFRQIE");
 
-    // exon with no parent not valid
-    SequenceFeature sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    // exon with no parent: not valid
+    SequenceFeature sf1 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    seq.addSequenceFeature(sf1);
 
-    // exon with wrong parent not valid
-    sf.setValue("Parent", "transcript:XYZ");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    // exon with wrong parent: not valid
+    SequenceFeature sf2 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf2.setValue("Parent", "transcript:XYZ");
+    seq.addSequenceFeature(sf2);
 
     // exon with right parent is valid
-    sf.setValue("Parent", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf3.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // exon sub-type with right parent is valid
-    sf.setType("coding_exon");
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("coding_exon", "", 1, 2, 0f,
+            null);
+    sf4.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // transcript not valid:
-    sf.setType("transcript");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf5.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // CDS not valid:
-    sf.setType("CDS");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf6.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf6);
+
+    List<SequenceFeature> sfs = new EnsemblCdna()
+            .getIdentifyingFeatures(seq, accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertFalse(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
   }
 
   @Test(groups = "Functional")
index b7f9f8d..a44ab7f 100644 (file)
@@ -24,6 +24,7 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -130,11 +131,12 @@ public class EnsemblCdsTest
             null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("CDS_predicted");
+    sf = new SequenceFeature("CDS_predicted", "", 20000, 20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
     // other feature with no parent is retained
-    sf.setType("sequence_variant");
+    sf = new SequenceFeature("CDS_psequence_variantredicted", "", 20000,
+            20500, 0f, null);
     assertTrue(testee.retainFeature(sf, accId));
 
     // other feature with desired parent is retained
@@ -151,34 +153,50 @@ public class EnsemblCdsTest
    * accession id as parent
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblCds testee = new EnsemblCds();
+    SequenceI seq = new Sequence(accId, "MKDONS");
 
     // cds with no parent not valid
-    SequenceFeature sf = new SequenceFeature("CDS", "", 1, 2, 0f, null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf1 = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    seq.addSequenceFeature(sf1);
 
     // cds with wrong parent not valid
-    sf.setValue("Parent", "transcript:XYZ");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf2 = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    sf2.setValue("Parent", "transcript:XYZ");
+    seq.addSequenceFeature(sf2);
 
     // cds with right parent is valid
-    sf.setValue("Parent", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    sf3.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // cds sub-type with right parent is valid
-    sf.setType("CDS_predicted");
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("CDS_predicted", "", 1, 2, 0f,
+            null);
+    sf4.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // transcript not valid:
-    sf.setType("transcript");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf5.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // exon not valid:
-    sf.setType("exon");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf6.setValue("Parent", "transcript:" + accId);
+    seq.addSequenceFeature(sf6);
+
+    List<SequenceFeature> sfs = new EnsemblCds().getIdentifyingFeatures(seq,
+            accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertFalse(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
   }
 
   @Test(groups = "Functional")
index 6cfd85b..446b4f7 100644 (file)
@@ -22,10 +22,11 @@ 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.api.FeatureSettingsModelI;
+import jalview.bin.Cache;
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -54,6 +55,7 @@ public class EnsemblGeneTest
   @BeforeClass(alwaysRun = true)
   public void setUp()
   {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
     SequenceOntologyFactory.setInstance(new SequenceOntologyLite());
   }
 
@@ -76,16 +78,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // gene at (start+10000) length 501
-    SequenceFeature sf = new SequenceFeature("gene", "", 20000, 20500, 0f,
-            null);
-    sf.setValue("ID", "gene:" + geneId);
-    sf.setStrand("+");
-    genomic.addSequenceFeature(sf);
-
     // gene at (start + 10500) length 101
-    // should be ignored - the first 'gene' found defines the whole range
-    sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null);
+    SequenceFeature sf = new SequenceFeature("gene", "", 10500, 10600, 0f,
+            null);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
     genomic.addSequenceFeature(sf);
@@ -94,13 +89,13 @@ public class EnsemblGeneTest
             23);
     List<int[]> fromRanges = ranges.getFromRanges();
     assertEquals(1, fromRanges.size());
-    assertEquals(20000, fromRanges.get(0)[0]);
-    assertEquals(20500, fromRanges.get(0)[1]);
+    assertEquals(10500, fromRanges.get(0)[0]);
+    assertEquals(10600, fromRanges.get(0)[1]);
     // to range should start from given start numbering
     List<int[]> toRanges = ranges.getToRanges();
     assertEquals(1, toRanges.size());
     assertEquals(23, toRanges.get(0)[0]);
-    assertEquals(523, toRanges.get(0)[1]);
+    assertEquals(123, toRanges.get(0)[1]);
   }
 
   /**
@@ -115,17 +110,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // gene at (start+10000) length 501
-    SequenceFeature sf = new SequenceFeature("ncRNA_gene", "", 20000,
-            20500, 0f, null);
-    sf.setValue("ID", "gene:" + geneId);
-    sf.setStrand("-");
-    genomic.addSequenceFeature(sf);
-
     // gene at (start + 10500) length 101
-    // should be ignored - the first 'gene' found defines the whole range
-    // (real data would only have one such feature)
-    sf = new SequenceFeature("gene", "", 10500, 10600, 0f, null);
+    SequenceFeature sf = new SequenceFeature("gene", "", 10500, 10600, 0f,
+            null);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
     genomic.addSequenceFeature(sf);
@@ -135,13 +122,13 @@ public class EnsemblGeneTest
     List<int[]> fromRanges = ranges.getFromRanges();
     assertEquals(1, fromRanges.size());
     // from range on reverse strand:
-    assertEquals(20500, fromRanges.get(0)[0]);
-    assertEquals(20000, fromRanges.get(0)[1]);
+    assertEquals(10500, fromRanges.get(0)[0]);
+    assertEquals(10600, fromRanges.get(0)[1]);
     // to range should start from given start numbering
     List<int[]> toRanges = ranges.getToRanges();
     assertEquals(1, toRanges.size());
     assertEquals(23, toRanges.get(0)[0]);
-    assertEquals(523, toRanges.get(0)[1]);
+    assertEquals(123, toRanges.get(0)[1]);
   }
 
   /**
@@ -164,7 +151,7 @@ public class EnsemblGeneTest
     genomic.addSequenceFeature(sf1);
 
     // transcript sub-type feature
-    SequenceFeature sf2 = new SequenceFeature("snRNA", "", 20000, 20500,
+    SequenceFeature sf2 = new SequenceFeature("snRNA", "", 21000, 21500,
             0f, null);
     sf2.setValue("Parent", "gene:" + geneId);
     sf2.setValue("transcript_id", "transcript2");
@@ -172,13 +159,14 @@ public class EnsemblGeneTest
 
     // NMD_transcript_variant treated like transcript in Ensembl
     SequenceFeature sf3 = new SequenceFeature("NMD_transcript_variant", "",
-            20000, 20500, 0f, null);
-    sf3.setValue("Parent", "gene:" + geneId);
+            22000, 22500, 0f, null);
+    // id matching should not be case-sensitive
+    sf3.setValue("Parent", "gene:" + geneId.toLowerCase());
     sf3.setValue("transcript_id", "transcript3");
     genomic.addSequenceFeature(sf3);
 
     // transcript for a different gene - ignored
-    SequenceFeature sf4 = new SequenceFeature("snRNA", "", 20000, 20500,
+    SequenceFeature sf4 = new SequenceFeature("snRNA", "", 23000, 23500,
             0f, null);
     sf4.setValue("Parent", "gene:XYZ");
     sf4.setValue("transcript_id", "transcript4");
@@ -192,9 +180,9 @@ public class EnsemblGeneTest
     List<SequenceFeature> features = testee.getTranscriptFeatures(geneId,
             genomic);
     assertEquals(3, features.size());
-    assertSame(sf1, features.get(0));
-    assertSame(sf2, features.get(1));
-    assertSame(sf3, features.get(2));
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
   }
 
   /**
@@ -211,22 +199,24 @@ public class EnsemblGeneTest
     sf.setValue("ID", "gene:" + geneId);
     assertFalse(testee.retainFeature(sf, geneId));
 
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 20000, 20500, 0f, null);
     sf.setValue("Parent", "gene:" + geneId);
     assertTrue(testee.retainFeature(sf, geneId));
 
-    sf.setType("mature_transcript");
+    sf = new SequenceFeature("mature_transcript", "", 20000, 20500, 0f,
+            null);
     sf.setValue("Parent", "gene:" + geneId);
     assertTrue(testee.retainFeature(sf, geneId));
 
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500,
+            0f, null);
     sf.setValue("Parent", "gene:" + geneId);
     assertTrue(testee.retainFeature(sf, geneId));
 
     sf.setValue("Parent", "gene:XYZ");
     assertFalse(testee.retainFeature(sf, geneId));
 
-    sf.setType("anything");
+    sf = new SequenceFeature("anything", "", 20000, 20500, 0f, null);
     assertTrue(testee.retainFeature(sf, geneId));
   }
 
@@ -235,34 +225,48 @@ public class EnsemblGeneTest
    * accession id as ID
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblGene testee = new EnsemblGene();
+    SequenceI seq = new Sequence(accId, "HIBEES");
 
     // gene with no ID not valid
-    SequenceFeature sf = new SequenceFeature("gene", "", 1, 2, 0f, null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf1 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    seq.addSequenceFeature(sf1);
 
     // gene with wrong ID not valid
-    sf.setValue("ID", "gene:XYZ");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf2 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf2.setValue("ID", "gene:XYZ");
+    seq.addSequenceFeature(sf2);
 
     // gene with right ID is valid
-    sf.setValue("ID", "gene:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf3.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // gene sub-type with right ID is valid
-    sf.setType("snRNA_gene");
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("snRNA_gene", "", 1, 2, 0f, null);
+    sf4.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // transcript not valid:
-    sf.setType("transcript");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf5.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // exon not valid:
-    sf.setType("exon");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf6.setValue("ID", "gene:" + accId);
+    seq.addSequenceFeature(sf6);
+    
+    List<SequenceFeature> sfs = new EnsemblGene()
+            .getIdentifyingFeatures(seq, accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertFalse(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
   }
 
   /**
@@ -291,4 +295,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 654797c..72ee492 100644 (file)
@@ -24,6 +24,7 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -136,14 +137,16 @@ public class EnsemblGenomeTest
             20500, 0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("mature_transcript");
+    sf = new SequenceFeature("mature_transcript", "", 20000, 20500, 0f,
+            null);
     assertFalse(testee.retainFeature(sf, accId));
 
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 20000, 20500,
+            0f, null);
     assertFalse(testee.retainFeature(sf, accId));
 
     // other feature with no parent is kept
-    sf.setType("anything");
+    sf = new SequenceFeature("anything", "", 20000, 20500, 0f, null);
     assertTrue(testee.retainFeature(sf, accId));
 
     // other feature with correct parent is kept
@@ -160,39 +163,58 @@ public class EnsemblGenomeTest
    * accession id as ID
    */
   @Test(groups = "Functional")
-  public void testIdentifiesSequence()
+  public void testGetIdentifyingFeatures()
   {
     String accId = "ABC123";
-    EnsemblGenome testee = new EnsemblGenome();
+    SequenceI seq = new Sequence(accId, "HEARTS");
 
     // transcript with no ID not valid
-    SequenceFeature sf = new SequenceFeature("transcript", "", 1, 2, 0f,
+    SequenceFeature sf1 = new SequenceFeature("transcript", "", 1, 2, 0f,
             null);
-    assertFalse(testee.identifiesSequence(sf, accId));
+    seq.addSequenceFeature(sf1);
 
     // transcript with wrong ID not valid
-    sf.setValue("ID", "transcript");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf2 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf2.setValue("ID", "transcript");
+    seq.addSequenceFeature(sf2);
 
     // transcript with right ID is valid
-    sf.setValue("ID", "transcript:" + accId);
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf3 = new SequenceFeature("transcript", "", 1, 2, 0f,
+            null);
+    sf3.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf3);
 
     // transcript sub-type with right ID is valid
-    sf.setType("ncRNA");
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf4 = new SequenceFeature("ncRNA", "", 1, 2, 0f, null);
+    sf4.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf4);
 
     // Ensembl treats NMD_transcript_variant as if a transcript
-    sf.setType("NMD_transcript_variant");
-    assertTrue(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf5 = new SequenceFeature("NMD_transcript_variant", "",
+            1, 2, 0f, null);
+    sf5.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf5);
 
     // gene not valid:
-    sf.setType("gene");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf6 = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf6.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf6);
 
     // exon not valid:
-    sf.setType("exon");
-    assertFalse(testee.identifiesSequence(sf, accId));
+    SequenceFeature sf7 = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf7.setValue("ID", "transcript:" + accId);
+    seq.addSequenceFeature(sf7);
+
+    List<SequenceFeature> sfs = new EnsemblGenome()
+            .getIdentifyingFeatures(seq, accId);
+    assertFalse(sfs.contains(sf1));
+    assertFalse(sfs.contains(sf2));
+    assertTrue(sfs.contains(sf3));
+    assertTrue(sfs.contains(sf4));
+    assertTrue(sfs.contains(sf5));
+    assertFalse(sfs.contains(sf6));
+    assertFalse(sfs.contains(sf7));
   }
 
 }
index 31001da..460d16c 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
@@ -60,30 +79,15 @@ public class EnsemblRestClientTest
       {
         return false;
       }
-
-      @Override
-      protected String getRequestMimeType(boolean b)
-      {
-        return null;
-      }
-
-      @Override
-      protected String getResponseMimeType()
-      {
-        return null;
-      }
-
     };
-    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 9fad30e..be7bdf2 100644 (file)
 package jalview.ext.ensembl;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A convenience class to simplify writing unit tests (pending Mockito or
@@ -65,9 +69,10 @@ public class EnsemblSeqProxyAdapter extends EnsemblSeqProxy
   }
 
   @Override
-  protected boolean identifiesSequence(SequenceFeature sf, String accId)
+  protected List<SequenceFeature> getIdentifyingFeatures(SequenceI seq,
+          String accId)
   {
-    return false;
+    return new ArrayList<>();
   }
 
 }
index e977233..69b2ad4 100644 (file)
 package jalview.ext.ensembl;
 
 import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertTrue;
-import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
+import static org.testng.AssertJUnit.assertSame;
 
-import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FastaFile;
-import jalview.io.FileParse;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyLite;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.List;
 
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
@@ -125,7 +124,11 @@ public class EnsemblSeqProxyTest
                   + "LKKALMMRGLIPECCAVYRIQDGEKKPIGWDTDISWLTGEELHVEVLENVPLTTHNFVRK\n"
                   + "TFFTLAFCDFCRKLLFQGFRCQTCGYKFHQRCSTEVPLMCVNYDQLDLLFVSKFFEHHPI\n"
                   + "PQEEASLAETALTSGSSPSAPASDSIGPQILTSPSPSKSIPIPQPFRPADEDHRNQFGQR\n"
-                  + "DRSSSAPNVHINTIEPVNIDDLIRDQGFRGDGGSTTGLSATPPASLPGSLTNVKALQKSP\n"
+                  + "DRSSSAPNVHINTIEPVNIDDLIRDQGFRGDG\n"
+                  // ? insertion added in ENSP00000288602.11, not in P15056
+                  + "APLNQLMRCLRKYQSRTPSPLLHSVPSEIVFDFEPGPVFR\n"
+                  // end insertion
+                  + "GSTTGLSATPPASLPGSLTNVKALQKSP\n"
                   + "GPQRERKSSSSSEDRNRMKTLGRRDSSDDWEIPDGQITVGQRIGSGSFGTVYKGKWHGDV\n"
                   + "AVKMLNVTAPTPQQLQAFKNEVGVLRKTRHVNILLFMGYSTKPQLAIVTQWCEGSSLYHH\n"
                   + "LHIIETKFEMIKLIDIARQTAQGMDYLHAKSIIHRDLKSNNIFLHEDLTVKIGDFGLATV\n"
@@ -153,22 +156,21 @@ public class EnsemblSeqProxyTest
   }
 
   @Test(dataProvider = "ens_seqs", suiteName = "live")
-  public void testGetOneSeqs(EnsemblRestClient proxy, String sq,
+  public void testGetSequenceRecords(EnsemblSeqProxy proxy, String sq,
           String fastasq) throws Exception
   {
-    FileParse fp = proxy.getSequenceReader(Arrays
-            .asList(new String[] { sq }));
-    SequenceI[] sqs = new FastaFile(fp).getSeqsAsArray();
     FastaFile trueRes = new FastaFile(fastasq, DataSourceType.PASTE);
-    SequenceI[] trueSqs = trueRes.getSeqsAsArray();
-    Assert.assertEquals(sqs.length, trueSqs.length,
+    SequenceI[] expected = trueRes.getSeqsAsArray();
+    AlignmentI retrieved = proxy.getSequenceRecords(sq);
+
+    Assert.assertEquals(retrieved.getHeight(), expected.length,
             "Different number of sequences retrieved for query " + sq);
-    Alignment ral = new Alignment(sqs);
-    for (SequenceI tr : trueSqs)
+
+    for (SequenceI tr : expected)
     {
       SequenceI[] rseq;
       Assert.assertNotNull(
-              rseq = ral.findSequenceMatch(tr.getName()),
+              rseq = retrieved.findSequenceMatch(tr.getName()),
               "Couldn't find sequences matching expected sequence "
                       + tr.getName());
       Assert.assertEquals(rseq.length, 1,
@@ -179,7 +181,6 @@ public class EnsemblSeqProxyTest
               "Sequences differ for " + tr.getName() + "\n" + "Exp:"
                       + tr.getSequenceAsString() + "\n" + "Got:"
                       + rseq[0].getSequenceAsString());
-
     }
   }
 
@@ -189,34 +190,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
@@ -269,15 +242,22 @@ public class EnsemblSeqProxyTest
     SequenceFeature sf2 = new SequenceFeature("", "", 8, 12, 0f, null);
     SequenceFeature sf3 = new SequenceFeature("", "", 8, 13, 0f, null);
     SequenceFeature sf4 = new SequenceFeature("", "", 11, 11, 0f, null);
-    SequenceFeature[] sfs = new SequenceFeature[] { sf1, sf2, sf3, sf4 };
+    List<SequenceFeature> sfs = Arrays.asList(new SequenceFeature[] { sf1,
+        sf2, sf3, sf4 });
 
     // sort by start position ascending (forward strand)
     // sf2 and sf3 tie and should not be reordered by sorting
-    EnsemblSeqProxy.sortFeatures(sfs, true);
-    assertArrayEquals(new SequenceFeature[] { sf2, sf3, sf1, sf4 }, sfs);
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertSame(sfs.get(0), sf2);
+    assertSame(sfs.get(1), sf3);
+    assertSame(sfs.get(2), sf1);
+    assertSame(sfs.get(3), sf4);
 
     // sort by end position descending (reverse strand)
-    EnsemblSeqProxy.sortFeatures(sfs, false);
-    assertArrayEquals(new SequenceFeature[] { sf1, sf3, sf2, sf4 }, sfs);
+    SequenceFeatures.sortFeatures(sfs, false);
+    assertSame(sfs.get(0), sf1);
+    assertSame(sfs.get(1), sf3);
+    assertSame(sfs.get(2), sf2);
+    assertSame(sfs.get(3), sf4);
   }
 }
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..69634a9 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,110 @@ 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
 
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
+    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))
+  }
+
+  /**
+   * 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)
+    {
+      // we expect an IO Exception because the pgmB.fasta.fai exists, since it
+      // was checked it in.
+    }
+
+    /*
+     * 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);
 
-    Assert.assertTrue(remmadb.isValid());
+    /*
+     * 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();
 
-    SequenceI sq = remmadb.getSequenceProxy("Deminut");
-    Assert.assertNotNull(sq);
-    Assert.assertNotEquals(0, sq.getLength());
+    /*
+     * 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 36e9b20..bcad464 100644 (file)
@@ -62,9 +62,9 @@ public class JmolParserTest
    * 1GAQ has been reduced to alpha carbons only
    * 1QCF is the full PDB file including headers, HETATM etc
    */
-  String[] testFile = new String[] { "./examples/1GAQ.txt",
+  String[] testFile = new String[] { "./examples/1gaq.txt",
       "./test/jalview/ext/jmol/1xyz.pdb",
-      "./test/jalview/ext/jmol/1qcf.pdb" };
+      "./test/jalview/ext/jmol/1QCF.pdb" };
 
   //@formatter:off
   // a modified and very cut-down extract of 4UJ4
@@ -130,17 +130,17 @@ public class JmolParserTest
       JmolParser jtest = new JmolParser(pdbStr, DataSourceType.FILE);
       Vector<SequenceI> seqs = jtest.getSeqs(), mcseqs = mctest.getSeqs();
 
-      assertTrue(
-              "No sequences extracted from testfile\n"
-                      + (jtest.hasWarningMessage() ? jtest.getWarningMessage()
-                              : "(No warnings raised)"), seqs != null
-                      && seqs.size() > 0);
+      assertTrue("No sequences extracted from testfile\n"
+              + (jtest.hasWarningMessage() ? jtest.getWarningMessage()
+                      : "(No warnings raised)"),
+              seqs != null && seqs.size() > 0);
       for (SequenceI sq : seqs)
       {
-        assertEquals("JMol didn't process " + pdbStr
-                + " to the same sequence as MCView",
-                sq.getSequenceAsString(), mcseqs.remove(0)
-                        .getSequenceAsString());
+        assertEquals(
+                "JMol didn't process " + pdbStr
+                        + " to the same sequence as MCView",
+                sq.getSequenceAsString(),
+                mcseqs.remove(0).getSequenceAsString());
         AlignmentI al = new Alignment(new SequenceI[] { sq });
         validateSecStrRows(al);
       }
@@ -175,13 +175,15 @@ public class JmolParserTest
 
   private void checkFirstAAIsAssoc(SequenceI sq)
   {
-    assertTrue("No secondary structure assigned for protein sequence for "
-            + sq.getName(),
+    assertTrue(
+            "No secondary structure assigned for protein sequence for "
+                    + sq.getName(),
             sq.getAnnotation() != null && sq.getAnnotation().length >= 1
                     && sq.getAnnotation()[0].hasIcons);
     assertTrue(
             "Secondary structure not associated for sequence "
-                    + sq.getName(), sq.getAnnotation()[0].sequenceRef == sq);
+                    + sq.getName(),
+            sq.getAnnotation()[0].sequenceRef == sq);
   }
 
   /**
@@ -194,7 +196,8 @@ public class JmolParserTest
   {
     PDBfile mctest = new PDBfile(false, false, false,
             pastePDBDataWithChainBreak, DataSourceType.PASTE);
-    JmolParser jtest = new JmolParser(pastePDBDataWithChainBreak, DataSourceType.PASTE);
+    JmolParser jtest = new JmolParser(pastePDBDataWithChainBreak,
+            DataSourceType.PASTE);
     Vector<SequenceI> seqs = jtest.getSeqs();
     Vector<SequenceI> mcseqs = mctest.getSeqs();
 
@@ -216,8 +219,7 @@ public class JmolParserTest
   {
     PDBfile mctest = new PDBfile(false, false, false, pdbWithAltLoc,
             DataSourceType.PASTE);
-    JmolParser jtest = new JmolParser(pdbWithAltLoc,
-            DataSourceType.PASTE);
+    JmolParser jtest = new JmolParser(pdbWithAltLoc, DataSourceType.PASTE);
     Vector<SequenceI> seqs = jtest.getSeqs();
     Vector<SequenceI> mcseqs = mctest.getSeqs();
 
@@ -277,10 +279,9 @@ public class JmolParserTest
     /*
      * the ID is also the group for features derived from structure data 
      */
-    assertNotNull(structureData.getSeqs().get(0).getSequenceFeatures()[0].featureGroup);
-    assertEquals(
-            structureData.getSeqs().get(0).getSequenceFeatures()[0].featureGroup,
-            "localstruct");
-
+    String featureGroup = structureData.getSeqs().get(0)
+            .getSequenceFeatures().get(0).featureGroup;
+    assertNotNull(featureGroup);
+    assertEquals(featureGroup, "localstruct");
   }
 }
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 85fc039..c6c1a29 100644 (file)
@@ -152,10 +152,10 @@ public class TestAnnotate3D
         {
           {
             SequenceI struseq = null;
-            String sq_ = new String(sq.getSequence()).toLowerCase();
+            String sq_ = sq.getSequenceAsString().toLowerCase();
             for (SequenceI _struseq : pdbf.getSeqsAsArray())
             {
-              final String lowerCase = new String(_struseq.getSequence())
+              final String lowerCase = _struseq.getSequenceAsString()
                       .toLowerCase();
               if (lowerCase.equals(sq_))
               {
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 4d904cf..99394dc 100644 (file)
@@ -41,7 +41,7 @@ public class ChimeraConnect
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "External" })
   public void testLaunchAndExit()
   {
     final StructureManager structureManager = new StructureManager(true);
index 29fd092..734f7eb 100644 (file)
@@ -39,6 +39,7 @@ import jalview.gui.JvOptionPane;
 import jalview.gui.Preferences;
 import jalview.gui.StructureViewer;
 import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
@@ -50,7 +51,6 @@ import java.io.File;
 import java.io.IOException;
 import java.util.List;
 import java.util.Vector;
-import jalview.io.DataSourceType;
 
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.AfterMethod;
@@ -440,15 +440,18 @@ public class JalviewChimeraView
     binding.copyStructureAttributesToFeatures("phi", af.getViewport()
             .getAlignPanel());
     fr.setVisible("phi");
-    List<SequenceFeature> fs = fr.findFeaturesAtRes(fer2Arath, 54);
+    List<SequenceFeature> fs = fer2Arath.getFeatures().findFeatures(54, 54);
     assertEquals(fs.size(), 3);
-    assertEquals(fs.get(0).getType(), "RESNUM");
-    assertEquals(fs.get(1).getType(), "phi");
-    assertEquals(fs.get(2).getType(), "phi");
-    assertEquals(fs.get(1).getDescription(), "A"); // chain
-    assertEquals(fs.get(2).getDescription(), "B");
-    assertEquals(fs.get(1).getScore(), -131.0713f, 0.001f);
-    assertEquals(fs.get(2).getScore(), -127.39512, 0.001f);
+    /*
+     * order of returned features is not guaranteed
+     */
+    assertTrue("RESNUM".equals(fs.get(0).getType())
+            || "RESNUM".equals(fs.get(1).getType())
+            || "RESNUM".equals(fs.get(2).getType()));
+    assertTrue(fs.contains(new SequenceFeature("phi", "A", 54, 54,
+            -131.0713f, "Chimera")));
+    assertTrue(fs.contains(new SequenceFeature("phi", "B", 54, 54,
+            -127.39512f, "Chimera")));
 
     /*
      * tear down - also in AfterMethod
@@ -470,7 +473,8 @@ public class JalviewChimeraView
           int res, String featureType)
   {
     String where = "at position " + res;
-    List<SequenceFeature> fs = fr.findFeaturesAtRes(seq, res);
+    List<SequenceFeature> fs = seq.getFeatures().findFeatures(res, res);
+
     assertEquals(fs.size(), 2, where);
     assertEquals(fs.get(0).getType(), "RESNUM", where);
     SequenceFeature sf = fs.get(1);
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 b2286e0..7801250 100644 (file)
@@ -75,6 +75,13 @@ public class AlignViewportTest
   {
     Jalview.main(new String[] { "-nonews", "-props",
         "test/jalview/testProps.jvprops" });
+
+    /*
+     * remove any sequence mappings left lying around by other tests
+     */
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+    ssm.resetAll();
   }
 
   @BeforeMethod(alwaysRun = true)
@@ -89,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
@@ -173,18 +129,19 @@ public class AlignViewportTest
      */
     StructureSelectionManager ssm = StructureSelectionManager
             .getStructureSelectionManager(Desktop.instance);
-    assertEquals(2, ssm.getSequenceMappings().size());
-    assertTrue(ssm.getSequenceMappings().contains(acf1));
-    assertTrue(ssm.getSequenceMappings().contains(acf2));
+    List<AlignedCodonFrame> sequenceMappings = ssm.getSequenceMappings();
+    assertEquals(2, sequenceMappings.size());
+    assertTrue(sequenceMappings.contains(acf1));
+    assertTrue(sequenceMappings.contains(acf2));
 
     /*
      * Close the second view. Verify that mappings are not removed as the first
      * view still holds a reference to them.
      */
     af1.closeMenuItem_actionPerformed(false);
-    assertEquals(2, ssm.getSequenceMappings().size());
-    assertTrue(ssm.getSequenceMappings().contains(acf1));
-    assertTrue(ssm.getSequenceMappings().contains(acf2));
+    assertEquals(2, sequenceMappings.size());
+    assertTrue(sequenceMappings.contains(acf1));
+    assertTrue(sequenceMappings.contains(acf2));
   }
 
   /**
@@ -320,7 +277,7 @@ public class AlignViewportTest
    * Test for JAL-1306 - conservation thread should run even when only Quality
    * (and not Conservation) is enabled in Preferences
    */
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Functional" }, timeOut=2000)
   public void testUpdateConservation_qualityOnly()
   {
     Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS",
@@ -335,7 +292,24 @@ public class AlignViewportTest
             Boolean.FALSE.toString());
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/uniref50.fa", DataSourceType.FILE);
-    AlignmentAnnotation[] anns = af.viewport.getAlignment()
+
+    /*
+     * wait for Conservation thread to complete
+     */
+    AlignViewport viewport = af.getViewport();
+    synchronized (this)
+    {
+      while (viewport.getAlignmentConservationAnnotation() != null)
+      {
+        try
+        {
+          wait(50);
+        } catch (InterruptedException e)
+        {
+        }
+      }
+    }
+    AlignmentAnnotation[] anns = viewport.getAlignment()
             .getAlignmentAnnotation();
     assertNotNull("No annotations found", anns);
     assertEquals("More than one annotation found", 1, anns.length);
index b228ba1..e84b87a 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
+   * when switching from wrapped back 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 has changed
+    assertNotEquals(ranges.getEndRes(), endres);
+
+    // unwrap
+    af.alignPanel.getAlignViewport().setWrapAlignment(false);
+    af.alignPanel.updateLayout();
+
+    // endRes back to original value
+    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..5298680
--- /dev/null
@@ -0,0 +1,339 @@
+/*
+ * 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.getRanges().setStartEndSeq(0, 14);
+
+    SeqCanvas testee = af.alignPanel.getSeqPanel().seqCanvas;
+
+    av.setWrapAlignment(true);
+    av.setFont(new Font("SansSerif", Font.PLAIN, 14), true);
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    assertEquals(charHeight, 17);
+    assertEquals(charWidth, 12);
+
+    /*
+     * 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);
+  }
+
+  /**
+   * Test simulates loading an unwrapped alignment, shrinking it vertically so
+   * not all sequences are visible, then changing to wrapped mode. The ranges
+   * endSeq should be unchanged, but the vertical repeat height should include
+   * all sequences.
+   */
+  @Test(groups = "Functional")
+  public void testCalculateWrappedGeometry_fromScrolled()
+  {
+    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.getRanges().setStartEndSeq(0, 3);
+    av.setShowAnnotation(false);
+    av.setScaleAboveWrapped(true);
+
+    SeqCanvas testee = af.alignPanel.getSeqPanel().seqCanvas;
+
+    av.setWrapAlignment(true);
+    av.setFont(new Font("SansSerif", Font.PLAIN, 14), true);
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    assertEquals(charHeight, 17);
+    assertEquals(charWidth, 12);
+
+    int canvasWidth = 400;
+    int canvasHeight = 300;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+
+    assertEquals(av.getRanges().getEndSeq(), 3); // unchanged
+    int repeatingHeight = (int) PA.getValue(testee,
+            "wrappedRepeatHeightPx");
+    assertEquals(repeatingHeight, charHeight * (2 + al.getHeight()));
+  }
+}
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 d8ae999..c0038a1 100644 (file)
@@ -30,13 +30,16 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.structure.StructureImportSettings;
 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;
@@ -99,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));
         }
@@ -127,32 +131,35 @@ public class AnnotatedPDBFileInputTest
     /*
      * 1GAQ/A
      */
-    SequenceFeature[] sf = al.getSequenceAt(0).getSequenceFeatures();
-    assertEquals(296, sf.length);
-    assertEquals("RESNUM", sf[0].getType());
-    assertEquals("GLU:  19  1gaqA", sf[0].getDescription());
-    assertEquals("RESNUM", sf[295].getType());
-    assertEquals("TYR: 314  1gaqA", sf[295].getDescription());
+    List<SequenceFeature> sf = al.getSequenceAt(0).getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sf, true);
+    assertEquals(296, sf.size());
+    assertEquals("RESNUM", sf.get(0).getType());
+    assertEquals("GLU:  19  1gaqA", sf.get(0).getDescription());
+    assertEquals("RESNUM", sf.get(295).getType());
+    assertEquals("TYR: 314  1gaqA", sf.get(295).getDescription());
 
     /*
      * 1GAQ/B
      */
     sf = al.getSequenceAt(1).getSequenceFeatures();
-    assertEquals(98, sf.length);
-    assertEquals("RESNUM", sf[0].getType());
-    assertEquals("ALA:   1  1gaqB", sf[0].getDescription());
-    assertEquals("RESNUM", sf[97].getType());
-    assertEquals("ALA:  98  1gaqB", sf[97].getDescription());
+    SequenceFeatures.sortFeatures(sf, true);
+    assertEquals(98, sf.size());
+    assertEquals("RESNUM", sf.get(0).getType());
+    assertEquals("ALA:   1  1gaqB", sf.get(0).getDescription());
+    assertEquals("RESNUM", sf.get(97).getType());
+    assertEquals("ALA:  98  1gaqB", sf.get(97).getDescription());
 
     /*
      * 1GAQ/C
      */
     sf = al.getSequenceAt(2).getSequenceFeatures();
-    assertEquals(296, sf.length);
-    assertEquals("RESNUM", sf[0].getType());
-    assertEquals("GLU:  19  1gaqC", sf[0].getDescription());
-    assertEquals("RESNUM", sf[295].getType());
-    assertEquals("TYR: 314  1gaqC", sf[295].getDescription());
+    SequenceFeatures.sortFeatures(sf, true);
+    assertEquals(296, sf.size());
+    assertEquals("RESNUM", sf.get(0).getType());
+    assertEquals("GLU:  19  1gaqC", sf.get(0).getDescription());
+    assertEquals("RESNUM", sf.get(295).getType());
+    assertEquals("TYR: 314  1gaqC", sf.get(295).getDescription());
   }
 
   @Test(groups = { "Functional" })
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 cc7dca0..32ca841 100644 (file)
@@ -23,8 +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.assertNull;
+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;
@@ -33,19 +34,46 @@ 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;
 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;
 
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 public class FeaturesFileTest
 {
+  private static String simpleGffFile = "examples/testdata/simpleGff3.gff";
+
+  @AfterClass(alwaysRun = true)
+  public void tearDownAfterClass()
+  {
+    /*
+     * remove any sequence mappings created so they don't pollute other tests
+     */
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+    ssm.resetAll();
+  }
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -54,8 +82,6 @@ public class FeaturesFileTest
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  private static String simpleGffFile = "examples/testdata/simpleGff3.gff";
-
   @Test(groups = { "Functional" })
   public void testParse() throws Exception
   {
@@ -86,10 +112,15 @@ public class FeaturesFileTest
     /*
      * verify (some) features on sequences
      */
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures(); // FER_CAPAA
-    assertEquals(8, sfs.length);
-    SequenceFeature sf = sfs[0];
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertEquals(8, sfs.size());
+
+    /*
+     * verify (in ascending start position order)
+     */
+    SequenceFeature sf = sfs.get(0);
     assertEquals("Pfam family%LINK%", sf.description);
     assertEquals(0, sf.begin);
     assertEquals(0, sf.end);
@@ -99,46 +130,52 @@ public class FeaturesFileTest
     assertEquals("Pfam family|http://pfam.xfam.org/family/PF00111",
             sf.links.get(0));
 
-    sf = sfs[1];
+    sf = sfs.get(1);
+    assertEquals("Ferredoxin_fold Status: True Positive ", sf.description);
+    assertEquals(3, sf.begin);
+    assertEquals(93, sf.end);
+    assertEquals("uniprot", sf.featureGroup);
+    assertEquals("Cath", sf.type);
+
+    sf = sfs.get(2);
+    assertEquals("Fer2 Status: True Positive Pfam 8_8%LINK%",
+            sf.description);
+    assertEquals("Pfam 8_8|http://pfam.xfam.org/family/PF00111",
+            sf.links.get(0));
+    assertEquals(8, sf.begin);
+    assertEquals(83, sf.end);
+    assertEquals("uniprot", sf.featureGroup);
+    assertEquals("Pfam", sf.type);
+
+    sf = sfs.get(3);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(39, sf.begin);
     assertEquals(39, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[2];
+
+    sf = sfs.get(4);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(44, sf.begin);
     assertEquals(44, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[3];
+
+    sf = sfs.get(5);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(47, sf.begin);
     assertEquals(47, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[4];
+
+    sf = sfs.get(6);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(77, sf.begin);
     assertEquals(77, sf.end);
     assertEquals("uniprot", sf.featureGroup);
     assertEquals("METAL", sf.type);
-    sf = sfs[5];
-    assertEquals("Fer2 Status: True Positive Pfam 8_8%LINK%",
-            sf.description);
-    assertEquals("Pfam 8_8|http://pfam.xfam.org/family/PF00111",
-            sf.links.get(0));
-    assertEquals(8, sf.begin);
-    assertEquals(83, sf.end);
-    assertEquals("uniprot", sf.featureGroup);
-    assertEquals("Pfam", sf.type);
-    sf = sfs[6];
-    assertEquals("Ferredoxin_fold Status: True Positive ", sf.description);
-    assertEquals(3, sf.begin);
-    assertEquals(93, sf.end);
-    assertEquals("uniprot", sf.featureGroup);
-    assertEquals("Cath", sf.type);
-    sf = sfs[7];
+
+    sf = sfs.get(7);
     assertEquals(
             "High confidence server. Only hits with scores over 0.8 are reported. PHOSPHORYLATION (T) 89_8%LINK%",
             sf.description);
@@ -181,10 +218,10 @@ public class FeaturesFileTest
     assertEquals(colours.get("METAL").getColour(), new Color(0xcc9900));
 
     // verify feature on FER_CAPAA
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    SequenceFeature sf = sfs[0];
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
     assertEquals("Iron-sulfur,2Fe-2S", sf.description);
     assertEquals(44, sf.begin);
     assertEquals(45, sf.end);
@@ -194,8 +231,8 @@ public class FeaturesFileTest
 
     // verify feature on FER1_SOLLC
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    sf = sfs[0];
+    assertEquals(1, sfs.size());
+    sf = sfs.get(0);
     assertEquals("uniprot", sf.description);
     assertEquals(55, sf.begin);
     assertEquals(130, sf.end);
@@ -242,10 +279,10 @@ public class FeaturesFileTest
             featuresFile.parse(al.getDataset(), colours, true));
 
     // verify feature on FER_CAPAA
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    SequenceFeature sf = sfs[0];
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
     // description parsed from Note attribute
     assertEquals("Iron-sulfur (2Fe-2S),another note", sf.description);
     assertEquals(39, sf.begin);
@@ -258,8 +295,8 @@ public class FeaturesFileTest
 
     // verify feature on FER1_SOLLC1
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    sf = sfs[0];
+    assertEquals(1, sfs.size());
+    sf = sfs.get(0);
     // ID used for description if available
     assertEquals("$23", sf.description);
     assertEquals(55, sf.begin);
@@ -295,10 +332,10 @@ public class FeaturesFileTest
             featuresFile.parse(al.getDataset(), colours, true));
 
     // verify FER_CAPAA feature
-    SequenceFeature[] sfs = al.getSequenceAt(0).getDatasetSequence()
+    List<SequenceFeature> sfs = al.getSequenceAt(0).getDatasetSequence()
             .getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    SequenceFeature sf = sfs[0];
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
     assertEquals("Iron-sulfur (2Fe-2S)", sf.description);
     assertEquals(39, sf.begin);
     assertEquals(39, sf.end);
@@ -306,8 +343,8 @@ public class FeaturesFileTest
 
     // verify FER1_SOLLC feature
     sfs = al.getSequenceAt(2).getDatasetSequence().getSequenceFeatures();
-    assertEquals(1, sfs.length);
-    sf = sfs[0];
+    assertEquals(1, sfs.size());
+    sf = sfs.get(0);
     assertEquals("Iron-phosphorus (2Fe-P)", sf.description);
     assertEquals(86, sf.begin);
     assertEquals(87, sf.end);
@@ -337,14 +374,14 @@ public class FeaturesFileTest
     assertFalse("dummy replacement buggy for seq2",
             placeholderseq.equals(seq2.getSequenceAsString()));
     assertNotNull("No features added to seq1", seq1.getSequenceFeatures());
-    assertEquals("Wrong number of features", 3,
-            seq1.getSequenceFeatures().length);
-    assertNull(seq2.getSequenceFeatures());
+    assertEquals("Wrong number of features", 3, seq1.getSequenceFeatures()
+            .size());
+    assertTrue(seq2.getSequenceFeatures().isEmpty());
     assertEquals(
             "Wrong number of features",
             0,
             seq2.getSequenceFeatures() == null ? 0 : seq2
-                    .getSequenceFeatures().length);
+                    .getSequenceFeatures().size());
     assertTrue(
             "Expected at least one CDNA/Protein mapping for seq1",
             dataset.getCodonFrame(seq1) != null
@@ -410,6 +447,7 @@ public class FeaturesFileTest
             + "GAMMA-TURN\tred|0,255,255|20.0|95.0|below|66.0\n"
             + "Pfam\tred\n"
             + "STARTGROUP\tuniprot\n"
+            + "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\n" // non-positional feature
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\n"
             + "<html>Pfam domain<a href=\"http://pfam.xfam.org/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\n"
@@ -419,28 +457,57 @@ public class FeaturesFileTest
     featuresFile.parse(al.getDataset(), colours, false);
 
     /*
-     * first with no features displayed
+     * add positional and non-positional features with null and
+     * empty feature group to check handled correctly
+     */
+    SequenceI seq = al.getSequenceAt(1); // FER_CAPAN
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc1", 0, 0, 1.3f,
+            null));
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc2", 4, 9,
+            Float.NaN, null));
+    seq = al.getSequenceAt(2); // FER1_SOLLC
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc3", 0, 0,
+            Float.NaN, ""));
+    seq.addSequenceFeature(new SequenceFeature("Pfam", "desc4", 5, 8,
+            -2.6f, ""));
+
+    /*
+     * first with no features displayed, exclude non-positional features
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     Map<String, FeatureColourI> visible = fr.getDisplayedFeatureCols();
+    List<String> visibleGroups = new ArrayList<>(
+            Arrays.asList(new String[] {}));
     String exported = featuresFile.printJalviewFormat(
-            al.getSequencesArray(), visible);
+            al.getSequencesArray(), visible, null, visibleGroups, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
     /*
+     * include non-positional features
+     */
+    visibleGroups.add("uniprot");
+    exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
+            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
+            + "\nSTARTGROUP\tuniprot\nENDGROUP\tuniprot\n";
+    assertEquals(expected, exported);
+
+    /*
      * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
      */
     fr.setVisible("METAL");
     fr.setVisible("GAMMA-TURN");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible);
+            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"
-            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
+            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "ENDGROUP\tuniprot\n";
     assertEquals(expected, exported);
 
@@ -450,19 +517,194 @@ public class FeaturesFileTest
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible);
+            visible, null, visibleGroups, false);
     /*
-     * note the order of feature types is uncontrolled - derives from
-     * FeaturesDisplayed.featuresDisplayed which is a HashSet
+     * 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"
-            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
+            + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
             + "<html>Pfam domain<a href=\"http://pfam.xfam.org/family/PF00111\">Pfam_3_4</a></html>\tFER_CAPAA\t-1\t20\t20\tPfam\t0.0\n"
-            + "ENDGROUP\tuniprot\n";
+            + "ENDGROUP\tuniprot\n"
+            // null / empty group features output after features in named
+            // groups:
+            + "desc2\tFER_CAPAN\t-1\t4\t9\tPfam\n"
+            + "desc4\tFER1_SOLLC\t-1\t5\t8\tPfam\t-2.6\n";
+    assertEquals(expected, exported);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testPrintGffFormat() throws Exception
+  {
+    File f = new File("examples/uniref50.fa");
+    AlignmentI al = readAlignmentFile(f);
+    AlignFrame af = new AlignFrame(al, 500, 500);
+
+    /*
+     * no features
+     */
+    FeaturesFile featuresFile = new FeaturesFile();
+    FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
+    Map<String, FeatureColourI> visible = new HashMap<>();
+    List<String> visibleGroups = new ArrayList<>(
+            Arrays.asList(new String[] {}));
+    String exported = featuresFile.printGffFormat(al.getSequencesArray(),
+            visible, visibleGroups, false);
+    String gffHeader = "##gff-version 2\n";
+    assertEquals(gffHeader, exported);
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, true);
+    assertEquals(gffHeader, exported);
+
+    /*
+     * add some features
+     */
+    al.getSequenceAt(0).addSequenceFeature(
+            new SequenceFeature("Domain", "Cath", 0, 0, 0f, "Uniprot"));
+    al.getSequenceAt(0).addSequenceFeature(
+            new SequenceFeature("METAL", "Cath", 39, 39, 1.2f, null));
+    al.getSequenceAt(1)
+            .addSequenceFeature(
+                    new SequenceFeature("GAMMA-TURN", "Turn", 36, 38, 2.1f,
+                            "s3dm"));
+    SequenceFeature sf = new SequenceFeature("Pfam", "", 20, 20, 0f,
+            "Uniprot");
+    sf.setAttributes("x=y;black=white");
+    sf.setStrand("+");
+    sf.setPhase("2");
+    al.getSequenceAt(1).addSequenceFeature(sf);
+
+    /*
+     * with no features displayed, exclude non-positional features
+     */
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    assertEquals(gffHeader, exported);
+
+    /*
+     * include non-positional features
+     */
+    visibleGroups.add("Uniprot");
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, true);
+    String expected = gffHeader
+            + "FER_CAPAA\tUniprot\tDomain\t0\t0\t0.0\t.\t.\n";
+    assertEquals(expected, exported);
+
+    /*
+     * set METAL (in uniprot group) and GAMMA-TURN visible, but not Pfam
+     * only Uniprot group visible here...
+     */
+    fr.setVisible("METAL");
+    fr.setVisible("GAMMA-TURN");
+    visible = fr.getDisplayedFeatureCols();
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    // METAL feature has null group: description used for column 2
+    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n";
+    assertEquals(expected, exported);
+
+    /*
+     * set s3dm group visible
+     */
+    visibleGroups.add("s3dm");
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    // METAL feature has null group: description used for column 2
+    expected = gffHeader + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
+            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n";
+    assertEquals(expected, exported);
+
+    /*
+     * now set Pfam visible
+     */
+    fr.setVisible("Pfam");
+    visible = fr.getDisplayedFeatureCols();
+    exported = featuresFile.printGffFormat(al.getSequencesArray(), visible,
+            visibleGroups, false);
+    // Pfam feature columns include strand(+), phase(2), attributes
+    expected = gffHeader
+            + "FER_CAPAA\tCath\tMETAL\t39\t39\t1.2\t.\t.\n"
+            + "FER_CAPAN\ts3dm\tGAMMA-TURN\t36\t38\t2.1\t.\t.\n"
+            + "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 dd4f6ba..5be7968 100644 (file)
@@ -94,7 +94,7 @@ public class IdentifyFileTest
         { "examples/plantfdx.fa", FileFormat.Fasta },
         { "examples/dna_interleaved.phy", FileFormat.Phylip },
         { "examples/2GIS.pdb", FileFormat.PDB },
-        { "examples/rf00031_folded.stk", FileFormat.Stockholm },
+        { "examples/RF00031_folded.stk", FileFormat.Stockholm },
         { "examples/testdata/test.rnaml", FileFormat.Rnaml },
         { "examples/testdata/test.aln", FileFormat.Clustal },
         { "examples/testdata/test.pfam", FileFormat.Pfam },
index e046d94..5e835bf 100644 (file)
@@ -32,6 +32,7 @@ import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.json.binding.biojson.v1.ColourSchemeMapper;
@@ -41,7 +42,9 @@ 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;
 
 import org.testng.Assert;
 import org.testng.AssertJUnit;
@@ -96,6 +99,10 @@ public class JSONFileTest
   @BeforeTest(alwaysRun = true)
   public void setup() throws Exception
   {
+    /*
+     * construct expected values
+     * nb this have to match the data in examples/example.json
+     */
     // create and add sequences
     Sequence[] seqs = new Sequence[5];
     seqs[0] = new Sequence("FER_CAPAN",
@@ -115,14 +122,18 @@ public class JSONFileTest
 
     // create and add sequence features
     SequenceFeature seqFeature2 = new SequenceFeature("feature_x",
-            "desciption", "status", 6, 15, "Jalview");
+            "theDesc", 6, 15, "Jalview");
     SequenceFeature seqFeature3 = new SequenceFeature("feature_x",
-            "desciption", "status", 9, 18, "Jalview");
+            "theDesc", 9, 18, "Jalview");
     SequenceFeature seqFeature4 = new SequenceFeature("feature_x",
-            "desciption", "status", 9, 18, "Jalview");
+            "theDesc", 9, 18, "Jalview");
+    // non-positional feature:
+    SequenceFeature seqFeature5 = new SequenceFeature("Domain",
+            "My description", 0, 0, "Pfam");
     seqs[2].addSequenceFeature(seqFeature2);
     seqs[3].addSequenceFeature(seqFeature3);
     seqs[4].addSequenceFeature(seqFeature4);
+    seqs[2].addSequenceFeature(seqFeature5);
 
     for (Sequence seq : seqs)
     {
@@ -195,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()
     {
@@ -315,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!");
   }
 
@@ -456,7 +468,7 @@ public class JSONFileTest
     return true;
   }
 
-  public boolean isSeqMatched(SequenceI expectedSeq, SequenceI actualSeq)
+  boolean isSeqMatched(SequenceI expectedSeq, SequenceI actualSeq)
   {
     System.out.println("Testing >>> " + actualSeq.getName());
 
@@ -490,14 +502,19 @@ public class JSONFileTest
             + actualGrp.getStartRes());
     System.out.println(expectedGrp.getEndRes() + " | "
             + actualGrp.getEndRes());
-    System.out.println(expectedGrp.cs + " | " + actualGrp.cs);
+    System.out.println(expectedGrp.cs.getColourScheme() + " | "
+            + actualGrp.cs.getColourScheme());
 
+    boolean colourSchemeMatches = (expectedGrp.cs.getColourScheme() == null && actualGrp.cs
+            .getColourScheme() == null)
+            || expectedGrp.cs.getColourScheme().getClass()
+                    .equals(actualGrp.cs.getColourScheme().getClass());
     if (expectedGrp.getName().equals(actualGrp.getName())
             && expectedGrp.getColourText() == actualGrp.getColourText()
             && expectedGrp.getDisplayBoxes() == actualGrp.getDisplayBoxes()
             && expectedGrp.getIgnoreGapsConsensus() == actualGrp
                     .getIgnoreGapsConsensus()
-            && (expectedGrp.cs.getClass().equals(actualGrp.cs.getClass()))
+            && colourSchemeMatches
             && expectedGrp.getSequences().size() == actualGrp
                     .getSequences().size()
             && expectedGrp.getStartRes() == actualGrp.getStartRes()
@@ -510,7 +527,6 @@ public class JSONFileTest
 
   private boolean featuresMatched(SequenceI seq1, SequenceI seq2)
   {
-    boolean matched = false;
     try
     {
       if (seq1 == null && seq2 == null)
@@ -518,52 +534,95 @@ public class JSONFileTest
         return true;
       }
 
-      SequenceFeature[] inFeature = seq1.getSequenceFeatures();
-      SequenceFeature[] outFeature = seq2.getSequenceFeatures();
+      List<SequenceFeature> inFeature = seq1.getFeatures().getAllFeatures();
+      List<SequenceFeature> outFeature = seq2.getFeatures()
+              .getAllFeatures();
 
-      if (inFeature == null && outFeature == null)
-      {
-        return true;
-      }
-      else if ((inFeature == null && outFeature != null)
-              || (inFeature != null && outFeature == null))
+      if (inFeature.size() != outFeature.size())
       {
+        System.err.println("Feature count in: " + inFeature.size()
+                + ", out: " + outFeature.size());
         return false;
       }
 
-      int testSize = inFeature.length;
-      int matchedCount = 0;
+      SequenceFeatures.sortFeatures(inFeature, true);
+      SequenceFeatures.sortFeatures(outFeature, true);
+      int i = 0;
       for (SequenceFeature in : inFeature)
       {
-        for (SequenceFeature out : outFeature)
+        SequenceFeature out = outFeature.get(i);
+        /*
+        System.out.println(out.getType() + " | " + in.getType());
+        System.out.println(out.getBegin() + " | " + in.getBegin());
+        System.out.println(out.getEnd() + " | " + in.getEnd());
+        */
+        if (!in.equals(out))
         {
-          System.out.println(out.getType() + " | " + in.getType());
-          System.out.println(out.getBegin() + " | " + in.getBegin());
-          System.out.println(out.getEnd() + " | " + in.getEnd());
-
-          if (inFeature.length == outFeature.length
-                  && in.getBegin() == out.getBegin()
-                  && in.getEnd() == out.getEnd()
-                  && in.getScore() == out.getScore()
-                  && in.getFeatureGroup().equals(out.getFeatureGroup())
-                  && in.getType().equals(out.getType()))
-          {
-
-            ++matchedCount;
-          }
+          System.err.println("Mismatch of " + in.toString() + " "
+                  + out.toString());
+          return false;
         }
-      }
-      System.out.println("matched count >>>>>> " + matchedCount);
-      if (testSize == matchedCount)
-      {
-        matched = true;
+        /*
+                if (in.getBegin() == out.getBegin() && in.getEnd() == out.getEnd()
+                        && in.getScore() == out.getScore()
+                        && in.getFeatureGroup().equals(out.getFeatureGroup())
+                        && in.getType().equals(out.getType())
+                        && mapsMatch(in.otherDetails, out.otherDetails))
+                {
+                }
+                else
+                {
+                  System.err.println("Feature[" + i + "] mismatch, in: "
+                          + in.toString() + ", out: "
+                          + outFeature.get(i).toString());
+                  return false;
+                }
+                */
+        i++;
       }
     } catch (Exception e)
     {
       e.printStackTrace();
     }
     // System.out.println(">>>>>>>>>>>>>> features matched : " + matched);
-    return matched;
+    return true;
+  }
+
+  boolean mapsMatch(Map<String, Object> m1, Map<String, Object> m2)
+  {
+    if (m1 == null || m2 == null)
+    {
+      if (m1 != null || m2 != null)
+      {
+        System.err
+                .println("only one SequenceFeature.otherDetails is not null");
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    if (m1.size() != m2.size())
+    {
+      System.err.println("otherDetails map different sizes");
+      return false;
+    }
+    for (String key : m1.keySet())
+    {
+      if (!m2.containsKey(key))
+      {
+        System.err.println(key + " in only one otherDetails");
+        return false;
+      }
+      if (m1.get(key) == null && m2.get(key) != null || m1.get(key) != null
+              && m2.get(key) == null || !m1.get(key).equals(m2.get(key)))
+      {
+        System.err.println(key + " values in otherDetails don't match");
+        return false;
+      }
+    }
+    return true;
   }
 
   /**
@@ -599,7 +658,7 @@ public class JSONFileTest
     Assert.assertNotNull(newAlignment.getGroups());
     for (SequenceGroup seqGrp : newAlignment.getGroups())
     {
-      SequenceGroup expectedGrp = expectedGrps.get(seqGrp.getName());
+      SequenceGroup expectedGrp = copySg;
       AssertJUnit.assertTrue(
               "Failed SequenceGroup Test for >>> " + seqGrp.getName(),
               isGroupMatched(expectedGrp, seqGrp));
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 2895874..87e35c7 100644 (file)
 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.Hashtable;
+import java.awt.Color;
 import java.util.Map;
 
+import junit.extensions.PA;
+
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -87,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:
      */
@@ -98,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());
@@ -108,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());
   }
 
@@ -124,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();
@@ -133,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();
@@ -146,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());
   }
 
@@ -192,4 +236,144 @@ public class SequenceAnnotationReportTest
     // if no <html> tag, html-encodes > and < (only):
     assertEquals("METAL 1 3; &lt;br&gt;&kHD&gt;6", sb.toString());
   }
+
+  @Test(groups = "Functional")
+  public void testCreateSequenceAnnotationReport()
+  {
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    StringBuilder sb = new StringBuilder();
+
+    SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL");
+    seq.setDescription("SeqDesc");
+
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null);
+
+    /*
+     * positional features are ignored
+     */
+    seq.addSequenceFeature(new SequenceFeature("Domain", "Ferredoxin", 5,
+            10, 1f, null));
+    assertEquals("<i><br>SeqDesc</i>", sb.toString());
+
+    /*
+     * non-positional feature
+     */
+    seq.addSequenceFeature(new SequenceFeature("Type1", "Nonpos", 0, 0, 1f,
+            null));
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null);
+    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos Score=1.0</i>";
+    assertEquals(expected, sb.toString());
+
+    /*
+     * non-positional features not wanted
+     */
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, null);
+    assertEquals("<i><br>SeqDesc</i>", sb.toString());
+
+    /*
+     * add non-pos feature with score inside min-max range for feature type
+     * minmax holds { [positionalMin, positionalMax], [nonPosMin, nonPosMax] }
+     * score is only appended for positional features so ignored here!
+     * minMax are not recorded for non-positional features
+     */
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f,
+            null));
+
+    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, fr);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
+    assertEquals(expected, sb.toString());
+    
+    /*
+     * 'linkonly' features are ignored; this is obsolete, as linkonly
+     * is only set by DasSequenceFetcher, and DAS is history
+     */
+    SequenceFeature sf = new SequenceFeature("Metal", "Desc", 0, 0, 5f,
+            null);
+    sf.setValue("linkonly", Boolean.TRUE);
+    seq.addSequenceFeature(sf);
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    assertEquals(expected, sb.toString()); // unchanged!
+
+    /*
+     * '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, fr);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana</i>";
+    assertEquals(expected, sb.toString());
+
+    /*
+     * add dbrefs
+     */
+    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, fr);
+    assertEquals(expected, sb.toString()); // unchanged
+
+    // with showDbRefs = true, colour Variant features by clinical_significance
+    sb.setLength(0);
+    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, fr);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
+    assertEquals(expected, sb.toString());
+
+    // see other tests for treatment of status and html
+  }
+
+  /**
+   * Test that exercises an abbreviated sequence details report, with ellipsis
+   * where there are more than 40 different sources, or more than 4 dbrefs for a
+   * single source
+   */
+  @Test(groups = "Functional")
+  public void testCreateSequenceAnnotationReport_withEllipsis()
+  {
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    StringBuilder sb = new StringBuilder();
+  
+    SequenceI seq = new Sequence("s1", "ABC");
+
+    int maxSources = (int) PA.getValue(sar, "MAX_SOURCES");
+    for (int i = 0; i <= maxSources; i++)
+    {
+      seq.addDBRef(new DBRefEntry("PDB" + i, "0", "3iu1"));
+    }
+    
+    int maxRefs = (int) PA.getValue(sar, "MAX_REFS_PER_SOURCE");
+    for (int i = 0; i <= maxRefs; i++)
+    {
+      seq.addDBRef(new DBRefEntry("Uniprot", "0", "P3041" + i));
+    }
+  
+    sar.createSequenceAnnotationReport(sb, seq, true, true, null, true);
+    String report = sb.toString();
+    assertTrue(report
+            .startsWith("<i><br>UNIPROT P30410, P30411, P30412, P30413,...<br>PDB0 3iu1"));
+    assertTrue(report
+            .endsWith("<br>PDB7 3iu1<br>PDB8,...<br>(Output Sequence Details to list all database references)</i>"));
+  }
 }
index 228c935..e86c8ad 100644 (file)
@@ -38,6 +38,8 @@ import java.util.BitSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
@@ -54,7 +56,8 @@ public class StockholmFileTest
   }
 
   static String PfamFile = "examples/PF00111_seed.stk",
-          RfamFile = "examples/RF00031_folded.stk";
+          RfamFile = "examples/RF00031_folded.stk",
+          RnaSSTestFile = "examples/rna_ss_test.stk";
 
   @Test(groups = { "Functional" })
   public void pfamFileIO() throws Exception
@@ -230,8 +233,8 @@ public class StockholmFileTest
     // we might want to revise this in future
     int aa_new_size = (aa_new == null ? 0 : aa_new.length);
     int aa_original_size = (aa_original == null ? 0 : aa_original.length);
-    Map<Integer, BitSet> orig_groups = new HashMap<Integer, BitSet>();
-    Map<Integer, BitSet> new_groups = new HashMap<Integer, BitSet>();
+    Map<Integer, BitSet> orig_groups = new HashMap<>();
+    Map<Integer, BitSet> new_groups = new HashMap<>();
 
     if (aa_new != null && aa_original != null)
     {
@@ -287,7 +290,8 @@ public class StockholmFileTest
     seq_original = al.getSequencesArray();
     SequenceI[] seq_new = new SequenceI[al_input.getSequencesArray().length];
     seq_new = al_input.getSequencesArray();
-    SequenceFeature[] sequenceFeatures_original, sequenceFeatures_new;
+    List<SequenceFeature> sequenceFeatures_original;
+    List<SequenceFeature> sequenceFeatures_new;
     AlignmentAnnotation annot_original, annot_new;
     //
     for (int i = 0; i < al.getSequencesArray().length; i++)
@@ -323,23 +327,20 @@ public class StockholmFileTest
                   && seq_new[in].getSequenceFeatures() != null)
           {
             System.out.println("There are feature!!!");
-            sequenceFeatures_original = new SequenceFeature[seq_original[i]
-                    .getSequenceFeatures().length];
             sequenceFeatures_original = seq_original[i]
                     .getSequenceFeatures();
-            sequenceFeatures_new = new SequenceFeature[seq_new[in]
-                    .getSequenceFeatures().length];
             sequenceFeatures_new = seq_new[in].getSequenceFeatures();
 
-            assertEquals("different number of features",
-                    seq_original[i].getSequenceFeatures().length,
-                    seq_new[in].getSequenceFeatures().length);
+            assertEquals("different number of features", seq_original[i]
+                    .getSequenceFeatures().size(), seq_new[in]
+                    .getSequenceFeatures().size());
 
-            for (int feat = 0; feat < seq_original[i].getSequenceFeatures().length; feat++)
+            for (int feat = 0; feat < seq_original[i].getSequenceFeatures()
+                    .size(); feat++)
             {
               assertEquals("Different features",
-                      sequenceFeatures_original[feat],
-                      sequenceFeatures_new[feat]);
+                      sequenceFeatures_original.get(feat),
+                      sequenceFeatures_new.get(feat));
             }
           }
           // compare alignment annotation
@@ -625,13 +626,13 @@ public class StockholmFileTest
   {
     for (char ch : new char[] { '{', '}', '[', ']', '(', ')', '<', '>' })
     {
-      Assert.assertTrue(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
-              "Didn't recognise " + ch + " as a WUSS bracket");
+      Assert.assertTrue(StockholmFile.RNASS_BRACKETS.indexOf(ch) >= 0,
+              "Didn't recognise '" + ch + "' as a WUSS bracket");
     }
-    for (char ch : new char[] { '@', '!', 'V', 'Q', '*', ' ', '-', '.' })
+    for (char ch : new char[] { '@', '!', '*', ' ', '-', '.' })
     {
-      Assert.assertFalse(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
-              "Shouldn't recognise " + ch + " as a WUSS bracket");
+      Assert.assertFalse(StockholmFile.RNASS_BRACKETS.indexOf(ch) >= 0,
+              "Shouldn't recognise '" + ch + "' as a WUSS bracket");
     }
   }
   private static void roundTripSSForRNA(String aliFile, String annFile)
@@ -656,4 +657,191 @@ public class StockholmFileTest
     testAlignmentEquivalence(al, newAl, true, true, true);
 
   }
+
+  // this is the single sequence alignment and the SS annotations equivalent to
+  // the ones in file RnaSSTestFile
+  String aliFileRnaSS = ">Test.sequence/1-14\n"
+          + "GUACAAAAAAAAAA";
+  String annFileRnaSSAlphaChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|(,(|E,E|H,H|B,B|h,h|e,e|b,b|(,(|E,E|),)|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  String wrongAnnFileRnaSSAlphaChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|(,(|H,H|E,E|B,B|h,h|e,e|b,b|(,(|E,E|),)|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  @Test(groups = { "Functional" })
+  public void stockholmFileRnaSSAlphaChars() throws Exception
+  {
+    AppletFormatAdapter af = new AppletFormatAdapter();
+    AlignmentI al = af.readFile(RnaSSTestFile, DataSourceType.FILE,
+            jalview.io.FileFormat.Stockholm);
+    Iterable<AlignmentAnnotation> aai = al.findAnnotations(null, null,
+            "Secondary Structure");
+    AlignmentAnnotation aa = aai.iterator().next();
+    Assert.assertTrue(aa.isRNA(),
+            "'" + RnaSSTestFile + "' not recognised as RNA SS");
+    Assert.assertTrue(aa.isValidStruc(),
+            "'" + RnaSSTestFile + "' not recognised as valid structure");
+    Annotation[] as = aa.annotations;
+    char[] As = new char[as.length];
+    for (int i = 0; i < as.length; i++)
+    {
+      As[i] = as[i].secondaryStructure;
+    }
+    char[] shouldBe = { '<', '(', 'E', 'H', 'B', 'h', 'e', 'b', '(', 'E',
+        ')', 'e', ')', '>' };
+    Assert.assertTrue(
+            Arrays.equals(As, shouldBe),
+            "Annotation is " + new String(As) + " but should be "
+                    + new String(shouldBe));
+
+    // this should result in the same RNA SS Annotations
+    AlignmentI newAl = new AppletFormatAdapter().readFile(
+            aliFileRnaSS,
+            DataSourceType.PASTE, jalview.io.FileFormat.Fasta);
+    AnnotationFile aaf = new AnnotationFile();
+    aaf.readAnnotationFile(newAl, annFileRnaSSAlphaChars,
+            DataSourceType.PASTE);
+
+    Assert.assertTrue(
+            testRnaSSAnnotationsEquivalent(al.getAlignmentAnnotation()[0],
+                    newAl.getAlignmentAnnotation()[0]),
+            "RNA SS Annotations SHOULD be pair-wise equivalent (but apparently aren't): \n"
+                    + "RNA SS A 1:" + al.getAlignmentAnnotation()[0] + "\n"
+                    + "RNA SS A 2:" + newAl.getAlignmentAnnotation()[0]);
+
+    // this should NOT result in the same RNA SS Annotations
+    newAl = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    aaf = new AnnotationFile();
+    aaf.readAnnotationFile(newAl, wrongAnnFileRnaSSAlphaChars,
+            DataSourceType.PASTE);
+
+    boolean mismatch = testRnaSSAnnotationsEquivalent(al.getAlignmentAnnotation()[0],
+            newAl.getAlignmentAnnotation()[0]);
+    Assert.assertFalse(mismatch,
+            "RNA SS Annotations SHOULD NOT be pair-wise equivalent (but apparently are): \n"
+                    + "RNA SS A 1:" + al.getAlignmentAnnotation()[0] + "\n"
+                    + "RNA SS A 2:" + newAl.getAlignmentAnnotation()[0]);
+  }
+
+  private static boolean testRnaSSAnnotationsEquivalent(
+          AlignmentAnnotation a1,
+          AlignmentAnnotation a2)
+  {
+    return a1.rnaSecondaryStructureEquivalent(a2);
+  }
+
+  String annFileRnaSSWithSpaceChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|.,.|H,H| , |B,B|h,h| , |b,b|(,(|E,E|.,.|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  String annFileRnaSSWithoutSpaceChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|.,.|H,H|.,.|B,B|h,h|.,.|b,b|(,(|E,E|.,.|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+
+  String wrongAnnFileRnaSSWithoutSpaceChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|.,.|H,H|Z,Z|B,B|h,h|z,z|b,b|(,(|E,E|.,.|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+
+  @Test(groups = { "Functional" })
+  public void stockholmFileRnaSSSpaceChars() throws Exception
+  {
+    AlignmentI alWithSpaces = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    AnnotationFile afWithSpaces = new AnnotationFile();
+    afWithSpaces.readAnnotationFile(alWithSpaces,
+            annFileRnaSSWithSpaceChars, DataSourceType.PASTE);
+
+    Iterable<AlignmentAnnotation> aaiWithSpaces = alWithSpaces
+            .findAnnotations(null, null, "Secondary Structure");
+    AlignmentAnnotation aaWithSpaces = aaiWithSpaces.iterator().next();
+    Assert.assertTrue(aaWithSpaces.isRNA(),
+            "'" + aaWithSpaces + "' not recognised as RNA SS");
+    Assert.assertTrue(aaWithSpaces.isValidStruc(),
+            "'" + aaWithSpaces + "' not recognised as valid structure");
+    Annotation[] annWithSpaces = aaWithSpaces.annotations;
+    char[] As = new char[annWithSpaces.length];
+    for (int i = 0; i < annWithSpaces.length; i++)
+    {
+      As[i] = annWithSpaces[i].secondaryStructure;
+    }
+    // check all spaces and dots are spaces in the internal representation
+    char[] shouldBe = { '<', ' ', 'H', ' ', 'B', 'h', ' ', 'b', '(', 'E',
+        ' ', 'e', ')', '>' };
+    Assert.assertTrue(Arrays.equals(As, shouldBe), "Annotation is "
+            + new String(As) + " but should be " + new String(shouldBe));
+
+    // this should result in the same RNA SS Annotations
+    AlignmentI alWithoutSpaces = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    AnnotationFile afWithoutSpaces = new AnnotationFile();
+    afWithoutSpaces.readAnnotationFile(alWithoutSpaces,
+            annFileRnaSSWithoutSpaceChars,
+            DataSourceType.PASTE);
+
+    Assert.assertTrue(
+            testRnaSSAnnotationsEquivalent(
+                    alWithSpaces.getAlignmentAnnotation()[0],
+                    alWithoutSpaces.getAlignmentAnnotation()[0]),
+            "RNA SS Annotations SHOULD be pair-wise equivalent (but apparently aren't): \n"
+                    + "RNA SS A 1:"
+                    + alWithSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure()
+                    + "\n" + "RNA SS A 2:"
+                    + alWithoutSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure());
+
+    // this should NOT result in the same RNA SS Annotations
+    AlignmentI wrongAlWithoutSpaces = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    AnnotationFile wrongAfWithoutSpaces = new AnnotationFile();
+    wrongAfWithoutSpaces.readAnnotationFile(wrongAlWithoutSpaces,
+            wrongAnnFileRnaSSWithoutSpaceChars,
+            DataSourceType.PASTE);
+
+    Assert.assertFalse(
+            testRnaSSAnnotationsEquivalent(
+                    alWithSpaces.getAlignmentAnnotation()[0],
+                    wrongAlWithoutSpaces.getAlignmentAnnotation()[0]),
+            "RNA SS Annotations SHOULD NOT be pair-wise equivalent (but apparently are): \n"
+                    + "RNA SS A 1:"
+                    + alWithSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure()
+                    + "\n" + "RNA SS A 2:"
+                    + wrongAlWithoutSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure());
+
+    // check no spaces in the output
+    // TODO: create a better 'save as <format>' pattern
+    alWithSpaces.getAlignmentAnnotation()[0].visible = true;
+    StockholmFile sf = new StockholmFile(alWithSpaces);
+
+    String stockholmFile = sf.print(alWithSpaces.getSequencesArray(), true);
+    Pattern noSpacesInRnaSSAnnotation = Pattern
+            .compile("\\n#=GC SS_cons\\s+\\S{14}\\n");
+    Matcher m = noSpacesInRnaSSAnnotation.matcher(stockholmFile);
+    boolean matches = m.find();
+    Assert.assertTrue(matches,
+            "StockholmFile output does not contain expected output (may contain spaces):\n"
+                    + stockholmFile);
+
+  }
 }
index dfd7973..010a4b2 100644 (file)
@@ -55,10 +55,9 @@ public class JvCacheableInputBoxTest
     cacheBox.updateCache();
     try
     {
-      // This 1ms delay is essential to prevent the
-      // assertion below from executing before
-      // cacheBox.updateCache() finishes updating the cache
-      Thread.sleep(100);
+      // This delay is to let
+      // cacheBox.updateCache() finish updating the cache
+      Thread.sleep(200);
     } catch (InterruptedException e)
     {
       e.printStackTrace();
index bf038ac..cd5a0d8 100644 (file)
@@ -37,7 +37,9 @@ import jalview.gui.JvOptionPane;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -231,4 +233,48 @@ public class Gff3HelperTest
             .getToRanges().get(0));
   }
 
+  @Test(groups = "Functional")
+  public void testGetDescription()
+  {
+    Gff3Helper testee = new Gff3Helper();
+    SequenceFeature sf = new SequenceFeature("type", "desc", 10, 20, 3f,
+            "group");
+    Map<String, List<String>> attributes = new HashMap<String, List<String>>();
+    assertNull(testee.getDescription(sf, attributes));
+
+    // ID if any is a fall-back for description
+    sf.setValue("ID", "Patrick");
+    assertEquals("Patrick", testee.getDescription(sf, attributes));
+
+    // Target is set by Exonerate
+    sf.setValue("Target", "Destination Moon");
+    assertEquals("Destination", testee.getDescription(sf, attributes));
+
+    // Ensembl variant feature - extract "alleles" value
+    // may be sequence_variant or a sub-type in the sequence ontology
+    sf = new SequenceFeature("feature_variant", "desc", 10, 20, 3f, "group");
+    List<String> atts = new ArrayList<String>();
+    atts.add("A");
+    atts.add("C");
+    atts.add("T");
+    attributes.put("alleles", atts);
+    assertEquals("A,C,T", testee.getDescription(sf, attributes));
+
+    // Ensembl transcript or exon feature - extract Name
+    List<String> atts2 = new ArrayList<String>();
+    atts2.add("ENSE00001871077");
+    attributes.put("Name", atts2);
+    sf = new SequenceFeature("transcript", "desc", 10, 20, 3f, "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+    // transcript sub-type in SO
+    sf = new SequenceFeature("mRNA", "desc", 10, 20, 3f, "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+    // special usage of feature by Ensembl
+    sf = new SequenceFeature("NMD_transcript_variant", "desc", 10, 20, 3f,
+            "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+    // exon feature
+    sf = new SequenceFeature("exon", "desc", 10, 20, 3f, "group");
+    assertEquals("ENSE00001871077", testee.getDescription(sf, attributes));
+  }
 }
index bcccf35..dde83a3 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.io.gff;
 
 import static org.testng.AssertJUnit.assertEquals;
+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;
@@ -30,6 +31,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceDummy;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
@@ -76,6 +78,16 @@ public class InterProScanHelperTest
     assertEquals(1, newseqs.size());
     assertTrue(newseqs.get(0) instanceof SequenceDummy);
     assertEquals("match$17_5_30", newseqs.get(0).getName());
+
+    assertNotNull(newseqs.get(0).getSequenceFeatures());
+    assertEquals(1, newseqs.get(0).getSequenceFeatures().size());
+    SequenceFeature sf = newseqs.get(0).getSequenceFeatures().get(0);
+    assertEquals(1, sf.getBegin());
+    assertEquals(26, sf.getEnd());
+    assertEquals("Pfam", sf.getType());
+    assertEquals("4Fe-4S dicluster domain", sf.getDescription());
+    assertEquals("InterProScan", sf.getFeatureGroup());
+
     assertEquals(1, align.getCodonFrames().size());
     AlignedCodonFrame mapping = align.getCodonFrames().iterator().next();
 
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 4fc079e..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,8 +15,10 @@ 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;
 
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.BeforeTest;
@@ -70,13 +73,10 @@ public class FeatureColourFinderTest
   @BeforeMethod(alwaysRun = true)
   public void setUpBeforeTest()
   {
-    SequenceFeature[] sfs = seq.getSequenceFeatures();
-    if (sfs != null)
+    List<SequenceFeature> sfs = seq.getSequenceFeatures();
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
-      {
-        seq.deleteFeature(sf);
-      }
+      seq.deleteFeature(sf);
     }
     fr.findAllFeatures(true);
 
@@ -173,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);
@@ -183,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);
@@ -217,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);
@@ -226,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);
@@ -287,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,
@@ -378,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));
@@ -390,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));
@@ -426,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());
 
@@ -454,15 +476,19 @@ public class FeatureColourFinderTest
   @Test(groups = "Functional")
   public void testFindFeatureColour_graduatedWithThreshold()
   {
-    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 2,
+    String kdFeature = "kd";
+    String metalFeature = "Metal";
+    seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 2,
             2, 0f, "KdGroup"));
-    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 4,
+    seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 4,
             4, 5f, "KdGroup"));
-    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 7,
+    seq.addSequenceFeature(new SequenceFeature(metalFeature, "Fe", 4, 4,
+            5f, "MetalGroup"));
+    seq.addSequenceFeature(new SequenceFeature(kdFeature, "hydrophobicity", 7,
             7, 10f, "KdGroup"));
   
     /*
-     * graduated colour from 0 to 10
+     * kd feature has graduated colour from 0 to 10
      * above threshold value of 5
      */
     Color min = new Color(100, 50, 150);
@@ -470,8 +496,19 @@ public class FeatureColourFinderTest
     FeatureColourI fc = new FeatureColour(min, max, 0, 10);
     fc.setAboveThreshold(true);
     fc.setThreshold(5f);
-    fr.setColour("kd", fc);
+    fr.setColour(kdFeature, fc);
+    FeatureColour green = new FeatureColour(Color.green);
+    fr.setColour(metalFeature, green);
     fr.featuresAdded();
+
+    /*
+     * render order is kd above Metal
+     */
+    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);
   
     /*
@@ -481,10 +518,11 @@ public class FeatureColourFinderTest
     assertEquals(c, Color.blue);
 
     /*
-     * position 4, column 3, score 5 - at threshold - default colour
+     * position 4, column 3, score 5 - at threshold
+     * should return Green (colour of Metal feature)
      */
     c = finder.findFeatureColour(Color.blue, seq, 3);
-    assertEquals(c, Color.blue);
+    assertEquals(c, Color.green);
   
     /*
      * position 7, column 9, score 10 - maximum colour in range
@@ -504,10 +542,11 @@ public class FeatureColourFinderTest
     assertEquals(c, min);
 
     /*
-     * position 4, column 3, score 5 - at threshold - default colour
+     * position 4, column 3, score 5 - at threshold
+     * should return Green (colour of Metal feature)
      */
     c = finder.findFeatureColour(Color.blue, seq, 3);
-    assertEquals(c, Color.blue);
+    assertEquals(c, Color.green);
 
     /*
      * position 7, column 9, score 10 - above threshold - default colour
diff --git a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java
new file mode 100644 (file)
index 0000000..11b129e
--- /dev/null
@@ -0,0 +1,538 @@
+/*
+ * 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;
+
+import org.testng.annotations.Test;
+
+public class FeatureRendererTest
+{
+
+  @Test(groups = "Functional")
+  public void testFindAllFeatures()
+  {
+    String seqData = ">s1\nabcdef\n>s2\nabcdef\n>s3\nabcdef\n>s4\nabcdef\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
+
+    /*
+     * with no features
+     */
+    fr.findAllFeatures(true);
+    assertTrue(fr.getRenderOrder().isEmpty());
+    assertTrue(fr.getFeatureGroups().isEmpty());
+
+    List<SequenceI> seqs = av.getAlignment().getSequences();
+
+    // add a non-positional feature - should be ignored by FeatureRenderer
+    SequenceFeature sf1 = new SequenceFeature("Type", "Desc", 0, 0, 1f,
+            "Group");
+    seqs.get(0).addSequenceFeature(sf1);
+    fr.findAllFeatures(true);
+    // ? bug - types and groups added for non-positional features
+    List<String> types = fr.getRenderOrder();
+    List<String> groups = fr.getFeatureGroups();
+    assertEquals(types.size(), 0);
+    assertFalse(types.contains("Type"));
+    assertEquals(groups.size(), 0);
+    assertFalse(groups.contains("Group"));
+
+    // add some positional features
+    seqs.get(1).addSequenceFeature(
+            new SequenceFeature("Pfam", "Desc", 5, 9, 1f, "PfamGroup"));
+    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"));
+    // existing feature type with null group
+    seqs.get(3).addSequenceFeature(
+            new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
+    // new feature type with null group
+    seqs.get(3).addSequenceFeature(
+            new SequenceFeature("Scop", "Desc", 5, 9, Float.NaN, null));
+    // null value for type produces NullPointerException
+    fr.findAllFeatures(true);
+    types = fr.getRenderOrder();
+    groups = fr.getFeatureGroups();
+    assertEquals(types.size(), 3);
+    assertFalse(types.contains("Type"));
+    assertTrue(types.contains("Pfam"));
+    assertTrue(types.contains("Rfam"));
+    assertTrue(types.contains("Scop"));
+    assertEquals(groups.size(), 2);
+    assertFalse(groups.contains("Group"));
+    assertTrue(groups.contains("PfamGroup"));
+    assertTrue(groups.contains("RfamGroup"));
+    assertFalse(groups.contains(null)); // null group is ignored
+
+    /*
+     * check min-max values
+     */
+    Map<String, float[][]> minMax = fr.getMinMax();
+    assertEquals(minMax.size(), 1); // non-positional and NaN not stored
+    assertEquals(minMax.get("Pfam")[0][0], 1f); // positional min
+    assertEquals(minMax.get("Pfam")[0][1], 2f); // positional max
+
+    // increase max for Pfam, add scores for Rfam
+    seqs.get(0).addSequenceFeature(
+            new SequenceFeature("Pfam", "Desc", 14, 22, 8f, "RfamGroup"));
+    seqs.get(1).addSequenceFeature(
+            new SequenceFeature("Rfam", "Desc", 5, 9, 6f, "RfamGroup"));
+    fr.findAllFeatures(true);
+    // note minMax is not a defensive copy, shouldn't expose this
+    assertEquals(minMax.size(), 2);
+    assertEquals(minMax.get("Pfam")[0][0], 1f);
+    assertEquals(minMax.get("Pfam")[0][1], 8f);
+    assertEquals(minMax.get("Rfam")[0][0], 6f);
+    assertEquals(minMax.get("Rfam")[0][1], 6f);
+
+    /*
+     * check render order (last is on top)
+     */
+    List<String> renderOrder = fr.getRenderOrder();
+    assertEquals(renderOrder, Arrays.asList("Scop", "Rfam", "Pfam"));
+
+    /*
+     * change render order (todo: an easier way)
+     * nb here last comes first in the data array
+     */
+    FeatureSettingsBean[] data = new FeatureSettingsBean[3];
+    FeatureColourI colour = new FeatureColour(Color.RED);
+    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.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
+
+    /*
+     * add a new feature type: should go on top of render order as visible,
+     * other feature ordering and visibility should be unchanged
+     */
+    seqs.get(2).addSequenceFeature(
+            new SequenceFeature("Metal", "Desc", 14, 22, 8f, "MetalGroup"));
+    fr.findAllFeatures(true);
+    assertEquals(fr.getRenderOrder(),
+            Arrays.asList("Scop", "Pfam", "Rfam", "Metal"));
+    assertEquals(fr.getDisplayedFeatureTypes(),
+            Arrays.asList("Rfam", "Metal"));
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeaturesAtColumn()
+  {
+    String seqData = ">s1/4-29\n-ab--cdefghijklmnopqrstuvwxyz\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
+    SequenceI seq = av.getAlignment().getSequenceAt(0);
+
+    /*
+     * with no features
+     */
+    List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, 3);
+    assertTrue(features.isEmpty());
+
+    /*
+     * add features
+     */
+    SequenceFeature sf1 = new SequenceFeature("Type1", "Desc", 0, 0, 1f,
+            "Group"); // non-positional
+    seq.addSequenceFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Type2", "Desc", 8, 18, 1f,
+            "Group1");
+    seq.addSequenceFeature(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
+            "Group2");
+    seq.addSequenceFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Type3", "Desc", 8, 18, 1f,
+            null); // null group is always treated as visible
+    seq.addSequenceFeature(sf4);
+
+    /*
+     * add contact features
+     */
+    SequenceFeature sf5 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, "Group1");
+    seq.addSequenceFeature(sf5);
+    SequenceFeature sf6 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, "Group2");
+    seq.addSequenceFeature(sf6);
+    SequenceFeature sf7 = new SequenceFeature("Disulphide Bond", "Desc", 7,
+            15, 1f, null);
+    seq.addSequenceFeature(sf7);
+
+    // feature spanning B--C
+    SequenceFeature sf8 = new SequenceFeature("Type1", "Desc", 5, 6, 1f,
+            "Group");
+    seq.addSequenceFeature(sf8);
+    // contact feature B/C
+    SequenceFeature sf9 = new SequenceFeature("Disulphide Bond", "Desc", 5,
+            6, 1f, "Group");
+    seq.addSequenceFeature(sf9);
+
+    /*
+     * let feature renderer discover features (and make visible)
+     */
+    fr.findAllFeatures(true);
+    features = fr.findFeaturesAtColumn(seq, 15); // all positional
+    assertEquals(features.size(), 6);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf6));
+    assertTrue(features.contains(sf7));
+
+    /*
+     * at a non-contact position
+     */
+    features = fr.findFeaturesAtColumn(seq, 14);
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+
+    /*
+     * make "Type2" not displayed
+     */
+    FeatureColourI colour = new FeatureColour(Color.RED);
+    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);
+    assertEquals(features.size(), 5); // no sf2
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf6));
+    assertTrue(features.contains(sf7));
+
+    /*
+     * make "Group2" not displayed
+     */
+    fr.setGroupVisibility("Group2", false);
+
+    features = fr.findFeaturesAtColumn(seq, 15);
+    assertEquals(features.size(), 3); // no sf2, sf3, sf6
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+    assertTrue(features.contains(sf7));
+
+    // features 'at' a gap between b and c
+    // - returns enclosing feature BC but not contact feature B/C
+    features = fr.findFeaturesAtColumn(seq, 4);
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf8));
+    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")
+  public void testFilterFeaturesForDisplay()
+  {
+    String seqData = ">s1\nabcdef\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
+
+    List<SequenceFeature> features = new ArrayList<>();
+    fr.filterFeaturesForDisplay(features); // empty list, does nothing
+
+    SequenceI seq = av.getAlignment().getSequenceAt(0);
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
+            "group1");
+    seq.addSequenceFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 5, 11, 2f,
+            "group2");
+    seq.addSequenceFeature(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 5, 11, 3f,
+            "group3");
+    seq.addSequenceFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 6, 8, 4f,
+            "group4");
+    seq.addSequenceFeature(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "", 6, 9, 5f,
+            "group4");
+    seq.addSequenceFeature(sf5);
+
+    fr.findAllFeatures(true);
+
+    features = seq.getSequenceFeatures();
+    assertEquals(features.size(), 5);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * filter out duplicate (co-located) features
+     * note: which gets removed is not guaranteed
+     */
+    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) || features.contains(sf3));
+    assertFalse(features.contains(sf2) && features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * 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);
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertTrue(features.contains(sf2) || features.contains(sf3));
+    assertFalse(features.contains(sf2) && features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * no filtering if transparency is applied
+     */
+    fr.setTransparency(0.5f);
+    features = seq.getSequenceFeatures();
+    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);
+
+    /*
+     * simple colour, feature type and group displayed
+     */
+    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);
+
+    /*
+     * hide feature type, then unhide
+     * - feature type visibility should not affect the result
+     */
+    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 1c93856..b7a5164 100644 (file)
@@ -49,7 +49,7 @@ public class AnnotationColourGradientTest
       anns[col] = new Annotation("a", "a", 'a', col, colour);
     }
 
-    seq = new Sequence("", "");
+    seq = new Sequence("Seq", "");
     al = new Alignment(new SequenceI[]{ seq});
     
     /*
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 c16d541..a96caec 100644 (file)
@@ -22,9 +22,12 @@ package jalview.schemes;
 
 import static org.testng.AssertJUnit.assertEquals;
 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;
@@ -35,6 +38,8 @@ import java.awt.Color;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class FeatureColourTest
 {
 
@@ -56,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
@@ -67,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());
 
@@ -81,63 +110,135 @@ public class FeatureColourTest
     fc1 = new FeatureColour(fc);
     assertTrue(fc1.isColourByLabel());
     assertFalse(fc1.isGraduatedColour());
-  }
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
 
-  @Test(groups = { "Functional" })
-  public void testIsColored_simpleColour()
-  {
-    FeatureColour fc = new FeatureColour(Color.RED);
-    assertTrue(fc.isColored(new SequenceFeature()));
-  }
-
-  @Test(groups = { "Functional" })
-  public void testIsColored_colourByLabel()
-  {
-    FeatureColour fc = new FeatureColour();
+    /*
+     * colour by attribute (label)
+     */
+    fc = new FeatureColour();
     fc.setColourByLabel(true);
-    assertTrue(fc.isColored(new SequenceFeature()));
+    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 testIsColored_aboveThreshold()
+  public void testCopyConstructor_minMax()
   {
-    // graduated colour range from score 20 to 100
-    FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 20f,
-            100f);
+    /*
+     * 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());
 
-    // score 0 is adjusted to bottom of range
-    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 0f,
-            null);
-    assertTrue(fc.isColored(sf));
-    assertEquals(Color.WHITE, fc.getColor(sf));
-
-    // score 120 is adjusted to top of range
-    sf.setScore(120f);
-    assertEquals(Color.BLACK, fc.getColor(sf));
-
-    // value below threshold is still rendered
-    // setting threshold has no effect yet...
-    fc.setThreshold(60f);
-    sf.setScore(36f);
-    assertTrue(fc.isColored(sf));
-    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
+    /*
+     * 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"));
 
-    // now apply threshold:
-    fc.setAboveThreshold(true);
-    assertFalse(fc.isColored(sf));
-    // colour is still returned though ?!?
-    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
+    /*
+     * 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());
 
-    sf.setScore(84); // above threshold now
-    assertTrue(fc.isColored(sf));
-    assertEquals(new Color(51, 51, 51), fc.getColor(sf));
+    /*
+     * 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" })
   public void testGetColor_simpleColour()
   {
     FeatureColour fc = new FeatureColour(Color.RED);
-    assertEquals(Color.RED, fc.getColor(new SequenceFeature()));
+    assertEquals(Color.RED,
+            fc.getColor(new SequenceFeature("Cath", "", 1, 2, 0f, null)));
   }
 
   @Test(groups = { "Functional" })
@@ -154,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,
@@ -169,20 +273,35 @@ public class FeatureColourTest
   }
 
   @Test(groups = { "Functional" })
-  public void testGetColor_belowThreshold()
+  public void testGetColor_aboveBelowThreshold()
   {
     // gradient from [50, 150] from WHITE(255, 255, 255) to BLACK(0, 0, 0)
     FeatureColour fc = new FeatureColour(Color.WHITE, Color.BLACK, 50f,
             150f);
     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 70f,
             null);
+
+    /*
+     * feature with score of Float.NaN is always assigned minimum colour
+     */
+    SequenceFeature sf2 = new SequenceFeature("type", "desc", 0, 20,
+            Float.NaN, null);
+
     fc.setThreshold(100f); // ignore for now
-    assertTrue(fc.isColored(sf));
     assertEquals(new Color(204, 204, 204), fc.getColor(sf));
+    assertEquals(Color.white, fc.getColor(sf2));
 
     fc.setAboveThreshold(true); // feature lies below threshold
-    assertFalse(fc.isColored(sf));
-    assertEquals(new Color(204, 204, 204), fc.getColor(sf));
+    assertNull(fc.getColor(sf));
+    assertEquals(Color.white, fc.getColor(sf2));
+
+    fc.setBelowThreshold(true);
+    fc.setThreshold(70f);
+    assertNull(fc.getColor(sf)); // feature score == threshold - hidden
+    assertEquals(Color.white, fc.getColor(sf2));
+    fc.setThreshold(69f);
+    assertNull(fc.getColor(sf)); // feature score > threshold - hidden
+    assertEquals(Color.white, fc.getColor(sf2));
   }
 
   /**
@@ -207,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"));
 
@@ -232,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"));
   }
@@ -277,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());
 
@@ -325,19 +495,117 @@ 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());
     assertFalse(fc.hasThreshold());
     assertEquals(Color.RED, fc.getMinColour());
     assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(Color.RED, fc.getNoColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * the same, with 'no value colour' specified as max
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("red|green|novaluemax|10.0|20.0");
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(Color.GREEN, fc.getNoColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+
+    /*
+     * the same, with 'no value colour' specified as min
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("red|green|novalueMin|10.0|20.0");
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(Color.RED, fc.getNoColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+
+    /*
+     * the same, with 'no value colour' specified as none
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("red|green|novaluenone|10.0|20.0");
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertNull(fc.getNoColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+
+    /*
+     * the same, with invalid 'no value colour'
+     */
+    try
+    {
+      fc = FeatureColour
+              .parseJalviewFeatureColour("red|green|blue|10.0|20.0");
+      fail("expected exception");
+    } catch (IllegalArgumentException e)
+    {
+      assertEquals(
+              "Couldn't parse the minimum value for graduated colour ('blue')",
+              e.getMessage());
+    }
+
+    /*
+     * 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 hex code) (above threshold)
+     * 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),
@@ -354,9 +622,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());
@@ -368,9 +653,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 a7e52ff..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()
   {
@@ -145,7 +170,7 @@ public class StructureSelectionManagerTest
     /*
      * Verify a RESNUM sequence feature in the PDBfile sequence
      */
-    SequenceFeature sf = pmap.getSeqs().get(0).getSequenceFeatures()[0];
+    SequenceFeature sf = pmap.getSeqs().get(0).getSequenceFeatures().get(0);
     assertEquals("RESNUM", sf.getType());
     assertEquals("1gaq", sf.getFeatureGroup());
     assertEquals("GLU:  19  1gaqA", sf.getDescription());
@@ -155,9 +180,318 @@ public class StructureSelectionManagerTest
      * sequence
      */
     StructureMapping map = sm.getMapping("examples/1gaq.txt")[0];
-    sf = map.sequence.getSequenceFeatures()[0];
+    sf = map.sequence.getSequenceFeatures().get(0);
     assertEquals("RESNUM", sf.getType());
     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..86dcc39 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,169 @@ 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);
+    assertEquals("{[3, 4], [8, 12]}", prettyPrint(ml1.getFromRanges()));
+    assertEquals("{[5, 8], [11, 13]}", prettyPrint(ml1.getToRanges()));
+
+    MapList ml2 = new MapList(new int[] { 1, 50 }, new int[] { 40, 45, 70,
+        75, 90, 127 }, 1, 1);
+    assertEquals("{[1, 50]}", prettyPrint(ml2.getFromRanges()));
+    assertEquals("{[40, 45], [70, 75], [90, 127]}",
+            prettyPrint(ml2.getToRanges()));
+
+    MapList compound = ml1.traverse(ml2);
+
+    assertEquals(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    List<int[]> fromRanges = compound.getFromRanges();
+    assertEquals(2, fromRanges.size());
+    assertArrayEquals(new int[] { 3, 4 }, fromRanges.get(0));
+    assertArrayEquals(new int[] { 8, 12 }, fromRanges.get(1));
+    List<int[]> toRanges = compound.getToRanges();
+    assertEquals(4, toRanges.size());
+    // 5-8 maps to 44-45,70-71
+    // 11-13 maps to 74-75,90
+    assertArrayEquals(new int[] { 44, 45 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 70, 71 }, toRanges.get(1));
+    assertArrayEquals(new int[] { 74, 75 }, toRanges.get(2));
+    assertArrayEquals(new int[] { 90, 90 }, toRanges.get(3));
+
+    /*
+     * 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(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 1, 50 }, fromRanges.get(0));
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 931, 901 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 600, 582 }, toRanges.get(1));
+
+    /*
+     * 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(1, compound.getFromRatio());
+    assertEquals(3, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+    // 11-40 maps to 31-50,91-160
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 31, 50 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 91, 160 }, toRanges.get(1));
+
+    /*
+     * 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(3, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+    // 11-20 maps to 11-15, 91-95
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 11, 15 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 91, 95 }, toRanges.get(1));
+
+    /*
+     * 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(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 21, 40 }, fromRanges.get(0));
+    // 13-72 maps 3:1 to 55-70, 121-124
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 55, 70 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 121, 124 }, toRanges.get(1));
+
+    /*
+     * 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(1, compound.getFromRatio());
+    assertEquals(1, compound.getToRatio());
+    fromRanges = compound.getFromRanges();
+    assertEquals(1, fromRanges.size());
+    assertArrayEquals(new int[] { 31, 90 }, fromRanges.get(0));
+    // 13-32 maps to 47-50,71-126
+    toRanges = compound.getToRanges();
+    assertEquals(2, toRanges.size());
+    assertArrayEquals(new int[] { 47, 50 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 71, 126 }, toRanges.get(1));
+
+    /*
+     * 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 70a3687..41a313f 100644 (file)
@@ -1,6 +1,7 @@
 package jalview.viewmodel;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
 import jalview.analysis.AlignmentGenerator;
@@ -12,6 +13,7 @@ import jalview.datamodel.HiddenSequences;
 import java.beans.PropertyChangeEvent;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.testng.annotations.BeforeClass;
@@ -83,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);
   }
@@ -167,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);
@@ -346,27 +365,50 @@ public class ViewportRangesTest {
   @Test(groups = { "Functional" })
   public void testScrollToWrappedVisible()
   {
-    ViewportRanges vr = new ViewportRanges(al);
+    AlignmentI al2 = gen.generate(60, 30, 1, 5, 5);
+
+    ViewportRanges vr = new ViewportRanges(al2);
+
+    // start with viewport on 5-14
     vr.setViewportStartAndWidth(5, 10);
+    assertEquals(vr.getStartRes(), 5);
+    assertEquals(vr.getEndRes(), 14);
+
+    // scroll to 12 - no change
+    assertFalse(vr.scrollToWrappedVisible(12));
+    assertEquals(vr.getStartRes(), 5);
 
-    vr.scrollToWrappedVisible(0);
+    // scroll to 2 - back to 0-9
+    assertTrue(vr.scrollToWrappedVisible(2));
     assertEquals(vr.getStartRes(), 0);
+    assertEquals(vr.getEndRes(), 9);
 
-    vr.scrollToWrappedVisible(10);
-    assertEquals(vr.getStartRes(), 10);
+    // scroll to 9 - no change
+    assertFalse(vr.scrollToWrappedVisible(9));
+    assertEquals(vr.getStartRes(), 0);
 
-    vr.scrollToWrappedVisible(15);
+    // scroll to 12 - moves to 10-19
+    assertTrue(vr.scrollToWrappedVisible(12));
     assertEquals(vr.getStartRes(), 10);
+    assertEquals(vr.getEndRes(), 19);
+
+    vr.setStartRes(13);
+    assertEquals(vr.getStartRes(), 13);
+    assertEquals(vr.getEndRes(), 22);
+
+    // scroll to 45 - jumps to 43-52
+    assertTrue(vr.scrollToWrappedVisible(45));
+    assertEquals(vr.getStartRes(), 43);
+    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);
@@ -377,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()
@@ -392,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
@@ -401,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);
@@ -409,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);
@@ -417,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);
@@ -426,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);
@@ -448,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
@@ -462,55 +516,71 @@ 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();
+
+    /*
+     * scrollToWrappedVisible does nothing if the target position is
+     * within the current startRes-endRes range
+     */
+    assertFalse(vr.scrollToWrappedVisible(5));
+    assertTrue(l.verify(0, Collections.<String> emptyList()));
     l.reset();
 
-    vr.scrollToWrappedVisible(5);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    vr.scrollToWrappedVisible(25);
+    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();
   }
 
@@ -730,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
@@ -751,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 26c3574..bc74d8a 100644 (file)
@@ -68,7 +68,7 @@ public class ViewStyleTest
     for (Field field : fields)
     {
       field.setAccessible(true);
-      if (!copyConstructorIgnores(field.getName()))
+      if (!copyConstructorIgnores(field))
       {
         changeValue(vs1, field);
       }
@@ -78,27 +78,36 @@ public class ViewStyleTest
 
     for (Field field1 : fields)
     {
-      final Object value1 = field1.get(vs1);
-      final Object value2 = field1.get(vs2);
-      String msg = "Mismatch in " + field1.getName() + "(" + value1 + "/"
+      if (!copyConstructorIgnores(field1))
+      {
+        final Object value1 = field1.get(vs1);
+        final Object value2 = field1.get(vs2);
+        String msg = "Mismatch in " + field1.getName() + "(" + value1 + "/"
               + value2 + ") - not set in copy constructor?";
-      assertEquals(msg, value1, value2);
+        assertEquals(msg, value1, value2);
+      }
     }
     assertEquals("Hashcode not equals", vs1.hashCode(), vs2.hashCode());
   }
 
   /**
-   * Add any field names in here that we expect to be ignored by the copy
-   * constructor
+   * Add tests here for any fields that we expect to be ignored by 
+   * the copy constructor
    * 
-   * @param name
+   * @param field
    * @return
    */
-  private boolean copyConstructorIgnores(String name)
+  private boolean copyConstructorIgnores(Field field)
   {
     /*
-     * currently none!
+     * just instrumentation added by test coverage while testing
      */
+    String type = field.getClass().toString();
+    if (type.toString().contains("com_atlassian_clover"))
+    {
+      // instrumentation added for test coverage - ignore
+      return true;
+    }
     return false;
   }
 
@@ -210,19 +219,22 @@ public class ViewStyleTest
     Field[] fields = ViewStyle.class.getDeclaredFields();
     for (Field field : fields)
     {
-      field.setAccessible(true);
-      Object oldValue = field.get(vs2);
-      changeValue(vs2, field);
-      assertFalse("equals method ignores " + field.getName(),
+      if (!copyConstructorIgnores(field))
+      {
+        field.setAccessible(true);
+        Object oldValue = field.get(vs2);
+        changeValue(vs2, field);
+        assertFalse("equals method ignores " + field.getName(),
               vs1.equals(vs2));
 
-      if (vs1.hashCode() == vs2.hashCode())
-      {
-        // uncomment next line to see which fields hashCode ignores
-        // System.out.println("hashCode ignores " + field.getName());
+        if (vs1.hashCode() == vs2.hashCode())
+        {
+          // uncomment next line to see which fields hashCode ignores
+          // System.out.println("hashCode ignores " + field.getName());
+        }
+        // restore original value before testing the next field
+        field.set(vs2, oldValue);
       }
-      // restore original value before testing the next field
-      field.set(vs2, oldValue);
     }
   }
 }
index 95863e7..d1e32b9 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;
@@ -58,7 +62,7 @@ public class PDBSequenceFetcherTest
     Cache.applicationProperties.setProperty("ADD_SS_ANN",
             Boolean.TRUE.toString());
 
-    sf = new SequenceFetcher(false);
+    sf = new SequenceFetcher();
   }
 
   /**
@@ -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());
     }
   }
-
 }
index 32afd5f..0b501ee 100644 (file)
@@ -52,26 +52,24 @@ public class SequenceFetcherTest
    */
   public static void main(String[] argv)
   {
-    // TODO: extracted from SequenceFetcher - convert to proper unit test with
+    // TODO: extracted from SequenceFetcher - convert to network dependent
+    // functional integration test with
     // assertions
 
     String usage = "SequenceFetcher.main [-nodas] [<DBNAME> [<ACCNO>]]\n"
             + "With no arguments, all DbSources will be queried with their test Accession number.\n"
             + "With one argument, the argument will be resolved to one or more db sources and each will be queried with their test accession only.\n"
-            + "If given two arguments, SequenceFetcher will try to find the DbFetcher corresponding to <DBNAME> and retrieve <ACCNO> from it.\n"
-            + "The -nodas option will exclude DAS sources from the database fetchers Jalview will try to use.";
-    boolean withDas = true;
-    if (argv != null && argv.length > 0
-            && argv[0].toLowerCase().startsWith("-nodas"))
+            + "If given two arguments, SequenceFetcher will try to find the DbFetcher corresponding to <DBNAME> and retrieve <ACCNO> from it.";
+
+    if (argv != null && argv.length > 0)
     {
-      withDas = false;
       String targs[] = new String[argv.length - 1];
       System.arraycopy(argv, 1, targs, 0, targs.length);
       argv = targs;
     }
     if (argv != null && argv.length > 0)
     {
-      List<DbSourceProxy> sps = new SequenceFetcher(withDas)
+      List<DbSourceProxy> sps = new SequenceFetcher()
               .getSourceProxy(argv[0]);
 
       if (sps != null)
@@ -102,7 +100,7 @@ public class SequenceFetcherTest
       System.out.println(usage);
       return;
     }
-    ASequenceFetcher sfetcher = new SequenceFetcher(withDas);
+    ASequenceFetcher sfetcher = new SequenceFetcher();
     String[] dbSources = sfetcher.getSupportedDb();
     for (int dbsource = 0; dbsource < dbSources.length; dbsource++)
     {
@@ -124,7 +122,7 @@ public class SequenceFetcherTest
           String testQuery)
   {
     AlignmentI ds = null;
-    Vector<Object[]> noProds = new Vector<Object[]>();
+    Vector<Object[]> noProds = new Vector<>();
     System.out.println("Source: " + sp.getDbName() + " (" + db
             + "): retrieving test:" + sp.getTestQuery());
     {
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
-package jalview.ws.seqfetcher;
+package jalview.ws.dbsources;
 
-import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertEquals;
 
 import jalview.bin.Cache;
-import jalview.gui.JvOptionPane;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
-public class DasSequenceFetcher
+public class PfamFullTest
 {
-
   @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
+  public void setUp()
   {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
   }
 
-  @Test(groups = { "Network" })
-  public void testDasRegistryContact()
+  @Test(groups = "Functional")
+  public void testGetURL()
   {
-    Cache.getDasSourceRegistry().refreshSources();
-    assertTrue(Cache.getDasSourceRegistry().getSources().isEmpty(),
-            "Expected to find no DAS sources at the registry. Check config.");
-  }
+    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);
+  }
+}
index 90d4472..e04d195 100644 (file)
@@ -51,7 +51,7 @@ public class RemoteFormatTest
     Cache.applicationProperties.setProperty("ADD_SS_ANN",
             Boolean.TRUE.toString());
 
-    sf = new SequenceFetcher(false);
+    sf = new SequenceFetcher();
   }
 
   @DataProvider(name = "AccessionData")
@@ -92,7 +92,7 @@ public class RemoteFormatTest
   @Test(groups = { "Network" })
   public void testUniprotFreeTextSearch() throws Exception
   {
-    List<FTSDataColumnI> wantedFields = new ArrayList<FTSDataColumnI>();
+    List<FTSDataColumnI> wantedFields = new ArrayList<>();
     FTSRestClientI client = UniProtFTSRestClient.getInstance();
     wantedFields.add(client.getDataColumnByNameOrCode("id"));
     wantedFields.add(client.getDataColumnByNameOrCode("entry name"));
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 2f548d0..060c303 100644 (file)
@@ -26,15 +26,16 @@ import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertNull;
 
 import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.datamodel.UniprotEntry;
+import jalview.datamodel.xdb.uniprot.UniprotEntry;
+import jalview.datamodel.xdb.uniprot.UniprotFeature;
 import jalview.gui.JvOptionPane;
 
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.Vector;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -63,6 +64,12 @@ 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>"
+          + "<feature type=\"sequence variant\" description=\"Foo\"><variation>L</variation><variation>LMV</variation><original>M</original><location><position position=\"42\"/></location></feature>"
+          + "<feature type=\"sequence variant\" description=\"Foo\"><variation>LL</variation><variation>LMV</variation><original>ML</original><location><begin position=\"42\"/><end position=\"43\"/></location></feature>"
+          + "<feature type=\"sequence variant\" description=\"Foo Too\"><variation>LL</variation><variation>LMVK</variation><original>MLML</original><location><begin position=\"42\"/><end position=\"45\"/></location></feature>"
           + "<sequence length=\"10\" mass=\"27410\" checksum=\"8CB760AACF88FE6C\" modified=\"2008-01-15\" version=\"1\">MHAPL VSKDL</sequence></entry>"
           + "</uniprot>";
 
@@ -97,13 +104,12 @@ public class UniprotTest
     /*
      * Check sequence features
      */
-    Vector<SequenceFeature> features = entry.getFeature();
-    assertEquals(3, features.size());
-    SequenceFeature sf = features.get(0);
+    Vector<UniprotFeature> features = entry.getFeature();
+    assertEquals(9, features.size());
+    UniprotFeature sf = features.get(0);
     assertEquals("signal peptide", sf.getType());
     assertNull(sf.getDescription());
     assertNull(sf.getStatus());
-    assertEquals(1, sf.getPosition());
     assertEquals(1, sf.getBegin());
     assertEquals(18, sf.getEnd());
     sf = features.get(1);
@@ -119,6 +125,52 @@ public class UniprotTest
     assertEquals(21, sf.getBegin());
     assertEquals(247, sf.getEnd());
 
+    sf = features.get(3);
+    assertEquals("sequence variant", sf.getType());
+    assertNull(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", 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", sf.getDescription());
+    assertEquals(41, sf.getPosition());
+    assertEquals(41, sf.getBegin());
+    assertEquals(41, sf.getEnd());
+
+    sf = features.get(6);
+    assertEquals("sequence variant", sf.getType());
+    assertEquals("Foo",
+            sf.getDescription());
+    assertEquals(42, sf.getPosition());
+    assertEquals(42, sf.getBegin());
+    assertEquals(42, sf.getEnd());
+    Assert.assertEquals(Uniprot.getDescription(sf),
+            "<html>p.Met42Leu" + "<br/>&nbsp;&nbsp;"
+                    + "p.Met42LeuMetVal Foo</html>");
+
+    sf = features.get(7);
+    assertEquals(42, sf.getBegin());
+    assertEquals(43, sf.getEnd());
+    Assert.assertEquals(Uniprot.getDescription(sf),
+            "<html>p.MetLeu42LeuLeu" + "<br/>&nbsp;&nbsp;"
+                    + "p.MetLeu42LeuMetVal Foo</html>");
+
+    sf = features.get(8);
+    assertEquals(42, sf.getBegin());
+    assertEquals(45, sf.getEnd());
+    Assert.assertEquals(Uniprot.getDescription(sf),
+            "<html>p.MLML42LeuLeu" + "<br/>&nbsp;&nbsp;"
+                    + "p.MLML42LMVK Foo Too</html>");
+
     /*
      * Check cross-references
      */
@@ -139,10 +191,8 @@ public class UniprotTest
     xref = xrefs.get(2);
     assertEquals("AE007869", xref.getId());
     assertEquals("EMBL", xref.getType());
-    assertEquals("AAK85932.1",
- xref.getProperty("protein sequence ID"));
-    assertEquals("Genomic_DNA",
- xref.getProperty("molecule type"));
+    assertEquals("AAK85932.1", xref.getProperty("protein sequence ID"));
+    assertEquals("Genomic_DNA", xref.getProperty("molecule type"));
   }
 
   @Test(groups = { "Functional" })
@@ -166,11 +216,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));
   }
 
   /**
@@ -189,4 +239,53 @@ public class UniprotTest
     assertEquals(expectedDescription,
             Uniprot.getUniprotEntryDescription(entry));
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetDescription()
+  {
+    UniprotFeature uf = new UniprotFeature();
+    assertEquals("", Uniprot.getDescription(uf));
+
+    uf.setDescription("Hello");
+    assertEquals("Hello", Uniprot.getDescription(uf));
+
+    uf.setPosition(23);
+    uf.setOriginal("K");
+    Vector<String> vars = new Vector<>();
+    vars.add("y");
+    uf.setVariation(vars);
+    assertEquals("p.Lys23Tyr Hello", Uniprot.getDescription(uf));
+
+    // multiple variants generate an html description over more than one line
+    vars.add("W");
+    assertEquals("<html>p.Lys23Tyr<br/>&nbsp;&nbsp;p.Lys23Trp Hello</html>",
+            Uniprot.getDescription(uf));
+
+    /*
+     * indel cases
+     * up to 3 bases (original or variant) are shown using 3 letter code
+     */
+    vars.clear();
+    vars.add("KWE");
+    uf.setOriginal("KLS");
+    assertEquals("p.LysLeuSer23LysTrpGlu Hello",
+            Uniprot.getDescription(uf));
+
+    // adding a fourth original base switches to single letter code
+    uf.setOriginal("KLST");
+    assertEquals("p.KLST23LysTrpGlu Hello", Uniprot.getDescription(uf));
+
+    // adding a fourth variant switches to single letter code
+    vars.clear();
+    vars.add("KWES");
+    assertEquals("p.KLST23KWES Hello", Uniprot.getDescription(uf));
+
+    vars.clear();
+    vars.add("z"); // unknown variant - fails gracefully
+    uf.setOriginal("K");
+    assertEquals("p.Lys23z Hello", Uniprot.getDescription(uf));
+
+    uf.setVariation(null); // variant missing - is ignored
+    assertEquals("Hello", Uniprot.getDescription(uf));
+  }
 }
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 e35f83e..de91af3 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.ws.seqfetcher;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertTrue;
 
@@ -173,13 +174,13 @@ public class DbRefFetcherTest
     SequenceI seq = alsq.getSequenceAt(0);
     assertEquals("Wrong sequence name", embl.getDbSource() + "|"
             + retrievalId, seq.getName());
-    SequenceFeature[] sfs = seq.getSequenceFeatures();
-    assertNotNull("Sequence features missing", sfs);
+    List<SequenceFeature> sfs = seq.getSequenceFeatures();
+    assertFalse("Sequence features missing", sfs.isEmpty());
     assertTrue(
             "Feature not CDS",
             FeatureProperties.isCodingFeature(embl.getDbSource(),
-                    sfs[0].getType()));
-    assertEquals(embl.getDbSource(), sfs[0].getFeatureGroup());
+ sfs.get(0).getType()));
+    assertEquals(embl.getDbSource(), sfs.get(0).getFeatureGroup());
     DBRefEntry[] dr = DBRefUtils.selectRefs(seq.getDBRefs(),
             new String[] { DBRefSource.UNIPROT });
     assertNotNull(dr);
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..c69d554 100755 (executable)
@@ -200,7 +200,7 @@ Can be separated by colons (Mac OS/Unix) or semicolons (Windows)]]></string>
                                                        <method name="addElement">
                                                                <object class="com.zerog.ia.installer.util.LAXPropertyData" objectID="97fa91cfa6f7">
                                                                        <property name="propertyValue">
-                                                                               <string><![CDATA[268400000]]></string>
+                                                                               <string><![CDATA[1060000000]]></string>
                                                                        </property>
                                                                        <property name="propertyName">
                                                                                <string><![CDATA[lax.nl.java.option.java.heap.size.max]]></string>
@@ -254,7 +254,7 @@ it will search for ones in this list]]></string>
                                                        <method name="addElement">
                                                                <object class="com.zerog.ia.installer.util.LAXPropertyData" objectID="97f991ffa6f7">
                                                                        <property name="propertyValue">
-                                                                               <string><![CDATA[33554432]]></string>
+                                                                               <string><![CDATA[1000000000]]></string>
                                                                        </property>
                                                                        <property name="propertyName">
                                                                                <string><![CDATA[lax.nl.java.option.java.heap.size.initial]]></string>
@@ -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[VAqua5-patch.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[VAqua5-patch.jar]]></string>
+              </property>
+              <property name="fileSize">
+                <long>1370564</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 e9e5176..82062f9 100755 (executable)
@@ -6,18 +6,26 @@ Jalview Readme
 
 To run application:
 
-java -Djava.ext.dirs=JALVIEW_HOME/lib -jar JALVIEW_HOME/jalview.jar [-help ... ]
+java -classpath "JALVIEW_HOME/lib/*" -jar JALVIEW_HOME/jalview.jar [-help ... ]
 
 Replace JALVIEW_HOME with the full path to Jalview Installation Directory.
 Use -help to see the available command line arguments.
 
 For best results use a Sun java run time environment (not the gcj gnu java, sorry!). 
 
-If you want to the java runtime bundled with Jalview, then launch jalview like this:
+If you want to use the java runtime bundled with Jalview, then launch jalview like this:
 
-JAVA_HOME=JALVIEW_HOME/jre JALVIEW_HOME/jre/bin/java -Djava.ext.dirs=JALVIEW_HOME/lib -jar JALVIEW_HOME/jalview.jar 
+in Windows:
+JAVA_HOME=JALVIEW_HOME/jre JALVIEW_HOME/jre/bin/java -classpath "JALVIEW_HOME/lib/*;JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+
+in Linux:
+JAVA_HOME=JALVIEW_HOME/jre JALVIEW_HOME/jre/bin/java -classpath "JALVIEW_HOME/lib/*:JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+
+in macOS:
+JAVA_HOME=JALVIEW_HOME/jre/Contents/Home JALVIEW_HOME/jre/Contents/Home/bin/java -classpath "JALVIEW_HOME/lib/*:JALVIEW_HOME/jalview.jar" jalview.bin.Jalview
+
+Note: ensure the -classpath argument is quoted and only use a terminating wildcard (e.g. 'DIR/*') to refer to all jar files in a directory, don't use e.g. 'DIR/*.jar'
 
-Please Note: the -jar jalview.jar option launches the jalview.bin.Jalview class in the jalview.jar
 ##################
 
 
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 e38064e..54f4e48 100644 (file)
@@ -1,6 +1,8 @@
 Checkstyle for Jalview
 ----------------------
 
+See
+https://issues.jalview.org/browse/JAL-1854
 http://checkstyle.sourceforge.net/
 GNU LGPL
 
@@ -9,8 +11,16 @@ To get the Eclipse Checkstyle plugin
        - Help | Eclipse Marketplace
        - search for checkstyle
        - install eclipse-cs checkstyle plugin
-The current version is 6.19.1 (August 2016).
 
+Change Log
+----------
+See http://checkstyle.sourceforge.net/releasenotes.html
+Aug 2016       Initial version used is 6.19.1
+Dec 2018       Updated to 8.12.0 (latest on Eclipse Marketplace, 8.15 is latest release)
+                       SuppressionCommentFilter relocated (changed in 8.1)
+                       FileContentsHolder removed (changed in 8.2)
+                       Updates to import-control.xml for code changes (htsjdk, stackoverflowusers)
+                       
 Config
 ------
 
@@ -37,6 +47,7 @@ How to use checkstyle
        Option 2: on demand on selected code
                - right-click on a class or package and Checkstyle | Check code with checkstyle
                - (or Clear Checkstyle violations to remove checkstyle warnings)
+               - recommended to use this as a QA step when changing or reviewing code
 
 Checkstyle rules
 ----------------
@@ -64,11 +75,11 @@ Suppressing findings
 Tips
 ----
        Sometimes checkstyle needs a kick before it will refresh its findings.
-       A whitespace edit in checkstyle.xml usually does this. There may be better ways.
+       Click the 'refresh' icon at top right in Eclipse | Preferences | Checkstyle.
        
        Invalid configuration files may result in checkstyle failing with an error reported
        in the Eclipse log file. 
-       Help | Installation Details | Configuration takes you to a screen with a 
+       Eclipse | About | Installation Details | Configuration takes you to a screen with a 
        'View Error Log' button.
        
        Sometimes checkstyle can fail silently. Try 'touching' (editing) config files, failing
index ac9e260..29a3047 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="[\\/]org[\\/]stackoverflowusers*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]uk[\\/]ac*"/>
     
     <!-- 
        ImportControl can only handle one top level package
index 85ac8e6..17946f7 100644 (file)
        </module>
 
        <!-- 
-               Allow suppression of rules by comments, e.g.:
-               // CHECKSTYLE.OFF: ParameterNumber
-               ..method declaration
-               // CHECKSTYLE.ON: ParameterNumber
-       -->
-       <module name="SuppressionCommentFilter">
-               <property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)"/>
-               <property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)"/>
-               <property name="checkFormat" value="$1"/>
-       </module>
-
-       <!-- 
                Check language bundles have the same keys and no duplicates
                (ensure Checkstyle is configured to scan non-source files)
         -->
                <property name="tabWidth" value="4"/>
 
                <!-- 
-                       Enables parsing of suppressions comments
-                       see http://checkstyle.sourceforge.net/config_filters.html#SuppressionCommentFilter 
+                       Allow suppression of rules by comments, e.g.:
+                       // CHECKSTYLE.OFF: ParameterNumber
+                       ..method declaration
+                       // CHECKSTYLE.ON: ParameterNumber
                -->
-               <module name="FileContentsHolder"/>
+               <module name="SuppressionCommentFilter">
+                       <property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)"/>
+                       <property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)"/>
+                       <property name="checkFormat" value="$1"/>
+               </module>
 
        <!-- ****************************** -->
        <!--         NAMING STANDARDS       -->
index b41aab3..478966f 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"/>
@@ -68,6 +94,7 @@
                <allow pkg="compbio.metadata" class="jalview.gui.WsJobParameters"/>
                <allow pkg="fr.orsay.lri.varna" class="jalview.gui.AppVarna"/>
                <allow pkg="fr.orsay.lri.varna" class="jalview.gui.AppVarnaBinding"/>
+               <allow pkg="org.stackoverflowusers.file" class="jalview.gui.Desktop"/>
                <allow pkg="uk.ac.vamsas" class="jalview.gui.VamsasApplication"/>
            </subpackage>
                
                <allow pkg="uk.ac.vamsas"/>
                <allow pkg="fr.orsay.lri.varna"/>
                <allow pkg="MCview"/>
+                       <subpackage name="vcf">
+                       <allow pkg="htsjdk\.*" regex="true"/>
+                       </subpackage>       
                </subpackage>       
                                
                <subpackage name="javascript">
                <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/classgraph-4.1.6.jar b/utils/classgraph-4.1.6.jar
new file mode 100644 (file)
index 0000000..a0788bc
Binary files /dev/null and b/utils/classgraph-4.1.6.jar differ
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