JAL-2446 merged to spike branch
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 9 Jun 2017 15:28:28 +0000 (16:28 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 9 Jun 2017 15:28:28 +0000 (16:28 +0100)
491 files changed:
.classpath
examples/appletParameters.html
examples/example.json
examples/exampleFeatures.txt
examples/groovy/colourConserved.groovy [new file with mode: 0644]
examples/groovy/colourSchemes.groovy [new file with mode: 0644]
examples/groovy/colourUnconserved.groovy [new file with mode: 0644]
examples/groovy/featuresCounter.groovy [new file with mode: 0644]
examples/groovy/visibleFeaturesCounter.groovy [new file with mode: 0644]
examples/testdata/example_annot_file.jva
help/help.jhm
help/helpTOC.xml
help/html/colourSchemes/clustal.html
help/html/colourSchemes/user.html
help/html/colourSchemes/userDefined_java7.gif
help/html/features/chimera.html
help/html/features/groovy.html
help/html/features/overview.html
help/html/features/preferences.html
help/html/features/splitView.html
help/html/groovy/featuresCounter.html [new file with mode: 0644]
help/html/io/tcoffeescores.html
help/html/menus/desktopMenu.html
help/html/menus/popupMenu.html
help/html/releases.html
help/html/webServices/JABAWS.html
help/html/webServices/jnet.html
help/html/webServices/jnetprediction.gif [changed mode: 0755->0644]
help/html/webServices/msaclient.html
help/html/webServices/proteinDisorder.html
help/html/webServices/urllinks.html
help/html/whatsNew.html
lib/jabaws-min-client-2.2.0.jar [new file with mode: 0644]
resources/lang/Messages.properties
resources/lang/Messages_es.properties
resources/scoreModel/blosum62.scm [new file with mode: 0644]
resources/scoreModel/blosum80.scm [new file with mode: 0644]
resources/scoreModel/dna.scm [new file with mode: 0644]
resources/scoreModel/pam250.scm [new file with mode: 0644]
resources/uniprot_mapping.xml
src/MCview/AppletPDBCanvas.java
src/MCview/AppletPDBViewer.java
src/MCview/PDBCanvas.java
src/MCview/PDBChain.java
src/MCview/PDBViewer.java [deleted file]
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/AlignSeq.java
src/jalview/analysis/AlignmentSorter.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/AverageDistanceTree.java [new file with mode: 0644]
src/jalview/analysis/Conservation.java
src/jalview/analysis/CrossRef.java
src/jalview/analysis/Dna.java
src/jalview/analysis/NJTree.java
src/jalview/analysis/PCA.java
src/jalview/analysis/Rna.java
src/jalview/analysis/SeqsetUtils.java
src/jalview/analysis/TreeBuilder.java [new file with mode: 0644]
src/jalview/analysis/TreeModel.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/DistanceScoreModel.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/FeatureDistanceModel.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/FeatureScoreModel.java [deleted file]
src/jalview/analysis/scoremodels/PIDModel.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/PIDScoreModel.java [deleted file]
src/jalview/analysis/scoremodels/ScoreMatrix.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/ScoreModels.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/SimilarityParams.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/SimilarityScoreModel.java [new file with mode: 0644]
src/jalview/analysis/scoremodels/SmithWatermanModel.java [moved from src/jalview/analysis/scoremodels/SWScoreModel.java with 63% similarity]
src/jalview/api/AlignViewportI.java
src/jalview/api/AlignmentColsCollectionI.java [new file with mode: 0644]
src/jalview/api/AlignmentRowsCollectionI.java [new file with mode: 0644]
src/jalview/api/ComplexAlignFile.java
src/jalview/api/FeatureRenderer.java
src/jalview/api/FeaturesDisplayedI.java
src/jalview/api/SequenceRenderer.java
src/jalview/api/SiftsClientI.java
src/jalview/api/ViewStyleI.java
src/jalview/api/analysis/PairwiseScoreModelI.java [new file with mode: 0644]
src/jalview/api/analysis/ScoreModelI.java
src/jalview/api/analysis/SimilarityParamsI.java [new file with mode: 0644]
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/CutAndPasteTransfer.java
src/jalview/appletgui/ExtJmol.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/OverviewCanvas.java [new file with mode: 0644]
src/jalview/appletgui/OverviewPanel.java
src/jalview/appletgui/PCAPanel.java
src/jalview/appletgui/RedundancyPanel.java
src/jalview/appletgui/ScalePanel.java
src/jalview/appletgui/SeqCanvas.java
src/jalview/appletgui/SeqPanel.java
src/jalview/appletgui/SequenceRenderer.java
src/jalview/appletgui/SliderPanel.java
src/jalview/appletgui/TreeCanvas.java
src/jalview/appletgui/TreePanel.java
src/jalview/appletgui/UserDefinedColours.java
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/bin/JalviewLite.java
src/jalview/commands/EditCommand.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/AlignmentI.java
src/jalview/datamodel/AlignmentView.java
src/jalview/datamodel/AllColsCollection.java [new file with mode: 0644]
src/jalview/datamodel/AllColsIterator.java [new file with mode: 0644]
src/jalview/datamodel/AllRowsCollection.java [new file with mode: 0644]
src/jalview/datamodel/AllRowsIterator.java [new file with mode: 0644]
src/jalview/datamodel/AnnotatedCollectionI.java
src/jalview/datamodel/Annotation.java
src/jalview/datamodel/BinarySequence.java
src/jalview/datamodel/CigarArray.java
src/jalview/datamodel/ColumnSelection.java
src/jalview/datamodel/ContiguousI.java [new file with mode: 0644]
src/jalview/datamodel/HiddenColumns.java [new file with mode: 0644]
src/jalview/datamodel/HiddenSequences.java
src/jalview/datamodel/Mapping.java
src/jalview/datamodel/Range.java [new file with mode: 0644]
src/jalview/datamodel/SearchResults.java
src/jalview/datamodel/SeqCigar.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceCollectionI.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/SequenceNode.java
src/jalview/datamodel/VisibleColsCollection.java [new file with mode: 0644]
src/jalview/datamodel/VisibleColsIterator.java [new file with mode: 0644]
src/jalview/datamodel/VisibleRowsCollection.java [new file with mode: 0644]
src/jalview/datamodel/VisibleRowsIterator.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureLocationI.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 [new file with mode: 0755]
src/jalview/datamodel/xdb/uniprot/UniprotFeature.java [new file with mode: 0644]
src/jalview/datamodel/xdb/uniprot/UniprotFile.java [new file with mode: 0755]
src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java [new file with mode: 0755]
src/jalview/datamodel/xdb/uniprot/UniprotSequence.java [new file with mode: 0755]
src/jalview/ext/android/ContainerHelpers.java
src/jalview/ext/android/SparseDoubleArray.java [new file with mode: 0644]
src/jalview/ext/ensembl/EnsemblCdna.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblRestClient.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/rbvi/chimera/AtomSpecModel.java
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/ext/varna/VarnaCommands.java
src/jalview/fts/api/GFTSPanelI.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/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/AppVarnaBinding.java
src/jalview/gui/CalculationChooser.java [new file with mode: 0644]
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/ColourMenuHelper.java [new file with mode: 0644]
src/jalview/gui/ComboBoxTooltipRenderer.java [new file with mode: 0644]
src/jalview/gui/Console.java
src/jalview/gui/CutAndPasteTransfer.java
src/jalview/gui/DasSourceBrowser.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureRenderer.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/FontChooser.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JDatabaseTree.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/Jalview2XML_V1.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/JalviewDialog.java
src/jalview/gui/OverviewCanvas.java [new file with mode: 0644]
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/RedundancyPanel.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/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/WsParamSetManager.java
src/jalview/gui/WsPreferences.java
src/jalview/io/AnnotationFile.java
src/jalview/io/AppletFormatAdapter.java
src/jalview/io/FeaturesFile.java
src/jalview/io/FileFormat.java
src/jalview/io/FileFormats.java
src/jalview/io/FileLoader.java
src/jalview/io/FormatAdapter.java
src/jalview/io/HTMLOutput.java
src/jalview/io/HtmlFile.java
src/jalview/io/IdentifyFile.java
src/jalview/io/JSONFile.java
src/jalview/io/JalviewFileChooser.java
src/jalview/io/ScoreMatrixFile.java [new file with mode: 0644]
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/StructureFile.java
src/jalview/io/TCoffeeScoreFile.java
src/jalview/io/VamsasAppDatastore.java
src/jalview/io/cache/AppCache.java [new file with mode: 0644]
src/jalview/io/cache/JvCacheableInputBox.java [new file with mode: 0644]
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/packed/JalviewDataset.java
src/jalview/io/vamsas/Datasetsequence.java
src/jalview/io/vamsas/Sequencefeature.java
src/jalview/io/vamsas/Tree.java
src/jalview/javascript/JsSelectionSender.java
src/jalview/javascript/MouseOverStructureListener.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GDesktop.java
src/jalview/jbgui/GFinder.java
src/jalview/jbgui/GFontChooser.java
src/jalview/jbgui/GPCAPanel.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GSequenceLink.java
src/jalview/jbgui/GSliderPanel.java
src/jalview/jbgui/GStructureChooser.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/jbgui/GUserDefinedColours.java
src/jalview/math/Matrix.java
src/jalview/math/MatrixI.java [new file with mode: 0644]
src/jalview/math/SparseMatrix.java [new file with mode: 0644]
src/jalview/renderer/AnnotationRenderer.java
src/jalview/renderer/OverviewRenderer.java [new file with mode: 0644]
src/jalview/renderer/ResidueShader.java [new file with mode: 0644]
src/jalview/renderer/ResidueShaderI.java [new file with mode: 0644]
src/jalview/renderer/ScaleRenderer.java
src/jalview/renderer/seqfeatures/FeatureColourFinder.java [new file with mode: 0644]
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/schemes/AnnotationColourGradient.java
src/jalview/schemes/Blosum62ColourScheme.java
src/jalview/schemes/BuriedColourScheme.java
src/jalview/schemes/ClustalxColourScheme.java
src/jalview/schemes/ColourSchemeI.java
src/jalview/schemes/ColourSchemeLoader.java [new file with mode: 0644]
src/jalview/schemes/ColourSchemeProperty.java
src/jalview/schemes/ColourSchemes.java [new file with mode: 0644]
src/jalview/schemes/CovariationColourScheme.java
src/jalview/schemes/FeatureColour.java
src/jalview/schemes/FollowerColourScheme.java
src/jalview/schemes/HelixColourScheme.java
src/jalview/schemes/HydrophobicColourScheme.java
src/jalview/schemes/JalviewColourScheme.java [new file with mode: 0644]
src/jalview/schemes/NucleotideColourScheme.java
src/jalview/schemes/PIDColourScheme.java
src/jalview/schemes/PurinePyrimidineColourScheme.java
src/jalview/schemes/RNAHelicesColour.java
src/jalview/schemes/RNAHelicesColourChooser.java
src/jalview/schemes/RNAInteractionColourScheme.java
src/jalview/schemes/ResidueColourScheme.java
src/jalview/schemes/ResidueProperties.java
src/jalview/schemes/ScoreColourScheme.java
src/jalview/schemes/ScoreMatrix.java [deleted file]
src/jalview/schemes/StrandColourScheme.java
src/jalview/schemes/TCoffeeColourScheme.java
src/jalview/schemes/TaylorColourScheme.java
src/jalview/schemes/TurnColourScheme.java
src/jalview/schemes/UserColourScheme.java
src/jalview/schemes/ZappoColourScheme.java
src/jalview/structure/SelectionListener.java
src/jalview/structure/StructureListener.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/structures/models/AAStructureBindingModel.java
src/jalview/urls/CustomUrlProvider.java [new file with mode: 0644]
src/jalview/urls/IdOrgSettings.java [new file with mode: 0644]
src/jalview/urls/IdentifiersUrlProvider.java [new file with mode: 0644]
src/jalview/urls/UrlLinkDisplay.java [new file with mode: 0644]
src/jalview/urls/UrlLinkTableModel.java [new file with mode: 0644]
src/jalview/urls/UrlProvider.java [new file with mode: 0644]
src/jalview/urls/UrlProviderImpl.java [new file with mode: 0644]
src/jalview/urls/api/UrlProviderFactoryI.java [new file with mode: 0644]
src/jalview/urls/api/UrlProviderI.java [new file with mode: 0644]
src/jalview/urls/applet/AppletUrlProviderFactory.java [new file with mode: 0644]
src/jalview/urls/desktop/DesktopUrlProviderFactory.java [new file with mode: 0644]
src/jalview/util/ColorUtils.java
src/jalview/util/Comparison.java
src/jalview/util/ImageMaker.java
src/jalview/util/IntRangeComparator.java [new file with mode: 0644]
src/jalview/util/LinkedIdentityHashSet.java
src/jalview/util/MappingUtils.java
src/jalview/util/ParseHtmlBodyAndLinks.java
src/jalview/util/SetUtils.java [new file with mode: 0644]
src/jalview/util/UrlConstants.java
src/jalview/util/UrlLink.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/OverviewDimensions.java [new file with mode: 0644]
src/jalview/viewmodel/OverviewDimensionsHideHidden.java [new file with mode: 0644]
src/jalview/viewmodel/OverviewDimensionsShowHidden.java [new file with mode: 0644]
src/jalview/viewmodel/PCAModel.java
src/jalview/viewmodel/ViewportListenerI.java [new file with mode: 0644]
src/jalview/viewmodel/ViewportProperties.java [new file with mode: 0644]
src/jalview/viewmodel/ViewportRanges.java [new file with mode: 0644]
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/seqfeatures/FeaturesDisplayed.java
src/jalview/viewmodel/styles/ViewStyle.java
src/jalview/workers/AlignmentAnnotationFactory.java
src/jalview/workers/ColumnCounterSetWorker.java [moved from src/jalview/workers/ColumnCounterWorker.java with 60% similarity]
src/jalview/workers/ConsensusThread.java
src/jalview/workers/ConservationThread.java
src/jalview/workers/FeatureSetCounterI.java [moved from src/jalview/workers/FeatureCounterI.java with 76% similarity]
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/DasSequenceFeatureFetcher.java
src/jalview/ws/dbsources/Uniprot.java
src/jalview/ws/dbsources/das/datamodel/DasSourceRegistry.java
src/jalview/ws/jws1/JPredThread.java
src/jalview/ws/jws1/MsaWSThread.java
src/jalview/ws/jws1/SeqSearchWSThread.java
src/jalview/ws/jws2/AADisorderClient.java
src/jalview/ws/jws2/JPred301Client.java [deleted file]
src/jalview/ws/jws2/Jws2Discoverer.java
src/jalview/ws/jws2/MsaWSThread.java
src/jalview/ws/jws2/jabaws2/Jws2InstanceFactory.java
src/jalview/ws/rest/RestClient.java
src/jalview/ws/rest/RestJobThread.java
src/jalview/ws/rest/params/Alignment.java
src/jalview/ws/sifts/SiftsClient.java
src/jalview/ws/utils/UrlDownloadClient.java [new file with mode: 0644]
test/MCview/PDBChainTest.java
test/jalview/analysis/AlignSeqTest.java
test/jalview/analysis/AlignmentSorterTest.java [new file with mode: 0644]
test/jalview/analysis/AlignmentUtilsTests.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 [new file with mode: 0644]
test/jalview/analysis/scoremodels/FeatureScoreModelTest.java [deleted file]
test/jalview/analysis/scoremodels/PIDModelTest.java [new file with mode: 0644]
test/jalview/analysis/scoremodels/ScoreMatrixTest.java [new file with mode: 0644]
test/jalview/analysis/scoremodels/ScoreModelsTest.java [new file with mode: 0644]
test/jalview/commands/EditCommandTest.java
test/jalview/datamodel/AlignmentAnnotationTests.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/datamodel/AlignmentViewTest.java
test/jalview/datamodel/AllColsIteratorTest.java [new file with mode: 0644]
test/jalview/datamodel/AllRowsIteratorTest.java [new file with mode: 0644]
test/jalview/datamodel/ColumnSelectionTest.java
test/jalview/datamodel/HiddenColumnsTest.java [new file with mode: 0644]
test/jalview/datamodel/HiddenSequencesTest.java
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/datamodel/SequenceGroupTest.java [new file with mode: 0644]
test/jalview/datamodel/SequenceTest.java
test/jalview/datamodel/VisibleColsIteratorTest.java [new file with mode: 0644]
test/jalview/datamodel/VisibleRowsIteratorTest.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/android/SparseDoubleArrayTest.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/EnsemblSeqProxyTest.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/jmol/JmolParserTest.java
test/jalview/ext/jmol/JmolViewerTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/fts/service/pdb/PDBFTSPanelTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/AlignViewportTest.java
test/jalview/gui/AlignmentPanelTest.java [new file with mode: 0644]
test/jalview/gui/AnnotationChooserTest.java
test/jalview/gui/AnnotationRowFilterTest.java [new file with mode: 0644]
test/jalview/gui/PopupMenuTest.java
test/jalview/gui/SeqPanelTest.java [new file with mode: 0644]
test/jalview/gui/SequenceRendererTest.java
test/jalview/gui/StructureChooserTest.java
test/jalview/io/AnnotatedPDBFileInputTest.java
test/jalview/io/AnnotationFileIOTest.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/FileFormatsTest.java
test/jalview/io/FileLoaderTest.java [new file with mode: 0644]
test/jalview/io/IdentifyFileTest.java
test/jalview/io/JSONFileTest.java
test/jalview/io/Jalview2xmlTests.java
test/jalview/io/JalviewFileViewTest.java
test/jalview/io/NewickFileTests.java
test/jalview/io/PhylipFileTests.java
test/jalview/io/RNAMLfileTest.java
test/jalview/io/ScoreMatrixFileTest.java [new file with mode: 0644]
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/StockholmFileTest.java
test/jalview/io/cache/AppCacheTest.java [new file with mode: 0644]
test/jalview/io/cache/JvCacheableInputBoxTest.java [new file with mode: 0644]
test/jalview/io/gff/Gff3HelperTest.java
test/jalview/io/gff/InterProScanHelperTest.java
test/jalview/io/testProps.jvprops
test/jalview/math/MatrixTest.java
test/jalview/math/SparseMatrixTest.java [new file with mode: 0644]
test/jalview/renderer/ResidueShaderTest.java [new file with mode: 0644]
test/jalview/renderer/ScaleRendererTest.java [new file with mode: 0644]
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java [new file with mode: 0644]
test/jalview/renderer/seqfeatures/FeatureRendererTest.java [new file with mode: 0644]
test/jalview/schemes/AnnotationColourGradientTest.java [new file with mode: 0644]
test/jalview/schemes/Blosum62ColourSchemeTest.java [new file with mode: 0644]
test/jalview/schemes/ClustalxColourSchemeTest.java [new file with mode: 0644]
test/jalview/schemes/ColourSchemePropertyTest.java [new file with mode: 0644]
test/jalview/schemes/ColourSchemesTest.java [new file with mode: 0644]
test/jalview/schemes/FeatureColourTest.java
test/jalview/schemes/JalviewColourSchemeTest.java [new file with mode: 0644]
test/jalview/schemes/PIDColourSchemeTest.java [new file with mode: 0644]
test/jalview/schemes/ResidueColourSchemeTest.java
test/jalview/schemes/ScoreMatrixPrinter.java [deleted file]
test/jalview/schemes/UserColourSchemeTest.java
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java
test/jalview/urls/AppletUrlProviderFactoryTest.java [new file with mode: 0644]
test/jalview/urls/CustomUrlProviderTest.java [new file with mode: 0644]
test/jalview/urls/DesktopUrlProviderFactoryTest.java [new file with mode: 0644]
test/jalview/urls/IdentifiersUrlProviderTest.java [new file with mode: 0644]
test/jalview/urls/UrlLinkDisplayTest.java [new file with mode: 0644]
test/jalview/urls/UrlLinkTableModelTest.java [new file with mode: 0644]
test/jalview/urls/UrlProviderTest.java [new file with mode: 0644]
test/jalview/util/ColorUtilsTest.java
test/jalview/util/ComparisonTest.java
test/jalview/util/MappingUtilsTest.java
test/jalview/util/SetUtilsTest.java [new file with mode: 0644]
test/jalview/util/UrlLinkTest.java
test/jalview/viewmodel/OverviewDimensionsHideHiddenTest.java [new file with mode: 0644]
test/jalview/viewmodel/OverviewDimensionsShowHiddenTest.java [new file with mode: 0644]
test/jalview/viewmodel/ViewportRangesTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/RemoteFormatTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/UniprotTest.java
test/jalview/ws/jabaws/DisorderAnnotExportImport.java
test/jalview/ws/jabaws/JpredJabaStructExportImport.java [deleted file]
test/jalview/ws/jabaws/MinJabawsClientTests.java
test/jalview/ws/jabaws/RNAStructExportImport.java
test/jalview/ws/jws2/ParameterUtilsTest.java
test/jalview/ws/seqfetcher/DasSequenceFetcher.java
test/jalview/ws/seqfetcher/DbRefFetcherTest.java
test/jalview/ws/sifts/SiftsClientTest.java
test/jalview/ws/utils/UrlDownloadClientTest.java [new file with mode: 0644]
test/junit/extensions/PA.java [new file with mode: 0644]
test/junit/extensions/PrivilegedAccessor.java [new file with mode: 0644]

index 8aef745..c4a2832 100644 (file)
@@ -39,7 +39,7 @@
        <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/min-jabaws-client-2.1.0.jar" sourcepath="/clustengine"/>
+       <classpathentry kind="lib" path="lib/jabaws-min-client-2.2.0.jar" sourcepath="/clustengine"/>
        <classpathentry kind="lib" path="lib/json_simple-1.1.jar" sourcepath="/Users/jimp/Downloads/json_simple-1.1-all.zip"/>
        <classpathentry kind="lib" path="lib/slf4j-api-1.7.7.jar"/>
        <classpathentry kind="lib" path="lib/jsoup-1.8.1.jar"/>
index 277fd69..04cc235 100644 (file)
@@ -115,7 +115,7 @@ the applet can be interacted with <em>via</em> its
             <td>defaultColour</td>
             <td> <em>One of: </em><br>
               Clustal, Blosum62, % Identity, Hydrophobic, Zappo, Taylor, Helix 
-              Propensity, Strand Propensity, Turn Propensity, Buried Index, Nucleotide, T-Coffee Scores, RNA Helices</td>
+              Propensity, Strand Propensity, Turn Propensity, Buried Index, Nucleotide, Purine/Pyrimidine, T-Coffee Scores, RNA Helices</td>
             <td>Default is no colour.</td>
           </tr>
           <tr> 
@@ -159,6 +159,11 @@ the applet can be interacted with <em>via</em> its
             <td>Default is true.</td>
           </tr>
           <tr> 
+            <td>showOccupancy</td>
+            <td>true <em>or</em> false</td>
+            <td>Default is true.</td>
+          </tr>
+          <tr> 
             <td>sortBy</td>
             <td> Id <em>, </em> Pairwise Identity<em>, or</em> Length</td>
             <td> Sorts the alignment on startup</td>
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 2de9817..83dc4b1 100755 (executable)
@@ -1,5 +1,5 @@
 ST-TURN-IIL    blue|255,0,255|absolute|20.0|95.0|below|66.0
-GAMMA-TURN-CLASSIC     red|0,255,255|20.0|95.0|below|66.0
+GAMMA-TURN-CLASSIC     lightGray|0,255,255|20.0|95.0|below|66.0
 BETA-TURN-IR   9a6a94
 BETA-TURN-IL   d6a6ca
 BETA-BULGE     1dc451
diff --git a/examples/groovy/colourConserved.groovy b/examples/groovy/colourConserved.groovy
new file mode 100644 (file)
index 0000000..4a15922
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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 java.awt.Color
+import jalview.schemes.ColourSchemeI
+import jalview.schemes.ColourSchemes
+import jalview.datamodel.AnnotatedCollectionI
+import jalview.datamodel.SequenceI
+import jalview.datamodel.SequenceCollectionI
+import jalview.util.Comparison
+
+/*
+ * Closure that defines a colour scheme where fully conserved residues are red,
+ * partly conserved (match consensus but < 100% consensus) are yellow,
+ * unconserved and gaps are white
+ */
+def conserved
+conserved = { ->
+  [
+    /*
+     * name shown in the colour menu
+     */
+    getSchemeName: { -> 'Conserved' },
+    
+    /*
+     * to make a new instance for each alignment view
+     */
+    getInstance: { AnnotatedCollectionI coll, Map<SequenceI, SequenceCollectionI> map -> conserved() },
+    
+    /*
+     * method only needed if colour scheme has to recalculate
+     * values when an alignment is modified
+     */
+    alignmentChanged: { AnnotatedCollectionI coll, Map<SequenceI, SequenceCollectionI> map -> },
+    
+    /*
+     * determine colour for a residue at an aligned position of a
+     * sequence, given consensus residue(s) for the column and the
+     * consensus percentage identity score for the column
+     */
+    findColour: { char res, int col, SequenceI seq, String consensus, float pid -> 
+        if ('a' <= res && res <= 'z')
+        {
+            res -= ('a' - 'A');
+        }
+        if (Comparison.isGap(res) || !consensus.contains(String.valueOf(res)))
+        {
+            Color.white
+        } else if (pid < 100)
+        {
+            Color.yellow
+        } else
+        {
+            Color.red
+        }
+    },
+    
+    /*
+     * true means applicable to nucleotide or peptide data
+     */
+    isApplicableTo: {AnnotatedCollectionI coll -> true},
+    
+    /*
+     * simple colour schemes are those that depend on the residue
+     * only (these are also available to colour structure viewers)
+     */
+    isSimple: { false }
+ ] as ColourSchemeI
+}
+
+ColourSchemes.instance.registerColourScheme(conserved())
diff --git a/examples/groovy/colourSchemes.groovy b/examples/groovy/colourSchemes.groovy
new file mode 100644 (file)
index 0000000..d5ca973
--- /dev/null
@@ -0,0 +1,155 @@
+import java.awt.Color;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemes;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.SequenceCollectionI;
+
+/*
+ * Example script that registers two new alignment colour schemes
+ */
+
+/*
+ * Closure that defines a colour scheme where consensus residues are pink,
+ * other residues are red in odd columns and blue in even columns, and
+ * gaps are yellow  
+ */
+def candy
+candy = { ->
+  [
+    /*
+     * name shown in the colour menu
+     */
+    getSchemeName: { -> 'candy' },
+    
+    /*
+     * to make a new instance for each alignment view
+     */
+    getInstance: { AnnotatedCollectionI coll, Map<SequenceI, SequenceCollectionI> map -> candy() },
+    
+    /*
+     * method only needed if colour scheme has to recalculate
+     * values when an alignment is modified
+     */
+    alignmentChanged: { AnnotatedCollectionI coll, Map<SequenceI, SequenceCollectionI> map -> },
+    
+    /*
+     * determine colour for a residue at an aligned position of a
+     * sequence, given consensus residue(s) for the column and the
+     * consensus percentage identity score for the column
+     */
+    findColour: { char res, int col, SequenceI seq, String consensus, float pid -> 
+        if (res == ' ' || res == '-' || res == '.') 
+        {
+            Color.yellow
+        } else if (consensus.contains(String.valueOf(res)))
+        {
+            Color.pink
+        } else if (col % 2 == 0) 
+        {
+            Color.blue
+        } else 
+        {
+            Color.red
+        }
+    },
+    
+    /*
+     * true means applicable to nucleotide or peptide data
+     */
+    isApplicableTo: {AnnotatedCollectionI coll -> true},
+    
+    /*
+     * simple colour schemes are those that depend on the residue
+     * only (these are also available to colour structure viewers)
+     */
+    isSimple: { false }
+ ] as ColourSchemeI
+}
+
+/*
+ * A closure that defines a colour scheme graduated 
+ * (approximately) by amino acid weight
+ * here from lightest (G) Blue, to heaviest (W) Red
+ */
+def makeColour = { weight -> 
+    minWeight = 75 // Glycine
+    maxWeight = 204 // Tryptophan
+    int i = 255 * (weight - minWeight) / (maxWeight - minWeight);
+    new Color(i, 0, 255-i);
+}
+def byWeight
+byWeight = { ->
+  [
+    getSchemeName: { 'By Weight' },
+    // this colour scheme is peptide-specific:
+    isApplicableTo: { coll -> !coll.isNucleotide() },
+    alignmentChanged: { coll, map -> },
+    getInstance: { coll, map -> byWeight() },
+    isSimple: { true },
+    findColour: {res, col, seq, consensus, pid -> 
+        switch (res) {
+          case ' ':
+          case '-':
+          case '.':
+            Color.white
+             break
+          case 'A':
+            makeColour(89)
+            break
+          case 'R':
+            makeColour(174)
+            break
+          case 'N':
+          case 'D':
+          case 'B':
+          case 'I':
+          case 'L':
+            makeColour(132)
+            break
+          case 'C':
+            makeColour(121)
+            break
+          case 'Q':
+          case 'E':
+          case 'Z':
+          case 'K':
+          case 'M':
+            makeColour(146)
+            break
+          case 'G':
+            makeColour(75)
+            break
+          case 'H':
+            makeColour(155)
+            break
+          case 'F':
+            makeColour(165)
+            break
+          case 'P':
+            makeColour(115)
+            break
+          case 'S':
+            makeColour(105)
+            break
+          case 'T':
+            makeColour(119)
+            break
+          case 'W':
+            makeColour(204)
+            break
+          case 'Y':
+            makeColour(181)
+            break
+          case 'V':
+            makeColour(117)
+            break
+          default:
+            makeColour(150)
+        }
+      }
+  ] as ColourSchemeI
+}
+
+ColourSchemes.instance.registerColourScheme(candy())
+ColourSchemes.instance.registerColourScheme(byWeight())
diff --git a/examples/groovy/colourUnconserved.groovy b/examples/groovy/colourUnconserved.groovy
new file mode 100644 (file)
index 0000000..68730f3
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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 java.awt.Color
+import jalview.schemes.ColourSchemeI
+import jalview.schemes.ColourSchemes
+import jalview.datamodel.AnnotatedCollectionI
+import jalview.datamodel.SequenceI
+import jalview.datamodel.SequenceCollectionI
+import jalview.util.Comparison
+
+/*
+ * Closure that defines a colour scheme where non-consensus residues are pink,
+ * other residues (and gaps) are white
+ */
+def unconserved
+unconserved = { ->
+  [
+    /*
+     * name shown in the colour menu
+     */
+    getSchemeName: { -> 'Unconserved' },
+    
+    /*
+     * to make a new instance for each alignment view
+     */
+    getInstance: { AnnotatedCollectionI coll, Map<SequenceI, SequenceCollectionI> map -> unconserved() },
+    
+    /*
+     * method only needed if colour scheme has to recalculate
+     * values when an alignment is modified
+     */
+    alignmentChanged: { AnnotatedCollectionI coll, Map<SequenceI, SequenceCollectionI> map -> },
+    
+    /*
+     * determine colour for a residue at an aligned position of a
+     * sequence, given consensus residue(s) for the column and the
+     * consensus percentage identity score for the column
+     */
+    findColour: { char res, int col, SequenceI seq, String consensus, float pid -> 
+        if ('a' <= res && res <= 'z')
+        {
+            res -= ('a' - 'A');
+        }
+        if (Comparison.isGap(res) || consensus.contains(String.valueOf(res)))
+        {
+            Color.white
+        } else 
+        {
+            Color.pink
+        }
+    },
+    
+    /*
+     * true means applicable to nucleotide or peptide data
+     */
+    isApplicableTo: {AnnotatedCollectionI coll -> true},
+    
+    /*
+     * simple colour schemes are those that depend on the residue
+     * only (these are also available to colour structure viewers)
+     */
+    isSimple: { false }
+ ] as ColourSchemeI
+}
+
+ColourSchemes.instance.registerColourScheme(unconserved())
diff --git a/examples/groovy/featuresCounter.groovy b/examples/groovy/featuresCounter.groovy
new file mode 100644 (file)
index 0000000..dc4c97c
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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.workers.AlignmentAnnotationFactory;
+import jalview.workers.FeatureSetCounterI;
+
+/*
+ * Example script to compute two alignment annotations
+ * - count of Phosphorylation features
+ * - count of Turn features
+ * To try this, first load example file uniref50.fa and load on features file
+ * exampleFeatures.txt, before running this script
+ *
+ * The script only needs to be run once - it will be registered by Jalview
+ * and recalculated automatically when the alignment changes.
+ * 
+ * Note: The feature api provided by 2.10.2 is not compatible with scripts
+ * that worked with earlier Jalview versions. Apologies for the inconvenience.
+ */
+def annotator = 
+    [
+     getNames: { ['Phosphorylation', 'Turn'] as String[] }, 
+     getDescriptions:  { ['Count of Phosphorylation features', 'Count of Turn features'] as String[] },
+     getMinColour: { [0, 255, 255] as int[] }, // cyan
+     getMaxColour: { [0, 0, 255] as int[] }, // blue
+     count: 
+         { res, feats -> 
+                int phos
+                int turn
+                for (sf in feats)
+                {
+                         /*
+                          * Here we inspect the type of the sequence feature.
+                          * You can also test sf.description, sf.score, sf.featureGroup,
+                          * sf.strand, sf.phase, sf.begin, sf.end
+                          * or sf.getValue(attributeName) for GFF 'column 9' properties
+                          */
+                          if (sf.type.contains('TURN'))
+                   {
+                      turn++
+                   }
+                   if (sf.type.contains('PHOSPHORYLATION'))
+                   {
+                      phos++
+                   }
+                }
+                [phos, turn] as int[]
+         }
+     ] as FeatureSetCounterI
+    
+/*
+ * Register the annotation calculator with Jalview
+ */
+AlignmentAnnotationFactory.newCalculator(annotator) 
diff --git a/examples/groovy/visibleFeaturesCounter.groovy b/examples/groovy/visibleFeaturesCounter.groovy
new file mode 100644 (file)
index 0000000..b3180f8
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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.bin.Jalview
+import jalview.workers.FeatureSetCounterI
+import jalview.workers.AlignmentAnnotationFactory
+
+/*
+ * Demonstration of FeatureSetCounterI
+ * compute annotation tracks counting number of displayed 
+ * features of each type in each column
+ */
+
+/*
+ * discover features on the current view
+ */
+def featuresDisp=Jalview.currentAlignFrame.currentView.featuresDisplayed
+if (featuresDisp == null) {
+    print 'Need at least one feature visible on alignment'
+}
+def visibleFeatures=featuresDisp.visibleFeatures.toList()
+assert 'java.util.ArrayList' == visibleFeatures.class.name
+
+/*
+ * A closure that returns an array of features present 
+ * for each feature type in visibleFeatures
+ * Argument 'features' will be a list of SequenceFeature 
+ */
+def getCounts = 
+    { features -> 
+        int[] obs = new int[visibleFeatures.size]
+        for (sf in features)
+        {
+            /*
+             * Here we inspect the type of the sequence feature.
+             * You can also test sf.description, sf.score, sf.featureGroup,
+             * sf.strand, sf.phase, sf.begin, sf.end
+             * or sf.getValue(attributeName) for GFF 'column 9' properties
+             */
+            int pos = 0
+            for (type in visibleFeatures) 
+            {
+              if (type.equals(sf.type)) 
+              {
+                  obs[pos]++
+              }
+              pos++
+            }
+        }
+        obs
+}
+  
+/*
+ * Define something that counts each visible feature type
+ */
+def columnSetCounter =
+    [
+     getNames: { visibleFeatures as String[] }, 
+     getDescriptions:  { visibleFeatures as String[] },
+     getMinColour: { [0, 255, 255] as int[] }, // cyan
+     getMaxColour: { [0, 0, 255] as int[] }, // blue
+     count: 
+         { res, feats -> 
+             getCounts.call(feats) 
+         }
+     ] as FeatureSetCounterI
+
+/*
+ * and register the counter
+ */
+AlignmentAnnotationFactory.newCalculator(columnSetCounter)
index 794f42b..1779247 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
+PROPERTIES     Group_B outlineColour=red       colour=None
 PROPERTIES     Group_C colour=Clustal
index f69ed00..54cce2a 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.1"/>
+   <mapID target="release" url="html/releases.html#Jalview.2.10.2"/>
    <mapID target="alannotation" url="html/features/annotation.html"/>
    <mapID target="keys" url="html/keys.html"/>
    <mapID target="newkeys" url="html/features/newkeystrokes.html"/>
@@ -75,6 +75,7 @@
    <mapID target="jalarchive" url="html/features/jalarchive.html"/>
    <mapID target="multipleviews" url="html/features/multipleViews.html"/>
    <mapID target="splitframe" url="html/features/splitView.html"/>
+   <mapID target="splitframe.mirrorfonts" url="html/features/splitView.html#mirror"/>
    <mapID target="trees" url="html/calculations/tree.html"/>
    <mapID target="treeviewer" url="html/calculations/treeviewer.html"/>
    <mapID target="sorting" url="html/calculations/sorting.html"/>
    
    <mapID target="memory" url="html/memory.html" />
    <mapID target="groovy" url="html/features/groovy.html" />
-   <mapID target="groovy.featurecounter" url="html/groovy/featureCounter.html" />
+   <mapID target="groovy.colours" url="html/features/groovy.html#groovyColours" />
+   <mapID target="groovy.featurescounter" url="html/groovy/featuresCounter.html" />
    <mapID target="privacy" url="html/privacy.html" />
    <mapID target="vamsas" url="html/vamsas/index.html"/>
    <mapID target="aminoAcids" url="html/misc/aminoAcids.html" />
    
    <mapID target="uniprotfetcher" url="html/features/uniprotsequencefetcher.html" />
    
+   <mapID target="urllinks" url="html/webServices/urllinks.html" />
+   <mapID target="linksprefs" url="html/features/Preferences.html#links" />
+
    <mapID target="backIcon" url="icons/back.png" />
    <mapID target="forwardIcon" url="icons/forward.png" />
    <mapID target="homeIcon" url="icons/Home.png" />
    <mapID target="printIcon" url="icons/print.png" />
    <mapID target="printSetupIcon" url="icons/setup.png" />
+   
+   <mapID target="overview" url="features/overview.html" />
 </map>
index 54abd53..a8aadf5 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="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="identifers.org for URL Links" target="linksprefs" />
+                               <tocitem text="New features in Split Frame View" target="splitframe.mirrorfonts" />
                </tocitem>
                
                <tocitem text="Editing Alignments" target="edit" />
                        <tocitem text="Chimera Viewer" target="chimera" />                      
                </tocitem>
                <tocitem text="Viewing RNA structures" target="varna" expand="false"/>
+               <tocitem text="Opening URLs from Jalview" target="urllinks" expand="true">
+                   <tocitem text="Configuring URL Links" target="linksprefs" />
+               </tocitem>
                <tocitem text="VAMSAS Data Exchange" target="vamsas">
                        <!-- what can Jalview share with other apps -->
                        <!-- what other apps exist -->
                <tocitem text="Preferences" target="preferences" />
                <tocitem text="Memory Settings" target="memory" expand="false"/>
                <tocitem text="Scripting with Groovy" target="groovy">
-                       <tocitem text="Groovy Feature Counter example" target="groovy.featurecounter"/>
+                       <tocitem text="Groovy Features Counter example" target="groovy.featurescounter"/>
                </tocitem>
                <tocitem text="Command Line" target="commandline" expand="false">
                        <tocitem text="Command Line Arguments" target="clarguments" />
index 7b68c84..b3d44db 100755 (executable)
@@ -40,9 +40,10 @@ td {
     is assigned a colour if the amino acid profile of the alignment at
     that position meets some minimum criteria specific for the residue
     type.</p>
-  <p>The table below gives these criteria as clauses: {+X%,xx,y},
-    where X is the minimum percentage presence for any of the xx (or y)
-    residue types.</p>
+  <p>The table below gives these criteria as clauses: {&gt;X%,xx,y},
+    where X is the threshold percentage presence for any of the xx (or y)
+    residue types.
+    <br>For example, K or R is coloured red if the column includes more than 60% K or R (combined), or more than 80% of either K or R or Q (individually).</p>
   <div align="center">
     <p>&nbsp;</p>
     <table border="1">
@@ -52,70 +53,81 @@ td {
       <tr>
         <td><table border="1">
             <tr>
+              <th>Category</th>
+              <th>Colour</th>
               <th>Residue at position</th>
-              <th>Applied Colour</th>
-              <th>{ Threshhold, Residue group }</th>
+              <th>{ Threshold, Residue group }</th>
             </tr>
             <tr>
+              <td rowspan="2">Hydrophobic</td>
+              <td rowspan="2" bgcolor="#80a0f0">BLUE</td>
               <td>A,I,L,M,F,W,V</td>
-              <td bgcolor="#80a0f0">BLUE</td>
-              <td>{+60%, WLVIMAFCHP}</td>
+              <td>{&gt;60%, WLVIMAFCHP}</td>
             </tr>
             <tr>
-              <td>R,K</td>
+              <td>C</td>
+              <td>{&gt;60%, WLVIMAFCHP}</td>
+            </tr>
+            <tr>
+              <td>Positive charge</td>
               <td bgcolor="#f01505">RED</td>
-              <td>{+60%,KR},{+80%, K,R,Q}</td>
+              <td>K,R</td>
+              <td>{&gt;60%,KR},{&gt;80%, K,R,Q}</td>
             </tr>
             <tr>
-              <td>N</td>
-              <td bgcolor="#15c015">GREEN</td>
-              <td>{+50%, N}, {+85%, N,Y}</td>
+              <td rowspan="2">Negative charge</td>
+              <td rowspan="2" bgcolor="#c048c0">MAGENTA</td>
+              <td>E</td>
+              <td>{&gt;60%,KR},{&gt;50%,QE},{&gt;85%,E,Q,D}</td>
             </tr>
             <tr>
-              <td>C</td>
-              <td bgcolor="#80a0f0">BLUE</td>
-              <td>{+60%, WLVIMAFCHP}</td>
+              <td>D</td>
+              <td>{&gt;60%,KR}, {&gt;85%, K,R,Q}, {&gt;50%,ED}</td>
             </tr>
             <tr>
-              <td>C</td>
-              <td bgcolor="#f08080">PINK</td>
-              <td>{100%, C}</td>
+              <td rowspan="3">Polar</td>
+              <td rowspan="3" bgcolor="#15c015">GREEN</td>
+              <td>N</td>
+              <td>{&gt;50%, N}, {&gt;85%, N,Y}</td>
             </tr>
             <tr>
               <td>Q</td>
-              <td bgcolor="#15c015">GREEN</td>
-              <td>{+60%,KR},{+50%,QE},{+85%,Q,E,K,R}</td>
+              <td>{&gt;60%,KR},{&gt;50%,QE},{&gt;85%,Q,E,K,R}</td>
             </tr>
             <tr>
-              <td>E</td>
-              <td bgcolor="#c048c0">MAGENTA</td>
-              <td>{+60%,KR},{+50%,QE},{+85%,E,Q,D}</td>
+              <td>S,T</td>
+              <td>{&gt;60%, WLVIMAFCHP}, {&gt;50%, TS}, {&gt;85%,S,T}</td>
             </tr>
             <tr>
-              <td>D</td>
-              <td bgcolor="#c048c0">MAGENTA</td>
-              <td>{+60%,KR}, {+85%, K,R,Q}, {+50%,ED}</td>
+              <td>Cysteines</td>
+              <td bgcolor="#f08080">PINK</td>
+              <td>C</td>
+              <td>{>85%, C}</td>
             </tr>
             <tr>
-              <td>G</td>
+              <td>Glycines</td>
               <td bgcolor="#f09048">ORANGE</td>
-              <td>{+0%, G}</td>
+              <td>G</td>
+              <td>{&gt;0%, G}</td>
             </tr>
             <tr>
-              <td>H,Y</td>
-              <td bgcolor="#15a4a4">CYAN</td>
-              <td>{+60%, WLVIMAFCHP}, {+85%,
-                W,Y,A,C,P,Q,F,H,I,L,M,V}</td>
+              <td>Prolines</td>
+              <td bgcolor="#c0c000">YELLOW</td>
+              <td>P</td>
+              <td>{&gt;0%, P}</td>
             </tr>
             <tr>
-              <td>P</td>
-              <td bgcolor="#c0c000">YELLOW</td>
-              <td>{+0%, P}</td>
+              <td>Aromatic</td>
+              <td bgcolor="#15a4a4">CYAN</td>
+              <td>H,Y</td>
+              <td>{&gt;60%, WLVIMAFCHP}, {&gt;85%,
+                W,Y,A,C,P,Q,F,H,I,L,M,V}</td>
             </tr>
             <tr>
-              <td>S,T</td>
-              <td bgcolor="#15c015">GREEN</td>
-              <td>{+60%, WLVIMAFCHP}, {+50%, TS}, {+85%,S,T}</td>
+              <td>Unconserved</td>
+              <td>WHITE</td>
+              <td>any / gap</td>
+              <td>If none of the above criteria are met</td>
             </tr>
           </table></td>
       </tr>
index 7239e4d..c2fde1c 100755 (executable)
@@ -27,7 +27,7 @@
     <strong>User Defined Colours</strong>
   </p>
   <p>
-    <img src="userDefined_java6.gif" width="815" height="402">
+    <img src="userDefined_java7.gif" width="815" height="402">
   </p>
   <p>
     You may define any number of new colour schemes, each with a unique
@@ -41,6 +41,9 @@
     The <strong>Case Sensitive</strong> option allows you to choose
     distinct colours for upper and lower case residue codes.
   <p>
+    The <strong>Colour All Lower Case  </strong> option allows you to apply a selected colour
+    to all lower case residues.
+  <p>
     Click <strong>Apply</strong> or <strong>OK</strong> to set your new
     colours on the active alignment window.<br /> Click <strong>Cancel</strong>
     to undo your changes if you pressed the <strong>Apply</strong>
   <br> Any saved colour schemes will be automatically loaded the
   next time you use Jalview.
   <br>
-  <br>
-  <em>Note: the screenshot shows the appearance when running Java
-    version 6. For Java 7 (from Jalview 2.8.2) only the Swatches colour
-    chooser is currently supported (for reasons of available screen
-    space).</em>
-  <p />
 </body>
 </html>
index f0ced11..de80b84 100644 (file)
Binary files a/help/html/colourSchemes/userDefined_java7.gif and b/help/html/colourSchemes/userDefined_java7.gif differ
index 5ae00af..68ac465 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>
+            trace.<br/><br/>
         </em></li>
-      </ul></li>
-    <li><strong>Help<br>
+        <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>
     </strong>
       <ul>
         <li><strong>Chimera Help<br>
index 254f92e..ead4436 100644 (file)
@@ -108,9 +108,18 @@ print currentAlFrame.getTitle();</pre>
     simplified the alignment analysis programming interface in Jalview
     2.10 to make it easy for you to add your own dynamic annotation
     tracks with Groovy. Have a look at the <a
-      href="../groovy/featureCounter.html">featureCounter.groovy</a>
+      href="../groovy/featuresCounter.html">featuresCounter.groovy</a>
     example for more information.
   </p>
+  <p><a name="groovyColours"/>
+    <em>Creating custom colourschemes</em><br/>
+    You can create your own alignment colourschemes with a groovy script. We've provided two examples:<br/>
+    <ul>
+    <li><a href="http://www.jalview.org/examples/groovy/colourConserved.groovy">colourConserved.groovy</a> creates an 'Conserved' colourscheme - similar to the classic <a href="http://www.nrbsc.org/old/gfx/genedoc/">GeneDOC</a> shading model.</li>
+    <li><a href="http://www.jalview.org/examples/groovy/colourUnconserved.groovy">colourUnconserved.groovy</a> creates an 'Unconserved' colourscheme, where any unconserved residues are coloured pink.</li>
+    
+    </ul>
+  </p>
 
 </body>
 </html>
index 9d36a1c..4f26592 100755 (executable)
   <p>The red box indicates the currently viewed region of the
     alignment, this may be moved by clicking and dragging with the
     mouse.</p>
+    <p><strong>Right-click</strong> (or CMD-Click) to open the
+    overview's popup menu. This provides an option to include hidden
+    regions in the overview (shown as dark-grey rows and columns). <br/><br/>
+    <em>The option to include/exclude hidden regions in the
+    overview was introduced in Jalview 2.10.2</em>. 
   <p>
     <img src="overview.gif" width="407" height="137">
   </p>
index 6a8c86c..acd7ba6 100755 (executable)
       and displaying structure information.
     </li>
     <li>The <a href="#connections"><strong>&quot;Connections&quot;</strong>
-        Preferences</a> tab allows you to change the links made from Jalview
-      to your default web browser.
+        Preferences</a> tab allows you to configure Jalview's internet
+      settings and specify your default web browser.
+    </li>
+    <li>The <a href="#links"><strong>&quot;Links&quot;</strong>
+        Preferences</a> tab shows the currently configured <em>URL
+        Links</em> shown in the <strong>Link</strong> submenu in the Sequence
+      ID popup menu.
     </li>
     <li>The <a href="#output"><strong>&quot;Output&quot;</strong>
         Preferences</a> tab contains settings affecting the export of
   <p>
     <em>Show Annotations</em> - If this is selected the new window will
     display an annotation panel below the sequences. This annotation
-    panel may have several rows describing the whole alignment. The 3
-    standard annotations <em>Conservation</em>, <em>Quality</em> and <em>Consensus</em>
-    for the alignment may be shown or hidden by default using the
-    checkboxes below.
+    panel may have several rows describing the whole alignment. The 4
+    standard annotations <em>Conservation</em>, <em>Quality</em>, 
+    <em>Occupancy</em> and <em>Consensus</em> for the alignment may 
+    be shown or hidden by default using the checkboxes adjacent and
+    below.
   </p>
   <p>
     <em>Show group: Conservation and Consensus</em> controls the display
         Preferences tab</strong></a>
   </p>
   <p>
-    <em>URL Link From Sequence ID</em><br> These definitions are
-    used to generate URLs from a sequence's ID or database cross
-    references. Read more about <a
-      href="../webServices/urllinks.html#urllinks">configuring
-      URL links here</a>.
-  </p>
-  <p>
     <em>Default Browser (Unix)</em><br> Its difficult in Java to
     detect the default web browser for Unix users. If Jalview can't find
     your default web browser, enter the name or full path to your web
       statement</a> for more information.
   </p>
   <p>
+    <a name="links"><strong>The &quot;Links&quot; Preferences
+        tab</strong></a>
+  </p>
+  <p>
+    This panel shows a table, and two sections - <em>Edit</em> and <em>Filter</em>.
+    The table shows the available URL link definitions (consisting of a
+    database, Name, and URL template string), a checkbox <em>In
+      Menu</em> which indicates if the link is enabled, and <em>Double
+      Click</em> which marks the link that will be opened if a sequence's ID
+    is double clicked. The table can be sorted by clicking on the column headers.
+  </p>
+  <p><em>Edit Links</em><br /> This section contains three buttons,
+    <em>New</em>, <em>Edit</em> and <em>Delete</em>, which allow you to
+    create, modify and remove user-defined URL links from the Sequence
+    ID's links submenu.
+  </p>
+  <p>
+    <em>Filter</em><br /> The <em>Filter text</em> box allows you to
+    quickly show rows in the table containing a particular text string.
+    The <em>Custom only</em> button limits the entries in the table to
+    just those you have configured yourself <em>via</em> the <em>Edit
+      Links</em> buttons. Press <em>Show all</em> to clear any filters.
+  </p>
+  <p>The links table is prepoulated with persistent URLs for many common
+    bioinformatics databases (since 2.10.2). These links are downloaded by Jalview from
+    the <em>identifiers.org</em> website, and the names and URLs are not
+    user editable.
+    <a href="../webServices/urllinks.html#urllinks">Read more about configuring
+      URL links.</a>
+  </p>
+  <p>
     <a name="output"><strong>Output Preferences tab</strong></a>
   </p>
   <p>
     and PDB file association (if available). The Jalview id/start-end
     option is ignored if Modeller output is selected.
   <p>
-    <a name="editing"><strong>Editing Preferences tab</strong></a>
+    <a name="editing"><strong>e&quot;Editinge&quot; Preferences tab</strong></a>
   </p>
   <p>There are currently three options available which can be
     selected / deselected.</p>
index 03b993c..3862c39 100644 (file)
       alignments, the <strong><a href="../menus/alwformat.html">"Format&#8594;Font"</a></strong>
       menu option has an option 'Scale protein residues to codons'. This
       option will make each protein residue the same width as a DNA
-      codon (so the alignments 'line up' vertically)
+      codon (so the alignments 'line up' vertically).<br/><br/>
+      <a name="mirror"/>The 'Use same 
+      font for cDNA and peptide' checkbox, when enabled, ensures that font or
+       font-size changes in either the cDNA or Protein alignment will also 
+       be mirrored. (<em>Added in 2.10.2</em>)
     </li>
     <li><strong>"View&#8594;Protein"</strong> (in the cDNA panel)
       or <strong>"View&#8594;Nucleotide"</strong> (in the protein panel)
diff --git a/help/html/groovy/featuresCounter.html b/help/html/groovy/featuresCounter.html
new file mode 100644 (file)
index 0000000..3b6705b
--- /dev/null
@@ -0,0 +1,123 @@
+<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>Extending Jalview with Groovy - Feature Counter Example</title>
+</head>
+<body>
+  <p>
+    <strong>Extending Jalview with Groovy - A customisable
+      feature counter</strong><br /> <br />The groovy script below shows how to
+    add a new calculation track to a Jalview alignment window.
+  </p>
+  <p>As currently written, it will add two tracks to a protein
+    alignment view which count Pfam features in each column, and ones
+    where a charge residue also occur.</p>
+  <p>To try it for yourself:</p>
+  <ol>
+    <li>Copy and paste it into the groovy script console</li>
+    <li>Load the example Feredoxin project (the one that opens by
+      default when you first launched Jalview)</li>
+    <li>Select <strong>Calculations&#8594;Execute Groovy
+        Script</strong> from the alignment window's menu bar to run the script on
+      the current view.
+    </li>
+  </ol>
+  <strong>Please note: The 2.10.2 feature counting interface is not compatible with earlier versions.</strong><br/><br/>
+  <em><a
+    href="http://www.jalview.org/examples/groovy/featuresCounter.groovy">http://www.jalview.org/examples/groovy/featuresCounter.groovy</a>
+    - rendered with <a href="http://hilite.me">hilite.me</a></em>
+  <!-- HTML generated using hilite.me --><div style="background: #f8f8f8; overflow:auto;width:auto;border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;"><pre style="margin: 0; line-height: 125%"><span style="color: #408080; font-style: italic">/*</span>
+<span style="color: #408080; font-style: italic"> * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)</span>
+<span style="color: #408080; font-style: italic"> * Copyright (C) $$Year-Rel$$ The Jalview Authors</span>
+<span style="color: #408080; font-style: italic"> * </span>
+<span style="color: #408080; font-style: italic"> * This file is part of Jalview.</span>
+<span style="color: #408080; font-style: italic"> * </span>
+<span style="color: #408080; font-style: italic"> * Jalview is free software: you can redistribute it and/or</span>
+<span style="color: #408080; font-style: italic"> * modify it under the terms of the GNU General Public License </span>
+<span style="color: #408080; font-style: italic"> * as published by the Free Software Foundation, either version 3</span>
+<span style="color: #408080; font-style: italic"> * of the License, or (at your option) any later version.</span>
+<span style="color: #408080; font-style: italic"> *  </span>
+<span style="color: #408080; font-style: italic"> * Jalview is distributed in the hope that it will be useful, but </span>
+<span style="color: #408080; font-style: italic"> * WITHOUT ANY WARRANTY; without even the implied warranty </span>
+<span style="color: #408080; font-style: italic"> * of MERCHANTABILITY or FITNESS FOR A PARTICULAR </span>
+<span style="color: #408080; font-style: italic"> * PURPOSE.  See the GNU General Public License for more details.</span>
+<span style="color: #408080; font-style: italic"> * </span>
+<span style="color: #408080; font-style: italic"> * You should have received a copy of the GNU General Public License</span>
+<span style="color: #408080; font-style: italic"> * along with Jalview.  If not, see &lt;http://www.gnu.org/licenses/&gt;.</span>
+<span style="color: #408080; font-style: italic"> * The Jalview Authors are detailed in the &#39;AUTHORS&#39; file.</span>
+<span style="color: #408080; font-style: italic"> */</span>
+
+<span style="color: #008000; font-weight: bold">import</span> <span style="color: #0000FF; font-weight: bold">jalview.workers.AlignmentAnnotationFactory</span><span style="color: #666666">;</span>
+<span style="color: #008000; font-weight: bold">import</span> <span style="color: #0000FF; font-weight: bold">jalview.workers.FeatureSetCounterI</span><span style="color: #666666">;</span>
+
+<span style="color: #408080; font-style: italic">/*</span>
+<span style="color: #408080; font-style: italic"> * Example script to compute two alignment annotations</span>
+<span style="color: #408080; font-style: italic"> * - count of Phosphorylation features</span>
+<span style="color: #408080; font-style: italic"> * - count of Turn features</span>
+<span style="color: #408080; font-style: italic"> * To try this, first load example file uniref50.fa and load on features file</span>
+<span style="color: #408080; font-style: italic"> * exampleFeatures.txt, before running this script</span>
+<span style="color: #408080; font-style: italic"> *</span>
+<span style="color: #408080; font-style: italic"> * The script only needs to be run once - it will be registered by Jalview</span>
+<span style="color: #408080; font-style: italic"> * and recalculated automatically when the alignment changes.</span>
+<span style="color: #408080; font-style: italic"> * </span>
+<span style="color: #408080; font-style: italic"> * Note: The feature api provided by 2.10.2 is not compatible with scripts</span>
+<span style="color: #408080; font-style: italic"> * that worked with earlier Jalview versions. Apologies for the inconvenience.</span>
+<span style="color: #408080; font-style: italic"> */</span>
+<span style="color: #B00040">def</span> annotator <span style="color: #666666">=</span> 
+    <span style="color: #666666">[</span>
+     <span style="color: #A0A000">getNames:</span> <span style="color: #666666">{</span> <span style="color: #666666">[</span><span style="color: #BA2121">&#39;Phosphorylation&#39;</span><span style="color: #666666">,</span> <span style="color: #BA2121">&#39;Turn&#39;</span><span style="color: #666666">]</span> <span style="color: #008000; font-weight: bold">as</span> String<span style="color: #666666">[]</span> <span style="color: #666666">},</span> 
+     <span style="color: #A0A000">getDescriptions:</span>  <span style="color: #666666">{</span> <span style="color: #666666">[</span><span style="color: #BA2121">&#39;Count of Phosphorylation features&#39;</span><span style="color: #666666">,</span> <span style="color: #BA2121">&#39;Count of Turn features&#39;</span><span style="color: #666666">]</span> <span style="color: #008000; font-weight: bold">as</span> String<span style="color: #666666">[]</span> <span style="color: #666666">},</span>
+     <span style="color: #A0A000">getMinColour:</span> <span style="color: #666666">{</span> <span style="color: #666666">[0,</span> <span style="color: #666666">255,</span> <span style="color: #666666">255]</span> <span style="color: #008000; font-weight: bold">as</span> <span style="color: #B00040">int</span><span style="color: #666666">[]</span> <span style="color: #666666">},</span> <span style="color: #408080; font-style: italic">// cyan</span>
+     <span style="color: #A0A000">getMaxColour:</span> <span style="color: #666666">{</span> <span style="color: #666666">[0,</span> <span style="color: #666666">0,</span> <span style="color: #666666">255]</span> <span style="color: #008000; font-weight: bold">as</span> <span style="color: #B00040">int</span><span style="color: #666666">[]</span> <span style="color: #666666">},</span> <span style="color: #408080; font-style: italic">// blue</span>
+     <span style="color: #A0A000">count:</span> 
+         <span style="color: #666666">{</span> res<span style="color: #666666">,</span> feats <span style="color: #666666">-&gt;</span> 
+                <span style="color: #B00040">int</span> phos
+                <span style="color: #B00040">int</span> turn
+                <span style="color: #0000FF">for</span> <span style="color: #666666">(</span>sf <span style="color: #008000; font-weight: bold">in</span> feats<span style="color: #666666">)</span>
+                <span style="color: #666666">{</span>
+                         <span style="color: #408080; font-style: italic">/*</span>
+<span style="color: #408080; font-style: italic">                         * Here we inspect the type of the sequence feature.</span>
+<span style="color: #408080; font-style: italic">                         * You can also test sf.description, sf.score, sf.featureGroup,</span>
+<span style="color: #408080; font-style: italic">                         * sf.strand, sf.phase, sf.begin, sf.end</span>
+<span style="color: #408080; font-style: italic">                         * or sf.getValue(attributeName) for GFF &#39;column 9&#39; properties</span>
+<span style="color: #408080; font-style: italic">                         */</span>
+                          <span style="color: #008000; font-weight: bold">if</span> <span style="color: #666666">(</span>sf<span style="color: #666666">.</span><span style="color: #7D9029">type</span><span style="color: #666666">.</span><span style="color: #7D9029">contains</span><span style="color: #666666">(</span><span style="color: #BA2121">&#39;TURN&#39;</span><span style="color: #666666">))</span>
+                   <span style="color: #666666">{</span>
+                      turn<span style="color: #666666">++</span>
+                   <span style="color: #666666">}</span>
+                   <span style="color: #008000; font-weight: bold">if</span> <span style="color: #666666">(</span>sf<span style="color: #666666">.</span><span style="color: #7D9029">type</span><span style="color: #666666">.</span><span style="color: #7D9029">contains</span><span style="color: #666666">(</span><span style="color: #BA2121">&#39;PHOSPHORYLATION&#39;</span><span style="color: #666666">))</span>
+                   <span style="color: #666666">{</span>
+                      phos<span style="color: #666666">++</span>
+                   <span style="color: #666666">}</span>
+                <span style="color: #666666">}</span>
+                <span style="color: #666666">[</span>phos<span style="color: #666666">,</span> turn<span style="color: #666666">]</span> <span style="color: #008000; font-weight: bold">as</span> <span style="color: #B00040">int</span><span style="color: #666666">[]</span>
+         <span style="color: #666666">}</span>
+     <span style="color: #666666">]</span> <span style="color: #008000; font-weight: bold">as</span> FeatureSetCounterI
+    
+<span style="color: #408080; font-style: italic">/*</span>
+<span style="color: #408080; font-style: italic"> * Register the annotation calculator with Jalview</span>
+<span style="color: #408080; font-style: italic"> */</span>
+AlignmentAnnotationFactory<span style="color: #666666">.</span><span style="color: #7D9029">newCalculator</span><span style="color: #666666">(</span>annotator<span style="color: #666666">)</span> 
+</pre></div>
+</body>
+</html>
index 08d1889..e67e3af 100644 (file)
@@ -79,6 +79,20 @@ cons   999999999999999999999999999851000110321100001134
 1QCF   ----------6878742356789999999999889
 cons   00100000006877641356789999999999889
 </pre>
-
+<table border="1">
+<tr><th>Score</th><th>0</th><th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th><th>7</th><th>8</th><th>9</th></tr>
+<tr>
+<td>RGB colour</td>
+<td bgcolor="#6666FF">102, 102, 255</td>
+<td bgcolor="#00FF00">0, 255, 0</td>
+<td bgcolor="#66FF00">102, 255, 0</td>
+<td bgcolor="#CCFF00">204, 255, 0</td>
+<td bgcolor="#FFFF00">255, 255, 0</td>
+<td bgcolor="#FFCC00">255, 204, 0</td>
+<td bgcolor="#FF9900">255, 153, 0</td>
+<td bgcolor="#FF6600">255, 102, 0</td>
+<td bgcolor="#FF3300">255, 51, 0</td>
+<td bgcolor="#FF2000">255, 34, 0</td></tr>
+</table>
 </body>
 </html>
index 20e784b..a93ce4b 100755 (executable)
@@ -86,6 +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>
 
       </ul></li>
     <li><strong>Vamsas</strong> <em>For more details, read the
index d42f854..7625606 100755 (executable)
         href="../features/varna.html">VARNA</a>.
     </em></li>
     <li><a name="hideinserts"><strong>Hide Insertions</strong></a><br />
-      <em>Hides columns containing gaps in the current sequence or
-        selected region, and reveals columns not including gaps.</em>
+      <em>Hides columns containing gaps in both the current
+        sequence and selected region, and reveals columns not including
+        gaps. (before 2.10.2, this option hid or revealed columns
+        according to gaps in just the current sequence)</em></li>
     <li><strong>Hide Sequences</strong><br> <em>Hides the
         currently selected sequences in this alignment view.</em><strong><br>
     </strong></li>
index 6f44b3d..5c8da91 100755 (executable)
  -->
 <head>
 <title>Release History</title>
+<style>
+ul {
+  /* remove bullets, narrower indent */
+  list-style-type: none;
+  margin:0;
+  padding-left: 10px;
+  padding-bottom: 4px;
+}
+
+li {
+  /* separate the items from eachother */
+   margin-left: -3px;
+   padding-bottom: 3px;
+   padding-left: 6px;   
+}
+li:before {
+  /*   doesnt get processed in javahelp */
+  content: '\00b7 ';
+  padding: 3px;
+  margin-left: -14px;
+}
+
+</style>
 </head>
 <body>
   <p>
     <tr>
       <td width="60" nowrap>
         <div align="center">
+          <strong><a name="Jalview.2.10.2">2.10.2</a><br />
+            <em>30/5/2017</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <em>General</em>
+          <ul>
+          <li><!-- JAL-2360,JAL-2371, -->More robust colours and shader model for alignments and groups</li>
+          <li><!--  JAL-384 -->Custom shading schemes created via groovy scripts</li>
+          <li><!--  JAL-2491 -->linked scrolling of CDS/Protein views via Overview or sequence motif search operations</li>
+          <li><!--  JAL-2526 -->Efficiency improvements for interacting with alignment and overview windows</li>
+          <li><!-- JAL-2388 -->Hidden columns and sequences can be omitted in Overview</li>
+            <li>
+              <!-- JAL-2535 -->Posterior probability annotation from
+              Stockholm files imported as sequence associated annotation
+            </li>
+            <li>
+              <!-- JAL-2533 -->Sequence names don't include file
+              extension when importing structure files without embedded
+              names or PDB accessions
+            </li>
+            <li><!-- JAL-2547 -->Amend sequence features dialog box can be opened by double clicking gaps within sequence feature extent</li>
+          </ul>
+          <em>Application</em>
+          <ul>
+            <li>
+              <!-- JAL-2447 -->
+              Experimental Features Checkbox in Desktop's Tools
+              menu to hide or show untested features in the application.
+            </li>
+            <li><!-- JAL-1476 -->Warning in alignment status bar when there are not enough columns to superimpose structures in Chimera</li>
+          <li><!-- JAL-1596 -->Faster Chimera/Jalview communication by file-based command exchange</li>  
+          <li><!-- JAL-2316, -->URLs for viewing database cross-references provided by identifiers.org and the EMBL-EBI's MIRIAM DB</li>
+          <li><!-- JAL-2549 -->Updated JABAWS client to v2.2</li>
+          </ul>
+          <em>Experimental features</em>
+          <ul>
+            <li>
+              <!-- JAL-2295, JAL-2296 -->New entries in the Chimera menu
+              to transfer Chimera's structure attributes as Jalview
+              features, and vice-versa.
+            </li>
+          </ul>
+          <em>Applet</em>
+          <ul>
+          <li><!--  --></li>
+          </ul>
+          <em>Test Suite</em>
+          <li><!--  JAL-2474 -->Added PrivilegedAccessor to test suite</li>
+          <li><!-- JAL-2326 -->Prevent or clear modal dialogs raised during tests</li>
+          <li><!--  -->  
+          </ul>
+          </div></td><td><div align="left">
+          <em>General</em>
+          <ul>
+            <li>
+              <!-- JAL-2398, -->Fixed incorrect value in BLOSUM 62 score
+              matrix - C->R should be '3'<br />Old matrix restored with
+              this one-line groovy script:<br />jalview.analysis.scoremodels.ScoreModels.instance.BLOSUM62.@matrix[4][1]=3
+            </li>
+            <li>
+              <!-- JAL-2397 -->Fixed Jalview's treatment of gaps in PCA
+              and substitution matrix based Tree calculations.<br />In
+              earlier versions of Jalview, gaps matching gaps were
+              penalised, and gaps matching non-gaps penalised even more.
+              In the PCA calculation, gaps were actually treated as
+              non-gaps - so different costs were applied, which mean't
+              Jalview's PCAs were different to those produced by
+              SeqSpace.<br />Jalview now treats gaps in the same way as
+              SeqSpace (ie it scores them as 0). To restore pre-2.10.2
+              behaviour<br />
+              jalview.viewmodel.PCAModel.scoreGapAsAny=true // for
+              2.10.1 mode<br />
+              jalview.viewmodel.PCAModel.scoreGapAsAny=false // to
+              restore 2.10.2 mode
+            </li>
+            <li><!-- JAL-2346 -->Reopening Colour by annotation dialog doesn't reselect a specific sequence's associated annotation after it was used for colouring a view</li>
+          <li><!-- JAL-2430 -->Hidden regions in alignment views are not coloured in linked structure views</li>
+          <li><!-- JAL-2419 -->Current selection lost if popup menu opened on a region of alignment without groups</li>
+          <li><!-- JAL-2374 -->Popup menu not always shown for regions of an alignment with overlapping groups</li> 
+          <li><!-- JAL-2310 -->Finder double counts if both a sequence's name and description match</li>
+          <li><!-- JAL-2370 -->Hiding column selection containing two hidden regions results in incorrect hidden regions</li>
+          <li><!-- JAL-2377 -->PCA calculation could hang when generating output report when working with highly redundant alignments</li>
+          <li><!-- JAL-2365 -->Cannot configure feature colours with lightGray or darkGray via features file</li>
+          <li><!-- JAL-2421 -->Overview window visible region moves erratically when hidden rows or columns are present</li>
+          <li><!-- JAL-2362 -->Per-residue colourschemes applied via the Structure Viewer's colour menu don't correspond to sequence colouring</li>  
+          <li><!-- JAL-2405 -->Protein specific colours only offered in colour and group colour menu for protein alignments</li>
+          <li><!-- JAL-2386 -->'Apply to all groups' setting when changing colour does not apply Conservation slider value to all groups</li>
+          <li><!-- JAL-2385 -->Colour threshold slider doesn't update to reflect currently selected view or group's shading thresholds</li>
+          <li><!-- JAL-2373 -->Percentage identity and conservation menu items do not show a tick or allow shading to be disabled</li>
+          <li><!-- JAL-2385 -->Conservation shading or PID threshold lost when base colourscheme changed if slider not visible</li>
+          <li><!-- JAL-2547 -->Sequence features shown in tooltip for gaps before start of features</li>
+          <li><!-- JAL-2576 -->Very large alignments take a long time to load</li>
+          <li><!-- JAL-2590 -->Cannot load Newick trees from eggnog ortholog database</li>
+          </ul>
+          <em>Application</em>
+          <ul>
+          <li><!-- JAL-2401 -->Easier creation of colours for all 'Lower case' residues (button in colourscheme editor debugged and new documentation and tooltips added)</li> 
+          <li><!-- JAL-2399-->Text colour threshold's 'Cancel' button doesn't restore group-specific text colour thresholds</li>
+          <li><!-- JAL-2243 -->Feature settings panel does not update as new features are added to alignment</li> 
+          <li><!-- JAL-2436 -->Structure viewer's View -> Colour By view selection menu changes colours of alignment views</li>
+          <li><!--  JAL-2366 -->Proxy server address and port always appear enabled in Preferences->Connections</li>
+          <li><!-- JAL-2426 -->Spurious exceptions in console raised from alignment calculation workers after alignment has been closed</li>
+          <li><!-- JAL-1608 -->Typo in selection popup menu - Create groups now 'Create Group'</li>
+          <li><!-- JAL-1608 -->CMD/CTRL and G or Shift G for Create/Undefine group doesn't always work</li>
+          <li><!-- JAL-2464 -->Tree Viewer's Print Dialog doesn't get shown again after pressing 'Cancel'</li>
+          <li><!--  JAL-2461 -->DAS registry not found exceptions removed from console output</li>
+          <li><!--  JAL-2383 -->Above PID colour threshold not recovered when alignment view imported from project</li> 
+          <li><!-- JAL-2465 -->No mappings generated between structure and sequences extracted from structure files imported via URL</li>
+            <li>
+              <!-- JAL-2520 -->Structures loaded via URL are saved in
+              Jalview Projects rather than fetched via URL again when
+              the project is loaded and the structure viewed
+            </li>
+            <li><!-- JAL-1256 -->Trackpad horizontal scroll gesture adjusts start position in wrap mode</li>
+            <li><!-- JAL-2563 -->Status bar doesn't show positions for ambiguous amino acids</li>
+            <li><!-- JAL-2291 -->Hide insertions in PopUp menu excludes gaps in selection, current sequence and only within selected columns</li> 
+          </ul>
+          <em>Applet</em>
+          <ul>
+          <li><!-- JAL-2442 -->Features not rendered as transparent on overview or linked structure view</li> 
+          <li><!--  JAL-2372 -->Colour group by conservation doesn't work (since 2.8)</li>
+          <li><!-- JAL-2517 -->Hitting Cancel after applying user-defined colourscheme doesn't restore original colourscheme</li>
+          </ul>
+          <em>New Known Issues</em>
+          <ul>
+          <li><!--  JAL-2566 -->Protein/CDS view scrolling not always in phase after a sequence motif find operation</li>
+          <li><!-- JAL-2550 -->Importing annotation file with rows containing just upper and lower case letters are interpreted as WUSS rna secondary structure symbols</li>  
+          </ul>
+          
+          </div>
+    <tr>
+      <td width="60" nowrap>
+        <div align="center">
           <strong><a name="Jalview.2.10.1">2.10.1</a><br />
             <em>29/11/2016</em></strong>
         </div>
               <!--JAL-2332 -->Attempting to view structure for Hen
               lysozyme results in a PDB Client error dialog box
             </li>
+            <li>
+            <!-- JAL-2319 -->Structure View's mapping report switched ranges for PDB and sequence for SIFTS</li> 
+            <!-- JAL-2319 -->SIFTS 'Not_Observed' residues mapped to non-existant coordindate data</li> 
           </ul>
 <!--           <em>New Known Issues</em>
           <ul>
index f74e4c4..84b2b86 100644 (file)
     <strong><em>A</em></strong>nalysis <strong><em>W</em></strong>eb <strong><em>S</em></strong>ervices
     <strong>system</strong> (<strong>JABAWS</strong>)<br> Jalview
     includes a client for interacting with programmatic (SOAP) web
-    services for the <a href="http://www.compbio.dundee.ac.uk/jabaws">JABAWS</a>
-    service model, developed at the University of Dundee by Peter
-    Troshin and Geoff Barton. This is an open source system that
-    provides a framework for wrapping command line bioinformatics
+    services provided by the <a href="http://www.compbio.dundee.ac.uk/jabaws">JABAWS</a>
+    system, developed at the University of Dundee by Peter
+    Troshin, Sasha Sherstnev, Dan Barton, Fabio Madeira-Marquez, Jim Procter and Geoff Barton.
+    This is an open source system that provides a framework for wrapping command line bioinformatics
     analysis programs that enables them to be executed locally or on a
     cluster using data and analysis parameters provided by a program
     linked with the JABA engine directly or accessing it remotely <em>via</em>
     each new server.
   </p>
   <p>
-    <em>Support for accessing JABAWS servers was introduced in
-      Jalview 2.6.</em>
+    <em>JABAWS Client updated to version 2.2 in Jalview 2.10.2</em>
   </p>
   <p>
     <em>Option for adding JABAWS servers which fails validation was
       introduced from version 2.8.2 </em>
   </p>
+  <p>
+    <em>Support for accessing JABAWS servers was introduced in
+      Jalview 2.6.</em>
+  </p>
 </body>
 </html>
index 077ace6..e42a4e6 100755 (executable)
     <li>Lupas_21, Lupas_14, Lupas_28<br> <em>Coiled-coil
         predictions for the sequence. These are binary predictions for
         each location.</em></li>
-    <li>JNETSOL25,JNETSOL5,JNETSOL0<br> <em>Solvent
-        accessibility predictions - binary predictions of 25%, 5% or 0%
-        solvent accessibility.</em></li>
+    <li>Jnet Burial<br> <em>Prediction of Solvent
+        Accessibility. levels are
+        <ul>
+          <li>0 - Exposed</li>
+          <li>3 - 25% or more S.A. accessible</li>
+          <li>6 - 5% or more S.A. accessible</li>
+          <li>9 - Buried (<5% exposed)</li>
+        </ul></li>
     <li>JNetPRED<br> <em>The consensus prediction -
         helices are marked as red tubes, and sheets as dark green
         arrows.</em></li>
     <li>JNetHMM<br> <em>HMM profile based prediction -
         helices are marked as red tubes, and sheets as dark green
         arrows.</em></li>
-    <li>jpred<br> <em>Jpred prediction - helices are
-        marked as red tubes, and sheets as dark green arrows.</em></li>
     <li>JNETPSSM<br> <em>PSSM based prediction - helices
         are marked as red tubes, and sheets as dark green arrows.</em></li>
-    <li>JNETFREQ<br> <em>Amino Acid frequency based
-        prediction - helices are marked as red tubes, and sheets as dark
-        green arrows.</em></li>
     <li>JNETJURY<br> <em>A '*' in this annotation
         indicates that the JNETJURY was invoked to rationalise
         significantly different primary predictions.</em></li>
     href="../features/annotation.html#seqannots">Add reference
       annotation</a> Sequence ID popup menu option.
   </em>
-  <em>As of Jalview 2.6, the JPred service accessed accessed via the
-    'Secondary structure prediction' submenu should be considered a
+  <em>As of Jalview 2.6, the JPred service accessed accessed via
+    the 'Secondary structure prediction' submenu should be considered a
     legacy Jalview SOAP service, and will be replaced in the near future
     by a JPred4 Rest service.</em>
 
old mode 100755 (executable)
new mode 100644 (file)
index 3f827d9..006c50d
Binary files a/help/html/webServices/jnetprediction.gif and b/help/html/webServices/jnetprediction.gif differ
index 2fbbdbc..d627c66 100644 (file)
   </p>
   <p>
     <strong>Alignment programs supported by JABAWS</strong>. <br />Versions
-    shown are those bundled with JABAWS 2.01 - if you are using a
+    shown are those bundled with JABAWS 2.2 - if you are using a
     different server, check its home page to find out which versions are
     provided.
-  <ul>
-    <li><a href="http://www.clustal.org/">Clustal Omega and
-        Clustal W</a> (version 2.0.12)</li>
-    <li><a href="http://mafft.cbrc.jp/alignment/software/">Mafft</a>
-      (version 6.8.57b)</li>
-    <li><a href="http://www.drive5.com/muscle">Muscle</a> (version
-      3.8.31)</li>
-    <li><a
-      href="http://www.tcoffee.org/Projects_home_page/t_coffee_home_page.html">Tcoffee</a>
-      (version 8.99)</li>
-    <li><a href="http://probcons.stanford.edu/">Probcons</a>
-      (version 1.12)</li>
+    <ul>
+      <li><a href="http://www.clustal.org/omega">Clustal Omega</a> (version 1.2.4)</li>
+      <li><a href="http://www.clustal.org/clustal2">ClustalW</a> (version 2.1)</li>
+      <li><a href="http://align.bmr.kyushu-u.ac.jp/mafft/software/">Mafft</a> (version 7.310)</li>
+      <li><a href="http://www.drive5.com/muscle">Muscle</a> (version 3.8.31)</li>
+      <li><a href="http://www.tcoffee.org/Projects_home_page/t_coffee_home_page.html">T-coffee</a> (version 11.00.8cbe486)</li>
+      <li><a href="http://probcons.stanford.edu/">Probcons</a> (version 1.12)</li>
+      <li><a href="http://msaprobs.sourceforge.net/">MSAProbs</a> (version 0.9.7)</li>
+      <li><a href="http://sourceforge.net/projects/glprobs/">GLProbs</a> (version 0.9.7)</li>
   </ul>
   </p>
 
index 1c35bf3..a06b3a9 100644 (file)
@@ -48,7 +48,7 @@
       By Annotation</a> dialog box to colour sequences according to the
     results of predictors shown as annotation rows.
   </p>
-  <p>JABAWS 2.0 provides four disorder predictors which are
+  <p>JABAWS 2.2 provides four disorder predictors which are
     described below:</p>
   <ul>
     <li><a href="#disembl">DisEMBL</a></li>
   </p>
   <p>
     <strong><a name="iupred"></a><a
-      href="http://iupred.enzim.hu/Help.php">IUPred</a></strong><br />
+      href="http://iupred.enzim.hu/">IUPred</a></strong><br />
     IUPred employs an empirical model to estimate likely regions of
     disorder. There are three different prediction types offered, each
     using different parameters optimized for slightly different
index 088a539..56469e5 100644 (file)
 </head>
 <body>
   <p>
-  <p>
     <strong>Opening URLs from Jalview</strong><br> Both the applet
     and the desktop application are able to open URLs as 'popups' in
-    your web browser. <br> Double-clicking on the ID of a sequence
-    will open the first URL that can be generated from its sequence ID.
+    your web browser.</p>
+    <p> Double-clicking on the ID of a sequence
+    will open whichever URL is selected for 'popups' in the <strong>&quot;Links&quot;</strong> tab of the <a
+    href="../features/preferences.html#links">Jalview desktop
+    preferences</a>.
     This is by default the EMBL-EBI site, but you can easily configure your own <a
       href="#urllinks">sequence URL links</a>.
   </p>
   <p>
-    Other links for a sequence either derived from any other configured
+    Other links for a sequence, either derived from any other configured
     URL links, or imported from the sequence's annotation, are accessed
     by right clicking to open the sequence pop-up menu, and selecting
     from the <em>Links</em> submenu.
   </p>
   <p>
     <strong><a name="urllinks">Configuring URL Links</a></strong> <br>URL
-    links are defined in the &quot;Connections&quot; tab of the <a
-    href="../features/preferences.html">Jalview desktop
+    links are defined in the &quot;Links&quot; tab of the <a
+    href="../features/preferences.html#links">Jalview desktop
     preferences</a>, or specified as <a
     href="http://www.jalview.org/examples/appletParameters.html#parameters">applet
-    parameters</a>. <br> By default the item &quot;EMBL-EBI Search&quot; is added
-    to this link menu. This link will show a web page in your default
-    browser with the selected sequence id as part of the URL.<br>
-    In the preferences dialog box, click <strong>new</strong> to add a
-    new link, and <strong>edit</strong> to modify an existing link, or <strong>delete</strong>
-    to remove it.<br> You can name the link, this will be displayed
-    on a new menu item under the &quot;Link&quot; menu when you right
-    click on a sequence id. <br> The URL string must contain a
-    token that can be replaced with a sequence ID or DB accession ID. The simplest token is
-    &quot;$SEQUENCE_ID$&quot;, which will be replaced by the chosen
-    sequence id when you click on it. 
+    parameters</a>.</p>
+  <p>
+    <em>Default Link Settings</em><br /> The &quot;EMBL-EBI Search&quot;
+    link is the default link shown in the &quot;Link&quot; submenu, and
+    opened when double-clicking on a sequence ID. When clicked, this
+    link will show a web page in your default browser with the selected
+    sequence ID as part of the URL.
+  </p>
+  <p>
+    <em>Adding additional links</em><br /> You can configure your own
+    links via the Jalview <a href="../features/preferences.html#links"><strong>Preferences</strong></a>
+    dialog. Jalview also provides persistent URLs for many common
+    bioinformatics databases (since 2.10.2). These links are downloaded by Jalview from
+    the <em>identifiers.org</em> website, and the names and URLs are not
+    user editable.
+  </p>
+  <p>
+    <em>Creating your own URL link</em> <br/>URL links are specified as a
+    template containing special tokens that Jalview will replace with
+    the Sequence ID or Database Accession of the sequence when you
+    double click on its ID or open it's <strong>Link</strong> submenu.
+    Link URL templates must contain at least one token. 
   </p>
+    <em>eg.</em><pre> UniRef100 =
+    http://www.ebi.uniprot.org/uniprot-srv/uniRefView.do?proteinAc=$SEQUENCE_ID$&amp;library=uniref100<br/>
+    Swissprot = http://www.expasy.org/uniprot/$SEQUENCE_ID$ <br> </pre>
   <p>
-    eg.<br> UniRef100 =
-    http://www.ebi.uniprot.org/uniprot-srv/uniRefView.do?proteinAc=$SEQUENCE_ID$&amp;library=uniref100<br>
-    Swissprot = http://www.expasy.org/uniprot/$SEQUENCE_ID$ <br> <br>
     Links will also be made for any database cross references associated
-    with the sequence where the database name exactly matches a URL link
-    name. In this case, the $DB_ACCESSION$ string will be replaced with
+    with the sequence for any link templates whose name begins with the database name.
+    In this case, the $DB_ACCESSION$ string will be replaced with
     the accession string for the database cross-reference, rather than
     the sequence ID for the sequence (<em>since Jalview 2.10.1</em>).
+    <br /> <em>For example: to create a link for viewing MACiE records
+      from PDB Entries, create a new custom link entry with the name
+      "PDB in MACiE", and link URL template:
+      <pre>https://www.ebi.ac.uk/thornton-srv/databases/cgi-bin/MACiE/index.pl?query_pdb=1&amp;pdb=$DBACCESSION$</pre>
+      <br />The sequence ID popup menu for seuqences with a PDB entry
+      will now show 'PDB in MACiE|1xyz..' links in the <strong>links</strong>
+      submenu.
+    </em>
   </p>
   <p>
     <strong><a name="warning">Warning dialog about updating
   <p>
     <strong>Regular Expression Substitution</strong><br> A url may
     contain a string of the form $SEQUENCE_ID=/<em>regular
-    expression</em>/=$ or $DB_ACCESSION=/<em>regular expression</em>/=$. 
-    In this case, the regular expression will be
-    applied to the full sequence ID or DB accession ID string and the resulting match will
+      expression</em>/=$ or $DB_ACCESSION=/<em>regular expression</em>/=$. In
+    this case, the regular expression will be applied to the full
+    sequence ID or DB accession ID string and the resulting match will
     be inserted into the URL. Groups of parentheses can be used to
     specify which regions of the regular expression will be used to
     generate the URL:
+  
   <ul>
     <li>Each top level parenthesis will yield a URL containing the
       text matched within that parenthesis.</li>
     <li>Regions matching sub-parentheses within a top-level
       parenthesis will be concatenated to form the text inserted into
       the URL for the top-level parenthesis.</li>
-    <em>Please Note:
-      <ul>
-        <li>The regular expressions supported by Jalview are those
-          provided by the <a href="http://www.javaregex.com">Stevesoft
-            javaregex package</a>.
-        </li>
-        <li>Some characters must be escaped when specifying them as
-          a match within a regular expression.</li>
-      </ul> <br> Many Thanks to Bernd Brandt of the Free University of
-      Amsterdam for testing this new regular-expression expansion
-      feature!
-    </em>
-    <em>
   </ul>
-  </p>
-  </p>
+  <em>Please Note:</em>
+    <ul>
+      <li>The regular expressions supported by Jalview are those
+        provided by the <a href="http://www.javaregex.com">Stevesoft
+          javaregex package</a>.
+      </li>
+      <li>Some characters must be escaped when specifying them as a
+        match within a regular expression.</li>
+    </ul> <br> Many Thanks to Bernd Brandt of the Free University of
+    Amsterdam for testing the regular-expression expansion feature!
 </body>
 </html>
index d1d141d..4b82179 100755 (executable)
 </head>
 <body>
   <p>
-    <strong>What's new in Jalview 2.10.1 ?</strong>
+    <strong>What's new in Jalview 2.10.2 ?</strong>
   </p>
   <p>
-    Jalview 2.10.1 was released on 29th November 2016. Full details are
-    in the <a href="releases.html#Jalview.2.10.1">Jalview 2.10.1
-      Release Notes</a>, but the highlights are below. This is also the
-    first release to include contributions from Kira Mour&atilde;o, who
-    joined Jalview's core development team in October 2016.
+    Full details about Jalview 2.10.2 are in the <a
+      href="releases.html#Jalview.2.10.2"> Release Notes</a>, but the
+    highlights are below.
   </p>
   <ul>
-    <li><strong>More memory efficient</strong><br />We've slimmed
-      down the consensus analysis data structures used by Jalview so
-      even wider alignments can be worked with.</li>
-    <li><strong>Select highlighted region</strong><br />Press 'B'
-      or use the new menu option in the alignment window's Select menu
-      to mark columns containing highlighted regions generated from
-      structure selections, mouse-overs, or resulting from a Find
-      operation.</li>
-    <li><strong>New custom link mechanism for opening URLs
-        for database cross references.</strong><br /> If you have customised URL
-      links in your Jalview preferences, then you may already have seen
-      the <a href="#warning"> warning dialog (see below).</a></li>
-    <li><strong>New command line export option for BioJS
-        MSAviewer</strong><br />A number of small bugs with the HTML export
-      functions from the Jalview desktop were also fixed.</li>
-    <li><strong>Small but significant changes to the
-        physicochemical properties and consensus calculations</strong><br />Threonine
-      is no longer considered a non-hydrophobic residue in the protein
-      conservation calculation, and minor bugs addressed in PID and
-      consensus colouring.</li>
-    <li><strong>Correct display of disulphide bond
-        features</strong><br /> In linked structure views, Jalview would
-      highlight all residues between in addition to the two linked
-      cysteines. The 'select columns by feature' function in the feature
-      settings would also select all intermediate columns.
+    <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 services have been updated, so
+      their options and parameters have changed.</li>
+    <li>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><em>Showing and hiding regions</em>
+      <ul>
+        <li><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>
+      </ul></li>
   </ul>
-
   <p>
-    <strong><a name="warning">Warning dialog about updating
-        your configured URL links</a></strong><br /> In the desktop prior to Jalview
-    2.10.1, the only way to configure custom links for a particular
-    database cross-reference for a sequence was to give it a name that <em>exactly</em>
-    matched the database source, and a regular expression for filtering
-    out any spurious matches generated when the custom linked was tested
-    against the Sequence's ID string. Since the introduction of the
-    $DB_ACCESSION$ token, however, $SEQUENCE_ID$ will not be used for
-    database cross-reference accession strings, and if you have custom
-    links configured, Jalview will raise a warning message so let you
-    know that you may need to update your links to use $DB_ACCESSION$.
+    <strong><a name="experimental">Experimental Features</a></strong>
   </p>
+  <p>
+    This release of Jalview includes a new option in the Jalview Desktop
+    that allows you to try out features that are still in development.
+    To access the features described below, please first enable the <strong>Tools&#8594;Enable
+      Experimental Features</strong> option, and then restart Jalview.
+  </p>
+  <ul>
+    <li><em>Annotation transfer between Chimera and Jalview</em><br />Two
+      <a href="features/chimera.html#experimental">new entries in
+        the Chimera viewer's Chimera menu</a> allow positional annotation to
+      be exchanged between Chimera and Jalview.</li>
+  </ul>
 </body>
 </html>
diff --git a/lib/jabaws-min-client-2.2.0.jar b/lib/jabaws-min-client-2.2.0.jar
new file mode 100644 (file)
index 0000000..bf1e8b1
Binary files /dev/null and b/lib/jabaws-min-client-2.2.0.jar differ
index f1e371d..f1ac099 100644 (file)
@@ -3,6 +3,9 @@ action.reset_services = Reset Services
 action.merge_results = Merge Results
 action.load_scheme = Load scheme
 action.save_scheme = Save scheme
+label.scheme_changed = Changes to scheme ''{0}'' have not been saved.<br><br>Save changes, or continue without saving to make a new colour scheme.
+label.save_changes = Save Changes
+label.dont_save_changes = Don't Save
 action.save_image = Save Image
 action.paste = Paste
 action.show_html_source = Show HTML Source
@@ -61,7 +64,6 @@ action.set_as_reference = Set as Reference
 action.remove = Remove
 action.remove_redundancy = Remove Redundancy...
 action.pairwise_alignment = Pairwise Alignment
-action.by_rna_helixes = By RNA Helices
 action.user_defined = User Defined...
 action.by_conservation = By Conservation
 action.wrap = Wrap
@@ -69,7 +71,6 @@ action.show_gaps = Show Gaps
 action.show_hidden_markers = Show Hidden Markers
 action.find = Find
 action.undefine_groups = Undefine Groups
-action.create_groups = Create Groups
 action.make_groups_selection = Make Groups For Selection
 action.copy = Copy
 action.cut = Cut
@@ -79,7 +80,8 @@ action.scale_left = Scale Left
 action.scale_right = Scale Right
 action.by_tree_order = By Tree Order
 action.sort = Sort
-action.calculate_tree = Calculate Tree
+action.calculate_tree = Calculate Tree...
+action.calculate_tree_pca = Calculate Tree or PCA...
 action.help = Help
 action.by_annotation = By Annotation...
 action.invert_sequence_selection = Invert Sequence Selection
@@ -139,7 +141,8 @@ action.view_flanking_regions = Show flanking regions
 label.view_flanking_regions = Show sequence data either side of the subsequences involved in this alignment
 label.structures_manager = Structures Manager
 label.nickname = Nickname:
-label.url = URL:
+label.url = URL
+label.url\: = URL:
 label.input_file_url = Enter URL or Input File
 label.select_feature = Select feature
 label.name = Name
@@ -168,6 +171,7 @@ label.redo_command = Redo {0}
 label.principal_component_analysis = Principal Component Analysis
 label.average_distance_identity = Average Distance Using % Identity
 label.neighbour_joining_identity = Neighbour Joining Using % Identity
+label.choose_calculation = Choose Calculation
 label.treecalc_title = {0} Using {1}
 label.tree_calc_av = Average Distance
 label.tree_calc_nj = Neighbour Joining
@@ -175,31 +179,37 @@ label.select_score_model = Select score model
 label.score_model_pid = % Identity
 label.score_model_blosum62 = BLOSUM62
 label.score_model_pam250 = PAM 250
+label.score_model_smithwatermanscore = Score between two sequences aligned with Smith-Waterman with default Peptide/Nucleotide matrix
+label.score_model_sequencefeaturesimilarity = Distance measure of average number of features not shared at sequence positions 
 label.score_model_conservation = Physicochemical property conservation
 label.score_model_enhconservation = Physicochemical property conservation
 label.status_bar = Status bar
 label.out_to_textbox = Output to Textbox
-label.clustalx = Clustalx
+label.occupancy = Occupancy
+# delete Clustal - use FileFormat name instead
 label.clustal = Clustal
-label.zappo = Zappo
-label.taylor = Taylor
+# label.colourScheme_<schemeName> as in JalviewColourScheme
+label.colourScheme_clustal = Clustalx
+label.colourScheme_blosum62 = BLOSUM62 Score
+label.colourScheme_%_identity = Percentage Identity
+label.colourScheme_zappo = Zappo
+label.colourScheme_taylor = Taylor
+label.colourScheme_hydrophobic = Hydrophobicity
+label.colourScheme_helix_propensity = Helix Propensity
+label.colourScheme_strand_propensity = Strand Propensity
+label.colourScheme_turn_propensity = Turn Propensity
+label.colourScheme_buried_index = Buried Index
+label.colourScheme_purine/pyrimidine = Purine/Pyrimidine
+label.colourScheme_nucleotide = Nucleotide
+label.colourScheme_t-coffee_scores = T-Coffee Scores
+label.colourScheme_rna_helices = By RNA Helices
 label.blc = BLC
 label.fasta = Fasta
 label.msf = MSF
 label.pfam = PFAM
 label.pileup = Pileup
 label.pir = PIR
-label.hydrophobicity = Hydrophobicity
-label.helix_propensity = Helix Propensity
-label.strand_propensity = Strand Propensity
-label.turn_propensity = Turn Propensity
-label.buried_index = Buried Index
-label.purine_pyrimidine = Purine/Pyrimidine
-label.percentage_identity = Percentage Identity
-label.blosum62 = BLOSUM62
-label.blosum62_score = BLOSUM62 Score
-label.tcoffee_scores = T-Coffee Scores
-label.average_distance_bloslum62 = Average Distance Using BLOSUM62
+label.average_distance_blosum62 = Average Distance Using BLOSUM62
 label.neighbour_blosum62 = Neighbour Joining Using BLOSUM62
 label.show_annotations = Show annotations
 label.hide_annotations = Hide annotations
@@ -211,7 +221,7 @@ label.hide_all = Hide all
 label.add_reference_annotations = Add reference annotations
 label.find_tip = Search alignment, selection or sequence ids for a subsequence (ignoring gaps).<br>Accepts regular expressions - search Help for 'regex' for details.
 label.colour_text = Colour Text
-label.show_non_conversed = Show nonconserved
+label.show_non_conserved = Show nonconserved
 label.overview_window = Overview Window
 label.none = None
 label.above_identity_threshold = Above Identity Threshold
@@ -323,10 +333,11 @@ label.size = Size:
 label.style = Style:
 label.calculating = Calculating....
 label.modify_conservation_visibility = Modify conservation visibility
-label.colour_residues_above_occurence = Colour residues above % occurence
+label.colour_residues_above_occurrence = Colour residues above % occurrence
 label.set_this_label_text = set this label text
 label.sequences_from = Sequences from {0}
 label.successfully_loaded_file  = Successfully loaded file {0}
+label.successfully_loaded_matrix  = Successfully loaded score matrix {0}
 label.successfully_saved_to_file_in_format = Successfully saved to file: {0} in {1} format.
 label.copied_sequences_to_clipboard = Copied {0} sequences to clipboard.
 label.check_file_matches_sequence_ids_alignment = Check that the file matches sequence IDs in the alignment.
@@ -370,14 +381,11 @@ label.remove_from_default_list = Remove from default list?
 label.remove_user_defined_colour = Remove user defined colour
 label.you_must_select_least_two_sequences = You must select at least 2 sequences.
 label.invalid_selection = Invalid Selection
-label.principal_component_analysis_must_take_least_four_input_sequences = Principal component analysis must take\nat least 4 input sequences.
 label.sequence_selection_insufficient = Sequence selection insufficient
-label.you_need_more_two_sequences_selected_build_tree = You need to have more than two sequences selected to build a tree!
+label.you_need_at_least_n_sequences = You need to select at least {0} sequences
 label.not_enough_sequences = Not enough sequences
 label.selected_region_to_tree_may_only_contain_residues_or_gaps =  The selected region to create a tree may\nonly contain residues or gaps.\nTry using the Pad function in the edit menu,\nor one of the multiple sequence alignment web services.
 label.sequences_selection_not_aligned = Sequences in selection are not aligned
-label.sequences_must_be_aligned_before_creating_tree = The sequences must be aligned before creating a tree.\nTry using the Pad function in the edit menu,\n or one of the multiple sequence alignment web services.
-label.sequences_not_aligned = Sequences not aligned
 label.problem_reading_tree_file =  Problem reading tree file
 label.possible_problem_with_tree_file = Possible problem with tree file
 label.select_at_least_three_bases_in_at_least_one_sequence_to_cDNA_translation = Please select at least three bases in at least one sequence in order to perform a cDNA translation.
@@ -411,7 +419,6 @@ label.couldnt_import_as_vamsas_session = Couldn't import {0} as a new vamsas ses
 label.vamsas_document_import_failed = Vamsas Document Import Failed
 label.couldnt_locate = Couldn't locate {0}
 label.url_not_found = URL not found
-label.no_link_selected = No link selected
 label.new_sequence_url_link = New sequence URL link
 label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
 label.wrapped_view_no_edit = Wrapped view - no edit
@@ -513,7 +520,7 @@ label.retrieve_parse_sequence_database_records_alignment_or_selected_sequences =
 label.standard_databases = Standard Databases
 label.fetch_embl_uniprot = Fetch from EMBL/EMBLCDS or Uniprot/PDB and any selected DAS sources
 label.reset_min_max_colours_to_defaults = Reset min and max colours to defaults from user preferences.
-label.align_structures_using_linked_alignment_views = Align structures using {0} linked alignment views
+label.align_structures_using_linked_alignment_views = Superpose structures using {0} selected alignment view(s)
 label.connect_to_session = Connect to session {0}
 label.threshold_feature_display_by_score = Threshold the feature display by score.
 label.threshold_feature_no_threshold = No Threshold
@@ -618,6 +625,8 @@ label.web_services = Web Services
 label.right_click_to_edit_currently_selected_parameter = Right click to edit currently selected parameter.
 label.let_jmol_manage_structure_colours = Let Jmol manage structure colours
 label.let_chimera_manage_structure_colours = Let Chimera manage structure colours
+label.fetch_chimera_attributes = Fetch Chimera attributes
+label.fetch_chimera_attributes_tip = Copy Chimera attribute to Jalview feature
 label.marks_leaves_tree_not_associated_with_sequence = Marks leaves of tree not associated with a sequence
 label.index_web_services_menu_by_host_site = Index web services in menu by the host site
 label.option_want_informed_web_service_URL_cannot_be_accessed_jalview_when_starts_up = Check this option if you want to be informed<br>when a web service URL cannot be accessed by Jalview<br>when it starts up
@@ -665,13 +674,10 @@ 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.text_colour = Text Colour
-action.set_text_colour = Text Colour...
+label.text_colour = Text Colour...
 label.structure = Structure
 label.show_pdbstruct_dialog = 3D Structure Data...
 label.view_rna_structure = VARNA 2D Structure
-label.clustalx_colours = Clustalx colours
-label.above_identity_percentage = Above % Identity
 label.create_sequence_details_report_annotation_for = Annotation for {0}
 label.sequence_details_for = Sequence Details for {0}
 label.sequence_name = Sequence Name
@@ -708,22 +714,25 @@ label.set_as_default = Set as Default
 label.show_labels = Show labels
 action.background_colour = Background Colour...
 label.associate_nodes_with = Associate Nodes With
-label.jalview_pca_calculation = Jalview PCA Calculation
 label.link_name = Link Name
 label.pdb_file = PDB file
 label.colour_with_jmol = Colour with Jmol
 label.colour_with_chimera = Colour with Chimera
-label.align_structures = Align Structures
+label.superpose_structures = Superpose Structures
+error.superposition_failed = Superposition failed: {0}
+label.insufficient_residues = Not enough aligned residues ({0}) to perform superposition
 label.jmol = Jmol
 label.chimera = Chimera
 label.create_chimera_attributes = Write Jalview features
 label.create_chimera_attributes_tip = Set Chimera residue attributes for visible features
+label.attributes_set = {0} attribute values set on Chimera
 label.sort_alignment_by_tree = Sort Alignment By Tree
 label.mark_unlinked_leaves = Mark Unlinked Leaves
 label.associate_leaves_with = Associate Leaves With
 label.save_colour_scheme_with_unique_name_added_to_colour_menu = Save your colour scheme with a unique name and it will be added to the Colour menu
 label.case_sensitive = Case Sensitive
-label.lower_case_colour = Lower Case Colour
+label.lower_case_colour = Colour All Lower Case
+label.lower_case_tip = Chosen colour applies to all lower case symbols
 label.index_by_host = Index by Host
 label.index_by_type = Index by Type
 label.enable_jabaws_services = Enable JABAWS Services
@@ -772,7 +781,7 @@ 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.select_backgroud_colour = Select Background Colour
+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 ";"
 label.separate_multiple_query_values = Enter one or more {0}s separated by a semi-colon ";"
@@ -847,7 +856,6 @@ label.couldnt_save_project = Couldn't save project
 label.error_whilst_saving_current_state_to = Error whilst saving current state to {0}
 label.error_whilst_loading_project_from = Error whilst loading project from {0}
 label.couldnt_load_project = Couldn't load project
-label.pca_sequences_not_aligned = The sequences must be aligned before calculating PCA.\nTry using the Pad function in the edit menu,\nor one of the multiple sequence alignment web services.
 label.invalid_name_preset_exists = Invalid name - preset already exists.
 label.invalid_name = Invalid name
 label.set_proxy_settings = Please set up your proxy settings in the 'Connections' tab of the Preferences window
@@ -891,6 +899,7 @@ label.choose_filename_for_param_file = Choose a filename for this parameter file
 label.save_as_html = Save as HTML
 label.recently_opened = Recently Opened
 label.blasting_for_unidentified_sequence_jobs_running = BLASTing for unidentified sequences - {0}  jobs running.
+label.tree = Tree
 label.tree_from = Tree from {0}
 label.webservice_job_title = {0} using {1}
 label.select_visible_region_of = selected {0} region of {1}
@@ -901,10 +910,10 @@ label.webservice_job_title_on = {0} using {1} on {2}
 label.updating_vamsas_session = Updating vamsas session
 label.loading_file = Loading File: {0}
 label.edit_params = Edit {0}
+label.as_percentage = As Percentage
 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.
@@ -962,7 +971,6 @@ error.implementation_error_maplist_is_null = Implementation error. MapList is nu
 error.implementation_error_cannot_have_null_alignment = Implementation error: Cannot have null alignment property key
 error.implementation_error_null_fileparse = Implementation error. Null FileParse in copy constructor
 error.implementation_error_cannot_map_alignment_sequences = IMPLEMENTATION ERROR: Cannot map an alignment of sequences from different datasets into a single alignment in the vamsas document.
-error.implementation_error_cannot_duplicate_colour_scheme = Serious implementation error: cannot duplicate colourscheme {0}
 error.implementation_error_structure_selection_manager_null = Implementation error. Structure selection manager's context is 'null'
 exception.ssm_context_is_null = SSM context is null
 error.idstring_seqstrings_only_one_per_sequence = idstrings and seqstrings contain one string each per sequence
@@ -1027,7 +1035,6 @@ exception.replace_null_regex_pointer = Replacer has null Regex pointer
 exception.bad_pattern_to_regex_perl_code = bad pattern to Regex.perlCode: {0}
 exception.no_stub_implementation_for_interface = There is no stub implementation for the interface: {0}
 exception.cannot_set_endpoint_address_unknown_port = Cannot set Endpoint Address for Unknown Port {0}
-exception.querying_matching_opening_parenthesis_for_non_closing_parenthesis = Querying matching opening parenthesis for non-closing parenthesis character {0}
 exception.mismatched_unseen_closing_char = Mismatched (unseen) closing character {0}
 exception.mismatched_closing_char = Mismatched closing character {0}
 exception.mismatched_opening_char = Mismatched opening character {0} at {1}
@@ -1038,7 +1045,6 @@ exception.couldnt_parse_responde_from_annotated3d_server = Couldn't parse respon
 exception.application_test_npe = Application test: throwing an NullPointerException It should arrive at the console
 exception.overwriting_vamsas_id_binding = Overwriting vamsas id binding
 exception.overwriting_jalview_id_binding = Overwriting jalview id binding
-error.implementation_error_unknown_file_format_string = Implementation error: Unknown file format string
 exception.failed_to_resolve_gzip_stream = Failed to resolve GZIP stream
 exception.problem_opening_file_also_tried = Problem opening {0} (also tried {1}) : {2}
 exception.problem_opening_file = Problem opening {0} : {1}
@@ -1218,6 +1224,7 @@ label.configure_displayed_columns = Customise Displayed Options
 label.start_jalview = Start Jalview
 label.biojs_html_export = BioJS
 label.scale_as_cdna = Scale protein residues to codons
+label.font_as_cdna = Use same font for cDNA and peptide
 label.scale_protein_to_cdna = Scale Protein to cDNA
 label.scale_protein_to_cdna_tip = Make protein residues same width as codons in split frame views
 info.select_annotation_row = Select Annotation Row
@@ -1234,7 +1241,6 @@ action.export_hidden_columns = Export Hidden Columns
 action.export_hidden_sequences = Export Hidden Sequences
 action.export_features = Export Features
 label.export_settings = Export Settings
-label.save_as_biojs_html = Save as BioJs HTML
 label.pdb_web-service_error = PDB Web-service Error
 label.structure_chooser_manual_association = Structure Chooser - Manual association
 label.structure_chooser_filter_time = Structure Chooser - Filter time ({0})
@@ -1276,4 +1282,33 @@ label.SEQUENCE_ID_no_longer_used = $SEQUENCE_ID$ is no longer used for DB access
 label.SEQUENCE_ID_for_DB_ACCESSION1 = Please review your URL links in the 'Connections' tab of the Preferences window:
 label.SEQUENCE_ID_for_DB_ACCESSION2 = URL links using '$SEQUENCE_ID$' for DB accessions now use '$DB_ACCESSION$'.
 label.do_not_display_again = Do not display this message again
+exception.url_cannot_have_miriam_id = {0} is a MIRIAM id and cannot be used as a custom url name
+exception.url_cannot_have_duplicate_id = {0} cannot be used as a label for more than one line
+label.filter = Filter text:
+action.customfilter = Custom only
+action.showall = Show All
+label.insert = Insert:
+action.seq_id = $SEQUENCE_ID$
+action.db_acc = $DB_ACCESSION$
+label.primary = Double Click
+label.inmenu = In Menu
+label.id = ID
+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
+action.clear_cached_items = Clear Cached Items
+label.togglehidden = Show hidden regions
+label.quality_descr = Alignment Quality based on Blosum62 scores
+label.conservation_descr = Conservation of total alignment less than {0}% gaps
+label.consensus_descr = PID
+label.complement_consensus_descr = PID for cDNA
+label.strucconsensus_descr = PID for base pairs
+label.occupancy_descr = Number of aligned positions 
+label.show_experimental = Enable experimental features
+label.show_experimental_tip = Enable any new and currently 'experimental' features (see Latest Release Notes for details)
+label.warning_hidden = Warning: {0} {1} is currently hidden
index 8cdcd52..7808480 100644 (file)
@@ -3,6 +3,9 @@ action.reset_services = Reiniciar servicios
 action.merge_results = Unificar resultados
 action.load_scheme = Cargar esquema
 action.save_scheme = Guardar esquema
+label.scheme_changed = Cambios en el esquema ''{0}'' no se han guardado.<br><br>Guardar cambios, o continuar sin guardar para hacer un nuevo esquema.
+label.save_changes = Guardar cambios
+label.dont_save_changes = No guardar
 action.save_image = Guardar imagen
 action.paste = Pegar
 action.show_html_source = Mostrar código HTML
@@ -59,7 +62,6 @@ action.by_group = Por grupo
 action.remove = Eliminar
 action.remove_redundancy = Eliminar redundancia...
 action.pairwise_alignment = Alineamiento de pares...
-action.by_rna_helixes = Por hélices de RNA
 action.user_defined = Definido por el usuario...
 action.by_conservation = Por conservación
 action.wrap = Envolver
@@ -67,7 +69,6 @@ action.show_gaps = Mostrar huecos
 action.show_hidden_markers = Mostrar marcadores ocultos
 action.find = Buscar
 action.undefine_groups = Grupos sin definir
-action.create_groups = Crear grupos
 action.make_groups_selection = Hacer grupos para seleccionar
 action.copy = Copiar
 action.cut = Cortar
@@ -77,7 +78,8 @@ action.scale_left = Escala izquierda
 action.scale_right = Escala derecha
 action.by_tree_order = Por orden del árbol
 action.sort = Ordenar
-action.calculate_tree = Calcular árbol
+action.calculate_tree = Calcular árbol...
+action.calculate_tree_pca = Calcular árbol o ACP...
 action.help = Ayuda
 action.by_annotation = Por anotación...
 action.invert_sequence_selection = Invertir selección de secuencias
@@ -136,7 +138,8 @@ action.view_flanking_regions = Mostrar flancos
 label.view_flanking_regions = Mostrar los datos de la secuencia a ambos lados de las subsecuencias implicadas en este alineamiento
 label.structures_manager = Administrar estructuras
 label.nickname = Sobrenombre:
-label.url = URL: 
+label.url\: = URL:
+label.url = URL 
 label.input_file_url = Introducir URL en el fichero de entrada
 label.select_feature = Seleccionar característica
 label.name = Nombre
@@ -165,6 +168,7 @@ label.redo_command = Rehacer {0}
 label.principal_component_analysis = Análisis del Componente Principal
 label.average_distance_identity = Distancia Media Usando % de Identidad
 label.neighbour_joining_identity = Unir vecinos utilizando % de Identidad
+label.choose_calculation = Elegir el cálculo
 label.treecalc_title = {0} utilizando {1}
 label.tree_calc_av = Distancia media
 label.tree_calc_nj = Unir vecinos
@@ -172,35 +176,40 @@ label.select_score_model = Selecciones modelo de puntuaci
 label.score_model_pid = % Identidad
 label.score_model_blosum62 = BLOSUM62
 label.score_model_pam250 = PAM 250
+label.score_model_smithwatermanscore = Puntuación entre secuencias alineadas por Smith-Waterman con matriz por defecto proteica / nucleotídica
+label.score_model_sequencefeaturesimilarity = Medida de distancia por cuenta promedia de características no compartidas en posiciones de secuencia
 label.score_model_conservation = Conservación de las propiedades físico-químicas
 label.score_model_enhconservation = Conservación de las propiedades físico-químicas
 label.status_bar = Barra de estado
 label.out_to_textbox = Generar cuadro de texto
-label.clustalx = Clustalx
+label.occupancy = Ocupación
 label.clustal = Clustal
-label.zappo = Zappo
-label.taylor = Taylor
+# label.colourScheme_<schemeName> as in JalviewColourScheme
+label.colourScheme_clustal = Clustalx
+label.colourScheme_blosum62 = Puntuación del BLOSUM62
+label.colourScheme_%_identity = Porcentaje de identidad
+label.colourScheme_zappo = Zappo
+label.colourScheme_taylor = Taylor
+label.colourScheme_hydrophobic = Hidrofobicidad
+label.colourScheme_helix_propensity = Tendencia de la hélice
+label.colourScheme_strand_propensity = Tendencia de la hebra
+label.colourScheme_turn_propensity = Tendencia de giro
+label.colourScheme_buried_index = Índice de encubrimiento
+label.colourScheme_purine/pyrimidine = Purina/Pirimidina
+label.colourScheme_nucleotide = Nucleótido
+label.colourScheme_t-coffee_scores = Puntuación del T-Coffee
+label.colourScheme_rna_helices = Por hélices de RNA
 label.blc = BLC
 label.fasta = Fasta
 label.msf = MSF
 label.pfam = PFAM
 label.pileup = Pileup
 label.pir = PIR
-label.hydrophobicity = Hidrofobicidad
-label.helix_propensity = Tendencia de la hélice
-label.strand_propensity = Tendencia de la hebra
-label.turn_propensity = Tendencia de giro
-label.buried_index = Índice de encubrimiento
-label.purine_pyrimidine = Purina/Pirimidina
-label.percentage_identity = Porcentaje de identidad
-label.blosum62 = BLOSUM62
-label.blosum62_score = Puntuación del BLOSUM62 
-label.tcoffee_scores = Puntuación del T-Coffee
-label.average_distance_bloslum62 = Distancia Media Usando BLOSUM62
+label.average_distance_blosum62 = Distancia Media Usando BLOSUM62
 label.neighbour_blosum62 = Neighbour Joining usando BLOSUM62
 label.show_annotations = Mostrar anotaciones
 label.colour_text = Color del texto
-label.show_non_conversed = Mostrar no conservadas
+label.show_non_conserved = Mostrar no conservadas
 label.overview_window = Ventana resumen
 label.none = Ninguno
 label.above_identity_threshold = Por encima del umbral de identidad
@@ -293,10 +302,11 @@ label.size = Talla:
 label.style = Estilo:
 label.calculating = Calculando....
 label.modify_conservation_visibility = Modificar la visibilidad de conservación
-label.colour_residues_above_occurence = Residuos de color por encima del % de aparición 
+label.colour_residues_above_occurrence = Residuos de color por encima del % de aparición 
 label.set_this_label_text = fijar como etiqueta 
 label.sequences_from = Secuencias de {0}
 label.successfully_loaded_file  = Fichero cargado exitosamente {0}
+label.successfully_loaded_matrix  = Matriz cargada exitosamente {0}
 label.successfully_saved_to_file_in_format = Guardado exitosamente en el fichero: {0} en formato {1}.
 label.copied_sequences_to_clipboard = Copiadas {0} secuencias en el portapapeles.
 label.check_file_matches_sequence_ids_alignment = Comprobar que el fichero coincide con el ID de la secuencia en el alineamiento.
@@ -339,14 +349,11 @@ label.remove_from_default_list = eliminar de la lista de defectuosos?
 label.remove_user_defined_colour = Eliminar el color definido por el usuario
 label.you_must_select_least_two_sequences = Debes seleccionar al menos 2 secuencias.
 label.invalid_selection = Selección inválida
-label.principal_component_analysis_must_take_least_four_input_sequences = El an\u00E1lisis de la componente principal debe tomar\nal menos 4 secuencias de entrada.
 label.sequence_selection_insufficient = Selección de secuencias insuficiente
-label.you_need_more_two_sequences_selected_build_tree = necesitas seleccionar más de dos secuencias para construir un árbol!
+label.you_need_at_least_n_sequences = Necesitas seleccionar al menos {0} secuencias
 label.not_enough_sequences = No suficientes secuencias
 label.selected_region_to_tree_may_only_contain_residues_or_gaps = La regi\u00F3n seleccionada para construir un \u00E1rbol puede\ncontener s\u00F3lo residuos o espacios.\nPrueba usando la funci\u00F3n Pad en el men\u00FA de edici\u00F3n,\n o uno de los m\u00FAltiples servicios web de alineamiento de secuencias.
 label.sequences_selection_not_aligned = Las secuencias seleccionadas no están alineadas
-label.sequences_must_be_aligned_before_creating_tree = Las secuencias deben estar alineadas antes de crear el \u00E1rbol.\nPrueba usando la funci\u00F3n Pad en el men\u00FA de editar,\n o uno de los m\u00FAltiples servicios web de alineamiento de secuencias.
-label.sequences_not_aligned = Secuencias no alineadas
 label.problem_reading_tree_file =  Problema al leer el fichero del árbol
 label.possible_problem_with_tree_file = Posible problema con el fichero del árbol
 label.select_at_least_three_bases_in_at_least_one_sequence_to_cDNA_translation = Por favor seleccionar al menos tres bases de al menos una secuencia para poder realizar la traducción de cDNA.
@@ -379,7 +386,6 @@ label.couldnt_import_as_vamsas_session = No se pudo importar {0} como una nueva
 label.vamsas_document_import_failed =  Fallo en la importación del documento Vamsas
 label.couldnt_locate = No se pudo localizar {0}
 label.url_not_found = URL no encontrada
-label.no_link_selected = Enlace no seleccionado
 label.new_sequence_url_link = Enlace a una nueva secuencia URL
 label.cannot_edit_annotations_in_wrapped_view = No se pueden editar anotaciones en vista envolvente
 label.wrapped_view_no_edit = Vista envolvente - no editar
@@ -410,7 +416,7 @@ label.colour_by_annotation = Color por anotaci
 label.selection_output_command = Seleccionar salida - {0}
 label.annotation_for_displayid = <p><h2>Anotación para {0} </h2></p><p>
 label.pdb_sequence_mapping = PDB - Mapeado de secuencia
-label.pca_details = detalles de la PCA
+label.pca_details = detalles de la ACP
 label.redundancy_threshold_selection = Selección del umbral de redundancia
 label.user_defined_colours = Colores definidos del usuario
 label.jalviewLite_release = JalviewLite - versión {0}
@@ -431,7 +437,7 @@ label.label = Etiqueta
 label.no_features_added_to_this_alignment = No hay funciones asociadas a este alineamiento!!
 label.features_can_be_added_from_searches_1 = (Las funciones pueden ser añadidas de búsquedas o
 label.features_can_be_added_from_searches_2 = de ficheros de funciones Jalview / GFF)
-label.calculating_pca= Calculando PCA
+label.calculating_pca= Calculando ACP
 label.jalview_cannot_open_file = Jalview no puede abrir el fichero
 label.jalview_applet = Aplicación Jalview  
 label.loading_data = Cargando datos
@@ -475,7 +481,7 @@ label.retrieve_parse_sequence_database_records_alignment_or_selected_sequences =
 label.standard_databases = Bases de datos estándar
 label.fetch_embl_uniprot = Recuperar de EMBL/EMBLCDS o Uniprot/PDB y de cualquier fuente DAS seleccionada
 label.reset_min_max_colours_to_defaults = Reiniciar los colores min y max colours a los valores por defecto establecidos en las preferencias de usuario
-label.align_structures_using_linked_alignment_views = Alinear las estructuras utlizando las {0} vistas de alineamiento enlazadas
+label.align_structures_using_linked_alignment_views = Alinear las estructuras utilizando las {0} vista(s) de alineamiento enlazada(s)
 label.connect_to_session = Conectar a la sesión {0}
 label.threshold_feature_display_by_score = Filtrar la característica mostrada por puntuación.
 label.threshold_feature_no_threshold = Sin umbral
@@ -620,10 +626,8 @@ 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.text_colour = Color del texto
+label.text_colour = Color de texto...
 label.structure = Estructura
-label.clustalx_colours = Colores de Clustalx
-label.above_identity_percentage = Sobre % identidad
 label.create_sequence_details_report_annotation_for = Anotación para {0}
 label.sequence_details_for = Detalles de la secuencia para {0}
 label.sequence_name = Nombre de la secuencia
@@ -655,18 +659,17 @@ label.add_local_source = A
 label.set_as_default = Establecer por defecto
 label.show_labels = Mostrar etiquetas
 label.associate_nodes_with = Asociar nodos con
-label.jalview_pca_calculation = Cálculo del PCA por Jalview
 label.link_name = Nombre del enalce
 label.pdb_file = Fichero PDB
 label.colour_with_jmol = Colorear con Jmol
-label.align_structures = Alinear estructuras
 label.jmol = Jmol
 label.sort_alignment_by_tree = Ordenar alineamiento por árbol
 label.mark_unlinked_leaves = Marcar las hojas como no enlazadas
 label.associate_leaves_with = Asociar hojas con
 label.save_colour_scheme_with_unique_name_added_to_colour_menu = Guarde el esquema cromáticos con un nombre único y se añadirá al menú de colores
 label.case_sensitive = Sensible a mayúsculas
-label.lower_case_colour = Color para las minúsculas
+label.lower_case_colour = Colorear todas las minúsculas
+label.lower_case_tip = El color elegido se aplicará a todas las minúsculas
 label.index_by_host = Indizar por host
 label.index_by_type = Indizar por tipo
 label.enable_jabaws_services = Habilitar servicios JABAWS
@@ -707,7 +710,7 @@ 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.select_backgroud_colour = Seleccionar color de fondo
+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 ";"
 label.replace_commas_semicolons = Cambiar comas por puntos y comas
@@ -778,7 +781,6 @@ label.couldnt_save_project = No es posible guardar el proyecto
 label.error_whilst_saving_current_state_to = Error mientras se guardaba el estado a {0}
 label.error_whilst_loading_project_from = Error cargando el proyecto desde  {0}
 label.couldnt_load_project = No es posible cargar el proyecto
-label.pca_sequences_not_aligned = Las secuencias deben estar alineadas antes de calcular el PCA.\nPruebe a utilizar la funci\u00F3n de rellenar huecos en el men\u00FA Editar,\no cualquiera de los servicios web de alineamiento m\u00FAltiple.
 label.invalid_name_preset_exists = Nombre no válido - esta preconfiguración ya existe.
 label.invalid_name = Nombre no válido
 label.set_proxy_settings = Por favor, configure su proxy en la pestaña 'Conexiones' de la ventana de Preferencia
@@ -822,6 +824,7 @@ label.choose_filename_for_param_file = Escoja un nombre de fichero para este fic
 label.save_as_html = Guardar como HTML
 label.recently_opened = Abiertos recientemente
 label.blasting_for_unidentified_sequence_jobs_running = Ejecutando BLAST de las secuencias no indentificadas - {0}  trabajos en marcha.
+label.tree = Árbol
 label.tree_from = Árbol de {0}
 label.webservice_job_title = {0} usando {1}
 label.select_visible_region_of = seleccionada {0} región de {1}
@@ -832,10 +835,10 @@ label.webservice_job_title_on = {0} usando {1} de {2}
 label.updating_vamsas_session = Actualizando sesión VAMSAS
 label.loading_file = Cargando fichero: {0}
 label.edit_params = Editar {0}
+label.as_percentage = Como Porcentaje
 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.
@@ -893,7 +896,6 @@ error.implementation_error_maplist_is_null = Error de implementaci
 error.implementation_error_cannot_have_null_alignment = Error de implementación: no es posible tener una clave nula en el alineamiento
 error.implementation_error_null_fileparse = Error de implementación. FileParse nulo en el construictor de copia
 error.implementation_error_cannot_map_alignment_sequences = Error de implementación: no es posible maper un alineamiento de secuencias desde distintos conjuntos de datos en un único alineamiento en el documento VAMSAS.
-error.implementation_error_cannot_duplicate_colour_scheme = Error grave de implementación: no es posible duplicar el esquema cromático {0}
 error.implementation_error_structure_selection_manager_null = Error de implementación. El contexto structure selection manager's es nulo
 exception.ssm_context_is_null = El contexto SSM es nulo
 error.idstring_seqstrings_only_one_per_sequence = idstrings y seqstrings contienen una cadena por cada secuencia
@@ -939,8 +941,8 @@ label.submission_params = Env
 label.empty_alignment_job = Trabajo de alineamiento vacío
 label.add_new_sbrs_service = Añadir un nuevo SBRS
 label.edit_sbrs_entry = Editar entrada SBRS
-label.pca_recalculating = Recalculando PCA
-label.pca_calculating = Calculando PCA
+label.pca_recalculating = Recalculando ACP
+label.pca_calculating = Calculando ACP
 label.select_foreground_colour = Escoger color del primer plano
 label.select_colour_for_text = Seleccione el color del texto
 label.adjunst_foreground_text_colour_threshold = Ajustar el umbral del color del texto en primer plano
@@ -958,7 +960,6 @@ exception.replace_null_regex_pointer = Reemplazador tiene un puntero Regex nulo
 exception.bad_pattern_to_regex_perl_code = patrón erróneo en Regex.perlCode: {0}
 exception.no_stub_implementation_for_interface = No existe una implementación del stub para la interfaz: {0}
 exception.cannot_set_endpoint_address_unknown_port = No es posible estabelcer la dirección de punto final para el puerto desconocido {0}
-exception.querying_matching_opening_parenthesis_for_non_closing_parenthesis = Consultando la coincidencia de apertura de paréntesis para paréntesis sin cerrar (?)
 exception.mismatched_unseen_closing_char = Discordancia (no vista) en el carácter de cierre {0}
 exception.mismatched_closing_char = Carácter de cierre discordante {0}
 exception.mismatched_opening_char = Carácter de apertura discordante {0} en {1}
@@ -969,7 +970,6 @@ exception.couldnt_parse_responde_from_annotated3d_server = No es posible parsear
 exception.application_test_npe = Prueba de aplicación: lanzando un NullPointerException que debe aparecer en la consola
 exception.overwriting_vamsas_id_binding = Sobreescribiendo la asociación al VAMSAS id
 exception.overwriting_jalview_id_binding = Sobreescribiendo la asociación al Jalview id
-error.implementation_error_unknown_file_format_string = Error de implementación: cadena de formato de fichero desconocido
 exception.failed_to_resolve_gzip_stream = Fallo al resolver el flujo GZIP 
 exception.problem_opening_file_also_tried = Problema abriendo {0} (también se intentó {1}) : {2}
 exception.problem_opening_file = Problema abriendo {0} : {1}
@@ -1144,10 +1144,14 @@ action.annotations=Anotaciones
 label.nuc_alignment_colour=Color del Alineamiento Nucleotídico
 label.copy_format_from=Copiar formato de
 label.chimera=Chimera
+label.create_chimera_attributes = Escribir características de Jalview
+label.create_chimera_attributes_tip = Establecer atributos en Chimera para características visibles 
+label.attributes_set = {0} valores de atributos establecidos en Chimera
 label.open_split_window=Abrir ventana dividida
 label.open_split_window?=¿Quieres abrir ventana dividida, con cDNA y proteína vinculadas?
 status.searching_for_pdb_structures=Buscando Estructuras PDB
 label.scale_as_cdna=Adaptar residuos proteicos a los codones
+label.font_as_cdna=Utilizar la misma fuente para nucleotídos y proteicos
 action.export_hidden_sequences=Exportar Secuencias Ocultas
 action.export_hidden_columns=Exportar Columnas Ocultas
 label.found_structures_summary=Resumen de Estructuras Encontradas
@@ -1161,12 +1165,12 @@ label.invalid_search=Texto de b
 action.export_annotations=Exportar Anotaciones
 action.set_as_reference=Marcar como Referencia
 action.unmark_as_reference=Desmarcar como Referencia
-action.set_text_colour=Color de Texto...
 label.chimera_failed=Error al abrir Chimera - está instalado?\nCompruebe ruta en Preferencias, Estructura
 label.find=Buscar
 label.select_pdb_file=Seleccionar Fichero PDB
 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
@@ -1183,9 +1187,13 @@ label.hide_insertions=Ocultar Inserciones
 info.change_threshold_mode_to_enable=Cambiar Modo de Umbral para Habilitar
 label.separate_multiple_query_values=Introducir uno o mas {0}s separados por punto y coma ";"
 label.let_chimera_manage_structure_colours=Deja que Chimera maneje colores de estructuras
+label.fetch_chimera_attributes = Buscar atributos desde Chimera
+label.fetch_chimera_attributes_tip = Copiar atributo de Chimera a característica de Jalview
 label.view_rna_structure=Estructura 2D VARNA
-label.scale_protein_to_cdna_tip=Hacer a los residuos de proteínas de la misma anchura que los codones en ventanas divididas
 label.colour_with_chimera=Colorear con Chimera
+label.superpose_structures = Superponer estructuras
+error.superposition_failed = Superposición fallido: {0}
+label.insufficient_residues = Residuos alineados ({0}) insuficentes para superponer
 label.show_pdbstruct_dialog=Datos de Estructura 3D...
 label.hide_all=Ocultar todos
 label.invert=Invertir
@@ -1222,7 +1230,7 @@ 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 for defecto
+label.structure_viewer=Visualizador de estructura 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
@@ -1233,7 +1241,6 @@ tooltip.aacon_calculations=Actualizar c
 info.select_filter_option=Escoger Opción de Filtro / Entrada Manual
 info.invalid_msa_input_mininfo=Necesita por lo menos dos secuencias con al menos 3 residuos cada una, sin regiones ocultas entre ellas.
 label.chimera_missing=Visualizador de estructura Chimera no encontrado.<br/>Por favor, introduzca la ruta de Chimera,<br/>o descargar e instalar la UCSF Chimera.
-label.save_as_biojs_html=Guardar como HTML BioJs
 exception.fts_server_unreachable=Jalview no puede conectar con el servidor {0}. \nPor favor asegúrese de que está conectado a Internet y vuelva a intentarlo.
 exception.outofmemory_loading_mmcif_file=Sin memoria al cargar el fichero mmCIF
 label.hide_columns_not_containing=Ocultar las columnas que no contengan
@@ -1275,4 +1282,29 @@ label.SEQUENCE_ID_no_longer_used = $SEQUENCE_ID$ no se utiliza m
 label.SEQUENCE_ID_for_DB_ACCESSION1 = Por favor, revise sus URLs en la pestaña 'Conexiones' de la ventana de Preferencias:
 label.SEQUENCE_ID_for_DB_ACCESSION2 = URL enlaza usando '$SEQUENCE_ID$' para accesiones DB ahora usar '$DB_ACCESSION$'.
 label.do_not_display_again = No mostrar este mensaje de nuevo
+exception.url_cannot_have_miriam_id = {0} es una id MIRIAM y no puede ser usada como nombre url personalizado
+exception.url_cannot_have_duplicate_id = {0} no puede ser usada como etiqueta en más de un enlace
+label.filter = Filtrar texto:
+action.customfilter = Sólo personalizado
+action.showall = Mostrar todo
+label.insert = Insertar:
+action.seq_id = $SEQUENCE_ID$
+action.db_acc = $DB_ACCESSION$
+label.primary = Doble clic
+label.inmenu = En Menú
+label.id = ID
+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.quality_descr = Calidad de alineamiento basándose en puntuación Blosum62
+label.conservation_descr = Conservación del alineamiento total menos de {0}% huecos
+label.consensus_descr = % Identidad
+label.complement_consensus_descr = % Identidad para cDNA
+label.strucconsensus_descr = % Identidad para pares de bases
+label.occupancy_descr = Número de posiciones alineadas
+label.togglehidden = Show hidden regions
+label.warning_hidden = Advertencia: {0} {1} está actualmente oculto
diff --git a/resources/scoreModel/blosum62.scm b/resources/scoreModel/blosum62.scm
new file mode 100644 (file)
index 0000000..b0e927d
--- /dev/null
@@ -0,0 +1,34 @@
+ScoreMatrix BLOSUM62
+#
+# The BLOSUM62 substitution matrix, as at https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt
+# The first line declares a ScoreMatrix with the name BLOSUM62 (shown in menus)
+#
+# Scores are not symbol case sensitive, unless column(s) are provided for lower case characters
+# The 'guide symbol' at the start of each row of score values is optional
+# Values may be integer or floating point, delimited by tab, space, comma or combinations
+#
+        A       R       N       D       C       Q       E       G       H       I       L       K       M       F       P       S       T       W       Y       V       B       Z       X       * 
+A       4      -1      -2      -2       0      -1      -1       0      -2      -1      -1      -1      -1      -2      -1       1       0      -3      -2       0      -2      -1       0      -4
+R      -1       5       0      -2      -3       1       0      -2       0      -3      -2       2      -1      -3      -2      -1      -1      -3      -2      -3      -1       0      -1      -4
+N      -2       0       6       1      -3       0       0       0       1      -3      -3       0      -2      -3      -2       1       0      -4      -2      -3       3       0      -1      -4
+D      -2      -2       1       6      -3       0       2      -1      -1      -3      -4      -1      -3      -3      -1       0      -1      -4      -3      -3       4       1      -1      -4
+C       0      -3      -3      -3       9      -3      -4      -3      -3      -1      -1      -3      -1      -2      -3      -1      -1      -2      -2      -1      -3      -3      -2      -4
+Q      -1       1       0       0      -3       5       2      -2       0      -3      -2       1       0      -3      -1       0      -1      -2      -1      -2       0       3      -1      -4
+E      -1       0       0       2      -4       2       5      -2       0      -3      -3       1      -2      -3      -1       0      -1      -3      -2      -2       1       4      -1      -4
+G       0      -2       0      -1      -3      -2      -2       6      -2      -4      -4      -2      -3      -3      -2       0      -2      -2      -3      -3      -1      -2      -1      -4
+H      -2       0       1      -1      -3       0       0      -2       8      -3      -3      -1      -2      -1      -2      -1      -2      -2       2      -3       0       0      -1      -4
+I      -1      -3      -3      -3      -1      -3      -3      -4      -3       4       2      -3       1       0      -3      -2      -1      -3      -1       3      -3      -3      -1      -4
+L      -1      -2      -3      -4      -1      -2      -3      -4      -3       2       4      -2       2       0      -3      -2      -1      -2      -1       1      -4      -3      -1      -4
+K      -1       2       0      -1      -3       1       1      -2      -1      -3      -2       5      -1      -3      -1       0      -1      -3      -2      -2       0       1      -1      -4
+M      -1      -1      -2      -3      -1       0      -2      -3      -2       1       2      -1       5       0      -2      -1      -1      -1      -1       1      -3      -1      -1      -4
+F      -2      -3      -3      -3      -2      -3      -3      -3      -1       0       0      -3       0       6      -4      -2      -2       1       3      -1      -3      -3      -1      -4
+P      -1      -2      -2      -1      -3      -1      -1      -2      -2      -3      -3      -1      -2      -4       7      -1      -1      -4      -3      -2      -2      -1      -2      -4
+S       1      -1       1       0      -1       0       0       0      -1      -2      -2       0      -1      -2      -1       4       1      -3      -2      -2       0       0       0      -4
+T       0      -1       0      -1      -1      -1      -1      -2      -2      -1      -1      -1      -1      -2      -1       1       5      -2      -2       0      -1      -1       0      -4
+W      -3      -3      -4      -4      -2      -2      -3      -2      -2      -3      -2      -3      -1       1      -4      -3      -2      11       2      -3      -4      -3      -2      -4
+Y      -2      -2      -2      -3      -2      -1      -2      -3       2      -1      -1      -2      -1       3      -3      -2      -2       2       7      -1      -3      -2      -1      -4
+V       0      -3      -3      -3      -1      -2      -2      -3      -3       3       1      -2       1      -1      -2      -2       0      -3      -1       4      -3      -2      -1      -4
+B      -2      -1       3       4      -3       0       1      -1       0      -3      -4       0      -3      -3      -2       0      -1      -4      -3      -3       4       1      -1      -4
+Z      -1       0       0       1      -3       3       4      -2       0      -3      -3       1      -1      -3      -1       0      -1      -3      -2      -2       1       4      -1      -4
+X       0      -1      -1      -1      -2      -1      -1      -1      -1      -1      -1      -1      -1      -1      -2       0       0      -2      -1      -1      -1      -1      -1      -4
+*      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4      -4       1 
diff --git a/resources/scoreModel/blosum80.scm b/resources/scoreModel/blosum80.scm
new file mode 100644 (file)
index 0000000..8153d3b
--- /dev/null
@@ -0,0 +1,32 @@
+#
+# Source: http://www.genome.jp/dbget-bin/www_bget?aaindex:HENS920103
+#
+H HENS920103
+D BLOSUM80 substitution matrix (Henikoff-Henikoff, 1992)
+R PMID:1438297
+A Henikoff, S. and Henikoff, J.G.
+T Amino acid substitution matrices from protein blocks
+J Proc. Natl. Acad. Sci. USA 89, 10915-10919 (1992)
+* matrix in 1/3 Bit Units
+M rows = ARNDCQEGHILKMFPSTWYV, cols = ARNDCQEGHILKMFPSTWYV
+      7.
+     -3.      9.
+     -3.     -1.      9.
+     -3.     -3.      2.     10.
+     -1.     -6.     -5.     -7.     13.
+     -2.      1.      0.     -1.     -5.      9.
+     -2.     -1.     -1.      2.     -7.      3.      8.
+      0.     -4.     -1.     -3.     -6.     -4.     -4.      9.
+     -3.      0.      1.     -2.     -7.      1.      0.     -4.     12.
+     -3.     -5.     -6.     -7.     -2.     -5.     -6.     -7.     -6.      7.
+     -3.     -4.     -6.     -7.     -3.     -4.     -6.     -7.     -5.      2.      6.
+     -1.      3.      0.     -2.     -6.      2.      1.     -3.     -1.     -5.     -4.      8.
+     -2.     -3.     -4.     -6.     -3.     -1.     -4.     -5.     -4.      2.      3.     -3.      9.
+     -4.     -5.     -6.     -6.     -4.     -5.     -6.     -6.     -2.     -1.      0.     -5.      0.     10.
+     -1.     -3.     -4.     -3.     -6.     -3.     -2.     -5.     -4.     -5.     -5.     -2.     -4.     -6.     12.
+      2.     -2.      1.     -1.     -2.     -1.     -1.     -1.     -2.     -4.     -4.     -1.     -3.     -4.     -2.      7.
+      0.     -2.      0.     -2.     -2.     -1.     -2.     -3.     -3.     -2.     -3.     -1.     -1.     -4.     -3.      2.      8.
+     -5.     -5.     -7.     -8.     -5.     -4.     -6.     -6.     -4.     -5.     -4.     -6.     -3.      0.     -7.     -6.     -5.     16.
+     -4.     -4.     -4.     -6.     -5.     -3.     -5.     -6.      3.     -3.     -2.     -4.     -3.      4.     -6.     -3.     -3.      3.     11.
+     -1.     -4.     -5.     -6.     -2.     -4.     -4.     -6.     -5.      4.      1.     -4.      1.     -2.     -4.     -3.      0.     -5.     -3.      7.
+//
diff --git a/resources/scoreModel/dna.scm b/resources/scoreModel/dna.scm
new file mode 100644 (file)
index 0000000..0d7cbc1
--- /dev/null
@@ -0,0 +1,27 @@
+ScoreMatrix DNA
+#
+# A DNA substitution matrix.
+# This is an ad-hoc matrix which, in addition to penalising mutations between the common 
+# nucleotides (ACGT), includes T/U equivalence in order to allow both DNA and/or RNA. 
+# In addition, it encodes weak equivalence between R and Y with AG and CTU, respectively, 
+# and N is allowed to match any other base weakly. 
+# This matrix also includes I (Inosine) and X (Xanthine), but encodes them to weakly match
+# any of (ACGTU), and unfavourably match each other.
+#
+# The first line declares a ScoreMatrix with the name DNA (shown in menus)
+# Scores are not case sensitive, unless column(s) are provided for lower case characters
+#
+# Values may be integer or floating point, delimited by tab, space, comma or combinations
+#
+        A       C       G       T       U       I       X       R       Y       N      -
+A      10      -8      -8      -8      -8       1       1       1      -8       1      1 
+C      -8      10      -8      -8      -8       1       1      -8       1       1      1
+G      -8      -8      10      -8      -8       1       1       1      -8       1      1
+T      -8      -8      -8      10      10       1       1      -8       1       1      1
+U      -8      -8      -8      10      10       1       1      -8       1       1      1
+I       1       1       1       1       1      10       0       0       0       1      1
+X       1       1       1       1       1       0      10       0       0       1      1
+R       1      -8       1      -8      -8       0       0      10      -8       1      1
+Y      -8       1      -8       1       1       0       0      -8      10       1      1
+N       1       1       1       1       1       1       1       1       1      10      1
+-       1       1       1       1       1       1       1       1       1       1      1
diff --git a/resources/scoreModel/pam250.scm b/resources/scoreModel/pam250.scm
new file mode 100644 (file)
index 0000000..898c723
--- /dev/null
@@ -0,0 +1,32 @@
+ScoreMatrix PAM250
+#
+# The PAM250 substitution matrix
+# The first line declares a ScoreMatrix with the name PAM250 (shown in menus)
+# Scores are not case sensitive, unless column(s) are provided for lower case characters
+# Values may be integer or floating point, delimited by tab, space, comma or combinations
+#
+        A       R       N       D       C       Q       E       G       H       I       L       K       M       F       P       S       T       W       Y       V       B       Z       X       *
+A       2      -2       0       0      -2       0       0       1      -1      -1      -2      -1      -1      -3       1       1       1      -6      -3       0       0       0       0      -8
+R      -2       6       0      -1      -4       1      -1      -3       2      -2      -3       3       0      -4       0       0      -1       2      -4      -2      -1       0      -1      -8
+N       0       0       2       2      -4       1       1       0       2      -2      -3       1      -2      -3       0       1       0      -4      -2      -2       2       1       0      -8
+D       0      -1       2       4      -5       2       3       1       1      -2      -4       0      -3      -6      -1       0       0      -7      -4      -2       3       3      -1      -8
+C      -2      -4      -4      -5      12      -5      -5      -3      -3      -2      -6      -5      -5      -4      -3       0      -2      -8       0      -2      -4      -5      -3      -8
+Q       0       1       1       2      -5       4       2      -1       3      -2      -2       1      -1      -5       0      -1      -1      -5      -4      -2       1       3      -1      -8
+E       0      -1       1       3      -5       2       4       0       1      -2      -3       0      -2      -5      -1       0       0      -7      -4      -2       3       3      -1      -8
+G       1      -3       0       1      -3      -1       0       5      -2      -3      -4      -2      -3      -5       0       1       0      -7      -5      -1       0       0      -1      -8
+H      -1       2       2       1      -3       3       1      -2       6      -2      -2       0      -2      -2       0      -1      -1      -3       0      -2       1       2      -1      -8
+I      -1      -2      -2      -2      -2      -2      -2      -3      -2       5       2      -2       2       1      -2      -1       0      -5      -1       4      -2      -2      -1      -8
+L      -2      -3      -3      -4      -6      -2      -3      -4      -2       2       6      -3       4       2      -3      -3      -2      -2      -1       2      -3      -3      -1      -8
+K      -1       3       1       0      -5       1       0      -2       0      -2      -3       5       0      -5      -1       0       0      -3      -4      -2       1       0      -1      -8
+M      -1       0      -2      -3      -5      -1      -2      -3      -2       2       4       0       6       0      -2      -2      -1      -4      -2       2      -2      -2      -1      -8
+F      -3      -4      -3      -6      -4      -5      -5      -5      -2       1       2      -5       0      9       -5      -3      -3       0       7      -1      -4      -5      -2      -8
+P       1       0       0      -1      -3       0      -1       0       0      -2      -3      -1      -2      -5       6       1       0      -6      -5      -1      -1       0      -1      -8
+S       1       0       1       0       0      -1       0       1      -1      -1      -3       0      -2      -3       1       2       1      -2      -3      -1       0       0       0      -8
+T       1      -1       0       0      -2      -1       0       0      -1       0      -2       0      -1      -3       0       1       3      -5      -3       0       0      -1       0      -8
+W      -6       2      -4      -7      -8      -5      -7      -7      -3      -5      -2      -3      -4       0      -6      -2      -5      17       0      -6      -5      -6      -4      -8
+Y      -3      -4      -2      -4       0      -4      -4      -5       0      -1      -1      -4      -2       7      -5      -3      -3       0      10      -2      -3      -4      -2      -8
+V       0      -2      -2      -2      -2      -2      -2      -1      -2       4       2      -2       2      -1      -1      -1       0      -6      -2       4      -2      -2      -1      -8
+B       0      -1       2       3      -4       1       3       0       1      -2      -3       1      -2      -4      -1       0       0      -5      -3      -2       3       2      -1      -8
+Z       0       0       1       3      -5       3       3       0       2      -2      -3       0      -2      -5       0       0      -1      -6      -4      -2       2       3      -1      -8
+X       0      -1       0      -1      -3      -1      -1      -1      -1      -1      -1      -1      -1      -2      -1       0       0      -4      -2      -1      -1      -1      -1      -8
+*      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8      -8       1
index 4a981ad..6344d1e 100755 (executable)
  * The Jalview Authors are detailed in the 'AUTHORS' file.
 -->
 <mapping>
-       <class name="jalview.datamodel.UniprotFile">
+       <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>
@@ -71,7 +71,7 @@
                 </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 aac796c..39111c3 100644 (file)
@@ -28,6 +28,7 @@ import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.AtomSpec;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
@@ -177,7 +178,7 @@ public class AppletPDBCanvas extends Panel implements MouseListener,
 
     colourBySequence();
 
-    int max = -10;
+    float max = -10;
     int maxchain = -1;
     int pdbstart = 0;
     int pdbend = 0;
@@ -577,6 +578,8 @@ public class AppletPDBCanvas extends Panel implements MouseListener,
       showFeatures = true;
     }
 
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
+
     PDBChain chain;
     if (bysequence && pdb != null)
     {
@@ -604,25 +607,16 @@ public class AppletPDBCanvas extends Panel implements MouseListener,
                 if (pos > 0)
                 {
                   pos = sequence[s].findIndex(pos);
-                  tmp.startCol = sr.getResidueBoxColour(sequence[s], pos);
-                  if (showFeatures)
-                  {
-                    tmp.startCol = fr.findFeatureColour(tmp.startCol,
-                            sequence[s], pos);
-                  }
+                  tmp.startCol = sr.getResidueColour(sequence[s], pos,
+                          finder);
                 }
                 pos = mapping[m].getSeqPos(tmp.at2.resNumber) - 1;
                 if (pos > 0)
                 {
                   pos = sequence[s].findIndex(pos);
-                  tmp.endCol = sr.getResidueBoxColour(sequence[s], pos);
-                  if (showFeatures)
-                  {
-                    tmp.endCol = fr.findFeatureColour(tmp.endCol,
-                            sequence[s], pos);
-                  }
+                  tmp.endCol = sr
+                          .getResidueColour(sequence[s], pos, finder);
                 }
-
               }
             }
           }
@@ -1125,7 +1119,7 @@ public class AppletPDBCanvas extends Panel implements MouseListener,
   // ////////////////////////////////
   // /StructureListener
   @Override
-  public String[] getPdbFile()
+  public String[] getStructureFiles()
   {
     return new String[] { pdbentry.getFile() };
   }
index 76ee4b0..133565c 100644 (file)
@@ -185,7 +185,8 @@ public class AppletPDBViewer extends EmbmenuFrame implements
     zbuffer.addItemListener(this);
     charge.setLabel(MessageManager.getString("label.charge_cysteine"));
     charge.addActionListener(this);
-    hydro.setLabel(MessageManager.getString("label.hydrophobicity"));
+    hydro.setLabel(MessageManager
+            .getString("label.colourScheme_hydrophobic"));
     hydro.addActionListener(this);
     chain.setLabel(MessageManager.getString("action.by_chain"));
     chain.addActionListener(this);
@@ -195,17 +196,21 @@ public class AppletPDBViewer extends EmbmenuFrame implements
             .setLabel(MessageManager.getString("label.all_chains_visible"));
     allchains.addItemListener(this);
     viewMenu.setLabel(MessageManager.getString("action.view"));
-    zappo.setLabel(MessageManager.getString("label.zappo"));
+    zappo.setLabel(MessageManager.getString("label.colourScheme_zappo"));
     zappo.addActionListener(this);
-    taylor.setLabel(MessageManager.getString("label.taylor"));
+    taylor.setLabel(MessageManager.getString("label.colourScheme_taylor"));
     taylor.addActionListener(this);
-    helix.setLabel(MessageManager.getString("label.helix_propensity"));
+    helix.setLabel(MessageManager
+            .getString("label.colourScheme_helix_propensity"));
     helix.addActionListener(this);
-    strand.setLabel(MessageManager.getString("label.strand_propensity"));
+    strand.setLabel(MessageManager
+            .getString("label.colourScheme_strand_propensity"));
     strand.addActionListener(this);
-    turn.setLabel(MessageManager.getString("label.turn_propensity"));
+    turn.setLabel(MessageManager
+            .getString("label.colourScheme_turn_propensity"));
     turn.addActionListener(this);
-    buried.setLabel(MessageManager.getString("label.buried_index"));
+    buried.setLabel(MessageManager
+            .getString("label.colourScheme_buried_index"));
     buried.addActionListener(this);
     user.setLabel(MessageManager.getString("action.user_defined"));
     user.addActionListener(this);
index 292de91..83642cc 100644 (file)
@@ -28,6 +28,7 @@ import jalview.gui.FeatureRenderer;
 import jalview.gui.SequenceRenderer;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.AtomSpec;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
@@ -176,7 +177,7 @@ public class PDBCanvas extends JPanel implements MouseListener,
 
     colourBySequence();
 
-    int max = -10;
+    float max = -10;
     int maxchain = -1;
     int pdbstart = 0;
     int pdbend = 0;
@@ -546,6 +547,7 @@ public class PDBCanvas extends JPanel implements MouseListener,
       showFeatures = true;
     }
 
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
     PDBChain chain;
     if (bysequence && pdb != null)
     {
@@ -573,23 +575,15 @@ public class PDBCanvas extends JPanel implements MouseListener,
                 if (pos > 0)
                 {
                   pos = sequence[s].findIndex(pos);
-                  tmp.startCol = sr.getResidueBoxColour(sequence[s], pos);
-                  if (showFeatures)
-                  {
-                    tmp.startCol = fr.findFeatureColour(tmp.startCol,
-                            sequence[s], pos);
-                  }
+                  tmp.startCol = sr.getResidueColour(sequence[s], pos,
+                          finder);
                 }
                 pos = mapping[m].getSeqPos(tmp.at2.resNumber) - 1;
                 if (pos > 0)
                 {
                   pos = sequence[s].findIndex(pos);
-                  tmp.endCol = sr.getResidueBoxColour(sequence[s], pos);
-                  if (showFeatures)
-                  {
-                    tmp.endCol = fr.findFeatureColour(tmp.endCol,
-                            sequence[s], pos);
-                  }
+                  tmp.endCol = sr
+                          .getResidueColour(sequence[s], pos, finder);
                 }
 
               }
@@ -1081,7 +1075,7 @@ public class PDBCanvas extends JPanel implements MouseListener,
   // ////////////////////////////////
   // /StructureListener
   @Override
-  public String[] getPdbFile()
+  public String[] getStructureFiles()
   {
     return new String[] { pdbentry.getFile() };
   }
index 3c0a1f2..8285d88 100755 (executable)
@@ -167,15 +167,14 @@ public class PDBChain
   }
 
   /**
-   * copy over the RESNUM seqfeatures from the internal chain sequence to the
+   * 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 +183,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 +197,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) ? ""
                         : ":" + tx.getStatus()));
@@ -221,7 +219,6 @@ public class PDBChain
         }
       }
     }
-    return features;
   }
 
   /**
@@ -353,25 +350,25 @@ public class PDBChain
               && !residues.isEmpty()
               && 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
@@ -518,10 +515,12 @@ public class PDBChain
       try
       {
         index = ResidueProperties.aa3Hash.get(b.at1.resName).intValue();
-        b.startCol = cs.findColour(ResidueProperties.aa[index].charAt(0));
+        b.startCol = cs.findColour(ResidueProperties.aa[index].charAt(0),
+                0, null, null, 0f);
 
         index = ResidueProperties.aa3Hash.get(b.at2.resName).intValue();
-        b.endCol = cs.findColour(ResidueProperties.aa[index].charAt(0));
+        b.endCol = cs.findColour(ResidueProperties.aa[index].charAt(0), 0,
+                null, null, 0f);
 
       } catch (Exception e)
       {
diff --git a/src/MCview/PDBViewer.java b/src/MCview/PDBViewer.java
deleted file mode 100755 (executable)
index f108fc2..0000000
+++ /dev/null
@@ -1,784 +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 MCview;
-
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.gui.AlignmentPanel;
-import jalview.gui.Desktop;
-import jalview.gui.JvOptionPane;
-import jalview.gui.OOMWarning;
-import jalview.gui.UserDefinedColours;
-import jalview.io.DataSourceType;
-import jalview.io.JalviewFileChooser;
-import jalview.io.JalviewFileView;
-import jalview.schemes.BuriedColourScheme;
-import jalview.schemes.HelixColourScheme;
-import jalview.schemes.HydrophobicColourScheme;
-import jalview.schemes.StrandColourScheme;
-import jalview.schemes.TaylorColourScheme;
-import jalview.schemes.TurnColourScheme;
-import jalview.schemes.UserColourScheme;
-import jalview.schemes.ZappoColourScheme;
-import jalview.util.MessageManager;
-import jalview.ws.ebi.EBIFetchClient;
-
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.awt.event.KeyAdapter;
-import java.awt.event.KeyEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.PrintWriter;
-
-import javax.swing.ButtonGroup;
-import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JColorChooser;
-import javax.swing.JInternalFrame;
-import javax.swing.JMenu;
-import javax.swing.JMenuBar;
-import javax.swing.JMenuItem;
-import javax.swing.JRadioButtonMenuItem;
-
-public class PDBViewer extends JInternalFrame implements Runnable
-{
-
-  /**
-   * The associated sequence in an alignment
-   */
-  PDBCanvas pdbcanvas;
-
-  PDBEntry pdbentry;
-
-  SequenceI[] seq;
-
-  String[] chains;
-
-  AlignmentPanel ap;
-
-  DataSourceType protocol;
-
-  String tmpPDBFile;
-
-  public PDBViewer(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
-          AlignmentPanel ap, DataSourceType protocol)
-  {
-    this.pdbentry = pdbentry;
-    this.seq = seq;
-    this.chains = chains;
-    this.ap = ap;
-    this.protocol = protocol;
-
-    try
-    {
-      jbInit();
-    } catch (Exception ex)
-    {
-      ex.printStackTrace();
-    }
-
-    StringBuffer title = new StringBuffer(seq[0].getName() + ":"
-            + pdbentry.getFile());
-
-    pdbcanvas = new PDBCanvas();
-
-    setContentPane(pdbcanvas);
-
-    if (pdbentry.getFile() != null)
-    {
-      try
-      {
-        tmpPDBFile = pdbentry.getFile();
-        PDBfile pdbfile = new PDBfile(false, false, false, tmpPDBFile,
-                DataSourceType.FILE);
-
-        pdbcanvas.init(pdbentry, seq, chains, ap, protocol);
-
-      } catch (java.io.IOException ex)
-      {
-        ex.printStackTrace();
-      }
-    }
-    else
-    {
-      Thread worker = new Thread(this);
-      worker.start();
-    }
-
-    String method = (String) pdbentry.getProperty("method");
-    if (method != null)
-    {
-      title.append(" Method: ");
-      title.append(method);
-    }
-    String ch = (String) pdbentry.getProperty("chains");
-    if (ch != null)
-    {
-      title.append(" Chain:");
-      title.append(ch);
-    }
-    Desktop.addInternalFrame(this, title.toString(), 400, 400);
-  }
-
-  @Override
-  public void run()
-  {
-    try
-    {
-      EBIFetchClient ebi = new EBIFetchClient();
-      String query = "pdb:" + pdbentry.getId();
-      pdbentry.setFile(ebi.fetchDataAsFile(query, "default", "xml")
-              .getAbsolutePath());
-
-      if (pdbentry.getFile() != null)
-      {
-        pdbcanvas.init(pdbentry, seq, chains, ap, protocol);
-      }
-    } catch (Exception ex)
-    {
-      pdbcanvas.errorMessage = "Error retrieving file: " + pdbentry.getId();
-      ex.printStackTrace();
-    }
-  }
-
-  private void jbInit() throws Exception
-  {
-    this.addKeyListener(new KeyAdapter()
-    {
-      @Override
-      public void keyPressed(KeyEvent evt)
-      {
-        pdbcanvas.keyPressed(evt);
-      }
-    });
-
-    this.setJMenuBar(jMenuBar1);
-    fileMenu.setText(MessageManager.getString("action.file"));
-    coloursMenu.setText(MessageManager.getString("label.colours"));
-    saveMenu.setActionCommand(MessageManager.getString("action.save_image"));
-    saveMenu.setText(MessageManager.getString("action.save_as"));
-    png.setText("PNG");
-    png.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        png_actionPerformed(e);
-      }
-    });
-    eps.setText("EPS");
-    eps.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        eps_actionPerformed(e);
-      }
-    });
-    mapping.setText(MessageManager.getString("label.view_mapping"));
-    mapping.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        mapping_actionPerformed(e);
-      }
-    });
-    wire.setText(MessageManager.getString("label.wireframe"));
-    wire.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        wire_actionPerformed(e);
-      }
-    });
-    depth.setSelected(true);
-    depth.setText(MessageManager.getString("label.depthcue"));
-    depth.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        depth_actionPerformed(e);
-      }
-    });
-    zbuffer.setSelected(true);
-    zbuffer.setText(MessageManager.getString("label.z_buffering"));
-    zbuffer.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        zbuffer_actionPerformed(e);
-      }
-    });
-    charge.setText(MessageManager.getString("label.charge_cysteine"));
-    charge.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        charge_actionPerformed(e);
-      }
-    });
-    chain.setText(MessageManager.getString("action.by_chain"));
-    chain.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        chain_actionPerformed(e);
-      }
-    });
-    seqButton.setSelected(true);
-    seqButton.setText(MessageManager.getString("action.by_sequence"));
-    seqButton.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        seqButton_actionPerformed(e);
-      }
-    });
-    allchains.setSelected(true);
-    allchains.setText(MessageManager.getString("label.show_all_chains"));
-    allchains.addItemListener(new ItemListener()
-    {
-      @Override
-      public void itemStateChanged(ItemEvent e)
-      {
-        allchains_itemStateChanged(e);
-      }
-    });
-    zappo.setText(MessageManager.getString("label.zappo"));
-    zappo.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        zappo_actionPerformed(e);
-      }
-    });
-    taylor.setText(MessageManager.getString("label.taylor"));
-    taylor.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        taylor_actionPerformed(e);
-      }
-    });
-    hydro.setText(MessageManager.getString("label.hydrophobicity"));
-    hydro.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        hydro_actionPerformed(e);
-      }
-    });
-    helix.setText(MessageManager.getString("label.helix_propensity"));
-    helix.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        helix_actionPerformed(e);
-      }
-    });
-    strand.setText(MessageManager.getString("label.strand_propensity"));
-    strand.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        strand_actionPerformed(e);
-      }
-    });
-    turn.setText(MessageManager.getString("label.turn_propensity"));
-    turn.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        turn_actionPerformed(e);
-      }
-    });
-    buried.setText(MessageManager.getString("label.buried_index"));
-    buried.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        buried_actionPerformed(e);
-      }
-    });
-    user.setText(MessageManager.getString("action.user_defined"));
-    user.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        user_actionPerformed(e);
-      }
-    });
-    viewMenu.setText(MessageManager.getString("action.view"));
-    background
-            .setText(MessageManager.getString("action.background_colour"));
-    background.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        background_actionPerformed(e);
-      }
-    });
-    savePDB.setText(MessageManager.getString("label.pdb_file"));
-    savePDB.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        savePDB_actionPerformed(e);
-      }
-    });
-    jMenuBar1.add(fileMenu);
-    jMenuBar1.add(coloursMenu);
-    jMenuBar1.add(viewMenu);
-    fileMenu.add(saveMenu);
-    fileMenu.add(mapping);
-    saveMenu.add(savePDB);
-    saveMenu.add(png);
-    saveMenu.add(eps);
-    coloursMenu.add(seqButton);
-    coloursMenu.add(chain);
-    coloursMenu.add(charge);
-    coloursMenu.add(zappo);
-    coloursMenu.add(taylor);
-    coloursMenu.add(hydro);
-    coloursMenu.add(helix);
-    coloursMenu.add(strand);
-    coloursMenu.add(turn);
-    coloursMenu.add(buried);
-    coloursMenu.add(user);
-    coloursMenu.add(background);
-    ButtonGroup bg = new ButtonGroup();
-    bg.add(seqButton);
-    bg.add(chain);
-    bg.add(charge);
-    bg.add(zappo);
-    bg.add(taylor);
-    bg.add(hydro);
-    bg.add(helix);
-    bg.add(strand);
-    bg.add(turn);
-    bg.add(buried);
-    bg.add(user);
-
-    if (jalview.gui.UserDefinedColours.getUserColourSchemes() != null)
-    {
-      java.util.Enumeration userColours = jalview.gui.UserDefinedColours
-              .getUserColourSchemes().keys();
-
-      while (userColours.hasMoreElements())
-      {
-        final JRadioButtonMenuItem radioItem = new JRadioButtonMenuItem(
-                userColours.nextElement().toString());
-        radioItem.setName("USER_DEFINED");
-        radioItem.addMouseListener(new MouseAdapter()
-        {
-          @Override
-          public void mousePressed(MouseEvent evt)
-          {
-            if (evt.isPopupTrigger()) // Mac
-            {
-              offerRemoval(radioItem);
-            }
-          }
-
-          @Override
-          public void mouseReleased(MouseEvent evt)
-          {
-            if (evt.isPopupTrigger()) // Windows
-            {
-              offerRemoval(radioItem);
-            }
-          }
-
-          /**
-           * @param radioItem
-           */
-          void offerRemoval(final JRadioButtonMenuItem radioItem)
-          {
-            radioItem.removeActionListener(radioItem.getActionListeners()[0]);
-
-            int option = JvOptionPane.showInternalConfirmDialog(
-                    jalview.gui.Desktop.desktop, MessageManager
-                            .getString("label.remove_from_default_list"),
-                    MessageManager
-                            .getString("label.remove_user_defined_colour"),
-                    JvOptionPane.YES_NO_OPTION);
-            if (option == JvOptionPane.YES_OPTION)
-            {
-              jalview.gui.UserDefinedColours
-                      .removeColourFromDefaults(radioItem.getText());
-              coloursMenu.remove(radioItem);
-            }
-            else
-            {
-              radioItem.addActionListener(new ActionListener()
-              {
-                @Override
-                public void actionPerformed(ActionEvent evt)
-                {
-                  user_actionPerformed(evt);
-                }
-              });
-            }
-          }
-        });
-        radioItem.addActionListener(new ActionListener()
-        {
-          @Override
-          public void actionPerformed(ActionEvent evt)
-          {
-            user_actionPerformed(evt);
-          }
-        });
-        coloursMenu.add(radioItem);
-        bg.add(radioItem);
-      }
-    }
-
-    viewMenu.add(wire);
-    viewMenu.add(depth);
-    viewMenu.add(zbuffer);
-    viewMenu.add(allchains);
-  }
-
-  JMenuBar jMenuBar1 = new JMenuBar();
-
-  JMenu fileMenu = new JMenu();
-
-  JMenu coloursMenu = new JMenu();
-
-  JMenu saveMenu = new JMenu();
-
-  JMenuItem png = new JMenuItem();
-
-  JMenuItem eps = new JMenuItem();
-
-  JMenuItem mapping = new JMenuItem();
-
-  JCheckBoxMenuItem wire = new JCheckBoxMenuItem();
-
-  JCheckBoxMenuItem depth = new JCheckBoxMenuItem();
-
-  JCheckBoxMenuItem zbuffer = new JCheckBoxMenuItem();
-
-  JCheckBoxMenuItem allchains = new JCheckBoxMenuItem();
-
-  JRadioButtonMenuItem charge = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem chain = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem seqButton = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem hydro = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem taylor = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem zappo = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem user = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem buried = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem turn = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem strand = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem helix = new JRadioButtonMenuItem();
-
-  JMenu viewMenu = new JMenu();
-
-  JMenuItem background = new JMenuItem();
-
-  JMenuItem savePDB = new JMenuItem();
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  public void eps_actionPerformed(ActionEvent e)
-  {
-    makePDBImage(jalview.util.ImageMaker.TYPE.EPS);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  public void png_actionPerformed(ActionEvent e)
-  {
-    makePDBImage(jalview.util.ImageMaker.TYPE.PNG);
-  }
-
-  void makePDBImage(jalview.util.ImageMaker.TYPE type)
-  {
-    int width = pdbcanvas.getWidth();
-    int height = pdbcanvas.getHeight();
-
-    jalview.util.ImageMaker im;
-
-    if (type == jalview.util.ImageMaker.TYPE.PNG)
-    {
-      im = new jalview.util.ImageMaker(this,
-              jalview.util.ImageMaker.TYPE.PNG, "Make PNG image from view",
-              width, height, null, null, null, 0, false);
-    }
-    else if (type == jalview.util.ImageMaker.TYPE.EPS)
-    {
-      im = new jalview.util.ImageMaker(this,
-              jalview.util.ImageMaker.TYPE.EPS, "Make EPS file from view",
-              width, height, null, this.getTitle(), null, 0, false);
-    }
-    else
-    {
-
-      im = new jalview.util.ImageMaker(this,
-              jalview.util.ImageMaker.TYPE.SVG, "Make SVG file from PCA",
-              width, height, null, this.getTitle(), null, 0, false);
-    }
-
-    if (im.getGraphics() != null)
-    {
-      pdbcanvas.drawAll(im.getGraphics(), width, height);
-      im.writeImage();
-    }
-  }
-
-  public void charge_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setChargeColours();
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void hydro_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new HydrophobicColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void chain_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setChainColours();
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void zbuffer_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.zbuffer = !pdbcanvas.zbuffer;
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void molecule_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bymolecule = !pdbcanvas.bymolecule;
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void depth_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.depthcue = !pdbcanvas.depthcue;
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void wire_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.wire = !pdbcanvas.wire;
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void seqButton_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = true;
-    pdbcanvas.updateSeqColours();
-  }
-
-  public void mapping_actionPerformed(ActionEvent e)
-  {
-    jalview.gui.CutAndPasteTransfer cap = new jalview.gui.CutAndPasteTransfer();
-    try
-    {
-      cap.setText(pdbcanvas.mappingDetails.toString());
-      Desktop.addInternalFrame(cap,
-              MessageManager.getString("label.pdb_sequence_mapping"), 550,
-              600);
-    } catch (OutOfMemoryError oom)
-    {
-      new OOMWarning("Opening sequence to structure mapping report", oom);
-      cap.dispose();
-    }
-  }
-
-  public void allchains_itemStateChanged(ItemEvent e)
-  {
-    pdbcanvas.setAllchainsVisible(allchains.getState());
-  }
-
-  public void zappo_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new ZappoColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void taylor_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new TaylorColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void helix_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new HelixColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void strand_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new StrandColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void turn_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new TurnColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void buried_actionPerformed(ActionEvent e)
-  {
-    pdbcanvas.bysequence = false;
-    pdbcanvas.pdb.setColours(new BuriedColourScheme());
-    pdbcanvas.redrawneeded = true;
-    pdbcanvas.repaint();
-  }
-
-  public void user_actionPerformed(ActionEvent e)
-  {
-    if (e.getActionCommand().equals(
-            MessageManager.getString("action.user_defined")))
-    {
-      // new UserDefinedColours(pdbcanvas, null);
-    }
-    else
-    {
-      UserColourScheme udc = (UserColourScheme) UserDefinedColours
-              .getUserColourSchemes().get(e.getActionCommand());
-
-      pdbcanvas.pdb.setColours(udc);
-      pdbcanvas.redrawneeded = true;
-      pdbcanvas.repaint();
-    }
-  }
-
-  public void background_actionPerformed(ActionEvent e)
-  {
-    java.awt.Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_backgroud_colour"),
-            pdbcanvas.backgroundColour);
-
-    if (col != null)
-    {
-      pdbcanvas.backgroundColour = col;
-      pdbcanvas.redrawneeded = true;
-      pdbcanvas.repaint();
-    }
-  }
-
-  public void savePDB_actionPerformed(ActionEvent e)
-  {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
-
-    chooser.setFileView(new JalviewFileView());
-    chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
-    chooser.setToolTipText(MessageManager.getString("action.save"));
-
-    int value = chooser.showSaveDialog(this);
-
-    if (value == JalviewFileChooser.APPROVE_OPTION)
-    {
-      try
-      {
-        BufferedReader in = new BufferedReader(new FileReader(tmpPDBFile));
-        File outFile = chooser.getSelectedFile();
-
-        PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
-        String data;
-        while ((data = in.readLine()) != null)
-        {
-          if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
-          {
-            out.println(data);
-          }
-        }
-        out.close();
-        in.close();
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
-      }
-    }
-  }
-}
index ffa413b..b806355 100755 (executable)
  */
 package jalview.analysis;
 
-import java.util.Arrays;
-import java.util.Hashtable;
-import java.util.List;
-
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
@@ -41,6 +37,11 @@ import jalview.util.Format;
 import jalview.util.MappingUtils;
 import jalview.util.QuickSort;
 
+import java.awt.Color;
+import java.util.Arrays;
+import java.util.Hashtable;
+import java.util.List;
+
 /**
  * Takes in a vector or array of sequences and column start and column end and
  * returns a new Hashtable[] of size maxSeqLength, if Hashtable not supplied.
@@ -291,7 +292,7 @@ public class AAFrequency
   /**
    * Derive the gap count annotation row.
    * 
-   * @param consensus
+   * @param gaprow
    *          the annotation row to add annotations to
    * @param profiles
    *          the source consensus data
@@ -300,11 +301,11 @@ public class AAFrequency
    * @param endCol
    *          end column (exclusive)
    */
-  public static void completeGapAnnot(AlignmentAnnotation consensus,
+  public static void completeGapAnnot(AlignmentAnnotation gaprow,
           ProfilesI profiles, int startCol, int endCol, long nseq)
   {
-    if (consensus == null || consensus.annotations == null
-            || consensus.annotations.length < endCol)
+    if (gaprow == null || gaprow.annotations == null
+            || gaprow.annotations.length < endCol)
     {
       /*
        * called with a bad alignment annotation row 
@@ -313,8 +314,9 @@ public class AAFrequency
       return;
     }
     // always set ranges again
-    consensus.graphMax = nseq;
-    consensus.graphMin = 0;
+    gaprow.graphMax = nseq;
+    gaprow.graphMin = 0;
+    double scale = 0.8/nseq;
     for (int i = startCol; i < endCol; i++)
     {
       ProfileI profile = profiles.get(i);
@@ -324,17 +326,17 @@ public class AAFrequency
          * happens if sequences calculated over were 
          * shorter than alignment width
          */
-        consensus.annotations[i] = null;
+        gaprow.annotations[i] = null;
         return;
       }
 
       final int gapped = profile.getNonGapped();
 
-      String description = String.valueOf(gapped);
+      String description = "" + gapped;
 
-      consensus.annotations[i] = new Annotation(description, description,
-              '\0',
-              gapped);
+      gaprow.annotations[i] = new Annotation("", description,
+              '\0', gapped, jalview.util.ColorUtils.bleachColour(
+                      Color.DARK_GRAY, (float) scale * gapped));
     }
   }
 
index 86bf721..07f43da 100755 (executable)
  */
 package jalview.analysis;
 
+import jalview.analysis.scoremodels.PIDModel;
+import jalview.analysis.scoremodels.ScoreMatrix;
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.analysis.scoremodels.SimilarityParams;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.ResidueProperties;
-import jalview.schemes.ScoreMatrix;
 import jalview.util.Comparison;
 import jalview.util.Format;
 import jalview.util.MapList;
@@ -53,17 +55,11 @@ public class AlignSeq
 
   private static final String NEWLINE = System.lineSeparator();
 
-  static String[] dna = { "A", "C", "G", "T", "-" };
+  float[][] score;
 
-  // "C", "T", "A", "G", "-"};
-  static String[] pep = { "A", "R", "N", "D", "C", "Q", "E", "G", "H", "I",
-      "L", "K", "M", "F", "P", "S", "T", "W", "Y", "V", "B", "Z", "X", "-" };
+  float[][] E;
 
-  int[][] score;
-
-  int[][] E;
-
-  int[][] F;
+  float[][] F;
 
   int[][] traceback;
 
@@ -106,7 +102,7 @@ public class AlignSeq
   int count;
 
   /** DOCUMENT ME!! */
-  public int maxscore;
+  public float maxscore;
 
   float pid;
 
@@ -116,31 +112,24 @@ public class AlignSeq
 
   int gapExtend = 20;
 
-  int[][] lookup = ResidueProperties.getBLOSUM62();
-
-  String[] intToStr = pep;
-
-  int defInt = 23;
-
   StringBuffer output = new StringBuffer();
 
-  String type;
+  String type; // AlignSeq.PEP or AlignSeq.DNA
+
+  private ScoreMatrix scoreMatrix;
 
-  private int[] charToInt;
+  private static final int GAP_INDEX = -1;
 
   /**
    * Creates a new AlignSeq object.
    * 
-   * @param s1
-   *          DOCUMENT ME!
-   * @param s2
-   *          DOCUMENT ME!
-   * @param type
-   *          DOCUMENT ME!
+   * @param s1 first sequence for alignment
+   * @param s2 second sequence for alignment
+   * @param type molecule type, either AlignSeq.PEP or AlignSeq.DNA
    */
   public AlignSeq(SequenceI s1, SequenceI s2, String type)
   {
-    SeqInit(s1, s1.getSequenceAsString(), s2, s2.getSequenceAsString(),
+    seqInit(s1, s1.getSequenceAsString(), s2, s2.getSequenceAsString(),
             type);
   }
 
@@ -157,7 +146,7 @@ public class AlignSeq
   public AlignSeq(SequenceI s1, String string1, SequenceI s2,
           String string2, String type)
   {
-    SeqInit(s1, string1.toUpperCase(), s2, string2.toUpperCase(), type);
+    seqInit(s1, string1.toUpperCase(), s2, string2.toUpperCase(), type);
   }
 
   /**
@@ -165,7 +154,7 @@ public class AlignSeq
    * 
    * @return DOCUMENT ME!
    */
-  public int getMaxScore()
+  public float getMaxScore()
   {
     return maxscore;
   }
@@ -261,26 +250,6 @@ public class AlignSeq
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public SequenceI getS1()
-  {
-    return s1;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public SequenceI getS2()
-  {
-    return s2;
-  }
-
-  /**
    * 
    * @return aligned instance of Seq 1
    */
@@ -322,36 +291,13 @@ public class AlignSeq
    * @param type
    *          DNA or PEPTIDE
    */
-  public void SeqInit(SequenceI s1, String string1, SequenceI s2,
+  public void seqInit(SequenceI s1, String string1, SequenceI s2,
           String string2, String type)
   {
     this.s1 = s1;
     this.s2 = s2;
     setDefaultParams(type);
-    SeqInit(string1, string2);
-  }
-
-  /**
-   * Construct score matrix for sequences with custom substitution matrix
-   * 
-   * @param s1
-   *          - sequence 1
-   * @param string1
-   *          - string to use for s1
-   * @param s2
-   *          - sequence 2
-   * @param string2
-   *          - string to use for s2
-   * @param scoreMatrix
-   *          - substitution matrix to use for alignment
-   */
-  public void SeqInit(SequenceI s1, String string1, SequenceI s2,
-          String string2, ScoreMatrix scoreMatrix)
-  {
-    this.s1 = s1;
-    this.s2 = s2;
-    setType(scoreMatrix.isDNA() ? AlignSeq.DNA : AlignSeq.PEP);
-    lookup = scoreMatrix.getMatrix();
+    seqInit(string1, string2);
   }
 
   /**
@@ -361,7 +307,7 @@ public class AlignSeq
    * @param string1
    * @param string2
    */
-  private void SeqInit(String string1, String string2)
+  private void seqInit(String string1, String string2)
   {
     s1str = extractGaps(jalview.util.Comparison.GapChars, string1);
     s2str = extractGaps(jalview.util.Comparison.GapChars, string2);
@@ -374,84 +320,31 @@ public class AlignSeq
       return;
     }
 
-    // System.out.println("lookuip " + rt.freeMemory() + " "+ rt.totalMemory());
-    seq1 = new int[s1str.length()];
-
-    // System.out.println("seq1 " + rt.freeMemory() +" " + rt.totalMemory());
-    seq2 = new int[s2str.length()];
-
-    // System.out.println("seq2 " + rt.freeMemory() + " " + rt.totalMemory());
-    score = new int[s1str.length()][s2str.length()];
+    score = new float[s1str.length()][s2str.length()];
 
-    // System.out.println("score " + rt.freeMemory() + " " + rt.totalMemory());
-    E = new int[s1str.length()][s2str.length()];
+    E = new float[s1str.length()][s2str.length()];
 
-    // System.out.println("E " + rt.freeMemory() + " " + rt.totalMemory());
-    F = new int[s1str.length()][s2str.length()];
+    F = new float[s1str.length()][s2str.length()];
     traceback = new int[s1str.length()][s2str.length()];
 
-    // System.out.println("F " + rt.freeMemory() + " " + rt.totalMemory());
-    seq1 = stringToInt(s1str, type);
-
-    // System.out.println("seq1 " + rt.freeMemory() + " " + rt.totalMemory());
-    seq2 = stringToInt(s2str, type);
-
-    // System.out.println("Seq2 " + rt.freeMemory() + " " + rt.totalMemory());
-    // long tstart = System.currentTimeMillis();
-    // calcScoreMatrix();
-    // long tend = System.currentTimeMillis();
-    // System.out.println("Time take to calculate score matrix = " +
-    // (tend-tstart) + " ms");
-    // printScoreMatrix(score);
-    // System.out.println();
-    // printScoreMatrix(traceback);
-    // System.out.println();
-    // printScoreMatrix(E);
-    // System.out.println();
-    // /printScoreMatrix(F);
-    // System.out.println();
-    // tstart = System.currentTimeMillis();
-    // traceAlignment();
-    // tend = System.currentTimeMillis();
-    // System.out.println("Time take to traceback alignment = " + (tend-tstart)
-    // + " ms");
-  }
-
-  private void setDefaultParams(String type)
-  {
-    setType(type);
+    seq1 = indexEncode(s1str);
 
-    if (type.equals(AlignSeq.PEP))
-    {
-      lookup = ResidueProperties.getDefaultPeptideMatrix();
-    }
-    else if (type.equals(AlignSeq.DNA))
-    {
-      lookup = ResidueProperties.getDefaultDnaMatrix();
-    }
+    seq2 = indexEncode(s2str);
   }
 
-  private void setType(String type2)
+  private void setDefaultParams(String moleculeType)
   {
-    this.type = type2;
-    if (type.equals(AlignSeq.PEP))
-    {
-      intToStr = pep;
-      charToInt = ResidueProperties.aaIndex;
-      defInt = ResidueProperties.maxProteinIndex;
-    }
-    else if (type.equals(AlignSeq.DNA))
-    {
-      intToStr = dna;
-      charToInt = ResidueProperties.nucleotideIndex;
-      defInt = ResidueProperties.maxNucleotideIndex;
-    }
-    else
+    if (!PEP.equals(moleculeType) && !DNA.equals(moleculeType))
     {
       output.append("Wrong type = dna or pep only");
       throw new Error(MessageManager.formatMessage(
-              "error.unknown_type_dna_or_pep", new String[] { type2 }));
+              "error.unknown_type_dna_or_pep",
+              new String[] { moleculeType }));
     }
+
+    type = moleculeType;
+    scoreMatrix = ScoreModels.getInstance().getDefaultModel(
+            PEP.equals(type));
   }
 
   /**
@@ -460,7 +353,7 @@ public class AlignSeq
   public void traceAlignment()
   {
     // Find the maximum score along the rhs or bottom row
-    int max = -9999;
+    float max = -Float.MAX_VALUE;
 
     for (int i = 0; i < seq1.length; i++)
     {
@@ -494,21 +387,17 @@ public class AlignSeq
     aseq1 = new int[seq1.length + seq2.length];
     aseq2 = new int[seq1.length + seq2.length];
 
+    StringBuilder sb1 = new StringBuilder(aseq1.length);
+    StringBuilder sb2 = new StringBuilder(aseq2.length);
+
     count = (seq1.length + seq2.length) - 1;
 
-    while ((i > 0) && (j > 0))
+    while (i > 0 && j > 0)
     {
-      if ((aseq1[count] != defInt) && (i >= 0))
-      {
-        aseq1[count] = seq1[i];
-        astr1 = s1str.charAt(i) + astr1;
-      }
-
-      if ((aseq2[count] != defInt) && (j > 0))
-      {
-        aseq2[count] = seq2[j];
-        astr2 = s2str.charAt(j) + astr2;
-      }
+      aseq1[count] = seq1[i];
+      sb1.append(s1str.charAt(i));
+      aseq2[count] = seq2[j];
+      sb2.append(s2str.charAt(j));
 
       trace = findTrace(i, j);
 
@@ -520,14 +409,14 @@ public class AlignSeq
       else if (trace == 1)
       {
         j--;
-        aseq1[count] = defInt;
-        astr1 = "-" + astr1.substring(1);
+        aseq1[count] = GAP_INDEX;
+        sb1.replace(sb1.length() - 1, sb1.length(), "-");
       }
       else if (trace == -1)
       {
         i--;
-        aseq2[count] = defInt;
-        astr2 = "-" + astr2.substring(1);
+        aseq2[count] = GAP_INDEX;
+        sb2.replace(sb2.length() - 1, sb2.length(), "-");
       }
 
       count--;
@@ -536,17 +425,24 @@ public class AlignSeq
     seq1start = i + 1;
     seq2start = j + 1;
 
-    if (aseq1[count] != defInt)
+    if (aseq1[count] != GAP_INDEX)
     {
       aseq1[count] = seq1[i];
-      astr1 = s1str.charAt(i) + astr1;
+      sb1.append(s1str.charAt(i));
     }
 
-    if (aseq2[count] != defInt)
+    if (aseq2[count] != GAP_INDEX)
     {
       aseq2[count] = seq2[j];
-      astr2 = s2str.charAt(j) + astr2;
+      sb2.append(s2str.charAt(j));
     }
+
+    /*
+     * we built the character strings backwards, so now
+     * reverse them to convert to sequence strings
+     */
+    astr1 = sb1.reverse().toString();
+    astr2 = sb2.reverse().toString();
   }
 
   /**
@@ -599,6 +495,8 @@ public class AlignSeq
             .append(String.valueOf(s2str.length())).append(")")
             .append(NEWLINE).append(NEWLINE);
 
+    ScoreMatrix pam250 = ScoreModels.getInstance().getPam250();
+
     for (int j = 0; j < nochunks; j++)
     {
       // Print the first aligned sequence
@@ -615,25 +513,27 @@ public class AlignSeq
       output.append(NEWLINE);
       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
 
-      // Print out the matching chars
+      /*
+       * Print out the match symbols:
+       * | for exact match (ignoring case)
+       * . if PAM250 score is positive
+       * else a space
+       */
       for (int i = 0; i < len; i++)
       {
         if ((i + (j * len)) < astr1.length())
         {
-          boolean sameChar = Comparison.isSameResidue(
-                  astr1.charAt(i + (j * len)), astr2.charAt(i + (j * len)),
-                  false);
-          if (sameChar
-                  && !jalview.util.Comparison.isGap(astr1.charAt(i
-                          + (j * len))))
+          char c1 = astr1.charAt(i + (j * len));
+          char c2 = astr2.charAt(i + (j * len));
+          boolean sameChar = Comparison.isSameResidue(c1, c2, false);
+          if (sameChar && !Comparison.isGap(c1))
           {
             pid++;
             output.append("|");
           }
           else if (type.equals("pep"))
           {
-            if (ResidueProperties.getPAM250(astr1.charAt(i + (j * len)),
-                    astr2.charAt(i + (j * len))) > 0)
+            if (pam250.getPairwiseScore(c1, c2) > 0)
             {
               output.append(".");
             }
@@ -678,46 +578,6 @@ public class AlignSeq
   /**
    * DOCUMENT ME!
    * 
-   * @param mat
-   *          DOCUMENT ME!
-   */
-  public void printScoreMatrix(int[][] mat)
-  {
-    int n = seq1.length;
-    int m = seq2.length;
-
-    for (int i = 0; i < n; i++)
-    {
-      // Print the top sequence
-      if (i == 0)
-      {
-        Format.print(System.out, "%8s", s2str.substring(0, 1));
-
-        for (int jj = 1; jj < m; jj++)
-        {
-          Format.print(System.out, "%5s", s2str.substring(jj, jj + 1));
-        }
-
-        System.out.println();
-      }
-
-      for (int j = 0; j < m; j++)
-      {
-        if (j == 0)
-        {
-          Format.print(System.out, "%3s", s1str.substring(i, i + 1));
-        }
-
-        Format.print(System.out, "%3d ", mat[i][j] / 10);
-      }
-
-      System.out.println();
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
    * @param i
    *          DOCUMENT ME!
    * @param j
@@ -728,7 +588,10 @@ public class AlignSeq
   public int findTrace(int i, int j)
   {
     int t = 0;
-    int max = score[i - 1][j - 1] + (lookup[seq1[i]][seq2[j]] * 10);
+    // 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);
 
     if (F[i][j] > max)
     {
@@ -772,7 +635,8 @@ public class AlignSeq
     int m = seq2.length;
 
     // top left hand element
-    score[0][0] = lookup[seq1[0]][seq2[0]] * 10;
+    score[0][0] = scoreMatrix.getPairwiseScore(s1str.charAt(0),
+            s2str.charAt(0)) * 10;
     E[0][0] = -gapExtend;
     F[0][0] = 0;
 
@@ -783,7 +647,9 @@ public class AlignSeq
       E[0][j] = max(score[0][j - 1] - gapOpen, E[0][j - 1] - gapExtend);
       F[0][j] = -gapExtend;
 
-      score[0][j] = max(lookup[seq1[0]][seq2[j]] * 10, -gapOpen, -gapExtend);
+      float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(0),
+              s2str.charAt(j));
+      score[0][j] = max(pairwiseScore * 10, -gapOpen, -gapExtend);
 
       traceback[0][j] = 1;
     }
@@ -794,7 +660,9 @@ public class AlignSeq
       E[i][0] = -gapOpen;
       F[i][0] = max(score[i - 1][0] - gapOpen, F[i - 1][0] - gapExtend);
 
-      score[i][0] = max(lookup[seq1[i]][seq2[0]] * 10, E[i][0], F[i][0]);
+      float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(i),
+              s2str.charAt(0));
+      score[i][0] = max(pairwiseScore * 10, E[i][0], F[i][0]);
       traceback[i][0] = -1;
     }
 
@@ -806,8 +674,10 @@ public class AlignSeq
         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);
 
+        float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(i),
+                s2str.charAt(j));
         score[i][j] = max(score[i - 1][j - 1]
-                + (lookup[seq1[i]][seq2[j]] * 10), E[i][j], F[i][j]);
+                + (pairwiseScore * 10), E[i][j], F[i][j]);
         traceback[i][j] = findTrace(i, j);
       }
     }
@@ -843,27 +713,27 @@ public class AlignSeq
   /**
    * DOCUMENT ME!
    * 
-   * @param i1
+   * @param f1
    *          DOCUMENT ME!
-   * @param i2
+   * @param f2
    *          DOCUMENT ME!
-   * @param i3
+   * @param f3
    *          DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
-  public int max(int i1, int i2, int i3)
+  private static float max(float f1, float f2, float f3)
   {
-    int max = i1;
+    float max = f1;
 
-    if (i2 > i1)
+    if (f2 > f1)
     {
-      max = i2;
+      max = f2;
     }
 
-    if (i3 > max)
+    if (f3 > max)
     {
-      max = i3;
+      max = f3;
     }
 
     return max;
@@ -872,65 +742,44 @@ public class AlignSeq
   /**
    * DOCUMENT ME!
    * 
-   * @param i1
+   * @param f1
    *          DOCUMENT ME!
-   * @param i2
+   * @param f2
    *          DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
-  public int max(int i1, int i2)
+  private static float max(float f1, float f2)
   {
-    int max = i1;
+    float max = f1;
 
-    if (i2 > i1)
+    if (f2 > f1)
     {
-      max = i2;
+      max = f2;
     }
 
     return max;
   }
 
   /**
-   * DOCUMENT ME!
+   * Converts the character string to an array of integers which are the
+   * corresponding indices to the characters in the score matrix
    * 
    * @param s
-   *          DOCUMENT ME!
-   * @param type
-   *          DOCUMENT ME!
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
-  public int[] stringToInt(String s, String type)
+  int[] indexEncode(String s)
   {
-    int[] seq1 = new int[s.length()];
+    int[] encoded = new int[s.length()];
 
     for (int i = 0; i < s.length(); i++)
     {
-      // String ss = s.substring(i, i + 1).toUpperCase();
       char c = s.charAt(i);
-      if ('a' <= c && c <= 'z')
-      {
-        // TO UPPERCASE !!!
-        c -= ('a' - 'A');
-      }
-
-      try
-      {
-        seq1[i] = charToInt[c]; // set accordingly from setType
-        if (seq1[i] < 0 || seq1[i] > defInt) // set from setType: 23 for
-                                             // peptides, or 4 for NA.
-        {
-          seq1[i] = defInt;
-        }
-
-      } catch (Exception e)
-      {
-        seq1[i] = defInt;
-      }
+      encoded[i] = scoreMatrix.getMatrixIndex(c);
     }
 
-    return seq1;
+    return encoded;
   }
 
   /**
@@ -950,7 +799,7 @@ public class AlignSeq
   public static void displayMatrix(Graphics g, int[][] mat, int n, int m,
           int psize)
   {
-    // TODO method dosen't seem to be referenced anywhere delete??
+    // TODO method doesn't seem to be referenced anywhere delete??
     int max = -1000;
     int min = 1000;
 
@@ -1113,7 +962,7 @@ public class AlignSeq
       {
         SequenceI bestm = null;
         AlignSeq bestaseq = null;
-        int bestscore = 0;
+        float bestscore = 0;
         for (SequenceI msq : al.getSequences())
         {
           AlignSeq aseq = doGlobalNWAlignment(msq, sq, dnaOrProtein);
@@ -1124,8 +973,8 @@ public class AlignSeq
             bestm = msq;
           }
         }
-        System.out.println("Best Score for " + (matches.size() + 1) + " :"
-                + bestscore);
+        // System.out.println("Best Score for " + (matches.size() + 1) + " :"
+        // + bestscore);
         matches.add(bestm);
         aligns.add(bestaseq);
         al.deleteSequence(bestm);
@@ -1214,6 +1063,8 @@ public class AlignSeq
 
     // long start = System.currentTimeMillis();
 
+    SimilarityParams pidParams = new SimilarityParams(true, true, true,
+            true);
     float pid;
     String seqi, seqj;
     for (int i = 0; i < height; i++)
@@ -1254,7 +1105,7 @@ public class AlignSeq
             seqj = ug;
           }
         }
-        pid = Comparison.PID(seqi, seqj);
+        pid = (float) PIDModel.computePID(seqi, seqj, pidParams);
 
         // use real sequence length rather than string length
         if (lngth[j] < lngth[i])
index 59cdccf..9943a22 100755 (executable)
@@ -20,6 +20,8 @@
  */
 package jalview.analysis;
 
+import jalview.analysis.scoremodels.PIDModel;
+import jalview.analysis.scoremodels.SimilarityParams;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
@@ -27,12 +29,11 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.SequenceNode;
-import jalview.util.Comparison;
-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;
 
 /**
@@ -52,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)
    */
@@ -66,67 +67,49 @@ public class AlignmentSorter
 
   static boolean sortOrderAscending = true;
 
-  static NJTree lastTree = null;
+  static TreeModel lastTree = null;
 
   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;
 
   /**
-   * Sort by Percentage Identity w.r.t. s
+   * Sorts sequences in the alignment by Percentage Identity with the given
+   * reference sequence, sorting the highest identity to the top
    * 
    * @param align
    *          AlignmentI
    * @param s
    *          SequenceI
-   * @param tosort
-   *          sequences from align that are to be sorted.
-   */
-  public static void sortByPID(AlignmentI align, SequenceI s,
-          SequenceI[] tosort)
-  {
-    sortByPID(align, s, tosort, 0, -1);
-  }
-
-  /**
-   * Sort by Percentage Identity w.r.t. s
-   * 
-   * @param align
-   *          AlignmentI
-   * @param s
-   *          SequenceI
-   * @param tosort
-   *          sequences from align that are to be sorted.
-   * @param start
-   *          start column (0 for beginning
    * @param end
    */
-  public static void sortByPID(AlignmentI align, SequenceI s,
-          SequenceI[] tosort, int start, int end)
+  public static void sortByPID(AlignmentI align, SequenceI s)
   {
     int nSeq = align.getHeight();
 
     float[] scores = new float[nSeq];
     SequenceI[] seqs = new SequenceI[nSeq];
+    String refSeq = s.getSequenceAsString();
 
+    SimilarityParams pidParams = new SimilarityParams(true, true, true,
+            true);
     for (int i = 0; i < nSeq; i++)
     {
-      scores[i] = Comparison.PID(align.getSequenceAt(i)
-              .getSequenceAsString(), s.getSequenceAsString());
+      scores[i] = (float) PIDModel.computePID(align.getSequenceAt(i)
+              .getSequenceAsString(), refSeq, pidParams);
       seqs[i] = align.getSequenceAt(i);
     }
 
@@ -447,7 +430,7 @@ public class AlignmentSorter
    * @return DOCUMENT ME!
    */
   private static List<SequenceI> getOrderByTree(AlignmentI align,
-          NJTree tree)
+          TreeModel tree)
   {
     int nSeq = align.getHeight();
 
@@ -487,7 +470,7 @@ public class AlignmentSorter
    * @param tree
    *          tree which has
    */
-  public static void sortByTree(AlignmentI align, NJTree tree)
+  public static void sortByTree(AlignmentI align, TreeModel tree)
   {
     List<SequenceI> tmp = getOrderByTree(align, tree);
 
@@ -675,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
@@ -698,35 +681,6 @@ 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)
-   * 
-   * @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 alignment
-   *          - aligned sequences containing features
-   * @param method
-   *          - one of the string constants FEATURE_SCORE, FEATURE_LABEL,
-   *          FEATURE_DENSITY
-   */
-  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)
   {
@@ -748,51 +702,41 @@ public class AlignmentSorter
     return false;
   }
 
-  public static void sortByFeature(List<String> featureLabels,
-          List<String> groupLabels, int start, int stop,
+  /**
+   * 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 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
+   *          the alignment to be sorted
+   * @param method
+   *          either "average_score" or "density" ("text" not yet implemented)
+   */
+  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"));
-    }
-
-    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);
-      }
+      String msg = String
+              .format("Implementation Error - sortByFeature method must be either '%s' or '%s'",
+                      FEATURE_SCORE, FEATURE_DENSITY);
+      System.err.println(msg);
+      return;
     }
 
-    /*
-     * 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();
 
@@ -801,52 +745,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
-        if (
-        // ignore features outwith alignment start-stop positions.
-        (sf[f].end < sstart || sf[f].begin > sstop) ||
-        // or ignore based on selection criteria
-                (featureLabels != null && !AlignmentSorter
-                        .containsIgnoreCase(sf[f].type, featureLabels))
-                || (groupLabels != null
-                // problem here: we cannot eliminate null feature group features
-                && (sf[f].getFeatureGroup() != null && !AlignmentSorter
-                        .containsIgnoreCase(sf[f].getFeatureGroup(),
-                                groupLabels))))
+        SequenceFeature sf = it.next();
+
+        /*
+         * accept all features with null or empty group, otherwise
+         * check group is one of the currently visible groups
+         */
+        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(sf[f].getScore()))
+          float score = sf.getScore();
+          if (FEATURE_SCORE.equals(method) && !Float.isNaN(score))
           {
             if (seqScores[i] == 0)
             {
@@ -854,33 +790,26 @@ public class AlignmentSorter
             }
             seqScores[i]++;
             hasScore[i] = true;
-            scores[i] += sf[f].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])
@@ -890,23 +819,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)
       {
@@ -931,9 +855,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++)
       {
@@ -943,18 +867,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 fbec4be..69ac947 100644 (file)
@@ -35,14 +35,14 @@ 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.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 +51,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;
@@ -61,6 +60,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.SortedMap;
 import java.util.TreeMap;
 
 /**
@@ -2054,11 +2054,11 @@ 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,
@@ -2070,75 +2070,74 @@ 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;
   }
@@ -2203,49 +2202,44 @@ public class AlignmentUtils
   public static List<int[]> findCdsPositions(SequenceI dnaSeq)
   {
     List<int[]> result = new ArrayList<int[]>();
-    SequenceFeature[] sfs = dnaSeq.getSequenceFeatures();
-    if (sfs == null)
+
+    List<SequenceFeature> sfs = dnaSeq.getFeatures().getFeaturesByOntology(
+            SequenceOntologyI.CDS);
+    if (sfs.isEmpty())
     {
       return result;
     }
-
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    SequenceFeatures.sortFeatures(sfs, true);
     int startPhase = 0;
 
     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())
       {
-        int phase = 0;
-        try
+        begin += phase;
+        if (begin > end)
         {
-          phase = Integer.parseInt(sf.getPhase());
-        } catch (NumberFormatException e)
-        {
-          // ignore
-        }
-        /*
-         * 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());
-          }
+          // shouldn't happen!
+          System.err
+                  .println("Error: start phase extends beyond start CDS in "
+                          + dnaSeq.getName());
         }
-        result.add(new int[] { begin, end });
       }
+      result.add(new int[] { begin, end });
     }
 
     /*
@@ -2265,7 +2259,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;
   }
 
@@ -2318,24 +2312,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;
   }
 
@@ -2463,10 +2439,9 @@ public class AlignmentUtils
       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());
+              peptidePos, var.getSource());
       StringBuilder attributes = new StringBuilder(32);
       String id = (String) var.variant.getValue(ID);
       if (id != null)
@@ -2526,10 +2501,10 @@ public class AlignmentUtils
      * LinkedHashMap ensures we keep the peptide features in sequence order
      */
     LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<Integer, List<DnaVariant>[]>();
-    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
 
-    SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures();
-    if (dnaFeatures == null)
+    List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
+            .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
+    if (dnaFeatures.isEmpty())
     {
       return variants;
     }
@@ -2549,84 +2524,80 @@ public class AlignmentUtils
         // not handling multi-locus variant features
         continue;
       }
-      if (so.isA(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT))
+      int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
+      if (mapsTo == 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);
-        }
+        // 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);
+      }
 
-        /*
-         * extract dna variants to a string array
-         */
-        String alls = (String) sf.getValue("alleles");
-        if (alls == null)
-        {
-          continue;
-        }
-        String[] alleles = alls.toUpperCase().split(",");
-        int i = 0;
-        for (String allele : alleles)
-        {
-          alleles[i++] = allele.trim(); // lose any space characters "A, G"
-        }
+      /*
+       * extract dna variants to a string array
+       */
+      String alls = (String) sf.getValue("alleles");
+      if (alls == null)
+      {
+        continue;
+      }
+      String[] alleles = alls.toUpperCase().split(",");
+      int i = 0;
+      for (String allele : alleles)
+      {
+        alleles[i++] = allele.trim(); // lose any space characters "A, G"
+      }
 
-        /*
-         * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
-         */
-        int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
-                : MappingUtils.flattenRanges(dnaToProtein.locateInFrom(
-                        peptidePosition, peptidePosition));
-        lastPeptidePostion = peptidePosition;
-        lastCodon = codon;
+      /*
+       * 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++)
+      /*
+       * 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;
@@ -2834,7 +2805,7 @@ public class AlignmentUtils
    * @param unmapped
    * @return
    */
-  static Map<Integer, Map<SequenceI, Character>> buildMappedColumnsMap(
+  static SortedMap<Integer, Map<SequenceI, Character>> buildMappedColumnsMap(
           AlignmentI unaligned, AlignmentI aligned, List<SequenceI> unmapped)
   {
     /*
@@ -2842,7 +2813,7 @@ public class AlignmentUtils
      * {unalignedSequence, characterPerSequence} at that position.
      * TreeMap keeps the entries in ascending column order. 
      */
-    Map<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
+    SortedMap<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
 
     /*
      * record any sequences that have no mapping so can't be realigned
diff --git a/src/jalview/analysis/AverageDistanceTree.java b/src/jalview/analysis/AverageDistanceTree.java
new file mode 100644 (file)
index 0000000..907109e
--- /dev/null
@@ -0,0 +1,121 @@
+package jalview.analysis;
+
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.SequenceNode;
+import jalview.viewmodel.AlignmentViewport;
+
+/**
+ * This class implements distance calculations used in constructing a Average
+ * Distance tree (also known as UPGMA)
+ */
+public class AverageDistanceTree extends TreeBuilder
+{
+  /**
+   * Constructor
+   * 
+   * @param av
+   * @param sm
+   * @param scoreParameters
+   */
+  public AverageDistanceTree(AlignmentViewport av, ScoreModelI sm,
+          SimilarityParamsI scoreParameters)
+  {
+    super(av, sm, scoreParameters);
+  }
+
+  /**
+   * Calculates and saves the distance between the combination of cluster(i) and
+   * cluster(j) and all other clusters. An average of the distances from
+   * cluster(i) and cluster(j) is calculated, weighted by the sizes of each
+   * cluster.
+   * 
+   * @param i
+   * @param j
+   */
+  @Override
+  protected void findClusterDistance(int i, int j)
+  {
+    int noi = clusters.elementAt(i).cardinality();
+    int noj = clusters.elementAt(j).cardinality();
+
+    // New distances from cluster i to others
+    double[] newdist = new double[noseqs];
+
+    for (int l = 0; l < noseqs; l++)
+    {
+      if ((l != i) && (l != j))
+      {
+        newdist[l] = ((distances.getValue(i, l) * noi) + (distances
+                .getValue(j, l) * noj)) / (noi + noj);
+      }
+      else
+      {
+        newdist[l] = 0;
+      }
+    }
+
+    for (int ii = 0; ii < noseqs; ii++)
+    {
+      distances.setValue(i, ii, newdist[ii]);
+      distances.setValue(ii, i, newdist[ii]);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected double findMinDistance()
+  {
+    double min = Double.MAX_VALUE;
+
+    for (int i = 0; i < (noseqs - 1); i++)
+    {
+      for (int j = i + 1; j < noseqs; j++)
+      {
+        if (!done.get(i) && !done.get(j))
+        {
+          if (distances.getValue(i, j) < min)
+          {
+            mini = i;
+            minj = j;
+
+            min = distances.getValue(i, j);
+          }
+        }
+      }
+    }
+    return min;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected void findNewDistances(SequenceNode nodei, SequenceNode nodej,
+          double dist)
+  {
+    double ih = 0;
+    double jh = 0;
+
+    SequenceNode sni = nodei;
+    SequenceNode snj = nodej;
+
+    while (sni != null)
+    {
+      ih = ih + sni.dist;
+      sni = (SequenceNode) sni.left();
+    }
+
+    while (snj != null)
+    {
+      jh = jh + snj.dist;
+      snj = (SequenceNode) snj.left();
+    }
+
+    nodei.dist = ((dist / 2) - ih);
+    nodej.dist = ((dist / 2) - jh);
+  }
+
+}
index 7b9da46..2b5a8f6 100755 (executable)
@@ -20,6 +20,8 @@
  */
 package jalview.analysis;
 
+import jalview.analysis.scoremodels.ScoreMatrix;
+import jalview.analysis.scoremodels.ScoreModels;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ResidueCount;
@@ -33,6 +35,7 @@ import java.awt.Color;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.Vector;
 
@@ -49,14 +52,19 @@ public class Conservation
 
   private static final int TOUPPERCASE = 'a' - 'A';
 
+  private static final int GAP_INDEX = -1;
+
   SequenceI[] sequences;
 
   int start;
 
   int end;
 
-  Vector<int[]> seqNums; // vector of int vectors where first is sequence
-                         // checksum
+  /*
+   * a list whose i'th element is an array whose first entry is the checksum
+   * of the i'th sequence, followed by residues encoded to score matrix index
+   */
+  Vector<int[]> seqNums;
 
   int maxLength = 0; // used by quality calcs
 
@@ -69,17 +77,17 @@ public class Conservation
    */
   Map<String, Integer>[] total;
 
-  boolean canonicaliseAa = true; // if true then conservation calculation will
-
-  // map all symbols to canonical aa numbering
-  // rather than consider conservation of that
-  // symbol
+  /*
+   * if true then conservation calculation will map all symbols to canonical aa
+   * numbering rather than consider conservation of that symbol
+   */
+  boolean canonicaliseAa = true;
 
-  /** Stores calculated quality values */
   private Vector<Double> quality;
 
-  /** Stores maximum and minimum values of quality values */
-  private double[] qualityRange = new double[2];
+  private double qualityMinimum;
+
+  private double qualityMaximum;
 
   private Sequence consSequence;
 
@@ -90,8 +98,16 @@ public class Conservation
 
   private String name = "";
 
+  /*
+   * an array, for each column, of counts of symbols (by score matrix index)
+   */
   private int[][] cons2;
 
+  /*
+   * gap counts for each column
+   */
+  private int[] cons2GapCounts;
+
   private String[] consSymbs;
 
   /**
@@ -161,27 +177,29 @@ public class Conservation
   }
 
   /**
-   * Translate sequence i into a numerical representation and store it in the
-   * i'th position of the seqNums array.
+   * Translate sequence i into score matrix indices and store it in the i'th
+   * position of the seqNums array.
    * 
    * @param i
+   * @param sm
    */
-  private void calcSeqNum(int i)
+  private void calcSeqNum(int i, ScoreMatrix sm)
   {
-    String sq = null; // for dumb jbuilder not-inited exception warning
-    int[] sqnum = null;
-
     int sSize = sequences.length;
 
     if ((i > -1) && (i < sSize))
     {
-      sq = sequences[i].getSequenceAsString();
+      String sq = sequences[i].getSequenceAsString();
 
       if (seqNums.size() <= i)
       {
         seqNums.addElement(new int[sq.length() + 1]);
       }
 
+      /*
+       * the first entry in the array is the sequence's hashcode,
+       * following entries are matrix indices of sequence characters
+       */
       if (sq.hashCode() != seqNums.elementAt(i)[0])
       {
         int j;
@@ -194,14 +212,26 @@ public class Conservation
           maxLength = len;
         }
 
-        sqnum = new int[len + 1]; // better to always make a new array -
+        int[] sqnum = new int[len + 1]; // better to always make a new array -
         // sequence can change its length
         sqnum[0] = sq.hashCode();
 
         for (j = 1; j <= len; j++)
         {
-          sqnum[j] = jalview.schemes.ResidueProperties.aaIndex[sq
-                  .charAt(j - 1)];
+          // sqnum[j] = ResidueProperties.aaIndex[sq.charAt(j - 1)];
+          char residue = sq.charAt(j - 1);
+          if (Comparison.isGap(residue))
+          {
+            sqnum[j] = GAP_INDEX;
+          }
+          else
+          {
+            sqnum[j] = sm.getMatrixIndex(residue);
+            if (sqnum[j] == -1)
+            {
+              sqnum[j] = GAP_INDEX;
+            }
+          }
         }
 
         seqNums.setElementAt(sqnum, i);
@@ -243,7 +273,7 @@ public class Conservation
        * or not conserved (-1)
        * Using TreeMap means properties are displayed in alphabetical order
        */
-      Map<String, Integer> resultHash = new TreeMap<String, Integer>();
+      SortedMap<String, Integer> resultHash = new TreeMap<String, Integer>();
       SymbolCounts symbolCounts = values.getSymbolCounts();
       char[] symbols = symbolCounts.symbols;
       int[] counts = symbolCounts.values;
@@ -518,7 +548,7 @@ public class Conservation
    * 
    * @return Conservation sequence
    */
-  public Sequence getConsSequence()
+  public SequenceI getConsSequence()
   {
     return consSequence;
   }
@@ -526,137 +556,133 @@ public class Conservation
   // From Alignment.java in jalview118
   public void findQuality()
   {
-    findQuality(0, maxLength - 1);
+    findQuality(0, maxLength - 1, ScoreModels.getInstance().getBlosum62());
   }
 
   /**
    * DOCUMENT ME!
+   * 
+   * @param sm
    */
-  private void percentIdentity2()
+  private void percentIdentity(ScoreMatrix sm)
   {
     seqNums = new Vector<int[]>();
-    // calcSeqNum(s);
     int i = 0, iSize = sequences.length;
     // Do we need to calculate this again?
     for (i = 0; i < iSize; i++)
     {
-      calcSeqNum(i);
+      calcSeqNum(i, sm);
     }
 
     if ((cons2 == null) || seqNumsChanged)
     {
+      // FIXME remove magic number 24 without changing calc
+      // sm.getSize() returns 25 so doesn't quite do it...
       cons2 = new int[maxLength][24];
+      cons2GapCounts = new int[maxLength];
 
-      // Initialize the array
-      for (int j = 0; j < 24; j++)
-      {
-        for (i = 0; i < maxLength; i++)
-        {
-          cons2[i][j] = 0;
-        }
-      }
-
-      int[] sqnum;
       int j = 0;
 
       while (j < sequences.length)
       {
-        sqnum = seqNums.elementAt(j);
+        int[] sqnum = seqNums.elementAt(j);
 
         for (i = 1; i < sqnum.length; i++)
         {
-          cons2[i - 1][sqnum[i]]++;
+          int index = sqnum[i];
+          if (index == GAP_INDEX)
+          {
+            cons2GapCounts[i - 1]++;
+          }
+          else
+          {
+            cons2[i - 1][index]++;
+          }
         }
 
+        // TODO should this start from sqnum.length?
         for (i = sqnum.length - 1; i < maxLength; i++)
         {
-          cons2[i][23]++; // gap count
+          cons2GapCounts[i]++;
         }
-
         j++;
       }
-
-      // unnecessary ?
-
-      /*
-       * for (int i=start; i <= end; i++) { int max = -1000; int maxi = -1; int
-       * maxj = -1;
-       * 
-       * for (int j=0;j<24;j++) { if (cons2[i][j] > max) { max = cons2[i][j];
-       * maxi = i; maxj = j; } } }
-       */
     }
   }
 
   /**
-   * Calculates the quality of the set of sequences
+   * Calculates the quality of the set of sequences over the given inclusive
+   * column range, using the specified substitution score matrix
    * 
-   * @param startRes
-   *          Start residue
-   * @param endRes
-   *          End residue
+   * @param startCol
+   * @param endCol
+   * @param scoreMatrix
    */
-  public void findQuality(int startRes, int endRes)
+  protected void findQuality(int startCol, int endCol, ScoreMatrix scoreMatrix)
   {
     quality = new Vector<Double>();
 
-    double max = -10000;
-    int[][] BLOSUM62 = ResidueProperties.getBLOSUM62();
+    double max = -Double.MAX_VALUE;
+    float[][] scores = scoreMatrix.getMatrix();
 
-    // Loop over columns // JBPNote Profiling info
-    // long ts = System.currentTimeMillis();
-    // long te = System.currentTimeMillis();
-    percentIdentity2();
+    percentIdentity(scoreMatrix);
 
     int size = seqNums.size();
     int[] lengths = new int[size];
-    double tot, bigtot, sr, tmp;
-    double[] x, xx;
-    int l, j, i, ii, i2, k, seqNum;
 
-    for (l = 0; l < size; l++)
+    for (int l = 0; l < size; l++)
     {
       lengths[l] = seqNums.elementAt(l).length - 1;
     }
 
-    for (j = startRes; j <= endRes; j++)
+    final int symbolCount = scoreMatrix.getSize();
+
+    for (int j = startCol; j <= endCol; j++)
     {
-      bigtot = 0;
+      double bigtot = 0;
 
       // First Xr = depends on column only
-      x = new double[24];
+      double[] x = new double[symbolCount];
 
-      for (ii = 0; ii < 24; ii++)
+      for (int ii = 0; ii < symbolCount; ii++)
       {
         x[ii] = 0;
 
-        for (i2 = 0; i2 < 24; i2++)
+        /*
+         * todo JAL-728 currently assuming last symbol in matrix is * for gap
+         * (which we ignore as counted separately); true for BLOSUM62 but may
+         * not be once alternative matrices are supported
+         */
+        for (int i2 = 0; i2 < symbolCount - 1; i2++)
         {
-          x[ii] += (((double) cons2[j][i2] * BLOSUM62[ii][i2]) + 4);
+          x[ii] += (((double) cons2[j][i2] * scores[ii][i2]) + 4D);
         }
+        x[ii] += 4D + cons2GapCounts[j] * scoreMatrix.getMinimumScore();
 
         x[ii] /= size;
       }
 
       // Now calculate D for each position and sum
-      for (k = 0; k < size; k++)
+      for (int k = 0; k < size; k++)
       {
-        tot = 0;
-        xx = new double[24];
-        seqNum = (j < lengths[k]) ? seqNums.elementAt(k)[j + 1] : 23; // Sequence,
-                                                                      // or gap
-                                                                      // at the
-                                                                      // end
-
-        // This is a loop over r
-        for (i = 0; i < 23; i++)
-        {
-          sr = 0;
+        double tot = 0;
+        double[] xx = new double[symbolCount];
+        // sequence character index, or implied gap if sequence too short
+        int seqNum = (j < lengths[k]) ? seqNums.elementAt(k)[j + 1]
+                : GAP_INDEX;
 
-          sr = (double) BLOSUM62[i][seqNum] + 4;
+        for (int i = 0; i < symbolCount - 1; i++)
+        {
+          double sr = 4D;
+          if (seqNum == GAP_INDEX)
+          {
+            sr += scoreMatrix.getMinimumScore();
+          }
+          else
+          {
+            sr += scores[i][seqNum];
+          }
 
-          // Calculate X with another loop over residues
-          // System.out.println("Xi " + i + " " + x[i] + " " + sr);
           xx[i] = x[i] - sr;
 
           tot += (xx[i] * xx[i]);
@@ -665,24 +691,18 @@ public class Conservation
         bigtot += Math.sqrt(tot);
       }
 
-      // This is the quality for one column
-      if (max < bigtot)
-      {
-        max = bigtot;
-      }
+      max = Math.max(max, bigtot);
 
-      // bigtot = bigtot * (size-cons2[j][23])/size;
       quality.addElement(new Double(bigtot));
-
-      // Need to normalize by gaps
     }
 
-    double newmax = -10000;
+    double newmax = -Double.MAX_VALUE;
 
-    for (j = startRes; j <= endRes; j++)
+    for (int j = startCol; j <= endCol; j++)
     {
-      tmp = quality.elementAt(j).doubleValue();
-      tmp = ((max - tmp) * (size - cons2[j][23])) / size;
+      double tmp = quality.elementAt(j).doubleValue();
+      // tmp = ((max - tmp) * (size - cons2[j][23])) / size;
+      tmp = ((max - tmp) * (size - cons2GapCounts[j])) / size;
 
       // System.out.println(tmp+ " " + j);
       quality.setElementAt(new Double(tmp), j);
@@ -693,9 +713,8 @@ public class Conservation
       }
     }
 
-    // System.out.println("Quality " + s);
-    qualityRange[0] = 0D;
-    qualityRange[1] = newmax;
+    qualityMinimum = 0D;
+    qualityMaximum = newmax;
   }
 
   /**
@@ -745,14 +764,14 @@ public class Conservation
 
     if (quality2 != null)
     {
-      quality2.graphMax = (float) qualityRange[1];
+      quality2.graphMax = (float) qualityMaximum;
       if (quality2.annotations != null
               && quality2.annotations.length < alWidth)
       {
         quality2.annotations = new Annotation[alWidth];
       }
-      qmin = (float) qualityRange[0];
-      qmax = (float) qualityRange[1];
+      qmin = (float) qualityMinimum;
+      qmax = (float) qualityMaximum;
     }
 
     for (int i = istart; i < alWidth; i++)
index 4ba7e41..103025c 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)
index 799a8ed..2106dc2 100644 (file)
@@ -45,7 +45,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 
 public class Dna
 {
@@ -685,7 +684,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.
@@ -754,25 +753,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)
@@ -786,24 +775,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))
-            {
 
-            }
           }
         }
-      }
     }
   }
 
index e0e50fb..487e85e 100644 (file)
 package jalview.analysis;
 
 import jalview.api.analysis.ScoreModelI;
-import jalview.datamodel.AlignmentView;
-import jalview.datamodel.BinaryNode;
-import jalview.datamodel.CigarArray;
-import jalview.datamodel.NodeTransformI;
-import jalview.datamodel.SeqCigar;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceI;
+import jalview.api.analysis.SimilarityParamsI;
 import jalview.datamodel.SequenceNode;
-import jalview.io.NewickFile;
-import jalview.schemes.ResidueProperties;
-
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Vector;
+import jalview.viewmodel.AlignmentViewport;
 
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * This class implements distance calculations used in constructing a Neighbour
+ * Joining tree
  */
-public class NJTree
+public class NJTree extends TreeBuilder
 {
-  Vector<Cluster> cluster;
-
-  SequenceI[] sequence;
-
-  // SequenceData is a string representation of what the user
-  // sees. The display may contain hidden columns.
-  public AlignmentView seqData = null;
-
-  int[] done;
-
-  int noseqs;
-
-  int noClus;
-
-  float[][] distance;
-
-  int mini;
-
-  int minj;
-
-  float ri;
-
-  float rj;
-
-  Vector<SequenceNode> groups = new Vector<SequenceNode>();
-
-  SequenceNode maxdist;
-
-  SequenceNode top;
-
-  float maxDistValue;
-
-  float maxheight;
-
-  int ycount;
-
-  Vector<SequenceNode> node;
-
-  String type;
-
-  String pwtype;
-
-  Object found = null;
-
-  boolean hasDistances = true; // normal case for jalview trees
-
-  boolean hasBootstrap = false; // normal case for jalview trees
-
-  private boolean hasRootDistance = true;
-
-  /**
-   * Create a new NJTree object with leaves associated with sequences in seqs,
-   * and original alignment data represented by Cigar strings.
-   * 
-   * @param seqs
-   *          SequenceI[]
-   * @param odata
-   *          Cigar[]
-   * @param treefile
-   *          NewickFile
-   */
-  public NJTree(SequenceI[] seqs, AlignmentView odata, NewickFile treefile)
-  {
-    this(seqs, treefile);
-    if (odata != null)
-    {
-      seqData = odata;
-    }
-    /*
-     * sequenceString = new String[odata.length]; char gapChar =
-     * jalview.util.Comparison.GapChars.charAt(0); for (int i = 0; i <
-     * odata.length; i++) { SequenceI oseq_aligned = odata[i].getSeq(gapChar);
-     * sequenceString[i] = oseq_aligned.getSequence(); }
-     */
-  }
-
-  /**
-   * Creates a new NJTree object from a tree from an external source
-   * 
-   * @param seqs
-   *          SequenceI which should be associated with leafs of treefile
-   * @param treefile
-   *          A parsed tree
-   */
-  public NJTree(SequenceI[] seqs, NewickFile treefile)
-  {
-    this.sequence = seqs;
-    top = treefile.getTree();
-
-    /**
-     * There is no dependent alignment to be recovered from an imported tree.
-     * 
-     * if (sequenceString == null) { sequenceString = new String[seqs.length];
-     * for (int i = 0; i < seqs.length; i++) { sequenceString[i] =
-     * seqs[i].getSequence(); } }
-     */
-
-    hasDistances = treefile.HasDistances();
-    hasBootstrap = treefile.HasBootstrap();
-    hasRootDistance = treefile.HasRootDistance();
-
-    maxheight = findHeight(top);
-
-    SequenceIdMatcher algnIds = new SequenceIdMatcher(seqs);
-
-    Vector<SequenceNode> leaves = findLeaves(top);
-
-    int i = 0;
-    int namesleft = seqs.length;
-
-    SequenceNode j;
-    SequenceI nam;
-    String realnam;
-    Vector<SequenceI> one2many = new Vector<SequenceI>();
-    int countOne2Many = 0;
-    while (i < leaves.size())
-    {
-      j = leaves.elementAt(i++);
-      realnam = j.getName();
-      nam = null;
-
-      if (namesleft > -1)
-      {
-        nam = algnIds.findIdMatch(realnam);
-      }
-
-      if (nam != null)
-      {
-        j.setElement(nam);
-        if (one2many.contains(nam))
-        {
-          countOne2Many++;
-          // if (jalview.bin.Cache.log.isDebugEnabled())
-          // jalview.bin.Cache.log.debug("One 2 many relationship for
-          // "+nam.getName());
-        }
-        else
-        {
-          one2many.addElement(nam);
-          namesleft--;
-        }
-      }
-      else
-      {
-        j.setElement(new Sequence(realnam, "THISISAPLACEHLDER"));
-        j.setPlaceholder(true);
-      }
-    }
-    // if (jalview.bin.Cache.log.isDebugEnabled() && countOne2Many>0) {
-    // jalview.bin.Cache.log.debug("There were "+countOne2Many+" alignment
-    // sequence ids (out of "+one2many.size()+" unique ids) linked to two or
-    // more leaves.");
-    // }
-    // one2many.clear();
-  }
-
   /**
-   * Creates a new NJTree object.
+   * Constructor given a viewport, tree type and score model
    * 
-   * @param sequence
-   *          DOCUMENT ME!
-   * @param type
-   *          DOCUMENT ME!
-   * @param pwtype
-   *          DOCUMENT ME!
-   * @param start
-   *          DOCUMENT ME!
-   * @param end
-   *          DOCUMENT ME!
+   * @param av
+   *          the current alignment viewport
+   * @param sm
+   *          a distance or similarity score model to use to compute the tree
+   * @param scoreParameters
    */
-  public NJTree(SequenceI[] sequence, AlignmentView seqData, String type,
-          String pwtype, ScoreModelI sm, int start, int end)
+  public NJTree(AlignmentViewport av, ScoreModelI sm,
+          SimilarityParamsI scoreParameters)
   {
-    this.sequence = sequence;
-    this.node = new Vector<SequenceNode>();
-    this.type = type;
-    this.pwtype = pwtype;
-    if (seqData != null)
-    {
-      this.seqData = seqData;
-    }
-    else
-    {
-      SeqCigar[] seqs = new SeqCigar[sequence.length];
-      for (int i = 0; i < sequence.length; i++)
-      {
-        seqs[i] = new SeqCigar(sequence[i], start, end);
-      }
-      CigarArray sdata = new CigarArray(seqs);
-      sdata.addOperation(CigarArray.M, end - start + 1);
-      this.seqData = new AlignmentView(sdata, start);
-    }
-    // System.err.println("Made seqData");// dbg
-    if (!(type.equals("NJ")))
-    {
-      type = "AV";
-    }
-
-    if (sm == null && !(pwtype.equals("PID")))
-    {
-      if (ResidueProperties.getScoreMatrix(pwtype) == null)
-      {
-        pwtype = "BLOSUM62";
-      }
-    }
-
-    int i = 0;
-
-    done = new int[sequence.length];
-
-    while ((i < sequence.length) && (sequence[i] != null))
-    {
-      done[i] = 0;
-      i++;
-    }
-
-    noseqs = i++;
-
-    distance = findDistances(sm);
-    // System.err.println("Made distances");// dbg
-    makeLeaves();
-    // System.err.println("Made leaves");// dbg
-
-    noClus = cluster.size();
-
-    cluster();
-    // System.err.println("Made clusters");// dbg
-
+    super(av, sm, scoreParameters);
   }
 
   /**
-   * Generate a string representation of the Tree
-   * 
-   * @return Newick File with all tree data available
+   * {@inheritDoc}
    */
   @Override
-  public String toString()
-  {
-    jalview.io.NewickFile fout = new jalview.io.NewickFile(getTopNode());
-
-    return fout.print(isHasBootstrap(), isHasDistances(),
-            isHasRootDistance()); // output all data available for tree
-  }
-
-  /**
-   * 
-   * used when the alignment associated to a tree has changed.
-   * 
-   * @param list
-   *          Sequence set to be associated with tree nodes
-   */
-  public void UpdatePlaceHolders(List<SequenceI> list)
+  protected double findMinDistance()
   {
-    Vector<SequenceNode> leaves = findLeaves(top);
+    double min = Double.MAX_VALUE;
 
-    int sz = leaves.size();
-    SequenceIdMatcher seqmatcher = null;
-    int i = 0;
-
-    while (i < sz)
+    for (int i = 0; i < (noseqs - 1); i++)
     {
-      SequenceNode leaf = leaves.elementAt(i++);
-
-      if (list.contains(leaf.element()))
-      {
-        leaf.setPlaceholder(false);
-      }
-      else
+      for (int j = i + 1; j < noseqs; j++)
       {
-        if (seqmatcher == null)
+        if (!done.get(i) && !done.get(j))
         {
-          // Only create this the first time we need it
-          SequenceI[] seqs = new SequenceI[list.size()];
+          double tmp = distances.getValue(i, j)
+                  - (findr(i, j) + findr(j, i));
 
-          for (int j = 0; j < seqs.length; j++)
+          if (tmp < min)
           {
-            seqs[j] = list.get(j);
-          }
-
-          seqmatcher = new SequenceIdMatcher(seqs);
-        }
-
-        SequenceI nam = seqmatcher.findIdMatch(leaf.getName());
+            mini = i;
+            minj = j;
 
-        if (nam != null)
-        {
-          if (!leaf.isPlaceholder())
-          {
-            // remapping the node to a new sequenceI - should remove any refs to
-            // old one.
-            // TODO - make many sequenceI to one leaf mappings possible!
-            // (JBPNote)
-          }
-          leaf.setPlaceholder(false);
-          leaf.setElement(nam);
-        }
-        else
-        {
-          if (!leaf.isPlaceholder())
-          {
-            // Construct a new placeholder sequence object for this leaf
-            leaf.setElement(new Sequence(leaf.getName(),
-                    "THISISAPLACEHLDER"));
+            min = tmp;
           }
-          leaf.setPlaceholder(true);
-
-        }
-      }
-    }
-  }
-
-  /**
-   * rename any nodes according to their associated sequence. This will modify
-   * the tree's metadata! (ie the original NewickFile or newly generated
-   * BinaryTree's label data)
-   */
-  public void renameAssociatedNodes()
-  {
-    applyToNodes(new NodeTransformI()
-    {
-
-      @Override
-      public void transform(BinaryNode nd)
-      {
-        Object el = nd.element();
-        if (el != null && el instanceof SequenceI)
-        {
-          nd.setName(((SequenceI) el).getName());
         }
       }
-    });
-  }
-
-  /**
-   * DOCUMENT ME!
-   */
-  public void cluster()
-  {
-    while (noClus > 2)
-    {
-      if (type.equals("NJ"))
-      {
-        findMinNJDistance();
-      }
-      else
-      {
-        findMinDistance();
-      }
-
-      Cluster c = joinClusters(mini, minj);
-
-      done[minj] = 1;
-
-      cluster.setElementAt(null, minj);
-      cluster.setElementAt(c, mini);
-
-      noClus--;
-    }
-
-    boolean onefound = false;
-
-    int one = -1;
-    int two = -1;
-
-    for (int i = 0; i < noseqs; i++)
-    {
-      if (done[i] != 1)
-      {
-        if (onefound == false)
-        {
-          two = i;
-          onefound = true;
-        }
-        else
-        {
-          one = i;
-        }
-      }
-    }
-
-    joinClusters(one, two);
-    top = (node.elementAt(one));
-
-    reCount(top);
-    findHeight(top);
-    findMaxDist(top);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param i
-   *          DOCUMENT ME!
-   * @param j
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public Cluster joinClusters(int i, int j)
-  {
-    float dist = distance[i][j];
-
-    int noi = cluster.elementAt(i).value.length;
-    int noj = cluster.elementAt(j).value.length;
-
-    int[] value = new int[noi + noj];
-
-    for (int ii = 0; ii < noi; ii++)
-    {
-      value[ii] = cluster.elementAt(i).value[ii];
-    }
-
-    for (int ii = noi; ii < (noi + noj); ii++)
-    {
-      value[ii] = cluster.elementAt(j).value[ii - noi];
-    }
-
-    Cluster c = new Cluster(value);
-
-    ri = findr(i, j);
-    rj = findr(j, i);
-
-    if (type.equals("NJ"))
-    {
-      findClusterNJDistance(i, j);
-    }
-    else
-    {
-      findClusterDistance(i, j);
-    }
-
-    SequenceNode sn = new SequenceNode();
-
-    sn.setLeft((node.elementAt(i)));
-    sn.setRight((node.elementAt(j)));
-
-    SequenceNode tmpi = (node.elementAt(i));
-    SequenceNode tmpj = (node.elementAt(j));
-
-    if (type.equals("NJ"))
-    {
-      findNewNJDistances(tmpi, tmpj, dist);
-    }
-    else
-    {
-      findNewDistances(tmpi, tmpj, dist);
-    }
-
-    tmpi.setParent(sn);
-    tmpj.setParent(sn);
-
-    node.setElementAt(sn, i);
-
-    return c;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param tmpi
-   *          DOCUMENT ME!
-   * @param tmpj
-   *          DOCUMENT ME!
-   * @param dist
-   *          DOCUMENT ME!
-   */
-  public void findNewNJDistances(SequenceNode tmpi, SequenceNode tmpj,
-          float dist)
-  {
-
-    tmpi.dist = ((dist + ri) - rj) / 2;
-    tmpj.dist = (dist - tmpi.dist);
-
-    if (tmpi.dist < 0)
-    {
-      tmpi.dist = 0;
-    }
-
-    if (tmpj.dist < 0)
-    {
-      tmpj.dist = 0;
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param tmpi
-   *          DOCUMENT ME!
-   * @param tmpj
-   *          DOCUMENT ME!
-   * @param dist
-   *          DOCUMENT ME!
-   */
-  public void findNewDistances(SequenceNode tmpi, SequenceNode tmpj,
-          float dist)
-  {
-    float ih = 0;
-    float jh = 0;
-
-    SequenceNode sni = tmpi;
-    SequenceNode snj = tmpj;
-
-    while (sni != null)
-    {
-      ih = ih + sni.dist;
-      sni = (SequenceNode) sni.left();
-    }
-
-    while (snj != null)
-    {
-      jh = jh + snj.dist;
-      snj = (SequenceNode) snj.left();
     }
 
-    tmpi.dist = ((dist / 2) - ih);
-    tmpj.dist = ((dist / 2) - jh);
+    return min;
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param i
-   *          DOCUMENT ME!
-   * @param j
-   *          DOCUMENT ME!
+   * {@inheritDoc}
    */
-  public void findClusterDistance(int i, int j)
+  @Override
+  protected void findNewDistances(SequenceNode nodei, SequenceNode nodej,
+          double dist)
   {
-    int noi = cluster.elementAt(i).value.length;
-    int noj = cluster.elementAt(j).value.length;
+    nodei.dist = ((dist + ri) - rj) / 2;
+    nodej.dist = (dist - nodei.dist);
 
-    // New distances from cluster to others
-    float[] newdist = new float[noseqs];
-
-    for (int l = 0; l < noseqs; l++)
+    if (nodei.dist < 0)
     {
-      if ((l != i) && (l != j))
-      {
-        newdist[l] = ((distance[i][l] * noi) + (distance[j][l] * noj))
-                / (noi + noj);
-      }
-      else
-      {
-        newdist[l] = 0;
-      }
+      nodei.dist = 0;
     }
 
-    for (int ii = 0; ii < noseqs; ii++)
+    if (nodej.dist < 0)
     {
-      distance[i][ii] = newdist[ii];
-      distance[ii][i] = newdist[ii];
+      nodej.dist = 0;
     }
   }
 
   /**
-   * DOCUMENT ME!
+   * Calculates and saves the distance between the combination of cluster(i) and
+   * cluster(j) and all other clusters. The new distance to cluster k is
+   * calculated as the average of the distances from i to k and from j to k,
+   * less half the distance from i to j.
    * 
    * @param i
-   *          DOCUMENT ME!
    * @param j
-   *          DOCUMENT ME!
    */
-  public void findClusterNJDistance(int i, int j)
+  @Override
+  protected
+  void findClusterDistance(int i, int j)
   {
-
-    // New distances from cluster to others
-    float[] newdist = new float[noseqs];
-
+    // New distances from cluster i to others
+    double[] newdist = new double[noseqs];
+  
+    double ijDistance = distances.getValue(i, j);
     for (int l = 0; l < noseqs; l++)
     {
       if ((l != i) && (l != j))
       {
-        newdist[l] = ((distance[i][l] + distance[j][l]) - distance[i][j]) / 2;
+        newdist[l] = (distances.getValue(i, l) + distances.getValue(j, l) - ijDistance) / 2;
       }
       else
       {
         newdist[l] = 0;
       }
     }
-
+  
     for (int ii = 0; ii < noseqs; ii++)
     {
-      distance[i][ii] = newdist[ii];
-      distance[ii][i] = newdist[ii];
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param i
-   *          DOCUMENT ME!
-   * @param j
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public float findr(int i, int j)
-  {
-    float tmp = 1;
-
-    for (int k = 0; k < noseqs; k++)
-    {
-      if ((k != i) && (k != j) && (done[k] != 1))
-      {
-        tmp = tmp + distance[i][k];
-      }
-    }
-
-    if (noClus > 2)
-    {
-      tmp = tmp / (noClus - 2);
+      distances.setValue(i, ii, newdist[ii]);
+      distances.setValue(ii, i, newdist[ii]);
     }
-
-    return tmp;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public float findMinNJDistance()
-  {
-    float min = 100000;
-
-    for (int i = 0; i < (noseqs - 1); i++)
-    {
-      for (int j = i + 1; j < noseqs; j++)
-      {
-        if ((done[i] != 1) && (done[j] != 1))
-        {
-          float tmp = distance[i][j] - (findr(i, j) + findr(j, i));
-
-          if (tmp < min)
-          {
-            mini = i;
-            minj = j;
-
-            min = tmp;
-          }
-        }
-      }
-    }
-
-    return min;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public float findMinDistance()
-  {
-    float min = 100000;
-
-    for (int i = 0; i < (noseqs - 1); i++)
-    {
-      for (int j = i + 1; j < noseqs; j++)
-      {
-        if ((done[i] != 1) && (done[j] != 1))
-        {
-          if (distance[i][j] < min)
-          {
-            mini = i;
-            minj = j;
-
-            min = distance[i][j];
-          }
-        }
-      }
-    }
-
-    return min;
-  }
-
-  /**
-   * Calculate a distance matrix given the sequence input data and score model
-   * 
-   * @return similarity matrix used to compute tree
-   */
-  public float[][] findDistances(ScoreModelI _pwmatrix)
-  {
-
-    float[][] dist = new float[noseqs][noseqs];
-    if (_pwmatrix == null)
-    {
-      // Resolve substitution model
-      _pwmatrix = ResidueProperties.getScoreModel(pwtype);
-      if (_pwmatrix == null)
-      {
-        _pwmatrix = ResidueProperties.getScoreMatrix("BLOSUM62");
-      }
-    }
-    dist = _pwmatrix.findDistances(seqData);
-    return dist;
-
-  }
-
-  /**
-   * DOCUMENT ME!
-   */
-  public void makeLeaves()
-  {
-    cluster = new Vector<Cluster>();
-
-    for (int i = 0; i < noseqs; i++)
-    {
-      SequenceNode sn = new SequenceNode();
-
-      sn.setElement(sequence[i]);
-      sn.setName(sequence[i].getName());
-      node.addElement(sn);
-
-      int[] value = new int[1];
-      value[0] = i;
-
-      Cluster c = new Cluster(value);
-      cluster.addElement(c);
-    }
-  }
-
-  /**
-   * Search for leaf nodes below (or at) the given node
-   * 
-   * @param nd
-   *          root node to search from
-   * 
-   * @return
-   */
-  public Vector<SequenceNode> findLeaves(SequenceNode nd)
-  {
-    Vector<SequenceNode> leaves = new Vector<SequenceNode>();
-    findLeaves(nd, leaves);
-    return leaves;
-  }
-
-  /**
-   * Search for leaf nodes.
-   * 
-   * @param nd
-   *          root node to search from
-   * @param leaves
-   *          Vector of leaves to add leaf node objects too.
-   * 
-   * @return Vector of leaf nodes on binary tree
-   */
-  Vector<SequenceNode> findLeaves(SequenceNode nd,
-          Vector<SequenceNode> leaves)
-  {
-    if (nd == null)
-    {
-      return leaves;
-    }
-
-    if ((nd.left() == null) && (nd.right() == null)) // Interior node
-    // detection
-    {
-      leaves.addElement(nd);
-
-      return leaves;
-    }
-    else
-    {
-      /*
-       * TODO: Identify internal nodes... if (node.isSequenceLabel()) {
-       * leaves.addElement(node); }
-       */
-      findLeaves((SequenceNode) nd.left(), leaves);
-      findLeaves((SequenceNode) nd.right(), leaves);
-    }
-
-    return leaves;
-  }
-
-  /**
-   * Find the leaf node with a particular ycount
-   * 
-   * @param nd
-   *          initial point on tree to search from
-   * @param count
-   *          value to search for
-   * 
-   * @return null or the node with ycound=count
-   */
-  public Object findLeaf(SequenceNode nd, int count)
-  {
-    found = _findLeaf(nd, count);
-
-    return found;
-  }
-
-  /*
-   * #see findLeaf(SequenceNode node, count)
-   */
-  public Object _findLeaf(SequenceNode nd, int count)
-  {
-    if (nd == null)
-    {
-      return null;
-    }
-
-    if (nd.ycount == count)
-    {
-      found = nd.element();
-
-      return found;
-    }
-    else
-    {
-      _findLeaf((SequenceNode) nd.left(), count);
-      _findLeaf((SequenceNode) nd.right(), count);
-    }
-
-    return found;
-  }
-
-  /**
-   * printNode is mainly for debugging purposes.
-   * 
-   * @param nd
-   *          SequenceNode
-   */
-  public void printNode(SequenceNode nd)
-  {
-    if (nd == null)
-    {
-      return;
-    }
-
-    if ((nd.left() == null) && (nd.right() == null))
-    {
-      System.out.println("Leaf = " + ((SequenceI) nd.element()).getName());
-      System.out.println("Dist " + nd.dist);
-      System.out.println("Boot " + nd.getBootstrap());
-    }
-    else
-    {
-      System.out.println("Dist " + nd.dist);
-      printNode((SequenceNode) nd.left());
-      printNode((SequenceNode) nd.right());
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   */
-  public void findMaxDist(SequenceNode nd)
-  {
-    if (nd == null)
-    {
-      return;
-    }
-
-    if ((nd.left() == null) && (nd.right() == null))
-    {
-      float dist = nd.dist;
-
-      if (dist > maxDistValue)
-      {
-        maxdist = nd;
-        maxDistValue = dist;
-      }
-    }
-    else
-    {
-      findMaxDist((SequenceNode) nd.left());
-      findMaxDist((SequenceNode) nd.right());
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public Vector<SequenceNode> getGroups()
-  {
-    return groups;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public float getMaxHeight()
-  {
-    return maxheight;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   * @param threshold
-   *          DOCUMENT ME!
-   */
-  public void groupNodes(SequenceNode nd, float threshold)
-  {
-    if (nd == null)
-    {
-      return;
-    }
-
-    if ((nd.height / maxheight) > threshold)
-    {
-      groups.addElement(nd);
-    }
-    else
-    {
-      groupNodes((SequenceNode) nd.left(), threshold);
-      groupNodes((SequenceNode) nd.right(), threshold);
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public float findHeight(SequenceNode nd)
-  {
-    if (nd == null)
-    {
-      return maxheight;
-    }
-
-    if ((nd.left() == null) && (nd.right() == null))
-    {
-      nd.height = ((SequenceNode) nd.parent()).height + nd.dist;
-
-      if (nd.height > maxheight)
-      {
-        return nd.height;
-      }
-      else
-      {
-        return maxheight;
-      }
-    }
-    else
-    {
-      if (nd.parent() != null)
-      {
-        nd.height = ((SequenceNode) nd.parent()).height + nd.dist;
-      }
-      else
-      {
-        maxheight = 0;
-        nd.height = (float) 0.0;
-      }
-
-      maxheight = findHeight((SequenceNode) (nd.left()));
-      maxheight = findHeight((SequenceNode) (nd.right()));
-    }
-
-    return maxheight;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public SequenceNode reRoot()
-  {
-    if (maxdist != null)
-    {
-      ycount = 0;
-
-      float tmpdist = maxdist.dist;
-
-      // New top
-      SequenceNode sn = new SequenceNode();
-      sn.setParent(null);
-
-      // New right hand of top
-      SequenceNode snr = (SequenceNode) maxdist.parent();
-      changeDirection(snr, maxdist);
-      System.out.println("Printing reversed tree");
-      printN(snr);
-      snr.dist = tmpdist / 2;
-      maxdist.dist = tmpdist / 2;
-
-      snr.setParent(sn);
-      maxdist.setParent(sn);
-
-      sn.setRight(snr);
-      sn.setLeft(maxdist);
-
-      top = sn;
-
-      ycount = 0;
-      reCount(top);
-      findHeight(top);
-    }
-
-    return top;
-  }
-
-  /**
-   * 
-   * @return true if original sequence data can be recovered
-   */
-  public boolean hasOriginalSequenceData()
-  {
-    return seqData != null;
-  }
-
-  /**
-   * Returns original alignment data used for calculation - or null where not
-   * available.
-   * 
-   * @return null or cut'n'pasteable alignment
-   */
-  public String printOriginalSequenceData(char gapChar)
-  {
-    if (seqData == null)
-    {
-      return null;
-    }
-
-    StringBuffer sb = new StringBuffer();
-    String[] seqdatas = seqData.getSequenceStrings(gapChar);
-    for (int i = 0; i < seqdatas.length; i++)
-    {
-      sb.append(new jalview.util.Format("%-" + 15 + "s").form(sequence[i]
-              .getName()));
-      sb.append(" " + seqdatas[i] + "\n");
-    }
-    return sb.toString();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   */
-  public void printN(SequenceNode nd)
-  {
-    if (nd == null)
-    {
-      return;
-    }
-
-    if ((nd.left() != null) && (nd.right() != null))
-    {
-      printN((SequenceNode) nd.left());
-      printN((SequenceNode) nd.right());
-    }
-    else
-    {
-      System.out.println(" name = " + ((SequenceI) nd.element()).getName());
-    }
-
-    System.out.println(" dist = " + nd.dist + " " + nd.count + " "
-            + nd.height);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   */
-  public void reCount(SequenceNode nd)
-  {
-    ycount = 0;
-    _lycount = 0;
-    // _lylimit = this.node.size();
-    _reCount(nd);
-  }
-
-  private long _lycount = 0, _lylimit = 0;
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   */
-  public void _reCount(SequenceNode nd)
-  {
-    // if (_lycount<_lylimit)
-    // {
-    // System.err.println("Warning: depth of _recount greater than number of nodes.");
-    // }
-    if (nd == null)
-    {
-      return;
-    }
-    _lycount++;
-
-    if ((nd.left() != null) && (nd.right() != null))
-    {
-
-      _reCount((SequenceNode) nd.left());
-      _reCount((SequenceNode) nd.right());
-
-      SequenceNode l = (SequenceNode) nd.left();
-      SequenceNode r = (SequenceNode) nd.right();
-
-      nd.count = l.count + r.count;
-      nd.ycount = (l.ycount + r.ycount) / 2;
-    }
-    else
-    {
-      nd.count = 1;
-      nd.ycount = ycount++;
-    }
-    _lycount--;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   */
-  public void swapNodes(SequenceNode nd)
-  {
-    if (nd == null)
-    {
-      return;
-    }
-
-    SequenceNode tmp = (SequenceNode) nd.left();
-
-    nd.setLeft(nd.right());
-    nd.setRight(tmp);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param nd
-   *          DOCUMENT ME!
-   * @param dir
-   *          DOCUMENT ME!
-   */
-  public void changeDirection(SequenceNode nd, SequenceNode dir)
-  {
-    if (nd == null)
-    {
-      return;
-    }
-
-    if (nd.parent() != top)
-    {
-      changeDirection((SequenceNode) nd.parent(), nd);
-
-      SequenceNode tmp = (SequenceNode) nd.parent();
-
-      if (dir == nd.left())
-      {
-        nd.setParent(dir);
-        nd.setLeft(tmp);
-      }
-      else if (dir == nd.right())
-      {
-        nd.setParent(dir);
-        nd.setRight(tmp);
-      }
-    }
-    else
-    {
-      if (dir == nd.left())
-      {
-        nd.setParent(nd.left());
-
-        if (top.left() == nd)
-        {
-          nd.setRight(top.right());
-        }
-        else
-        {
-          nd.setRight(top.left());
-        }
-      }
-      else
-      {
-        nd.setParent(nd.right());
-
-        if (top.left() == nd)
-        {
-          nd.setLeft(top.right());
-        }
-        else
-        {
-          nd.setLeft(top.left());
-        }
-      }
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public SequenceNode getMaxDist()
-  {
-    return maxdist;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public SequenceNode getTopNode()
-  {
-    return top;
-  }
-
-  /**
-   * 
-   * @return true if tree has real distances
-   */
-  public boolean isHasDistances()
-  {
-    return hasDistances;
-  }
-
-  /**
-   * 
-   * @return true if tree has real bootstrap values
-   */
-  public boolean isHasBootstrap()
-  {
-    return hasBootstrap;
-  }
-
-  public boolean isHasRootDistance()
-  {
-    return hasRootDistance;
-  }
-
-  /**
-   * apply the given transform to all the nodes in the tree.
-   * 
-   * @param nodeTransformI
-   */
-  public void applyToNodes(NodeTransformI nodeTransformI)
-  {
-    for (Enumeration<SequenceNode> nodes = node.elements(); nodes
-            .hasMoreElements(); nodeTransformI.transform(nodes
-            .nextElement()))
-    {
-      ;
-    }
-  }
-}
-
-/**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
- */
-class Cluster
-{
-  int[] value;
-
-  /**
-   * Creates a new Cluster object.
-   * 
-   * @param value
-   *          DOCUMENT ME!
-   */
-  public Cluster(int[] value)
-  {
-    this.value = value;
   }
 }
index 06a139b..3ec7995 100755 (executable)
  */
 package jalview.analysis;
 
-import jalview.datamodel.BinarySequence;
-import jalview.datamodel.BinarySequence.InvalidSequenceTypeException;
-import jalview.math.Matrix;
-import jalview.schemes.ResidueProperties;
-import jalview.schemes.ScoreMatrix;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.math.MatrixI;
 
 import java.io.PrintStream;
 
 /**
  * Performs Principal Component Analysis on given sequences
- * 
- * @author $author$
- * @version $Revision$
  */
 public class PCA implements Runnable
 {
-  Matrix m;
-
-  Matrix symm;
-
-  Matrix m2;
+  MatrixI symm;
 
   double[] eigenvalue;
 
-  Matrix eigenvector;
+  MatrixI eigenvector;
 
-  StringBuffer details = new StringBuffer();
+  StringBuilder details = new StringBuilder(1024);
 
-  /**
-   * Creates a new PCA object. By default, uses blosum62 matrix to generate
-   * sequence similarity matrices
-   * 
-   * @param s
-   *          Set of amino acid sequences to perform PCA on
-   */
-  public PCA(String[] s)
-  {
-    this(s, false);
-  }
+  final private AlignmentView seqs;
 
-  /**
-   * Creates a new PCA object. By default, uses blosum62 matrix to generate
-   * sequence similarity matrices
-   * 
-   * @param s
-   *          Set of sequences to perform PCA on
-   * @param nucleotides
-   *          if true, uses standard DNA/RNA matrix for sequence similarity
-   *          calculation.
-   */
-  public PCA(String[] s, boolean nucleotides)
-  {
-    this(s, nucleotides, null);
-  }
+  private ScoreModelI scoreModel;
+  
+  private SimilarityParamsI similarityParams;
 
-  public PCA(String[] s, boolean nucleotides, String s_m)
+  public PCA(AlignmentView s, ScoreModelI sm, SimilarityParamsI options)
   {
-
-    BinarySequence[] bs = new BinarySequence[s.length];
-    int ii = 0;
-
-    while ((ii < s.length) && (s[ii] != null))
-    {
-      bs[ii] = new BinarySequence(s[ii], nucleotides);
-      bs[ii].encode();
-      ii++;
-    }
-
-    BinarySequence[] bs2 = new BinarySequence[s.length];
-    ii = 0;
-    ScoreMatrix smtrx = null;
-    String sm = s_m;
-    if (sm != null)
-    {
-      smtrx = ResidueProperties.getScoreMatrix(sm);
-    }
-    if (smtrx == null)
-    {
-      // either we were given a non-existent score matrix or a scoremodel that
-      // isn't based on a pairwise symbol score matrix
-      smtrx = ResidueProperties.getScoreMatrix(sm = (nucleotides ? "DNA"
-              : "BLOSUM62"));
-    }
-    details.append("PCA calculation using " + sm
+    this.seqs = s;
+    this.similarityParams = options;
+    this.scoreModel = sm;
+    
+    details.append("PCA calculation using " + sm.getName()
             + " sequence similarity matrix\n========\n\n");
-    while ((ii < s.length) && (s[ii] != null))
-    {
-      bs2[ii] = new BinarySequence(s[ii], nucleotides);
-      if (smtrx != null)
-      {
-        try
-        {
-          bs2[ii].matrixEncode(smtrx);
-        } catch (InvalidSequenceTypeException x)
-        {
-          details.append("Unexpected mismatch of sequence type and score matrix. Calculation will not be valid!\n\n");
-        }
-      }
-      ii++;
-    }
-
-    // System.out.println("Created binary encoding");
-    // printMemory(rt);
-    int count = 0;
-
-    while ((count < bs.length) && (bs[count] != null))
-    {
-      count++;
-    }
-
-    double[][] seqmat = new double[count][bs[0].getDBinary().length];
-    double[][] seqmat2 = new double[count][bs2[0].getDBinary().length];
-    int i = 0;
-
-    while (i < count)
-    {
-      seqmat[i] = bs[i].getDBinary();
-      seqmat2[i] = bs2[i].getDBinary();
-      i++;
-    }
-
-    // System.out.println("Created array");
-    // printMemory(rt);
-    // System.out.println(" --- Original matrix ---- ");
-    m = new Matrix(seqmat, count, bs[0].getDBinary().length);
-    m2 = new Matrix(seqmat2, count, bs2[0].getDBinary().length);
-
-  }
-
-  /**
-   * Returns the matrix used in PCA calculation
-   * 
-   * @return java.math.Matrix object
-   */
-
-  public Matrix getM()
-  {
-    return m;
   }
 
   /**
@@ -170,7 +66,7 @@ public class PCA implements Runnable
    */
   public double getEigenvalue(int i)
   {
-    return eigenvector.d[i];
+    return eigenvector.getD()[i];
   }
 
   /**
@@ -189,9 +85,9 @@ public class PCA implements Runnable
    */
   public float[][] getComponents(int l, int n, int mm, float factor)
   {
-    float[][] out = new float[m.rows][3];
+    float[][] out = new float[getHeight()][3];
 
-    for (int i = 0; i < m.rows; i++)
+    for (int i = 0; i < getHeight(); i++)
     {
       out[i][0] = (float) component(i, l) * factor;
       out[i][1] = (float) component(i, n) * factor;
@@ -212,9 +108,9 @@ public class PCA implements Runnable
   public double[] component(int n)
   {
     // n = index of eigenvector
-    double[] out = new double[m.rows];
+    double[] out = new double[getHeight()];
 
-    for (int i = 0; i < m.rows; i++)
+    for (int i = 0; i < out.length; i++)
     {
       out[i] = component(i, n);
     }
@@ -236,12 +132,12 @@ public class PCA implements Runnable
   {
     double out = 0.0;
 
-    for (int i = 0; i < symm.cols; i++)
+    for (int i = 0; i < symm.width(); i++)
     {
-      out += (symm.value[row][i] * eigenvector.value[i][n]);
+      out += (symm.getValue(row, i) * eigenvector.getValue(i, n));
     }
 
-    return out / eigenvector.d[n];
+    return out / eigenvector.getD()[n];
   }
 
   public String getDetails()
@@ -252,40 +148,31 @@ public class PCA implements Runnable
   /**
    * DOCUMENT ME!
    */
+  @Override
   public void run()
   {
     PrintStream ps = new PrintStream(System.out)
     {
+      @Override
       public void print(String x)
       {
         details.append(x);
       }
 
+      @Override
       public void println()
       {
         details.append("\n");
       }
     };
 
+    // long now = System.currentTimeMillis();
     try
     {
-      details.append("PCA Calculation Mode is "
-              + (jvCalcMode ? "Jalview variant" : "Original SeqSpace")
-              + "\n");
-      Matrix mt = m.transpose();
+      eigenvector = scoreModel.findSimilarities(seqs, similarityParams);
 
       details.append(" --- OrigT * Orig ---- \n");
-      if (!jvCalcMode)
-      {
-        eigenvector = mt.preMultiply(m); // standard seqspace comparison matrix
-      }
-      else
-      {
-        eigenvector = mt.preMultiply(m2); // jalview variation on seqsmace
-                                          // method
-      }
-
-      eigenvector.print(ps);
+      eigenvector.print(ps, "%8.2f");
 
       symm = eigenvector.copy();
 
@@ -293,10 +180,10 @@ public class PCA implements Runnable
 
       details.append(" ---Tridiag transform matrix ---\n");
       details.append(" --- D vector ---\n");
-      eigenvector.printD(ps);
+      eigenvector.printD(ps, "%15.4e");
       ps.println();
       details.append("--- E vector ---\n");
-      eigenvector.printE(ps);
+      eigenvector.printE(ps, "%15.4e");
       ps.println();
 
       // Now produce the diagonalization matrix
@@ -310,9 +197,9 @@ public class PCA implements Runnable
     }
 
     details.append(" --- New diagonalization matrix ---\n");
-    eigenvector.print(ps);
+    eigenvector.print(ps, "%8.2f");
     details.append(" --- Eigenvalues ---\n");
-    eigenvector.printD(ps);
+    eigenvector.printD(ps, "%15.4e");
     ps.println();
     /*
      * for (int seq=0;seq<symm.rows;seq++) { ps.print("\"Seq"+seq+"\""); for
@@ -320,12 +207,19 @@ public class PCA implements Runnable
      * 
      * ps.print(","+component(seq, ev)); } ps.println(); }
      */
+    // System.out.println(("PCA.run() took "
+    // + (System.currentTimeMillis() - now) + "ms"));
   }
 
-  boolean jvCalcMode = true;
-
-  public void setJvCalcMode(boolean calcMode)
+  /**
+   * Answers the N dimensions of the NxN PCA matrix. This is the number of
+   * sequences involved in the pairwise score calculation.
+   * 
+   * @return
+   */
+  public int getHeight()
   {
-    this.jvCalcMode = calcMode;
+    // TODO can any of seqs[] be null?
+    return seqs.getSequences().length;
   }
 }
index 89c5c30..3108ef3 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
 {
@@ -132,11 +133,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())
     {
@@ -195,25 +196,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
@@ -230,88 +215,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, ()[]{}<>.
    * 
@@ -500,4 +403,76 @@ 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() - 1;
+      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;
+          }
+        }
+        j -= 1;
+      }
+
+      // 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 21ad1cc..27b3041 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)
     {
diff --git a/src/jalview/analysis/TreeBuilder.java b/src/jalview/analysis/TreeBuilder.java
new file mode 100644 (file)
index 0000000..effef9a
--- /dev/null
@@ -0,0 +1,460 @@
+package jalview.analysis;
+
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.CigarArray;
+import jalview.datamodel.SeqCigar;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.SequenceNode;
+import jalview.math.MatrixI;
+import jalview.viewmodel.AlignmentViewport;
+
+import java.util.BitSet;
+import java.util.Vector;
+
+public abstract class TreeBuilder
+{
+  public static final String AVERAGE_DISTANCE = "AV";
+
+  public static final String NEIGHBOUR_JOINING = "NJ";
+
+  protected Vector<BitSet> clusters;
+
+  protected SequenceI[] sequences;
+
+  public AlignmentView seqData;
+
+  protected BitSet done;
+
+  protected int noseqs;
+
+  int noClus;
+
+  protected MatrixI distances;
+
+  protected int mini;
+
+  protected int minj;
+
+  protected double ri;
+
+  protected double rj;
+
+  SequenceNode maxdist;
+
+  SequenceNode top;
+
+  double maxDistValue;
+
+  double maxheight;
+
+  int ycount;
+
+  Vector<SequenceNode> node;
+
+  private AlignmentView seqStrings;
+
+  /**
+   * Constructor
+   * 
+   * @param av
+   * @param sm
+   * @param scoreParameters
+   */
+  public TreeBuilder(AlignmentViewport av, ScoreModelI sm,
+          SimilarityParamsI scoreParameters)
+  {
+    int start, end;
+    boolean selview = av.getSelectionGroup() != null
+            && av.getSelectionGroup().getSize() > 1;
+    seqStrings = av.getAlignmentView(selview);
+    if (!selview)
+    {
+      start = 0;
+      end = av.getAlignment().getWidth();
+      this.sequences = av.getAlignment().getSequencesArray();
+    }
+    else
+    {
+      start = av.getSelectionGroup().getStartRes();
+      end = av.getSelectionGroup().getEndRes() + 1;
+      this.sequences = av.getSelectionGroup().getSequencesInOrder(
+              av.getAlignment());
+    }
+
+    init(seqStrings, start, end);
+
+    computeTree(sm, scoreParameters);
+  }
+
+  public SequenceI[] getSequences()
+  {
+    return sequences;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  double findHeight(SequenceNode nd)
+  {
+    if (nd == null)
+    {
+      return maxheight;
+    }
+  
+    if ((nd.left() == null) && (nd.right() == null))
+    {
+      nd.height = ((SequenceNode) nd.parent()).height + nd.dist;
+  
+      if (nd.height > maxheight)
+      {
+        return nd.height;
+      }
+      else
+      {
+        return maxheight;
+      }
+    }
+    else
+    {
+      if (nd.parent() != null)
+      {
+        nd.height = ((SequenceNode) nd.parent()).height + nd.dist;
+      }
+      else
+      {
+        maxheight = 0;
+        nd.height = (float) 0.0;
+      }
+  
+      maxheight = findHeight((SequenceNode) (nd.left()));
+      maxheight = findHeight((SequenceNode) (nd.right()));
+    }
+  
+    return maxheight;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   */
+  void reCount(SequenceNode nd)
+  {
+    ycount = 0;
+    // _lycount = 0;
+    // _lylimit = this.node.size();
+    _reCount(nd);
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   */
+  void _reCount(SequenceNode nd)
+  {
+    // if (_lycount<_lylimit)
+    // {
+    // System.err.println("Warning: depth of _recount greater than number of nodes.");
+    // }
+    if (nd == null)
+    {
+      return;
+    }
+    // _lycount++;
+  
+    if ((nd.left() != null) && (nd.right() != null))
+    {
+  
+      _reCount((SequenceNode) nd.left());
+      _reCount((SequenceNode) nd.right());
+  
+      SequenceNode l = (SequenceNode) nd.left();
+      SequenceNode r = (SequenceNode) nd.right();
+  
+      nd.count = l.count + r.count;
+      nd.ycount = (l.ycount + r.ycount) / 2;
+    }
+    else
+    {
+      nd.count = 1;
+      nd.ycount = ycount++;
+    }
+    // _lycount--;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  public SequenceNode getTopNode()
+  {
+    return top;
+  }
+
+  /**
+   * 
+   * @return true if tree has real distances
+   */
+  public boolean hasDistances()
+  {
+    return true;
+  }
+
+  /**
+   * 
+   * @return true if tree has real bootstrap values
+   */
+  public boolean hasBootstrap()
+  {
+    return false;
+  }
+
+  public boolean hasRootDistance()
+  {
+    return true;
+  }
+
+  /**
+   * Form clusters by grouping sub-clusters, starting from one sequence per
+   * cluster, and finishing when only two clusters remain
+   */
+  void cluster()
+  {
+    while (noClus > 2)
+    {
+      findMinDistance();
+  
+      joinClusters(mini, minj);
+  
+      noClus--;
+    }
+  
+    int rightChild = done.nextClearBit(0);
+    int leftChild = done.nextClearBit(rightChild + 1);
+  
+    joinClusters(leftChild, rightChild);
+    top = (node.elementAt(leftChild));
+  
+    reCount(top);
+    findHeight(top);
+    findMaxDist(top);
+  }
+
+  /**
+   * Returns the minimum distance between two clusters, and also sets the
+   * indices of the clusters in fields mini and minj
+   * 
+   * @return
+   */
+  protected abstract double findMinDistance();
+
+  /**
+   * Calculates the tree using the given score model and parameters, and the
+   * configured tree type
+   * <p>
+   * If the score model computes pairwise distance scores, then these are used
+   * directly to derive the tree
+   * <p>
+   * If the score model computes similarity scores, then the range of the scores
+   * is reversed to give a distance measure, and this is used to derive the tree
+   * 
+   * @param sm
+   * @param scoreOptions
+   */
+  protected void computeTree(ScoreModelI sm, SimilarityParamsI scoreOptions)
+  {
+    distances = sm.findDistances(seqData, scoreOptions);
+  
+    makeLeaves();
+  
+    noClus = clusters.size();
+  
+    cluster();
+  }
+
+  /**
+   * Finds the node, at or below the given node, with the maximum distance, and
+   * saves the node and the distance value
+   * 
+   * @param nd
+   */
+  void findMaxDist(SequenceNode nd)
+  {
+    if (nd == null)
+    {
+      return;
+    }
+  
+    if ((nd.left() == null) && (nd.right() == null))
+    {
+      double dist = nd.dist;
+  
+      if (dist > maxDistValue)
+      {
+        maxdist = nd;
+        maxDistValue = dist;
+      }
+    }
+    else
+    {
+      findMaxDist((SequenceNode) nd.left());
+      findMaxDist((SequenceNode) nd.right());
+    }
+  }
+
+  /**
+   * Calculates and returns r, whatever that is
+   * 
+   * @param i
+   * @param j
+   * 
+   * @return
+   */
+  protected double findr(int i, int j)
+  {
+    double tmp = 1;
+  
+    for (int k = 0; k < noseqs; k++)
+    {
+      if ((k != i) && (k != j) && (!done.get(k)))
+      {
+        tmp = tmp + distances.getValue(i, k);
+      }
+    }
+  
+    if (noClus > 2)
+    {
+      tmp = tmp / (noClus - 2);
+    }
+  
+    return tmp;
+  }
+
+  protected void init(AlignmentView seqView, int start, int end)
+  {
+    this.node = new Vector<SequenceNode>();
+    if (seqView != null)
+    {
+      this.seqData = seqView;
+    }
+    else
+    {
+      SeqCigar[] seqs = new SeqCigar[sequences.length];
+      for (int i = 0; i < sequences.length; i++)
+      {
+        seqs[i] = new SeqCigar(sequences[i], start, end);
+      }
+      CigarArray sdata = new CigarArray(seqs);
+      sdata.addOperation(CigarArray.M, end - start + 1);
+      this.seqData = new AlignmentView(sdata, start);
+    }
+  
+    /*
+     * count the non-null sequences
+     */
+    noseqs = 0;
+  
+    done = new BitSet();
+  
+    for (SequenceI seq : sequences)
+    {
+      if (seq != null)
+      {
+        noseqs++;
+      }
+    }
+  }
+
+  /**
+   * Merges cluster(j) to cluster(i) and recalculates cluster and node distances
+   * 
+   * @param i
+   * @param j
+   */
+  void joinClusters(final int i, final int j)
+  {
+    double dist = distances.getValue(i, j);
+  
+    ri = findr(i, j);
+    rj = findr(j, i);
+  
+    findClusterDistance(i, j);
+  
+    SequenceNode sn = new SequenceNode();
+  
+    sn.setLeft((node.elementAt(i)));
+    sn.setRight((node.elementAt(j)));
+  
+    SequenceNode tmpi = (node.elementAt(i));
+    SequenceNode tmpj = (node.elementAt(j));
+  
+    findNewDistances(tmpi, tmpj, dist);
+  
+    tmpi.setParent(sn);
+    tmpj.setParent(sn);
+  
+    node.setElementAt(sn, i);
+  
+    /*
+     * move the members of cluster(j) to cluster(i)
+     * and mark cluster j as out of the game
+     */
+    clusters.get(i).or(clusters.get(j));
+    clusters.get(j).clear();
+    done.set(j);
+  }
+
+  /*
+   * Computes and stores new distances for nodei and nodej, given the previous
+   * distance between them
+   */
+  protected abstract void findNewDistances(SequenceNode nodei,
+          SequenceNode nodej, double previousDistance);
+
+  /**
+   * Calculates and saves the distance between the combination of cluster(i) and
+   * cluster(j) and all other clusters. The form of the calculation depends on
+   * the tree clustering method being used.
+   * 
+   * @param i
+   * @param j
+   */
+  protected abstract void findClusterDistance(int i, int j);
+
+  /**
+   * Start by making a cluster for each individual sequence
+   */
+  void makeLeaves()
+  {
+    clusters = new Vector<BitSet>();
+  
+    for (int i = 0; i < noseqs; i++)
+    {
+      SequenceNode sn = new SequenceNode();
+  
+      sn.setElement(sequences[i]);
+      sn.setName(sequences[i].getName());
+      node.addElement(sn);
+      BitSet bs = new BitSet();
+      bs.set(i);
+      clusters.addElement(bs);
+    }
+  }
+
+  public AlignmentView getOriginalData()
+  {
+    return seqStrings;
+  }
+
+}
diff --git a/src/jalview/analysis/TreeModel.java b/src/jalview/analysis/TreeModel.java
new file mode 100644 (file)
index 0000000..5a41802
--- /dev/null
@@ -0,0 +1,673 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.analysis;
+
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.BinaryNode;
+import jalview.datamodel.NodeTransformI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.SequenceNode;
+import jalview.io.NewickFile;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * A model of a tree, either computed by Jalview or loaded from a file or other
+ * resource or service
+ */
+public class TreeModel
+{
+
+  SequenceI[] sequences;
+
+  /* 
+   * SequenceData is a string representation of what the user
+   * sees. The display may contain hidden columns.
+   */
+  private AlignmentView seqData;
+
+  int noseqs;
+
+  SequenceNode top;
+
+  double maxDistValue;
+
+  double maxheight;
+
+  int ycount;
+
+  Vector<SequenceNode> node;
+
+  boolean hasDistances = true; // normal case for jalview trees
+
+  boolean hasBootstrap = false; // normal case for jalview trees
+
+  private boolean hasRootDistance = true;
+
+  /**
+   * Create a new TreeModel object with leaves associated with sequences in
+   * seqs, and (optionally) original alignment data represented by Cigar strings
+   * 
+   * @param seqs
+   *          SequenceI[]
+   * @param odata
+   *          Cigar[]
+   * @param treefile
+   *          NewickFile
+   */
+  public TreeModel(SequenceI[] seqs, AlignmentView odata, NewickFile treefile)
+  {
+    this(seqs, treefile.getTree(), treefile.HasDistances(), treefile
+            .HasBootstrap(), treefile.HasRootDistance());
+    seqData = odata;
+
+    associateLeavesToSequences(seqs);
+  }
+
+  /**
+   * Constructor given a calculated tree
+   * 
+   * @param tree
+   */
+  public TreeModel(TreeBuilder tree)
+  {
+    this(tree.getSequences(), tree.getTopNode(), tree.hasDistances(), tree
+            .hasBootstrap(), tree.hasRootDistance());
+    seqData = tree.getOriginalData();
+  }
+
+  /**
+   * Constructor given sequences, root node and tree property flags
+   * 
+   * @param seqs
+   * @param root
+   * @param hasDist
+   * @param hasBoot
+   * @param hasRootDist
+   */
+  public TreeModel(SequenceI[] seqs, SequenceNode root, boolean hasDist,
+          boolean hasBoot, boolean hasRootDist)
+  {
+    this.sequences = seqs;
+    top = root;
+
+    hasDistances = hasDist;
+    hasBootstrap = hasBoot;
+    hasRootDistance = hasRootDist;
+
+    maxheight = findHeight(top);
+  }
+
+  /**
+   * @param seqs
+   */
+  public void associateLeavesToSequences(SequenceI[] seqs)
+  {
+    SequenceIdMatcher algnIds = new SequenceIdMatcher(seqs);
+
+    Vector<SequenceNode> leaves = findLeaves(top);
+
+    int i = 0;
+    int namesleft = seqs.length;
+
+    SequenceNode j;
+    SequenceI nam;
+    String realnam;
+    Vector<SequenceI> one2many = new Vector<SequenceI>();
+    // int countOne2Many = 0;
+    while (i < leaves.size())
+    {
+      j = leaves.elementAt(i++);
+      realnam = j.getName();
+      nam = null;
+
+      if (namesleft > -1)
+      {
+        nam = algnIds.findIdMatch(realnam);
+      }
+
+      if (nam != null)
+      {
+        j.setElement(nam);
+        if (one2many.contains(nam))
+        {
+          // countOne2Many++;
+          // if (jalview.bin.Cache.log.isDebugEnabled())
+          // jalview.bin.Cache.log.debug("One 2 many relationship for
+          // "+nam.getName());
+        }
+        else
+        {
+          one2many.addElement(nam);
+          namesleft--;
+        }
+      }
+      else
+      {
+        j.setElement(new Sequence(realnam, "THISISAPLACEHLDER"));
+        j.setPlaceholder(true);
+      }
+    }
+    // if (jalview.bin.Cache.log.isDebugEnabled() && countOne2Many>0) {
+    // jalview.bin.Cache.log.debug("There were "+countOne2Many+" alignment
+    // sequence ids (out of "+one2many.size()+" unique ids) linked to two or
+    // more leaves.");
+    // }
+    // one2many.clear();
+  }
+
+  /**
+   * Generate a string representation of the Tree
+   * 
+   * @return Newick File with all tree data available
+   */
+  public String print()
+  {
+    NewickFile fout = new NewickFile(getTopNode());
+
+    return fout.print(hasBootstrap(), hasDistances(),
+            hasRootDistance()); // output all data available for tree
+  }
+
+  /**
+   * 
+   * used when the alignment associated to a tree has changed.
+   * 
+   * @param list
+   *          Sequence set to be associated with tree nodes
+   */
+  public void updatePlaceHolders(List<SequenceI> list)
+  {
+    Vector<SequenceNode> leaves = findLeaves(top);
+
+    int sz = leaves.size();
+    SequenceIdMatcher seqmatcher = null;
+    int i = 0;
+
+    while (i < sz)
+    {
+      SequenceNode leaf = leaves.elementAt(i++);
+
+      if (list.contains(leaf.element()))
+      {
+        leaf.setPlaceholder(false);
+      }
+      else
+      {
+        if (seqmatcher == null)
+        {
+          // Only create this the first time we need it
+          SequenceI[] seqs = new SequenceI[list.size()];
+
+          for (int j = 0; j < seqs.length; j++)
+          {
+            seqs[j] = list.get(j);
+          }
+
+          seqmatcher = new SequenceIdMatcher(seqs);
+        }
+
+        SequenceI nam = seqmatcher.findIdMatch(leaf.getName());
+
+        if (nam != null)
+        {
+          if (!leaf.isPlaceholder())
+          {
+            // remapping the node to a new sequenceI - should remove any refs to
+            // old one.
+            // TODO - make many sequenceI to one leaf mappings possible!
+            // (JBPNote)
+          }
+          leaf.setPlaceholder(false);
+          leaf.setElement(nam);
+        }
+        else
+        {
+          if (!leaf.isPlaceholder())
+          {
+            // Construct a new placeholder sequence object for this leaf
+            leaf.setElement(new Sequence(leaf.getName(),
+                    "THISISAPLACEHLDER"));
+          }
+          leaf.setPlaceholder(true);
+
+        }
+      }
+    }
+  }
+
+  /**
+   * rename any nodes according to their associated sequence. This will modify
+   * the tree's metadata! (ie the original NewickFile or newly generated
+   * BinaryTree's label data)
+   */
+  public void renameAssociatedNodes()
+  {
+    applyToNodes(new NodeTransformI()
+    {
+
+      @Override
+      public void transform(BinaryNode nd)
+      {
+        Object el = nd.element();
+        if (el != null && el instanceof SequenceI)
+        {
+          nd.setName(((SequenceI) el).getName());
+        }
+      }
+    });
+  }
+
+  /**
+   * Search for leaf nodes below (or at) the given node
+   * 
+   * @param nd
+   *          root node to search from
+   * 
+   * @return
+   */
+  public Vector<SequenceNode> findLeaves(SequenceNode nd)
+  {
+    Vector<SequenceNode> leaves = new Vector<SequenceNode>();
+    findLeaves(nd, leaves);
+    return leaves;
+  }
+
+  /**
+   * Search for leaf nodes.
+   * 
+   * @param nd
+   *          root node to search from
+   * @param leaves
+   *          Vector of leaves to add leaf node objects too.
+   * 
+   * @return Vector of leaf nodes on binary tree
+   */
+  Vector<SequenceNode> findLeaves(SequenceNode nd,
+          Vector<SequenceNode> leaves)
+  {
+    if (nd == null)
+    {
+      return leaves;
+    }
+
+    if ((nd.left() == null) && (nd.right() == null)) // Interior node
+    // detection
+    {
+      leaves.addElement(nd);
+
+      return leaves;
+    }
+    else
+    {
+      /*
+       * TODO: Identify internal nodes... if (node.isSequenceLabel()) {
+       * leaves.addElement(node); }
+       */
+      findLeaves((SequenceNode) nd.left(), leaves);
+      findLeaves((SequenceNode) nd.right(), leaves);
+    }
+
+    return leaves;
+  }
+
+  /**
+   * printNode is mainly for debugging purposes.
+   * 
+   * @param nd
+   *          SequenceNode
+   */
+  void printNode(SequenceNode nd)
+  {
+    if (nd == null)
+    {
+      return;
+    }
+
+    if ((nd.left() == null) && (nd.right() == null))
+    {
+      System.out.println("Leaf = " + ((SequenceI) nd.element()).getName());
+      System.out.println("Dist " + nd.dist);
+      System.out.println("Boot " + nd.getBootstrap());
+    }
+    else
+    {
+      System.out.println("Dist " + nd.dist);
+      printNode((SequenceNode) nd.left());
+      printNode((SequenceNode) nd.right());
+    }
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  public double getMaxHeight()
+  {
+    return maxheight;
+  }
+
+  /**
+   * Makes a list of groups, where each group is represented by a node whose
+   * height (distance from the root node), as a fraction of the height of the
+   * whole tree, is greater than the given threshold. This corresponds to
+   * selecting the nodes immediately to the right of a vertical line
+   * partitioning the tree (if the tree is drawn with root to the left). Each
+   * such node represents a group that contains all of the sequences linked to
+   * the child leaf nodes.
+   * 
+   * @param threshold
+   * @see #getGroups()
+   */
+  public List<SequenceNode> groupNodes(float threshold)
+  {
+    List<SequenceNode> groups = new ArrayList<SequenceNode>();
+    _groupNodes(groups, getTopNode(), threshold);
+    return groups;
+  }
+
+  protected void _groupNodes(List<SequenceNode> groups, SequenceNode nd,
+          float threshold)
+  {
+    if (nd == null)
+    {
+      return;
+    }
+
+    if ((nd.height / maxheight) > threshold)
+    {
+      groups.add(nd);
+    }
+    else
+    {
+      _groupNodes(groups, (SequenceNode) nd.left(), threshold);
+      _groupNodes(groups, (SequenceNode) nd.right(), threshold);
+    }
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  public double findHeight(SequenceNode nd)
+  {
+    if (nd == null)
+    {
+      return maxheight;
+    }
+
+    if ((nd.left() == null) && (nd.right() == null))
+    {
+      nd.height = ((SequenceNode) nd.parent()).height + nd.dist;
+
+      if (nd.height > maxheight)
+      {
+        return nd.height;
+      }
+      else
+      {
+        return maxheight;
+      }
+    }
+    else
+    {
+      if (nd.parent() != null)
+      {
+        nd.height = ((SequenceNode) nd.parent()).height + nd.dist;
+      }
+      else
+      {
+        maxheight = 0;
+        nd.height = (float) 0.0;
+      }
+
+      maxheight = findHeight((SequenceNode) (nd.left()));
+      maxheight = findHeight((SequenceNode) (nd.right()));
+    }
+
+    return maxheight;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   */
+  void printN(SequenceNode nd)
+  {
+    if (nd == null)
+    {
+      return;
+    }
+
+    if ((nd.left() != null) && (nd.right() != null))
+    {
+      printN((SequenceNode) nd.left());
+      printN((SequenceNode) nd.right());
+    }
+    else
+    {
+      System.out.println(" name = " + ((SequenceI) nd.element()).getName());
+    }
+
+    System.out.println(" dist = " + nd.dist + " " + nd.count + " "
+            + nd.height);
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   */
+  public void reCount(SequenceNode nd)
+  {
+    ycount = 0;
+    // _lycount = 0;
+    // _lylimit = this.node.size();
+    _reCount(nd);
+  }
+
+  // private long _lycount = 0, _lylimit = 0;
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   */
+  void _reCount(SequenceNode nd)
+  {
+    // if (_lycount<_lylimit)
+    // {
+    // System.err.println("Warning: depth of _recount greater than number of nodes.");
+    // }
+    if (nd == null)
+    {
+      return;
+    }
+    // _lycount++;
+
+    if ((nd.left() != null) && (nd.right() != null))
+    {
+
+      _reCount((SequenceNode) nd.left());
+      _reCount((SequenceNode) nd.right());
+
+      SequenceNode l = (SequenceNode) nd.left();
+      SequenceNode r = (SequenceNode) nd.right();
+
+      nd.count = l.count + r.count;
+      nd.ycount = (l.ycount + r.ycount) / 2;
+    }
+    else
+    {
+      nd.count = 1;
+      nd.ycount = ycount++;
+    }
+    // _lycount--;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   */
+  public void swapNodes(SequenceNode nd)
+  {
+    if (nd == null)
+    {
+      return;
+    }
+
+    SequenceNode tmp = (SequenceNode) nd.left();
+
+    nd.setLeft(nd.right());
+    nd.setRight(tmp);
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param nd
+   *          DOCUMENT ME!
+   * @param dir
+   *          DOCUMENT ME!
+   */
+  void changeDirection(SequenceNode nd, SequenceNode dir)
+  {
+    if (nd == null)
+    {
+      return;
+    }
+
+    if (nd.parent() != top)
+    {
+      changeDirection((SequenceNode) nd.parent(), nd);
+
+      SequenceNode tmp = (SequenceNode) nd.parent();
+
+      if (dir == nd.left())
+      {
+        nd.setParent(dir);
+        nd.setLeft(tmp);
+      }
+      else if (dir == nd.right())
+      {
+        nd.setParent(dir);
+        nd.setRight(tmp);
+      }
+    }
+    else
+    {
+      if (dir == nd.left())
+      {
+        nd.setParent(nd.left());
+
+        if (top.left() == nd)
+        {
+          nd.setRight(top.right());
+        }
+        else
+        {
+          nd.setRight(top.left());
+        }
+      }
+      else
+      {
+        nd.setParent(nd.right());
+
+        if (top.left() == nd)
+        {
+          nd.setLeft(top.right());
+        }
+        else
+        {
+          nd.setLeft(top.left());
+        }
+      }
+    }
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  public SequenceNode getTopNode()
+  {
+    return top;
+  }
+
+  /**
+   * 
+   * @return true if tree has real distances
+   */
+  public boolean hasDistances()
+  {
+    return hasDistances;
+  }
+
+  /**
+   * 
+   * @return true if tree has real bootstrap values
+   */
+  public boolean hasBootstrap()
+  {
+    return hasBootstrap;
+  }
+
+  public boolean hasRootDistance()
+  {
+    return hasRootDistance;
+  }
+
+  /**
+   * apply the given transform to all the nodes in the tree.
+   * 
+   * @param nodeTransformI
+   */
+  public void applyToNodes(NodeTransformI nodeTransformI)
+  {
+    for (Enumeration<SequenceNode> nodes = node.elements(); nodes
+            .hasMoreElements(); nodeTransformI.transform(nodes
+            .nextElement()))
+    {
+      ;
+    }
+  }
+
+  public AlignmentView getOriginalData()
+  {
+    return seqData;
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/DistanceScoreModel.java b/src/jalview/analysis/scoremodels/DistanceScoreModel.java
new file mode 100644 (file)
index 0000000..0dd7617
--- /dev/null
@@ -0,0 +1,40 @@
+package jalview.analysis.scoremodels;
+
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.math.MatrixI;
+
+public abstract class DistanceScoreModel implements ScoreModelI
+{
+  /**
+   * A similarity score is calculated by first computing a distance score, and
+   * then reversing the min-max range of the score values
+   */
+  @Override
+  public MatrixI findSimilarities(AlignmentView seqData,
+          SimilarityParamsI options)
+  {
+    MatrixI distances = findDistances(seqData, options);
+
+    MatrixI similarities = distanceToSimilarity(distances);
+
+    return similarities;
+  }
+
+  /**
+   * Converts distance scores to similarity scores, by reversing the range of
+   * score values so that max becomes min and vice versa. The input matrix is
+   * not modified.
+   * 
+   * @param distances
+   */
+  public static MatrixI distanceToSimilarity(MatrixI distances)
+  {
+    MatrixI similarities = distances.copy();
+
+    similarities.reverseRange(false);
+
+    return similarities;
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/FeatureDistanceModel.java b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java
new file mode 100644 (file)
index 0000000..ddbaf73
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.analysis.scoremodels;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.SeqCigar;
+import jalview.datamodel.SequenceFeature;
+import jalview.math.Matrix;
+import jalview.math.MatrixI;
+import jalview.util.SetUtils;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class FeatureDistanceModel extends DistanceScoreModel
+{
+  private static final String NAME = "Sequence Feature Similarity";
+
+  private String description;
+
+  FeatureRenderer fr;
+
+  /**
+   * Constructor
+   */
+  public FeatureDistanceModel()
+  {
+  }
+
+  @Override
+  public ScoreModelI getInstance(AlignmentViewPanel view)
+  {
+    FeatureDistanceModel instance;
+    try
+    {
+      instance = this.getClass().newInstance();
+      instance.configureFromAlignmentView(view);
+      return instance;
+    } catch (InstantiationException | IllegalAccessException e)
+    {
+      System.err.println("Error in " + getClass().getName()
+              + ".getInstance(): " + e.getMessage());
+      return null;
+    }
+  }
+
+  boolean configureFromAlignmentView(AlignmentViewPanel view)
+
+  {
+    fr = view.cloneFeatureRenderer();
+    return true;
+  }
+
+  /**
+   * Calculates a distance measure [i][j] between each pair of sequences as the
+   * average number of features they have but do not share. That is, find the
+   * features each sequence pair has at each column, ignore feature types they
+   * have in common, and count the rest. The totals are normalised by the number
+   * of columns processed.
+   * <p>
+   * The parameters argument provides settings for treatment of gap-residue
+   * aligned positions, and whether the score is over the longer or shorter of
+   * each pair of sequences
+   * 
+   * @param seqData
+   * @param params
+   */
+  @Override
+  public MatrixI findDistances(AlignmentView seqData,
+          SimilarityParamsI params)
+  {
+    SeqCigar[] seqs = seqData.getSequences();
+    int noseqs = seqs.length;
+    int cpwidth = 0;// = seqData.getWidth();
+    double[][] distances = new double[noseqs][noseqs];
+    List<String> dft = null;
+    if (fr != null)
+    {
+      dft = fr.getDisplayedFeatureTypes();
+    }
+    if (dft == null || dft.isEmpty())
+    {
+      return new Matrix(distances);
+    }
+
+    // need to get real position for view position
+    int[] viscont = seqData.getVisibleContigs();
+
+    /*
+     * scan each column, compute and add to each distance[i, j]
+     * the number of feature types that seqi and seqj do not share
+     */
+    for (int vc = 0; vc < viscont.length; vc += 2)
+    {
+      for (int cpos = viscont[vc]; cpos <= viscont[vc + 1]; cpos++)
+      {
+        cpwidth++;
+
+        /*
+         * first record feature types in this column for each sequence
+         */
+        Map<SeqCigar, Set<String>> sfap = findFeatureTypesAtColumn(
+                seqs, cpos);
+
+        /*
+         * count feature types on either i'th or j'th sequence but not both
+         * and add this 'distance' measure to the total for [i, j] for j > i
+         */
+        for (int i = 0; i < (noseqs - 1); i++)
+        {
+          for (int j = i + 1; j < noseqs; j++)
+          {
+            SeqCigar sc1 = seqs[i];
+            SeqCigar sc2 = seqs[j];
+            Set<String> set1 = sfap.get(sc1);
+            Set<String> set2 = sfap.get(sc2);
+            boolean gap1 = set1 == null;
+            boolean gap2 = set2 == null;
+
+            /*
+             * gap-gap always scores zero
+             * residue-residue is always scored
+             * include gap-residue score if params say to do so
+             */
+            if ((!gap1 && !gap2) || params.includeGaps())
+            {
+              int seqDistance = SetUtils.countDisjunction(set1, set2);
+              distances[i][j] += seqDistance;
+            }
+          }
+        }
+      }
+    }
+
+    /*
+     * normalise the distance scores (summed over columns) by the
+     * number of visible columns used in the calculation
+     * and fill in the bottom half of the matrix
+     */
+    // TODO JAL-2424 cpwidth may be out by 1 - affects scores but not tree shape
+    for (int i = 0; i < noseqs; i++)
+    {
+      for (int j = i + 1; j < noseqs; j++)
+      {
+        distances[i][j] /= cpwidth;
+        distances[j][i] = distances[i][j];
+      }
+    }
+    return new Matrix(distances);
+  }
+
+  /**
+   * Builds and returns a map containing a (possibly empty) list (one per
+   * SeqCigar) of visible feature types at the given column position. The map
+   * does not include entries for features which straddle a gapped column
+   * positions.
+   * 
+   * @param seqs
+   * @param columnPosition
+   *          (0..)
+   * @return
+   */
+  protected Map<SeqCigar, Set<String>> findFeatureTypesAtColumn(
+          SeqCigar[] seqs, int columnPosition)
+  {
+    Map<SeqCigar, Set<String>> sfap = new HashMap<SeqCigar, Set<String>>();
+    for (SeqCigar seq : seqs)
+    {
+      int spos = seq.findPosition(columnPosition);
+      if (spos != -1)
+      {
+        /*
+         * position is not a gap
+         */
+        Set<String> types = new HashSet<String>();
+        List<SequenceFeature> sfs = fr.findFeaturesAtResidue(
+                seq.getRefSeq(), spos);
+        for (SequenceFeature sf : sfs)
+        {
+          types.add(sf.getType());
+        }
+        sfap.put(seq, types);
+      }
+    }
+    return sfap;
+  }
+
+  @Override
+  public String getName()
+  {
+    return NAME;
+  }
+
+  @Override
+  public String getDescription()
+  {
+    return description;
+  }
+
+  @Override
+  public boolean isDNA()
+  {
+    return true;
+  }
+
+  @Override
+  public boolean isProtein()
+  {
+    return true;
+  }
+
+  @Override
+  public String toString()
+  {
+    return "Score between sequences based on hamming distance between binary vectors marking features displayed at each column";
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/FeatureScoreModel.java b/src/jalview/analysis/scoremodels/FeatureScoreModel.java
deleted file mode 100644 (file)
index 7c81912..0000000
+++ /dev/null
@@ -1,161 +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.analysis.scoremodels;
-
-import jalview.api.analysis.ScoreModelI;
-import jalview.api.analysis.ViewBasedAnalysisI;
-import jalview.datamodel.AlignmentView;
-import jalview.datamodel.SeqCigar;
-import jalview.datamodel.SequenceFeature;
-
-import java.util.ArrayList;
-import java.util.Hashtable;
-import java.util.List;
-
-public class FeatureScoreModel implements ScoreModelI, ViewBasedAnalysisI
-{
-  jalview.api.FeatureRenderer fr;
-
-  @Override
-  public boolean configureFromAlignmentView(
-          jalview.api.AlignmentViewPanel view)
-  {
-    fr = view.cloneFeatureRenderer();
-    return true;
-  }
-
-  @Override
-  public float[][] findDistances(AlignmentView seqData)
-  {
-    int nofeats = 0;
-    List<String> dft = fr.getDisplayedFeatureTypes();
-    nofeats = dft.size();
-    SeqCigar[] seqs = seqData.getSequences();
-    int noseqs = seqs.length;
-    int cpwidth = 0;// = seqData.getWidth();
-    float[][] distance = new float[noseqs][noseqs];
-    if (nofeats == 0)
-    {
-      for (float[] d : distance)
-      {
-        for (int i = 0; i < d.length; d[i++] = 0f)
-        {
-          ;
-        }
-      }
-      return distance;
-    }
-    // need to get real position for view position
-    int[] viscont = seqData.getVisibleContigs();
-    for (int vc = 0; vc < viscont.length; vc += 2)
-    {
-
-      for (int cpos = viscont[vc]; cpos <= viscont[vc + 1]; cpos++)
-      {
-        cpwidth++;
-        // get visible features at cpos under view's display settings and
-        // compare them
-        List<Hashtable<String, SequenceFeature>> sfap = new ArrayList<Hashtable<String, SequenceFeature>>();
-        for (int i = 0; i < noseqs; i++)
-        {
-          Hashtable<String, SequenceFeature> types = new Hashtable<String, SequenceFeature>();
-          int spos = seqs[i].findPosition(cpos);
-          if (spos != -1)
-          {
-            List<SequenceFeature> sfs = fr.findFeaturesAtRes(
-                    seqs[i].getRefSeq(), spos);
-            for (SequenceFeature sf : sfs)
-            {
-              types.put(sf.getType(), sf);
-            }
-          }
-          sfap.add(types);
-        }
-        for (int i = 0; i < (noseqs - 1); i++)
-        {
-          if (cpos == 0)
-          {
-            distance[i][i] = 0f;
-          }
-          for (int j = i + 1; j < noseqs; j++)
-          {
-            int sfcommon = 0;
-            // compare the two lists of features...
-            Hashtable<String, SequenceFeature> fi = sfap.get(i), fk, fj = sfap
-                    .get(j);
-            if (fi.size() > fj.size())
-            {
-              fk = fj;
-            }
-            else
-            {
-              fk = fi;
-              fi = fj;
-            }
-            for (String k : fi.keySet())
-            {
-              SequenceFeature sfj = fk.get(k);
-              if (sfj != null)
-              {
-                sfcommon++;
-              }
-            }
-            distance[i][j] += (fi.size() + fk.size() - 2f * sfcommon);
-            distance[j][i] += distance[i][j];
-          }
-        }
-      }
-    }
-    for (int i = 0; i < noseqs; i++)
-    {
-      for (int j = i + 1; j < noseqs; j++)
-      {
-        distance[i][j] /= cpwidth;
-        distance[j][i] = distance[i][j];
-      }
-    }
-    return distance;
-  }
-
-  @Override
-  public String getName()
-  {
-    return "Sequence Feature Similarity";
-  }
-
-  @Override
-  public boolean isDNA()
-  {
-    return true;
-  }
-
-  @Override
-  public boolean isProtein()
-  {
-    return true;
-  }
-
-  @Override
-  public String toString()
-  {
-    return "Score between sequences based on hamming distance between binary vectors marking features displayed at each column";
-  }
-}
diff --git a/src/jalview/analysis/scoremodels/PIDModel.java b/src/jalview/analysis/scoremodels/PIDModel.java
new file mode 100644 (file)
index 0000000..721ba45
--- /dev/null
@@ -0,0 +1,243 @@
+package jalview.analysis.scoremodels;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.analysis.PairwiseScoreModelI;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.math.Matrix;
+import jalview.math.MatrixI;
+import jalview.util.Comparison;
+
+/**
+ * A class to provide sequence pairwise similarity based on residue identity.
+ * Instances of this class are immutable and thread-safe, so the same object is
+ * returned from calls to getInstance().
+ */
+public class PIDModel extends SimilarityScoreModel implements
+        PairwiseScoreModelI
+{
+  private static final String NAME = "PID";
+
+  /**
+   * Constructor
+   */
+  public PIDModel()
+  {
+  }
+
+  @Override
+  public String getName()
+  {
+    return NAME;
+  }
+
+  /**
+   * Answers null for description. If a display name is needed, use getName() or
+   * an internationalized string built from the name.
+   */
+  @Override
+  public String getDescription()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean isDNA()
+  {
+    return true;
+  }
+
+  @Override
+  public boolean isProtein()
+  {
+    return true;
+  }
+
+  /**
+   * Answers 1 if c and d are the same residue (ignoring case), and not gap
+   * characters. Answers 0 for non-matching or gap characters.
+   */
+  @Override
+  public float getPairwiseScore(char c, char d)
+  {
+    c = toUpper(c);
+    d = toUpper(d);
+    if (c == d && !Comparison.isGap(c))
+    {
+      return 1f;
+    }
+    return 0f;
+  }
+
+  /**
+   * @param c
+   */
+  protected static char toUpper(char c)
+  {
+    if ('a' <= c && c <= 'z')
+    {
+      c += 'A' - 'a';
+    }
+    return c;
+  }
+
+  /**
+   * Computes similarity scores based on pairwise percentage identity of
+   * sequences. For consistency with Jalview 2.10.1's SeqSpace mode PCA
+   * calculation, the percentage scores are rescaled to the width of the
+   * sequences (as if counts of identical residues). This method is thread-safe.
+   */
+  @Override
+  public MatrixI findSimilarities(AlignmentView seqData,
+          SimilarityParamsI options)
+  {
+    String[] seqs = seqData.getSequenceStrings(Comparison.GAP_DASH);
+
+    MatrixI result = findSimilarities(seqs, options);
+
+    result.multiply(seqData.getWidth() / 100d);
+
+    return result;
+  }
+
+  /**
+   * A distance score is computed in the usual way (by reversing the range of
+   * the similarity score results), and then rescaled to percentage values
+   * (reversing the rescaling to count values done in findSimilarities). This
+   * method is thread-safe.
+   */
+  @Override
+  public MatrixI findDistances(AlignmentView seqData,
+          SimilarityParamsI options)
+  {
+    MatrixI result = super.findDistances(seqData, options);
+
+    if (seqData.getWidth() != 0)
+    {
+      result.multiply(100d / seqData.getWidth());
+    }
+
+    return result;
+  }
+
+  /**
+   * Compute percentage identity scores, using the gap treatment and
+   * normalisation specified by the options parameter
+   * 
+   * @param seqs
+   * @param options
+   * @return
+   */
+  protected MatrixI findSimilarities(String[] seqs,
+          SimilarityParamsI options)
+  {
+    // TODO reuse code in ScoreMatrix instead somehow
+    double[][] values = new double[seqs.length][];
+    for (int row = 0; row < seqs.length; row++)
+    {
+      values[row] = new double[seqs.length];
+      for (int col = 0; col < seqs.length; col++)
+      {
+        double total = computePID(seqs[row], seqs[col], options);
+        values[row][col] = total;
+      }
+    }
+    return new Matrix(values);
+  }
+
+  /**
+   * Computes a percentage identity for two sequences, using the algorithm
+   * choices specified by the options parameter
+   * 
+   * @param seq1
+   * @param seq2
+   * @param options
+   * @return
+   */
+  public static double computePID(String seq1, String seq2,
+          SimilarityParamsI options)
+  {
+    int len1 = seq1.length();
+    int len2 = seq2.length();
+    int width = Math.max(len1, len2);
+    int total = 0;
+    int divideBy = 0;
+
+    for (int i = 0; i < width; i++)
+    {
+      if (i >= len1 || i >= len2)
+      {
+        /*
+         * off the end of one sequence; stop if we are only matching
+         * on the shorter sequence length, else treat as trailing gap
+         */
+        if (options.denominateByShortestLength())
+        {
+          break;
+        }
+        if (options.includeGaps())
+        {
+          divideBy++;
+        }
+        if (options.matchGaps())
+        {
+          total++;
+        }
+        continue;
+      }
+      char c1 = seq1.charAt(i);
+      char c2 = seq2.charAt(i);
+      boolean gap1 = Comparison.isGap(c1);
+      boolean gap2 = Comparison.isGap(c2);
+
+      if (gap1 && gap2)
+      {
+        /*
+         * gap-gap: include if options say so, if so
+         * have to score as identity; else ignore
+         */
+        if (options.includeGappedColumns())
+        {
+          divideBy++;
+          total++;
+        }
+        continue;
+      }
+
+      if (gap1 || gap2)
+      {
+        /*
+         * gap-residue: include if options say so, 
+         * count as match if options say so
+         */
+        if (options.includeGaps())
+        {
+          divideBy++;
+        }
+        if (options.matchGaps())
+        {
+          total++;
+        }
+        continue;
+      }
+
+      /*
+       * remaining case is gap-residue
+       */
+      if (toUpper(c1) == toUpper(c2))
+      {
+        total++;
+      }
+      divideBy++;
+    }
+
+    return divideBy == 0 ? 0D : 100D * total / divideBy;
+  }
+
+  @Override
+  public ScoreModelI getInstance(AlignmentViewPanel avp)
+  {
+    return this;
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/PIDScoreModel.java b/src/jalview/analysis/scoremodels/PIDScoreModel.java
deleted file mode 100644 (file)
index 0f7a67a..0000000
+++ /dev/null
@@ -1,75 +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.analysis.scoremodels;
-
-import jalview.api.analysis.ScoreModelI;
-import jalview.datamodel.AlignmentView;
-import jalview.util.Comparison;
-
-public class PIDScoreModel implements ScoreModelI
-{
-
-  @Override
-  public float[][] findDistances(AlignmentView seqData)
-  {
-    String[] sequenceString = seqData
-            .getSequenceStrings(Comparison.GapChars.charAt(0));
-    int noseqs = sequenceString.length;
-    float[][] distance = new float[noseqs][noseqs];
-    for (int i = 0; i < (noseqs - 1); i++)
-    {
-      for (int j = i; j < noseqs; j++)
-      {
-        if (j == i)
-        {
-          distance[i][i] = 0;
-        }
-        else
-        {
-          distance[i][j] = 100 - Comparison.PID(sequenceString[i],
-                  sequenceString[j]);
-
-          distance[j][i] = distance[i][j];
-        }
-      }
-    }
-    return distance;
-  }
-
-  @Override
-  public String getName()
-  {
-    return "PID";
-  }
-
-  @Override
-  public boolean isDNA()
-  {
-    return true;
-  }
-
-  @Override
-  public boolean isProtein()
-  {
-    return true;
-  }
-
-}
diff --git a/src/jalview/analysis/scoremodels/ScoreMatrix.java b/src/jalview/analysis/scoremodels/ScoreMatrix.java
new file mode 100644 (file)
index 0000000..efaeb43
--- /dev/null
@@ -0,0 +1,594 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.analysis.scoremodels;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.analysis.PairwiseScoreModelI;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.math.Matrix;
+import jalview.math.MatrixI;
+import jalview.util.Comparison;
+
+import java.util.Arrays;
+
+/**
+ * A class that models a substitution score matrix for any given alphabet of
+ * symbols. Instances of this class are immutable and thread-safe, so the same
+ * object is returned from calls to getInstance().
+ */
+public class ScoreMatrix extends SimilarityScoreModel implements
+        PairwiseScoreModelI
+{
+  private static final char GAP_CHARACTER = Comparison.GAP_DASH;
+
+  /*
+   * an arbitrary score to assign for identity of an unknown symbol
+   * (this is the value on the diagonal in the * column of the NCBI matrix)
+   * (though a case could be made for using the minimum diagonal value)
+   */
+  private static final int UNKNOWN_IDENTITY_SCORE = 1;
+
+  /*
+   * Jalview 2.10.1 treated gaps as X (peptide) or N (nucleotide)
+   * for pairwise scoring; 2.10.2 uses gap score (last column) in
+   * score matrix (JAL-2397)
+   * Set this flag to true (via Groovy) for 2.10.1 behaviour
+   */
+  private static boolean scoreGapAsAny = false;
+
+  public static final short UNMAPPED = (short) -1;
+
+  private static final String BAD_ASCII_ERROR = "Unexpected character %s in getPairwiseScore";
+
+  private static final int MAX_ASCII = 127;
+
+  /*
+   * the name of the model as shown in menus
+   * each score model in use should have a unique name
+   */
+  private String name;
+
+  /*
+   * a description for the model as shown in tooltips
+   */
+  private String description;
+
+  /*
+   * the characters that the model provides scores for
+   */
+  private char[] symbols;
+
+  /*
+   * the score matrix; both dimensions must equal the number of symbols
+   * matrix[i][j] is the substitution score for replacing symbols[i] with symbols[j]
+   */
+  private float[][] matrix;
+
+  /*
+   * quick lookup to convert from an ascii character value to the index
+   * of the corresponding symbol in the score matrix 
+   */
+  private short[] symbolIndex;
+
+  /*
+   * true for Protein Score matrix, false for dna score matrix
+   */
+  private boolean peptide;
+
+  private float minValue;
+
+  private float maxValue;
+  
+  /**
+   * Constructor given a name, symbol alphabet, and matrix of scores for pairs
+   * of symbols. The matrix should be square and of the same size as the
+   * alphabet, for example 20x20 for a 20 symbol alphabet.
+   * 
+   * @param theName
+   *          Unique, human readable name for the matrix
+   * @param alphabet
+   *          the symbols to which scores apply
+   * @param values
+   *          Pairwise scores indexed according to the symbol alphabet
+   */
+  public ScoreMatrix(String theName, char[] alphabet, float[][] values)
+  {
+    this(theName, null, alphabet, values);
+  }
+
+  /**
+   * Constructor given a name, description, symbol alphabet, and matrix of
+   * scores for pairs of symbols. The matrix should be square and of the same
+   * size as the alphabet, for example 20x20 for a 20 symbol alphabet.
+   * 
+   * @param theName
+   *          Unique, human readable name for the matrix
+   * @param theDescription
+   *          descriptive display name suitable for use in menus
+   * @param alphabet
+   *          the symbols to which scores apply
+   * @param values
+   *          Pairwise scores indexed according to the symbol alphabet
+   */
+  public ScoreMatrix(String theName, String theDescription,
+          char[] alphabet, float[][] values)
+  {
+    if (alphabet.length != values.length)
+    {
+      throw new IllegalArgumentException(
+              "score matrix size must match alphabet size");
+    }
+    for (float[] row : values)
+    {
+      if (row.length != alphabet.length)
+      {
+        throw new IllegalArgumentException(
+                "score matrix size must be square");
+      }
+    }
+
+    this.matrix = values;
+    this.name = theName;
+    this.description = theDescription;
+    this.symbols = alphabet;
+
+    symbolIndex = buildSymbolIndex(alphabet);
+
+    findMinMax();
+
+    /*
+     * crude heuristic for now...
+     */
+    peptide = alphabet.length >= 20;
+  }
+
+  /**
+   * Record the minimum and maximum score values
+   */
+  protected void findMinMax()
+  {
+    float min = Float.MAX_VALUE;
+    float max = -Float.MAX_VALUE;
+    if (matrix != null)
+    {
+      for (float[] row : matrix)
+      {
+        if (row != null)
+        {
+          for (float f : row)
+          {
+            min = Math.min(min, f);
+            max = Math.max(max, f);
+          }
+        }
+      }
+    }
+    minValue = min;
+    maxValue = max;
+  }
+
+  /**
+   * Returns an array A where A[i] is the position in the alphabet array of the
+   * character whose value is i. For example if the alphabet is { 'A', 'D', 'X'
+   * } then A['D'] = A[68] = 1.
+   * <p>
+   * Unmapped characters (not in the alphabet) get an index of -1.
+   * <p>
+   * Mappings are added automatically for lower case symbols (for non case
+   * sensitive scoring), unless they are explicitly present in the alphabet (are
+   * scored separately in the score matrix).
+   * <p>
+   * the gap character (space, dash or dot) included in the alphabet (if any) is
+   * recorded in a field
+   * 
+   * @param alphabet
+   * @return
+   */
+  short[] buildSymbolIndex(char[] alphabet)
+  {
+    short[] index = new short[MAX_ASCII + 1];
+    Arrays.fill(index, UNMAPPED);
+    short pos = 0;
+    for (char c : alphabet)
+    {
+      if (c <= MAX_ASCII)
+      {
+        index[c] = pos;
+      }
+
+      /*
+       * also map lower-case character (unless separately mapped)
+       */
+      if (c >= 'A' && c <= 'Z')
+      {
+        short lowerCase = (short) (c + ('a' - 'A'));
+        if (index[lowerCase] == UNMAPPED)
+        {
+          index[lowerCase] = pos;
+        }
+      }
+      pos++;
+    }
+    return index;
+  }
+
+  @Override
+  public String getName()
+  {
+    return name;
+  }
+
+  @Override
+  public String getDescription()
+  {
+    return description;
+  }
+
+  @Override
+  public boolean isDNA()
+  {
+    return !peptide;
+  }
+
+  @Override
+  public boolean isProtein()
+  {
+    return peptide;
+  }
+
+  /**
+   * Returns a copy of the score matrix as used in getPairwiseScore. If using
+   * this matrix directly, callers <em>must</em> also call
+   * <code>getMatrixIndex</code> in order to get the matrix index for each
+   * character (symbol).
+   * 
+   * @return
+   * @see #getMatrixIndex(char)
+   */
+  public float[][] getMatrix()
+  {
+    float[][] v = new float[matrix.length][matrix.length];
+    for (int i = 0; i < matrix.length; i++)
+    {
+      v[i] = Arrays.copyOf(matrix[i], matrix[i].length);
+    }
+    return v;
+  }
+
+  /**
+   * Answers the matrix index for a given character, or -1 if unmapped in the
+   * matrix. Use this method only if using <code>getMatrix</code> in order to
+   * compute scores directly (without symbol lookup) for efficiency.
+   * 
+   * @param c
+   * @return
+   * @see #getMatrix()
+   */
+  public int getMatrixIndex(char c)
+  {
+    if (c < symbolIndex.length)
+    {
+      return symbolIndex[c];
+    }
+    else
+    {
+      return UNMAPPED;
+    }
+  }
+
+  /**
+   * Returns the pairwise score for substituting c with d. If either c or d is
+   * an unexpected character, returns 1 for identity (c == d), else the minimum
+   * score value in the matrix.
+   */
+  @Override
+  public float getPairwiseScore(char c, char d)
+  {
+    if (c >= symbolIndex.length)
+    {
+      System.err.println(String.format(BAD_ASCII_ERROR, c));
+      return 0;
+    }
+    if (d >= symbolIndex.length)
+    {
+      System.err.println(String.format(BAD_ASCII_ERROR, d));
+      return 0;
+    }
+
+    int cIndex = symbolIndex[c];
+    int dIndex = symbolIndex[d];
+    if (cIndex != UNMAPPED && dIndex != UNMAPPED)
+    {
+      return matrix[cIndex][dIndex];
+    }
+
+    /*
+     * one or both symbols not found in the matrix
+     * currently scoring as 1 (for identity) or the minimum
+     * matrix score value (otherwise)
+     * (a case could be made for using minimum row/column value instead)
+     */
+    return c == d ? UNKNOWN_IDENTITY_SCORE : getMinimumScore();
+  }
+
+  /**
+   * pretty print the matrix
+   */
+  @Override
+  public String toString()
+  {
+    return outputMatrix(false);
+  }
+
+  /**
+   * Print the score matrix, optionally formatted as html, with the alphabet
+   * symbols as column headings and at the start of each row.
+   * <p>
+   * The non-html format should give an output which can be parsed as a score
+   * matrix file
+   * 
+   * @param html
+   * @return
+   */
+  public String outputMatrix(boolean html)
+  {
+    StringBuilder sb = new StringBuilder(512);
+
+    /*
+     * heading row with alphabet
+     */
+    if (html)
+    {
+      sb.append("<table border=\"1\">");
+      sb.append(html ? "<tr><th></th>" : "");
+    }
+    else
+    {
+      sb.append("ScoreMatrix ").append(getName()).append("\n");
+    }
+    for (char sym : symbols)
+    {
+      if (html)
+      {
+        sb.append("<th>&nbsp;").append(sym).append("&nbsp;</th>");
+      }
+      else
+      {
+        sb.append("\t").append(sym);
+      }
+    }
+    sb.append(html ? "</tr>\n" : "\n");
+
+    /*
+     * table of scores
+     */
+    for (char c1 : symbols)
+    {
+      if (html)
+      {
+        sb.append("<tr><td>");
+      }
+      sb.append(c1).append(html ? "</td>" : "");
+      for (char c2 : symbols)
+      {
+        sb.append(html ? "<td>" : "\t")
+                .append(matrix[symbolIndex[c1]][symbolIndex[c2]])
+                .append(html ? "</td>" : "");
+      }
+      sb.append(html ? "</tr>\n" : "\n");
+    }
+    if (html)
+    {
+      sb.append("</table>");
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Answers the number of symbols coded for (also equal to the number of rows
+   * and columns of the score matrix)
+   * 
+   * @return
+   */
+  public int getSize()
+  {
+    return symbols.length;
+  }
+
+  /**
+   * Computes an NxN matrix where N is the number of sequences, and entry [i, j]
+   * is sequence[i] pairwise multiplied with sequence[j], as a sum of scores
+   * computed using the current score matrix. For example
+   * <ul>
+   * <li>Sequences:</li>
+   * <li>FKL</li>
+   * <li>R-D</li>
+   * <li>QIA</li>
+   * <li>GWC</li>
+   * <li>Score matrix is BLOSUM62</li>
+   * <li>Gaps treated same as X (unknown)</li>
+   * <li>product [0, 0] = F.F + K.K + L.L = 6 + 5 + 4 = 15</li>
+   * <li>product [1, 1] = R.R + -.- + D.D = 5 + -1 + 6 = 10</li>
+   * <li>product [2, 2] = Q.Q + I.I + A.A = 5 + 4 + 4 = 13</li>
+   * <li>product [3, 3] = G.G + W.W + C.C = 6 + 11 + 9 = 26</li>
+   * <li>product[0, 1] = F.R + K.- + L.D = -3 + -1 + -3 = -8
+   * <li>and so on</li>
+   * </ul>
+   * This method is thread-safe.
+   */
+  @Override
+  public MatrixI findSimilarities(AlignmentView seqstrings,
+          SimilarityParamsI options)
+  {
+    char gapChar = scoreGapAsAny ? (seqstrings.isNa() ? 'N' : 'X')
+            : GAP_CHARACTER;
+    String[] seqs = seqstrings.getSequenceStrings(gapChar);
+    return findSimilarities(seqs, options);
+  }
+
+  /**
+   * Computes pairwise similarities of a set of sequences using the given
+   * parameters
+   * 
+   * @param seqs
+   * @param params
+   * @return
+   */
+  protected MatrixI findSimilarities(String[] seqs, SimilarityParamsI params)
+  {
+    double[][] values = new double[seqs.length][];
+    for (int row = 0; row < seqs.length; row++)
+    {
+      values[row] = new double[seqs.length];
+      for (int col = 0; col < seqs.length; col++)
+      {
+        double total = computeSimilarity(seqs[row], seqs[col], params);
+        values[row][col] = total;
+      }
+    }
+    return new Matrix(values);
+  }
+
+  /**
+   * Calculates the pairwise similarity of two strings using the given
+   * calculation parameters
+   * 
+   * @param seq1
+   * @param seq2
+   * @param params
+   * @return
+   */
+  protected double computeSimilarity(String seq1, String seq2,
+          SimilarityParamsI params)
+  {
+    int len1 = seq1.length();
+    int len2 = seq2.length();
+    double total = 0;
+
+    int width = Math.max(len1, len2);
+    for (int i = 0; i < width; i++)
+    {
+      if (i >= len1 || i >= len2)
+      {
+        /*
+         * off the end of one sequence; stop if we are only matching
+         * on the shorter sequence length, else treat as trailing gap
+         */
+        if (params.denominateByShortestLength())
+        {
+          break;
+        }
+      }
+
+      char c1 = i >= len1 ? GAP_CHARACTER : seq1.charAt(i);
+      char c2 = i >= len2 ? GAP_CHARACTER : seq2.charAt(i);
+      boolean gap1 = Comparison.isGap(c1);
+      boolean gap2 = Comparison.isGap(c2);
+
+      if (gap1 && gap2)
+      {
+        /*
+         * gap-gap: include if options say so, else ignore
+         */
+        if (!params.includeGappedColumns())
+        {
+          continue;
+        }
+      }
+      else if (gap1 || gap2)
+      {
+        /*
+         * gap-residue: score if options say so
+         */
+        if (!params.includeGaps())
+        {
+          continue;
+        }
+      }
+      float score = getPairwiseScore(c1, c2);
+      total += score;
+    }
+    return total;
+  }
+
+  /**
+   * Answers a hashcode computed from the symbol alphabet and the matrix score
+   * values
+   */
+  @Override
+  public int hashCode()
+  {
+    int hs = Arrays.hashCode(symbols);
+    for (float[] row : matrix)
+    {
+      hs = hs * 31 + Arrays.hashCode(row);
+    }
+    return hs;
+  }
+
+  /**
+   * Answers true if the argument is a ScoreMatrix with the same symbol alphabet
+   * and score values, else false
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (!(obj instanceof ScoreMatrix))
+    {
+      return false;
+    }
+    ScoreMatrix sm = (ScoreMatrix) obj;
+    if (Arrays.equals(symbols, sm.symbols)
+            && Arrays.deepEquals(matrix, sm.matrix))
+    {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Returns the alphabet the matrix scores for, as a string of characters
+   * 
+   * @return
+   */
+  String getSymbols()
+  {
+    return new String(symbols);
+  }
+
+  public float getMinimumScore()
+  {
+    return minValue;
+  }
+
+  public float getMaximumScore()
+  {
+    return maxValue;
+  }
+
+  @Override
+  public ScoreModelI getInstance(AlignmentViewPanel avp)
+  {
+    return this;
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/ScoreModels.java b/src/jalview/analysis/scoremodels/ScoreModels.java
new file mode 100644 (file)
index 0000000..654136a
--- /dev/null
@@ -0,0 +1,141 @@
+package jalview.analysis.scoremodels;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.analysis.ScoreModelI;
+import jalview.io.DataSourceType;
+import jalview.io.FileParse;
+import jalview.io.ScoreMatrixFile;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A class that can register and serve instances of ScoreModelI
+ */
+public class ScoreModels
+{
+  private final ScoreMatrix BLOSUM62;
+
+  private final ScoreMatrix PAM250;
+
+  private final ScoreMatrix DNA;
+
+  private static ScoreModels instance = new ScoreModels();
+
+  private Map<String, ScoreModelI> models;
+
+  public static ScoreModels getInstance()
+  {
+    return instance;
+  }
+
+  /**
+   * Private constructor to enforce use of singleton. Registers Jalview's
+   * "built-in" score models:
+   * <ul>
+   * <li>BLOSUM62</li>
+   * <li>PAM250</li>
+   * <li>PID</li>
+   * <li>DNA</li>
+   * <li>Sequence Feature Similarity</li>
+   * </ul>
+   */
+  private ScoreModels()
+  {
+    /*
+     * using LinkedHashMap keeps models ordered as added
+     */
+    models = new LinkedHashMap<String, ScoreModelI>();
+    BLOSUM62 = loadScoreMatrix("scoreModel/blosum62.scm");
+    PAM250 = loadScoreMatrix("scoreModel/pam250.scm");
+    registerScoreModel(new PIDModel());
+    DNA = loadScoreMatrix("scoreModel/dna.scm");
+    registerScoreModel(new FeatureDistanceModel());
+  }
+
+  /**
+   * Tries to load a score matrix from the given resource file, and if
+   * successful, registers it.
+   * 
+   * @param string
+   * @return
+   */
+  ScoreMatrix loadScoreMatrix(String resourcePath)
+  {
+    try
+    {
+      /*
+       * delegate parsing to ScoreMatrixFile
+       */
+      FileParse fp = new FileParse(resourcePath, DataSourceType.CLASSLOADER);
+      ScoreMatrix sm = new ScoreMatrixFile(fp).parseMatrix();
+      registerScoreModel(sm);
+      return sm;
+    } catch (IOException e)
+    {
+      System.err.println("Error reading " + resourcePath + ": "
+              + e.getMessage());
+    }
+    return null;
+  }
+
+  /**
+   * Answers an iterable set of the registered score models. Currently these are
+   * returned in the order in which they were registered.
+   * 
+   * @return
+   */
+  public Iterable<ScoreModelI> getModels()
+  {
+    return models.values();
+  }
+
+  /**
+   * Returns an instance of a score model for the given name. If the model is of
+   * 'view dependent' type (e.g. feature similarity), instantiates a new
+   * instance configured for the given view. Otherwise returns a cached instance
+   * of the score model.
+   * 
+   * @param name
+   * @param avp
+   * @return
+   */
+  public ScoreModelI getScoreModel(String name, AlignmentViewPanel avp)
+  {
+    ScoreModelI model = models.get(name);
+    return model == null ? null : model.getInstance(avp);
+  }
+
+  public void registerScoreModel(ScoreModelI sm)
+  {
+    ScoreModelI sm2 = models.get(sm.getName());
+    if (sm2 != null)
+    {
+      System.err.println("Warning: replacing score model " + sm2.getName());
+    }
+    models.put(sm.getName(), sm);
+  }
+
+  /**
+   * Returns the default peptide or nucleotide score model, currently BLOSUM62
+   * or DNA
+   * 
+   * @param forPeptide
+   * @return
+   */
+  public ScoreMatrix getDefaultModel(boolean forPeptide)
+  {
+    return forPeptide ? BLOSUM62 : DNA;
+  }
+
+  public ScoreMatrix getBlosum62()
+  {
+    return BLOSUM62;
+  }
+
+  public ScoreMatrix getPam250()
+  {
+    return PAM250;
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/SimilarityParams.java b/src/jalview/analysis/scoremodels/SimilarityParams.java
new file mode 100644 (file)
index 0000000..e5751ca
--- /dev/null
@@ -0,0 +1,130 @@
+package jalview.analysis.scoremodels;
+
+import jalview.api.analysis.SimilarityParamsI;
+
+/**
+ * A class to hold parameters that configure the pairwise similarity
+ * calculation. Based on the paper
+ * 
+ * <pre>
+ * Quantification of the variation in percentage identity for protein sequence alignments
+ * Raghava, GP and Barton, GJ
+ * BMC Bioinformatics. 2006 Sep 19;7:415
+ * </pre>
+ * 
+ * @see https://www.ncbi.nlm.nih.gov/pubmed/16984632
+ */
+public class SimilarityParams implements SimilarityParamsI
+{
+  /**
+   * Based on Jalview's Comparison.PID method, which includes gaps and counts
+   * them as matching; it counts over the length of the shorter sequence
+   */
+  public static final SimilarityParamsI Jalview = new SimilarityParams(
+          true, true, true, true);
+
+  /**
+   * 'SeqSpace' mode PCA calculation includes gaps but does not count them as
+   * matching; it uses the longest sequence length
+   */
+  public static final SimilarityParamsI SeqSpace = new SimilarityParams(
+          true, false, true, true);
+
+  /**
+   * as described in the Raghava-Barton paper
+   * <ul>
+   * <li>ignores gap-gap</li>
+   * <li>does not score gap-residue</li>
+   * <li>includes gap-residue in lengths</li>
+   * <li>matches on longer of two sequences</li>
+   * </ul>
+   */
+  public static final SimilarityParamsI PID1 = new SimilarityParams(false,
+          false, true, false);
+
+  /**
+   * as described in the Raghava-Barton paper
+   * <ul>
+   * <li>ignores gap-gap</li>
+   * <li>ignores gap-residue</li>
+   * <li>matches on longer of two sequences</li>
+   * </ul>
+   */
+  public static final SimilarityParamsI PID2 = new SimilarityParams(false,
+          false, false, false);
+
+  /**
+   * as described in the Raghava-Barton paper
+   * <ul>
+   * <li>ignores gap-gap</li>
+   * <li>ignores gap-residue</li>
+   * <li>matches on shorter of sequences only</li>
+   * </ul>
+   */
+  public static final SimilarityParamsI PID3 = new SimilarityParams(false,
+          false, false, true);
+
+  /**
+   * as described in the Raghava-Barton paper
+   * <ul>
+   * <li>ignores gap-gap</li>
+   * <li>does not score gap-residue</li>
+   * <li>includes gap-residue in lengths</li>
+   * <li>matches on shorter of sequences only</li>
+   * </ul>
+   */
+  public static final SimilarityParamsI PID4 = new SimilarityParams(false,
+          false, true, true);
+
+  private boolean includeGappedColumns;
+
+  private boolean matchGaps;
+
+  private boolean includeGaps;
+
+  private boolean denominateByShortestLength;
+
+  /**
+   * Constructor
+   * 
+   * @param includeGapGap
+   * @param matchGapResidue
+   * @param includeGapResidue
+   *          if true, gapped positions are counted for normalisation by length
+   * @param shortestLength
+   *          if true, the denominator is the shorter sequence length (possibly
+   *          including gaps)
+   */
+  public SimilarityParams(boolean includeGapGap, boolean matchGapResidue,
+          boolean includeGapResidue, boolean shortestLength)
+  {
+    includeGappedColumns = includeGapGap;
+    matchGaps = matchGapResidue;
+    includeGaps = includeGapResidue;
+    denominateByShortestLength = shortestLength;
+  }
+
+  @Override
+  public boolean includeGaps()
+  {
+    return includeGaps;
+  }
+
+  @Override
+  public boolean denominateByShortestLength()
+  {
+    return denominateByShortestLength;
+  }
+
+  @Override
+  public boolean includeGappedColumns()
+  {
+    return includeGappedColumns;
+  }
+
+  @Override
+  public boolean matchGaps()
+  {
+    return matchGaps;
+  }
+}
diff --git a/src/jalview/analysis/scoremodels/SimilarityScoreModel.java b/src/jalview/analysis/scoremodels/SimilarityScoreModel.java
new file mode 100644 (file)
index 0000000..dae1f62
--- /dev/null
@@ -0,0 +1,43 @@
+package jalview.analysis.scoremodels;
+
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.math.MatrixI;
+
+public abstract class SimilarityScoreModel implements ScoreModelI
+{
+
+  /**
+   * Computed similarity scores are converted to distance scores by subtracting
+   * every value from the maximum value. That is, maximum similarity corresponds
+   * to zero distance, and smaller similarities to larger distances.
+   */
+  @Override
+  public MatrixI findDistances(AlignmentView seqData,
+          SimilarityParamsI options)
+  {
+    MatrixI similarities = findSimilarities(seqData, options);
+
+    MatrixI distances = similarityToDistance(similarities);
+
+    return distances;
+  }
+
+  /**
+   * Converts a matrix of similarity scores to distance scores, by reversing the
+   * range of the scores, mapping the maximum to zero. The input matrix is not
+   * modified.
+   * 
+   * @param similarities
+   */
+  public static MatrixI similarityToDistance(MatrixI similarities)
+  {
+    MatrixI distances = similarities.copy();
+
+    distances.reverseRange(true);
+
+    return distances;
+  }
+
+}
 package jalview.analysis.scoremodels;
 
 import jalview.analysis.AlignSeq;
+import jalview.api.AlignmentViewPanel;
 import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
 import jalview.datamodel.AlignmentView;
 import jalview.datamodel.SequenceI;
+import jalview.math.Matrix;
+import jalview.math.MatrixI;
 import jalview.util.Comparison;
 
-public class SWScoreModel implements ScoreModelI
+/**
+ * A class that computes pairwise similarity scores using the Smith-Waterman
+ * alignment algorithm
+ */
+public class SmithWatermanModel extends SimilarityScoreModel
 {
+  private static final String NAME = "Smith Waterman Score";
+
+  private String description;
+
+  /**
+   * Constructor
+   */
+  public SmithWatermanModel()
+  {
+  }
 
   @Override
-  public float[][] findDistances(AlignmentView seqData)
+  public MatrixI findSimilarities(AlignmentView seqData,
+          SimilarityParamsI options)
   {
     SequenceI[] sequenceString = seqData.getVisibleAlignment(
-            Comparison.GapChars.charAt(0)).getSequencesArray();
+            Comparison.GAP_SPACE).getSequencesArray();
     int noseqs = sequenceString.length;
-    float[][] distance = new float[noseqs][noseqs];
+    double[][] distances = new double[noseqs][noseqs];
 
-    float max = -1;
+    double max = -1;
 
     for (int i = 0; i < (noseqs - 1); i++)
     {
@@ -48,31 +67,22 @@ public class SWScoreModel implements ScoreModelI
         as.calcScoreMatrix();
         as.traceAlignment();
         as.printAlignment(System.out);
-        distance[i][j] = (float) as.maxscore;
+        distances[i][j] = as.maxscore;
 
-        if (max < distance[i][j])
+        if (max < distances[i][j])
         {
-          max = distance[i][j];
+          max = distances[i][j];
         }
       }
     }
 
-    for (int i = 0; i < (noseqs - 1); i++)
-    {
-      for (int j = i; j < noseqs; j++)
-      {
-        distance[i][j] = max - distance[i][j];
-        distance[j][i] = distance[i][j];
-      }
-    }
-
-    return distance;
+    return new Matrix(distances);
   }
 
   @Override
   public String getName()
   {
-    return "Smith Waterman Score";
+    return NAME;
   }
 
   @Override
@@ -87,8 +97,15 @@ public class SWScoreModel implements ScoreModelI
     return true;
   }
 
-  public String toString()
+  @Override
+  public String getDescription()
+  {
+    return description;
+  }
+
+  @Override
+  public ScoreModelI getInstance(AlignmentViewPanel avp)
   {
-    return "Score between two sequences aligned with Smith Waterman with default Peptide/Nucleotide matrix";
+    return this;
   }
 }
index 7067328..9e6d1c0 100644 (file)
@@ -31,9 +31,12 @@ import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemes.ColourSchemeI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.Color;
+import java.awt.Font;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
@@ -45,7 +48,13 @@ import java.util.Map;
 public interface AlignViewportI extends ViewStyleI
 {
 
-  int getEndRes();
+  /**
+   * Get the ranges object containing details of the start and end sequences and
+   * residues
+   * 
+   * @return
+   */
+  public ViewportRanges getRanges();
 
   /**
    * calculate the height for visible annotation, revalidating bounds where
@@ -79,6 +88,14 @@ public interface AlignViewportI extends ViewStyleI
 
   ColourSchemeI getGlobalColourScheme();
 
+  /**
+   * Returns an object that describes colouring (including any thresholding or
+   * fading) of the alignment
+   * 
+   * @return
+   */
+  ResidueShaderI getResidueShading();
+
   AlignmentI getAlignment();
 
   ColumnSelection getColumnSelection();
@@ -164,7 +181,7 @@ public interface AlignViewportI extends ViewStyleI
 
   /**
    * 
-   * @return the alignment annotatino row for the structure consensus
+   * @return the alignment annotation row for the structure consensus
    *         calculation
    */
   AlignmentAnnotation getAlignmentStrucConsensusAnnotation();
@@ -177,11 +194,13 @@ public interface AlignViewportI extends ViewStyleI
   void setRnaStructureConsensusHash(Hashtable[] hStrucConsensus);
 
   /**
-   * set global colourscheme
+   * Sets the colour scheme for the background alignment (as distinct from
+   * sub-groups, which may have their own colour schemes). A null value is used
+   * for no residue colour (white).
    * 
-   * @param rhc
+   * @param cs
    */
-  void setGlobalColourScheme(ColourSchemeI rhc);
+  void setGlobalColourScheme(ColourSchemeI cs);
 
   Map<SequenceI, SequenceCollectionI> getHiddenRepSequences();
 
@@ -452,4 +471,27 @@ public interface AlignViewportI extends ViewStyleI
    * @return search results or null
    */
   SearchResultsI getSearchResults();
+
+  /**
+   * Updates view settings with the given font. You may need to call
+   * AlignmentPanel.fontChanged to update the layout geometry.
+   * 
+   * @param setGrid
+   *          when true, charWidth/height is set according to font metrics
+   */
+  void setFont(Font newFont, boolean b);
+
+  /**
+   * Answers true if split screen protein and cDNA use the same font
+   * 
+   * @return
+   */
+  boolean isProteinFontAsCdna();
+
+  /**
+   * Set the flag for whether split screen protein and cDNA use the same font
+   * 
+   * @return
+   */
+  void setProteinFontAsCdna(boolean b);
 }
diff --git a/src/jalview/api/AlignmentColsCollectionI.java b/src/jalview/api/AlignmentColsCollectionI.java
new file mode 100644 (file)
index 0000000..603da98
--- /dev/null
@@ -0,0 +1,13 @@
+package jalview.api;
+
+public interface AlignmentColsCollectionI extends Iterable<Integer>
+{
+  /**
+   * Answers if the column at the given position is hidden.
+   * 
+   * @param c
+   *          the column index to check
+   * @return true if the column at the position is hidden
+   */
+  public boolean isHidden(int c);
+}
diff --git a/src/jalview/api/AlignmentRowsCollectionI.java b/src/jalview/api/AlignmentRowsCollectionI.java
new file mode 100644 (file)
index 0000000..09b039d
--- /dev/null
@@ -0,0 +1,24 @@
+package jalview.api;
+
+import jalview.datamodel.SequenceI;
+
+public interface AlignmentRowsCollectionI extends Iterable<Integer>
+{
+  /**
+   * Answers if the sequence at the given position is hidden.
+   * 
+   * @param r
+   *          the row index to check
+   * @return true if the sequence at r is hidden
+   */
+  public boolean isHidden(int r);
+
+  /**
+   * Answers the sequence at the given position in the alignment
+   * 
+   * @param r
+   *          the row index to locate
+   * @return the sequence
+   */
+  public SequenceI getSequence(int r);
+}
index 2bf2782..1b579ad 100644 (file)
@@ -20,7 +20,7 @@
  */
 package jalview.api;
 
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 
 /**
@@ -50,7 +50,7 @@ public interface ComplexAlignFile
    * 
    * @return
    */
-  public ColumnSelection getColumnSelection();
+  public HiddenColumns getHiddenColumns();
 
   /**
    * Retrieves hidden sequences from a complex file parser
index f54231e..9d2d7f4 100644 (file)
@@ -24,6 +24,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 
 import java.awt.Color;
+import java.awt.Graphics;
 import java.util.List;
 import java.util.Map;
 
@@ -37,18 +38,33 @@ public interface FeatureRenderer
 {
 
   /**
-   * compute the perceived colour for a given column position in sequenceI,
-   * taking transparency and feature visibility into account.
+   * Computes the feature colour for a given sequence and column position,
+   * taking into account sequence feature locations, feature colour schemes,
+   * render ordering, feature and feature group visibility, and transparency.
+   * <p>
+   * The graphics argument should be provided if transparency is applied
+   * (getTransparency() < 1). With feature transparency, visible features are
+   * written to the graphics context and the composite colour may be read off
+   * from it. In this case, the returned feature colour is not the composite
+   * colour but that of the last feature drawn.
+   * <p>
+   * If no transparency applies, then the graphics argument may be null, and the
+   * returned colour is the one that would be drawn for the feature.
+   * <p>
+   * Returns null if there is no visible feature at the position.
+   * <p>
+   * This is provided to support rendering of feature colours other than on the
+   * sequence alignment, including by structure viewers and the overview window.
+   * Note this method takes no account of whether the sequence or column is
+   * hidden.
    * 
-   * @param col
-   *          - background colour (due to alignment/group shading schemes, etc).
-   * @param sequenceI
-   *          - sequence providing features
-   * @param r
-   *          - column position
+   * @param sequence
+   * @param column
+   *          aligned column position (1..)
+   * @param g
    * @return
    */
-  Color findFeatureColour(Color col, SequenceI sequenceI, int r);
+  Color findFeatureColour(SequenceI sequence, int column, Graphics g);
 
   /**
    * trigger the feature discovery process for a newly created feature renderer.
@@ -132,14 +148,27 @@ public interface FeatureRenderer
   void setGroupVisibility(String group, boolean visible);
 
   /**
-   * Returns features at the specified position on the given sequence.
+   * 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 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)
@@ -150,9 +179,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();
 
@@ -170,4 +199,20 @@ public interface FeatureRenderer
    */
   void setVisible(String featureType);
 
+  /**
+   * Sets the transparency value, between 0 (full transparency) and 1 (no
+   * transparency)
+   * 
+   * @param value
+   */
+  void setTransparency(float value);
+
+  /**
+   * Returns the transparency value, between 0 (full transparency) and 1 (no
+   * transparency)
+   * 
+   * @return
+   */
+  float getTransparency();
+
 }
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 d708902..54f7fb6 100644 (file)
 package jalview.api;
 
 import jalview.datamodel.SequenceI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 
 import java.awt.Color;
 
 public interface SequenceRenderer
 {
 
-  Color getResidueBoxColour(SequenceI sequenceI, int r);
-
-  Color getResidueColour(SequenceI seq, int position, FeatureRenderer fr);
+  Color getResidueColour(SequenceI seq, int position,
+          FeatureColourFinder finder);
 
 }
index c795f3f..367a0de 100644 (file)
@@ -100,7 +100,7 @@ public interface SiftsClientI
    * @return Sequence<->Structure mapping as int[][]
    * @throws SiftsException
    */
-  public StringBuffer getMappingOutput(MappingOutputPojo mop)
+  public StringBuilder getMappingOutput(MappingOutputPojo mop)
           throws SiftsException;
 
   /**
index db82dcf..2b554ea 100644 (file)
@@ -257,4 +257,18 @@ public interface ViewStyleI
    * @return
    */
   void setScaleProteinAsCdna(boolean b);
+
+  /**
+   * Answers true if split screen protein and cDNA use the same font
+   * 
+   * @return
+   */
+  boolean isProteinFontAsCdna();
+
+  /**
+   * Set the flag for whether split screen protein and cDNA use the same font
+   * 
+   * @return
+   */
+  void setProteinFontAsCdna(boolean b);
 }
diff --git a/src/jalview/api/analysis/PairwiseScoreModelI.java b/src/jalview/api/analysis/PairwiseScoreModelI.java
new file mode 100644 (file)
index 0000000..ecada36
--- /dev/null
@@ -0,0 +1,22 @@
+package jalview.api.analysis;
+
+/**
+ * An interface that describes classes that can compute similarity (aka
+ * substitution) scores for pairs of residues
+ */
+public interface PairwiseScoreModelI
+{
+  /**
+   * Answers a similarity score between two sequence characters (for
+   * substitution of the first by the second). Typically the highest scores are
+   * for identity, and the lowest for substitution of a residue by one with very
+   * different properties.
+   * 
+   * @param c
+   * @param d
+   * @return
+   */
+  abstract public float getPairwiseScore(char c, char d);
+  // TODO make this static when Java 8
+
+}
index 31a1c32..7f138cd 100644 (file)
@@ -1,36 +1,79 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
 package jalview.api.analysis;
 
+import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.AlignmentView;
+import jalview.math.MatrixI;
 
 public interface ScoreModelI
 {
-
-  float[][] findDistances(AlignmentView seqData);
-
+  /**
+   * Answers a name for the score model, suitable for display in menus. Names
+   * should be unique across score models in use.
+   * 
+   * @return
+   * @see jalview.analysis.scoremodels.ScoreModels#forName(String)
+   */
   String getName();
 
+  /**
+   * Answers an informative description of the model, suitable for use in
+   * tooltips. Descriptions may be internationalised, and need not be unique
+   * (but should be).
+   * 
+   * @return
+   */
+  String getDescription();
+
+  /**
+   * Answers true if this model is applicable for nucleotide data (so should be
+   * shown in menus in that context)
+   * 
+   * @return
+   */
   boolean isDNA();
 
+  /**
+   * Answers true if this model is applicable for peptide data (so should be
+   * shown in menus in that context)
+   * 
+   * @return
+   */
   boolean isProtein();
 
+  // TODO getName, isDNA, isProtein can be static methods in Java 8
+
+  /**
+   * Returns a distance score for the given sequence regions, that is, a matrix
+   * whose value [i][j] is the distance of sequence i from sequence j by some
+   * measure. The options parameter provides configuration choices for how the
+   * similarity score is calculated.
+   * 
+   * @param seqData
+   * @param options
+   * @return
+   */
+
+  MatrixI findDistances(AlignmentView seqData, SimilarityParamsI options);
+
+  /**
+   * Returns a similarity score for the given sequence regions, that is, a
+   * matrix whose value [i][j] is the similarity of sequence i to sequence j by
+   * some measure. The options parameter provides configuration choices for how
+   * the similarity score is calculated.
+   * 
+   * @param seqData
+   * @param options
+   * @return
+   */
+  MatrixI findSimilarities(AlignmentView seqData, SimilarityParamsI options);
+
+  /**
+   * Returns a score model object configured for the given alignment view.
+   * Depending on the score model, this may just be a singleton instance, or a
+   * new instance configured with data from the view.
+   * 
+   * @param avp
+   * @return
+   */
+  ScoreModelI getInstance(AlignmentViewPanel avp);
 }
diff --git a/src/jalview/api/analysis/SimilarityParamsI.java b/src/jalview/api/analysis/SimilarityParamsI.java
new file mode 100644 (file)
index 0000000..581449f
--- /dev/null
@@ -0,0 +1,43 @@
+package jalview.api.analysis;
+
+/**
+ * A description of options when computing percentage identity of two aligned
+ * sequences
+ */
+public interface SimilarityParamsI
+{
+  /**
+   * Answers true if gap-gap aligned positions should be included in the
+   * calculation
+   * 
+   * @return
+   */
+  boolean includeGappedColumns();
+
+  /**
+   * Answers true if gap-residue alignment is considered a match
+   * 
+   * @return
+   */
+  // TODO is this specific to a PID score only?
+  // score matrix will compute whatever is configured for gap-residue
+  boolean matchGaps();
+
+  /**
+   * Answers true if gaps are included in the calculation. This may affect the
+   * calculated score, the denominator (normalisation factor) of the score, or
+   * both. Gap-gap positions are included if this and includeGappedColumns both
+   * answer true.
+   * 
+   * @return
+   */
+  boolean includeGaps();
+
+  /**
+   * Answers true if only the shortest sequence length is used to divide the
+   * total score, false if the longest sequence length
+   * 
+   * @return
+   */
+  boolean denominateByShortestLength();
+}
index 8f7a15b..d5b7132 100644 (file)
@@ -39,13 +39,17 @@ import jalview.io.DataSourceType;
 import jalview.io.FileFormatI;
 import jalview.io.FileFormats;
 import jalview.io.SequenceAnnotationReport;
+import jalview.renderer.ResidueShader;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemes.Blosum62ColourScheme;
 import jalview.schemes.BuriedColourScheme;
 import jalview.schemes.ClustalxColourScheme;
 import jalview.schemes.HelixColourScheme;
 import jalview.schemes.HydrophobicColourScheme;
+import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.NucleotideColourScheme;
 import jalview.schemes.PIDColourScheme;
+import jalview.schemes.PurinePyrimidineColourScheme;
 import jalview.schemes.StrandColourScheme;
 import jalview.schemes.TaylorColourScheme;
 import jalview.schemes.TurnColourScheme;
@@ -61,12 +65,14 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.Vector;
 
@@ -77,33 +83,43 @@ public class APopupMenu extends java.awt.PopupMenu implements
 
   MenuItem editGroupName = new MenuItem();
 
-  protected MenuItem clustalColour = new MenuItem();
+  CheckboxMenuItem noColour = new CheckboxMenuItem();
 
-  protected MenuItem zappoColour = new MenuItem();
+  protected CheckboxMenuItem clustalColour = new CheckboxMenuItem();
 
-  protected MenuItem taylorColour = new MenuItem();
+  protected CheckboxMenuItem zappoColour = new CheckboxMenuItem();
 
-  protected MenuItem hydrophobicityColour = new MenuItem();
+  protected CheckboxMenuItem taylorColour = new CheckboxMenuItem();
 
-  protected MenuItem helixColour = new MenuItem();
+  protected CheckboxMenuItem hydrophobicityColour = new CheckboxMenuItem();
 
-  protected MenuItem strandColour = new MenuItem();
+  protected CheckboxMenuItem helixColour = new CheckboxMenuItem();
 
-  protected MenuItem turnColour = new MenuItem();
+  protected CheckboxMenuItem strandColour = new CheckboxMenuItem();
 
-  protected MenuItem buriedColour = new MenuItem();
+  protected CheckboxMenuItem turnColour = new CheckboxMenuItem();
 
-  protected CheckboxMenuItem abovePIDColour = new CheckboxMenuItem();
+  protected CheckboxMenuItem buriedColour = new CheckboxMenuItem();
+
+  protected CheckboxMenuItem PIDColour = new CheckboxMenuItem();
+
+  protected CheckboxMenuItem BLOSUM62Colour = new CheckboxMenuItem();
+
+  CheckboxMenuItem nucleotideColour = new CheckboxMenuItem();
+
+  CheckboxMenuItem purinePyrimidineColour = new CheckboxMenuItem();
 
   protected MenuItem userDefinedColour = new MenuItem();
 
-  protected MenuItem PIDColour = new MenuItem();
+  protected CheckboxMenuItem abovePIDColour = new CheckboxMenuItem();
 
-  protected MenuItem BLOSUM62Colour = new MenuItem();
+  MenuItem modifyPID = new MenuItem();
 
-  MenuItem noColourmenuItem = new MenuItem();
+  protected CheckboxMenuItem conservationColour = new CheckboxMenuItem();
 
-  protected CheckboxMenuItem conservationMenuItem = new CheckboxMenuItem();
+  MenuItem modifyConservation = new MenuItem();
+
+  MenuItem noColourmenuItem = new MenuItem();
 
   final AlignmentPanel ap;
 
@@ -111,8 +127,6 @@ public class APopupMenu extends java.awt.PopupMenu implements
 
   MenuItem createGroupMenuItem = new MenuItem();
 
-  MenuItem nucleotideMenuItem = new MenuItem();
-
   Menu colourMenu = new Menu();
 
   CheckboxMenuItem showBoxes = new CheckboxMenuItem();
@@ -197,7 +211,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
   Menu menu1 = new Menu();
 
   public APopupMenu(AlignmentPanel apanel, final SequenceI seq,
-          Vector<String> links)
+          List<String> links)
   {
     // /////////////////////////////////////////////////////////
     // If this is activated from the sequence panel, the user may want to
@@ -230,6 +244,24 @@ public class APopupMenu extends java.awt.PopupMenu implements
     SequenceGroup sg = ap.av.getSelectionGroup();
     if (sg != null && sg.getSize() > 0)
     {
+      if (sg.isNucleotide())
+      {
+        conservationColour.setEnabled(false);
+        clustalColour.setEnabled(false);
+        BLOSUM62Colour.setEnabled(false);
+        zappoColour.setEnabled(false);
+        taylorColour.setEnabled(false);
+        hydrophobicityColour.setEnabled(false);
+        helixColour.setEnabled(false);
+        strandColour.setEnabled(false);
+        turnColour.setEnabled(false);
+        buriedColour.setEnabled(false);
+      }
+      else
+      {
+        purinePyrimidineColour.setEnabled(false);
+        nucleotideColour.setEnabled(false);
+      }
       editGroupName.setLabel(MessageManager.formatMessage(
               "label.name_param", new Object[] { sg.getName() }));
       showText.setState(sg.getDisplayText());
@@ -248,9 +280,12 @@ public class APopupMenu extends java.awt.PopupMenu implements
         if (sg.cs != null)
         {
           abovePIDColour.setState(sg.cs.getThreshold() > 0);
-          conservationMenuItem.setState(sg.cs.conservationApplied());
+          conservationColour.setState(sg.cs.conservationApplied());
+          modifyPID.setEnabled(abovePIDColour.getState());
+          modifyConservation.setEnabled(conservationColour.getState());
         }
       }
+      setSelectedColour(sg.cs);
     }
     else
     {
@@ -308,6 +343,36 @@ public class APopupMenu extends java.awt.PopupMenu implements
   }
 
   /**
+   * Select the menu item (if any) matching the current colour scheme. This
+   * works by matching the menu item name (not display text) to the canonical
+   * name of the colour scheme.
+   * 
+   * @param cs
+   */
+  protected void setSelectedColour(ResidueShaderI cs)
+  {
+    if (cs == null || cs.getColourScheme() == null)
+    {
+      noColour.setState(true);
+    }
+    else
+    {
+      String name = cs.getColourScheme().getSchemeName();
+      for (int i = 0; i < colourMenu.getItemCount(); i++)
+      {
+        MenuItem item = colourMenu.getItem(i);
+        if (item instanceof CheckboxMenuItem)
+        {
+          if (name.equals(item.getName()))
+          {
+            ((CheckboxMenuItem) item).setState(true);
+          }
+        }
+      }
+    }
+  }
+
+  /**
    * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided.
    * 
    * @param seq
@@ -417,7 +482,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
      * Temporary store to hold distinct calcId / type pairs for the tooltip.
      * Using TreeMap means calcIds are shown in alphabetical order.
      */
-    Map<String, String> tipEntries = new TreeMap<String, String>();
+    SortedMap<String, String> tipEntries = new TreeMap<String, String>();
     final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<SequenceI, List<AlignmentAnnotation>>();
     AlignmentI al = this.ap.av.getAlignment();
     AlignmentUtils.findAddableReferenceAnnotations(forSequences,
@@ -502,43 +567,28 @@ public class APopupMenu extends java.awt.PopupMenu implements
     linkMenu.add(item);
   }
 
+  /**
+   * Actions on selecting / unselecting a checkbox menu item
+   */
   @Override
   public void itemStateChanged(ItemEvent evt)
   {
     Object source = evt.getSource();
-    if (source == abovePIDColour)
-    {
-      abovePIDColour_itemStateChanged();
-    }
-    else if (source == conservationMenuItem)
+    if (source == noColour)
     {
-      conservationMenuItem_itemStateChanged();
-    }
-    else if (source == showColourText)
-    {
-      showColourText_itemStateChanged();
-    }
-    else if (source == showText)
-    {
-      showText_itemStateChanged();
+      noColourmenuItem_actionPerformed();
     }
-    else if (source == showBoxes)
+    else if (source == clustalColour)
     {
-      showBoxes_itemStateChanged();
+      clustalColour_actionPerformed();
     }
-    else if (source == displayNonconserved)
+    else if (source == BLOSUM62Colour)
     {
-      this.showNonconserved_itemStateChanged();
+      BLOSUM62Colour_actionPerformed();
     }
-  }
-
-  @Override
-  public void actionPerformed(ActionEvent evt)
-  {
-    Object source = evt.getSource();
-    if (source == clustalColour)
+    else if (evt.getSource() == PIDColour)
     {
-      clustalColour_actionPerformed();
+      PIDColour_actionPerformed();
     }
     else if (source == zappoColour)
     {
@@ -568,26 +618,58 @@ public class APopupMenu extends java.awt.PopupMenu implements
     {
       buriedColour_actionPerformed();
     }
-    else if (source == nucleotideMenuItem)
+    else if (source == nucleotideColour)
     {
       nucleotideMenuItem_actionPerformed();
     }
-
-    else if (source == userDefinedColour)
+    else if (source == purinePyrimidineColour)
     {
-      userDefinedColour_actionPerformed();
+      purinePyrimidineColour_actionPerformed();
     }
-    else if (source == PIDColour)
+    else if (source == abovePIDColour)
     {
-      PIDColour_actionPerformed();
+      abovePIDColour_itemStateChanged();
     }
-    else if (source == BLOSUM62Colour)
+    else if (source == conservationColour)
     {
-      BLOSUM62Colour_actionPerformed();
+      conservationMenuItem_itemStateChanged();
     }
-    else if (source == noColourmenuItem)
+    else if (source == showColourText)
     {
-      noColourmenuItem_actionPerformed();
+      showColourText_itemStateChanged();
+    }
+    else if (source == showText)
+    {
+      showText_itemStateChanged();
+    }
+    else if (source == showBoxes)
+    {
+      showBoxes_itemStateChanged();
+    }
+    else if (source == displayNonconserved)
+    {
+      this.showNonconserved_itemStateChanged();
+    }
+  }
+
+  /**
+   * Actions on clicking a menu item
+   */
+  @Override
+  public void actionPerformed(ActionEvent evt)
+  {
+    Object source = evt.getSource();
+    if (source == userDefinedColour)
+    {
+      userDefinedColour_actionPerformed();
+    }
+    else if (source == modifyConservation)
+    {
+      conservationMenuItem_itemStateChanged();
+    }
+    else if (source == modifyPID)
+    {
+      abovePIDColour_itemStateChanged();
     }
     else if (source == unGroupMenuItem)
     {
@@ -738,9 +820,9 @@ public class APopupMenu extends java.awt.PopupMenu implements
         return;
       }
 
-      int rsize = 0, gSize = sg.getSize();
-      SequenceI[] rseqs, seqs = new SequenceI[gSize];
-      SequenceFeature[] tfeatures, features = new SequenceFeature[gSize];
+      int gSize = sg.getSize();
+      List<SequenceI> seqs = new ArrayList<SequenceI>();
+      List<SequenceFeature> features = new ArrayList<SequenceFeature>();
 
       for (int i = 0; i < gSize; i++)
       {
@@ -748,25 +830,17 @@ public class APopupMenu extends java.awt.PopupMenu implements
         int end = sg.findEndRes(sg.getSequenceAt(i));
         if (start <= end)
         {
-          seqs[rsize] = sg.getSequenceAt(i);
-          features[rsize] = new SequenceFeature(null, null, null, start,
-                  end, "Jalview");
-          rsize++;
+          seqs.add(sg.getSequenceAt(i));
+          features.add(new SequenceFeature(null, null, start, end,
+                  "Jalview"));
         }
       }
-      rseqs = new SequenceI[rsize];
-      tfeatures = new SequenceFeature[rsize];
-      System.arraycopy(seqs, 0, rseqs, 0, rsize);
-      System.arraycopy(features, 0, tfeatures, 0, rsize);
-      features = tfeatures;
-      seqs = rseqs;
 
       if (ap.seqPanel.seqCanvas.getFeatureRenderer().amendFeatures(seqs,
               features, true, ap))
       {
         ap.alignFrame.sequenceFeatures.setState(true);
         ap.av.setShowSequenceFeatures(true);
-        ;
         ap.highlightSearchResults(null);
       }
     }
@@ -854,14 +928,14 @@ public class APopupMenu extends java.awt.PopupMenu implements
   void addPDB()
   {
     Vector<PDBEntry> pdbs = seq.getAllPDBEntries();
-    if (pdbs != null&& !pdbs.isEmpty())
+    if (pdbs != null && !pdbs.isEmpty())
     {
       PDBEntry entry = pdbs.firstElement();
 
       if (ap.av.applet.jmolAvailable)
       {
-        new jalview.appletgui.AppletJmol(entry, new SequenceI[] { seq },
-                null, ap, DataSourceType.URL);
+        new AppletJmol(entry, new SequenceI[] { seq }, null, ap,
+                DataSourceType.URL);
       }
       else
       {
@@ -877,7 +951,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
       cap.setPDBImport(seq);
       Frame frame = new Frame();
       frame.add(cap);
-      jalview.bin.JalviewLite.addFrame(frame, MessageManager.formatMessage(
+      JalviewLite.addFrame(frame, MessageManager.formatMessage(
               "label.paste_pdb_file_for_sequence",
               new Object[] { seq.getName() }), 400, 300);
     }
@@ -897,11 +971,8 @@ public class APopupMenu extends java.awt.PopupMenu implements
             .getString("action.create_group"));
     createGroupMenuItem.addActionListener(this);
 
-    nucleotideMenuItem.setLabel(MessageManager
-            .getString("label.nucleotide"));
-    nucleotideMenuItem.addActionListener(this);
-    conservationMenuItem.addItemListener(this);
-    abovePIDColour.addItemListener(this);
+    modifyPID.setEnabled(abovePIDColour.getState());
+    modifyConservation.setEnabled(conservationColour.getState());
     colourMenu.setLabel(MessageManager.getString("label.group_colour"));
     showBoxes.setLabel(MessageManager.getString("action.boxes"));
     showBoxes.setState(true);
@@ -910,7 +981,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
     sequenceDetails.addActionListener(this);
     selSeqDetails.addActionListener(this);
     displayNonconserved.setLabel(MessageManager
-            .getString("label.show_non_conversed"));
+            .getString("label.show_non_conserved"));
     displayNonconserved.setState(false);
     displayNonconserved.addItemListener(this);
     showText.setLabel(MessageManager.getString("action.text"));
@@ -942,7 +1013,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
     groupMenu.add(unGroupMenuItem);
     groupMenu.add(menu1);
 
-    colourMenu.add(noColourmenuItem);
+    colourMenu.add(noColour);
     colourMenu.add(clustalColour);
     colourMenu.add(BLOSUM62Colour);
     colourMenu.add(PIDColour);
@@ -953,48 +1024,91 @@ public class APopupMenu extends java.awt.PopupMenu implements
     colourMenu.add(strandColour);
     colourMenu.add(turnColour);
     colourMenu.add(buriedColour);
-    colourMenu.add(nucleotideMenuItem);
+    colourMenu.add(nucleotideColour);
+    colourMenu.add(purinePyrimidineColour);
     colourMenu.add(userDefinedColour);
     colourMenu.addSeparator();
+    colourMenu.add(conservationColour);
+    colourMenu.add(modifyConservation);
     colourMenu.add(abovePIDColour);
-    colourMenu.add(conservationMenuItem);
+    colourMenu.add(modifyPID);
 
-    noColourmenuItem.setLabel(MessageManager.getString("label.none"));
-    noColourmenuItem.addActionListener(this);
+    noColour.setLabel(MessageManager.getString("label.none"));
+    noColour.addItemListener(this);
 
+    /*
+     * setName allows setSelectedColour to do its thing
+     */
     clustalColour.setLabel(MessageManager
-            .getString("label.clustalx_colours"));
-    clustalColour.addActionListener(this);
-    zappoColour.setLabel(MessageManager.getString("label.zappo"));
-    zappoColour.addActionListener(this);
-    taylorColour.setLabel(MessageManager.getString("label.taylor"));
-    taylorColour.addActionListener(this);
+            .getString("label.colourScheme_clustal"));
+    clustalColour.setName(JalviewColourScheme.Clustal.toString());
+    clustalColour.addItemListener(this);
+    BLOSUM62Colour.setLabel(MessageManager
+            .getString("label.colourScheme_blosum62"));
+    BLOSUM62Colour.setName(JalviewColourScheme.Blosum62.toString());
+    BLOSUM62Colour.addItemListener(this);
+    PIDColour.setLabel(MessageManager
+            .getString("label.colourScheme_%_identity"));
+    PIDColour.setName(JalviewColourScheme.PID.toString());
+    PIDColour.addItemListener(this);
+    zappoColour.setLabel(MessageManager
+            .getString("label.colourScheme_zappo"));
+    zappoColour.setName(JalviewColourScheme.Zappo.toString());
+    zappoColour.addItemListener(this);
+    taylorColour.setLabel(MessageManager
+            .getString("label.colourScheme_taylor"));
+    taylorColour.setName(JalviewColourScheme.Taylor.toString());
+    taylorColour.addItemListener(this);
     hydrophobicityColour.setLabel(MessageManager
-            .getString("label.hydrophobicity"));
-    hydrophobicityColour.addActionListener(this);
-    helixColour
-            .setLabel(MessageManager.getString("label.helix_propensity"));
-    helixColour.addActionListener(this);
+            .getString("label.colourScheme_hydrophobic"));
+    hydrophobicityColour
+            .setName(JalviewColourScheme.Hydrophobic.toString());
+    hydrophobicityColour.addItemListener(this);
+    helixColour.setLabel(MessageManager
+            .getString("label.colourScheme_helix_propensity"));
+    helixColour.setName(JalviewColourScheme.Helix.toString());
+    helixColour.addItemListener(this);
     strandColour.setLabel(MessageManager
-            .getString("label.strand_propensity"));
-    strandColour.addActionListener(this);
-    turnColour.setLabel(MessageManager.getString("label.turn_propensity"));
-    turnColour.addActionListener(this);
-    buriedColour.setLabel(MessageManager.getString("label.buried_index"));
-    buriedColour.addActionListener(this);
-    abovePIDColour.setLabel(MessageManager
-            .getString("label.above_identity_percentage"));
+            .getString("label.colourScheme_strand_propensity"));
+    strandColour.setName(JalviewColourScheme.Strand.toString());
+    strandColour.addItemListener(this);
+    turnColour.setLabel(MessageManager
+            .getString("label.colourScheme_turn_propensity"));
+    turnColour.setName(JalviewColourScheme.Turn.toString());
+    turnColour.addItemListener(this);
+    buriedColour.setLabel(MessageManager
+            .getString("label.colourScheme_buried_index"));
+    buriedColour.setName(JalviewColourScheme.Buried.toString());
+    buriedColour.addItemListener(this);
+    nucleotideColour.setLabel(MessageManager
+            .getString("label.colourScheme_nucleotide"));
+    nucleotideColour.setName(JalviewColourScheme.Nucleotide.toString());
+    nucleotideColour.addItemListener(this);
+    purinePyrimidineColour.setLabel(MessageManager
+            .getString("label.colourScheme_purine/pyrimidine"));
+    purinePyrimidineColour.setName(JalviewColourScheme.PurinePyrimidine
+            .toString());
+    purinePyrimidineColour.addItemListener(this);
 
     userDefinedColour.setLabel(MessageManager
             .getString("action.user_defined"));
     userDefinedColour.addActionListener(this);
-    PIDColour.setLabel(MessageManager
-            .getString("label.percentage_identity"));
+
+    abovePIDColour.setLabel(MessageManager
+            .getString("label.above_identity_threshold"));
+    abovePIDColour.addItemListener(this);
+    modifyPID.setLabel(MessageManager
+            .getString("label.modify_identity_threshold"));
+    modifyPID.addActionListener(this);
+    conservationColour.setLabel(MessageManager
+            .getString("action.by_conservation"));
+    conservationColour.addItemListener(this);
+    modifyConservation.setLabel(MessageManager
+            .getString("label.modify_conservation_threshold"));
+    modifyConservation.addActionListener(this);
+
     PIDColour.addActionListener(this);
-    BLOSUM62Colour.setLabel("BLOSUM62");
     BLOSUM62Colour.addActionListener(this);
-    conservationMenuItem.setLabel(MessageManager
-            .getString("label.conservation"));
 
     editMenu.add(copy);
     copy.addActionListener(this);
@@ -1044,55 +1158,63 @@ public class APopupMenu extends java.awt.PopupMenu implements
   protected void clustalColour_actionPerformed()
   {
     SequenceGroup sg = getGroup();
-    sg.cs = new ClustalxColourScheme(sg, ap.av.getHiddenRepSequences());
+    sg.cs = new ResidueShader(new ClustalxColourScheme(sg,
+            ap.av.getHiddenRepSequences()));
     refresh();
   }
 
   protected void zappoColour_actionPerformed()
   {
-    getGroup().cs = new ZappoColourScheme();
+    getGroup().cs = new ResidueShader(new ZappoColourScheme());
     refresh();
   }
 
   protected void taylorColour_actionPerformed()
   {
-    getGroup().cs = new TaylorColourScheme();
+    getGroup().cs = new ResidueShader(new TaylorColourScheme());
     refresh();
   }
 
   protected void hydrophobicityColour_actionPerformed()
   {
-    getGroup().cs = new HydrophobicColourScheme();
+    getGroup().cs = new ResidueShader(new HydrophobicColourScheme());
     refresh();
   }
 
   protected void helixColour_actionPerformed()
   {
-    getGroup().cs = new HelixColourScheme();
+    getGroup().cs = new ResidueShader(new HelixColourScheme());
     refresh();
   }
 
   protected void strandColour_actionPerformed()
   {
-    getGroup().cs = new StrandColourScheme();
+    getGroup().cs = new ResidueShader(new StrandColourScheme());
     refresh();
   }
 
   protected void turnColour_actionPerformed()
   {
-    getGroup().cs = new TurnColourScheme();
+    getGroup().cs = new ResidueShader(new TurnColourScheme());
     refresh();
   }
 
   protected void buriedColour_actionPerformed()
   {
-    getGroup().cs = new BuriedColourScheme();
+    getGroup().cs = new ResidueShader(new BuriedColourScheme());
     refresh();
   }
 
   public void nucleotideMenuItem_actionPerformed()
   {
-    getGroup().cs = new NucleotideColourScheme();
+    getGroup().cs = new ResidueShader(new NucleotideColourScheme());
+    refresh();
+  }
+
+  public void purinePyrimidineColour_actionPerformed()
+  {
+    getGroup().cs = new ResidueShader(
+            new PurinePyrimidineColourScheme());
     refresh();
   }
 
@@ -1119,11 +1241,11 @@ public class APopupMenu extends java.awt.PopupMenu implements
     else
     // remove PIDColouring
     {
+      SliderPanel.hidePIDSlider();
       sg.cs.setThreshold(0, ap.av.isIgnoreGapsConsensus());
     }
-
+    modifyPID.setEnabled(abovePIDColour.getState());
     refresh();
-
   }
 
   protected void userDefinedColour_actionPerformed()
@@ -1134,7 +1256,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
   protected void PIDColour_actionPerformed()
   {
     SequenceGroup sg = getGroup();
-    sg.cs = new PIDColourScheme();
+    sg.cs = new ResidueShader(new PIDColourScheme());
     sg.cs.setConsensus(AAFrequency.calculate(sg.getSequences(ap.av
             .getHiddenRepSequences()), 0, ap.av.getAlignment().getWidth()));
     refresh();
@@ -1144,7 +1266,7 @@ public class APopupMenu extends java.awt.PopupMenu implements
   {
     SequenceGroup sg = getGroup();
 
-    sg.cs = new Blosum62ColourScheme();
+    sg.cs = new ResidueShader(new Blosum62ColourScheme());
 
     sg.cs.setConsensus(AAFrequency.calculate(sg.getSequences(ap.av
             .getHiddenRepSequences()), 0, ap.av.getAlignment().getWidth()));
@@ -1166,21 +1288,24 @@ public class APopupMenu extends java.awt.PopupMenu implements
       return;
     }
 
-    if (conservationMenuItem.getState())
+    if (conservationColour.getState())
     {
-      sg.cs.setConservation(Conservation.calculateConservation("Group", sg
+      Conservation conservation = Conservation.calculateConservation(
+              "Group", sg
               .getSequences(ap.av.getHiddenRepSequences()), 0, ap.av
               .getAlignment().getWidth(), false, ap.av.getConsPercGaps(),
-              false));
+              false);
+      sg.getGroupColourScheme().setConservation(conservation);
       SliderPanel.setConservationSlider(ap, sg.cs, sg.getName());
       SliderPanel.showConservationSlider();
     }
     else
     // remove ConservationColouring
     {
+      SliderPanel.hideConservationSlider();
       sg.cs.setConservation(null);
     }
-
+    modifyConservation.setEnabled(conservationColour.getState());
     refresh();
   }
 
index 86dc19b..24f882e 100644 (file)
@@ -22,6 +22,9 @@ package jalview.appletgui;
 
 import jalview.analysis.AlignmentSorter;
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+import jalview.analysis.TreeBuilder;
+import jalview.analysis.scoremodels.PIDModel;
+import jalview.analysis.scoremodels.ScoreModels;
 import jalview.api.AlignViewControllerGuiI;
 import jalview.api.AlignViewControllerI;
 import jalview.api.AlignViewportI;
@@ -43,6 +46,7 @@ import jalview.datamodel.AlignmentAnnotation;
 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;
@@ -64,7 +68,7 @@ import jalview.schemes.HydrophobicColourScheme;
 import jalview.schemes.NucleotideColourScheme;
 import jalview.schemes.PIDColourScheme;
 import jalview.schemes.PurinePyrimidineColourScheme;
-import jalview.schemes.RNAHelicesColourChooser;
+import jalview.schemes.RNAHelicesColour;
 import jalview.schemes.StrandColourScheme;
 import jalview.schemes.TCoffeeColourScheme;
 import jalview.schemes.TaylorColourScheme;
@@ -75,6 +79,7 @@ import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.BorderLayout;
 import java.awt.Canvas;
@@ -168,14 +173,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   }
 
   public AlignFrame(AlignmentI al, SequenceI[] hiddenSeqs,
-          ColumnSelection columnSelection, JalviewLite applet,
+          HiddenColumns hidden, JalviewLite applet,
           String title, boolean embedded)
   {
-    this(al, hiddenSeqs, columnSelection, applet, title, embedded, true);
+    this(al, hiddenSeqs, hidden, applet, title, embedded, true);
   }
 
   public AlignFrame(AlignmentI al, SequenceI[] hiddenSeqs,
-          ColumnSelection columnSelection, JalviewLite applet,
+          HiddenColumns hidden, JalviewLite applet,
           String title, boolean embedded, boolean addToDisplay)
   {
     if (applet != null)
@@ -218,9 +223,9 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       viewport.hideSequence(hiddenSeqs);
     }
-    if (columnSelection != null)
+    if (hidden != null)
     {
-      viewport.setColumnSelection(columnSelection);
+      viewport.getAlignment().setHiddenColumns(hidden);
     }
     viewport.setScaleAboveWrapped(scaleAbove.getState());
 
@@ -284,6 +289,16 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     }
     if (viewport.getAlignment().isNucleotide())
     {
+      conservationMenuItem.setEnabled(false);
+      clustalColour.setEnabled(false);
+      BLOSUM62Colour.setEnabled(false);
+      zappoColour.setEnabled(false);
+      taylorColour.setEnabled(false);
+      hydrophobicityColour.setEnabled(false);
+      helixColour.setEnabled(false);
+      strandColour.setEnabled(false);
+      turnColour.setEnabled(false);
+      buriedColour.setEnabled(false);
       viewport.updateStrucConsensus(alignPanel);
       if (viewport.getAlignment().hasRNAStructure())
       {
@@ -298,6 +313,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       RNAHelixColour.setEnabled(false);
       purinePyrimidineColour.setEnabled(false);
+      nucleotideColour.setEnabled(false);
     }
     // Some JVMS send keyevents to Top frame or lowest panel,
     // Havent worked out why yet. So add to both this frame and seqCanvas for
@@ -409,6 +425,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   @Override
   public void keyPressed(KeyEvent evt)
   {
+    ViewportRanges ranges = viewport.getRanges();
+
     if (viewport.cursorMode
             && ((evt.getKeyCode() >= KeyEvent.VK_0 && evt.getKeyCode() <= KeyEvent.VK_9) || (evt
                     .getKeyCode() >= KeyEvent.VK_NUMPAD0 && evt
@@ -560,8 +578,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
               new String[] { (viewport.cursorMode ? "on" : "off") }));
       if (viewport.cursorMode)
       {
-        alignPanel.seqPanel.seqCanvas.cursorX = viewport.startRes;
-        alignPanel.seqPanel.seqCanvas.cursorY = viewport.startSeq;
+        alignPanel.seqPanel.seqCanvas.cursorX = ranges.getStartRes();
+        alignPanel.seqPanel.seqCanvas.cursorY = ranges.getStartSeq();
       }
       break;
 
@@ -583,24 +601,22 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     case KeyEvent.VK_PAGE_UP:
       if (viewport.getWrapAlignment())
       {
-        alignPanel.scrollUp(true);
+        ranges.scrollUp(true);
       }
       else
       {
-        alignPanel.setScrollValues(viewport.startRes, viewport.startSeq
-                - viewport.endSeq + viewport.startSeq);
+        ranges.pageUp();
       }
       break;
 
     case KeyEvent.VK_PAGE_DOWN:
       if (viewport.getWrapAlignment())
       {
-        alignPanel.scrollUp(false);
+        ranges.scrollUp(false);
       }
       else
       {
-        alignPanel.setScrollValues(viewport.startRes, viewport.startSeq
-                + viewport.endSeq - viewport.startSeq);
+        ranges.pageDown();
       }
       break;
 
@@ -1057,6 +1073,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       delete_actionPerformed();
     }
+    else if (source == createGroup)
+    {
+      createGroup_actionPerformed();
+    }
+    else if (source == unGroup)
+    {
+      unGroup_actionPerformed();
+    }
     else if (source == grpsFromSelection)
     {
       makeGrpsFromSelection_actionPerformed();
@@ -1257,7 +1281,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     // }
     else if (source == RNAHelixColour)
     {
-      new RNAHelicesColourChooser(viewport, alignPanel);
+      changeColour(new RNAHelicesColour(viewport.getAlignment()));
+      // new RNAHelicesColourChooser(viewport, alignPanel);
     }
     else if (source == modifyPID)
     {
@@ -1413,6 +1438,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;
@@ -1420,12 +1456,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     if (format.equalsIgnoreCase("Jalview"))
     {
       features = formatter.printJalviewFormat(viewport.getAlignment()
-              .getSequencesArray(), getDisplayedFeatureCols());
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
     else
     {
       features = formatter.printGffFormat(viewport.getAlignment()
-              .getSequencesArray(), getDisplayedFeatureCols());
+              .getSequencesArray(), getDisplayedFeatureCols(),
+              getDisplayedFeatureGroups(), true);
     }
 
     if (displayTextbox)
@@ -1901,7 +1939,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       copiedHiddenColumns = new Vector();
       int hiddenOffset = viewport.getSelectionGroup().getStartRes();
-      for (int[] region : viewport.getColumnSelection().getHiddenColumns())
+      for (int[] region : viewport.getAlignment().getHiddenColumns()
+              .getHiddenRegions())
       {
         copiedHiddenColumns.addElement(new int[] {
             region[0] - hiddenOffset, region[1] - hiddenOffset });
@@ -2051,7 +2090,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             seqs, 0, viewport.getAlignment().getWidth(),
             viewport.getAlignment()));
 
-    viewport.setEndSeq(viewport.getAlignment().getHeight());
+    viewport.getRanges().setEndSeq(viewport.getAlignment().getHeight());
     viewport.getAlignment().getWidth();
     viewport.firePropertyChange("alignment", null, viewport.getAlignment()
             .getSequences());
@@ -2287,6 +2326,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
   void trimAlignment(boolean trimLeft)
   {
+    AlignmentI al = viewport.getAlignment();
+    ViewportRanges ranges = viewport.getRanges();
     ColumnSelection colSel = viewport.getColumnSelection();
     int column;
 
@@ -2309,20 +2350,20 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
       }
       else
       {
-        seqs = viewport.getAlignment().getSequencesArray();
+        seqs = al.getSequencesArray();
       }
 
       TrimRegionCommand trimRegion;
       if (trimLeft)
       {
         trimRegion = new TrimRegionCommand("Remove Left", true, seqs,
-                column, viewport.getAlignment());
-        viewport.setStartRes(0);
+                column, al);
+        ranges.setStartRes(0);
       }
       else
       {
         trimRegion = new TrimRegionCommand("Remove Right", false, seqs,
-                column, viewport.getAlignment());
+                column, al);
       }
 
       statusBar.setText(MessageManager.formatMessage(
@@ -2331,23 +2372,25 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
                       .toString() }));
       addHistoryItem(trimRegion);
 
-      for (SequenceGroup sg : viewport.getAlignment().getGroups())
+      for (SequenceGroup sg : al.getGroups())
       {
         if ((trimLeft && !sg.adjustForRemoveLeft(column))
                 || (!trimLeft && !sg.adjustForRemoveRight(column)))
         {
-          viewport.getAlignment().deleteGroup(sg);
+          al.deleteGroup(sg);
         }
       }
 
-      viewport.firePropertyChange("alignment", null, viewport
-              .getAlignment().getSequences());
+      viewport.firePropertyChange("alignment", null, al.getSequences());
     }
   }
 
   public void removeGappedColumnMenuItem_actionPerformed()
   {
-    int start = 0, end = viewport.getAlignment().getWidth() - 1;
+    AlignmentI al = viewport.getAlignment();
+    ViewportRanges ranges = viewport.getRanges();
+    int start = 0;
+    int end = ranges.getAbsoluteAlignmentWidth() - 1;
 
     SequenceI[] seqs;
     if (viewport.getSelectionGroup() != null)
@@ -2375,22 +2418,24 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
     // This is to maintain viewport position on first residue
     // of first sequence
-    SequenceI seq = viewport.getAlignment().getSequenceAt(0);
-    int startRes = seq.findPosition(viewport.startRes);
+    SequenceI seq = al.getSequenceAt(0);
+    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);
-    viewport.setStartRes(seq.findIndex(startRes) - 1);
-    viewport.firePropertyChange("alignment", null, viewport.getAlignment()
-            .getSequences());
+    ranges.setStartRes(seq.findIndex(startRes) - 1);
+    viewport.firePropertyChange("alignment", null, al.getSequences());
 
   }
 
   public void removeAllGapsMenuItem_actionPerformed()
   {
-    int start = 0, end = viewport.getAlignment().getWidth() - 1;
+    AlignmentI al = viewport.getAlignment();
+    ViewportRanges ranges = viewport.getRanges();
+    int start = 0;
+    int end = ranges.getAbsoluteAlignmentWidth() - 1;
 
     SequenceI[] seqs;
     if (viewport.getSelectionGroup() != null)
@@ -2407,16 +2452,15 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
     // This is to maintain viewport position on first residue
     // of first sequence
-    SequenceI seq = viewport.getAlignment().getSequenceAt(0);
-    int startRes = seq.findPosition(viewport.startRes);
+    SequenceI seq = al.getSequenceAt(0);
+    int startRes = seq.findPosition(ranges.getStartRes());
 
     addHistoryItem(new RemoveGapsCommand("Remove Gaps", seqs, start, end,
-            viewport.getAlignment()));
+            al));
 
-    viewport.setStartRes(seq.findIndex(startRes) - 1);
+    ranges.setStartRes(seq.findIndex(startRes) - 1);
 
-    viewport.firePropertyChange("alignment", null, viewport.getAlignment()
-            .getSequences());
+    viewport.firePropertyChange("alignment", null, al.getSequences());
 
   }
 
@@ -2629,26 +2673,6 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   @Override
   public void changeColour(ColourSchemeI cs)
   {
-
-    if (cs != null)
-    {
-      if (viewport.getAbovePIDThreshold())
-      {
-        viewport.setThreshold(SliderPanel.setPIDSliderSource(alignPanel,
-                cs, "Background"));
-      }
-
-      if (viewport.getConservationSelected())
-      {
-        cs.setConservationApplied(true);
-        viewport.setIncrement(SliderPanel.setConservationSlider(alignPanel,
-                cs, "Background"));
-      }
-      else
-      {
-        cs.setConservationApplied(false);
-      }
-    }
     viewport.setGlobalColourScheme(cs);
 
     alignPanel.paintAlignment(true);
@@ -2660,7 +2684,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             && viewport.getGlobalColourScheme() != null)
     {
       SliderPanel.setPIDSliderSource(alignPanel,
-              viewport.getGlobalColourScheme(), "Background");
+              viewport.getResidueShading(), alignPanel.getViewName());
       SliderPanel.showPIDSlider();
     }
   }
@@ -2671,40 +2695,58 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             && viewport.getGlobalColourScheme() != null)
     {
       SliderPanel.setConservationSlider(alignPanel,
-              viewport.getGlobalColourScheme(), "Background");
+              viewport.getResidueShading(), alignPanel.getViewName());
       SliderPanel.showConservationSlider();
     }
   }
 
   protected void conservationMenuItem_actionPerformed()
   {
-    viewport.setConservationSelected(conservationMenuItem.getState());
-
-    viewport.setAbovePIDThreshold(false);
-    abovePIDThreshold.setState(false);
+    boolean selected = conservationMenuItem.getState();
+    modifyConservation.setEnabled(selected);
+    viewport.setConservationSelected(selected);
+    viewport.getResidueShading().setConservationApplied(selected);
 
     changeColour(viewport.getGlobalColourScheme());
 
-    modifyConservation_actionPerformed();
+    if (selected)
+    {
+      modifyConservation_actionPerformed();
+    }
+    else
+    {
+      SliderPanel.hideConservationSlider();
+    }
   }
 
   public void abovePIDThreshold_actionPerformed()
   {
-    viewport.setAbovePIDThreshold(abovePIDThreshold.getState());
-
-    conservationMenuItem.setState(false);
-    viewport.setConservationSelected(false);
+    boolean selected = abovePIDThreshold.getState();
+    modifyPID.setEnabled(selected);
+    viewport.setAbovePIDThreshold(selected);
+    if (!selected)
+    {
+      viewport.getResidueShading().setThreshold(0,
+              viewport.isIgnoreGapsConsensus());
+    }
 
     changeColour(viewport.getGlobalColourScheme());
 
-    modifyPID_actionPerformed();
+    if (selected)
+    {
+      modifyPID_actionPerformed();
+    }
+    else
+    {
+      SliderPanel.hidePIDSlider();
+    }
   }
 
   public void sortPairwiseMenuItem_actionPerformed()
   {
     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
     AlignmentSorter.sortByPID(viewport.getAlignment(), viewport
-            .getAlignment().getSequenceAt(0), null);
+            .getAlignment().getSequenceAt(0));
 
     addHistoryItem(new OrderCommand("Pairwise Sort", oldOrder,
             viewport.getAlignment()));
@@ -2796,25 +2838,31 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
   public void averageDistanceTreeMenuItem_actionPerformed()
   {
-    NewTreePanel("AV", "PID", "Average distance tree using PID");
+    newTreePanel(TreeBuilder.AVERAGE_DISTANCE, new PIDModel().getName(),
+            "Average distance tree using PID");
   }
 
   public void neighbourTreeMenuItem_actionPerformed()
   {
-    NewTreePanel("NJ", "PID", "Neighbour joining tree using PID");
+    newTreePanel(TreeBuilder.NEIGHBOUR_JOINING, new PIDModel().getName(),
+            "Neighbour joining tree using PID");
   }
 
   protected void njTreeBlosumMenuItem_actionPerformed()
   {
-    NewTreePanel("NJ", "BL", "Neighbour joining tree using BLOSUM62");
+    newTreePanel(TreeBuilder.NEIGHBOUR_JOINING, ScoreModels.getInstance()
+            .getBlosum62().getName(),
+            "Neighbour joining tree using BLOSUM62");
   }
 
   protected void avTreeBlosumMenuItem_actionPerformed()
   {
-    NewTreePanel("AV", "BL", "Average distance tree using BLOSUM62");
+    newTreePanel(TreeBuilder.AVERAGE_DISTANCE, ScoreModels.getInstance()
+            .getBlosum62().getName(),
+            "Average distance tree using BLOSUM62");
   }
 
-  void NewTreePanel(String type, String pwType, String title)
+  void newTreePanel(String type, String pwType, String title)
   {
     // are the sequences aligned?
     if (!viewport.getAlignment().isAligned(false))
@@ -3320,7 +3368,10 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             .getString("action.make_groups_selection"));
     grpsFromSelection.addActionListener(this);
     createGroup.setLabel(MessageManager.getString("action.create_group"));
+    createGroup.addActionListener(this);
     unGroup.setLabel(MessageManager.getString("action.remove_group"));
+    unGroup.addActionListener(this);
+
     annotationColumnSelection.setLabel(MessageManager
             .getString("action.select_by_annotation"));
     annotationColumnSelection.addActionListener(this);
@@ -3442,7 +3493,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             .getString("label.colour_text"));
     colourTextMenuItem.addItemListener(this);
     displayNonconservedMenuItem.setLabel(MessageManager
-            .getString("label.show_non_conversed"));
+            .getString("label.show_non_conserved"));
     displayNonconservedMenuItem.addItemListener(this);
     wrapMenuItem.setLabel(MessageManager.getString("action.wrap"));
     wrapMenuItem.addItemListener(this);
@@ -3465,45 +3516,50 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             .getString("label.apply_colour_to_all_groups"));
     applyToAllGroups.setState(true);
     applyToAllGroups.addItemListener(this);
-    clustalColour.setLabel(MessageManager.getString("label.clustalx"));
+    clustalColour.setLabel(MessageManager
+            .getString("label.colourScheme_clustal"));
     clustalColour.addActionListener(this);
-    zappoColour.setLabel(MessageManager.getString("label.zappo"));
+    zappoColour.setLabel(MessageManager
+            .getString("label.colourScheme_zappo"));
     zappoColour.addActionListener(this);
-    taylorColour.setLabel(MessageManager.getString("label.taylor"));
+    taylorColour.setLabel(MessageManager
+            .getString("label.colourScheme_taylor"));
     taylorColour.addActionListener(this);
     hydrophobicityColour.setLabel(MessageManager
-            .getString("label.hydrophobicity"));
+            .getString("label.colourScheme_hydrophobic"));
     hydrophobicityColour.addActionListener(this);
-    helixColour
-            .setLabel(MessageManager.getString("label.helix_propensity"));
+    helixColour.setLabel(MessageManager
+            .getString("label.colourScheme_helix_propensity"));
     helixColour.addActionListener(this);
     strandColour.setLabel(MessageManager
-            .getString("label.strand_propensity"));
+            .getString("label.colourScheme_strand_propensity"));
     strandColour.addActionListener(this);
-    turnColour.setLabel(MessageManager.getString("label.turn_propensity"));
+    turnColour.setLabel(MessageManager
+            .getString("label.colourScheme_turn_propensity"));
     turnColour.addActionListener(this);
-    buriedColour.setLabel(MessageManager.getString("label.buried_index"));
+    buriedColour.setLabel(MessageManager
+            .getString("label.colourScheme_buried_index"));
     buriedColour.addActionListener(this);
     purinePyrimidineColour.setLabel(MessageManager
-            .getString("label.purine_pyrimidine"));
+            .getString("label.colourScheme_purine/pyrimidine"));
     purinePyrimidineColour.addActionListener(this);
     // RNAInteractionColour.setLabel(MessageManager
     // .getString("label.rna_interaction"));
     // RNAInteractionColour.addActionListener(this);
     RNAHelixColour.setLabel(MessageManager
-            .getString("action.by_rna_helixes"));
+            .getString("label.colourScheme_rna_helices"));
     RNAHelixColour.addActionListener(this);
     userDefinedColour.setLabel(MessageManager
             .getString("action.user_defined"));
     userDefinedColour.addActionListener(this);
     PIDColour.setLabel(MessageManager
-            .getString("label.percentage_identity"));
+            .getString("label.colourScheme_%_identity"));
     PIDColour.addActionListener(this);
     BLOSUM62Colour.setLabel(MessageManager
-            .getString("label.blosum62_score"));
+            .getString("label.colourScheme_blosum62"));
     BLOSUM62Colour.addActionListener(this);
-    tcoffeeColour
-            .setLabel(MessageManager.getString("label.tcoffee_scores"));
+    tcoffeeColour.setLabel(MessageManager
+            .getString("label.colourScheme_t-coffee_scores"));
     // it will be enabled only if a score file is provided
     tcoffeeColour.setEnabled(false);
     tcoffeeColour.addActionListener(this);
@@ -3515,13 +3571,16 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     abovePIDThreshold.setLabel(MessageManager
             .getString("label.above_identity_threshold"));
     abovePIDThreshold.addItemListener(this);
-    nucleotideColour.setLabel(MessageManager.getString("label.nucleotide"));
+    nucleotideColour.setLabel(MessageManager
+            .getString("label.colourScheme_nucleotide"));
     nucleotideColour.addActionListener(this);
     modifyPID.setLabel(MessageManager
             .getString("label.modify_identity_threshold"));
+    modifyPID.setEnabled(abovePIDThreshold.getState());
     modifyPID.addActionListener(this);
     modifyConservation.setLabel(MessageManager
             .getString("label.modify_conservation_threshold"));
+    modifyConservation.setEnabled(conservationMenuItem.getState());
     modifyConservation.addActionListener(this);
     annotationColour.setLabel(MessageManager
             .getString("action.by_annotation"));
@@ -3555,7 +3614,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             .getString("label.neighbour_joining_identity"));
     neighbourTreeMenuItem.addActionListener(this);
     avDistanceTreeBlosumMenuItem.setLabel(MessageManager
-            .getString("label.average_distance_bloslum62"));
+            .getString("label.average_distance_blosum62"));
     avDistanceTreeBlosumMenuItem.addActionListener(this);
     njTreeBlosumMenuItem.setLabel(MessageManager
             .getString("label.neighbour_blosum62"));
@@ -4161,9 +4220,10 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
    * @param csel
    *          - columns to be selected on the alignment
    */
-  public void select(SequenceGroup sel, ColumnSelection csel)
+  public void select(SequenceGroup sel, ColumnSelection csel,
+          HiddenColumns hidden)
   {
-    alignPanel.seqPanel.selection(sel, csel, null);
+    alignPanel.seqPanel.selection(sel, csel, hidden, null);
   }
 
   public void scrollTo(int row, int column)
index 4bd77b6..73cd9e9 100644 (file)
  */
 package jalview.appletgui;
 
-import jalview.analysis.NJTree;
+import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureSettingsModelI;
 import jalview.bin.JalviewLite;
 import jalview.commands.CommandI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.ResidueShader;
 import jalview.schemes.ColourSchemeProperty;
 import jalview.schemes.UserColourScheme;
-import jalview.structure.CommandListener;
 import jalview.structure.SelectionSource;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
 import jalview.viewmodel.AlignmentViewport;
 
 import java.awt.Font;
+import java.awt.FontMetrics;
 
 public class AlignViewport extends AlignmentViewport implements
-        SelectionSource, VamsasSource, CommandListener
+        SelectionSource
 {
   boolean cursorMode = false;
 
@@ -51,7 +53,7 @@ public class AlignViewport extends AlignmentViewport implements
 
   boolean validCharWidth = true;
 
-  NJTree currentTree = null;
+  TreeModel currentTree = null;
 
   public jalview.bin.JalviewLite applet;
 
@@ -70,16 +72,13 @@ public class AlignViewport extends AlignmentViewport implements
 
   public AlignViewport(AlignmentI al, JalviewLite applet)
   {
-    super();
+    super(al);
     calculator = new jalview.workers.AlignCalcManager();
     this.applet = applet;
-    alignment = al;
+
     // we always pad gaps
     this.setPadGaps(true);
-    this.startRes = 0;
-    this.endRes = al.getWidth() - 1;
-    this.startSeq = 0;
-    this.endSeq = al.getHeight() - 1;
+
     if (applet != null)
     {
       // get the width and height scaling factors if they were specified
@@ -130,7 +129,7 @@ public class AlignViewport extends AlignmentViewport implements
         }
       }
     }
-    setFont(font);
+    setFont(font, true);
 
     MAC = new jalview.util.Platform().isAMac();
 
@@ -150,6 +149,9 @@ public class AlignViewport extends AlignmentViewport implements
       showConsensus = applet.getDefaultParameter("showConsensus",
               showConsensus);
 
+      showOccupancy = applet.getDefaultParameter("showOccupancy",
+              showOccupancy);
+
       setShowUnconserved(applet.getDefaultParameter("showUnconserved",
               getShowUnconserved()));
 
@@ -208,18 +210,19 @@ public class AlignViewport extends AlignmentViewport implements
 
       if (colour != null)
       {
-        globalColourScheme = ColourSchemeProperty.getColour(alignment,
-                colour);
-        if (globalColourScheme != null)
+        residueShading = new ResidueShader(
+                ColourSchemeProperty.getColourScheme(alignment, colour));
+        if (residueShading != null)
         {
-          globalColourScheme.setConsensus(hconsensus);
+          residueShading.setConsensus(hconsensus);
         }
       }
 
       if (applet.getParameter("userDefinedColour") != null)
       {
-        ((UserColourScheme) globalColourScheme).parseAppletParameter(applet
-                .getParameter("userDefinedColour"));
+        residueShading = new ResidueShader(
+                new UserColourScheme(
+                        applet.getParameter("userDefinedColour")));
       }
     }
     initAutoAnnotation();
@@ -269,7 +272,11 @@ public class AlignViewport extends AlignmentViewport implements
 
   private float heightScale = 1, widthScale = 1;
 
-  public void setFont(Font f)
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setFont(Font f, boolean setGrid)
   {
     font = f;
     if (nullFrame == null)
@@ -278,14 +285,17 @@ public class AlignViewport extends AlignmentViewport implements
       nullFrame.addNotify();
     }
 
-    java.awt.FontMetrics fm = nullFrame.getGraphics().getFontMetrics(font);
-    setCharHeight((int) (heightScale * fm.getHeight()));
-    setCharWidth((int) (widthScale * fm.charWidth('M')));
+    if (setGrid)
+    {
+      FontMetrics fm = nullFrame.getGraphics().getFontMetrics(font);
+      setCharHeight((int) (heightScale * fm.getHeight()));
+      setCharWidth((int) (widthScale * fm.charWidth('M')));
+    }
 
     if (isUpperCasebold())
     {
       Font f2 = new Font(f.getName(), Font.BOLD, f.getSize());
-      fm = nullFrame.getGraphics().getFontMetrics(f2);
+      FontMetrics fm = nullFrame.getGraphics().getFontMetrics(f2);
       setCharWidth((int) (widthScale * (fm.stringWidth("MMMMMMMMMMM") / 10)));
     }
   }
@@ -297,15 +307,15 @@ public class AlignViewport extends AlignmentViewport implements
 
   public void resetSeqLimits(int height)
   {
-    setEndSeq(height / getCharHeight());
+    ranges.setEndSeq(height / getCharHeight());
   }
 
-  public void setCurrentTree(NJTree tree)
+  public void setCurrentTree(TreeModel tree)
   {
     currentTree = tree;
   }
 
-  public NJTree getCurrentTree()
+  public TreeModel getCurrentTree()
   {
     return currentTree;
   }
@@ -333,7 +343,8 @@ public class AlignViewport extends AlignmentViewport implements
   {
     getStructureSelectionManager().sendSelection(
             new SequenceGroup(getSelectionGroup()),
-            new ColumnSelection(getColumnSelection()), this);
+            new ColumnSelection(getColumnSelection()),
+            new HiddenColumns(getAlignment().getHiddenColumns()), this);
   }
 
   /**
@@ -438,8 +449,9 @@ public class AlignViewport extends AlignmentViewport implements
     int seqOffset = findComplementScrollTarget(sr);
     if (!sr.isEmpty())
     {
-      complementPanel.setFollowingComplementScroll(true);
+      complementPanel.setToScrollComplementPanel(false);
       complementPanel.scrollToCentre(sr, seqOffset);
+      complementPanel.setToScrollComplementPanel(true);
     }
   }
 
index e97c347..e402b9b 100644 (file)
@@ -28,6 +28,8 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceI;
 import jalview.structure.StructureSelectionManager;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -41,10 +43,11 @@ import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
 import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
 public class AlignmentPanel extends Panel implements AdjustmentListener,
-        AlignmentViewPanel
+        AlignmentViewPanel, ViewportListenerI
 {
 
   public AlignViewport av;
@@ -65,6 +68,8 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
 
   AnnotationLabels alabels;
 
+  ViewportRanges vpRanges;
+
   // this value is set false when selection area being dragged
   boolean fastPaint = true;
 
@@ -73,6 +78,7 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
   {
     alignFrame = null;
     av = null;
+    vpRanges = null;
     seqPanel = null;
     seqPanelHolder = null;
     sequenceHolderPanel = null;
@@ -96,6 +102,7 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
 
     alignFrame = af;
     this.av = av;
+    vpRanges = av.getRanges();
     seqPanel = new SeqPanel(av, this);
     idPanel = new IdPanel(av, this);
     scalePanel = new ScalePanel(av, this);
@@ -126,7 +133,26 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
       @Override
       public void componentResized(ComponentEvent evt)
       {
-        setScrollValues(av.getStartRes(), av.getStartSeq());
+        // reset the viewport ranges when the alignment panel is resized
+        // in particular, this initialises the end residue value when Jalview
+        // is initialised
+        if (av.getWrapAlignment())
+        {
+          int widthInRes = seqPanel.seqCanvas
+                  .getWrappedCanvasWidth(seqPanel.seqCanvas.getWidth());
+          vpRanges.setViewportWidth(widthInRes);
+        }
+        else
+        {
+          int widthInRes = seqPanel.seqCanvas.getWidth()
+                  / av.getCharWidth();
+          int heightInSeq = seqPanel.seqCanvas.getHeight()
+                  / av.getCharHeight();
+
+          vpRanges.setViewportWidth(widthInRes);
+          vpRanges.setViewportHeight(heightInSeq);
+        }
+        // setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
         if (getSize().height > 0
                 && annotationPanelHolder.getSize().height > 0)
         {
@@ -159,6 +185,7 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
         }
       }
     });
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   @Override
@@ -225,11 +252,6 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
     annotationPanel.repaint();
     validate();
     repaint();
-
-    if (overviewPanel != null)
-    {
-      overviewPanel.updateOverviewImage();
-    }
   }
 
   public void setIdWidth(int w, int h)
@@ -383,9 +405,10 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
        */
       if (centre)
       {
-        int offset = (av.getEndRes() - av.getStartRes() + 1) / 2 - 1;
+        int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2 - 1;
         start = Math.max(start - offset, 0);
-        end = Math.min(end + offset, seq.getEnd() - 1);
+        end = end + offset - 1;
+        // end = Math.min(end + offset, seq.getEnd() - 1);
       }
 
       if (start < 0)
@@ -414,11 +437,12 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
     int start = -1;
     if (av.hasHiddenColumns())
     {
-      start = av.getColumnSelection().findColumnPosition(ostart);
-      end = av.getColumnSelection().findColumnPosition(end);
+      AlignmentI al = av.getAlignment();
+      start = al.getHiddenColumns().findColumnPosition(ostart);
+      end = al.getHiddenColumns().findColumnPosition(end);
       if (start == end)
       {
-        if (!scrollToNearest && !av.getColumnSelection().isVisible(ostart))
+        if (!scrollToNearest && !al.getHiddenColumns().isVisible(ostart))
         {
           // don't scroll - position isn't visible
           return false;
@@ -468,33 +492,34 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
       // setScrollValues(start, seqIndex);
       // }
       // logic copied from jalview.gui.AlignmentPanel:
-      if ((startv = av.getStartRes()) >= start)
+      if ((startv = vpRanges.getStartRes()) >= start)
       {
         /*
          * Scroll left to make start of search results visible
          */
         setScrollValues(start - 1, seqIndex);
       }
-      else if ((endv = av.getEndRes()) <= end)
+      else if ((endv = vpRanges.getEndRes()) <= end)
       {
         /*
          * Scroll right to make end of search results visible
          */
         setScrollValues(startv + 1 + end - endv, seqIndex);
       }
-      else if ((starts = av.getStartSeq()) > seqIndex)
+      else if ((starts = vpRanges.getStartSeq()) > seqIndex)
       {
         /*
          * Scroll up to make start of search results visible
          */
-        setScrollValues(av.getStartRes(), seqIndex);
+        setScrollValues(vpRanges.getStartRes(), seqIndex);
       }
-      else if ((ends = av.getEndSeq()) <= seqIndex)
+      else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
       {
         /*
          * Scroll down to make end of search results visible
          */
-        setScrollValues(av.getStartRes(), starts + seqIndex - ends + 1);
+        setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends
+                + 1);
       }
       /*
        * Else results are already visible - no need to scroll
@@ -502,27 +527,13 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
     }
     else
     {
-      scrollToWrappedVisible(start);
-    }
-    if (redrawOverview && overviewPanel != null)
-    {
-      overviewPanel.setBoxPosition();
+      vpRanges.scrollToWrappedVisible(start);
     }
+
     paintAlignment(redrawOverview);
     return true;
   }
 
-  void scrollToWrappedVisible(int res)
-  {
-    int cwidth = seqPanel.seqCanvas
-            .getWrappedCanvasWidth(seqPanel.seqCanvas.getSize().width);
-    if (res <= av.getStartRes() || res >= (av.getStartRes() + cwidth))
-    {
-      vscroll.setValue(res / cwidth);
-      av.startRes = vscroll.getValue() * cwidth;
-    }
-  }
-
   public OverviewPanel getOverviewPanel()
   {
     return overviewPanel;
@@ -632,8 +643,8 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
 
   public void setWrapAlignment(boolean wrap)
   {
-    av.startSeq = 0;
-    av.startRes = 0;
+    vpRanges.setStartSeq(0);
+    vpRanges.setStartRes(0);
     scalePanelHolder.setVisible(!wrap);
 
     hscroll.setVisible(!wrap);
@@ -664,219 +675,175 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
 
   int vextent = 0;
 
-  // return value is true if the scroll is valid
-  public boolean scrollUp(boolean up)
+  public void setScrollValues(int xpos, int ypos)
   {
-    if (up)
+    int x = xpos;
+    int y = ypos;
+
+    if (av.getWrapAlignment())
     {
-      if (vscroll.getValue() < 1)
-      {
-        return false;
-      }
-      setScrollValues(hscroll.getValue(), vscroll.getValue() - 1);
+      setScrollingForWrappedPanel(x);
     }
     else
     {
-      if (vextent + vscroll.getValue() >= av.getAlignment().getHeight())
-      {
-        return false;
-      }
-      setScrollValues(hscroll.getValue(), vscroll.getValue() + 1);
-    }
-
-    repaint();
-    return true;
-  }
+      int width = av.getAlignment().getWidth();
+      int height = av.getAlignment().getHeight();
 
-  public boolean scrollRight(boolean right)
-  {
-    if (!right)
-    {
-      if (hscroll.getValue() < 1)
+      if (av.hasHiddenColumns())
       {
-        return false;
+        width = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(width);
       }
-      setScrollValues(hscroll.getValue() - 1, vscroll.getValue());
-    }
-    else
-    {
-      if (hextent + hscroll.getValue() >= av.getAlignment().getWidth())
+      if (x < 0)
       {
-        return false;
+        x = 0;
       }
-      setScrollValues(hscroll.getValue() + 1, vscroll.getValue());
-    }
-
-    repaint();
-    return true;
-  }
-
-  public void setScrollValues(int x, int y)
-  {
-    int width = av.getAlignment().getWidth();
-    int height = av.getAlignment().getHeight();
-
-    if (av.hasHiddenColumns())
-    {
-      width = av.getColumnSelection().findColumnPosition(width);
-    }
-    if (x < 0)
-    {
-      x = 0;
-    }
-    ;
-
-    hextent = seqPanel.seqCanvas.getSize().width / av.getCharWidth();
-    vextent = seqPanel.seqCanvas.getSize().height / av.getCharHeight();
-
-    if (hextent > width)
-    {
-      hextent = width;
-    }
 
-    if (vextent > height)
-    {
-      vextent = height;
-    }
+      hextent = seqPanel.seqCanvas.getSize().width / av.getCharWidth();
+      vextent = seqPanel.seqCanvas.getSize().height / av.getCharHeight();
 
-    if ((hextent + x) > width)
-    {
-      System.err.println("hextent was " + hextent + " and x was " + x);
+      if (hextent > width)
+      {
+        hextent = width;
+      }
 
-      x = width - hextent;
-    }
+      if (vextent > height)
+      {
+        vextent = height;
+      }
 
-    if ((vextent + y) > height)
-    {
-      y = height - vextent;
-    }
+      if ((hextent + x) > width)
+      {
+        System.err.println("hextent was " + hextent + " and x was " + x);
 
-    if (y < 0)
-    {
-      y = 0;
-    }
+        x = width - hextent;
+      }
 
-    if (x < 0)
-    {
-      System.err.println("x was " + x);
-      x = 0;
-    }
+      if ((vextent + y) > height)
+      {
+        y = height - vextent;
+      }
 
-    av.setStartSeq(y);
+      if (y < 0)
+      {
+        y = 0;
+      }
 
-    int endSeq = y + vextent;
-    if (endSeq > av.getAlignment().getHeight())
-    {
-      endSeq = av.getAlignment().getHeight();
-    }
+      if (x < 0)
+      {
+        System.err.println("x was " + x);
+        x = 0;
+      }
 
-    av.setEndSeq(endSeq);
-    av.setStartRes(x);
-    av.setEndRes((x + (seqPanel.seqCanvas.getSize().width / av
-            .getCharWidth())) - 1);
+      hscroll.setValues(x, hextent, 0, width);
+      vscroll.setValues(y, vextent, 0, height);
 
-    hscroll.setValues(x, hextent, 0, width);
-    vscroll.setValues(y, vextent, 0, height);
+      // AWT scrollbar does not fire adjustmentValueChanged for setValues
+      // so also call adjustment code!
+      adjustHorizontal(x);
+      adjustVertical(y);
 
-    if (overviewPanel != null)
-    {
-      overviewPanel.setBoxPosition();
+      sendViewPosition();
     }
-    sendViewPosition();
-
   }
 
+  /**
+   * Respond to adjustment event when horizontal or vertical scrollbar is
+   * changed
+   * 
+   * @param evt
+   *          adjustment event encoding whether apvscroll, hscroll or vscroll
+   *          changed
+   */
   @Override
   public void adjustmentValueChanged(AdjustmentEvent evt)
   {
-    int oldX = av.getStartRes();
-    int oldY = av.getStartSeq();
-
+    // Note that this event is NOT fired by the AWT scrollbar when setValues is
+    // called. Instead manually call adjustHorizontal and adjustVertical
+    // directly.
     if (evt == null || evt.getSource() == apvscroll)
     {
       annotationPanel.setScrollOffset(apvscroll.getValue(), false);
       alabels.setScrollOffset(apvscroll.getValue(), false);
-      // annotationPanel.image=null;
-      // alabels.image=null;
-      // alabels.repaint();
-      // annotationPanel.repaint();
     }
     if (evt == null || evt.getSource() == hscroll)
     {
       int x = hscroll.getValue();
-      av.setStartRes(x);
-      av.setEndRes(x + seqPanel.seqCanvas.getSize().width
-              / av.getCharWidth() - 1);
+      adjustHorizontal(x);
     }
 
     if (evt == null || evt.getSource() == vscroll)
     {
       int offy = vscroll.getValue();
-      if (av.getWrapAlignment())
-      {
-        int rowSize = seqPanel.seqCanvas
-                .getWrappedCanvasWidth(seqPanel.seqCanvas.getSize().width);
-        av.setStartRes(vscroll.getValue() * rowSize);
-        av.setEndRes((vscroll.getValue() + 1) * rowSize);
-      }
-      else
-      {
-        av.setStartSeq(offy);
-        av.setEndSeq(offy + seqPanel.seqCanvas.getSize().height
-                / av.getCharHeight());
-      }
+      adjustVertical(offy);
     }
 
-    if (overviewPanel != null)
+  }
+
+  private void adjustHorizontal(int x)
+  {
+    int oldX = vpRanges.getStartRes();
+    int oldwidth = vpRanges.getViewportWidth();
+    int width = seqPanel.seqCanvas.getWidth() / av.getCharWidth();
+
+    // if we're scrolling to the position we're already at, stop
+    // this prevents infinite recursion of events when the scroll/viewport
+    // ranges values are the same
+    if ((x == oldX) && (width == oldwidth))
     {
-      overviewPanel.setBoxPosition();
+      return;
     }
+    vpRanges.setViewportStartAndWidth(x, width);
 
-    int scrollX = av.startRes - oldX;
-    int scrollY = av.startSeq - oldY;
-
-    if (av.getWrapAlignment() || !fastPaint || av.MAC)
+    if (av.getWrapAlignment() || !fastPaint)
     {
       repaint();
     }
-    else
+    sendViewPosition();
+  }
+
+  private void adjustVertical(int offy)
+  {
+    int oldX = vpRanges.getStartRes();
+    int oldwidth = vpRanges.getViewportWidth();
+    int oldY = vpRanges.getStartSeq();
+    int oldheight = vpRanges.getViewportHeight();
+
+    if (av.getWrapAlignment())
     {
-      // Make sure we're not trying to draw a panel
-      // larger than the visible window
-      if (scrollX > av.endRes - av.startRes)
+      int rowSize = seqPanel.seqCanvas
+              .getWrappedCanvasWidth(seqPanel.seqCanvas.getWidth());
+
+      // if we're scrolling to the position we're already at, stop
+      // this prevents infinite recursion of events when the scroll/viewport
+      // ranges values are the same
+      if ((offy * rowSize == oldX) && (oldwidth == rowSize))
       {
-        scrollX = av.endRes - av.startRes;
+        return;
       }
-      else if (scrollX < av.startRes - av.endRes)
+      else if (offy > -1)
       {
-        scrollX = av.startRes - av.endRes;
+        vpRanges.setViewportStartAndWidth(offy * rowSize, rowSize);
       }
+    }
+    else
+    {
+      int height = seqPanel.seqCanvas.getHeight() / av.getCharHeight();
 
-      idPanel.idCanvas.fastPaint(scrollY);
-      seqPanel.seqCanvas.fastPaint(scrollX, scrollY);
-
-      scalePanel.repaint();
-      if (av.isShowAnnotation())
+      // if we're scrolling to the position we're already at, stop
+      // this prevents infinite recursion of events when the scroll/viewport
+      // ranges values are the same
+      if ((offy == oldY) && (height == oldheight))
       {
-        annotationPanel.fastPaint(av.getStartRes() - oldX);
+        return;
       }
+      vpRanges.setViewportStartAndHeight(offy, height);
     }
-    sendViewPosition();
-
-    /*
-     * If there is one, scroll the (Protein/cDNA) complementary alignment to
-     * match, unless we are ourselves doing that.
-     */
-    if (isFollowingComplementScroll())
-    {
-      setFollowingComplementScroll(false);
-    }
-    else
+    if (av.getWrapAlignment() || !fastPaint)
     {
-      AlignmentPanel ap = getComplementPanel();
-      av.scrollComplementaryAlignment(ap);
+      repaint();
     }
-
+    sendViewPosition();
   }
 
   /**
@@ -927,7 +894,6 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
      * This is like AlignmentI.findIndex(seq) but here we are matching the
      * dataset sequence not the aligned sequence
      */
-    int sequenceIndex = 0;
     boolean matched = false;
     for (SequenceI seq : seqs)
     {
@@ -936,7 +902,6 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
         matched = true;
         break;
       }
-      sequenceIndex++;
     }
     if (!matched)
     {
@@ -947,16 +912,15 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
      * Scroll to position but centring the target residue. Also set a state flag
      * to prevent adjustmentValueChanged performing this recursively.
      */
-    setFollowingComplementScroll(true);
-    // this should be scrollToPosition(sr,verticalOffset,
     scrollToPosition(sr, seqOffset, true, true);
   }
 
   private void sendViewPosition()
   {
     StructureSelectionManager.getStructureSelectionManager(av.applet)
-            .sendViewPosition(this, av.startRes, av.endRes, av.startSeq,
-                    av.endSeq);
+            .sendViewPosition(this, vpRanges.getStartRes(),
+                    vpRanges.getEndRes(), vpRanges.getStartSeq(),
+                    vpRanges.getEndSeq());
   }
 
   /**
@@ -1002,30 +966,7 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
       idPanel.idCanvas.setSize(d.width, canvasHeight);
     }
 
-    if (av.getWrapAlignment())
-    {
-      int maxwidth = av.getAlignment().getWidth();
-
-      if (av.hasHiddenColumns())
-      {
-        maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
-      }
-
-      int canvasWidth = seqPanel.seqCanvas
-              .getWrappedCanvasWidth(seqPanel.seqCanvas.getSize().width);
-
-      if (canvasWidth > 0)
-      {
-        int max = maxwidth / canvasWidth;
-        vscroll.setMaximum(1 + max);
-        vscroll.setUnitIncrement(1);
-        vscroll.setVisibleAmount(1);
-      }
-    }
-    else
-    {
-      setScrollValues(av.getStartRes(), av.getStartSeq());
-    }
+    setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
 
     seqPanel.seqCanvas.repaint();
     idPanel.idCanvas.repaint();
@@ -1041,6 +982,37 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
 
   }
 
+  /*
+   * Set vertical scroll bar parameters for wrapped panel
+   * @param res 
+   *    the residue to scroll to
+   */
+  private void setScrollingForWrappedPanel(int res)
+  {
+    // get the width of the alignment in residues
+    int maxwidth = av.getAlignment().getWidth();
+    if (av.hasHiddenColumns())
+    {
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
+    }
+
+    // get the width of the canvas in residues
+    int canvasWidth = seqPanel.seqCanvas
+            .getWrappedCanvasWidth(seqPanel.seqCanvas.getSize().width);
+    if (canvasWidth > 0)
+    {
+      // position we want to scroll to is number of canvasWidth's to get there
+      int current = res / canvasWidth;
+
+      // max scroll position: add one because extent is 1 and scrollbar value
+      // can only be set to at most max - extent
+      int max = maxwidth / canvasWidth + 1;
+      vscroll.setUnitIncrement(1);
+      vscroll.setValues(current, 1, 0, max);
+    }
+  }
+
   protected Panel sequenceHolderPanel = new Panel();
 
   protected Scrollbar vscroll = new Scrollbar();
@@ -1065,9 +1037,9 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
 
   /*
    * Flag set while scrolling to follow complementary cDNA/protein scroll. When
-   * true, suppresses invoking the same method recursively.
+   * false, suppresses invoking the same method recursively.
    */
-  private boolean followingComplementScroll;
+  private boolean scrollComplementaryPanel = true;
 
   private void jbInit() throws Exception
   {
@@ -1176,14 +1148,42 @@ public class AlignmentPanel extends Panel implements AdjustmentListener,
    * 
    * @param b
    */
-  protected void setFollowingComplementScroll(boolean b)
+  protected void setToScrollComplementPanel(boolean b)
+  {
+    this.scrollComplementaryPanel = b;
+  }
+
+  /**
+   * Get whether to scroll complement panel
+   * 
+   * @return true if cDNA/protein complement panels should be scrolled
+   */
+  protected boolean isSetToScrollComplementPanel()
   {
-    this.followingComplementScroll = b;
+    return this.scrollComplementaryPanel;
   }
 
-  protected boolean isFollowingComplementScroll()
+  @Override
+  /**
+   * Property change event fired when a change is made to the viewport ranges 
+   * object associated with this alignment panel's viewport
+   */
+  public void propertyChange(PropertyChangeEvent evt)
   {
-    return this.followingComplementScroll;
+    // update this panel's scroll values based on the new viewport ranges values
+    int x = vpRanges.getStartRes();
+    int y = vpRanges.getStartSeq();
+    setScrollValues(x, y);
+
+    // now update any complementary alignment (its viewport ranges object
+    // is different so does not get automatically updated)
+    if (isSetToScrollComplementPanel())
+    {
+      setToScrollComplementPanel(false);
+      av.scrollComplementaryAlignment(getComplementPanel());
+      setToScrollComplementPanel(true);
+    }
+
   }
 
 }
index 79d2f1f..f516bc9 100644 (file)
@@ -46,7 +46,8 @@ import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
-import java.util.Hashtable;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Vector;
 
 public class AnnotationColourChooser extends Panel implements
@@ -60,9 +61,15 @@ public class AnnotationColourChooser extends Panel implements
 
   ColourSchemeI oldcs;
 
-  Hashtable oldgroupColours;
+  Map<SequenceGroup, ColourSchemeI> oldgroupColours;
 
-  jalview.datamodel.AlignmentAnnotation currentAnnotation;
+  /*
+   * map from annotation to its menu item display label
+   * - so we know which item to pre-select on restore
+   */
+  private Map<AlignmentAnnotation, String> annotationLabels;
+
+  AlignmentAnnotation currentAnnotation;
 
   boolean adjusting = false;
 
@@ -78,17 +85,10 @@ public class AnnotationColourChooser extends Panel implements
     oldcs = av.getGlobalColourScheme();
     if (av.getAlignment().getGroups() != null)
     {
-      oldgroupColours = new Hashtable();
+      oldgroupColours = new HashMap<SequenceGroup, ColourSchemeI>();
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
-        if (sg.cs != null)
-        {
-          oldgroupColours.put(sg, sg.cs);
-        }
-        else
-        {
-          oldgroupColours.put(sg, "null");
-        }
+        oldgroupColours.put(sg, sg.getColourScheme());
       }
     }
     this.av = av;
@@ -119,24 +119,7 @@ public class AnnotationColourChooser extends Panel implements
       // seqAssociated.setState(acg.isSeqAssociated());
     }
 
-    Vector<String> list = new Vector<String>();
-    int index = 1;
-    for (int i = 0; i < anns.length; i++)
-    {
-      String label = anns[i].label;
-      if (anns[i].sequenceRef != null)
-      {
-        label = label + "_" + anns[i].sequenceRef.getName();
-      }
-      if (!list.contains(label))
-      {
-        list.addElement(label);
-      }
-      else
-      {
-        list.addElement(label + "_" + (index++));
-      }
-    }
+    Vector<String> list = getAnnotationItems();
 
     for (int i = 0; i < list.size(); i++)
     {
@@ -153,7 +136,8 @@ public class AnnotationColourChooser extends Panel implements
     if (oldcs instanceof AnnotationColourGradient)
     {
       AnnotationColourGradient acg = (AnnotationColourGradient) oldcs;
-      annotations.select(acg.getAnnotation());
+      String label = annotationLabels.get(acg.getAnnotation());
+      annotations.select(label);
       switch (acg.getAboveThreshold())
       {
       case AnnotationColourGradient.NO_THRESHOLD:
@@ -170,7 +154,7 @@ public class AnnotationColourChooser extends Panel implements
                 MessageManager
                         .getString("error.implementation_error_dont_know_threshold_annotationcolourgradient"));
       }
-      thresholdIsMin.setState(acg.thresholdIsMinMax);
+      thresholdIsMin.setState(acg.isThresholdIsMinMax());
       thresholdValue.setText("" + acg.getAnnotationThreshold());
     }
 
@@ -186,6 +170,51 @@ public class AnnotationColourChooser extends Panel implements
     validate();
   }
 
+  /**
+   * Builds and returns a list of menu items (display text) for choice of
+   * annotation. Also builds a map between annotations and their display labels.
+   * 
+   * @return
+   */
+  protected Vector<String> getAnnotationItems()
+  {
+    // TODO remove duplication with gui.AnnotationRowFilter
+    // TODO add 'per sequence only' option / parameter
+
+    annotationLabels = new HashMap<AlignmentAnnotation, String>();
+    Vector<String> list = new Vector<String>();
+    AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
+    if (anns == null)
+    {
+      return list;
+    }
+    int index = 1;
+    for (int i = 0; i < anns.length; i++)
+    {
+      String label = anns[i].label;
+      if (anns[i].sequenceRef != null)
+      {
+        /*
+         * be helpful and include sequence id in label for
+         * sequence-associated annotation (JAL-2236)
+         */
+        label = label + "_" + anns[i].sequenceRef.getName();
+      }
+      if (!list.contains(label))
+      {
+        list.addElement(label);
+        annotationLabels.put(anns[i], label);
+      }
+      else
+      {
+        label = label + "_" + (index++);
+        list.addElement(label);
+        annotationLabels.put(anns[i], label);
+      }
+    }
+    return list;
+  }
+
   private void setDefaultMinMax()
   {
     minColour.setBackground(av.applet.getDefaultColourParameter(
@@ -487,8 +516,6 @@ public class AnnotationColourChooser extends Panel implements
     AnnotationColourGradient acg = null;
     if (currentColours.getState())
     {
-      acg = new AnnotationColourGradient(currentAnnotation,
-              av.getGlobalColourScheme(), aboveThreshold);
     }
     else
     {
@@ -503,7 +530,7 @@ public class AnnotationColourChooser extends Panel implements
       acg.setPredefinedColours(true);
     }
 
-    acg.thresholdIsMinMax = thresholdIsMin.getState();
+    acg.setThresholdIsMinMax(thresholdIsMin.getState());
 
     av.setGlobalColourScheme(acg);
 
@@ -512,24 +539,22 @@ public class AnnotationColourChooser extends Panel implements
     {
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
-
-        if (sg.cs == null)
+        if (sg.getColourScheme() == null)
         {
           continue;
         }
 
         if (currentColours.getState())
         {
-          sg.cs = new AnnotationColourGradient(currentAnnotation, sg.cs,
-                  aboveThreshold);
+          sg.setColourScheme(new AnnotationColourGradient(
+                  currentAnnotation, sg.getColourScheme(), aboveThreshold));
         }
         else
         {
-          sg.cs = new AnnotationColourGradient(currentAnnotation,
-                  minColour.getBackground(), maxColour.getBackground(),
-                  aboveThreshold);
+          sg.setColourScheme(new AnnotationColourGradient(
+                  currentAnnotation, minColour.getBackground(), maxColour
+                          .getBackground(), aboveThreshold));
         }
-
       }
     }
 
@@ -545,20 +570,10 @@ public class AnnotationColourChooser extends Panel implements
     {
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
-        Object cs = oldgroupColours.get(sg);
-        if (cs instanceof ColourSchemeI)
-        {
-          sg.cs = (ColourSchemeI) cs;
-        }
-        else
-        {
-          // probably the "null" string we set it to if it was null originally.
-          sg.cs = null;
-        }
+        sg.setColourScheme(oldgroupColours.get(sg));
       }
     }
     ap.paintAlignment(true);
-
   }
 
   @Override
index a8dff62..60775d3 100644 (file)
@@ -21,7 +21,7 @@
 package jalview.appletgui;
 
 import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.schemes.AnnotationColourGradient;
 import jalview.util.MessageManager;
 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
@@ -110,7 +110,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
   private int actionOption = ACTION_OPTION_SELECT;
 
-  private ColumnSelection oldColumnSelection;
+  private HiddenColumns oldHiddenColumns;
 
   public AnnotationColumnChooser()
   {
@@ -140,7 +140,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     {
       return;
     }
-    setOldColumnSelection(av.getColumnSelection());
+    setOldHiddenColumns(av.getAlignment().getHiddenColumns());
     adjusting = true;
     Vector<String> list = new Vector<String>();
     int index = 1;
@@ -167,20 +167,22 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     }
 
     populateThresholdComboBox(threshold);
-
+    AnnotationColumnChooser lastChooser = av
+            .getAnnotationColumnSelectionState();
     // restore Object state from the previous session if one exists
-    if (av.getAnnotationColumnSelectionState() != null)
+    if (lastChooser != null)
     {
-      currentSearchPanel = av.getAnnotationColumnSelectionState()
+      currentSearchPanel = lastChooser
               .getCurrentSearchPanel();
-      currentStructureFilterPanel = av.getAnnotationColumnSelectionState()
+      currentStructureFilterPanel = lastChooser
               .getCurrentStructureFilterPanel();
-      annotations.select(av.getAnnotationColumnSelectionState()
+      annotations.select(lastChooser
               .getAnnotations().getSelectedIndex());
-      threshold.select(av.getAnnotationColumnSelectionState()
+      threshold.select(lastChooser
               .getThreshold().getSelectedIndex());
-      actionOption = av.getAnnotationColumnSelectionState()
+      actionOption = lastChooser
               .getActionOption();
+      percentThreshold.setState(lastChooser.percentThreshold.getState());
     }
 
     try
@@ -204,6 +206,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
     thresholdValue.setEnabled(false);
     thresholdValue.setColumns(7);
+    thresholdValue.setCaretPosition(0);
 
     ok.addActionListener(this);
     cancel.addActionListener(this);
@@ -219,6 +222,9 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     // thresholdPanel.setFont(JvSwingUtils.getLabelFont());
     // thresholdPanel.setLayout(new MigLayout("", "[left][right]", "[][]"));
 
+    percentThreshold.setLabel("As percentage");
+    percentThreshold.addItemListener(this);
+
     actionPanel.setBackground(Color.white);
     // actionPanel.setFont(JvSwingUtils.getLabelFont());
 
@@ -242,6 +248,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     thresholdPanel.add(getThreshold());
     thresholdPanel.add(slider);
     thresholdPanel.add(thresholdValue);
+    thresholdPanel.add(percentThreshold);
 
     actionPanel.add(ok);
     actionPanel.add(cancel);
@@ -282,26 +289,26 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
   @SuppressWarnings("unchecked")
   public void reset()
   {
-    if (this.getOldColumnSelection() != null)
+    if (this.getOldHiddenColumns() != null)
     {
       av.getColumnSelection().clear();
 
       if (av.getAnnotationColumnSelectionState() != null)
       {
-        ColumnSelection oldSelection = av
+        HiddenColumns oldHidden = av
                 .getAnnotationColumnSelectionState()
-                .getOldColumnSelection();
-        if (oldSelection != null && oldSelection.getHiddenColumns() != null
-                && !oldSelection.getHiddenColumns().isEmpty())
+                .getOldHiddenColumns();
+        if (oldHidden != null && oldHidden.getHiddenRegions() != null
+                && !oldHidden.getHiddenRegions().isEmpty())
         {
-          for (Iterator<int[]> itr = oldSelection.getHiddenColumns()
+          for (Iterator<int[]> itr = oldHidden.getHiddenRegions()
                   .iterator(); itr.hasNext();)
           {
             int positions[] = itr.next();
             av.hideColumns(positions[0], positions[1]);
           }
         }
-        av.setColumnSelection(oldSelection);
+        av.getAlignment().setHiddenColumns(oldHidden);
       }
       ap.paintAlignment(true);
     }
@@ -313,7 +320,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
   {
     if (!adjusting)
     {
-      thresholdValue.setText((slider.getValue() / 1000f) + "");
+      setThresholdValueText();
       valueChanged(!sliderDragging);
     }
   }
@@ -399,12 +406,14 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
     slider.setEnabled(true);
     thresholdValue.setEnabled(true);
+    percentThreshold.setEnabled(true);
 
     if (selectedThresholdItem == AnnotationColourGradient.NO_THRESHOLD)
     {
       slider.setEnabled(false);
       thresholdValue.setEnabled(false);
       thresholdValue.setText("");
+      percentThreshold.setEnabled(false);
       // build filter params
     }
     else if (selectedThresholdItem != AnnotationColourGradient.NO_THRESHOLD)
@@ -425,16 +434,17 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       slider.setMinimum((int) (getCurrentAnnotation().graphMin * 1000));
       slider.setMaximum((int) (getCurrentAnnotation().graphMax * 1000));
       slider.setValue((int) (getCurrentAnnotation().threshold.value * 1000));
-      thresholdValue.setText(getCurrentAnnotation().threshold.value + "");
+      setThresholdValueText();
       // slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
+      percentThreshold.setEnabled(true);
       adjusting = false;
 
       // build filter params
       filterParams
               .setThresholdType(AnnotationFilterParameter.ThresholdType.NO_THRESHOLD);
-      if (getCurrentAnnotation().graph != AlignmentAnnotation.NO_GRAPH)
+      if (getCurrentAnnotation().isQuantitative())
       {
         filterParams
                 .setThresholdValue(getCurrentAnnotation().threshold.value);
@@ -508,16 +518,16 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     ap.paintAlignment(true);
   }
 
-  public ColumnSelection getOldColumnSelection()
+  public HiddenColumns getOldHiddenColumns()
   {
-    return oldColumnSelection;
+    return oldHiddenColumns;
   }
 
-  public void setOldColumnSelection(ColumnSelection currentColumnSelection)
+  public void setOldHiddenColumns(HiddenColumns currentHiddenColumns)
   {
-    if (currentColumnSelection != null)
+    if (currentHiddenColumns != null)
     {
-      this.oldColumnSelection = new ColumnSelection(currentColumnSelection);
+      this.oldHiddenColumns = new HiddenColumns(currentHiddenColumns);
     }
   }
 
@@ -574,13 +584,21 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     {
       threshold_actionPerformed(null);
     }
+    else if (e.getSource() == percentThreshold)
+    {
+      if (!adjusting)
+      {
+        percentageValue_actionPerformed();
+      }
+
+    }
   }
 
   public void selectedAnnotationChanged()
   {
     String currentView = AnnotationColumnChooser.NO_GRAPH_VIEW;
     if (av.getAlignment().getAlignmentAnnotation()[getAnnotations()
-            .getSelectedIndex()].graph != AlignmentAnnotation.NO_GRAPH)
+            .getSelectedIndex()].isQuantitative())
     {
       currentView = AnnotationColumnChooser.GRAPH_VIEW;
     }
@@ -877,19 +895,8 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
   @Override
   public void actionPerformed(ActionEvent evt)
   {
-    if (evt.getSource() == thresholdValue)
-    {
-      try
-      {
-        float f = new Float(thresholdValue.getText()).floatValue();
-        slider.setValue((int) (f * 1000));
-        adjustmentValueChanged(null);
-      } catch (NumberFormatException ex)
-      {
-      }
-    }
 
-    else if (evt.getSource() == ok)
+    if (evt.getSource() == ok)
     {
       ok_actionPerformed(null);
     }
index b28ccc7..307301d 100755 (executable)
@@ -339,7 +339,8 @@ public class AnnotationLabels extends Panel implements ActionListener,
                 av.calcPanelHeight());
         f.height += dif;
         ap.seqPanelHolder.setPreferredSize(f);
-        ap.setScrollValues(av.getStartRes(), av.getStartSeq());
+        ap.setScrollValues(av.getRanges().getStartRes(), av.getRanges()
+                .getStartSeq());
         ap.validate();
         // ap.paintAlignment(true);
         ap.addNotify();
@@ -838,7 +839,8 @@ public class AnnotationLabels extends Panel implements ActionListener,
     if (av.hasHiddenColumns())
     {
       jalview.appletgui.AlignFrame.copiedHiddenColumns = new Vector();
-      for (int[] region : av.getColumnSelection().getHiddenColumns())
+      for (int[] region : av.getAlignment().getHiddenColumns()
+              .getHiddenRegions())
       {
         jalview.appletgui.AlignFrame.copiedHiddenColumns
                 .addElement(new int[] { region[0], region[1] });
index 6012c1a..c658811 100755 (executable)
@@ -29,6 +29,7 @@ import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.viewmodel.ViewportListenerI;
 
 import java.awt.Color;
 import java.awt.Dimension;
@@ -47,10 +48,11 @@ import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
+import java.beans.PropertyChangeEvent;
 
 public class AnnotationPanel extends Panel implements AwtRenderPanelI,
         AdjustmentListener, ActionListener, MouseListener,
-        MouseMotionListener
+        MouseMotionListener, ViewportListenerI
 {
   AlignViewport av;
 
@@ -122,6 +124,7 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
   {
     this.av = av;
     renderer = new AnnotationRenderer();
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   @Override
@@ -165,7 +168,7 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
     {
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (av.getColumnSelection().isVisible(index))
+        if (av.getAlignment().getHiddenColumns().isVisible(index))
         {
           anot[index] = null;
         }
@@ -189,7 +192,7 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
       {
         // TODO: JAL-2001 - provide a fast method to list visible selected
         // columns
-        if (!av.getColumnSelection().isVisible(index))
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
         {
           continue;
         }
@@ -211,7 +214,7 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
 
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (!av.getColumnSelection().isVisible(index))
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
         {
           continue;
         }
@@ -271,7 +274,7 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
 
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (!av.getColumnSelection().isVisible(index))
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
         {
           continue;
         }
@@ -462,11 +465,13 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
       }
     }
 
-    int column = evt.getX() / av.getCharWidth() + av.getStartRes();
+    int column = evt.getX() / av.getCharWidth()
+            + av.getRanges().getStartRes();
 
     if (av.hasHiddenColumns())
     {
-      column = av.getColumnSelection().adjustForHiddenColumns(column);
+      column = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(column);
     }
 
     if (row > -1 && column < aa[row].annotations.length
@@ -618,7 +623,8 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
 
     gg.setColor(Color.white);
     gg.fillRect(0, 0, getSize().width, getSize().height);
-    drawComponent(gg, av.startRes, av.endRes + 1);
+    drawComponent(gg, av.getRanges().getStartRes(), av.getRanges()
+            .getEndRes() + 1);
 
     g.drawImage(image, 0, 0, this);
   }
@@ -635,7 +641,7 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
 
     gg.copyArea(0, 0, imgWidth, getSize().height,
             -horizontal * av.getCharWidth(), 0);
-    int sr = av.startRes, er = av.endRes + 1, transX = 0;
+    int sr = av.getRanges().getStartRes(), er = av.getRanges().getEndRes() + 1, transX = 0;
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
     {
@@ -747,4 +753,15 @@ public class AnnotationPanel extends Panel implements AwtRenderPanelI,
       return null;
     }
   }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport range changes (e.g. alignment panel was scrolled)
+    if (evt.getPropertyName().equals("startres")
+            || evt.getPropertyName().equals("endres"))
+    {
+      fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
+    }
+  }
 }
index 8d67d71..315ce3b 100644 (file)
@@ -31,7 +31,6 @@ import java.awt.Panel;
 import java.awt.Scrollbar;
 import java.awt.TextField;
 import java.awt.event.ActionEvent;
-import java.util.Vector;
 
 @SuppressWarnings("serial")
 public abstract class AnnotationRowFilter extends Panel
@@ -60,6 +59,8 @@ public abstract class AnnotationRowFilter extends Panel
 
   protected Scrollbar slider = new Scrollbar(Scrollbar.HORIZONTAL);
 
+  protected Checkbox percentThreshold = new Checkbox();
+
   protected TextField thresholdValue = new TextField(20);
 
   protected Frame frame;
@@ -132,18 +133,54 @@ public abstract class AnnotationRowFilter extends Panel
     updateView();
   }
 
+  /**
+   * update the text field from the threshold slider. preserves state of
+   * 'adjusting' so safe to call in init.
+   */
+  protected void setThresholdValueText()
+  {
+    boolean oldadj = adjusting;
+    adjusting = true;
+    if (percentThreshold.getState())
+    {
+      double scl = slider.getMaximum() - slider.getMinimum();
+      scl = (slider.getValue() - slider.getMinimum()) / scl;
+      thresholdValue.setText(100f * scl + "");
+    }
+    else
+    {
+      thresholdValue.setText((slider.getValue() / 1000f) + "");
+    }
+    thresholdValue.setCaretPosition(0);
+    adjusting = oldadj;
+  }
+  
   public void thresholdValue_actionPerformed(ActionEvent e)
   {
     try
     {
       float f = Float.parseFloat(thresholdValue.getText());
-      slider.setValue((int) (f * 1000));
-      updateView();
+      if (percentThreshold.getState())
+      {
+        int pos = slider.getMinimum()
+                + (int) ((slider.getMaximum() - slider.getMinimum()) * f / 100f);
+        slider.setValue(pos);
+      }
+      else
+      {
+        slider.setValue((int) (f * 1000));
+      }
+      valueChanged(false);
     } catch (NumberFormatException ex)
     {
     }
   }
 
+  protected void percentageValue_actionPerformed()
+  {
+    setThresholdValueText();
+  }
+
   protected void populateThresholdComboBox(Choice threshold)
   {
     threshold.addItem(MessageManager
index 9d4779c..c0b4ff0 100644 (file)
@@ -24,8 +24,8 @@ import jalview.bin.JalviewLite;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
-import jalview.io.FileParse;
 import jalview.io.DataSourceType;
+import jalview.io.FileParse;
 import jalview.io.StructureFile;
 import jalview.schemes.BuriedColourScheme;
 import jalview.schemes.HelixColourScheme;
@@ -93,27 +93,29 @@ public class AppletJmol extends EmbmenuFrame implements
   MenuItem charge = new MenuItem(
           MessageManager.getString("label.charge_cysteine"));
 
-  MenuItem zappo = new MenuItem(MessageManager.getString("label.zappo"));
+  MenuItem zappo = new MenuItem(
+          MessageManager.getString("label.colourScheme_zappo"));
 
-  MenuItem taylor = new MenuItem(MessageManager.getString("label.taylor"));
+  MenuItem taylor = new MenuItem(
+          MessageManager.getString("label.colourScheme_taylor"));
 
   MenuItem hydro = new MenuItem(
-          MessageManager.getString("label.hydrophobicity"));
+          MessageManager.getString("label.colourScheme_hydrophobic"));
 
   MenuItem helix = new MenuItem(
-          MessageManager.getString("label.helix_propensity"));
+          MessageManager.getString("label.colourScheme_helix_propensity"));
 
   MenuItem strand = new MenuItem(
-          MessageManager.getString("label.strand_propensity"));
+          MessageManager.getString("label.colourScheme_strand_propensity"));
 
   MenuItem turn = new MenuItem(
-          MessageManager.getString("label.turn_propensity"));
+          MessageManager.getString("label.colourScheme_turn_propensity"));
 
   MenuItem buried = new MenuItem(
-          MessageManager.getString("label.buried_index"));
+          MessageManager.getString("label.colourScheme_buried_index"));
 
   MenuItem purinepyrimidine = new MenuItem(
-          MessageManager.getString("label.purine_pyrimidine"));
+          MessageManager.getString("label.colourScheme_purine/pyrimidine"));
 
   MenuItem user = new MenuItem(
           MessageManager.getString("label.user_defined_colours"));
@@ -584,7 +586,7 @@ public class AppletJmol extends EmbmenuFrame implements
 
   public void updateTitleAndMenus()
   {
-    if (jmb.fileLoadingError != null && jmb.fileLoadingError.length() > 0)
+    if (jmb.hasFileLoadingError())
     {
       repaint();
       return;
index f938cad..9b8a235 100644 (file)
@@ -54,21 +54,7 @@ class AppletJmolBinding extends JalviewJmolBinding
   public jalview.api.FeatureRenderer getFeatureRenderer(
           AlignmentViewPanel alignment)
   {
-    AlignmentPanel ap = (AlignmentPanel) alignment;
-    if (appletJmolBinding.ap.av.isShowSequenceFeatures())
-    {
-      if (appletJmolBinding.fr == null)
-      {
-        appletJmolBinding.fr = new jalview.appletgui.FeatureRenderer(
-                appletJmolBinding.ap.av);
-      }
-
-      appletJmolBinding.fr
-              .transferSettings(appletJmolBinding.ap.seqPanel.seqCanvas
-                      .getFeatureRenderer());
-    }
-
-    return appletJmolBinding.fr;
+    return appletJmolBinding.ap.getFeatureRenderer();
   }
 
   @Override
index c658734..1e806a5 100644 (file)
@@ -25,7 +25,7 @@ import jalview.api.ComplexAlignFile;
 import jalview.api.FeaturesSourceI;
 import jalview.bin.JalviewLite;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.io.AlignmentFileReaderI;
@@ -257,8 +257,8 @@ public class CutAndPasteTransfer extends Panel implements ActionListener,
 
         if (source instanceof ComplexAlignFile)
         {
-          ColumnSelection colSel = ((ComplexAlignFile) source)
-                  .getColumnSelection();
+            HiddenColumns colSel = ((ComplexAlignFile) source)
+                    .getHiddenColumns();
           SequenceI[] hiddenSeqs = ((ComplexAlignFile) source)
                   .getHiddenSequences();
           boolean showSeqFeatures = ((ComplexAlignFile) source)
index 189fe88..b369318 100644 (file)
@@ -82,10 +82,10 @@ public class ExtJmol extends JalviewJmolBinding
   @Override
   public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment)
   {
-    AlignmentPanel ap = (AlignmentPanel) alignment;
-    if (ap.av.isShowSequenceFeatures())
+    AlignmentPanel alignPanel = (AlignmentPanel) alignment;
+    if (alignPanel.av.isShowSequenceFeatures())
     {
-      return ap.getFeatureRenderer();
+      return alignPanel.getFeatureRenderer();
     }
     else
     {
index 2fca07d..435e78d 100644 (file)
@@ -27,7 +27,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.io.FeaturesFile;
 import jalview.schemes.FeatureColour;
-import jalview.schemes.UserColourScheme;
+import jalview.util.ColorUtils;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
 
@@ -36,7 +36,9 @@ import java.awt.Button;
 import java.awt.Choice;
 import java.awt.Color;
 import java.awt.Dimension;
+import java.awt.FlowLayout;
 import java.awt.Font;
+import java.awt.Frame;
 import java.awt.Graphics;
 import java.awt.GridLayout;
 import java.awt.Label;
@@ -48,7 +50,10 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.awt.event.TextEvent;
+import java.awt.event.TextListener;
 import java.util.Hashtable;
+import java.util.List;
 
 /**
  * DOCUMENT ME!
@@ -59,6 +64,13 @@ import java.util.Hashtable;
 public class FeatureRenderer extends
         jalview.renderer.seqfeatures.FeatureRenderer
 {
+  /*
+   * creating a new feature defaults to the type and group as
+   * the last one created
+   */
+  static String lastFeatureAdded = "feature_1";
+
+  static String lastFeatureGroupAdded = "Jalview";
 
   // Holds web links for feature groups and feature types
   // in the form label|link
@@ -75,12 +87,6 @@ public class FeatureRenderer extends
 
   }
 
-  static String lastFeatureAdded;
-
-  static String lastFeatureGroupAdded;
-
-  static String lastDescriptionAdded;
-
   int featureIndex = 0;
 
   boolean deleteFeature = false;
@@ -165,13 +171,25 @@ public class FeatureRenderer extends
 
   }
 
-  boolean amendFeatures(final SequenceI[] sequences,
-          final SequenceFeature[] features, boolean newFeatures,
+  /**
+   * Shows a dialog allowing the user to create, or amend or delete, sequence
+   * features. If null in the supplied feature(s), feature type and group
+   * default to those for the last feature created (with initial defaults of
+   * "feature_1" and "Jalview").
+   * 
+   * @param sequences
+   * @param features
+   * @param create
+   * @param ap
+   * @return
+   */
+  boolean amendFeatures(final List<SequenceI> sequences,
+          final List<SequenceFeature> features, boolean create,
           final AlignmentPanel ap)
   {
-    Panel bigPanel = new Panel(new BorderLayout());
+    final Panel bigPanel = new Panel(new BorderLayout());
     final TextField name = new TextField(16);
-    final TextField source = new TextField(16);
+    final TextField group = new TextField(16);
     final TextArea description = new TextArea(3, 35);
     final TextField start = new TextField(8);
     final TextField end = new TextField(8);
@@ -179,6 +197,22 @@ public class FeatureRenderer extends
     Button deleteButton = new Button("Delete");
     deleteFeature = false;
 
+    name.addTextListener(new TextListener()
+    {
+      @Override
+      public void textValueChanged(TextEvent e)
+      {
+        warnIfTypeHidden(ap.alignFrame, name.getText());
+      }
+    });
+    group.addTextListener(new TextListener()
+    {
+      @Override
+      public void textValueChanged(TextEvent e)
+      {
+        warnIfGroupHidden(ap.alignFrame, group.getText());
+      }
+    });
     colourPanel = new FeatureColourPanel();
     colourPanel.setSize(110, 15);
     final FeatureRenderer fr = this;
@@ -190,22 +224,20 @@ public class FeatureRenderer extends
 
     // /////////////////////////////////////
     // /MULTIPLE FEATURES AT SELECTED RESIDUE
-    if (!newFeatures && features.length > 1)
+    if (!create && features.size() > 1)
     {
       panel = new Panel(new GridLayout(4, 1));
       tmp = new Panel();
       tmp.add(new Label("Select Feature: "));
       overlaps = new Choice();
-      for (int i = 0; i < features.length; i++)
+      for (SequenceFeature sf : features)
       {
-        String item = features[i].getType() + "/" + features[i].getBegin()
-                + "-" + features[i].getEnd();
-
-        if (features[i].getFeatureGroup() != null)
+        String item = sf.getType() + "/" + sf.getBegin() + "-"
+                + sf.getEnd();
+        if (sf.getFeatureGroup() != null)
         {
-          item += " (" + features[i].getFeatureGroup() + ")";
+          item += " (" + sf.getFeatureGroup() + ")";
         }
-
         overlaps.addItem(item);
       }
 
@@ -220,15 +252,16 @@ public class FeatureRenderer extends
           if (index != -1)
           {
             featureIndex = index;
-            name.setText(features[index].getType());
-            description.setText(features[index].getDescription());
-            source.setText(features[index].getFeatureGroup());
-            start.setText(features[index].getBegin() + "");
-            end.setText(features[index].getEnd() + "");
+            SequenceFeature sf = features.get(index);
+            name.setText(sf.getType());
+            description.setText(sf.getDescription());
+            group.setText(sf.getFeatureGroup());
+            start.setText(sf.getBegin() + "");
+            end.setText(sf.getEnd() + "");
 
             SearchResultsI highlight = new SearchResults();
-            highlight.addResult(sequences[0], features[index].getBegin(),
-                    features[index].getEnd());
+            highlight.addResult(sequences.get(0), sf.getBegin(),
+                    sf.getEnd());
 
             ap.seqPanel.seqCanvas.highlightSearchResults(highlight);
 
@@ -236,8 +269,8 @@ public class FeatureRenderer extends
           FeatureColourI col = getFeatureStyle(name.getText());
           if (col == null)
           {
-            Color generatedColour = UserColourScheme
-                    .createColourFromName(name.getText());
+            Color generatedColour = ColorUtils.createColourFromName(name
+                    .getText());
             col = new FeatureColour(generatedColour);
           }
 
@@ -258,7 +291,7 @@ public class FeatureRenderer extends
     tmp = new Panel();
     panel.add(tmp);
     tmp.add(new Label(MessageManager.getString("label.group:"), Label.RIGHT));
-    tmp.add(source);
+    tmp.add(group);
 
     tmp = new Panel();
     panel.add(tmp);
@@ -272,7 +305,7 @@ public class FeatureRenderer extends
             Label.RIGHT));
     panel.add(new ScrollPane().add(description));
 
-    if (!newFeatures)
+    if (!create)
     {
       bigPanel.add(panel, BorderLayout.SOUTH);
 
@@ -290,46 +323,32 @@ public class FeatureRenderer extends
       bigPanel.add(panel, BorderLayout.CENTER);
     }
 
-    if (lastFeatureAdded == null)
-    {
-      if (features[0].type != null)
-      {
-        lastFeatureAdded = features[0].type;
-      }
-      else
-      {
-        lastFeatureAdded = "feature_1";
-      }
-    }
-
-    if (lastFeatureGroupAdded == null)
-    {
-      if (features[0].featureGroup != null)
-      {
-        lastFeatureGroupAdded = features[0].featureGroup;
-      }
-      else
-      {
-        lastFeatureAdded = "Jalview";
-      }
-    }
-
-    String title = newFeatures ? MessageManager
+    /*
+     * use defaults for type and group (and update them on Confirm) only
+     * if feature type has not been supplied by the caller
+     * (e.g. for Amend, or create features from Find) 
+     */
+    SequenceFeature firstFeature = features.get(0);
+    boolean useLastDefaults = firstFeature.getType() == null;
+    String featureType = useLastDefaults ? lastFeatureAdded : firstFeature
+            .getType();
+    String featureGroup = useLastDefaults ? lastFeatureGroupAdded
+            : firstFeature.getFeatureGroup();
+
+    String title = create ? MessageManager
             .getString("label.create_new_sequence_features")
             : MessageManager.formatMessage("label.amend_delete_features",
-                    new String[] { sequences[0].getName() });
+                    new String[] { sequences.get(0).getName() });
 
     final JVDialog dialog = new JVDialog(ap.alignFrame, title, true, 385,
             240);
 
     dialog.setMainPanel(bigPanel);
 
-    if (newFeatures)
-    {
-      name.setText(lastFeatureAdded);
-      source.setText(lastFeatureGroupAdded);
-    }
-    else
+    name.setText(featureType);
+    group.setText(featureGroup);
+
+    if (!create)
     {
       dialog.ok.setLabel(MessageManager.getString("label.amend"));
       dialog.buttonPanel.add(deleteButton, 1);
@@ -342,13 +361,11 @@ public class FeatureRenderer extends
           dialog.setVisible(false);
         }
       });
-      name.setText(features[0].getType());
-      source.setText(features[0].getFeatureGroup());
     }
 
-    start.setText(features[0].getBegin() + "");
-    end.setText(features[0].getEnd() + "");
-    description.setText(features[0].getDescription());
+    start.setText(firstFeature.getBegin() + "");
+    end.setText(firstFeature.getEnd() + "");
+    description.setText(firstFeature.getDescription());
     // lookup (or generate) the feature colour
     FeatureColourI fcol = getFeatureStyle(name.getText());
     // simply display the feature color in a box
@@ -375,77 +392,88 @@ public class FeatureRenderer extends
 
     FeaturesFile ffile = new FeaturesFile();
 
-    if (dialog.accept)
-    {
-      // This ensures that the last sequence
-      // is refreshed and new features are rendered
-      lastSeq = null;
-      lastFeatureAdded = name.getText().trim();
-      lastFeatureGroupAdded = source.getText().trim();
-      lastDescriptionAdded = description.getText().replace('\n', ' ');
-    }
+    /*
+     * only update default type and group if we used defaults
+     */
+    final String enteredType = name.getText().trim();
+    final String enteredGroup = group.getText().trim();
+    final String enteredDesc = description.getText().replace('\n', ' ');
 
-    if (lastFeatureGroupAdded != null && lastFeatureGroupAdded.length() < 1)
+    if (dialog.accept && useLastDefaults)
     {
-      lastFeatureGroupAdded = null;
+      lastFeatureAdded = enteredType;
+      lastFeatureGroupAdded = enteredGroup;
     }
 
-    if (!newFeatures)
+    if (!create)
     {
-
-      SequenceFeature sf = features[featureIndex];
+      SequenceFeature sf = features.get(featureIndex);
       if (dialog.accept)
       {
-        sf.type = lastFeatureAdded;
-        sf.featureGroup = lastFeatureGroupAdded;
-        sf.description = lastDescriptionAdded;
         if (!colourPanel.isGcol)
         {
           // update colour - otherwise its already done.
           setColour(sf.type, 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)
         {
+          // 
         }
 
+        /*
+         * 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, 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(sf.type) || !featureGroup
+                .equals(sf.featureGroup));
+
         ffile.parseDescriptionHTML(sf, false);
-        setVisible(lastFeatureAdded); // if user edited name then make sure new
-                                      // type is visible
+        if (typeOrGroupChanged)
+        {
+          featuresAdded();
+        }
       }
       if (deleteFeature)
       {
-        sequences[0].deleteFeature(sf);
+        sequences.get(0).deleteFeature(sf);
+        // ensure Feature Settings reflects removal of feature / group
+        featuresAdded();
       }
-
     }
     else
     {
+      /*
+       * adding feature(s)
+       */
       if (dialog.accept && name.getText().length() > 0)
       {
-        for (int i = 0; i < sequences.length; i++)
+        for (int i = 0; i < sequences.size(); i++)
         {
-          features[i].type = lastFeatureAdded;
-          features[i].featureGroup = lastFeatureGroupAdded;
-          features[i].description = lastDescriptionAdded;
-          sequences[i].addSequenceFeature(features[i]);
-          ffile.parseDescriptionHTML(features[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();
         // setColour(lastFeatureAdded, fcol);
 
-        if (lastFeatureGroupAdded != null)
-        {
-          setGroupVisibility(lastFeatureGroupAdded, true);
-        }
-        setColour(lastFeatureAdded, new FeatureColour(newColour)); // was fcol
-        setVisible(lastFeatureAdded);
-        findAllFeatures(false); // different to original applet behaviour ?
-        // findAllFeatures();
+        setColour(enteredType, new FeatureColour(newColour)); // was fcol
+        featuresAdded();
       }
       else
       {
@@ -464,4 +492,43 @@ public class FeatureRenderer extends
 
     return true;
   }
+
+  protected void warnIfGroupHidden(Frame frame, String group)
+  {
+    if (featureGroups.containsKey(group) && !featureGroups.get(group))
+    {
+      String msg = MessageManager.formatMessage("label.warning_hidden",
+              MessageManager.getString("label.group"), group);
+      showWarning(frame, msg);
+    }
+  }
+
+  protected void warnIfTypeHidden(Frame frame, String type)
+  {
+    if (getRenderOrder().contains(type))
+    {
+      if (!showFeatureOfType(type))
+      {
+        String msg = MessageManager.formatMessage("label.warning_hidden",
+                MessageManager.getString("label.feature_type"), type);
+        showWarning(frame, msg);
+      }
+    }
+  }
+
+  /**
+   * @param frame
+   * @param msg
+   */
+  protected void showWarning(Frame frame, String msg)
+  {
+    JVDialog d = new JVDialog(frame, "", true, 350, 200);
+    Panel mp = new Panel();
+    d.ok.setLabel(MessageManager.getString("action.ok"));
+    d.cancel.setVisible(false);
+    mp.setLayout(new FlowLayout());
+    mp.add(new Label(msg));
+    d.setMainPanel(mp);
+    d.setVisible(true);
+  }
 }
index 2c454a4..b0bb372 100755 (executable)
@@ -23,7 +23,7 @@ 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 java.awt.BorderLayout;
@@ -56,11 +56,12 @@ 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.Vector;
+import java.util.Set;
 
 public class FeatureSettings extends Panel implements ItemListener,
         MouseListener, MouseMotionListener, ActionListener,
@@ -106,6 +107,7 @@ public class FeatureSettings extends Panel implements ItemListener,
     {
       fr.findAllFeatures(true); // was default - now true to make all visible
     }
+    groupPanel = new Panel();
 
     discoverAllFeatureData();
 
@@ -133,17 +135,15 @@ public class FeatureSettings extends Panel implements ItemListener,
 
     add(lowerPanel, BorderLayout.SOUTH);
 
-    if (groupPanel != null)
-    {
-      groupPanel.setLayout(new GridLayout(
-              (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was
-                                                        // scaled on number of
-                                                        // visible groups. seems
-                                                        // broken
-      groupPanel.validate();
+    groupPanel.setLayout(new GridLayout(
+            (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was
+                                                      // scaled on number of
+                                                      // visible groups. seems
+                                                      // broken
+    groupPanel.validate();
+
+    add(groupPanel, BorderLayout.NORTH);
 
-      add(groupPanel, BorderLayout.NORTH);
-    }
     frame = new Frame();
     frame.add(this);
     final FeatureSettings me = this;
@@ -326,79 +326,86 @@ public class FeatureSettings extends Panel implements ItemListener,
     if (fr.getAllFeatureColours() != null
             && fr.getAllFeatureColours().size() > 0)
     {
-      rebuildGroups();
+      // rebuildGroups();
 
     }
     resetTable(false);
   }
 
   /**
-   * rebuilds the group panel
+   * Answers the visibility of the given group, and adds a checkbox for it if
+   * there is not one already
    */
-  public void rebuildGroups()
+  public boolean checkGroupState(String group)
   {
-    boolean rdrw = false;
-    if (groupPanel == null)
-    {
-      groupPanel = new Panel();
-    }
-    else
-    {
-      rdrw = true;
-      groupPanel.removeAll();
-    }
-    // TODO: JAL-964 - smoothly incorporate new group entries if panel already
-    // displayed and new groups present
-    for (String group : fr.getFeatureGroups())
-    {
-      boolean vis = fr.checkGroupVisibility(group, false);
-      Checkbox check = new MyCheckbox(group, vis, false);
-      check.addMouseListener(this);
-      check.setFont(new Font("Serif", Font.BOLD, 12));
-      check.addItemListener(groupItemListener);
-      // note - visibility seems to correlate with displayed. ???wtf ??
-      check.setVisible(vis);
-      groupPanel.add(check);
-    }
-    if (rdrw)
+    boolean visible = fr.checkGroupVisibility(group, true);
+
+    /*
+     * is there already a checkbox for this group?
+     */
+    for (int g = 0; g < groupPanel.getComponentCount(); g++)
     {
-      groupPanel.validate();
+      if (((Checkbox) groupPanel.getComponent(g)).getLabel().equals(group))
+      {
+        ((Checkbox) groupPanel.getComponent(g)).setState(visible);
+        return visible;
+      }
     }
+
+    /*
+     * add a new checkbox
+     */
+    Checkbox check = new MyCheckbox(group, visible, false);
+    check.addMouseListener(this);
+    check.setFont(new Font("Serif", Font.BOLD, 12));
+    check.addItemListener(groupItemListener);
+    groupPanel.add(check);
+
+    groupPanel.validate();
+    return visible;
   }
 
   // This routine adds and removes checkboxes depending on
   // Group selection states
   void resetTable(boolean groupsChanged)
   {
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector<String> visibleChecks = new Vector<String>();
+    List<String> displayableTypes = new ArrayList<String>();
+    Set<String> foundGroups = new HashSet<String>();
+
     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<String>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-
         if (group == null || fr.checkGroupVisibility(group, true))
         {
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
+          visibleGroups.add(group);
         }
-        index++;
       }
+
+      /*
+       * 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);
     }
 
+    /*
+     * remove any checkboxes for groups not present
+     */
+    pruneGroups(foundGroups);
+
     Component[] comps;
     int cSize = featurePanel.getComponentCount();
     MyCheckbox check;
@@ -408,7 +415,7 @@ public class FeatureSettings extends Panel implements ItemListener,
     {
       comps = featurePanel.getComponents();
       check = (MyCheckbox) comps[i];
-      if (!visibleChecks.contains(check.type))
+      if (!displayableTypes.contains(check.type))
       {
         featurePanel.remove(i);
         cSize--;
@@ -425,24 +432,24 @@ public class FeatureSettings extends Panel implements ItemListener,
       {
         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(new GridLayout(featurePanel.getComponentCount(),
@@ -458,6 +465,25 @@ public class FeatureSettings extends Panel implements ItemListener,
   }
 
   /**
+   * 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.
+   * 
+   * @param foundGroups
+   */
+  protected void pruneGroups(Set<String> foundGroups)
+  {
+    for (int g = 0; g < groupPanel.getComponentCount(); g++)
+    {
+      Checkbox checkbox = (Checkbox) groupPanel.getComponent(g);
+      if (!foundGroups.contains(checkbox.getLabel()))
+      {
+        groupPanel.remove(checkbox);
+      }
+    }
+  }
+
+  /**
    * update the checklist of feature types with the given type
    * 
    * @param groupsChanged
@@ -696,8 +722,7 @@ public class FeatureSettings extends Panel implements ItemListener,
   public void adjustmentValueChanged(AdjustmentEvent evt)
   {
     fr.setTransparency((100 - transparency.getValue()) / 100f);
-    ap.seqPanel.seqCanvas.repaint();
-
+    ap.paintAlignment(true);
   }
 
   class MyCheckbox extends Checkbox
index d2fe69c..41e2a64 100644 (file)
@@ -41,6 +41,8 @@ import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
 import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Vector;
 
 public class Finder extends Panel implements ActionListener
@@ -113,19 +115,15 @@ public class Finder extends Panel implements ActionListener
 
   public void createNewGroup_actionPerformed()
   {
-    SequenceI[] seqs = new SequenceI[searchResults.getSize()];
-    SequenceFeature[] features = new SequenceFeature[searchResults
-            .getSize()];
+    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+    String searchString = textfield.getText().trim();
 
-    int i = 0;
     for (SearchResultMatchI match : searchResults.getResults())
     {
-      seqs[i] = match.getSequence().getDatasetSequence();
-
-      features[i] = new SequenceFeature(textfield.getText().trim(),
-              "Search Results", null, match.getStart(), match.getEnd(),
-              "Search Results");
-      i++;
+      seqs.add(match.getSequence().getDatasetSequence());
+      features.add(new SequenceFeature(searchString, "Search Results",
+              match.getStart(), match.getEnd(), "Search Results"));
     }
 
     if (ap.seqPanel.seqCanvas.getFeatureRenderer().amendFeatures(seqs,
index 59f6957..991fb96 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.appletgui;
 
-import jalview.api.ViewStyleI;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -59,6 +58,8 @@ public class FontChooser extends Panel implements ItemListener
 
   private Checkbox scaleAsCdna = new Checkbox();
 
+  private Checkbox fontAsCdna = new Checkbox();
+
   private Button ok = new Button();
 
   private Button cancel = new Button();
@@ -69,10 +70,20 @@ public class FontChooser extends Panel implements ItemListener
 
   private Font oldFont;
 
+  private Font oldComplementFont;
+
   private int oldCharWidth = 0;
 
+  /*
+   * the state of 'scale protein to cDNA' on opening the dialog
+   */
   private boolean oldScaleProtein = false;
 
+  /*
+   * the state of 'same font for protein and cDNA' on opening the dialog
+   */
+  boolean oldMirrorFont;
+
   private Font lastSelected = null;
 
   private int lastSelStyle = 0;
@@ -83,6 +94,8 @@ public class FontChooser extends Panel implements ItemListener
 
   private Frame frame;
 
+  boolean inSplitFrame = false;
+
   /**
    * Constructor for a TreePanel font chooser
    * 
@@ -112,8 +125,9 @@ public class FontChooser extends Panel implements ItemListener
   {
     this.ap = ap;
     oldFont = ap.av.getFont();
-    oldCharWidth = ap.av.getViewStyle().getCharWidth();
-    oldScaleProtein = ap.av.getViewStyle().isScaleProteinAsCdna();
+    oldCharWidth = ap.av.getCharWidth();
+    oldScaleProtein = ap.av.isScaleProteinAsCdna();
+    oldMirrorFont = ap.av.isProteinFontAsCdna();
 
     try
     {
@@ -152,7 +166,7 @@ public class FontChooser extends Panel implements ItemListener
     this.frame = new Frame();
     frame.add(this);
     jalview.bin.JalviewLite.addFrame(frame,
-            MessageManager.getString("action.change_font"), 440, 115);
+            MessageManager.getString("action.change_font"), 440, 145);
 
     init = false;
   }
@@ -160,6 +174,7 @@ public class FontChooser extends Panel implements ItemListener
   /**
    * Actions on change of font name, size or style.
    */
+  @Override
   public void itemStateChanged(ItemEvent evt)
   {
     final Object source = evt.getSource();
@@ -179,6 +194,29 @@ public class FontChooser extends Panel implements ItemListener
     {
       scaleAsCdna_actionPerformed();
     }
+    else if (source == fontAsCdna)
+    {
+      mirrorFont_actionPerformed();
+    }
+  }
+
+  /**
+   * Action on checking or unchecking 'use same font across split screen'
+   * option. When checked, the font settings are copied to the other half of the
+   * split screen. When unchecked, the other half is restored to its initial
+   * settings.
+   */
+  protected void mirrorFont_actionPerformed()
+  {
+    boolean selected = fontAsCdna.getState();
+    ap.av.setProteinFontAsCdna(selected);
+    ap.av.getCodingComplement().setProteinFontAsCdna(selected);
+
+    if (!selected)
+    {
+      ap.av.getCodingComplement().setFont(oldComplementFont, true);
+    }
+    changeFont();
   }
 
   /**
@@ -205,18 +243,23 @@ public class FontChooser extends Panel implements ItemListener
     if (ap != null)
     {
       ap.av.setScaleProteinAsCdna(oldScaleProtein);
+      ap.av.setProteinFontAsCdna(oldMirrorFont);
+
       if (ap.av.getCodingComplement() != null)
       {
         ap.av.getCodingComplement().setScaleProteinAsCdna(oldScaleProtein);
-        ap.alignFrame.getSplitFrame().repaint();
+        ap.av.getCodingComplement().setProteinFontAsCdna(oldMirrorFont);
+        ap.av.getCodingComplement().setFont(oldComplementFont, true);
+        SplitFrame splitFrame = ap.alignFrame.getSplitFrame();
+        splitFrame.adjustLayout();
+        splitFrame.getComplement(ap.alignFrame).alignPanel.fontChanged();
+        splitFrame.repaint();
       }
 
-      ap.av.setFont(oldFont);
-      ViewStyleI style = ap.av.getViewStyle();
-      if (style.getCharWidth() != oldCharWidth)
+      ap.av.setFont(oldFont, true);
+      if (ap.av.getCharWidth() != oldCharWidth)
       {
-        style.setCharWidth(oldCharWidth);
-        ap.av.setViewStyle(style);
+        ap.av.setCharWidth(oldCharWidth);
       }
       ap.paintAlignment(true);
     }
@@ -276,8 +319,23 @@ public class FontChooser extends Panel implements ItemListener
     }
     else if (ap != null)
     {
-      ap.av.setFont(newFont);
+      ap.av.setFont(newFont, true);
       ap.fontChanged();
+
+      /*
+       * and change font in other half of split frame if any
+       */
+      if (inSplitFrame)
+      {
+        if (fontAsCdna.getState())
+        {
+          ap.av.getCodingComplement().setFont(newFont, true);
+        }
+        SplitFrame splitFrame = ap.alignFrame.getSplitFrame();
+        splitFrame.adjustLayout();
+        splitFrame.getComplement(ap.alignFrame).alignPanel.fontChanged();
+        splitFrame.repaint();
+      }
     }
     // remember last selected
     lastSelected = newFont;
@@ -344,6 +402,11 @@ public class FontChooser extends Panel implements ItemListener
     scaleAsCdna.addItemListener(this);
     scaleAsCdna.setState(ap.av.isScaleProteinAsCdna());
 
+    fontAsCdna.setLabel(MessageManager.getString("label.font_as_cdna"));
+    fontAsCdna.setFont(VERDANA_11PT);
+    fontAsCdna.addItemListener(this);
+    fontAsCdna.setState(ap.av.isProteinFontAsCdna());
+
     ok.setFont(VERDANA_11PT);
     ok.setLabel(MessageManager.getString("action.ok"));
     ok.addActionListener(new ActionListener()
@@ -388,7 +451,8 @@ public class FontChooser extends Panel implements ItemListener
     stylePanel.add(fontStyle, BorderLayout.CENTER);
     sizePanel.add(sizeLabel, BorderLayout.WEST);
     sizePanel.add(fontSize, BorderLayout.CENTER);
-    scalePanel.add(scaleAsCdna, BorderLayout.CENTER);
+    scalePanel.add(scaleAsCdna, BorderLayout.NORTH);
+    scalePanel.add(fontAsCdna, BorderLayout.SOUTH);
     okCancelPanel.add(ok, null);
     okCancelPanel.add(cancel, null);
 
@@ -402,6 +466,9 @@ public class FontChooser extends Panel implements ItemListener
     this.add(optionsPanel, BorderLayout.NORTH);
     if (ap.alignFrame.getSplitFrame() != null)
     {
+      inSplitFrame = true;
+      oldComplementFont = ((AlignViewport) ap.av.getCodingComplement())
+              .getFont();
       this.add(scalePanel, BorderLayout.CENTER);
     }
     this.add(okCancelPanel, BorderLayout.SOUTH);
@@ -416,9 +483,7 @@ public class FontChooser extends Panel implements ItemListener
     ap.av.setScaleProteinAsCdna(scaleAsCdna.getState());
     ap.av.getCodingComplement().setScaleProteinAsCdna(
             scaleAsCdna.getState());
-    ap.alignFrame.getSplitFrame().adjustLayout();
-    ap.paintAlignment(true);
-    ap.alignFrame.getSplitFrame().repaint();
+    changeFont();
   }
 
 }
index d72e91f..74bbcf5 100755 (executable)
 package jalview.appletgui;
 
 import jalview.datamodel.SequenceI;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.Color;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.Image;
 import java.awt.Panel;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
-public class IdCanvas extends Panel
+public class IdCanvas extends Panel implements ViewportListenerI
 {
   protected AlignViewport av;
 
@@ -54,6 +57,7 @@ public class IdCanvas extends Panel
     setLayout(null);
     this.av = av;
     PaintRefresher.Register(this, av.getSequenceSetId());
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   public void drawIdString(Graphics gg, boolean hiddenRows, SequenceI s,
@@ -103,28 +107,32 @@ public class IdCanvas extends Panel
       return;
     }
 
+    ViewportRanges ranges = av.getRanges();
+
     gg.copyArea(0, 0, getSize().width, imgHeight, 0,
             -vertical * av.getCharHeight());
 
-    int ss = av.startSeq, es = av.endSeq, transY = 0;
+    int ss = ranges.getStartSeq(), es = ranges.getEndSeq(), transY = 0;
     if (vertical > 0) // scroll down
     {
       ss = es - vertical;
-      if (ss < av.startSeq) // ie scrolling too fast, more than a page at a time
+      if (ss < ranges.getStartSeq()) // ie scrolling too fast, more than a page
+                                     // at a
+                                 // time
       {
-        ss = av.startSeq;
+        ss = ranges.getStartSeq();
       }
       else
       {
-        transY = imgHeight - vertical * av.getCharHeight();
+        transY = imgHeight - ((vertical + 1) * av.getCharHeight());
       }
     }
     else if (vertical < 0)
     {
       es = ss - vertical;
-      if (es > av.endSeq)
+      if (es > ranges.getEndSeq())
       {
-        es = av.endSeq;
+        es = ranges.getEndSeq();
       }
     }
 
@@ -180,7 +188,7 @@ public class IdCanvas extends Panel
     gg.setFont(italic);
 
     gg.fillRect(0, 0, getSize().width, getSize().height);
-    drawIds(av.startSeq, av.endSeq);
+    drawIds(av.getRanges().getStartSeq(), av.getRanges().getEndSeq());
     g.drawImage(image, 0, 0, this);
   }
 
@@ -213,7 +221,8 @@ public class IdCanvas extends Panel
 
       if (av.hasHiddenColumns())
       {
-        maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+        maxwidth = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(maxwidth) - 1;
       }
 
       int annotationHeight = 0;
@@ -233,9 +242,10 @@ public class IdCanvas extends Panel
 
       int cHeight = alheight * avcharHeight + hgap + annotationHeight;
 
-      int rowSize = av.getEndRes() - av.getStartRes();
+      int rowSize = av.getRanges().getEndRes()
+              - av.getRanges().getStartRes();
       // Draw the rest of the panels
-      for (int ypos = hgap, row = av.startRes; (ypos <= getSize().height)
+      for (int ypos = hgap, row = av.getRanges().getStartRes(); (ypos <= getSize().height)
               && (row < maxwidth); ypos += cHeight, row += rowSize)
       {
         for (int i = starty; i < alheight; i++)
@@ -263,7 +273,7 @@ public class IdCanvas extends Panel
     {
       // Now draw the id strings
       SequenceI seq;
-      for (int i = starty; i < endy; i++)
+      for (int i = starty; i <= endy; i++)
       {
 
         seq = av.getAlignment().getSequenceAt(i);
@@ -387,4 +397,15 @@ public class IdCanvas extends Panel
     }
     return false;
   }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport range changes (e.g. alignment panel was scrolled)
+    if (evt.getPropertyName().equals("startseq")
+            || evt.getPropertyName().equals("endseq"))
+    {
+      fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
+    }
+  }
 }
index 182f20e..a5c2e5a 100755 (executable)
  */
 package jalview.appletgui;
 
-import static jalview.util.UrlConstants.EMBLEBI_STRING;
-import static jalview.util.UrlConstants.SRS_STRING;
-
-import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.util.UrlLink;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.applet.AppletUrlProviderFactory;
 import jalview.viewmodel.AlignmentViewport;
 
 import java.awt.BorderLayout;
@@ -36,8 +34,9 @@ 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.HashMap;
 import java.util.List;
-import java.util.Vector;
 
 public class IdPanel extends Panel implements MouseListener,
         MouseMotionListener
@@ -55,13 +54,13 @@ public class IdPanel extends Panel implements MouseListener,
 
   boolean mouseDragging = false;
 
-  java.util.Vector links = new java.util.Vector();
+  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);
@@ -69,33 +68,32 @@ public class IdPanel extends Panel implements MouseListener,
 
     String label, url;
     // TODO: add in group link parameter
-    if (av.applet != null)
+
+    // make a list of label,url pairs
+    HashMap<String, String> urlList = new HashMap<String, String>();
+    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);
 
-        if (label != null && url != null)
+        // only add non-null parameters
+        if (label != null)
         {
-          links.addElement(label + "|" + url);
+          urlList.put(label, url);
         }
-
       }
-    }
-    {
-      // upgrade old SRS link
-      int srsPos = links.indexOf(SRS_STRING);
-      if (srsPos > -1)
+
+      if (!urlList.isEmpty())
       {
-        links.setElementAt(EMBLEBI_STRING, srsPos);
+        // set default as first entry in list
+        String defaultUrl = viewport.applet.getParameter("linkLabel_1");
+        UrlProviderFactoryI factory = new AppletUrlProviderFactory(
+                defaultUrl, urlList);
+        urlProvider = factory.createUrlProvider();
       }
     }
-    if (links.size() < 1)
-    {
-      links = new java.util.Vector();
-      links.addElement(EMBLEBI_STRING);
-    }
   }
 
   Tooltip tooltip;
@@ -107,64 +105,57 @@ public class IdPanel extends Panel implements MouseListener,
 
     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
@@ -217,7 +208,7 @@ public class IdPanel extends Panel implements MouseListener,
       return;
     }
 
-    // DEFAULT LINK IS FIRST IN THE LINK LIST
+    // get the sequence details
     int seq = alignPanel.seqPanel.findSeq(e);
     SequenceI sq = av.getAlignment().getSequenceAt(seq);
     if (sq == null)
@@ -226,53 +217,15 @@ public class IdPanel extends Panel implements MouseListener,
     }
     String id = sq.getName();
 
-    String target = null;
-    String url = null;
-    int i = 0;
-    while (url == null && i < links.size())
+    // get the default url with the sequence details filled in
+    if (urlProvider == null)
     {
-      // DEFAULT LINK IS FIRST IN THE LINK LIST
-      // BUT IF ITS A REGEX AND DOES NOT MATCH THE NEXT ONE WILL BE TRIED
-      url = links.elementAt(i++).toString();
-      jalview.util.UrlLink urlLink = null;
-      try
-      {
-        urlLink = new UrlLink(url);
-        target = urlLink.getTarget();
-      } catch (Exception foo)
-      {
-        System.err.println("Exception for URLLink '" + url + "'");
-        foo.printStackTrace();
-        url = null;
-        continue;
-      }
-
-      if (urlLink.usesDBAccession())
-      {
-        // this URL requires an accession id, not the name of a sequence
-        url = null;
-        continue;
-      }
-
-      if (!urlLink.isValid())
-      {
-        System.err.println(urlLink.getInvalidMessage());
-        url = null;
-        continue;
-      }
-
-      String urls[] = urlLink.makeUrls(id, true);
-      if (urls == null || urls[0] == null || urls[0].length() < 1)
-      {
-        url = null;
-        continue;
-      }
-      // just take first URL made from regex
-      url = urls[1];
+      return;
     }
+    String url = urlProvider.getPrimaryUrl(id);
+    String target = urlProvider.getPrimaryTarget(id);
     try
     {
-
       alignPanel.alignFrame.showURL(url, target);
     } catch (Exception ex)
     {
@@ -297,13 +250,13 @@ public class IdPanel extends Panel implements MouseListener,
       return;
     }
 
-    if (mouseDragging && e.getY() < 0 && av.getStartSeq() > 0)
+    if (mouseDragging && e.getY() < 0 && av.getRanges().getStartSeq() > 0)
     {
       scrollThread = new ScrollThread(true);
     }
 
     if (mouseDragging && e.getY() >= getSize().height
-            && av.getAlignment().getHeight() > av.getEndSeq())
+            && av.getAlignment().getHeight() > av.getRanges().getEndSeq())
     {
       scrollThread = new ScrollThread(false);
     }
@@ -327,26 +280,29 @@ public class IdPanel extends Panel implements MouseListener,
 
     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
-      Vector nlinks = new Vector();
-      for (int l = 0, lSize = links.size(); l < lSize; l++)
+      /*
+       *  build a new links menu based on the current links
+       *  and any non-positional features
+       */
+      List<String> nlinks;
+      if (urlProvider != null)
       {
-        nlinks.addElement(links.elementAt(l));
+        nlinks = urlProvider.getLinksForMenu();
       }
-      SequenceFeature sf[] = sq == null ? null : sq.getSequenceFeatures();
-      for (int sl = 0; sf != null && sl < sf.length; sl++)
+      else
+      {
+        nlinks = new ArrayList<String>();
+      }
+
+      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.addElement(sf[sl].links.elementAt(l));
-            }
+            nlinks.add(link);
           }
         }
       }
@@ -445,9 +401,10 @@ public class IdPanel extends Panel implements MouseListener,
     int index = av.getAlignment().findIndex(list.get(0));
 
     // do we need to scroll the panel?
-    if (av.getStartSeq() > index || av.getEndSeq() < index)
+    if (av.getRanges().getStartSeq() > index
+            || av.getRanges().getEndSeq() < index)
     {
-      alignPanel.setScrollValues(av.getStartRes(), index);
+      av.getRanges().setStartSeq(index);
     }
   }
 
@@ -458,9 +415,9 @@ public class IdPanel extends Panel implements MouseListener,
 
     boolean up = true;
 
-    public ScrollThread(boolean up)
+    public ScrollThread(boolean isUp)
     {
-      this.up = up;
+      this.up = isUp;
       start();
     }
 
@@ -475,13 +432,13 @@ public class IdPanel extends Panel implements MouseListener,
       running = true;
       while (running)
       {
-        if (alignPanel.scrollUp(up))
+        if (av.getRanges().scrollUp(up))
         {
           // scroll was ok, so add new sequence to selection
-          int seq = av.getStartSeq();
+          int seq = av.getRanges().getStartSeq();
           if (!up)
           {
-            seq = av.getEndSeq();
+            seq = av.getRanges().getEndSeq();
           }
 
           if (seq < lastid)
diff --git a/src/jalview/appletgui/OverviewCanvas.java b/src/jalview/appletgui/OverviewCanvas.java
new file mode 100644 (file)
index 0000000..23e82df
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * 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.appletgui;
+
+import jalview.renderer.OverviewRenderer;
+import jalview.viewmodel.OverviewDimensions;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.Graphics;
+import java.awt.Image;
+
+public class OverviewCanvas extends Component
+{
+  // This is set true if the alignment view changes whilst
+  // the overview is being calculated
+  private volatile boolean restart = false;
+
+  private volatile boolean updaterunning = false;
+
+  private OverviewDimensions od;
+
+  private Image miniMe;
+
+  private Image offscreen;
+
+  private AlignViewport av;
+
+  // Can set different properties in this seqCanvas than
+  // main visible SeqCanvas
+  private SequenceRenderer sr;
+
+  private jalview.renderer.seqfeatures.FeatureRenderer fr;
+
+  private Frame nullFrame;
+
+  public OverviewCanvas(OverviewDimensions overviewDims,
+          AlignViewport alignvp)
+  {
+    od = overviewDims;
+    av = alignvp;
+
+    nullFrame = new Frame();
+    nullFrame.addNotify();
+
+    sr = new SequenceRenderer(av);
+    sr.graphics = nullFrame.getGraphics();
+    sr.renderGaps = false;
+    sr.forOverview = true;
+    fr = new jalview.renderer.seqfeatures.FeatureRenderer(av);
+  }
+
+  /**
+   * Update the overview dimensions object used by the canvas (e.g. if we change
+   * from showing hidden columns to hiding them or vice versa)
+   * 
+   * @param overviewDims
+   */
+  public void resetOviewDims(OverviewDimensions overviewDims)
+  {
+    od = overviewDims;
+  }
+
+  /**
+   * Signals to drawing code that the associated alignment viewport has changed
+   * and a redraw will be required
+   */
+  public boolean restartDraw()
+  {
+    synchronized (this)
+    {
+      if (updaterunning)
+      {
+        restart = true;
+      }
+      else
+      {
+        updaterunning = true;
+      }
+      return restart;
+    }
+  }
+
+  public void draw(boolean showSequenceFeatures, boolean showAnnotation,
+          FeatureRenderer transferRenderer)
+  {
+    miniMe = null;
+
+    if (showSequenceFeatures)
+    {
+      fr.transferSettings(transferRenderer);
+    }
+
+    setPreferredSize(new Dimension(od.getWidth(), od.getHeight()));
+
+    OverviewRenderer or = new OverviewRenderer(sr, fr, od);
+    miniMe = nullFrame.createImage(od.getWidth(), od.getHeight());
+    offscreen = nullFrame.createImage(od.getWidth(), od.getHeight());
+
+    miniMe = or.draw(od.getRows(av.getAlignment()),
+            od.getColumns(av.getAlignment()));
+
+    Graphics mg = miniMe.getGraphics();
+
+    // checks for conservation annotation to make sure overview works for DNA
+    // too
+    if (showAnnotation)
+    {
+      mg.translate(0, od.getSequencesHeight());
+      or.drawGraph(mg, av.getAlignmentConservationAnnotation(),
+              av.getCharWidth(), od.getGraphHeight(),
+              od.getColumns(av.getAlignment()));
+      mg.translate(0, -od.getSequencesHeight());
+    }
+    System.gc();
+
+    if (restart)
+    {
+      restart = false;
+      draw(showSequenceFeatures, showAnnotation, transferRenderer);
+    }
+    else
+    {
+      updaterunning = false;
+    }
+  }
+
+  @Override
+  public void update(Graphics g)
+  {
+    paint(g);
+  }
+
+  @Override
+  public void paint(Graphics g)
+  {
+    Graphics og = offscreen.getGraphics();
+    if (miniMe != null)
+    {
+      og.drawImage(miniMe, 0, 0, this);
+      og.setColor(Color.red);
+      od.drawBox(og);
+      g.drawImage(offscreen, 0, 0, this);
+    }
+  }
+
+}
index 9b2be4c..b3c4a37 100755 (executable)
  */
 package jalview.appletgui;
 
-import jalview.datamodel.AlignmentI;
-
-import java.awt.Color;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.viewmodel.OverviewDimensions;
+import jalview.viewmodel.OverviewDimensionsHideHidden;
+import jalview.viewmodel.OverviewDimensionsShowHidden;
+import jalview.viewmodel.ViewportListenerI;
+
+import java.awt.BorderLayout;
+import java.awt.CheckboxMenuItem;
 import java.awt.Dimension;
-import java.awt.Frame;
-import java.awt.Graphics;
-import java.awt.Image;
 import java.awt.Panel;
+import java.awt.PopupMenu;
 import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
+import java.beans.PropertyChangeEvent;
 
 public class OverviewPanel extends Panel implements Runnable,
-        MouseMotionListener, MouseListener
+        MouseMotionListener, MouseListener, ViewportListenerI
 {
-  Image miniMe;
-
-  Image offscreen;
-
-  AlignViewport av;
-
-  AlignmentPanel ap;
-
-  float scalew = 1f;
+  private OverviewDimensions od;
 
-  float scaleh = 1f;
+  private OverviewCanvas oviewCanvas;
 
-  public int width, sequencesHeight;
+  private AlignViewport av;
 
-  int graphHeight = 20;
+  private AlignmentPanel ap;
 
-  int boxX = -1, boxY = -1, boxWidth = -1, boxHeight = -1;
+  private boolean showHidden = true;
 
-  boolean resizing = false;
+  private boolean updateRunning = false;
 
-  // Can set different properties in this seqCanvas than
-  // main visible SeqCanvas
-  SequenceRenderer sr;
-
-  FeatureRenderer fr;
-
-  Frame nullFrame;
-
-  public OverviewPanel(AlignmentPanel ap)
+  public OverviewPanel(AlignmentPanel alPanel)
   {
-    this.av = ap.av;
-    this.ap = ap;
+    this.av = alPanel.av;
+    this.ap = alPanel;
     setLayout(null);
-    nullFrame = new Frame();
-    nullFrame.addNotify();
 
-    sr = new SequenceRenderer(av);
-    sr.graphics = nullFrame.getGraphics();
-    sr.renderGaps = false;
-    sr.forOverview = true;
-    fr = new FeatureRenderer(av);
+    od = new OverviewDimensionsShowHidden(av.getRanges(),
+            (av.isShowAnnotation() && av.getSequenceConsensusHash() != null));
 
-    // scale the initial size of overviewpanel to shape of alignment
-    float initialScale = (float) av.getAlignment().getWidth()
-            / (float) av.getAlignment().getHeight();
+    oviewCanvas = new OverviewCanvas(od, av);
+    setLayout(new BorderLayout());
+    add(oviewCanvas, BorderLayout.CENTER);
 
-    if (av.getSequenceConsensusHash() == null)
-    {
-      graphHeight = 0;
-    }
+    setSize(new Dimension(od.getWidth(), od.getHeight()));
 
-    if (av.getAlignment().getWidth() > av.getAlignment().getHeight())
-    {
-      // wider
-      width = 400;
-      sequencesHeight = (int) (400f / initialScale);
-      if (sequencesHeight < 40)
-      {
-        sequencesHeight = 40;
-      }
-    }
-    else
-    {
-      // taller
-      width = (int) (400f * initialScale);
-      sequencesHeight = 300;
-      if (width < 120)
-      {
-        width = 120;
-      }
-    }
+    av.getRanges().addPropertyChangeListener(this);
 
-    setSize(new Dimension(width, sequencesHeight + graphHeight));
     addComponentListener(new ComponentAdapter()
     {
 
       @Override
       public void componentResized(ComponentEvent evt)
       {
-        if (getSize().width != width
-                || getSize().height != sequencesHeight + graphHeight)
+        if ((getWidth() != od.getWidth())
+                || (getHeight() != (od.getHeight())))
         {
           updateOverviewImage();
         }
@@ -145,6 +109,10 @@ public class OverviewPanel extends Panel implements Runnable,
   @Override
   public void mouseClicked(MouseEvent evt)
   {
+    if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
+    {
+      showPopupMenu(evt);
+    }
   }
 
   @Override
@@ -155,337 +123,138 @@ public class OverviewPanel extends Panel implements Runnable,
   @Override
   public void mousePressed(MouseEvent evt)
   {
-    boxX = evt.getX();
-    boxY = evt.getY();
-    checkValid();
+    mouseAction(evt);
   }
 
   @Override
   public void mouseReleased(MouseEvent evt)
   {
-    boxX = evt.getX();
-    boxY = evt.getY();
-    checkValid();
+    mouseAction(evt);
   }
 
   @Override
   public void mouseDragged(MouseEvent evt)
   {
-    boxX = evt.getX();
-    boxY = evt.getY();
-    checkValid();
+    mouseAction(evt);
   }
 
-  void checkValid()
+  private void mouseAction(MouseEvent evt)
   {
-    if (boxY < 0)
-    {
-      boxY = 0;
-    }
-
-    if (boxY > (sequencesHeight - boxHeight))
+    if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      boxY = sequencesHeight - boxHeight + 1;
-    }
-
-    if (boxX < 0)
-    {
-      boxX = 0;
-    }
-
-    if (boxX > (width - boxWidth))
-    {
-      if (av.hasHiddenColumns())
+      if (!Platform.isAMac())
       {
-        // Try smallest possible box
-        boxWidth = (int) ((av.endRes - av.startRes + 1) * av.getCharWidth() * scalew);
+        showPopupMenu(evt);
       }
-      boxX = width - boxWidth;
-    }
-
-    int col = (int) (boxX / scalew / av.getCharWidth());
-    int row = (int) (boxY / scaleh / av.getCharHeight());
-
-    if (av.hasHiddenColumns())
-    {
-      if (!av.getColumnSelection().isVisible(col))
-      {
-        return;
-      }
-
-      col = av.getColumnSelection().findColumnPosition(col);
     }
-
-    if (av.hasHiddenRows())
+    else
     {
-      row = av.getAlignment().getHiddenSequences()
-              .findIndexWithoutHiddenSeqs(row);
+      od.updateViewportFromMouse(evt.getX(), evt.getY(), av.getAlignment()
+              .getHiddenSequences(), av.getAlignment().getHiddenColumns());
+      ap.paintAlignment(false);
     }
-
-    ap.setScrollValues(col, row);
-    ap.paintAlignment(false);
   }
 
   /**
-   * DOCUMENT ME!
+   * Updates the overview image when the related alignment panel is updated
    */
   public void updateOverviewImage()
   {
-    if (resizing)
+    if ((getSize().width > 0) && (getSize().height > 0))
     {
-      resizeAgain = true;
-      return;
+      od.setWidth(getSize().width);
+      od.setHeight(getSize().height);
     }
+    setSize(new Dimension(od.getWidth(), od.getHeight()));
 
-    if (av.isShowSequenceFeatures())
+    synchronized (this)
     {
-      fr.transferSettings(ap.seqPanel.seqCanvas.fr);
-    }
-
-    resizing = true;
+      if (updateRunning)
+      {
+        oviewCanvas.restartDraw();
+        return;
+      }
 
-    if ((getSize().width > 0) && (getSize().height > 0))
-    {
-      width = getSize().width;
-      sequencesHeight = getSize().height - graphHeight;
+      updateRunning = true;
     }
-    setSize(new Dimension(width, sequencesHeight + graphHeight));
-
     Thread thread = new Thread(this);
     thread.start();
     repaint();
+    updateRunning = false;
   }
 
-  // This is set true if the user resizes whilst
-  // the overview is being calculated
-  boolean resizeAgain = false;
-
   @Override
   public void run()
   {
-    miniMe = null;
-    int alwidth = av.getAlignment().getWidth();
-    int alheight = av.getAlignment().getHeight()
-            + av.getAlignment().getHiddenSequences().getSize();
-
-    if (av.isShowSequenceFeatures())
-    {
-      fr.transferSettings(ap.seqPanel.seqCanvas.getFeatureRenderer());
-    }
-
-    if (getSize().width > 0 && getSize().height > 0)
-    {
-      width = getSize().width;
-      sequencesHeight = getSize().height - graphHeight;
-    }
-
-    setSize(new Dimension(width, sequencesHeight + graphHeight));
-
-    int fullsizeWidth = alwidth * av.getCharWidth();
-    int fullsizeHeight = alheight * av.getCharHeight();
-
-    scalew = (float) width / (float) fullsizeWidth;
-    scaleh = (float) sequencesHeight / (float) fullsizeHeight;
-
-    miniMe = nullFrame.createImage(width, sequencesHeight + graphHeight);
-    offscreen = nullFrame.createImage(width, sequencesHeight + graphHeight);
-
-    Graphics mg = miniMe.getGraphics();
-    float sampleCol = (float) alwidth / (float) width;
-    float sampleRow = (float) alheight / (float) sequencesHeight;
-
-    int lastcol = 0, lastrow = 0;
-    int xstart = 0, ystart = 0;
-    Color color = Color.yellow;
-    int row, col, sameRow = 0, sameCol = 0;
-    jalview.datamodel.SequenceI seq;
-    final boolean hasHiddenRows = av.hasHiddenRows(), hasHiddenCols = av
-            .hasHiddenColumns();
-    boolean hiddenRow = false;
-    AlignmentI alignment = av.getAlignment();
-    for (row = 0; row <= sequencesHeight; row++)
-    {
-      if (resizeAgain)
-      {
-        break;
-      }
-      if ((int) (row * sampleRow) == lastrow)
-      {
-        sameRow++;
-        continue;
-      }
-
-      hiddenRow = false;
-      if (hasHiddenRows)
-      {
-        seq = alignment.getHiddenSequences().getHiddenSequence(lastrow);
-        if (seq == null)
-        {
-          int index = alignment.getHiddenSequences()
-                  .findIndexWithoutHiddenSeqs(lastrow);
-
-          seq = alignment.getSequenceAt(index);
-        }
-        else
-        {
-          hiddenRow = true;
-        }
-      }
-      else
-      {
-        seq = alignment.getSequenceAt(lastrow);
-      }
-
-      for (col = 0; col < width; col++)
-      {
-        if ((int) (col * sampleCol) == lastcol
-                && (int) (row * sampleRow) == lastrow)
-        {
-          sameCol++;
-          continue;
-        }
-
-        lastcol = (int) (col * sampleCol);
-
-        if (seq.getLength() > lastcol)
-        {
-          color = sr.getResidueBoxColour(seq, lastcol);
-
-          if (av.isShowSequenceFeatures())
-          {
-            color = fr.findFeatureColour(color, seq, lastcol);
-          }
-        }
-        else
-        {
-          color = Color.white; // White
-        }
-
-        if (hiddenRow
-                || (hasHiddenCols && !av.getColumnSelection().isVisible(
-                        lastcol)))
-        {
-          color = color.darker().darker();
-        }
-
-        mg.setColor(color);
-        if (sameCol == 1 && sameRow == 1)
-        {
-          mg.drawLine(xstart, ystart, xstart, ystart);
-        }
-        else
-        {
-          mg.fillRect(xstart, ystart, sameCol, sameRow);
-        }
-
-        xstart = col;
-        sameCol = 1;
-      }
-      lastrow = (int) (row * sampleRow);
-      ystart = row;
-      sameRow = 1;
-    }
-
-    if (av.getAlignmentConservationAnnotation() != null)
-    {
-      for (col = 0; col < width; col++)
-      {
-        if (resizeAgain)
-        {
-          break;
-        }
-        lastcol = (int) (col * sampleCol);
-        {
-          mg.translate(col, sequencesHeight);
-          ap.annotationPanel.renderer.drawGraph(mg,
-                  av.getAlignmentConservationAnnotation(),
-                  av.getAlignmentConservationAnnotation().annotations,
-                  (int) (sampleCol) + 1, graphHeight,
-                  (int) (col * sampleCol), (int) (col * sampleCol) + 1);
-          mg.translate(-col, -sequencesHeight);
-        }
-      }
-    }
-    System.gc();
-
-    resizing = false;
-
+    oviewCanvas.draw(av.isShowSequenceFeatures(),
+            (av.isShowAnnotation() && av
+                    .getAlignmentConservationAnnotation() != null),
+            ap.seqPanel.seqCanvas.getFeatureRenderer());
     setBoxPosition();
-
-    if (resizeAgain)
-    {
-      resizeAgain = false;
-      updateOverviewImage();
-    }
   }
 
-  public void setBoxPosition()
+  /**
+   * Update the overview panel box when the associated alignment panel is
+   * changed
+   * 
+   */
+  private void setBoxPosition()
   {
-    int fullsizeWidth = av.getAlignment().getWidth() * av.getCharWidth();
-    int fullsizeHeight = (av.getAlignment().getHeight() + av.getAlignment()
-            .getHiddenSequences().getSize())
-            * av.getCharHeight();
-
-    int startRes = av.getStartRes();
-    int endRes = av.getEndRes();
-
-    if (av.hasHiddenColumns())
-    {
-      startRes = av.getColumnSelection().adjustForHiddenColumns(startRes);
-      endRes = av.getColumnSelection().adjustForHiddenColumns(endRes);
-    }
-
-    int startSeq = av.startSeq;
-    int endSeq = av.endSeq;
-
-    if (av.hasHiddenRows())
-    {
-      startSeq = av.getAlignment().getHiddenSequences()
-              .adjustForHiddenSeqs(startSeq);
-
-      endSeq = av.getAlignment().getHiddenSequences()
-              .adjustForHiddenSeqs(endSeq);
-
-    }
-
-    scalew = (float) width / (float) fullsizeWidth;
-    scaleh = (float) sequencesHeight / (float) fullsizeHeight;
-
-    boxX = (int) (startRes * av.getCharWidth() * scalew);
-    boxY = (int) (startSeq * av.getCharHeight() * scaleh);
+    od.setBoxPosition(av.getAlignment()
+.getHiddenSequences(), av
+            .getAlignment().getHiddenColumns());
+    repaint();
+  }
 
-    if (av.hasHiddenColumns())
-    {
-      boxWidth = (int) ((endRes - startRes + 1) * av.getCharWidth() * scalew);
-    }
-    else
+  /*
+   * Displays the popup menu and acts on user input
+   */
+  private void showPopupMenu(MouseEvent e)
+  {
+    PopupMenu popup = new PopupMenu();
+    ItemListener menuListener = new ItemListener()
     {
-      boxWidth = (int) ((endRes - startRes + 1) * av.getCharWidth() * scalew);
-    }
-
-    boxHeight = (int) ((endSeq - startSeq) * av.getCharHeight() * scaleh);
-
-    repaint();
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        toggleHiddenColumns();
+      }
+    };
+    CheckboxMenuItem item = new CheckboxMenuItem(
+            MessageManager.getString("label.togglehidden"));
+    item.setState(showHidden);
+    popup.add(item);
+    item.addItemListener(menuListener);
+    this.add(popup);
+    popup.show(this, e.getX(), e.getY());
   }
 
   @Override
-  public void update(Graphics g)
+  public void propertyChange(PropertyChangeEvent evt)
   {
-    paint(g);
+    setBoxPosition();
   }
 
-  @Override
-  public void paint(Graphics g)
+  /*
+   * Toggle overview display between showing hidden columns and hiding hidden columns
+   */
+  private void toggleHiddenColumns()
   {
-    Graphics og = offscreen.getGraphics();
-    if (miniMe != null)
+    if (showHidden)
     {
-      og.drawImage(miniMe, 0, 0, this);
-      og.setColor(Color.red);
-      og.drawRect(boxX, boxY, boxWidth, boxHeight);
-      og.drawRect(boxX + 1, boxY + 1, boxWidth - 2, boxHeight - 2);
-      g.drawImage(offscreen, 0, 0, this);
+      showHidden = false;
+      od = new OverviewDimensionsHideHidden(av.getRanges(),
+              (av.isShowAnnotation() && av
+                      .getAlignmentConservationAnnotation() != null));
     }
+    else
+    {
+      showHidden = true;
+      od = new OverviewDimensionsShowHidden(av.getRanges(),
+              (av.isShowAnnotation() && av
+                      .getAlignmentConservationAnnotation() != null));
+    }
+    oviewCanvas.resetOviewDims(od);
+    updateOverviewImage();
   }
-
 }
index c5ec0c1..5dc57f9 100644 (file)
  */
 package jalview.appletgui;
 
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.analysis.scoremodels.SimilarityParams;
+import jalview.api.analysis.ScoreModelI;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentView;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SeqCigar;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
@@ -56,7 +59,7 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
 
   int top = 0;
 
-  public PCAPanel(AlignViewport av)
+  public PCAPanel(AlignViewport viewport)
   {
     try
     {
@@ -73,19 +76,19 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
       zCombobox.addItem("dim " + i);
     }
 
-    this.av = av;
-    boolean selected = av.getSelectionGroup() != null
-            && av.getSelectionGroup().getSize() > 0;
-    AlignmentView seqstrings = av.getAlignmentView(selected);
-    boolean nucleotide = av.getAlignment().isNucleotide();
+    this.av = viewport;
+    boolean selected = viewport.getSelectionGroup() != null
+            && viewport.getSelectionGroup().getSize() > 0;
+    AlignmentView seqstrings = viewport.getAlignmentView(selected);
+    boolean nucleotide = viewport.getAlignment().isNucleotide();
     SequenceI[] seqs;
     if (!selected)
     {
-      seqs = av.getAlignment().getSequencesArray();
+      seqs = viewport.getAlignment().getSequencesArray();
     }
     else
     {
-      seqs = av.getSelectionGroup().getSequencesInOrder(av.getAlignment());
+      seqs = viewport.getSelectionGroup().getSequencesInOrder(viewport.getAlignment());
     }
     SeqCigar sq[] = seqstrings.getSequences();
     int length = sq[0].getWidth();
@@ -99,9 +102,13 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
         return;
       }
     }
-    pcaModel = new PCAModel(seqstrings, seqs, nucleotide);
 
-    rc = new RotatableCanvas(av);
+    ScoreModelI scoreModel = ScoreModels.getInstance().getDefaultModel(
+            !nucleotide);
+    pcaModel = new PCAModel(seqstrings, seqs, nucleotide, scoreModel,
+            SimilarityParams.SeqSpace);
+
+    rc = new RotatableCanvas(viewport);
     embedMenuIfNeeded(rc);
     add(rc, BorderLayout.CENTER);
 
@@ -116,6 +123,7 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
   /**
    * DOCUMENT ME!
    */
+  @Override
   public void run()
   {
     // TODO progress indicator
@@ -164,6 +172,7 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
     rc.paint(rc.getGraphics());
   }
 
+  @Override
   public void actionPerformed(ActionEvent evt)
   {
     if (evt.getSource() == inputData)
@@ -183,6 +192,7 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
     }
   }
 
+  @Override
   public void itemStateChanged(ItemEvent evt)
   {
     if (evt.getSource() == xCombobox)
@@ -206,6 +216,9 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
       if (!pcaModel.isNucleotide())
       {
         pcaModel.setNucleotide(true);
+        ScoreModelI scoreModel = ScoreModels.getInstance().getDefaultModel(
+                false);
+        pcaModel.setScoreModel(scoreModel);
         new Thread(this).start();
       }
     }
@@ -214,6 +227,9 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
       if (pcaModel.isNucleotide())
       {
         pcaModel.setNucleotide(false);
+        ScoreModelI scoreModel = ScoreModels.getInstance().getDefaultModel(
+                true);
+        pcaModel.setScoreModel(scoreModel);
         new Thread(this).start();
       }
     }
@@ -265,7 +281,7 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
     }
     ;
     Object[] alAndColsel = pcaModel.getSeqtrings()
-            .getAlignmentAndColumnSelection(gc);
+            .getAlignmentAndHiddenColumns(gc);
 
     if (alAndColsel != null && alAndColsel[0] != null)
     {
@@ -273,7 +289,8 @@ public class PCAPanel extends EmbmenuFrame implements Runnable,
       AlignFrame af = new AlignFrame(al, av.applet,
               "Original Data for PCA", false);
 
-      af.viewport.setHiddenColumns((ColumnSelection) alAndColsel[1]);
+      af.viewport.getAlignment().setHiddenColumns(
+              (HiddenColumns) alAndColsel[1]);
     }
   }
 
index 4aea837..6be416c 100644 (file)
@@ -74,9 +74,10 @@ public class RedundancyPanel extends SliderPanel implements Runnable,
 
     slider.addAdjustmentListener(new AdjustmentListener()
     {
+      @Override
       public void adjustmentValueChanged(AdjustmentEvent evt)
       {
-        valueField.setText(slider.getValue() + "");
+        valueField.setText(String.valueOf(slider.getValue()));
         sliderValueChanged();
       }
     });
@@ -104,6 +105,7 @@ public class RedundancyPanel extends SliderPanel implements Runnable,
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public void run()
   {
     label.setText(MessageManager.getString("label.calculating"));
@@ -172,6 +174,7 @@ public class RedundancyPanel extends SliderPanel implements Runnable,
 
   }
 
+  @Override
   public void applyButton_actionPerformed()
   {
     Vector del = new Vector();
@@ -230,6 +233,7 @@ public class RedundancyPanel extends SliderPanel implements Runnable,
 
   }
 
+  @Override
   public void undoButton_actionPerformed()
   {
     CommandI command = (CommandI) historyList.pop();
@@ -263,31 +267,38 @@ public class RedundancyPanel extends SliderPanel implements Runnable,
     }
   }
 
+  @Override
   public void windowOpened(WindowEvent evt)
   {
   }
 
+  @Override
   public void windowClosing(WindowEvent evt)
   {
     ap.idPanel.idCanvas.setHighlighted(null);
   }
 
+  @Override
   public void windowClosed(WindowEvent evt)
   {
   }
 
+  @Override
   public void windowActivated(WindowEvent evt)
   {
   }
 
+  @Override
   public void windowDeactivated(WindowEvent evt)
   {
   }
 
+  @Override
   public void windowIconified(WindowEvent evt)
   {
   }
 
+  @Override
   public void windowDeiconified(WindowEvent evt)
   {
   }
index 5a156fa..ec3e246 100755 (executable)
 package jalview.appletgui;
 
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceGroup;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
 import jalview.util.MessageManager;
+import jalview.viewmodel.ViewportListenerI;
 
 import java.awt.Color;
 import java.awt.FontMetrics;
@@ -38,10 +40,11 @@ import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
 public class ScalePanel extends Panel implements MouseMotionListener,
-        MouseListener
+        MouseListener, ViewportListenerI
 {
 
   protected int offy = 4;
@@ -71,17 +74,18 @@ public class ScalePanel extends Panel implements MouseMotionListener,
     addMouseListener(this);
     addMouseMotionListener(this);
 
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   @Override
   public void mousePressed(MouseEvent evt)
   {
-    int x = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int x = (evt.getX() / av.getCharWidth()) + av.getRanges().getStartRes();
     final int res;
 
     if (av.hasHiddenColumns())
     {
-      res = av.getColumnSelection().adjustForHiddenColumns(x);
+      res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(x);
     }
     else
     {
@@ -170,7 +174,7 @@ public class ScalePanel extends Panel implements MouseMotionListener,
       });
       pop.add(item);
 
-      if (av.getColumnSelection().hasManyHiddenColumns())
+      if (av.getAlignment().getHiddenColumns().hasManyHiddenColumns())
       {
         item = new MenuItem(MessageManager.getString("action.reveal_all"));
         item.addActionListener(new ActionListener()
@@ -229,7 +233,8 @@ public class ScalePanel extends Panel implements MouseMotionListener,
   {
     mouseDragging = false;
 
-    int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int res = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
 
     if (res > av.getAlignment().getWidth())
     {
@@ -238,7 +243,8 @@ public class ScalePanel extends Panel implements MouseMotionListener,
 
     if (av.hasHiddenColumns())
     {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
+      res = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(res);
     }
 
     if (!stretchingGroup)
@@ -264,72 +270,31 @@ public class ScalePanel extends Panel implements MouseMotionListener,
     av.sendSelection();
   }
 
+  /**
+   * Action on dragging the mouse in the scale panel is to expand or shrink the
+   * selection group range (including any hidden columns that it spans)
+   * 
+   * @param evt
+   */
   @Override
   public void mouseDragged(MouseEvent evt)
   {
     mouseDragging = true;
+    ColumnSelection cs = av.getColumnSelection();
 
-    int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
-    if (res < 0)
-    {
-      res = 0;
-    }
-
-    if (av.hasHiddenColumns())
-    {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
-    }
-
-    if (res > av.getAlignment().getWidth())
-    {
-      res = av.getAlignment().getWidth() - 1;
-    }
-
-    if (res < min)
-    {
-      min = res;
-    }
-
-    if (res > max)
-    {
-      max = res;
-    }
+    int res = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
+    res = Math.max(0, res);
+    res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(res);
+    res = Math.min(res, av.getAlignment().getWidth() - 1);
+    min = Math.min(res, min);
+    max = Math.max(res, max);
 
     SequenceGroup sg = av.getSelectionGroup();
-
     if (sg != null)
     {
       stretchingGroup = true;
-
-      if (!av.getColumnSelection().contains(res))
-      {
-        av.getColumnSelection().addElement(res);
-      }
-
-      if (res > sg.getStartRes())
-      {
-        sg.setEndRes(res);
-      }
-      if (res < sg.getStartRes())
-      {
-        sg.setStartRes(res);
-      }
-
-      int col;
-      for (int i = min; i <= max; i++)
-      {
-        col = av.getColumnSelection().adjustForHiddenColumns(i);
-
-        if ((col < sg.getStartRes()) || (col > sg.getEndRes()))
-        {
-          av.getColumnSelection().removeElement(col);
-        }
-        else
-        {
-          av.getColumnSelection().addElement(col);
-        }
-      }
-
+      cs.stretchGroup(res, sg, min, max);
       ap.paintAlignment(false);
     }
   }
@@ -366,12 +331,14 @@ public class ScalePanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int res = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
 
-    res = av.getColumnSelection().adjustForHiddenColumns(res);
+    res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(res);
 
     reveal = null;
-    for (int[] region : av.getColumnSelection().getHiddenColumns())
+    for (int[] region : av.getAlignment().getHiddenColumns()
+            .getHiddenRegions())
     {
       if (res + 1 == region[0] || res - 1 == region[1])
       {
@@ -392,7 +359,8 @@ public class ScalePanel extends Panel implements MouseMotionListener,
   @Override
   public void paint(Graphics g)
   {
-    drawScale(g, av.getStartRes(), av.getEndRes(), getSize().width,
+    drawScale(g, av.getRanges().getStartRes(), av.getRanges().getEndRes(),
+            getSize().width,
             getSize().height);
   }
 
@@ -408,21 +376,22 @@ public class ScalePanel extends Panel implements MouseMotionListener,
 
     // Fill the selected columns
     ColumnSelection cs = av.getColumnSelection();
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
     int avCharWidth = av.getCharWidth();
     int avcharHeight = av.getCharHeight();
     if (cs != null)
     {
       gg.setColor(new Color(220, 0, 0));
-      boolean hasHiddenColumns = cs.hasHiddenColumns();
+      boolean hasHiddenColumns = hidden.hasHiddenColumns();
       for (int sel : cs.getSelected())
       {
         // TODO: JAL-2001 - provide a fast method to list visible selected in a
         // given range
         if (hasHiddenColumns)
         {
-          if (cs.isVisible(sel))
+          if (hidden.isVisible(sel))
           {
-            sel = cs.findColumnPosition(sel);
+            sel = hidden.findColumnPosition(sel);
           }
           else
           {
@@ -481,10 +450,10 @@ public class ScalePanel extends Panel implements MouseMotionListener,
       if (av.getShowHiddenMarkers())
       {
         int widthx = 1 + endx - startx;
-        for (int i = 0; i < cs.getHiddenColumns().size(); i++)
+        for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
         {
 
-          res = cs.findHiddenRegionPosition(i) - startx;
+          res = hidden.findHiddenRegionPosition(i) - startx;
 
           if (res < 0 || res > widthx)
           {
@@ -500,4 +469,11 @@ public class ScalePanel extends Panel implements MouseMotionListener,
     }
   }
 
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport change events (e.g. alignment panel was scrolled)
+    repaint();
+  }
+
 }
index 5d6bb07..46a908e 100755 (executable)
 package jalview.appletgui;
 
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.Color;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Image;
 import java.awt.Panel;
+import java.beans.PropertyChangeEvent;
 
-public class SeqCanvas extends Panel
+public class SeqCanvas extends Panel implements ViewportListenerI
 {
   FeatureRenderer fr;
 
@@ -63,6 +67,8 @@ public class SeqCanvas extends Panel
     sr = new SequenceRenderer(av);
     PaintRefresher.Register(this, av.getSequenceSetId());
     updateViewport();
+
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   int avcharHeight = 0, avcharWidth = 0;
@@ -122,14 +128,17 @@ public class SeqCanvas extends Panel
     ypos += avcharHeight;
     if (av.hasHiddenColumns())
     {
-      startx = av.getColumnSelection().adjustForHiddenColumns(startx);
-      endx = av.getColumnSelection().adjustForHiddenColumns(endx);
+      startx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(startx);
+      endx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(endx);
     }
 
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     // WEST SCALE
@@ -169,7 +178,8 @@ public class SeqCanvas extends Panel
 
     if (av.hasHiddenColumns())
     {
-      endx = av.getColumnSelection().adjustForHiddenColumns(endx);
+      endx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(endx);
     }
 
     SequenceI seq;
@@ -211,17 +221,19 @@ public class SeqCanvas extends Panel
       return;
     }
 
+    ViewportRanges ranges = av.getRanges();
+
     updateViewport();
 
     // Its possible on certain browsers that the call to fastpaint
     // is faster than it can paint, so this check here catches
     // this possibility
-    if (lastsr + horizontal != av.startRes)
+    if (lastsr + horizontal != ranges.getStartRes())
     {
-      horizontal = av.startRes - lastsr;
+      horizontal = ranges.getStartRes() - lastsr;
     }
 
-    lastsr = av.startRes;
+    lastsr = ranges.getStartRes();
 
     fastPaint = true;
     gg.copyArea(horizontal * avcharWidth, vertical * avcharHeight, imgWidth
@@ -229,7 +241,9 @@ public class SeqCanvas extends Panel
             imgHeight - vertical * avcharHeight, -horizontal * avcharWidth,
             -vertical * avcharHeight);
 
-    int sr = av.startRes, er = av.endRes, ss = av.startSeq, es = av.endSeq, transX = 0, transY = 0;
+    int sr = ranges.getStartRes(), er = ranges.getEndRes(), ss = ranges
+            .getStartSeq(), es = ranges
+            .getEndSeq(), transX = 0, transY = 0;
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
     {
@@ -244,21 +258,23 @@ public class SeqCanvas extends Panel
     else if (vertical > 0) // scroll down
     {
       ss = es - vertical;
-      if (ss < av.startSeq) // ie scrolling too fast, more than a page at a time
+      if (ss < ranges.getStartSeq()) // ie scrolling too fast, more than a page
+                                     // at a
+                                 // time
       {
-        ss = av.startSeq;
+        ss = ranges.getStartSeq();
       }
       else
       {
-        transY = imgHeight - vertical * avcharHeight;
+        transY = imgHeight - ((vertical + 1) * avcharHeight);
       }
     }
     else if (vertical < 0)
     {
       es = ss - vertical;
-      if (es > av.endSeq)
+      if (es > ranges.getEndSeq())
       {
-        es = av.endSeq;
+        es = ranges.getEndSeq();
       }
     }
 
@@ -329,13 +345,16 @@ public class SeqCanvas extends Panel
     gg.setColor(Color.white);
     gg.fillRect(0, 0, imgWidth, imgHeight);
 
+    ViewportRanges ranges = av.getRanges();
+
     if (av.getWrapAlignment())
     {
-      drawWrappedPanel(gg, imgWidth, imgHeight, av.startRes);
+      drawWrappedPanel(gg, imgWidth, imgHeight, ranges.getStartRes());
     }
     else
     {
-      drawPanel(gg, av.startRes, av.endRes, av.startSeq, av.endSeq, 0);
+      drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
+              ranges.getStartSeq(), ranges.getEndSeq(), 0);
     }
 
     g.drawImage(img, 0, 0, this);
@@ -421,7 +440,7 @@ public class SeqCanvas extends Panel
 
     av.setWrappedWidth(cWidth);
 
-    av.endRes = av.startRes + cWidth;
+    av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
 
     int endx;
     int ypos = hgap;
@@ -430,7 +449,8 @@ public class SeqCanvas extends Panel
 
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     while ((ypos <= canvasHeight) && (startRes < maxwidth))
@@ -464,12 +484,13 @@ public class SeqCanvas extends Panel
       }
       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
       {
+        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
         g.setColor(Color.blue);
         int res;
-        for (int i = 0; i < av.getColumnSelection().getHiddenColumns()
+        for (int i = 0; i < hidden.getHiddenRegions()
                 .size(); i++)
         {
-          res = av.getColumnSelection().findHiddenRegionPosition(i)
+          res = hidden.findHiddenRegionPosition(i)
                   - startRes;
 
           if (res < 0 || res > endx - startRes)
@@ -491,7 +512,7 @@ public class SeqCanvas extends Panel
         g.setClip(0, 0, cWidth * avcharWidth, canvasHeight);
       }
 
-      drawPanel(g, startRes, endx, 0, al.getHeight(), ypos);
+      drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
       g.setClip(null);
 
       if (av.isShowAnnotation())
@@ -548,7 +569,8 @@ public class SeqCanvas extends Panel
 
       if (av.hasHiddenColumns())
       {
-        for (int[] region : av.getColumnSelection().getHiddenColumns())
+        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+        for (int[] region : hidden.getHiddenRegions())
         {
           int hideStart = region[0];
           int hideEnd = region[1];
@@ -570,7 +592,7 @@ public class SeqCanvas extends Panel
             g1.setColor(Color.blue);
             g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
                     0 + offset, (blockEnd - blockStart + 1) * avcharWidth
-                            - 1, (endSeq - startSeq) * avcharHeight
+                            - 1, (endSeq - startSeq + 1) * avcharHeight
                             + offset);
           }
 
@@ -610,7 +632,7 @@ public class SeqCanvas extends Panel
 
     // / First draw the sequences
     // ///////////////////////////
-    for (int i = startSeq; i < endSeq; i++)
+    for (int i = startSeq; i <= endSeq; i++)
     {
       nextSeq = av.getAlignment().getSequenceAt(i);
 
@@ -625,7 +647,7 @@ public class SeqCanvas extends Panel
       if (av.isShowSequenceFeatures())
       {
         fr.drawSequence(g, nextSeq, startRes, endRes, offset
-                + ((i - startSeq) * avcharHeight));
+                + ((i - startSeq) * avcharHeight), false);
       }
 
       // / Highlight search Results once all sequences have been drawn
@@ -694,7 +716,7 @@ public class SeqCanvas extends Panel
         int bottom = -1;
         int alHeight = av.getAlignment().getHeight() - 1;
 
-        for (i = startSeq; i < endSeq; i++)
+        for (i = startSeq; i <= endSeq; i++)
         {
           sx = (group.getStartRes() - startRes) * avcharWidth;
           sy = offset + ((i - startSeq) * avcharHeight);
@@ -848,4 +870,35 @@ public class SeqCanvas extends Panel
     repaint();
   }
 
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    if (!av.getWrapAlignment())
+    {
+      if (evt.getPropertyName().equals("startres")
+              || evt.getPropertyName().equals("endres"))
+      {
+        // Make sure we're not trying to draw a panel
+        // larger than the visible window
+        ViewportRanges vpRanges = av.getRanges();
+        int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
+        if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
+        {
+          scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
+        }
+        else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
+        {
+          scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
+        }
+        fastPaint(scrollX, 0);
+      }
+      else if (evt.getPropertyName().equals("startseq")
+              || evt.getPropertyName().equals("endseq"))
+      {
+        fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+      }
+    }
+
+  }
+
 }
index 4278744..9e31efe 100644 (file)
@@ -25,6 +25,7 @@ import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -38,9 +39,11 @@ import jalview.structure.SelectionSource;
 import jalview.structure.SequenceListener;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
+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;
@@ -51,6 +54,8 @@ import java.awt.event.InputEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
+import java.util.Collections;
+import java.util.List;
 import java.util.Vector;
 
 public class SeqPanel extends Panel implements MouseMotionListener,
@@ -179,19 +184,22 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     seqCanvas.cursorX += dx;
     seqCanvas.cursorY += dy;
     if (av.hasHiddenColumns()
-            && !av.getColumnSelection().isVisible(seqCanvas.cursorX))
+            && !av.getAlignment().getHiddenColumns()
+                    .isVisible(seqCanvas.cursorX))
     {
       int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
 
-      while (!av.getColumnSelection().isVisible(seqCanvas.cursorX)
+      while (!av.getAlignment().getHiddenColumns()
+              .isVisible(seqCanvas.cursorX)
               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
       {
         seqCanvas.cursorX += dx;
       }
 
       if (seqCanvas.cursorX >= maxWidth
-              || !av.getColumnSelection().isVisible(seqCanvas.cursorX))
+              || !av.getAlignment().getHiddenColumns()
+                      .isVisible(seqCanvas.cursorX))
       {
         seqCanvas.cursorX = original;
       }
@@ -222,31 +230,33 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     endEditing();
     if (av.getWrapAlignment())
     {
-      ap.scrollToWrappedVisible(seqCanvas.cursorX);
+      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
     }
     else
     {
-      while (seqCanvas.cursorY < av.startSeq)
+      ViewportRanges ranges = av.getRanges();
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      while (seqCanvas.cursorY < ranges.getStartSeq())
       {
-        ap.scrollUp(true);
+        ranges.scrollUp(true);
       }
-      while (seqCanvas.cursorY + 1 > av.endSeq)
+      while (seqCanvas.cursorY > ranges.getEndSeq())
       {
-        ap.scrollUp(false);
+        ranges.scrollUp(false);
       }
-      while (seqCanvas.cursorX < av.getColumnSelection()
-              .adjustForHiddenColumns(av.startRes))
+      while (seqCanvas.cursorX < hidden.adjustForHiddenColumns(ranges
+              .getStartRes()))
       {
 
-        if (!ap.scrollRight(false))
+        if (!ranges.scrollRight(false))
         {
           break;
         }
       }
-      while (seqCanvas.cursorX > av.getColumnSelection()
-              .adjustForHiddenColumns(av.endRes))
+      while (seqCanvas.cursorX > hidden.adjustForHiddenColumns(ranges
+              .getEndRes()))
       {
-        if (!ap.scrollRight(true))
+        if (!ranges.scrollRight(true))
         {
           break;
         }
@@ -405,13 +415,13 @@ public class SeqPanel extends Panel implements MouseMotionListener,
    * 
    * @param sequence
    *          aligned sequence object
-   * @param res
+   * @param column
    *          alignment column
    * @param seq
    *          index of sequence in alignment
-   * @return position of res in sequence
+   * @return position of column in sequence or -1 if at gap
    */
-  void setStatusMessage(SequenceI sequence, int res, int seq)
+  void setStatusMessage(SequenceI sequence, int column, int seq)
   {
     // TODO remove duplication of identical gui method
     StringBuilder text = new StringBuilder(32);
@@ -422,7 +432,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     /*
      * Try to translate the display character to residue name (null for gap).
      */
-    final String displayChar = String.valueOf(sequence.getCharAt(res));
+    final String displayChar = String.valueOf(sequence.getCharAt(column));
     if (av.getAlignment().isNucleotide())
     {
       residue = ResidueProperties.nucleotideName.get(displayChar);
@@ -445,7 +455,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     int pos = -1;
     if (residue != null)
     {
-      pos = sequence.findPosition(res);
+      pos = sequence.findPosition(column);
       text.append(" (").append(Integer.toString(pos)).append(")");
     }
 
@@ -519,7 +529,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)
     {
@@ -555,21 +565,18 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         av.setSelectionGroup(null);
       }
 
-      SequenceFeature[] features = findFeaturesAtRes(sequence,
-              sequence.findPosition(findRes(evt)));
+      int column = findColumn(evt);
+      List<SequenceFeature> features = findFeaturesAtColumn(sequence,
+              column + 1);
 
-      if (features != null && features.length > 0)
+      if (!features.isEmpty())
       {
         SearchResultsI highlight = new SearchResults();
-        highlight.addResult(sequence, features[0].getBegin(),
-                features[0].getEnd());
+        highlight.addResult(sequence, features.get(0).getBegin(), features
+                .get(0).getEnd());
         seqCanvas.highlightSearchResults(highlight);
-      }
-      if (features != null && features.length > 0)
-      {
         seqCanvas.getFeatureRenderer().amendFeatures(
-                new SequenceI[] { sequence }, features, false, ap);
-
+                Collections.singletonList(sequence), features, false, ap);
         seqCanvas.highlightSearchResults(null);
       }
     }
@@ -578,13 +585,13 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   @Override
   public void mouseReleased(MouseEvent evt)
   {
+    boolean didDrag = mouseDragging; // did we come here after a drag
     mouseDragging = false;
     mouseWheelPressed = false;
-    ap.paintAlignment(true);
 
     if (!editingSeqs)
     {
-      doMouseReleasedDefineMode(evt);
+      doMouseReleasedDefineMode(evt, didDrag);
       return;
     }
 
@@ -596,7 +603,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();
@@ -624,19 +638,20 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
 
       wrappedBlock = y / cHeight;
-      wrappedBlock += av.getStartRes() / cwidth;
+      wrappedBlock += av.getRanges().getStartRes() / cwidth;
 
       res = wrappedBlock * cwidth + x / av.getCharWidth();
 
     }
     else
     {
-      res = (x / av.getCharWidth()) + av.getStartRes();
+      res = (x / av.getCharWidth()) + av.getRanges().getStartRes();
     }
 
     if (av.hasHiddenColumns())
     {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
+      res = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(res);
     }
 
     return res;
@@ -681,7 +696,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
     else
     {
-      seq = Math.min((y / av.getCharHeight()) + av.getStartSeq(), av
+      seq = Math.min((y / av.getCharHeight())
+              + av.getRanges().getStartSeq(),
+              av
               .getAlignment().getHeight() - 1);
       if (seq < 0)
       {
@@ -696,7 +713,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())
@@ -736,10 +753,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   {
     if (av.isFollowHighlight())
     {
+      // don't allow highlight of protein/cDNA to also scroll a complementary
+      // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
+      // 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, true))
       {
         ap.alignFrame.repaint();
       }
+      ap.setToScrollComplementPanel(true);
     }
     setStatusMessage(results);
     seqCanvas.highlightSearchResults(results);
@@ -762,10 +785,10 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   @Override
   public void mouseMoved(MouseEvent evt)
   {
-    int res = findRes(evt);
+    final int column = findColumn(evt);
     int seq = findSeq(evt);
 
-    if (seq >= av.getAlignment().getHeight() || seq < 0 || res < 0)
+    if (seq >= av.getAlignment().getHeight() || seq < 0 || column < 0)
     {
       if (tooltip != null)
       {
@@ -775,7 +798,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    if (res > sequence.getLength())
+    if (column > sequence.getLength())
     {
       if (tooltip != null)
       {
@@ -784,38 +807,34 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    int respos = sequence.findPosition(res);
-    if (ssm != null)
+    final char ch = sequence.getCharAt(column);
+    boolean isGapped = Comparison.isGap(ch);
+    // find residue at column (or nearest if at a gap)
+    int respos = sequence.findPosition(column);
+
+    if (ssm != null && !isGapped)
     {
-      mouseOverSequence(sequence, res, respos);
+      mouseOverSequence(sequence, column, respos);
     }
 
     StringBuilder text = new StringBuilder();
     text.append("Sequence ").append(Integer.toString(seq + 1))
             .append(" ID: ").append(sequence.getName());
 
-    String obj = null;
-    final String ch = String.valueOf(sequence.getCharAt(res));
-    if (av.getAlignment().isNucleotide())
+    if (!isGapped)
     {
-      obj = ResidueProperties.nucleotideName.get(ch);
-      if (obj != null)
+      if (av.getAlignment().isNucleotide())
       {
-        text.append(" Nucleotide: ").append(obj);
+        String base = ResidueProperties.nucleotideName.get(ch);
+        text.append(" Nucleotide: ").append(base == null ? ch : base);
       }
-    }
-    else
-    {
-      obj = "X".equalsIgnoreCase(ch) ? "X" : ResidueProperties.aa2Triplet
-              .get(ch);
-      if (obj != null)
+      else
       {
-        text.append(" Residue: ").append(obj);
+        String residue = (ch == 'x' || ch == 'X') ? "X"
+                : ResidueProperties.aa2Triplet
+                .get(String.valueOf(ch));
+        text.append(" Residue: ").append(residue == null ? ch : residue);
       }
-    }
-
-    if (obj != null)
-    {
       text.append(" (").append(Integer.toString(respos)).append(")");
     }
 
@@ -827,7 +846,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       for (int g = 0; g < groups.length; g++)
       {
-        if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
+        if (groups[g].getStartRes() <= column && groups[g].getEndRes() >= column)
         {
           if (!groups[g].getName().startsWith("JTreeGroup")
                   && !groups[g].getName().startsWith("JGroup"))
@@ -843,33 +862,33 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
     }
 
-    // use aa to see if the mouse pointer is on a
-    SequenceFeature[] allFeatures = findFeaturesAtRes(sequence,
-            sequence.findPosition(res));
-
-    int index = 0;
-    while (index < allFeatures.length)
+    /*
+     * add feature details to tooltip, including any that straddle
+     * a gapped position
+     */
+    if (av.isShowSequenceFeatures())
     {
-      SequenceFeature sf = allFeatures[index];
-
-      tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
-
-      if (sf.getDescription() != null)
+      List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
+              column + 1);
+      for (SequenceFeature sf : allFeatures)
       {
-        tooltipText.append(" " + sf.getDescription());
-      }
+        tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
 
-      if (sf.getValue("status") != null)
-      {
-        String status = sf.getValue("status").toString();
-        if (status.length() > 0)
+        if (sf.getDescription() != null)
         {
-          tooltipText.append(" (" + sf.getValue("status") + ")");
+          tooltipText.append(" " + sf.getDescription());
         }
-      }
-      tooltipText.append("\n");
 
-      index++;
+        if (sf.getValue("status") != null)
+        {
+          String status = sf.getValue("status").toString();
+          if (status.length() > 0)
+          {
+            tooltipText.append(" (" + sf.getValue("status") + ")");
+          }
+        }
+        tooltipText.append("\n");
+      }
     }
 
     if (tooltip == null)
@@ -882,40 +901,19 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
   }
 
-  SequenceFeature[] findFeaturesAtRes(SequenceI sequence, int res)
+  /**
+   * 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 sequence
+   * @param column
+   *          (1..)
+   * @return
+   */
+  List<SequenceFeature> findFeaturesAtColumn(SequenceI sequence, int column)
   {
-    Vector tmp = new Vector();
-    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))
-        {
-          tmp.addElement(features[i]);
-        }
-      }
-    }
-
-    features = new SequenceFeature[tmp.size()];
-    tmp.copyInto(features);
-
-    return features;
+    return seqCanvas.getFeatureRenderer().findFeaturesAtColumn(sequence, column);
   }
 
   Tooltip tooltip;
@@ -955,7 +953,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
           fontSize = 1;
         }
 
-        av.setFont(new Font(av.font.getName(), av.font.getStyle(), fontSize));
+        av.setFont(
+                new Font(av.font.getName(), av.font.getStyle(), fontSize),
+                true);
         av.setCharWidth(oldWidth);
       }
       else
@@ -993,7 +993,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (res < 0)
     {
@@ -1127,8 +1127,10 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     if (av.hasHiddenColumns())
     {
       fixedColumns = true;
-      int y1 = av.getColumnSelection().getHiddenBoundaryLeft(startres);
-      int y2 = av.getColumnSelection().getHiddenBoundaryRight(startres);
+      int y1 = av.getAlignment().getHiddenColumns()
+              .getHiddenBoundaryLeft(startres);
+      int y2 = av.getAlignment().getHiddenColumns()
+              .getHiddenBoundaryRight(startres);
 
       if ((insertGap && startres > y1 && lastres < y1)
               || (!insertGap && startres < y2 && lastres > y2))
@@ -1199,8 +1201,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         {
           if (sg.getSize() == av.getAlignment().getHeight())
           {
-            if ((av.hasHiddenColumns() && startres < av
-                    .getColumnSelection().getHiddenBoundaryRight(startres)))
+            if ((av.hasHiddenColumns() && startres < av.getAlignment()
+                    .getHiddenColumns().getHiddenBoundaryRight(startres)))
             {
               endEditing();
               return;
@@ -1404,7 +1406,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;
@@ -1416,65 +1418,40 @@ 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)
+    if (stretchGroup == null || !stretchGroup.contains(sequence, column))
     {
-      stretchGroup = av.getAlignment().findGroup(sequence, res);
-      av.setSelectionGroup(stretchGroup);
-    }
-
-    if (stretchGroup == null
-            || !stretchGroup.getSequences(null).contains(sequence)
-            || stretchGroup.getStartRes() > res
-            || stretchGroup.getEndRes() < res)
-    {
-      stretchGroup = null;
-
-      SequenceGroup[] allGroups = av.getAlignment().findAllGroups(sequence);
-
-      if (allGroups != null)
+      stretchGroup = av.getAlignment().findGroup(sequence, column);
+      if (stretchGroup != null)
       {
-        for (int i = 0; i < allGroups.length; i++)
-        {
-          if (allGroups[i].getStartRes() <= res
-                  && allGroups[i].getEndRes() >= res)
-          {
-            stretchGroup = allGroups[i];
-            break;
-          }
-        }
+        // only update the current selection if the popup menu has a group to
+        // focus on
+        av.setSelectionGroup(stretchGroup);
       }
-      av.setSelectionGroup(stretchGroup);
     }
 
     // DETECT RIGHT MOUSE BUTTON IN AWT
     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      SequenceFeature[] allFeatures = findFeaturesAtRes(sequence,
-              sequence.findPosition(res));
+      List<SequenceFeature> allFeatures = findFeaturesAtColumn(sequence,
+              sequence.findPosition(column + 1));
 
       Vector<String> links = null;
-      if (allFeatures != null)
+      for (SequenceFeature sf : allFeatures)
       {
-        for (int i = 0; i < allFeatures.length; i++)
+        if (sf.links != null)
         {
-          if (allFeatures[i].links != null)
+          if (links == null)
           {
-            if (links == null)
-            {
-              links = new Vector<String>();
-            }
-            for (int j = 0; j < allFeatures[i].links.size(); j++)
-            {
-              links.addElement(allFeatures[i].links.elementAt(j));
-            }
+            links = new Vector<String>();
           }
+          links.addAll(sf.links);
         }
       }
       APopupMenu popup = new APopupMenu(ap, null, links);
@@ -1485,7 +1462,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;
@@ -1497,27 +1474,27 @@ 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;
 
       if (av.getConservationSelected())
       {
-        SliderPanel.setConservationSlider(ap, av.getGlobalColourScheme(),
-                "Background");
+        SliderPanel.setConservationSlider(ap, av.getResidueShading(),
+                ap.getViewName());
       }
       if (av.getAbovePIDThreshold())
       {
-        SliderPanel.setPIDSliderSource(ap, av.getGlobalColourScheme(),
-                "Background");
+        SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
+                ap.getViewName());
       }
 
     }
   }
 
-  public void doMouseReleasedDefineMode(MouseEvent evt)
+  public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
   {
     if (stretchGroup == null)
     {
@@ -1527,7 +1504,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     // but defer colourscheme update until hidden sequences are passed in
     boolean vischange = stretchGroup.recalcConservation(true);
     // here we rely on stretchGroup == av.getSelection()
-    needOverviewUpdate |= vischange && av.isSelectionDefinedGroup();
+    needOverviewUpdate |= vischange && av.isSelectionDefinedGroup()
+            && afterDrag;
     if (stretchGroup.cs != null)
     {
       stretchGroup.cs.alignmentChanged(stretchGroup,
@@ -1538,7 +1516,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         SliderPanel.setConservationSlider(ap, stretchGroup.cs,
                 stretchGroup.getName());
       }
-      else
+      if (stretchGroup.cs.getThreshold() > 0)
       {
         SliderPanel.setPIDSliderSource(ap, stretchGroup.cs,
                 stretchGroup.getName());
@@ -1555,7 +1533,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)
@@ -1662,8 +1640,10 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       oldSeq = -1;
     }
 
-    if (res > av.endRes || res < av.startRes || y < av.startSeq
-            || y > av.endSeq)
+    if (res > av.getRanges().getEndRes()
+            || res < av.getRanges().getStartRes()
+            || y < av.getRanges().getStartSeq()
+            || y > av.getRanges().getEndSeq())
     {
       mouseExited(evt);
     }
@@ -1761,25 +1741,27 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         if (evt != null)
         {
 
-          if (mouseDragging && evt.getY() < 0 && av.getStartSeq() > 0)
+          if (mouseDragging && evt.getY() < 0
+                  && av.getRanges().getStartSeq() > 0)
           {
-            running = ap.scrollUp(true);
+            running = av.getRanges().scrollUp(true);
           }
 
           if (mouseDragging && evt.getY() >= getSize().height
-                  && av.getAlignment().getHeight() > av.getEndSeq())
+                  && av.getAlignment().getHeight() > av.getRanges()
+                          .getEndSeq())
           {
-            running = ap.scrollUp(false);
+            running = av.getRanges().scrollUp(false);
           }
 
           if (mouseDragging && evt.getX() < 0)
           {
-            running = ap.scrollRight(false);
+            running = av.getRanges().scrollRight(false);
           }
 
           else if (mouseDragging && evt.getX() >= getSize().width)
           {
-            running = ap.scrollRight(true);
+            running = av.getRanges().scrollRight(true);
           }
         }
 
@@ -1798,7 +1780,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
    */
   @Override
   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
-          SelectionSource source)
+          HiddenColumns hidden, SelectionSource source)
   {
     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
     // handles selection messages...
@@ -1815,7 +1797,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
      * Check for selection in a view of which this one is a dna/protein
      * complement.
      */
-    if (selectionFromTranslation(seqsel, colsel, source))
+    if (selectionFromTranslation(seqsel, colsel, hidden, source))
     {
       return;
     }
@@ -1881,15 +1863,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         }
         else
         {
-          av.getColumnSelection().setElementsFrom(colsel);
+          av.getColumnSelection().setElementsFrom(colsel,
+                  av.getAlignment().getHiddenColumns());
         }
       }
       repaint |= av.isColSelChanged(true);
     }
     if (copycolsel
             && av.hasHiddenColumns()
-            && (av.getColumnSelection() == null || av.getColumnSelection()
-                    .getHiddenColumns() == null))
+            && (av.getColumnSelection() == null || av.getAlignment()
+                    .getHiddenColumns().getHiddenRegions() == null))
     {
       System.err.println("Bad things");
     }
@@ -1909,8 +1892,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   public void scrollTo(int row, int column)
   {
 
-    row = row < 0 ? ap.av.startSeq : row;
-    column = column < 0 ? ap.av.startRes : column;
+    row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
+    column = column < 0 ? ap.av.getRanges().getStartRes() : column;
     ap.scrollTo(column, column, row, true, true);
   }
 
@@ -1922,8 +1905,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   public void scrollToRow(int row)
   {
 
-    row = row < 0 ? ap.av.startSeq : row;
-    ap.scrollTo(ap.av.startRes, ap.av.startRes, row, true, true);
+    row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
+    ap.scrollTo(ap.av.getRanges().getStartRes(), ap.av.getRanges()
+            .getStartRes(), row, true, true);
   }
 
   /**
@@ -1934,8 +1918,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   public void scrollToColumn(int column)
   {
 
-    column = column < 0 ? ap.av.startRes : column;
-    ap.scrollTo(column, column, ap.av.startSeq, true, true);
+    column = column < 0 ? ap.av.getRanges().getStartRes() : column;
+    ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true, true);
   }
 
   /**
@@ -1948,7 +1932,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
    * @param source
    */
   protected boolean selectionFromTranslation(SequenceGroup seqsel,
-          ColumnSelection colsel, SelectionSource source)
+          ColumnSelection colsel, HiddenColumns hidden,
+          SelectionSource source)
   {
     if (!(source instanceof AlignViewportI))
     {
@@ -1971,9 +1956,13 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     /*
      * Map column selection
      */
-    ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
-            av);
+    // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
+    // av);
+    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns hs = new HiddenColumns();
+    MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
     av.setColumnSelection(cs);
+    av.getAlignment().setHiddenColumns(hs);
 
     ap.scalePanelHolder.repaint();
     ap.repaint();
index 970d20e..38031e4 100755 (executable)
  */
 package jalview.appletgui;
 
-import jalview.api.FeatureRenderer;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.ColourSchemeI;
+import jalview.renderer.ResidueShaderI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 
 import java.awt.Color;
 import java.awt.Font;
@@ -69,8 +69,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     this.renderGaps = renderGaps;
   }
 
-  @Override
-  public Color getResidueBoxColour(SequenceI seq, int i)
+  protected Color getResidueBoxColour(SequenceI seq, int i)
   {
     allGroups = av.getAlignment().findAllGroups(seq);
 
@@ -78,12 +77,12 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     {
       if (currentSequenceGroup.getDisplayBoxes())
       {
-        getBoxColour(currentSequenceGroup.cs, seq, i);
+        getBoxColour(currentSequenceGroup.getGroupColourScheme(), seq, i);
       }
     }
     else if (av.getShowBoxes())
     {
-      getBoxColour(av.getGlobalColourScheme(), seq, i);
+      getBoxColour(av.getResidueShading(), seq, i);
     }
 
     return resBoxColour;
@@ -96,29 +95,29 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
    * 
    * @param seq
    * @param position
-   * @param fr
+   * @param finder
    * @return
    */
   @Override
   public Color getResidueColour(final SequenceI seq, int position,
-          FeatureRenderer fr)
+          FeatureColourFinder finder)
   {
     // TODO replace 8 or so code duplications with calls to this method
     // (refactored as needed)
     Color col = getResidueBoxColour(seq, position);
 
-    if (fr != null)
+    if (finder != null)
     {
-      col = fr.findFeatureColour(col, seq, position);
+      col = finder.findFeatureColour(col, seq, position);
     }
     return col;
   }
 
-  void getBoxColour(ColourSchemeI cs, SequenceI seq, int i)
+  void getBoxColour(ResidueShaderI shader, SequenceI seq, int i)
   {
-    if (cs != null)
+    if (shader.getColourScheme() != null)
     {
-      resBoxColour = cs.findColour(seq.getCharAt(i), i, seq);
+      resBoxColour = shader.findColour(seq.getCharAt(i), i, seq);
     }
     else if (forOverview
             && !jalview.util.Comparison.isGap(seq.getCharAt(i)))
@@ -176,12 +175,13 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
         {
           if (currentSequenceGroup.getDisplayBoxes())
           {
-            getBoxColour(currentSequenceGroup.cs, seq, i);
+            getBoxColour(currentSequenceGroup.getGroupColourScheme(), seq,
+                    i);
           }
         }
         else if (av.getShowBoxes())
         {
-          getBoxColour(av.getGlobalColourScheme(), seq, i);
+          getBoxColour(av.getResidueShading(), seq, i);
         }
       }
 
@@ -254,7 +254,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
 
         if (currentSequenceGroup.getColourText())
         {
-          getBoxColour(currentSequenceGroup.cs, seq, i);
+          getBoxColour(currentSequenceGroup.getGroupColourScheme(), seq, i);
           graphics.setColor(resBoxColour.darker());
         }
         if (currentSequenceGroup.getShowNonconserved())
@@ -271,7 +271,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
 
         if (av.getColourText())
         {
-          getBoxColour(av.getGlobalColourScheme(), seq, i);
+          getBoxColour(av.getResidueShading(), seq, i);
           if (av.getShowBoxes())
           {
             graphics.setColor(resBoxColour.darker());
index 35c2a22..9154aa0 100644 (file)
@@ -21,7 +21,7 @@
 package jalview.appletgui;
 
 import jalview.datamodel.SequenceGroup;
-import jalview.schemes.ColourSchemeI;
+import jalview.renderer.ResidueShaderI;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -38,6 +38,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.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.WindowAdapter;
@@ -47,37 +49,39 @@ import java.util.Iterator;
 public class SliderPanel extends Panel implements ActionListener,
         AdjustmentListener, MouseListener
 {
+  private static final String BACKGROUND = "Background";
+
   AlignmentPanel ap;
 
   boolean forConservation = true;
 
-  ColourSchemeI cs;
+  ResidueShaderI cs;
 
   static Frame conservationSlider;
 
   static Frame PIDSlider;
 
   public static int setConservationSlider(AlignmentPanel ap,
-          ColourSchemeI cs, String source)
+          ResidueShaderI ccs, String source)
   {
     SliderPanel sp = null;
 
     if (conservationSlider == null)
     {
-      sp = new SliderPanel(ap, cs.getConservationInc(), true, cs);
+      sp = new SliderPanel(ap, ccs.getConservationInc(), true, ccs);
       conservationSlider = new Frame();
       conservationSlider.add(sp);
     }
     else
     {
       sp = (SliderPanel) conservationSlider.getComponent(0);
-      sp.cs = cs;
+      sp.cs = ccs;
+      sp.valueField.setText(String.valueOf(ccs.getConservationInc()));
     }
 
-    conservationSlider
-            .setTitle(MessageManager.formatMessage(
-                    "label.conservation_colour_increment",
-                    new String[] { source }));
+    conservationSlider.setTitle(MessageManager.formatMessage(
+            "label.conservation_colour_increment",
+            new String[] { source == null ? BACKGROUND : source }));
     if (ap.av.getAlignment().getGroups() != null)
     {
       sp.setAllGroupsCheckEnabled(true);
@@ -106,6 +110,7 @@ public class SliderPanel extends Panel implements ActionListener,
               conservationSlider.getTitle(), 420, 100);
       conservationSlider.addWindowListener(new WindowAdapter()
       {
+        @Override
         public void windowClosing(WindowEvent e)
         {
           conservationSlider = null;
@@ -116,25 +121,25 @@ public class SliderPanel extends Panel implements ActionListener,
 
   }
 
-  public static int setPIDSliderSource(AlignmentPanel ap, ColourSchemeI cs,
-          String source)
+  public static int setPIDSliderSource(AlignmentPanel ap,
+          ResidueShaderI ccs, String source)
   {
     SliderPanel pid = null;
     if (PIDSlider == null)
     {
-      pid = new SliderPanel(ap, 50, false, cs);
+      pid = new SliderPanel(ap, ccs.getThreshold(), false, ccs);
       PIDSlider = new Frame();
       PIDSlider.add(pid);
     }
     else
     {
       pid = (SliderPanel) PIDSlider.getComponent(0);
-      pid.cs = cs;
+      pid.cs = ccs;
+      pid.valueField.setText(String.valueOf(ccs.getThreshold()));
     }
-    PIDSlider
-            .setTitle(MessageManager.formatMessage(
-                    "label.percentage_identity_threshold",
-                    new String[] { source }));
+    PIDSlider.setTitle(MessageManager.formatMessage(
+            "label.percentage_identity_threshold",
+            new String[] { source == null ? BACKGROUND : source }));
 
     if (ap.av.getAlignment().getGroups() != null)
     {
@@ -165,6 +170,7 @@ public class SliderPanel extends Panel implements ActionListener,
               420, 100);
       PIDSlider.addWindowListener(new WindowAdapter()
       {
+        @Override
         public void windowClosing(WindowEvent e)
         {
           PIDSlider = null;
@@ -174,8 +180,31 @@ public class SliderPanel extends Panel implements ActionListener,
 
   }
 
+  /**
+   * Hides the PID slider panel if it is shown
+   */
+  public static void hidePIDSlider()
+  {
+    if (PIDSlider != null)
+    {
+      PIDSlider.setVisible(false);
+      PIDSlider = null;
+    }
+  }
+
+  /**
+   * Hides the Conservation slider panel if it is shown
+   */
+  public static void hideConservationSlider()
+  {
+    if (conservationSlider != null)
+    {
+      conservationSlider.setVisible(false);
+      conservationSlider = null;
+    }
+  }
   public SliderPanel(AlignmentPanel ap, int value, boolean forConserve,
-          ColourSchemeI cs)
+          ResidueShaderI shader)
   {
     try
     {
@@ -185,7 +214,7 @@ public class SliderPanel extends Panel implements ActionListener,
       e.printStackTrace();
     }
     this.ap = ap;
-    this.cs = cs;
+    this.cs = shader;
     forConservation = forConserve;
     undoButton.setVisible(false);
     applyButton.setVisible(false);
@@ -200,7 +229,7 @@ public class SliderPanel extends Panel implements ActionListener,
     else
     {
       label.setText(MessageManager
-              .getString("label.colour_residues_above_occurence"));
+              .getString("label.colour_residues_above_occurrence"));
       slider.setMinimum(0);
       slider.setMaximum(100 + slider.getVisibleAmount());
       slider.setBlockIncrement(1);
@@ -220,7 +249,7 @@ public class SliderPanel extends Panel implements ActionListener,
       return;
     }
 
-    ColourSchemeI toChange = cs;
+    ResidueShaderI toChange = cs;
     Iterator<SequenceGroup> allGroups = null;
 
     if (allGroupsCheck.getState())
@@ -261,6 +290,7 @@ public class SliderPanel extends Panel implements ActionListener,
     allGroupsCheck.setEnabled(b);
   }
 
+  @Override
   public void actionPerformed(ActionEvent evt)
   {
     if (evt.getSource() == applyButton)
@@ -277,6 +307,7 @@ public class SliderPanel extends Panel implements ActionListener,
     }
   }
 
+  @Override
   public void adjustmentValueChanged(AdjustmentEvent evt)
   {
     valueField.setText(slider.getValue() + "");
@@ -287,11 +318,11 @@ public class SliderPanel extends Panel implements ActionListener,
   {
     try
     {
-      int i = Integer.parseInt(valueField.getText());
+      int i = Integer.valueOf(valueField.getText());
       slider.setValue(i);
-    } catch (Exception ex)
+    } catch (NumberFormatException ex)
     {
-      valueField.setText(slider.getValue() + "");
+      valueField.setText(String.valueOf(slider.getValue()));
     }
   }
 
@@ -344,6 +375,16 @@ public class SliderPanel extends Panel implements ActionListener,
     valueField.setText("   ");
     valueField.addActionListener(this);
     valueField.setColumns(3);
+    valueField.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        valueField_actionPerformed();
+        valueChanged(slider.getValue());
+      }
+    });
+    
     label.setFont(new java.awt.Font("Verdana", 0, 11));
     label.setText(MessageManager.getString("label.set_this_label_text"));
     jPanel1.setLayout(borderLayout1);
@@ -381,23 +422,28 @@ public class SliderPanel extends Panel implements ActionListener,
   {
   }
 
+  @Override
   public void mousePressed(MouseEvent evt)
   {
   }
 
+  @Override
   public void mouseReleased(MouseEvent evt)
   {
     ap.paintAlignment(true);
   }
 
+  @Override
   public void mouseClicked(MouseEvent evt)
   {
   }
 
+  @Override
   public void mouseEntered(MouseEvent evt)
   {
   }
 
+  @Override
   public void mouseExited(MouseEvent evt)
   {
   }
index 8292a5a..272a2b3 100755 (executable)
@@ -21,7 +21,7 @@
 package jalview.appletgui;
 
 import jalview.analysis.Conservation;
-import jalview.analysis.NJTree;
+import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
@@ -48,12 +48,13 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Vector;
 
 public class TreeCanvas extends Panel implements MouseListener,
         MouseMotionListener
 {
-  NJTree tree;
+  TreeModel tree;
 
   ScrollPane scrollPane;
 
@@ -115,13 +116,13 @@ public class TreeCanvas extends Panel implements MouseListener,
     selected.addOrRemove(sequence, true);
   }
 
-  public void setTree(NJTree tree)
+  public void setTree(TreeModel tree2)
   {
-    this.tree = tree;
-    tree.findHeight(tree.getTopNode());
+    this.tree = tree2;
+    tree2.findHeight(tree2.getTopNode());
 
     // Now have to calculate longest name based on the leaves
-    Vector<SequenceNode> leaves = tree.findLeaves(tree.getTopNode());
+    Vector<SequenceNode> leaves = tree2.findLeaves(tree2.getTopNode());
     boolean has_placeholders = false;
     longestName = "";
 
@@ -146,7 +147,7 @@ public class TreeCanvas extends Panel implements MouseListener,
   }
 
   public void drawNode(Graphics g, SequenceNode node, float chunk,
-          float scale, int width, int offx, int offy)
+          double scale, int width, int offx, int offy)
   {
     if (node == null)
     {
@@ -157,8 +158,8 @@ public class TreeCanvas extends Panel implements MouseListener,
     {
       // Drawing leaf node
 
-      float height = node.height;
-      float dist = node.dist;
+      double height = node.height;
+      double dist = node.dist;
 
       int xstart = (int) ((height - dist) * scale) + offx;
       int xend = (int) (height * scale) + offx;
@@ -240,8 +241,8 @@ public class TreeCanvas extends Panel implements MouseListener,
       drawNode(g, (SequenceNode) node.right(), chunk, scale, width, offx,
               offy);
 
-      float height = node.height;
-      float dist = node.dist;
+      double height = node.height;
+      double dist = node.dist;
 
       int xstart = (int) ((height - dist) * scale) + offx;
       int xend = (int) (height * scale) + offx;
@@ -260,10 +261,11 @@ public class TreeCanvas extends Panel implements MouseListener,
         g.fillRect(xend - 2, ypos - 2, 4, 4);
       }
 
-      int ystart = (int) (((SequenceNode) node.left()).ycount * chunk)
-              + offy;
-      int yend = (int) (((SequenceNode) node.right()).ycount * chunk)
+      int ystart = (int) (node.left() == null ? 0 : (((SequenceNode) node
+              .left()).ycount * chunk))
               + offy;
+      int yend = (int) (node.right() == null ? 0 : (((SequenceNode) node
+              .right()).ycount * chunk)) + offy;
 
       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
       nodeHash.put(node, pos);
@@ -338,7 +340,7 @@ public class TreeCanvas extends Panel implements MouseListener,
 
     SequenceNode top = tree.getTopNode();
 
-    float wscale = (float) (width * .8 - offx * 2) / tree.getMaxHeight();
+    double wscale = (float) (width * .8 - offx * 2) / tree.getMaxHeight();
     if (top.count == 0)
     {
       top.count = ((SequenceNode) top.left()).count
@@ -350,7 +352,7 @@ public class TreeCanvas extends Panel implements MouseListener,
   }
 
   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
-          float scale, int width, int offx, int offy)
+          double scale, int width, int offx, int offy)
   {
     if (node == null)
     {
@@ -359,7 +361,7 @@ public class TreeCanvas extends Panel implements MouseListener,
 
     if (node.left() == null && node.right() == null)
     {
-      float height = node.height;
+      double height = node.height;
       // float dist = node.dist;
 
       // int xstart = (int) ( (height - dist) * scale) + offx;
@@ -465,7 +467,7 @@ public class TreeCanvas extends Panel implements MouseListener,
     // for
     // scrollbar
 
-    float wscale = (width - labelLength - offx * 2) / tree.getMaxHeight();
+    double wscale = (width - labelLength - offx * 2) / tree.getMaxHeight();
 
     SequenceNode top = tree.getTopNode();
 
@@ -593,8 +595,7 @@ public class TreeCanvas extends Panel implements MouseListener,
         threshold = (float) (x - offx)
                 / (float) (getSize().width - labelLength - 2 * offx);
 
-        tree.getGroups().removeAllElements();
-        tree.groupNodes(tree.getTopNode(), threshold);
+        List<SequenceNode> groups = tree.groupNodes(threshold);
         setColor(tree.getTopNode(), Color.black);
 
         av.setSelectionGroup(null);
@@ -608,7 +609,7 @@ public class TreeCanvas extends Panel implements MouseListener,
           codingComplement.clearSequenceColours();
         }
 
-        colourGroups();
+        colourGroups(groups);
 
       }
     }
@@ -618,17 +619,16 @@ public class TreeCanvas extends Panel implements MouseListener,
 
   }
 
-  void colourGroups()
+  void colourGroups(List<SequenceNode> groups)
   {
-    for (int i = 0; i < tree.getGroups().size(); i++)
+    for (int i = 0; i < groups.size(); i++)
     {
 
       Color col = new Color((int) (Math.random() * 255),
               (int) (Math.random() * 255), (int) (Math.random() * 255));
-      setColor(tree.getGroups().elementAt(i), col.brighter());
+      setColor(groups.get(i), col.brighter());
 
-      Vector<SequenceNode> l = tree.findLeaves(tree.getGroups()
-              .elementAt(i));
+      Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
 
       Vector<SequenceI> sequences = new Vector<SequenceI>();
       for (int j = 0; j < l.size(); j++)
@@ -656,35 +656,38 @@ public class TreeCanvas extends Panel implements MouseListener,
         }
         else
         {
-          cs = ColourSchemeProperty.getColour(sg, ColourSchemeProperty
-                  .getColourName(av.getGlobalColourScheme()));
+          cs = ColourSchemeProperty.getColourScheme(sg,
+                  ColourSchemeProperty.getColourName(av
+                          .getGlobalColourScheme()));
         }
         // cs is null if shading is an annotationColourGradient
-        if (cs != null)
-        {
-          cs.setThreshold(av.getGlobalColourScheme().getThreshold(),
-                  av.isIgnoreGapsConsensus());
-        }
+        // if (cs != null)
+        // {
+        // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
+        // av.isIgnoreGapsConsensus());
+        // }
       }
       // TODO: cs used to be initialized with a sequence collection and
       // recalcConservation called automatically
       // instead we set it manually - recalc called after updateAnnotation
-      sg.cs = cs;
+      sg.setColourScheme(cs);
+      sg.getGroupColourScheme().setThreshold(
+              av.getResidueShading().getThreshold(),
+              av.isIgnoreGapsConsensus());
 
       sg.setName("JTreeGroup:" + sg.hashCode());
       sg.setIdColour(col);
       if (av.getGlobalColourScheme() != null
-              && av.getGlobalColourScheme().conservationApplied())
+              && av.getResidueShading().conservationApplied())
       {
         Conservation c = new Conservation("Group", sg.getSequences(null),
                 sg.getStartRes(), sg.getEndRes());
 
         c.calculate();
         c.verdict(false, av.getConsPercGaps());
-        cs.setConservation(c);
-
-        sg.cs = cs;
 
+        sg.setColourScheme(cs);
+        sg.getGroupColourScheme().setConservation(c);
       }
 
       av.getAlignment().addGroup(sg);
index b4b8ec2..c7bf6aa 100644 (file)
  */
 package jalview.appletgui;
 
+import jalview.analysis.AverageDistanceTree;
 import jalview.analysis.NJTree;
+import jalview.analysis.TreeBuilder;
+import jalview.analysis.TreeModel;
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.analysis.scoremodels.SimilarityParams;
 import jalview.api.analysis.ScoreModelI;
-import jalview.api.analysis.ViewBasedAnalysisI;
 import jalview.datamodel.Alignment;
-import jalview.datamodel.AlignmentView;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.io.NewickFile;
-import jalview.schemes.ResidueProperties;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -58,17 +60,18 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
 
   TreeCanvas treeCanvas;
 
-  NJTree tree;
+  TreeModel tree;
 
   AlignmentPanel ap;
 
   AlignViewport av;
 
-  public NJTree getTree()
+  public TreeModel getTree()
   {
     return tree;
   }
 
+  @Override
   public void finalize() throws Throwable
   {
     ap = null;
@@ -78,21 +81,8 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
 
   /**
    * Creates a new TreePanel object.
-   * 
-   * @param av
-   *          DOCUMENT ME!
-   * @param seqVector
-   *          DOCUMENT ME!
-   * @param type
-   *          DOCUMENT ME!
-   * @param pwtype
-   *          DOCUMENT ME!
-   * @param s
-   *          DOCUMENT ME!
-   * @param e
-   *          DOCUMENT ME!
    */
-  public TreePanel(AlignmentPanel ap, String type, String pwtype)
+  public TreePanel(AlignmentPanel alignPanel, String type, String pwtype)
   {
     try
     {
@@ -103,22 +93,12 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
       ex.printStackTrace();
     }
 
-    initTreePanel(ap, type, pwtype, null);
+    initTreePanel(alignPanel, type, pwtype, null);
   }
 
   /**
    * Creates a new TreePanel object.
    * 
-   * @param av
-   *          DOCUMENT ME!
-   * @param seqVector
-   *          DOCUMENT ME!
-   * @param newtree
-   *          DOCUMENT ME!
-   * @param type
-   *          DOCUMENT ME!
-   * @param pwtype
-   *          DOCUMENT ME!
    */
   public TreePanel(AlignmentPanel ap, String type, String pwtype,
           NewickFile newtree)
@@ -159,7 +139,7 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
     // yields unaligned seqs)
     // or create a selection box around columns in alignment view
     // test Alignment(SeqCigar[])
-    if (tree.seqData != null)
+    if (tree.getOriginalData() != null)
     {
       char gc = '-';
       try
@@ -170,9 +150,9 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
       } catch (Exception ex)
       {
       }
-      ;
-      Object[] alAndColsel = tree.seqData
-              .getAlignmentAndColumnSelection(gc);
+
+      Object[] alAndColsel = tree.getOriginalData()
+              .getAlignmentAndHiddenColumns(gc);
 
       if (alAndColsel != null && alAndColsel[0] != null)
       {
@@ -180,7 +160,8 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
         AlignFrame af = new AlignFrame(al, av.applet,
                 "Original Data for Tree", false);
 
-        af.viewport.setHiddenColumns((ColumnSelection) alAndColsel[1]);
+        af.viewport.getAlignment().setHiddenColumns(
+                (HiddenColumns) alAndColsel[1]);
       }
     }
     else
@@ -200,62 +181,23 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
       this.newtree = newtree;
     }
 
+    @Override
     public void run()
     {
       if (newtree != null)
       {
-        if (odata == null)
-        {
-          tree = new NJTree(av.getAlignment().getSequencesArray(), newtree);
-        }
-        else
-        {
-          tree = new NJTree(av.getAlignment().getSequencesArray(), odata,
-                  newtree);
-        }
-
+        tree = new TreeModel(av.getAlignment().getSequencesArray(), odata,
+                newtree);
       }
       else
       {
-        int start, end;
-        SequenceI[] seqs;
-        boolean selview = av.getSelectionGroup() != null
-                && av.getSelectionGroup().getSize() > 1;
-        AlignmentView seqStrings = av.getAlignmentView(selview);
-        if (!selview)
-        {
-          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());
-        }
-        ScoreModelI sm = ResidueProperties.getScoreModel(pwtype);
-        if (sm instanceof ViewBasedAnalysisI)
-        {
-          try
-          {
-            sm = sm.getClass().newInstance();
-            ((ViewBasedAnalysisI) sm)
-                    .configureFromAlignmentView(treeCanvas.ap);
-          } catch (Exception q)
-          {
-            System.err.println("Couldn't create a scoremodel instance for "
-                    + sm.getName());
-            q.printStackTrace();
-          }
-          tree = new NJTree(seqs, seqStrings, type, pwtype, sm, start, end);
-        }
-        else
-        {
-          tree = new NJTree(seqs, seqStrings, type, pwtype, null, start,
-                  end);
-        }
+        ScoreModelI sm1 = ScoreModels.getInstance().getScoreModel(pwtype,
+                treeCanvas.ap);
+        ScoreModelI sm = sm1;
+        TreeBuilder njtree = type.equals(TreeBuilder.NEIGHBOUR_JOINING) ? new NJTree(
+                av, sm, SimilarityParams.Jalview)
+                : new AverageDistanceTree(av, sm, SimilarityParams.Jalview);
+        tree = new TreeModel(njtree);
       }
 
       tree.reCount(tree.getTopNode());
@@ -286,6 +228,7 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
     }
   }
 
+  @Override
   public void actionPerformed(ActionEvent evt)
   {
     if (evt.getSource() == newickOutput)
@@ -302,6 +245,7 @@ public class TreePanel extends EmbmenuFrame implements ActionListener,
     }
   }
 
+  @Override
   public void itemStateChanged(ItemEvent evt)
   {
     if (evt.getSource() == fitToWindow)
index 88098a9..845110e 100644 (file)
  */
 package jalview.appletgui;
 
+import jalview.analysis.AAFrequency;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceGroup;
+import jalview.renderer.ResidueShader;
+import jalview.schemes.Blosum62ColourScheme;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.FeatureColour;
+import jalview.schemes.PIDColourScheme;
 import jalview.schemes.ResidueProperties;
 import jalview.schemes.UserColourScheme;
 import jalview.util.MessageManager;
@@ -93,7 +97,7 @@ public class UserDefinedColours extends Panel implements ActionListener,
 
     if (seqGroup != null)
     {
-      oldColourScheme = seqGroup.cs;
+      oldColourScheme = seqGroup.getColourScheme();
     }
     else
     {
@@ -407,14 +411,9 @@ public class UserDefinedColours extends Panel implements ActionListener,
   {
     final Button button = new Button();
     Color col = Color.white;
-    if (oldColourScheme != null)
+    if (oldColourScheme != null && oldColourScheme.isSimple())
     {
-      try
-      {
-        col = oldColourScheme.findColour(aa.charAt(0), -1, null);
-      } catch (Exception ex)
-      {
-      }
+      col = oldColourScheme.findColour(aa.charAt(0), 0, null, null, 0f);
     }
     button.setBackground(col);
     oldColours.addElement(col);
@@ -501,20 +500,24 @@ public class UserDefinedColours extends Panel implements ActionListener,
     }
 
     UserColourScheme ucs = new UserColourScheme(newColours);
-    if (ap != null)
-    {
-      ucs.setThreshold(0, ap.av.isIgnoreGapsConsensus());
-    }
+    // if (ap != null)
+    // {
+    // ucs.setThreshold(0, ap.av.isIgnoreGapsConsensus());
+    // }
 
     if (ap != null)
     {
       if (seqGroup != null)
       {
-        seqGroup.cs = ucs;
+        seqGroup.cs = new ResidueShader(ucs);
+        seqGroup.getGroupColourScheme().setThreshold(0,
+                ap.av.isIgnoreGapsConsensus());
       }
       else
       {
         ap.av.setGlobalColourScheme(ucs);
+        ap.av.getResidueShading().setThreshold(0,
+                ap.av.isIgnoreGapsConsensus());
       }
       ap.seqPanel.seqCanvas.img = null;
       ap.paintAlignment(true);
@@ -579,35 +582,25 @@ public class UserDefinedColours extends Panel implements ActionListener,
       return;
     }
 
-    Color[] newColours = new Color[24];
-    for (int i = 0; i < 24; i++)
-    {
-      newColours[i] = oldColours.elementAt(i);
-      buttonPanel.getComponent(i).setBackground(newColours[i]);
-    }
-
-    UserColourScheme ucs = new UserColourScheme(newColours);
-
     if (ap != null)
     {
       if (seqGroup != null)
       {
-        seqGroup.cs = ucs;
+        seqGroup.cs = new ResidueShader(oldColourScheme);
+        if (oldColourScheme instanceof PIDColourScheme
+                || oldColourScheme instanceof Blosum62ColourScheme)
+        {
+          seqGroup.cs.setConsensus(AAFrequency.calculate(
+                  seqGroup.getSequences(ap.av.getHiddenRepSequences()), 0,
+                  ap.av.getAlignment().getWidth()));
+        }
       }
       else
       {
-        ap.av.setGlobalColourScheme(ucs);
+        ap.av.setGlobalColourScheme(oldColourScheme);
       }
       ap.paintAlignment(true);
     }
-    else if (jmol != null)
-    {
-      jmol.setJalviewColourScheme(ucs);
-    }
-    else if (pdbcanvas != null)
-    {
-      pdbcanvas.pdb.setColours(ucs);
-    }
 
     frame.setVisible(false);
   }
index 8412dab..da3cb92 100755 (executable)
 package jalview.bin;
 
 import jalview.datamodel.PDBEntry;
+import jalview.gui.UserDefinedColours;
+import jalview.schemes.ColourSchemeLoader;
+import jalview.schemes.ColourSchemes;
+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;
@@ -40,6 +46,7 @@ import java.util.Date;
 import java.util.Enumeration;
 import java.util.Locale;
 import java.util.Properties;
+import java.util.StringTokenizer;
 import java.util.TreeSet;
 
 import org.apache.log4j.ConsoleAppender;
@@ -122,6 +129,10 @@ import org.apache.log4j.SimpleLayout;
  * <li>SORT_ALIGNMENT (No sort|Id|Pairwise Identity)</li>
  * <li>SEQUENCE_LINKS list of name|URL pairs for opening a url with
  * $SEQUENCE_ID$</li>
+ * <li>STORED_LINKS list of name|url pairs which user has entered but are not
+ * currently used
+ * <li>DEFAULT_LINK name of single url to be used when user double clicks a
+ * sequence id (must be in SEQUENCE_LINKS or STORED_LINKS)
  * <li>GROUP_LINKS list of name|URL[|&lt;separator&gt;] tuples - see
  * jalview.utils.GroupURLLink for more info</li>
  * <li>DAS_REGISTRY_URL the registry to query</li>
@@ -179,6 +190,8 @@ import org.apache.log4j.SimpleLayout;
  * <li>STRUCTURE_DISPLAY choose from JMOL (default) or CHIMERA for 3D structure
  * display</li>
  * <li>CHIMERA_PATH specify full path to Chimera program (if non-standard)</li>
+ * <li>ID_ORG_HOSTURL location of jalview service providing identifiers.org urls
+ * </li>
  * 
  * </ul>
  * Deprecated settings:
@@ -220,6 +233,9 @@ public class Cache
 
   public static final String DAS_ACTIVE_SOURCE = "DAS_ACTIVE_SOURCE";
 
+  /**
+   * Sifts settings
+   */
   public static final String DEFAULT_SIFTS_DOWNLOAD_DIR = System
           .getProperty("user.home")
           + File.separatorChar
@@ -230,6 +246,12 @@ public class Cache
   private final static String DEFAULT_FAIL_SAFE_PID_THRESHOLD = "30";
 
   /**
+   * Identifiers.org download settings
+   */
+  private static final String ID_ORG_FILE = System.getProperty("user.home")
+          + File.separatorChar + ".identifiers.org.ids.json";
+
+  /**
    * Allowed values are PDB or mmCIF
    */
   private final static String PDB_DOWNLOAD_FORMAT = PDBEntry.Type.MMCIF
@@ -440,6 +462,10 @@ public class Cache
             "sifts_cache_threshold_in_days",
             DEFAULT_CACHE_THRESHOLD_IN_DAYS));
 
+    IdOrgSettings.setUrl(getDefault("ID_ORG_HOSTURL",
+            "http://www.jalview.org/services/identifiers"));
+    IdOrgSettings.setDownloadLocation(ID_ORG_FILE);
+
     System.out
             .println("Jalview Version: " + codeVersion + codeInstallation);
 
@@ -526,7 +552,7 @@ public class Cache
     setProperty("VERSION", codeVersion);
 
     // LOAD USERDEFINED COLOURS
-    jalview.gui.UserDefinedColours
+    jalview.bin.Cache
             .initUserColourSchemes(getProperty("USER_DEFINED_COLOURS"));
     jalview.io.PIRFile.useModellerOutput = Cache.getDefault("PIR_MODELLER",
             false);
@@ -868,19 +894,11 @@ public class Cache
     {
       return defcolour;
     }
-    Color col = jalview.schemes.ColourSchemeProperty
-            .getAWTColorFromName(colprop);
+    Color col = ColorUtils.parseColourString(colprop);
     if (col == null)
     {
-      try
-      {
-        col = new jalview.schemes.UserColourScheme(colprop).findColour('A');
-      } catch (Exception ex)
-      {
-        log.warn("Couldn't parse '" + colprop + "' as a colour for "
-                + property);
-        col = null;
-      }
+      log.warn("Couldn't parse '" + colprop + "' as a colour for "
+              + property);
     }
     return (col == null) ? defcolour : col;
   }
@@ -997,4 +1015,54 @@ public class Cache
       Cache.applicationProperties.setProperty(propName, value);
     }
   }
+
+  /**
+   * Loads in user colour schemes from files.
+   * 
+   * @param files
+   *          a '|'-delimited list of file paths
+   */
+  public static void initUserColourSchemes(String files)
+  {
+    if (files == null || files.length() == 0)
+    {
+      return;
+    }
+  
+    // In case colours can't be loaded, we'll remove them
+    // from the default list here.
+    StringBuffer coloursFound = new StringBuffer();
+    StringTokenizer st = new StringTokenizer(files, "|");
+    while (st.hasMoreElements())
+    {
+      String file = st.nextToken();
+      try
+      {
+        UserColourScheme ucs = ColourSchemeLoader.loadColourScheme(file);
+        if (ucs != null)
+        {
+          if (coloursFound.length() > 0)
+          {
+            coloursFound.append("|");
+          }
+          coloursFound.append(file);
+          ColourSchemes.getInstance().registerColourScheme(ucs);
+        }
+      } catch (Exception ex)
+      {
+        System.out.println("Error loading User ColourFile\n" + ex);
+      }
+    }
+    if (!files.equals(coloursFound.toString()))
+    {
+      if (coloursFound.toString().length() > 1)
+      {
+        setProperty(UserDefinedColours.USER_DEFINED_COLOURS, coloursFound.toString());
+      }
+      else
+      {
+        applicationProperties.remove(UserDefinedColours.USER_DEFINED_COLOURS);
+      }
+    }
+  }
 }
index 39c0a5b..954bb34 100755 (executable)
@@ -40,7 +40,6 @@ import jalview.io.NewickFile;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
-import jalview.schemes.UserColourScheme;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.ws.jws2.Jws2Discoverer;
@@ -511,16 +510,10 @@ public class Jalview
         {
           data.replaceAll("%20", " ");
 
-          ColourSchemeI cs = ColourSchemeProperty.getColour(af
+          ColourSchemeI cs = ColourSchemeProperty.getColourScheme(af
                   .getViewport().getAlignment(), data);
 
-          if (cs == null)
-          {
-            UserColourScheme ucs = new UserColourScheme("white");
-            ucs.parseAppletParameter(data);
-            cs = ucs;
-          }
-          else
+          if (cs != null)
           {
             System.out.println("CMD [-color " + data
                     + "] executed successfully!");
@@ -585,18 +578,14 @@ public class Jalview
         data = aparser.getValue("tree", true);
         if (data != null)
         {
-          jalview.io.NewickFile fin = null;
           try
           {
             System.out.println("CMD [-tree " + data
                     + "] executed successfully!");
-            fin = new NewickFile(data,
+            NewickFile nf = new NewickFile(data,
                     AppletFormatAdapter.checkProtocol(data));
-            if (fin != null)
-            {
-              af.getViewport().setCurrentTree(
-                      af.ShowNewickTree(fin, data).getTree());
-            }
+            af.getViewport().setCurrentTree(
+                    af.showNewickTree(nf, data).getTree());
           } catch (IOException ex)
           {
             System.err.println("Couldn't add tree " + data);
index d7b064a..dbc707d 100644 (file)
@@ -31,6 +31,7 @@ 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;
@@ -51,6 +52,7 @@ import jalview.javascript.JsCallBack;
 import jalview.javascript.MouseOverStructureListener;
 import jalview.structure.SelectionListener;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.ColorUtils;
 import jalview.util.HttpUtils;
 import jalview.util.MessageManager;
 
@@ -484,7 +486,8 @@ public class JalviewLite extends Applet implements
         @Override
         public void run()
         {
-          alf.select(sel, csel);
+          alf.select(sel, csel, alf.getAlignViewport().getAlignment()
+                  .getHiddenColumns());
         }
       });
     }
@@ -2272,9 +2275,9 @@ public class JalviewLite extends Applet implements
           SequenceI repseq = alignFrame.viewport.getAlignment()
                   .getSequenceAt(0);
           alignFrame.viewport.getAlignment().setSeqrep(repseq);
-          ColumnSelection cs = new ColumnSelection();
+          HiddenColumns cs = new HiddenColumns();
           cs.hideInsertionsFor(repseq);
-          alignFrame.viewport.setColumnSelection(cs);
+          alignFrame.viewport.getAlignment().setHiddenColumns(cs);
           alignFrame.alignPanel.fontChanged();
           alignFrame.alignPanel.setScrollValues(0, 0);
           result = true;
@@ -2888,22 +2891,13 @@ public class JalviewLite extends Applet implements
     {
       return defcolour;
     }
-    Color col = jalview.schemes.ColourSchemeProperty
-            .getAWTColorFromName(colprop);
+    Color col = ColorUtils.parseColourString(colprop);
     if (col == null)
     {
-      try
-      {
-        col = new jalview.schemes.UserColourScheme(colprop).findColour('A');
-      } catch (Exception ex)
-      {
-        System.err.println("Couldn't parse '" + colprop
-                + "' as a colour for " + colparam);
-        col = null;
-      }
+      System.err.println("Couldn't parse '" + colprop
+              + "' as a colour for " + colparam);
     }
     return (col == null) ? defcolour : col;
-
   }
 
   public void openJalviewHelpUrl()
index 21ff841..9eaeb7a 100644 (file)
@@ -122,15 +122,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 +139,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));
@@ -548,13 +548,14 @@ public class EditCommand implements CommandI
         {
           // modify the oldds if necessary
           if (oldds != sequence.getDatasetSequence()
-                  || sequence.getSequenceFeatures() != null)
+                  || sequence.getFeatures().hasFeatures())
           {
             if (command.oldds == null)
             {
               command.oldds = new SequenceI[command.seqs.length];
             }
             command.oldds[i] = oldds;
+            // FIXME JAL-2541 JAL-2526 get correct positions if on a gap
             adjustFeatures(
                     command,
                     i,
@@ -797,6 +798,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.
@@ -1101,8 +1104,8 @@ public class EditCommand implements CommandI
     }
   }
 
-  final static void adjustFeatures(Edit command, int index, int i, int j,
-          boolean insert)
+  final static void adjustFeatures(Edit command, int index, final int i,
+          final int j, boolean insert)
   {
     SequenceI seq = command.seqs[index];
     SequenceI sequence = seq.getDatasetSequence();
@@ -1122,56 +1125,73 @@ public class EditCommand implements CommandI
       return;
     }
 
-    SequenceFeature[] sf = sequence.getSequenceFeatures();
+    List<SequenceFeature> sf = sequence.getFeatures()
+            .getPositionalFeatures();
 
-    if (sf == null)
+    if (sf.isEmpty())
     {
       return;
     }
 
-    SequenceFeature[] oldsf = new SequenceFeature[sf.length];
+    List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
 
     int cSize = j - i;
 
-    for (int s = 0; s < sf.length; s++)
+    for (SequenceFeature feature : sf)
     {
-      SequenceFeature copy = new SequenceFeature(sf[s]);
+      SequenceFeature copy = new SequenceFeature(feature);
 
-      oldsf[s] = copy;
+      oldsf.add(copy);
 
-      if (sf[s].getEnd() < i)
+      if (feature.getEnd() < i)
       {
         continue;
       }
 
-      if (sf[s].getBegin() > j)
+      if (feature.getBegin() > j)
       {
-        sf[s].setBegin(copy.getBegin() - cSize);
-        sf[s].setEnd(copy.getEnd() - cSize);
+        int newBegin = copy.getBegin() - cSize;
+        int newEnd = copy.getEnd() - cSize;
+        SequenceFeature newSf = new SequenceFeature(feature, newBegin,
+                newEnd, feature.getFeatureGroup(), feature.getScore());
+        sequence.deleteFeature(feature);
+        sequence.addSequenceFeature(newSf);
+        // feature.setBegin(newBegin);
+        // feature.setEnd(newEnd);
         continue;
       }
 
-      if (sf[s].getBegin() >= i)
+      int newBegin = feature.getBegin();
+      int newEnd = feature.getEnd();
+      if (newBegin >= i)
       {
-        sf[s].setBegin(i);
+        newBegin = i;
+        // feature.setBegin(i);
       }
 
-      if (sf[s].getEnd() < j)
+      if (newEnd < j)
       {
-        sf[s].setEnd(j - 1);
+        newEnd = j - 1;
+        // feature.setEnd(j - 1);
       }
+      newEnd = newEnd - cSize;
+      // feature.setEnd(feature.getEnd() - (cSize));
 
-      sf[s].setEnd(sf[s].getEnd() - (cSize));
-
-      if (sf[s].getBegin() > sf[s].getEnd())
+      sequence.deleteFeature(feature);
+      if (newEnd >= newBegin)
       {
-        sequence.deleteFeature(sf[s]);
+        sequence.addSequenceFeature(new SequenceFeature(feature, newBegin,
+                newEnd, feature.getFeatureGroup(), feature.getScore()));
       }
+      // if (feature.getBegin() > feature.getEnd())
+      // {
+      // sequence.deleteFeature(feature);
+      // }
     }
 
     if (command.editedFeatures == null)
     {
-      command.editedFeatures = new Hashtable<SequenceI, SequenceFeature[]>();
+      command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
     }
 
     command.editedFeatures.put(seq, oldsf);
@@ -1298,7 +1318,7 @@ public class EditCommand implements CommandI
 
     Hashtable<String, Annotation[]> deletedAnnotations;
 
-    Hashtable<SequenceI, SequenceFeature[]> editedFeatures;
+    Hashtable<SequenceI, List<SequenceFeature>> editedFeatures;
 
     AlignmentI al;
 
@@ -1314,51 +1334,51 @@ public class EditCommand implements CommandI
 
     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.gapChar = align.getGapCharacter();
+      this.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      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)
+    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.command = cmd;
+      this.seqs = sqs;
+      this.position = pos;
+      this.number = count;
+      this.al = align;
+      this.gapChar = align.getGapCharacter();
+      string = new char[sqs.length][];
+      for (int i = 0; i < sqs.length; i++)
       {
         string[i] = replace.toCharArray();
       }
 
-      fullAlignmentHeight = (al.getHeight() == seqs.length);
+      fullAlignmentHeight = (align.getHeight() == sqs.length);
     }
 
     public SequenceI[] getSequences()
index bc7f212..5c1f403 100644 (file)
@@ -232,91 +232,66 @@ public class AlignViewController implements AlignViewControllerI
   static int findColumnsWithFeature(String featureType,
           SequenceCollectionI sqcol, BitSet bs)
   {
-    final int startPosition = sqcol.getStartRes() + 1; // converted to base 1
-    final int endPosition = sqcol.getEndRes() + 1;
+    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);
+
+        if (!sfs.isEmpty())
         {
-          int ist = sq.findIndex(sq.getStart());
-          int iend = sq.findIndex(sq.getEnd());
-          if (iend < startPosition || ist > endPosition)
-          {
-            // sequence not in region
-            continue;
-          }
-          for (SequenceFeature sf : sfs)
+          nseq++;
+        }
+
+        for (SequenceFeature sf : sfs)
+        {
+          int sfStartCol = sq.findIndex(sf.getBegin());
+          int sfEndCol = sq.findIndex(sf.getEnd());
+
+          if (sf.isContactFeature())
           {
-            // future functionality - featureType == null means mark columns
-            // containing all displayed features
-            if (sf != null && (featureType.equals(sf.getType())))
+            /*
+             * '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
+          }
         }
       }
     }
index 90bdcae..f5e6fc7 100755 (executable)
@@ -54,11 +54,7 @@ public class Alignment implements AlignmentI
 
   protected char gapCharacter = '-';
 
-  protected int type = NUCLEOTIDE;
-
-  public static final int PROTEIN = 0;
-
-  public static final int NUCLEOTIDE = 1;
+  private boolean nucleotide = true;
 
   public boolean hasRNAStructure = false;
 
@@ -66,6 +62,8 @@ public class Alignment implements AlignmentI
 
   HiddenSequences hiddenSequences;
 
+  HiddenColumns hiddenCols;
+
   public Hashtable alignmentProperties;
 
   private List<AlignedCodonFrame> codonFrameList;
@@ -74,16 +72,10 @@ public class Alignment implements AlignmentI
   {
     groups = Collections.synchronizedList(new ArrayList<SequenceGroup>());
     hiddenSequences = new HiddenSequences(this);
-    codonFrameList = new ArrayList<AlignedCodonFrame>();
+    hiddenCols = new HiddenColumns();
+    codonFrameList = new ArrayList<>();
 
-    if (Comparison.isNucleotide(seqs))
-    {
-      type = NUCLEOTIDE;
-    }
-    else
-    {
-      type = PROTEIN;
-    }
+    nucleotide = Comparison.isNucleotide(seqs);
 
     sequences = Collections.synchronizedList(new ArrayList<SequenceI>());
 
@@ -136,7 +128,7 @@ public class Alignment implements AlignmentI
   public Alignment(SeqCigar[] alseqs)
   {
     SequenceI[] seqs = SeqCigar.createAlignmentSequences(alseqs,
-            gapCharacter, new ColumnSelection(), null);
+            gapCharacter, new HiddenColumns(), null);
     initAlignment(seqs);
   }
 
@@ -196,14 +188,7 @@ public class Alignment implements AlignmentI
     return AlignmentUtils.getSequencesByName(this);
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param i
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
+
   @Override
   public SequenceI getSequenceAt(int i)
   {
@@ -217,8 +202,32 @@ public class Alignment implements AlignmentI
     return null;
   }
 
+  @Override
+  public SequenceI getSequenceAtAbsoluteIndex(int i)
+  {
+    SequenceI seq = null;
+    if (getHiddenSequences().getSize() > 0)
+    {
+      seq = getHiddenSequences().getHiddenSequence(i);
+      if (seq == null)
+      {
+        // didn't find the sequence in the hidden sequences, get it from the
+        // alignment
+        int index = getHiddenSequences().findIndexWithoutHiddenSeqs(i);
+        seq = getSequenceAt(index);
+      }
+    }
+    else
+    {
+      seq = getSequenceAt(i);
+    }
+    return seq;
+  }
+
   /**
-   * Adds a sequence to the alignment. Recalculates maxLength and size.
+   * Adds a sequence to the alignment. Recalculates maxLength and size. Note
+   * this currently does not recalculate whether or not the alignment is
+   * nucleotide, so mixed alignments may have undefined behaviour.
    * 
    * @param snew
    */
@@ -329,30 +338,21 @@ public class Alignment implements AlignmentI
     }
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param s
-   *          DOCUMENT ME!
-   */
   @Override
   public void deleteSequence(SequenceI s)
   {
-    deleteSequence(findIndex(s));
+    synchronized (sequences)
+    {
+      deleteSequence(findIndex(s));
+    }
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param i
-   *          DOCUMENT ME!
-   */
   @Override
   public void deleteSequence(int i)
   {
-    if (i > -1 && i < getHeight())
+    synchronized (sequences)
     {
-      synchronized (sequences)
+      if (i > -1 && i < getHeight())
       {
         sequences.remove(i);
         hiddenSequences.adjustHeightSequenceDeleted(i);
@@ -360,6 +360,18 @@ public class Alignment implements AlignmentI
     }
   }
 
+  @Override
+  public void deleteHiddenSequence(int i)
+  {
+    synchronized (sequences)
+    {
+      if (i > -1 && i < getHeight())
+      {
+        sequences.remove(i);
+      }
+    }
+  }
+
   /*
    * (non-Javadoc)
    * 
@@ -393,7 +405,7 @@ public class Alignment implements AlignmentI
   @Override
   public SequenceGroup[] findAllGroups(SequenceI s)
   {
-    ArrayList<SequenceGroup> temp = new ArrayList<SequenceGroup>();
+    ArrayList<SequenceGroup> temp = new ArrayList<>();
 
     synchronized (groups)
     {
@@ -444,7 +456,7 @@ public class Alignment implements AlignmentI
             return;
           }
         }
-        sg.setContext(this);
+        sg.setContext(this, true);
         groups.add(sg);
       }
     }
@@ -521,7 +533,7 @@ public class Alignment implements AlignmentI
       }
       for (SequenceGroup sg : groups)
       {
-        sg.setContext(null);
+        sg.setContext(null, false);
       }
       groups.clear();
     }
@@ -537,7 +549,7 @@ public class Alignment implements AlignmentI
       {
         removeAnnotationForGroup(g);
         groups.remove(g);
-        g.setContext(null);
+        g.setContext(null, false);
       }
     }
   }
@@ -677,22 +689,19 @@ public class Alignment implements AlignmentI
     return -1;
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
+
   @Override
   public int getHeight()
   {
     return sequences.size();
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
+  @Override
+  public int getAbsoluteHeight()
+  {
+    return sequences.size() + getHiddenSequences().getSize();
+  }
+
   @Override
   public int getWidth()
   {
@@ -778,6 +787,12 @@ public class Alignment implements AlignmentI
     return true;
   }
 
+  @Override
+  public boolean isHidden(int alignmentIndex)
+  {
+    return (getHiddenSequences().getHiddenSequence(alignmentIndex) != null);
+  }
+
   /**
    * Delete all annotations, including auto-calculated if the flag is set true.
    * Returns true if at least one annotation was deleted, else false.
@@ -978,29 +993,9 @@ public class Alignment implements AlignmentI
   }
 
   @Override
-  public void setNucleotide(boolean b)
-  {
-    if (b)
-    {
-      type = NUCLEOTIDE;
-    }
-    else
-    {
-      type = PROTEIN;
-    }
-  }
-
-  @Override
   public boolean isNucleotide()
   {
-    if (type == NUCLEOTIDE)
-    {
-      return true;
-    }
-    else
-    {
-      return false;
-    }
+    return nucleotide;
   }
 
   @Override
@@ -1019,6 +1014,10 @@ public class Alignment implements AlignmentI
     }
     else if (dataset == null && data != null)
     {
+      if (data == this)
+      {
+        throw new IllegalArgumentException("Circular dataset reference");
+      }
       if (!(data instanceof Alignment))
       {
         throw new Error(
@@ -1068,21 +1067,18 @@ public class Alignment implements AlignmentI
         currentSeq = currentSeq.createDatasetSequence();
       }
     }
-    if (seqs.contains(currentSeq))
-    {
-      return;
-    }
-    List<SequenceI> toProcess = new ArrayList<SequenceI>();
+
+    List<SequenceI> toProcess = new ArrayList<>();
     toProcess.add(currentSeq);
     while (toProcess.size() > 0)
     {
       // use a queue ?
       SequenceI curDs = toProcess.remove(0);
-      if (seqs.contains(curDs))
+
+      if (!seqs.add(curDs))
       {
         continue;
       }
-      seqs.add(curDs);
       // iterate over database references, making sure we add forward referenced
       // sequences
       if (curDs.getDBRefs() != null)
@@ -1125,7 +1121,7 @@ public class Alignment implements AlignmentI
       return;
     }
     // try to avoid using SequenceI.equals at this stage, it will be expensive
-    Set<SequenceI> seqs = new LinkedIdentityHashSet<SequenceI>();
+    Set<SequenceI> seqs = new LinkedIdentityHashSet<>();
 
     for (int i = 0; i < getHeight(); i++)
     {
@@ -1331,6 +1327,12 @@ public class Alignment implements AlignmentI
   }
 
   @Override
+  public HiddenColumns getHiddenColumns()
+  {
+    return hiddenCols;
+  }
+
+  @Override
   public CigarArray getCompactAlignment()
   {
     synchronized (sequences)
@@ -1404,7 +1406,7 @@ public class Alignment implements AlignmentI
     {
       return null;
     }
-    List<AlignedCodonFrame> cframes = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> cframes = new ArrayList<>();
     for (AlignedCodonFrame acf : getCodonFrames())
     {
       if (acf.involvesSequence(seq))
@@ -1481,7 +1483,7 @@ public class Alignment implements AlignmentI
     if (sqs != null)
     {
       // avoid self append deadlock by
-      List<SequenceI> toappendsq = new ArrayList<SequenceI>();
+      List<SequenceI> toappendsq = new ArrayList<>();
       synchronized (sqs)
       {
         for (SequenceI addedsq : sqs)
@@ -1591,7 +1593,6 @@ public class Alignment implements AlignmentI
           String calcId, boolean autoCalc, SequenceI seqRef,
           SequenceGroup groupRef)
   {
-    assert (name != null);
     if (annotations != null)
     {
       for (AlignmentAnnotation annot : getAlignmentAnnotation())
@@ -1623,33 +1624,35 @@ public class Alignment implements AlignmentI
   @Override
   public Iterable<AlignmentAnnotation> findAnnotation(String calcId)
   {
-    ArrayList<AlignmentAnnotation> aa = new ArrayList<AlignmentAnnotation>();
-    for (AlignmentAnnotation a : getAlignmentAnnotation())
+    List<AlignmentAnnotation> aa = new ArrayList<>();
+    AlignmentAnnotation[] alignmentAnnotation = getAlignmentAnnotation();
+    if (alignmentAnnotation != null)
     {
-      if (a.getCalcId() == calcId
-              || (a.getCalcId() != null && calcId != null && a.getCalcId()
-                      .equals(calcId)))
+      for (AlignmentAnnotation a : alignmentAnnotation)
       {
-        aa.add(a);
+        if (a.getCalcId() == calcId
+                || (a.getCalcId() != null && calcId != null && a
+                        .getCalcId().equals(calcId)))
+        {
+          aa.add(a);
+        }
       }
     }
     return aa;
   }
 
-  /**
-   * Returns an iterable collection of any annotations that match on given
-   * sequence ref, calcId and label (ignoring null values).
-   */
   @Override
   public Iterable<AlignmentAnnotation> findAnnotations(SequenceI seq,
           String calcId, String label)
   {
-    ArrayList<AlignmentAnnotation> aa = new ArrayList<AlignmentAnnotation>();
+    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
     for (AlignmentAnnotation ann : getAlignmentAnnotation())
     {
-      if (ann.getCalcId() != null && ann.getCalcId().equals(calcId)
-              && ann.sequenceRef != null && ann.sequenceRef == seq
-              && ann.label != null && ann.label.equals(label))
+      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);
       }
@@ -1847,7 +1850,7 @@ public class Alignment implements AlignmentI
   @Override
   public Set<String> getSequenceNames()
   {
-    Set<String> names = new HashSet<String>();
+    Set<String> names = new HashSet<>();
     for (SequenceI seq : getSequences())
     {
       names.add(seq.getName());
@@ -1947,4 +1950,10 @@ public class Alignment implements AlignmentI
     }
     return new int[] { startPos, endPos };
   }
+
+  @Override
+  public void setHiddenColumns(HiddenColumns cols)
+  {
+    hiddenCols = cols;
+  }
 }
index bbd3ce4..56bfd74 100755 (executable)
@@ -96,14 +96,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 +113,6 @@ public class AlignmentAnnotation
     {
       return;
     }
-    Rna.HelixMap(_rnasecstr);
-    // setRNAStruc(RNAannot);
 
     if (_rnasecstr != null && _rnasecstr.length > 0)
     {
@@ -273,12 +270,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.
    * 
@@ -867,6 +858,10 @@ public class AlignmentAnnotation
   @Override
   public String toString()
   {
+    if (annotations == null)
+    {
+      return "";
+    }
     StringBuilder buffer = new StringBuilder(256);
 
     for (int i = 0; i < annotations.length; i++)
@@ -1146,14 +1141,14 @@ public class AlignmentAnnotation
    * @param colSel
    */
   public AlignmentAnnotation(AlignmentAnnotation alignmentAnnotation,
-          ColumnSelection colSel)
+          HiddenColumns hidden)
   {
     this(alignmentAnnotation);
     if (annotations == null)
     {
       return;
     }
-    colSel.makeVisibleAnnotation(this);
+    hidden.makeVisibleAnnotation(this);
   }
 
   public void setPadGaps(boolean padgaps, char gapchar)
@@ -1490,4 +1485,13 @@ public class AlignmentAnnotation
   {
     return counter++;
   }
+
+  /**
+   * 
+   * @return true for rows that have a range of values in their annotation set
+   */
+  public boolean isQuantitative()
+  {
+    return graphMin < graphMax;
+  }
 }
index 2df099a..2e61f9d 100755 (executable)
@@ -31,13 +31,22 @@ import java.util.Set;
 public interface AlignmentI extends AnnotatedCollectionI
 {
   /**
-   * Calculates the number of sequences in an alignment
+   * Calculates the number of sequences in an alignment, excluding hidden
+   * sequences
    * 
    * @return Number of sequences in alignment
    */
   int getHeight();
 
   /**
+   * Calculates the number of sequences in an alignment, including hidden
+   * sequences
+   * 
+   * @return Number of sequences in alignment
+   */
+  int getAbsoluteHeight();
+
+  /**
    * 
    * Calculates the maximum width of the alignment, including gaps.
    * 
@@ -65,6 +74,15 @@ public interface AlignmentI extends AnnotatedCollectionI
   boolean isAligned(boolean includeHidden);
 
   /**
+   * Answers if the sequence at alignmentIndex is hidden
+   * 
+   * @param alignmentIndex
+   *          the index to check
+   * @return true if the sequence is hidden
+   */
+  boolean isHidden(int alignmentIndex);
+
+  /**
    * Gets sequences as a Synchronized collection
    * 
    * @return All sequences in alignment.
@@ -90,6 +108,17 @@ public interface AlignmentI extends AnnotatedCollectionI
   SequenceI getSequenceAt(int i);
 
   /**
+   * Find a specific sequence in this alignment.
+   * 
+   * @param i
+   *          Index of required sequence in full alignment, i.e. if all columns
+   *          were visible
+   * 
+   * @return SequenceI at given index.
+   */
+  SequenceI getSequenceAtAbsoluteIndex(int i);
+
+  /**
    * Returns a map of lists of sequences keyed by sequence name.
    * 
    * @return
@@ -118,7 +147,9 @@ public interface AlignmentI extends AnnotatedCollectionI
   SequenceI replaceSequenceAt(int i, SequenceI seq);
 
   /**
-   * Deletes a sequence from the alignment
+   * Deletes a sequence from the alignment. Updates hidden sequences to account
+   * for the removed sequence. Do NOT use this method to delete sequences which
+   * are just hidden.
    * 
    * @param s
    *          Sequence to be deleted.
@@ -126,7 +157,9 @@ public interface AlignmentI extends AnnotatedCollectionI
   void deleteSequence(SequenceI s);
 
   /**
-   * Deletes a sequence from the alignment.
+   * Deletes a sequence from the alignment. Updates hidden sequences to account
+   * for the removed sequence. Do NOT use this method to delete sequences which
+   * are just hidden.
    * 
    * @param i
    *          Index of sequence to be deleted.
@@ -134,6 +167,14 @@ public interface AlignmentI extends AnnotatedCollectionI
   void deleteSequence(int i);
 
   /**
+   * Deletes a sequence in the alignment which has been hidden.
+   * 
+   * @param i
+   *          Index of sequence to be deleted
+   */
+  void deleteHiddenSequence(int i);
+
+  /**
    * Finds sequence in alignment using sequence name as query.
    * 
    * @param name
@@ -157,10 +198,11 @@ public interface AlignmentI extends AnnotatedCollectionI
 
   /**
    * Returns the first group (in the order in which groups were added) that
-   * includes the given sequence and aligned position (base 0), or null if none
-   * found
+   * includes the given sequence instance and aligned position (base 0), or null
+   * if none found
    * 
    * @param seq
+   *          - must be contained in the alignment (not a dataset sequence)
    * @param position
    * 
    * @return
@@ -285,13 +327,6 @@ public interface AlignmentI extends AnnotatedCollectionI
   char getGapCharacter();
 
   /**
-   * Test for all nucleotide alignment
-   * 
-   * @return true if alignment is nucleotide sequence
-   */
-  boolean isNucleotide();
-
-  /**
    * Test if alignment contains RNA structure
    * 
    * @return true if RNA structure AligmnentAnnotation was added to alignment
@@ -299,12 +334,6 @@ public interface AlignmentI extends AnnotatedCollectionI
   boolean hasRNAStructure();
 
   /**
-   * Set alignment to be a nucleotide sequence
-   * 
-   */
-  void setNucleotide(boolean b);
-
-  /**
    * Get the associated dataset for the alignment.
    * 
    * @return Alignment containing dataset sequences or null of this is a
@@ -329,6 +358,8 @@ public interface AlignmentI extends AnnotatedCollectionI
 
   HiddenSequences getHiddenSequences();
 
+  HiddenColumns getHiddenColumns();
+
   /**
    * Compact representation of alignment
    * 
@@ -557,4 +588,7 @@ public interface AlignmentI extends AnnotatedCollectionI
    * @return
    */
   public int[] getVisibleStartAndEndIndex(List<int[]> hiddenCols);
+
+  public void setHiddenColumns(HiddenColumns cols);
+
 }
index 9db9f38..9ca70f2 100644 (file)
@@ -141,13 +141,14 @@ public class AlignmentView
    *          the view
    */
   public AlignmentView(AlignmentI alignment,
-          ColumnSelection columnSelection, SequenceGroup selection,
+ HiddenColumns hidden,
+          SequenceGroup selection,
           boolean hasHiddenColumns, boolean selectedRegionOnly,
           boolean recordGroups)
   {
     // refactored from AlignViewport.getAlignmentView(selectedOnly);
     this(new jalview.datamodel.CigarArray(alignment,
-            (hasHiddenColumns ? columnSelection : null),
+            (hasHiddenColumns ? hidden : null),
             (selectedRegionOnly ? selection : null)),
             (selectedRegionOnly && selection != null) ? selection
                     .getStartRes() : 0);
@@ -330,13 +331,13 @@ public class AlignmentView
    *          char
    * @return Object[] { SequenceI[], ColumnSelection}
    */
-  public Object[] getAlignmentAndColumnSelection(char gapCharacter)
+  public Object[] getAlignmentAndHiddenColumns(char gapCharacter)
   {
-    ColumnSelection colsel = new ColumnSelection();
+    HiddenColumns hidden = new HiddenColumns();
 
     return new Object[] {
-        SeqCigar.createAlignmentSequences(sequences, gapCharacter, colsel,
-                contigs), colsel };
+        SeqCigar.createAlignmentSequences(sequences, gapCharacter, hidden,
+                contigs), hidden };
   }
 
   /**
@@ -710,7 +711,8 @@ public class AlignmentView
     if (contigs != null && contigs.length > 0)
     {
       SequenceI[] alignment = new SequenceI[sequences.length];
-      ColumnSelection columnselection = new ColumnSelection();
+      // ColumnSelection columnselection = new ColumnSelection();
+      HiddenColumns hidden = new HiddenColumns();
       if (contigs != null && contigs.length > 0)
       {
         int start = 0;
@@ -823,7 +825,7 @@ public class AlignmentView
             }
           }
           // mark hidden segment as hidden in the new alignment
-          columnselection.hideColumns(nwidth, nwidth + contigs[contig + 2]
+          hidden.hideColumns(nwidth, nwidth + contigs[contig + 2]
                   - 1);
           nwidth += contigs[contig + 2];
         }
@@ -901,7 +903,7 @@ public class AlignmentView
           }
         }
       }
-      return new Object[] { alignment, columnselection };
+      return new Object[] { alignment, hidden };
     }
     else
     {
@@ -916,11 +918,11 @@ public class AlignmentView
       }
       if (nvismsa[0] != null)
       {
-        return new Object[] { nvismsa[0], new ColumnSelection() };
+        return new Object[] { nvismsa[0], new HiddenColumns() };
       }
       else
       {
-        return getAlignmentAndColumnSelection(gapCharacter);
+        return getAlignmentAndHiddenColumns(gapCharacter);
       }
     }
   }
@@ -970,14 +972,14 @@ public class AlignmentView
       if (start < fwidth)
       {
         viscontigs[nvis] = start;
-        viscontigs[nvis + 1] = fwidth; // end is inclusive
+        viscontigs[nvis + 1] = fwidth - 1; // end is inclusive
         nvis += 2;
       }
       return viscontigs;
     }
     else
     {
-      return new int[] { 0, width };
+      return new int[] { 0, width - 1 };
     }
   }
 
@@ -1135,7 +1137,7 @@ public class AlignmentView
   }
 
   public static void testSelectionViews(AlignmentI alignment,
-          ColumnSelection csel, SequenceGroup selection)
+          HiddenColumns hidden, SequenceGroup selection)
   {
     System.out.println("Testing standard view creation:\n");
     AlignmentView view = null;
@@ -1143,7 +1145,7 @@ public class AlignmentView
     {
       System.out
               .println("View with no hidden columns, no limit to selection, no groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, false, false,
+      view = new AlignmentView(alignment, hidden, selection, false, false,
               false);
       summariseAlignmentView(view, System.out);
 
@@ -1157,7 +1159,7 @@ public class AlignmentView
     {
       System.out
               .println("View with no hidden columns, no limit to selection, and all groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, false, false,
+      view = new AlignmentView(alignment, hidden, selection, false, false,
               true);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
@@ -1170,7 +1172,7 @@ public class AlignmentView
     {
       System.out
               .println("View with no hidden columns, limited to selection and no groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, false, true,
+      view = new AlignmentView(alignment, hidden, selection, false, true,
               false);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
@@ -1183,7 +1185,7 @@ public class AlignmentView
     {
       System.out
               .println("View with no hidden columns, limited to selection, and all groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, false, true,
+      view = new AlignmentView(alignment, hidden, selection, false, true,
               true);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
@@ -1196,7 +1198,7 @@ public class AlignmentView
     {
       System.out
               .println("View *with* hidden columns, no limit to selection, no groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, true, false,
+      view = new AlignmentView(alignment, hidden, selection, true, false,
               false);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
@@ -1209,7 +1211,7 @@ public class AlignmentView
     {
       System.out
               .println("View *with* hidden columns, no limit to selection, and all groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, true, false,
+      view = new AlignmentView(alignment, hidden, selection, true, false,
               true);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
@@ -1222,7 +1224,7 @@ public class AlignmentView
     {
       System.out
               .println("View *with* hidden columns, limited to selection and no groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, true, true,
+      view = new AlignmentView(alignment, hidden, selection, true, true,
               false);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
@@ -1235,7 +1237,8 @@ public class AlignmentView
     {
       System.out
               .println("View *with* hidden columns, limited to selection, and all groups to be collected:");
-      view = new AlignmentView(alignment, csel, selection, true, true, true);
+      view = new AlignmentView(alignment, hidden, selection, true, true,
+              true);
       summariseAlignmentView(view, System.out);
     } catch (Exception e)
     {
diff --git a/src/jalview/datamodel/AllColsCollection.java b/src/jalview/datamodel/AllColsCollection.java
new file mode 100644 (file)
index 0000000..f84ba95
--- /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.datamodel;
+
+import jalview.api.AlignmentColsCollectionI;
+
+import java.util.Iterator;
+
+public class AllColsCollection implements AlignmentColsCollectionI
+{
+  int start;
+  int end;
+
+  HiddenColumns hidden;
+  
+  public AllColsCollection(int s, int e, AlignmentI al)
+  {
+    start = s;
+    end = e;
+    hidden = al.getHiddenColumns();
+  }
+  
+  @Override
+  public Iterator<Integer> iterator()
+  {
+    return new AllColsIterator(start,end,hidden);
+  }
+
+  @Override
+  public boolean isHidden(int c)
+  {
+    return !hidden.isVisible(c);
+  }
+}
diff --git a/src/jalview/datamodel/AllColsIterator.java b/src/jalview/datamodel/AllColsIterator.java
new file mode 100644 (file)
index 0000000..c1296d5
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+/**
+ * An iterator which iterates over all columns or rows in an alignment, whether
+ * hidden or visible.
+ * 
+ * @author kmourao
+ *
+ */
+public class AllColsIterator implements Iterator<Integer>
+{
+  private int last;
+
+  private int next;
+
+  private int current;
+
+  public AllColsIterator(int firstcol, int lastcol, HiddenColumns hiddenCols)
+  {
+    last = lastcol;
+    next = firstcol;
+    current = firstcol;
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return next <= last;
+  }
+
+  @Override
+  public Integer next()
+  {
+    if (next > last)
+    {
+      throw new NoSuchElementException();
+    }
+    current = next;
+    next++;
+
+    return current;
+  }
+
+  @Override
+  public void remove()
+  {
+    throw new UnsupportedOperationException();
+  }
+}
+
diff --git a/src/jalview/datamodel/AllRowsCollection.java b/src/jalview/datamodel/AllRowsCollection.java
new file mode 100644 (file)
index 0000000..502ace4
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import jalview.api.AlignmentRowsCollectionI;
+
+import java.util.Iterator;
+
+public class AllRowsCollection implements AlignmentRowsCollectionI
+{
+  int start;
+
+  int end;
+
+  AlignmentI alignment;
+
+  HiddenSequences hidden;
+
+  public AllRowsCollection(int s, int e, AlignmentI al)
+  {
+    start = s;
+    end = e;
+    alignment = al;
+    hidden = al.getHiddenSequences();
+  }
+
+  @Override
+  public Iterator<Integer> iterator()
+  {
+    return new AllRowsIterator(start, end, alignment);
+  }
+
+  @Override
+  public boolean isHidden(int seq)
+  {
+    return hidden.isHidden(seq);
+  }
+
+  @Override
+  public SequenceI getSequence(int seq)
+  {
+    return alignment.getSequenceAtAbsoluteIndex(seq);
+  }
+}
+
diff --git a/src/jalview/datamodel/AllRowsIterator.java b/src/jalview/datamodel/AllRowsIterator.java
new file mode 100644 (file)
index 0000000..b6d45f8
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+/**
+ * An iterator which iterates over all columns or rows in an alignment, whether
+ * hidden or visible.
+ * 
+ * @author kmourao
+ *
+ */
+public class AllRowsIterator implements Iterator<Integer>
+{
+  private int last;
+
+  private int next;
+
+  private int current;
+
+  private AlignmentI al;
+
+  public AllRowsIterator(int firstrow, int lastrow, AlignmentI alignment)
+  {
+    last = lastrow;
+    current = firstrow;
+    next = firstrow;
+    al = alignment;
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return next <= last;
+  }
+
+  @Override
+  public Integer next()
+  {
+    if (next > last)
+    {
+      throw new NoSuchElementException();
+    }
+    current = next;
+    next++;
+
+    return current;
+  }
+
+  @Override
+  public void remove()
+  {
+    throw new UnsupportedOperationException();
+  }
+}
+
+
index 74568e4..2963fd5 100644 (file)
@@ -31,8 +31,26 @@ public interface AnnotatedCollectionI extends SequenceCollectionI
    */
   AlignmentAnnotation[] getAlignmentAnnotation();
 
+  /**
+   * Returns a list of annotations matching the given calc id, or an empty list
+   * if calcId is null
+   * 
+   * @param calcId
+   * @return
+   */
   Iterable<AlignmentAnnotation> findAnnotation(String calcId);
 
+  /**
+   * Returns an iterable collection of any annotations that match on given
+   * sequence ref, calcId and label (ignoring null values).
+   * 
+   * @param seq
+   *          null or reference sequence to select annotation for
+   * @param calcId
+   *          null or the calcId to select annotation for
+   * @param label
+   *          null or the label to select annotation for
+   */
   Iterable<AlignmentAnnotation> findAnnotations(SequenceI seq,
           String calcId, String label);
 
index 71ebbb3..8de8eb2 100755 (executable)
@@ -30,6 +30,12 @@ import java.awt.Color;
  */
 public class Annotation
 {
+  /**
+   * the empty annotation - proxy for null entries in annotation row
+   */
+  public static final Annotation EMPTY_ANNOTATION = new Annotation("", "",
+          ' ', 0f);
+
   /** Character label - also shown below histogram */
   public String displayCharacter = "";
 
@@ -192,4 +198,18 @@ public class Annotation
     }
     return sb.toString();
   }
+
+  /**
+   * @return true if annot is 'whitespace' annotation (zero score, whitespace or
+   *         zero length display character, label, description
+   */
+  public boolean isWhitespace()
+  {
+    return ((value == 0f)
+            && ((description == null) || (description.trim()
+                    .length() == 0))
+            && ((displayCharacter == null) || (displayCharacter
+                    .trim().length() == 0))
+            && (secondaryStructure == '\0' || (secondaryStructure == ' ')) && colour == null);
+  }
 }
index ed57dce..b7e15a6 100755 (executable)
@@ -20,8 +20,8 @@
  */
 package jalview.datamodel;
 
+import jalview.analysis.scoremodels.ScoreMatrix;
 import jalview.schemes.ResidueProperties;
-import jalview.schemes.ScoreMatrix;
 
 /**
  * Encode a sequence as a numeric vector using either classic residue binary
@@ -69,13 +69,9 @@ public class BinarySequence extends Sequence
   {
     int nores = (isNa) ? ResidueProperties.maxNucleotideIndex
             : ResidueProperties.maxProteinIndex;
-    // Set all matrix to 0
+
     dbinary = new double[getSequence().length * nores];
 
-    for (int i = 0; i < dbinary.length; i++)
-    {
-      dbinary[i] = 0.0;
-    }
     return nores;
   }
 
@@ -116,32 +112,26 @@ public class BinarySequence extends Sequence
   /**
    * ancode using substitution matrix given in matrix
    * 
-   * @param matrix
+   * @param smtrx
    */
-  public void matrixEncode(final ScoreMatrix matrix)
+  public void matrixEncode(final ScoreMatrix smtrx)
           throws InvalidSequenceTypeException
   {
-    if (isNa != matrix.isDNA())
+    if (isNa != smtrx.isDNA())
     {
       throw new InvalidSequenceTypeException("matrix "
-              + matrix.getClass().getCanonicalName()
+              + smtrx.getClass().getCanonicalName()
               + " is not a valid matrix for "
               + (isNa ? "nucleotide" : "protein") + "sequences");
     }
-    matrixEncode(matrix.isDNA() ? ResidueProperties.nucleotideIndex
-            : ResidueProperties.aaIndex, matrix.getMatrix());
+    matrixEncode(smtrx.isDNA() ? ResidueProperties.nucleotideIndex
+            : ResidueProperties.aaIndex, smtrx.getMatrix());
   }
 
-  private void matrixEncode(final int[] aaIndex, final int[][] matrix)
+  private void matrixEncode(final int[] aaIndex, final float[][] matrix)
   {
-    // Set all matrix to 0
-    // dbinary = new double[getSequence().length * 21];
-
     int nores = initMatrixGetNoRes();
 
-    // for (int i = 0; i < dbinary.length; i++) {
-    // dbinary[i] = 0.0;
-    // }
     for (int i = 0, iSize = getSequence().length; i < iSize; i++)
     {
       int aanum = nores - 1;
index f6e5862..837a10b 100644 (file)
@@ -85,12 +85,12 @@ public class CigarArray extends CigarBase
    * @param columnSelection
    * @param selectionGroup
    */
-  public CigarArray(AlignmentI alignment, ColumnSelection columnSelection,
+  public CigarArray(AlignmentI alignment, HiddenColumns hidden,
           SequenceGroup selectionGroup)
   {
     this(constructSeqCigarArray(alignment, selectionGroup));
     constructFromAlignment(alignment,
-            columnSelection != null ? columnSelection.getHiddenColumns()
+            hidden != null ? hidden.getHiddenRegions()
                     : null, selectionGroup);
   }
 
@@ -218,7 +218,7 @@ public class CigarArray extends CigarBase
   }
 
   /**
-   * @see Cigar.getSequenceAndDeletions
+   * @see CigarBase.getSequenceAndDeletions
    * @param GapChar
    *          char
    * @return Object[][]
index c467f4a..eb2d174 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.datamodel;
 
-import jalview.util.Comparison;
-import jalview.util.ShiftList;
 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter.SearchableAnnotationField;
 
@@ -29,7 +27,6 @@ import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.List;
-import java.util.Vector;
 
 /**
  * Data class holding the selected columns and hidden column ranges for a view.
@@ -268,12 +265,6 @@ public class ColumnSelection
 
   IntList selection = new IntList();
 
-  /*
-   * list of hidden column [start, end] ranges; the list is maintained in
-   * ascending start column order
-   */
-  Vector<int[]> hiddenColumns;
-
   /**
    * Add a column to the selection
    * 
@@ -361,986 +352,126 @@ public class ColumnSelection
     return (col > -1) ? selection.isSelected(col) : false;
   }
 
-  /**
-   * Answers true if no columns are selected, else false
-   */
-  public boolean isEmpty()
-  {
-    return selection == null || selection.isEmpty();
-  }
-
-  /**
-   * rightmost selected column
-   * 
-   * @return rightmost column in alignment that is selected
-   */
-  public int getMax()
-  {
-    if (selection.isEmpty())
-    {
-      return -1;
-    }
-    return selection.getMaxColumn();
-  }
-
-  /**
-   * Leftmost column in selection
-   * 
-   * @return column index of leftmost column in selection
-   */
-  public int getMin()
-  {
-    if (selection.isEmpty())
-    {
-      return 1000000000;
-    }
-    return selection.getMinColumn();
-  }
-
-  /**
-   * propagate shift in alignment columns to column selection
-   * 
-   * @param start
-   *          beginning of edit
-   * @param left
-   *          shift in edit (+ve for removal, or -ve for inserts)
-   */
-  public List<int[]> compensateForEdit(int start, int change)
-  {
-    List<int[]> deletedHiddenColumns = null;
-    selection.compensateForEdits(start, change);
-
-    if (hiddenColumns != null)
-    {
-      deletedHiddenColumns = new ArrayList<int[]>();
-      int hSize = hiddenColumns.size();
-      for (int i = 0; i < hSize; i++)
-      {
-        int[] region = hiddenColumns.elementAt(i);
-        if (region[0] > start && start + change > region[1])
-        {
-          deletedHiddenColumns.add(region);
-
-          hiddenColumns.removeElementAt(i);
-          i--;
-          hSize--;
-          continue;
-        }
-
-        if (region[0] > start)
-        {
-          region[0] -= change;
-          region[1] -= change;
-        }
-
-        if (region[0] < 0)
-        {
-          region[0] = 0;
-        }
-
-      }
-
-      this.revealHiddenColumns(0);
-    }
-
-    return deletedHiddenColumns;
-  }
-
-  /**
-   * 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)
-   */
-  private void compensateForDelEdits(int start, int change)
-  {
-
-    selection.compensateForEdits(start, change);
-
-    if (hiddenColumns != null)
-    {
-      for (int i = 0; i < hiddenColumns.size(); i++)
-      {
-        int[] region = hiddenColumns.elementAt(i);
-        if (region[0] >= start)
-        {
-          region[0] -= change;
-        }
-        if (region[1] >= start)
-        {
-          region[1] -= change;
-        }
-        if (region[1] < region[0])
-        {
-          hiddenColumns.removeElementAt(i--);
-        }
-
-        if (region[0] < 0)
-        {
-          region[0] = 0;
-        }
-        if (region[1] < 0)
-        {
-          region[1] = 0;
-        }
-      }
-    }
-  }
-
-  /**
-   * Adjust hidden column boundaries based on a series of column additions or
-   * deletions in visible regions.
-   * 
-   * @param shiftrecord
-   * @return
-   */
-  public 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);
-          // compensateForEdit(shifted+sh[0], sh[1]);
-          compensateForDelEdits(shifted + sh[0], sh[1]);
-          shifted -= sh[1];
-        }
-      }
-      return shiftrecord.getInverse();
-    }
-    return null;
-  }
-
-  /**
-   * removes intersection of position,length ranges in deletions from the
-   * start,end regions marked in intervals.
-   * 
-   * @param shifts
-   * @param intervals
-   * @return
-   */
-  private boolean pruneIntervalVector(final List<int[]> shifts,
-          Vector<int[]> intervals)
-  {
-    boolean pruned = false;
-    int i = 0, j = intervals.size() - 1, s = 0, t = shifts.size() - 1;
-    int hr[] = intervals.elementAt(i);
-    int sr[] = shifts.get(s);
-    while (i <= j && s <= t)
-    {
-      boolean trailinghn = hr[1] >= sr[0];
-      if (!trailinghn)
-      {
-        if (i < j)
-        {
-          hr = intervals.elementAt(++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.removeElementAt(i);
-          pruned = true;
-          j--;
-          if (i <= j)
-          {
-            hr = intervals.elementAt(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(ShiftList deletions)
-  {
-    if (deletions != null)
-    {
-      final List<int[]> shifts = deletions.getShifts();
-      if (shifts != null && shifts.size() > 0)
-      {
-        // delete any intervals intersecting.
-        if (hiddenColumns != null)
-        {
-          pruneIntervalVector(shifts, hiddenColumns);
-          if (hiddenColumns != null && hiddenColumns.size() == 0)
-          {
-            hiddenColumns = null;
-          }
-        }
-        if (selection != null && selection.size() > 0)
-        {
-          selection.pruneColumnList(shifts);
-          if (selection != null && selection.size() == 0)
-          {
-            selection = null;
-          }
-        }
-        // and shift the rest.
-        this.compensateForEdits(deletions);
-      }
-    }
-  }
-
-  /**
-   * This Method is used to return all the HiddenColumn regions
-   * 
-   * @return empty list or List of hidden column intervals
-   */
-  public List<int[]> getHiddenColumns()
-  {
-    return hiddenColumns == null ? Collections.<int[]> emptyList()
-            : hiddenColumns;
-  }
-
-  /**
-   * Return absolute column index for a visible column index
-   * 
-   * @param column
-   *          int column index in alignment view (count from zero)
-   * @return alignment column index for column
-   */
-  public int adjustForHiddenColumns(int column)
-  {
-    int result = column;
-    if (hiddenColumns != null)
-    {
-      for (int i = 0; i < hiddenColumns.size(); i++)
-      {
-        int[] region = hiddenColumns.elementAt(i);
-        if (result >= region[0])
-        {
-          result += region[1] - region[0] + 1;
-        }
-      }
-    }
-    return result;
-  }
-
-  /**
-   * 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.
-   * 
-   * @param hiddenColumn
-   *          int
-   * @return int
-   */
-  public int findColumnPosition(int hiddenColumn)
-  {
-    int result = hiddenColumn;
-    if (hiddenColumns != null)
-    {
-      int index = 0;
-      int[] region;
-      do
-      {
-        region = hiddenColumns.elementAt(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])
-      {
-        return region[0] + hiddenColumn - result;
-      }
-    }
-    return result; // return the shifted position after removing hidden columns.
-  }
-
-  /**
-   * Use this method to determine where the next hiddenRegion starts
-   * 
-   * @param hiddenRegion
-   *          index of hidden region (counts from 0)
-   * @return column number in visible view
-   */
-  public int findHiddenRegionPosition(int hiddenRegion)
-  {
-    int result = 0;
-    if (hiddenColumns != null)
-    {
-      int index = 0;
-      int gaps = 0;
-      do
-      {
-        int[] region = hiddenColumns.elementAt(index);
-        if (hiddenRegion == 0)
-        {
-          return region[0];
-        }
-
-        gaps += region[1] + 1 - region[0];
-        result = region[1] + 1;
-        index++;
-      } while (index <= hiddenRegion);
-
-      result -= gaps;
-    }
-
-    return result;
-  }
-
-  /**
-   * THis method returns the rightmost limit of a region of an alignment with
-   * hidden columns. In otherwords, the next hidden column.
-   * 
-   * @param index
-   *          int
-   */
-  public int getHiddenBoundaryRight(int alPos)
-  {
-    if (hiddenColumns != null)
-    {
-      int index = 0;
-      do
-      {
-        int[] region = hiddenColumns.elementAt(index);
-        if (alPos < region[0])
-        {
-          return region[0];
-        }
-
-        index++;
-      } while (index < hiddenColumns.size());
-    }
-
-    return alPos;
-
-  }
-
-  /**
-   * This method returns the leftmost limit of a region of an alignment with
-   * hidden columns. In otherwords, the previous hidden column.
-   * 
-   * @param index
-   *          int
-   */
-  public int getHiddenBoundaryLeft(int alPos)
-  {
-    if (hiddenColumns != null)
-    {
-      int index = hiddenColumns.size() - 1;
-      do
-      {
-        int[] region = hiddenColumns.elementAt(index);
-        if (alPos > region[1])
-        {
-          return region[1];
-        }
-
-        index--;
-      } while (index > -1);
-    }
-
-    return alPos;
-
-  }
-
-  public void hideSelectedColumns()
-  {
-    synchronized (selection)
-    {
-      for (int[] selregions : selection.getRanges())
-      {
-        hideColumns(selregions[0], selregions[1]);
-      }
-      selection.clear();
-    }
-
-  }
-
-  /**
-   * Adds the specified column range to the hidden columns
-   * 
-   * @param start
-   * @param end
-   */
-  public void hideColumns(int start, int end)
-  {
-    if (hiddenColumns == null)
-    {
-      hiddenColumns = new Vector<int[]>();
-    }
-
-    /*
-     * traverse existing hidden ranges and insert / amend / append as
-     * appropriate
-     */
-    for (int i = 0; i < hiddenColumns.size(); i++)
-    {
-      int[] region = hiddenColumns.elementAt(i);
-
-      if (end < region[0] - 1)
-      {
-        /*
-         * insert discontiguous preceding range
-         */
-        hiddenColumns.insertElementAt(new int[] { start, end }, i);
-        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;
-      }
-    }
-
-    /*
-     * remaining case is that the new range follows everything else
-     */
-    hiddenColumns.addElement(new int[] { start, end });
-  }
-
-  /**
-   * Hides the specified column and any adjacent selected columns
-   * 
-   * @param res
-   *          int
-   */
-  public void hideColumns(int col)
-  {
-    /*
-     * deselect column (whether selected or not!)
-     */
-    removeElement(col);
-
-    /*
-     * find adjacent selected columns
-     */
-    int min = col - 1, max = col + 1;
-    while (contains(min))
-    {
-      removeElement(min);
-      min--;
-    }
-
-    while (contains(max))
-    {
-      removeElement(max);
-      max++;
-    }
-
-    /*
-     * min, max are now the closest unselected columns
-     */
-    min++;
-    max--;
-    if (min > max)
-    {
-      min = max;
-    }
-
-    hideColumns(min, max);
-  }
-
-  /**
-   * Unhides, and adds to the selection list, all hidden columns
-   */
-  public void revealAllHiddenColumns()
-  {
-    if (hiddenColumns != null)
-    {
-      for (int i = 0; i < hiddenColumns.size(); i++)
-      {
-        int[] region = hiddenColumns.elementAt(i);
-        for (int j = region[0]; j < region[1] + 1; j++)
-        {
-          addElement(j);
-        }
-      }
-    }
-
-    hiddenColumns = null;
-  }
-
-  /**
-   * Reveals, and marks as selected, the hidden column range with the given
-   * start column
-   * 
-   * @param start
-   */
-  public void revealHiddenColumns(int start)
-  {
-    for (int i = 0; i < hiddenColumns.size(); i++)
-    {
-      int[] region = hiddenColumns.elementAt(i);
-      if (start == region[0])
-      {
-        for (int j = region[0]; j < region[1] + 1; j++)
-        {
-          addElement(j);
-        }
-
-        hiddenColumns.removeElement(region);
-        break;
-      }
-    }
-    if (hiddenColumns.size() == 0)
-    {
-      hiddenColumns = null;
-    }
-  }
-
-  public boolean isVisible(int column)
-  {
-    if (hiddenColumns != null)
-    {
-      for (int[] region : hiddenColumns)
-      {
-        if (column >= region[0] && column <= region[1])
-        {
-          return false;
-        }
-      }
-    }
-
-    return true;
-  }
-
-  /**
-   * Copy constructor
-   * 
-   * @param copy
-   */
-  public ColumnSelection(ColumnSelection copy)
-  {
-    if (copy != null)
-    {
-      selection = new IntList(copy.selection);
-      if (copy.hiddenColumns != null)
-      {
-        hiddenColumns = new Vector<int[]>(copy.hiddenColumns.size());
-        for (int i = 0, j = copy.hiddenColumns.size(); i < j; i++)
-        {
-          int[] rh, cp;
-          rh = copy.hiddenColumns.elementAt(i);
-          if (rh != null)
-          {
-            cp = new int[rh.length];
-            System.arraycopy(rh, 0, cp, 0, rh.length);
-            hiddenColumns.addElement(cp);
-          }
-        }
-      }
-    }
-  }
-
-  /**
-   * ColumnSelection
-   */
-  public ColumnSelection()
-  {
-  }
-
-  public String[] getVisibleSequenceStrings(int start, int end,
-          SequenceI[] seqs)
-  {
-    int i, iSize = seqs.length;
-    String selections[] = new String[iSize];
-    if (hiddenColumns != null && hiddenColumns.size() > 0)
-    {
-      for (i = 0; i < iSize; i++)
-      {
-        StringBuffer visibleSeq = new StringBuffer();
-        List<int[]> regions = getHiddenColumns();
-
-        int blockStart = start, blockEnd = end;
-        int[] region;
-        int hideStart, hideEnd;
-
-        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));
-        }
+  /**
+   * Answers true if no columns are selected, else false
+   */
+  public boolean isEmpty()
+  {
+    return selection == null || selection.isEmpty();
+  }
 
-        selections[i] = visibleSeq.toString();
-      }
-    }
-    else
+  /**
+   * rightmost selected column
+   * 
+   * @return rightmost column in alignment that is selected
+   */
+  public int getMax()
+  {
+    if (selection.isEmpty())
     {
-      for (i = 0; i < iSize; i++)
-      {
-        selections[i] = seqs[i].getSequenceAsString(start, end);
-      }
+      return -1;
     }
-
-    return selections;
+    return selection.getMaxColumn();
   }
 
   /**
-   * return all visible segments between the given start and end boundaries
+   * Leftmost column in selection
    * 
-   * @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
+   * @return column index of leftmost column in selection
    */
-  public int[] getVisibleContigs(int start, int end)
+  public int getMin()
   {
-    if (hiddenColumns != null && hiddenColumns.size() > 0)
+    if (selection.isEmpty())
     {
-      List<int[]> visiblecontigs = new ArrayList<int[]>();
-      List<int[]> regions = getHiddenColumns();
-
-      int vstart = start;
-      int[] region;
-      int hideStart, hideEnd;
-
-      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;
-      }
+      return 1000000000;
+    }
+    return selection.getMinColumn();
+  }
 
-      if (vstart < end)
-      {
-        visiblecontigs.add(new int[] { vstart, end - 1 });
-      }
-      int[] vcontigs = new int[visiblecontigs.size() * 2];
-      for (int i = 0, j = visiblecontigs.size(); i < j; i++)
+  public void hideSelectedColumns(AlignmentI al)
+  {
+    synchronized (selection)
+    {
+      for (int[] selregions : selection.getRanges())
       {
-        int[] vc = visiblecontigs.get(i);
-        visiblecontigs.set(i, null);
-        vcontigs[i * 2] = vc[0];
-        vcontigs[i * 2 + 1] = vc[1];
+        al.getHiddenColumns().hideColumns(selregions[0], selregions[1]);
       }
-      visiblecontigs.clear();
-      return vcontigs;
-    }
-    else
-    {
-      return new int[] { start, end - 1 };
+      selection.clear();
     }
+
   }
 
+
   /**
-   * 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
+   * Hides the specified column and any adjacent selected columns
    * 
-   * @param seq
-   * @return int[] { visible start, visible end, first seqpos, last seqpos,
-   *         alignment index for seq start, alignment index for seq end }
+   * @param res
+   *          int
    */
-  public int[] locateVisibleBoundsOfSequence(SequenceI seq)
+  public void hideSelectedColumns(int col, HiddenColumns hidden)
   {
-    int fpos = seq.getStart(), lpos = seq.getEnd();
-    int start = 0;
+    /*
+     * deselect column (whether selected or not!)
+     */
+    removeElement(col);
 
-    if (hiddenColumns == null || hiddenColumns.size() == 0)
+    /*
+     * find adjacent selected columns
+     */
+    int min = col - 1, max = col + 1;
+    while (contains(min))
     {
-      int ifpos = seq.findIndex(fpos) - 1, ilpos = seq.findIndex(lpos) - 1;
-      return new int[] { ifpos, ilpos, fpos, lpos, ifpos, ilpos };
+      removeElement(min);
+      min--;
     }
 
-    // Simply walk along the sequence whilst watching for hidden column
-    // boundaries
-    List<int[]> regions = getHiddenColumns();
-    int spos = fpos, lastvispos = -1, rcount = 0, hideStart = seq
-            .getLength(), hideEnd = -1;
-    int visPrev = 0, visNext = 0, firstP = -1, lastP = -1;
-    boolean foundStart = false;
-    for (int p = 0, pLen = seq.getLength(); spos <= seq.getEnd()
-            && p < pLen; p++)
+    while (contains(max))
     {
-      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++;
-      }
+      removeElement(max);
+      max++;
     }
-    if (foundStart)
+
+    /*
+     * min, max are now the closest unselected columns
+     */
+    min++;
+    max--;
+    if (min > max)
     {
-      return new int[] { findColumnPosition(start),
-          findColumnPosition(lastvispos), fpos, lpos, firstP, lastP };
+      min = max;
     }
-    // otherwise, sequence was completely hidden
-    return new int[] { visPrev, visNext, 0, 0, firstP, lastP };
+
+    hidden.hideColumns(min, max);
   }
 
+
+
+
+
   /**
-   * delete any columns in alignmentAnnotation that are hidden (including
-   * sequence associated annotation).
+   * Copy constructor
    * 
-   * @param alignmentAnnotation
+   * @param copy
    */
-  public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
+  public ColumnSelection(ColumnSelection copy)
   {
-    makeVisibleAnnotation(-1, -1, alignmentAnnotation);
+    if (copy != null)
+    {
+      selection = new IntList(copy.selection);
+    }
   }
 
   /**
-   * 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 alignmentAnnotation
-   *          the annotation to operate on
+   * ColumnSelection
    */
-  public void makeVisibleAnnotation(int start, int end,
-          AlignmentAnnotation alignmentAnnotation)
+  public ColumnSelection()
   {
-    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[]>();
-      Annotation[] els = null;
-      List<int[]> regions = getHiddenColumns();
-      int blockStart = start, blockEnd = end;
-      int[] region;
-      int hideStart, hideEnd, 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;
-      }
 
-      alignmentAnnotation.annotations = new Annotation[w];
-      w = 0;
 
-      for (Annotation[] chnk : annels)
-      {
-        System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
-                chnk.length);
-        w += chnk.length;
-      }
-    }
-    else
-    {
-      alignmentAnnotation.restrict(start, end);
-    }
-  }
 
   /**
    * Invert the column selection from first to end-1. leaves hiddenColumns
@@ -1349,9 +480,9 @@ public class ColumnSelection
    * @param first
    * @param end
    */
-  public void invertColumnSelection(int first, int width)
+  public void invertColumnSelection(int first, int width, AlignmentI al)
   {
-    boolean hasHidden = hiddenColumns != null && hiddenColumns.size() > 0;
+    boolean hasHidden = al.getHiddenColumns().hasHidden();
     for (int i = first; i < width; i++)
     {
       if (contains(i))
@@ -1360,7 +491,7 @@ public class ColumnSelection
       }
       else
       {
-        if (!hasHidden || isVisible(i))
+        if (!hasHidden || al.getHiddenColumns().isVisible(i))
         {
           addElement(i);
         }
@@ -1369,195 +500,41 @@ public class ColumnSelection
   }
 
   /**
-   * add in any unselected columns from the given column selection, excluding
-   * any that are hidden.
-   * 
-   * @param colsel
-   */
-  public void addElementsFrom(ColumnSelection colsel)
-  {
-    if (colsel != null && !colsel.isEmpty())
-    {
-      for (Integer col : colsel.getSelected())
-      {
-        if (hiddenColumns != null && isVisible(col.intValue()))
-        {
-          selection.add(col);
-        }
-      }
-    }
-  }
-
-  /**
-   * set the selected columns the given column selection, excluding any columns
-   * that are hidden.
+   * set the selected columns to the given column selection, excluding any
+   * columns that are hidden.
    * 
    * @param colsel
    */
-  public void setElementsFrom(ColumnSelection colsel)
+  public void setElementsFrom(ColumnSelection colsel,
+          HiddenColumns hiddenColumns)
   {
     selection = new IntList();
     if (colsel.selection != null && colsel.selection.size() > 0)
     {
-      if (hiddenColumns != null && hiddenColumns.size() > 0)
+      if (hiddenColumns.hasHidden())
       {
         // only select visible columns in this columns selection
-        addElementsFrom(colsel);
-      }
-      else
-      {
-        // add everything regardless
         for (Integer col : colsel.getSelected())
         {
-          addElement(col);
-        }
-      }
-    }
-  }
-
-  /**
-   * 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 Column selection for new alignment view, with insertions into
-   *         profileseq marked as hidden.
-   */
-  public static ColumnSelection propagateInsertions(SequenceI profileseq,
-          AlignmentI al, AlignmentView input)
-  {
-    int profsqpos = 0;
-
-    // return propagateInsertions(profileseq, al, )
-    char gc = al.getGapCharacter();
-    Object[] alandcolsel = input.getAlignmentAndColumnSelection(gc);
-    ColumnSelection nview = (ColumnSelection) alandcolsel[1];
-    SequenceI origseq = ((SequenceI[]) alandcolsel[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
-   */
-  public 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;
-    // input.pruneDeletions(ShiftList.parseMap(((SequenceI[])
-    // alandcolsel[0])[0].gapMap()))
-    // 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)
+          if (hiddenColumns != null
+                  && hiddenColumns.isVisible(col.intValue()))
           {
-            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));
-            }
+            selection.add(col);
           }
         }
-        // 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++)
+      else
       {
-        SequenceI sqobj = al.getSequenceAt(s);
-        if (sqobj == profileseq)
-        {
-          continue;
-        }
-        String sq = sqobj.getSequenceAsString();
-        // pad sequence
-        int diff = origseq.getLength() - sq.length();
-        while (diff > 0)
+        // add everything regardless
+        for (Integer col : colsel.getSelected())
         {
-          // 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();
+          addElement(col);
         }
       }
     }
   }
 
+
   /**
    * 
    * @return true if there are columns marked
@@ -1567,45 +544,13 @@ public class ColumnSelection
     return (selection != null && selection.size() > 0);
   }
 
-  /**
-   * 
-   * @return true if there are columns hidden
-   */
-  public boolean hasHiddenColumns()
-  {
-    return hiddenColumns != null && hiddenColumns.size() > 0;
-  }
-
-  /**
-   * 
-   * @return true if there are more than one set of columns hidden
-   */
-  public boolean hasManyHiddenColumns()
-  {
-    return hiddenColumns != null && hiddenColumns.size() > 1;
-  }
 
-  /**
-   * mark the columns corresponding to gap characters as hidden in the column
-   * selection
-   * 
-   * @param sr
-   */
-  public void hideInsertionsFor(SequenceI sr)
-  {
-    List<int[]> inserts = sr.getInsertions();
-    for (int[] r : inserts)
-    {
-      hideColumns(r[0], r[1]);
-    }
-  }
 
   public boolean filterAnnotations(Annotation[] annotations,
           AnnotationFilterParameter filterParams)
   {
     // JBPNote - this method needs to be refactored to become independent of
     // viewmodel package
-    this.revealAllHiddenColumns();
     this.clear();
     int count = 0;
     do
@@ -1688,21 +633,12 @@ public class ColumnSelection
   }
 
   /**
-   * Returns a hashCode built from selected columns and hidden column ranges
+   * Returns a hashCode built from selected columns ranges
    */
   @Override
   public int hashCode()
   {
-    int hashCode = selection.hashCode();
-    if (hiddenColumns != null)
-    {
-      for (int[] hidden : hiddenColumns)
-      {
-        hashCode = 31 * hashCode + hidden[0];
-        hashCode = 31 * hashCode + hidden[1];
-      }
-    }
-    return hashCode;
+    return selection.hashCode();
   }
 
   /**
@@ -1733,27 +669,6 @@ public class ColumnSelection
       return false;
     }
 
-    /*
-     * 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())
-    {
-      return false;
-    }
-    int i = 0;
-    for (int[] thisRange : hiddenColumns)
-    {
-      int[] thatRange = that.hiddenColumns.get(i++);
-      if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
-      {
-        return false;
-      }
-    }
     return true;
   }
 
@@ -1831,4 +746,55 @@ public class ColumnSelection
     return changed;
   }
 
+  /**
+   * Adjusts column selections, and the given selection group, to match the
+   * range of a stretch (e.g. mouse drag) operation
+   * <p>
+   * Method refactored from ScalePanel.mouseDragged
+   * 
+   * @param res
+   *          current column position, adjusted for hidden columns
+   * @param sg
+   *          current selection group
+   * @param min
+   *          start position of the stretch group
+   * @param max
+   *          end position of the stretch group
+   */
+  public void stretchGroup(int res, SequenceGroup sg, int min, int max)
+  {
+    if (!contains(res))
+    {
+      addElement(res);
+    }
+
+    if (res > sg.getStartRes())
+    {
+      // expand selection group to the right
+      sg.setEndRes(res);
+    }
+    if (res < sg.getStartRes())
+    {
+      // expand selection group to the left
+      sg.setStartRes(res);
+    }
+
+    /*
+     * expand or shrink column selection to match the
+     * range of the drag operation
+     */
+    for (int col = min; col <= max; col++)
+    {
+      if (col < sg.getStartRes() || col > sg.getEndRes())
+      {
+        // shrinking drag - remove from selection
+        removeElement(col);
+      }
+      else
+      {
+        // expanding drag - add to selection
+        addElement(col);
+      }
+    }
+  }
 }
diff --git a/src/jalview/datamodel/ContiguousI.java b/src/jalview/datamodel/ContiguousI.java
new file mode 100644 (file)
index 0000000..f2ae4b7
--- /dev/null
@@ -0,0 +1,8 @@
+package jalview.datamodel;
+
+public interface ContiguousI
+{
+  int getBegin(); // todo want long for genomic positions?
+
+  int getEnd();
+}
diff --git a/src/jalview/datamodel/HiddenColumns.java b/src/jalview/datamodel/HiddenColumns.java
new file mode 100644 (file)
index 0000000..f0d99e5
--- /dev/null
@@ -0,0 +1,1299 @@
+package jalview.datamodel;
+
+import jalview.util.Comparison;
+import jalview.util.ShiftList;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Vector;
+
+public class HiddenColumns
+{
+  /*
+   * list of hidden column [start, end] ranges; the list is maintained in
+   * ascending start column order
+   */
+  private Vector<int[]> hiddenColumns;
+
+  /**
+   * This Method is used to return all the HiddenColumn regions
+   * 
+   * @return empty list or List of hidden column intervals
+   */
+  public List<int[]> getHiddenRegions()
+  {
+    return hiddenColumns == null ? Collections.<int[]> emptyList()
+            : hiddenColumns;
+  }
+
+  /**
+   * Find the number of hidden columns
+   * 
+   * @return number of hidden columns
+   */
+  public int getSize()
+  {
+    int size = 0;
+    if (hasHidden())
+    {
+      for (int[] range : hiddenColumns)
+      {
+        size += range[1] - range[0] + 1;
+      }
+    }
+    return size;
+  }
+
+  /**
+   * Answers if there are any hidden columns
+   * 
+   * @return true if there are hidden columns
+   */
+  public boolean hasHidden()
+  {
+    return (hiddenColumns != null) && (!hiddenColumns.isEmpty());
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (!(obj instanceof HiddenColumns))
+    {
+      return false;
+    }
+    HiddenColumns that = (HiddenColumns) obj;
+
+    /*
+     * 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())
+    {
+      return false;
+    }
+    int i = 0;
+    for (int[] thisRange : hiddenColumns)
+    {
+      int[] thatRange = that.hiddenColumns.get(i++);
+      if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Return absolute column index for a visible column index
+   * 
+   * @param column
+   *          int column index in alignment view (count from zero)
+   * @return alignment column index for column
+   */
+  public int adjustForHiddenColumns(int column)
+  {
+    int result = column;
+    if (hiddenColumns != null)
+    {
+      for (int i = 0; i < hiddenColumns.size(); i++)
+      {
+        int[] region = hiddenColumns.elementAt(i);
+        if (result >= region[0])
+        {
+          result += region[1] - region[0] + 1;
+        }
+      }
+    }
+    return result;
+  }
+
+  /**
+   * 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.
+   * 
+   * @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)
+  {
+    int result = hiddenColumn;
+    if (hiddenColumns != null)
+    {
+      int index = 0;
+      int[] region;
+      do
+      {
+        region = hiddenColumns.elementAt(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])
+      {
+        // 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
+        {
+          return result - (hiddenColumn - region[0] + 1);
+        }
+      }
+    }
+    return result; // return the shifted position after removing hidden columns.
+  }
+
+  /**
+   * 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.
+   * 
+   * @param visibleDistance
+   *          the number of visible columns to offset by
+   * @param startColumn
+   *          the column to start from
+   * @return the position of the column in the visible alignment
+   */
+  public int subtractVisibleColumns(int visibleDistance, int startColumn)
+  {
+    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--;
+    }
+
+    if (distance - gap > 0)
+    {
+      // fell out of loop because there are no more hidden regions
+      distance -= gap;
+      return nextstart - distance;
+    }
+    return start - distance;
+
+  }
+
+  /**
+   * Use this method to determine where the next hiddenRegion starts
+   * 
+   * @param hiddenRegion
+   *          index of hidden region (counts from 0)
+   * @return column number in visible view
+   */
+  public int findHiddenRegionPosition(int hiddenRegion)
+  {
+    int result = 0;
+    if (hiddenColumns != null)
+    {
+      int index = 0;
+      int gaps = 0;
+      do
+      {
+        int[] region = hiddenColumns.elementAt(index);
+        if (hiddenRegion == 0)
+        {
+          return region[0];
+        }
+
+        gaps += region[1] + 1 - region[0];
+        result = region[1] + 1;
+        index++;
+      } while (index <= hiddenRegion);
+
+      result -= gaps;
+    }
+
+    return result;
+  }
+
+  /**
+   * This method returns the rightmost limit of a region of an alignment with
+   * hidden columns. In otherwords, the next hidden column.
+   * 
+   * @param index
+   *          int
+   */
+  public int getHiddenBoundaryRight(int alPos)
+  {
+    if (hiddenColumns != null)
+    {
+      int index = 0;
+      do
+      {
+        int[] region = hiddenColumns.elementAt(index);
+        if (alPos < region[0])
+        {
+          return region[0];
+        }
+
+        index++;
+      } while (index < hiddenColumns.size());
+    }
+
+    return alPos;
+
+  }
+
+  /**
+   * This method returns the leftmost limit of a region of an alignment with
+   * hidden columns. In otherwords, the previous hidden column.
+   * 
+   * @param index
+   *          int
+   */
+  public int getHiddenBoundaryLeft(int alPos)
+  {
+    if (hiddenColumns != null)
+    {
+      int index = hiddenColumns.size() - 1;
+      do
+      {
+        int[] region = hiddenColumns.elementAt(index);
+        if (alPos > region[1])
+        {
+          return region[1];
+        }
+
+        index--;
+      } while (index > -1);
+    }
+
+    return alPos;
+
+  }
+
+  /**
+   * 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
+   */
+  private int getHiddenIndexLeft(int pos)
+  {
+    if (hiddenColumns != null)
+    {
+      int index = hiddenColumns.size() - 1;
+      do
+      {
+        int[] region = hiddenColumns.elementAt(index);
+        if (pos > region[1])
+        {
+          return index;
+        }
+
+        index--;
+      } while (index > -1);
+    }
+
+    return -1;
+
+  }
+
+  /**
+   * Adds the specified column range to the hidden columns
+   * 
+   * @param start
+   * @param end
+   */
+  public void hideColumns(int start, int end)
+  {
+    if (hiddenColumns == null)
+    {
+      hiddenColumns = new Vector<int[]>();
+    }
+
+    /*
+     * traverse existing hidden ranges and insert / amend / append as
+     * appropriate
+     */
+    for (int i = 0; i < hiddenColumns.size(); i++)
+    {
+      int[] region = hiddenColumns.elementAt(i);
+
+      if (end < region[0] - 1)
+      {
+        /*
+         * insert discontiguous preceding range
+         */
+        hiddenColumns.insertElementAt(new int[] { start, end }, i);
+        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;
+      }
+    }
+
+    /*
+     * remaining case is that the new range follows everything else
+     */
+    hiddenColumns.addElement(new int[] { start, end });
+  }
+
+  public boolean isVisible(int column)
+  {
+    if (hiddenColumns != null)
+    {
+      for (int[] region : hiddenColumns)
+      {
+        if (column >= region[0] && column <= region[1])
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * ColumnSelection
+   */
+  public HiddenColumns()
+  {
+  }
+
+  /**
+   * Copy constructor
+   * 
+   * @param copy
+   */
+  public HiddenColumns(HiddenColumns copy)
+  {
+    if (copy != null)
+    {
+      if (copy.hiddenColumns != null)
+      {
+        hiddenColumns = new Vector<int[]>(copy.hiddenColumns.size());
+        for (int i = 0, j = copy.hiddenColumns.size(); i < j; i++)
+        {
+          int[] rh, cp;
+          rh = copy.hiddenColumns.elementAt(i);
+          if (rh != null)
+          {
+            cp = new int[rh.length];
+            System.arraycopy(rh, 0, cp, 0, rh.length);
+            hiddenColumns.addElement(cp);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * propagate shift in alignment columns to column selection
+   * 
+   * @param start
+   *          beginning of edit
+   * @param left
+   *          shift in edit (+ve for removal, or -ve for inserts)
+   */
+  public List<int[]> compensateForEdit(int start, int change,
+          ColumnSelection sel)
+  {
+    List<int[]> deletedHiddenColumns = null;
+
+    if (hiddenColumns != null)
+    {
+      deletedHiddenColumns = new ArrayList<int[]>();
+      int hSize = hiddenColumns.size();
+      for (int i = 0; i < hSize; i++)
+      {
+        int[] region = hiddenColumns.elementAt(i);
+        if (region[0] > start && start + change > region[1])
+        {
+          deletedHiddenColumns.add(region);
+
+          hiddenColumns.removeElementAt(i);
+          i--;
+          hSize--;
+          continue;
+        }
+
+        if (region[0] > start)
+        {
+          region[0] -= change;
+          region[1] -= change;
+        }
+
+        if (region[0] < 0)
+        {
+          region[0] = 0;
+        }
+
+      }
+
+      this.revealHiddenColumns(0, sel);
+    }
+
+    return deletedHiddenColumns;
+  }
+
+  /**
+   * 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)
+   */
+  public void compensateForDelEdits(int start, int change)
+  {
+    if (hiddenColumns != null)
+    {
+      for (int i = 0; i < hiddenColumns.size(); i++)
+      {
+        int[] region = hiddenColumns.elementAt(i);
+        if (region[0] >= start)
+        {
+          region[0] -= change;
+        }
+        if (region[1] >= start)
+        {
+          region[1] -= change;
+        }
+        if (region[1] < region[0])
+        {
+          hiddenColumns.removeElementAt(i--);
+        }
+
+        if (region[0] < 0)
+        {
+          region[0] = 0;
+        }
+        if (region[1] < 0)
+        {
+          region[1] = 0;
+        }
+      }
+    }
+  }
+
+  /**
+   * return all visible segments between the given start and end boundaries
+   * 
+   * @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
+   */
+  public int[] getVisibleContigs(int start, int end)
+  {
+    if (hiddenColumns != null && hiddenColumns.size() > 0)
+    {
+      List<int[]> visiblecontigs = new ArrayList<int[]>();
+      List<int[]> regions = getHiddenRegions();
+
+      int vstart = start;
+      int[] region;
+      int hideStart, hideEnd;
+
+      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 (vstart < end)
+      {
+        visiblecontigs.add(new int[] { vstart, end - 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 };
+    }
+  }
+
+  public String[] getVisibleSequenceStrings(int start, int end,
+          SequenceI[] seqs)
+  {
+    int i, iSize = seqs.length;
+    String selections[] = new String[iSize];
+    if (hiddenColumns != null && hiddenColumns.size() > 0)
+    {
+      for (i = 0; i < iSize; i++)
+      {
+        StringBuffer visibleSeq = new StringBuffer();
+        List<int[]> regions = getHiddenRegions();
+
+        int blockStart = start, blockEnd = end;
+        int[] region;
+        int hideStart, hideEnd;
+
+        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));
+        }
+
+        selections[i] = visibleSeq.toString();
+      }
+    }
+    else
+    {
+      for (i = 0; i < iSize; i++)
+      {
+        selections[i] = seqs[i].getSequenceAsString(start, end);
+      }
+    }
+
+    return selections;
+  }
+
+  /**
+   * 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 }
+   */
+  public int[] locateVisibleBoundsOfSequence(SequenceI seq)
+  {
+    int fpos = seq.getStart(), lpos = seq.getEnd();
+    int start = 0;
+
+    if (hiddenColumns == null || hiddenColumns.size() == 0)
+    {
+      int ifpos = seq.findIndex(fpos) - 1, 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, lastvispos = -1, rcount = 0, hideStart = seq
+            .getLength(), hideEnd = -1;
+    int visPrev = 0, visNext = 0, firstP = -1, 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 };
+  }
+
+  /**
+   * delete any columns in alignmentAnnotation that are hidden (including
+   * sequence associated annotation).
+   * 
+   * @param alignmentAnnotation
+   */
+  public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
+  {
+    makeVisibleAnnotation(-1, -1, alignmentAnnotation);
+  }
+
+  /**
+   * 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 alignmentAnnotation
+   *          the annotation to operate on
+   */
+  public void makeVisibleAnnotation(int start, int end,
+          AlignmentAnnotation alignmentAnnotation)
+  {
+    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[]>();
+      Annotation[] els = null;
+      List<int[]> regions = getHiddenRegions();
+      int blockStart = start, blockEnd = end;
+      int[] region;
+      int hideStart, hideEnd, 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;
+      }
+
+      alignmentAnnotation.annotations = new Annotation[w];
+      w = 0;
+
+      for (Annotation[] chnk : annels)
+      {
+        System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
+                chnk.length);
+        w += chnk.length;
+      }
+    }
+    else
+    {
+      alignmentAnnotation.restrict(start, end);
+    }
+  }
+
+  /**
+   * 
+   * @return true if there are columns hidden
+   */
+  public boolean hasHiddenColumns()
+  {
+    return hiddenColumns != null && hiddenColumns.size() > 0;
+  }
+
+  /**
+   * 
+   * @return true if there are more than one set of columns hidden
+   */
+  public boolean hasManyHiddenColumns()
+  {
+    return hiddenColumns != null && hiddenColumns.size() > 1;
+  }
+
+  /**
+   * mark the columns corresponding to gap characters as hidden in the column
+   * selection
+   * 
+   * @param sr
+   */
+  public void hideInsertionsFor(SequenceI sr)
+  {
+    List<int[]> inserts = sr.getInsertions();
+    for (int[] r : inserts)
+    {
+      hideColumns(r[0], r[1]);
+    }
+  }
+
+  /**
+   * Unhides, and adds to the selection list, all hidden columns
+   */
+  public void revealAllHiddenColumns(ColumnSelection sel)
+  {
+    if (hiddenColumns != null)
+    {
+      for (int i = 0; i < hiddenColumns.size(); i++)
+      {
+        int[] region = hiddenColumns.elementAt(i);
+        for (int j = region[0]; j < region[1] + 1; j++)
+        {
+          sel.addElement(j);
+        }
+      }
+    }
+
+    hiddenColumns = null;
+  }
+
+  /**
+   * Reveals, and marks as selected, the hidden column range with the given
+   * start column
+   * 
+   * @param start
+   */
+  public void revealHiddenColumns(int start, ColumnSelection sel)
+  {
+    for (int i = 0; i < hiddenColumns.size(); i++)
+    {
+      int[] region = hiddenColumns.elementAt(i);
+      if (start == region[0])
+      {
+        for (int j = region[0]; j < region[1] + 1; j++)
+        {
+          sel.addElement(j);
+        }
+
+        hiddenColumns.removeElement(region);
+        break;
+      }
+    }
+    if (hiddenColumns.size() == 0)
+    {
+      hiddenColumns = null;
+    }
+  }
+
+  /**
+   * removes intersection of position,length ranges in deletions from the
+   * start,end regions marked in intervals.
+   * 
+   * @param shifts
+   * @param intervals
+   * @return
+   */
+  private boolean pruneIntervalVector(final List<int[]> shifts,
+          Vector<int[]> intervals)
+  {
+    boolean pruned = false;
+    int i = 0, j = intervals.size() - 1, s = 0, t = shifts.size() - 1;
+    int hr[] = intervals.elementAt(i);
+    int sr[] = shifts.get(s);
+    while (i <= j && s <= t)
+    {
+      boolean trailinghn = hr[1] >= sr[0];
+      if (!trailinghn)
+      {
+        if (i < j)
+        {
+          hr = intervals.elementAt(++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.removeElementAt(i);
+          pruned = true;
+          j--;
+          if (i <= j)
+          {
+            hr = intervals.elementAt(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)
+  {
+    // delete any intervals intersecting.
+    if (hiddenColumns != null)
+    {
+      pruneIntervalVector(shifts, hiddenColumns);
+      if (hiddenColumns != null && hiddenColumns.size() == 0)
+      {
+        hiddenColumns = null;
+      }
+    }
+  }
+
+  /**
+   * 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 = al.getHiddenColumns().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()
+  {
+    int hashCode = 1;
+    if (hiddenColumns != null)
+    {
+      for (int[] hidden : hiddenColumns)
+      {
+        hashCode = 31 * hashCode + hidden[0];
+        hashCode = 31 * hashCode + hidden[1];
+      }
+    }
+    return hashCode;
+  }
+
+  /**
+   * Hide columns corresponding to the marked bits
+   * 
+   * @param inserts
+   *          - columns map to bits starting from zero
+   */
+  public void hideMarkedBits(BitSet inserts)
+  {
+    for (int firstSet = inserts.nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
+            .nextSetBit(lastSet))
+    {
+      lastSet = inserts.nextClearBit(firstSet);
+      hideColumns(firstSet, lastSet - 1);
+    }
+  }
+
+  /**
+   * 
+   * @param inserts
+   *          BitSet where hidden columns will be marked
+   */
+  public void markHiddenRegions(BitSet inserts)
+  {
+    if (hiddenColumns == null)
+    {
+      return;
+    }
+    for (int[] range : hiddenColumns)
+    {
+      inserts.set(range[0], range[1] + 1);
+    }
+  }
+
+}
index 9e2cf72..a98b10e 100755 (executable)
@@ -154,8 +154,8 @@ public class HiddenSequences
       hiddenSequences = new SequenceI[alignment.getHeight()];
     }
 
-    int alignmentIndex = alignment.findIndex(sequence);
-    alignmentIndex = adjustForHiddenSeqs(alignmentIndex);
+    int absAlignmentIndex = alignment.findIndex(sequence);
+    int alignmentIndex = adjustForHiddenSeqs(absAlignmentIndex);
 
     if (hiddenSequences[alignmentIndex] != null)
     {
@@ -164,13 +164,19 @@ public class HiddenSequences
 
     hiddenSequences[alignmentIndex] = sequence;
 
-    alignment.deleteSequence(sequence);
+    alignment.deleteHiddenSequence(absAlignmentIndex);
   }
 
   public List<SequenceI> showAll(
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
-    List<SequenceI> revealedSeqs = new ArrayList<SequenceI>();
+    List<SequenceI> revealedSeqs = new ArrayList<>();
+
+    if (hiddenSequences == null)
+    {
+      return revealedSeqs;
+    }
+
     for (int i = 0; i < hiddenSequences.length; i++)
     {
       if (hiddenSequences[i] != null)
@@ -199,7 +205,7 @@ public class HiddenSequences
   public List<SequenceI> showSequence(int alignmentIndex,
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
-    List<SequenceI> revealedSeqs = new ArrayList<SequenceI>();
+    List<SequenceI> revealedSeqs = new ArrayList<>();
     SequenceI repSequence = alignment.getSequenceAt(alignmentIndex);
     if (repSequence != null && hiddenRepSequences != null
             && hiddenRepSequences.containsKey(repSequence))
@@ -246,6 +252,12 @@ public class HiddenSequences
     return hiddenSequences == null ? null : hiddenSequences[alignmentIndex];
   }
 
+  /**
+   * Convert absolute alignment index to visible alignment index
+   * 
+   * @param alignmentIndex
+   * @return
+   */
   public int findIndexWithoutHiddenSeqs(int alignmentIndex)
   {
     if (hiddenSequences == null)
@@ -254,8 +266,14 @@ public class HiddenSequences
     }
     int index = 0;
     int hiddenSeqs = 0;
+    int diff = 0;
     if (hiddenSequences.length <= alignmentIndex)
     {
+      // if the alignmentIndex runs past the end of hidden sequences
+      // and therefore actually past the end of the alignment
+      // store the difference to add back on at the end, so that behaviour
+      // is consistent with hidden columns behaviour (used by overview panel)
+      diff = alignmentIndex - hiddenSequences.length + 1;
       alignmentIndex = hiddenSequences.length - 1;
     }
 
@@ -268,9 +286,50 @@ public class HiddenSequences
       index++;
     }
 
-    return (alignmentIndex - hiddenSeqs);
+    return (alignmentIndex - hiddenSeqs + diff);
   }
 
+  /**
+   * Find the visible row which is a given visible number of rows above another
+   * visible row. i.e. for a startRow x, the row which is distance 1 away will
+   * be row x-1.
+   * 
+   * @param visibleDistance
+   *          the number of visible rows to offset by
+   * @param startRow
+   *          the row to start from
+   * @return the position of the row in the visible alignment
+   */
+  public int subtractVisibleRows(int visibleDistance, int startRow)
+  {
+    // walk upwards through the alignment
+    // count all the non-null sequences until we have visibleDistance counted
+    // then return the next visible sequence
+    if (hiddenSequences == null)
+    {
+      return startRow - visibleDistance;
+    }
+
+    int index = startRow;
+    int count = 0;
+    while ((index > -1) && (count < visibleDistance))
+    {
+      if (hiddenSequences[index] == null)
+      {
+        // count visible sequences
+        count++;
+      }
+      index--;
+    }
+    return index;
+  }
+
+  /**
+   * Convert alignment index from visible alignment to absolute alignment
+   * 
+   * @param alignmentIndex
+   * @return
+   */
   public int adjustForHiddenSeqs(int alignmentIndex)
   {
     if (hiddenSequences == null)
@@ -350,4 +409,20 @@ public class HiddenSequences
 
     return false;
   }
+
+  /**
+   * Answers if a sequence is hidden
+   * 
+   * @param seq
+   *          (absolute) index to test
+   * @return true if sequence at index seq is hidden
+   */
+  public boolean isHidden(int seq)
+  {
+    if (hiddenSequences != null)
+    {
+      return (hiddenSequences[seq] != null);
+    }
+    return false;
+  }
 }
index 1c196be..c741603 100644 (file)
@@ -530,9 +530,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 +540,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 };
   }
diff --git a/src/jalview/datamodel/Range.java b/src/jalview/datamodel/Range.java
new file mode 100644 (file)
index 0000000..7886713
--- /dev/null
@@ -0,0 +1,52 @@
+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;
+  }
+}
index 1bf5475..8d98fc4 100755 (executable)
@@ -219,20 +219,15 @@ public class SearchResults implements SearchResultsI
       m = (Match) _m;
 
       mfound = false;
-      if (m.sequence == sequence)
+      if (m.sequence == sequence
+              || m.sequence == sequence.getDatasetSequence())
       {
         mfound = true;
-        // locate aligned position
         matchStart = sequence.findIndex(m.start) - 1;
-        matchEnd = sequence.findIndex(m.end) - 1;
-      }
-      else if (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)
index 98b0de5..9cc7b4a 100644 (file)
@@ -494,18 +494,18 @@ public class SeqCigar extends CigarSimple
   /**
    * create an alignment from the given array of cigar sequences and gap
    * character, and marking the given segments as visible in the given
-   * columselection.
+   * hiddenColumns.
    * 
    * @param alseqs
    * @param gapCharacter
-   * @param colsel
-   *          - columnSelection where hidden regions are marked
+   * @param hidden
+   *          - hiddenColumns where hidden regions are marked
    * @param segments
    *          - visible regions of alignment
    * @return SequenceI[]
    */
   public static SequenceI[] createAlignmentSequences(SeqCigar[] alseqs,
-          char gapCharacter, ColumnSelection colsel, int[] segments)
+          char gapCharacter, HiddenColumns hidden, int[] segments)
   {
     SequenceI[] seqs = new SequenceI[alseqs.length];
     StringBuffer[] g_seqs = new StringBuffer[alseqs.length];
@@ -577,7 +577,7 @@ public class SeqCigar extends CigarSimple
           if (segments == null)
           {
             // add a hidden column for this deletion
-            colsel.hideColumns(inspos, inspos + insert.length - 1);
+            hidden.hideColumns(inspos, inspos + insert.length - 1);
           }
         }
       }
@@ -598,7 +598,7 @@ public class SeqCigar extends CigarSimple
       {
         // int start=shifts.shift(segments[i]-1)+1;
         // int end=shifts.shift(segments[i]+segments[i+1]-1)-1;
-        colsel.hideColumns(segments[i + 1], segments[i + 1]
+        hidden.hideColumns(segments[i + 1], segments[i + 1]
                 + segments[i + 2] - 1);
       }
     }
index b0faf21..a442cf0 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;
@@ -29,11 +31,15 @@ import jalview.util.StringUtils;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Vector;
 
+import com.stevesoft.pat.Regex;
+
 import fr.orsay.lri.varna.models.rna.RNA;
 
 /**
@@ -45,6 +51,11 @@ import fr.orsay.lri.varna.models.rna.RNA;
  */
 public class Sequence extends ASequence implements SequenceI
 {
+  private static final Regex limitrx = new Regex(
+          "[/][0-9]{1,}[-][0-9]{1,}$");
+
+  private static final Regex endrx = new Regex("[0-9]{1,}$");
+
   SequenceI datasetSequence;
 
   String name;
@@ -78,8 +89,22 @@ public class Sequence extends ASequence implements SequenceI
    */
   int index = -1;
 
-  /** array of sequence features - may not be null for a valid sequence object */
-  public SequenceFeature[] sequenceFeatures;
+  private SequenceFeatures 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.
+   */
+  private SequenceCursor cursor;
+
+  /*
+   * 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. 
+   */
+  private int changeCount;
 
   /**
    * Creates a new Sequence object.
@@ -96,11 +121,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);
   }
 
@@ -124,11 +151,6 @@ 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,}$");
-
   void parseId()
   {
     if (name == null)
@@ -175,6 +197,14 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
+   * default constructor
+   */
+  private Sequence()
+  {
+    sequenceFeatureStore = new SequenceFeatures();
+  }
+
+  /**
    * Creates a new Sequence object.
    * 
    * @param name
@@ -213,8 +243,8 @@ public class Sequence extends ASequence implements SequenceI
    */
   public Sequence(SequenceI seq, AlignmentAnnotation[] alAnnotation)
   {
+    this();
     initSeqFrom(seq, alAnnotation);
-
   }
 
   /**
@@ -230,33 +260,39 @@ 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();
+    initSeqAndName(seq.getName(), Arrays.copyOf(oseq, oseq.length),
+            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++)
-      {
-        addDBRef(new DBRefEntry(dbr[i]));
-      }
-      if (seq.getSequenceFeatures() != null)
+      if (seq.getDBRefs() != 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();
@@ -293,122 +329,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
@@ -564,6 +545,7 @@ public class Sequence extends ASequence implements SequenceI
   {
     this.sequence = seq.toCharArray();
     checkValidRange();
+    sequenceChanged();
   }
 
   @Override
@@ -684,58 +666,320 @@ 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)
+  {
+    int endColumn = cursor == null ? 0 : cursor.lastColumnPosition;
+    if (residuePos == this.end)
     {
-      return i;
+      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(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 0-based array
+                                       // index
+    int newPos = curs.residuePosition;
+    int delta = newPos > pos ? -1 : 1;
+
+    while (newPos != pos)
+    {
+      col += delta; // shift one column left or right
+      if (col < 0 || col == sequence.length)
+      {
+        break;
+      }
+      if (!Comparison.isGap(sequence[col]))
+      {
+        newPos += delta;
+      }
+    }
+
+    col++; // convert back to base 1
+    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;
+  }
+
+  /**
    * Returns an int array where indices correspond to each residue in the
    * sequence and the element value gives its position in the alignment
    * 
@@ -819,6 +1063,40 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   @Override
+  public BitSet getInsertionsAsBits()
+  {
+    BitSet map = new BitSet();
+    int lastj = -1, j = 0;
+    int pos = start;
+    int seqlen = sequence.length;
+    while ((j < seqlen))
+    {
+      if (jalview.util.Comparison.isGap(sequence[j]))
+      {
+        if (lastj == -1)
+        {
+          lastj = j;
+        }
+      }
+      else
+      {
+        if (lastj != -1)
+        {
+          map.set(lastj, j);
+          lastj = -1;
+        }
+      }
+      j++;
+    }
+    if (lastj != -1)
+    {
+      map.set(lastj, j);
+      lastj = -1;
+    }
+    return map;
+  }
+
+  @Override
   public void deleteChars(int i, int j)
   {
     int newstart = start, newend = end;
@@ -891,6 +1169,7 @@ public class Sequence extends ASequence implements SequenceI
     start = newstart;
     end = newend;
     sequence = tmp;
+    sequenceChanged();
   }
 
   @Override
@@ -921,6 +1200,7 @@ public class Sequence extends ASequence implements SequenceI
     }
 
     sequence = tmp;
+    sequenceChanged();
   }
 
   @Override
@@ -1154,8 +1434,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
@@ -1214,11 +1494,11 @@ public class Sequence extends ASequence implements SequenceI
       return null;
     }
 
-    Vector subset = new Vector();
-    Enumeration e = annotation.elements();
+    Vector<AlignmentAnnotation> subset = new Vector<AlignmentAnnotation>();
+    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);
@@ -1233,7 +1513,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;
@@ -1286,12 +1566,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++)
           {
@@ -1304,10 +1584,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);
       }
     }
@@ -1475,4 +1755,74 @@ 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 = findPosition(toColumn - 1);
+    // to trace / debug behaviour:
+    // System.out
+    // .println(String
+    // .format("%s.findFeatures columns [%d-%d] positions [%d-%d] leaves cursor %s",
+    // getName(), fromColumn, toColumn, startPos,
+    // endPos, cursor));
+    List<SequenceFeature> result = new ArrayList<>();
+    if (datasetSequence != null)
+    {
+      result = datasetSequence.getFeatures().findFeatures(startPos, endPos,
+              types);
+    }
+    else
+    {
+      result = sequenceFeatureStore.findFeatures(startPos, endPos, types);
+    }
+
+    /*
+     * if the start or end column is gapped, startPos or endPos may be to the 
+     * left or right, and we may have included adjacent or enclosing features;
+     * remove any that are not enclosing, non-contact features
+     */
+    if (endPos > this.end || Comparison.isGap(sequence[fromColumn - 1])
+            || Comparison.isGap(sequence[toColumn - 1]))
+    {
+      ListIterator<SequenceFeature> it = result.listIterator();
+      while (it.hasNext())
+      {
+        SequenceFeature sf = it.next();
+        int featureStartColumn = findIndex(sf.getBegin());
+        int featureEndColumn = findIndex(sf.getEnd());
+        boolean noOverlap = featureStartColumn > toColumn
+                        || featureEndColumn < fromColumn;
+
+        /*
+         * reject an 'enclosing' feature if it is actually a contact feature
+         */
+        if (sf.isContactFeature() && featureStartColumn < fromColumn
+                && featureEndColumn > toColumn)
+        {
+          noOverlap = true;
+        }
+        if (noOverlap)
+        {
+          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++;
+  }
 }
index aca79c8..f681f11 100644 (file)
@@ -64,4 +64,10 @@ public interface SequenceCollectionI
    */
   int getEndRes();
 
+  /**
+   * Answers true if sequence data is nucleotide (according to some heuristic)
+   * 
+   * @return
+   */
+  boolean isNucleotide();
 }
diff --git a/src/jalview/datamodel/SequenceCursor.java b/src/jalview/datamodel/SequenceCursor.java
new file mode 100644 (file)
index 0000000..b5929bf
--- /dev/null
@@ -0,0 +1,125 @@
+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 15f54b9..9a6cf2b 100755 (executable)
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.FeatureLocationI;
+
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Vector;
 
 /**
@@ -30,8 +33,14 @@ import java.util.Vector;
  * @author $author$
  * @version $Revision$
  */
-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";
@@ -48,13 +57,22 @@ public class SequenceFeature
    */
   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,14 +84,6 @@ 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()
-  {
-  }
-
   /**
    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
    * otherDetails map, so the new and original SequenceFeature may reference the
@@ -83,96 +93,83 @@ 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 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.getType(), sf.getDescription(), newBegin, newEnd, newScore,
+            newGroup);
+
+    if (sf.otherDetails != null)
+    {
+      otherDetails = new HashMap<String, Object>();
+      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<String>();
+      for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
+      {
+        links.addElement(sf.links.elementAt(i));
+      }
+    }
   }
 
   /**
@@ -268,31 +265,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 +292,6 @@ public class SequenceFeature
     return type;
   }
 
-  public void setType(String type)
-  {
-    this.type = type;
-  }
-
   /**
    * DOCUMENT ME!
    * 
@@ -328,11 +312,6 @@ public class SequenceFeature
     return featureGroup;
   }
 
-  public void setFeatureGroup(String featureGroup)
-  {
-    this.featureGroup = featureGroup;
-  }
-
   public void addLink(String labelLink)
   {
     if (links == null)
@@ -340,7 +319,10 @@ public class SequenceFeature
       links = new Vector<String>();
     }
 
-    links.insertElementAt(labelLink, 0);
+    if (!links.contains(labelLink))
+    {
+      links.insertElementAt(labelLink, 0);
+    }
   }
 
   public float getScore()
@@ -348,11 +330,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
@@ -432,17 +409,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 +504,19 @@ 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 true;
-    }
-    return false;
+    return contactFeature;
+  }
+
+  /**
+   * Answers true if the sequence has zero start and end position
+   * 
+   * @return
+   */
+  public boolean isNonPositional()
+  {
+    return begin == 0 && end == 0;
   }
 }
index 9245761..463b909 100755 (executable)
@@ -22,6 +22,8 @@ package jalview.datamodel;
 
 import jalview.analysis.AAFrequency;
 import jalview.analysis.Conservation;
+import jalview.renderer.ResidueShader;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemes.ColourSchemeI;
 
 import java.awt.Color;
@@ -50,6 +52,12 @@ public class SequenceGroup implements AnnotatedCollectionI
   boolean colourText = false;
 
   /**
+   * True if the group is defined as a group on the alignment, false if it is
+   * just a selection.
+   */
+  boolean isDefined = false;
+
+  /**
    * after Olivier's non-conserved only character display
    */
   boolean showNonconserved = false;
@@ -57,7 +65,7 @@ public class SequenceGroup implements AnnotatedCollectionI
   /**
    * group members
    */
-  private List<SequenceI> sequences = new ArrayList<SequenceI>();
+  private List<SequenceI> sequences = new ArrayList<>();
 
   /**
    * representative sequence for this group (if any)
@@ -69,7 +77,7 @@ public class SequenceGroup implements AnnotatedCollectionI
   /**
    * Colourscheme applied to group if any
    */
-  public ColourSchemeI cs;
+  public ResidueShaderI cs;
 
   // start column (base 0)
   int startRes = 0;
@@ -102,13 +110,23 @@ public class SequenceGroup implements AnnotatedCollectionI
    */
   private boolean normaliseSequenceLogo;
 
-  /**
-   * @return the includeAllConsSymbols
+  /*
+   * visibility of rows or represented rows covered by group
    */
-  public boolean isShowSequenceLogo()
-  {
-    return showSequenceLogo;
-  }
+  private boolean hidereps = false;
+
+  /*
+   * visibility of columns intersecting this group
+   */
+  private boolean hidecols = false;
+
+  AlignmentAnnotation consensus = null;
+
+  AlignmentAnnotation conservation = null;
+
+  private boolean showConsensusHistogram;
+
+  private AnnotatedCollectionI context;
 
   /**
    * Creates a new SequenceGroup object.
@@ -116,6 +134,7 @@ public class SequenceGroup implements AnnotatedCollectionI
   public SequenceGroup()
   {
     groupName = "JGroup:" + this.hashCode();
+    cs = new ResidueShader();
   }
 
   /**
@@ -136,12 +155,13 @@ public class SequenceGroup implements AnnotatedCollectionI
           ColourSchemeI scheme, boolean displayBoxes, boolean displayText,
           boolean colourText, int start, int end)
   {
+    this();
     this.sequences = sequences;
     this.groupName = groupName;
     this.displayBoxes = displayBoxes;
     this.displayText = displayText;
     this.colourText = colourText;
-    this.cs = scheme;
+    this.cs = new ResidueShader(scheme);
     startRes = start;
     endRes = end;
     recalcConservation();
@@ -154,9 +174,10 @@ public class SequenceGroup implements AnnotatedCollectionI
    */
   public SequenceGroup(SequenceGroup seqsel)
   {
+    this();
     if (seqsel != null)
     {
-      sequences = new ArrayList<SequenceI>();
+      sequences = new ArrayList<>();
       sequences.addAll(seqsel.sequences);
       if (seqsel.groupName != null)
       {
@@ -167,13 +188,17 @@ public class SequenceGroup implements AnnotatedCollectionI
       colourText = seqsel.colourText;
       startRes = seqsel.startRes;
       endRes = seqsel.endRes;
-      cs = seqsel.cs;
+      cs = new ResidueShader(seqsel.getColourScheme());
       if (seqsel.description != null)
       {
         description = new String(seqsel.description);
       }
       hidecols = seqsel.hidecols;
       hidereps = seqsel.hidereps;
+      showNonconserved = seqsel.showNonconserved;
+      showSequenceLogo = seqsel.showSequenceLogo;
+      normaliseSequenceLogo = seqsel.normaliseSequenceLogo;
+      showConsensusHistogram = seqsel.showConsensusHistogram;
       idColour = seqsel.idColour;
       outlineColour = seqsel.outlineColour;
       seqrep = seqsel.seqrep;
@@ -190,6 +215,11 @@ public class SequenceGroup implements AnnotatedCollectionI
     }
   }
 
+  public boolean isShowSequenceLogo()
+  {
+    return showSequenceLogo;
+  }
+
   public SequenceI[] getSelectionAsNewSequences(AlignmentI align)
   {
     int iSize = sequences.size();
@@ -306,7 +336,7 @@ public class SequenceGroup implements AnnotatedCollectionI
     }
     else
     {
-      List<SequenceI> allSequences = new ArrayList<SequenceI>();
+      List<SequenceI> allSequences = new ArrayList<>();
       for (SequenceI seq : sequences)
       {
         allSequences.add(seq);
@@ -946,11 +976,6 @@ public class SequenceGroup implements AnnotatedCollectionI
   }
 
   /**
-   * visibility of rows or represented rows covered by group
-   */
-  private boolean hidereps = false;
-
-  /**
    * set visibility of sequences covered by (if no sequence representative is
    * defined) or represented by this group.
    * 
@@ -972,11 +997,6 @@ public class SequenceGroup implements AnnotatedCollectionI
   }
 
   /**
-   * visibility of columns intersecting this group
-   */
-  private boolean hidecols = false;
-
-  /**
    * set intended visibility of columns covered by this group
    * 
    * @param visibility
@@ -1010,7 +1030,7 @@ public class SequenceGroup implements AnnotatedCollectionI
   {
     SequenceGroup sgroup = new SequenceGroup(this);
     SequenceI[] insect = getSequencesInOrder(alignment);
-    sgroup.sequences = new ArrayList<SequenceI>();
+    sgroup.sequences = new ArrayList<>();
     for (int s = 0; insect != null && s < insect.length; s++)
     {
       if (map == null || map.containsKey(insect[s]))
@@ -1038,13 +1058,6 @@ public class SequenceGroup implements AnnotatedCollectionI
     this.showNonconserved = displayNonconserved;
   }
 
-  AlignmentAnnotation consensus = null, conservation = null;
-
-  /**
-   * flag indicating if consensus histogram should be rendered
-   */
-  private boolean showConsensusHistogram;
-
   /**
    * set this alignmentAnnotation object as the one used to render consensus
    * annotation
@@ -1237,7 +1250,7 @@ public class SequenceGroup implements AnnotatedCollectionI
   {
     // TODO add in other methods like 'getAlignmentAnnotation(String label),
     // etc'
-    ArrayList<AlignmentAnnotation> annot = new ArrayList<AlignmentAnnotation>();
+    ArrayList<AlignmentAnnotation> annot = new ArrayList<>();
     synchronized (sequences)
     {
       for (SequenceI seq : sequences)
@@ -1269,10 +1282,14 @@ public class SequenceGroup implements AnnotatedCollectionI
   @Override
   public Iterable<AlignmentAnnotation> findAnnotation(String calcId)
   {
-    ArrayList<AlignmentAnnotation> aa = new ArrayList<AlignmentAnnotation>();
+    List<AlignmentAnnotation> aa = new ArrayList<>();
+    if (calcId == null)
+    {
+      return aa;
+    }
     for (AlignmentAnnotation a : getAlignmentAnnotation())
     {
-      if (a.getCalcId() == calcId)
+      if (calcId.equals(a.getCalcId()))
       {
         aa.add(a);
       }
@@ -1280,22 +1297,18 @@ public class SequenceGroup implements AnnotatedCollectionI
     return aa;
   }
 
-  /**
-   * Returns a list of annotations that match the specified sequenceRef, calcId
-   * and label, ignoring null values.
-   * 
-   * @return list of AlignmentAnnotation objects
-   */
   @Override
   public Iterable<AlignmentAnnotation> findAnnotations(SequenceI seq,
           String calcId, String label)
   {
-    ArrayList<AlignmentAnnotation> aa = new ArrayList<AlignmentAnnotation>();
+    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
     for (AlignmentAnnotation ann : getAlignmentAnnotation())
     {
-      if (ann.getCalcId() != null && ann.getCalcId().equals(calcId)
-              && ann.sequenceRef != null && ann.sequenceRef == seq
-              && ann.label != null && ann.label.equals(label))
+      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);
       }
@@ -1335,16 +1348,45 @@ public class SequenceGroup implements AnnotatedCollectionI
     }
   }
 
-  private AnnotatedCollectionI context;
+  /**
+   * Sets the alignment or group context for this group, and whether it is
+   * defined as a group
+   * 
+   * @param ctx
+   *          the context for the group
+   * @param defined
+   *          whether the group is defined on the alignment or is just a
+   *          selection
+   * @throws IllegalArgumentException
+   *           if setting the context would result in a circular reference chain
+   */
+  public void setContext(AnnotatedCollectionI ctx, boolean defined)
+  {
+    setContext(ctx);
+    this.isDefined = defined;
+  }
 
   /**
-   * set the alignment or group context for this group
+   * Sets the alignment or group context for this group
    * 
-   * @param context
+   * @param ctx
+   *          the context for the group
+   * @throws IllegalArgumentException
+   *           if setting the context would result in a circular reference chain
    */
-  public void setContext(AnnotatedCollectionI context)
+  public void setContext(AnnotatedCollectionI ctx)
   {
-    this.context = context;
+    AnnotatedCollectionI ref = ctx;
+    while (ref != null)
+    {
+      if (ref == this || ref.getContext() == ctx)
+      {
+        throw new IllegalArgumentException(
+                "Circular reference in SequenceGroup.context");
+      }
+      ref = ref.getContext();
+    }
+    this.context = ctx;
   }
 
   /*
@@ -1357,4 +1399,62 @@ public class SequenceGroup implements AnnotatedCollectionI
   {
     return context;
   }
+
+  public boolean isDefined()
+  {
+    return isDefined;
+  }
+
+  public void setColourScheme(ColourSchemeI scheme)
+  {
+    if (cs == null)
+    {
+      cs = new ResidueShader();
+    }
+    cs.setColourScheme(scheme);
+  }
+
+  public void setGroupColourScheme(ResidueShaderI scheme)
+  {
+    cs = scheme;
+  }
+
+  public ColourSchemeI getColourScheme()
+  {
+    return cs == null ? null : cs.getColourScheme();
+  }
+
+  public ResidueShaderI getGroupColourScheme()
+  {
+    return cs;
+  }
+
+  @Override
+  public boolean isNucleotide()
+  {
+    if (context != null) {
+      return context.isNucleotide();
+    }
+    return false;
+  }
+
+  /**
+   * @param seq
+   * @return true if seq is a member of the group
+   */
+
+  public boolean contains(SequenceI seq1)
+  {
+    return sequences.contains(seq1);
+  }
+
+  /**
+   * @param seq
+   * @param apos
+   * @return true if startRes<=apos and endRes>=apos and seq is in the group
+   */
+  public boolean contains(SequenceI seq, int apos)
+  {
+    return (startRes <= apos && endRes >= apos) && sequences.contains(seq);
+  }
 }
index 92f797f..6992a8d 100755 (executable)
@@ -20,6 +20,9 @@
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.SequenceFeaturesI;
+
+import java.util.BitSet;
 import java.util.List;
 import java.util.Vector;
 
@@ -174,7 +177,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
@@ -189,12 +192,13 @@ 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 position.
    * 
    * @param i
    *          column index in alignment (from 0..<length)
    * 
-   * @return residue number for residue (left of and) nearest ith column
+   * @return TODO: JAL-2562 - residue number for residue (left of and) nearest
+   *         ith column
    */
   public int findPosition(int i);
 
@@ -259,22 +263,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!
@@ -339,7 +349,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
@@ -475,4 +485,34 @@ public interface SequenceI extends ASequenceI
    *         list
    */
   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();
 }
index b2f054c..fa12419 100755 (executable)
@@ -31,13 +31,13 @@ import java.awt.Color;
 public class SequenceNode extends BinaryNode
 {
   /** DOCUMENT ME!! */
-  public float dist;
+  public double dist;
 
   /** DOCUMENT ME!! */
   public int count;
 
   /** DOCUMENT ME!! */
-  public float height;
+  public double height;
 
   /** DOCUMENT ME!! */
   public float ycount;
@@ -178,7 +178,9 @@ public class SequenceNode extends BinaryNode
       {
         char q = name.charAt(c);
         if ('0' <= q && q <= '9')
+        {
           continue;
+        }
         return true;
       }
     }
diff --git a/src/jalview/datamodel/VisibleColsCollection.java b/src/jalview/datamodel/VisibleColsCollection.java
new file mode 100644 (file)
index 0000000..86233ab
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import jalview.api.AlignmentColsCollectionI;
+
+import java.util.Iterator;
+
+public class VisibleColsCollection implements AlignmentColsCollectionI
+{
+  int start;
+  int end;
+
+  HiddenColumns hidden;
+
+  public VisibleColsCollection(int s, int e, AlignmentI al)
+  {
+    start = s;
+    end = e;
+    hidden = al.getHiddenColumns();
+  }
+
+  @Override
+  public Iterator<Integer> iterator()
+  {
+    return new VisibleColsIterator(start, end, hidden);
+  }
+
+  @Override
+  public boolean isHidden(int c)
+  {
+    return false;
+  }
+
+}
diff --git a/src/jalview/datamodel/VisibleColsIterator.java b/src/jalview/datamodel/VisibleColsIterator.java
new file mode 100644 (file)
index 0000000..70de1e3
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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.getHiddenRegions();
+    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/VisibleRowsCollection.java b/src/jalview/datamodel/VisibleRowsCollection.java
new file mode 100644 (file)
index 0000000..ce8e8da
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import jalview.api.AlignmentRowsCollectionI;
+
+import java.util.Iterator;
+
+public class VisibleRowsCollection implements AlignmentRowsCollectionI
+{
+  int start;
+
+  int end;
+
+  AlignmentI alignment;
+
+  public VisibleRowsCollection(int s, int e, AlignmentI al)
+  {
+    start = s;
+    end = e;
+    alignment = al;
+  }
+
+  @Override
+  public Iterator<Integer> iterator()
+  {
+    return new VisibleRowsIterator(start, end, alignment);
+  }
+
+  @Override
+  public boolean isHidden(int seq)
+  {
+    return false;
+  }
+
+  @Override
+  public SequenceI getSequence(int seq)
+  {
+    return alignment.getSequenceAtAbsoluteIndex(seq);
+  }
+}
+
diff --git a/src/jalview/datamodel/VisibleRowsIterator.java b/src/jalview/datamodel/VisibleRowsIterator.java
new file mode 100644 (file)
index 0000000..a9c782d
--- /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.
+ */
+package jalview.datamodel;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * An iterator which iterates over all visible rows in an alignment
+ * 
+ * @author kmourao
+ *
+ */
+public class VisibleRowsIterator implements Iterator<Integer>
+{
+  private int last;
+
+  private int current;
+
+  private int next;
+
+  private HiddenSequences hidden;
+
+  private AlignmentI al;
+
+  /**
+   * Create an iterator for all visible rows in the alignment
+   * 
+   * @param firstrow
+   *          absolute row index to start from
+   * @param lastrow
+   *          absolute row index to end at
+   * @param alignment
+   *          alignment to work with
+   */
+  public VisibleRowsIterator(int firstrow, int lastrow, AlignmentI alignment)
+  {
+    al = alignment;
+    current = firstrow;
+    last = lastrow;
+    hidden = al.getHiddenSequences();
+    while (last > current && hidden.isHidden(last))
+    {
+      last--;
+    }
+    current = firstrow;
+    while (current < last && hidden.isHidden(current))
+    {
+      current++;
+    }
+    next = current;
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return next <= last;
+  }
+
+  @Override
+  public Integer next()
+  {
+    if (next > last)
+    {
+      throw new NoSuchElementException();
+    }
+    current = next;
+    do
+    {
+      next++;
+    } while (next <= last && hidden.isHidden(next));
+    return current;
+  }
+
+  @Override
+  public void remove()
+  {
+    throw new UnsupportedOperationException();
+  }
+}
+
diff --git a/src/jalview/datamodel/features/FeatureLocationI.java b/src/jalview/datamodel/features/FeatureLocationI.java
new file mode 100644 (file)
index 0000000..e651c13
--- /dev/null
@@ -0,0 +1,12 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.ContiguousI;
+
+/**
+ * An extension of ContiguousI that allows start/end values to be interpreted
+ * instead as two contact positions
+ */
+public interface FeatureLocationI extends ContiguousI
+{
+  boolean isContactFeature();
+}
diff --git a/src/jalview/datamodel/features/FeatureStore.java b/src/jalview/datamodel/features/FeatureStore.java
new file mode 100644 (file)
index 0000000..3e80966
--- /dev/null
@@ -0,0 +1,1036 @@
+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);
+
+    static SearchCriterion byStart(final long target)
+    {
+      return new SearchCriterion() {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return entry.getBegin() >= target;
+        }
+      };
+    }
+
+    static SearchCriterion byEnd(final long target)
+    {
+      return new SearchCriterion()
+      {
+
+        @Override
+        boolean compare(SequenceFeature entry)
+        {
+          return entry.getEnd() >= target;
+        }
+      };
+    }
+
+    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)
+  {
+    /*
+     * 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
+    {
+      if (!contains(nonNestedFeatures, feature))
+      {
+        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 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. If the
+   * non-positional features already include the new feature (by equality test),
+   * then it is not added, and this method returns false. The feature group is
+   * added to the set of distinct feature groups for non-positional features.
+   * 
+   * @param feature
+   */
+  protected boolean addNonPositionalFeature(SequenceFeature feature)
+  {
+    if (nonPositionalFeatures == null)
+    {
+      nonPositionalFeatures = new ArrayList<SequenceFeature>();
+    }
+    if (nonPositionalFeatures.contains(feature))
+    {
+      return false;
+    }
+
+    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<SequenceFeature>(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. If the contact feature lists already contain the
+   * given feature (by test for equality), does not add it and returns false.
+   * 
+   * @param feature
+   * @return
+   */
+  protected synchronized boolean addContactFeature(SequenceFeature feature)
+  {
+    if (contactFeatureStarts == null)
+    {
+      contactFeatureStarts = new ArrayList<SequenceFeature>();
+    }
+    if (contactFeatureEnds == null)
+    {
+      contactFeatureEnds = new ArrayList<SequenceFeature>();
+    }
+
+    if (contains(contactFeatureStarts, feature))
+    {
+      return false;
+    }
+
+    /*
+     * 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 contains(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<SequenceFeature>();
+
+    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)
+  {
+    int startIndex = binarySearch(nonNestedFeatures,
+            SearchCriterion.byEnd(from));
+
+    findNonNestedFeatures(startIndex, from, to, result);
+  }
+
+  /**
+   * Scans the list of non-nested features, starting from startIndex, to find
+   * those that overlap the from-to range, and adds them to the result list.
+   * Returns the index of the first feature whose start position is after the
+   * target range (or the length of the whole list if none such feature exists).
+   * 
+   * @param startIndex
+   * @param from
+   * @param to
+   * @param result
+   * @return
+   */
+  protected int findNonNestedFeatures(final int startIndex, long from,
+          long to, List<SequenceFeature> result)
+  {
+    int i = startIndex;
+    while (i < nonNestedFeatures.size())
+    {
+      SequenceFeature sf = nonNestedFeatures.get(i);
+      if (sf.getBegin() > to)
+      {
+        break;
+      }
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+      if (start <= to && end >= from)
+      {
+        result.add(sf);
+      }
+      i++;
+    }
+    return 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<SequenceFeature>();
+    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<SequenceFeature>(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<SequenceFeature>(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<SequenceFeature>();
+
+    /*
+     * 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 value to the start and end of all positional features.
+   * Returns true if at least one feature was updated, else false.
+   * 
+   * @param shift
+   * @return
+   */
+  public synchronized boolean shiftFeatures(int shift)
+  {
+    /*
+     * 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())
+    {
+      modified = true;
+      int newBegin = sf.getBegin() + shift;
+      int newEnd = sf.getEnd() + shift;
+
+      /*
+       * 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..a6a23e7
--- /dev/null
@@ -0,0 +1,626 @@
+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<T>(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<Range>();
+    
+    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<T>(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<T>(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<T>(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<T>(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<T>();
+    newNCList.addNodes(subranges.subList(i, j + 1));
+    NCNode<T> newNode = new NCNode<T>(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<T>();
+
+    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<T>();
+    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..c1be242
--- /dev/null
@@ -0,0 +1,255 @@
+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<V>();
+    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..26ffee1
--- /dev/null
@@ -0,0 +1,78 @@
+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..8f6d496
--- /dev/null
@@ -0,0 +1,494 @@
+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<String, FeatureStore>();
+  }
+
+  /**
+   * 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<SequenceFeature>();
+
+    for (String featureType : varargToTypes(type))
+    {
+      FeatureStore features = featureStore.get(featureType);
+      if (features != null)
+      {
+        result.addAll(features.findOverlappingFeatures(from, to));
+      }
+    }
+
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getAllFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    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<SequenceFeature>();
+    }
+
+    Set<String> featureTypes = getFeatureTypes(ontologyTerm);
+    return getAllFeatures(featureTypes.toArray(new String[featureTypes
+            .size()]));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getFeatureCount(boolean positional, String... type)
+  {
+    int result = 0;
+
+    for (String featureType : varargToTypes(type))
+    {
+      FeatureStore featureSet = featureStore.get(featureType);
+      if (featureSet != null)
+      {
+        result += featureSet.getFeatureCount(positional);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public int getTotalFeatureLength(String... type)
+  {
+    int result = 0;
+
+    for (String featureType : varargToTypes(type))
+    {
+      FeatureStore featureSet = featureStore.get(featureType);
+      if (featureSet != null)
+      {
+        result += featureSet.getTotalFeatureLength();
+      }
+    }
+    return result;
+
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getPositionalFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    for (String featureType : varargToTypes(type))
+    {
+      FeatureStore featureSet = featureStore.get(featureType);
+      if (featureSet != null)
+      {
+        result.addAll(featureSet.getPositionalFeatures());
+      }
+    }
+    return result;
+  }
+
+  /**
+   * A convenience method that converts a vararg for feature types to an
+   * Iterable, replacing the value with the stored feature types if it is null
+   * or empty
+   * 
+   * @param type
+   * @return
+   */
+  protected Iterable<String> varargToTypes(String... type)
+  {
+    if (type == null || type.length == 0)
+    {
+      /*
+       * no vararg parameter supplied
+       */
+      return featureStore.keySet();
+    }
+
+    /*
+     * else make a copy of the list, and remove any null value just in case,
+     * as it would cause errors looking up the features Map
+     * sort in alphabetical order for consistent output behaviour
+     */
+    List<String> types = new ArrayList<String>(Arrays.asList(type));
+    types.remove(null);
+    Collections.sort(types);
+    return types;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getContactFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    for (String featureType : varargToTypes(type))
+    {
+      FeatureStore featureSet = featureStore.get(featureType);
+      if (featureSet != null)
+      {
+        result.addAll(featureSet.getContactFeatures());
+      }
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public List<SequenceFeature> getNonPositionalFeatures(String... type)
+  {
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+
+    for (String featureType : varargToTypes(type))
+    {
+      FeatureStore featureSet = featureStore.get(featureType);
+      if (featureSet != null)
+      {
+        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<String>();
+
+    Iterable<String> types = varargToTypes(type);
+
+    for (String featureType : types)
+    {
+      FeatureStore featureSet = featureStore.get(featureType);
+      if (featureSet != null)
+      {
+        groups.addAll(featureSet.getFeatureGroups(positionalFeatures));
+      }
+    }
+
+    return groups;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Set<String> getFeatureTypesForGroups(boolean positionalFeatures,
+          String... groups)
+  {
+    Set<String> result = new HashSet<String>();
+
+    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<String>();
+    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 is one of the specified sequence ontology
+   * terms (or a sub-type of one), 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 (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<SequenceFeature>();
+    Iterable<String> types = varargToTypes(type);
+
+    for (String featureType : types)
+    {
+      /*
+       * check whether the feature type is present, and also
+       * whether it has features for the specified group
+       */
+      FeatureStore features = featureStore.get(featureType);
+      if (features != null
+              && features.getFeatureGroups(positional).contains(group))
+      {
+        result.addAll(features.getFeaturesForGroup(positional, group));
+      }
+    }
+    return result;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean shiftFeatures(int shift)
+  {
+    boolean modified = false;
+    for (FeatureStore fs : featureStore.values())
+    {
+      modified |= fs.shiftFeatures(shift);
+    }
+    return modified;
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/datamodel/features/SequenceFeaturesI.java b/src/jalview/datamodel/features/SequenceFeaturesI.java
new file mode 100644 (file)
index 0000000..58beca2
--- /dev/null
@@ -0,0 +1,204 @@
+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 one of the
+   * given ontology terms, or is a specialisation of a term in the Sequence
+   * Ontology. Results are returned in no particular guaranteed 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,
+   * returning true if at least one feature was shifted, else false
+   * 
+   * @param shift
+   */
+  abstract boolean shiftFeatures(int shift);
+}
\ No newline at end of file
index 4d09bdc..c3d4e66 100644 (file)
@@ -443,13 +443,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);
       }
     }
@@ -563,33 +577,25 @@ 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();
diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotEntry.java b/src/jalview/datamodel/xdb/uniprot/UniprotEntry.java
new file mode 100755 (executable)
index 0000000..a3537c9
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * 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 jalview.datamodel.PDBEntry;
+
+import java.util.Vector;
+
+/**
+ * Data model for an entry returned from a Uniprot query
+ * 
+ * @see uniprot_mapping.xml
+ */
+public class UniprotEntry
+{
+
+  UniprotSequence sequence;
+
+  Vector<String> name;
+
+  Vector<String> accession;
+
+  Vector<UniprotFeature> feature;
+
+  Vector<PDBEntry> dbrefs;
+
+  UniprotProteinName protName;
+
+  public void setAccession(Vector<String> items)
+  {
+    accession = items;
+  }
+
+  public void setFeature(Vector<UniprotFeature> items)
+  {
+    feature = items;
+  }
+
+  public Vector<UniprotFeature> getFeature()
+  {
+    return feature;
+  }
+
+  public Vector<String> getAccession()
+  {
+    return accession;
+  }
+
+  public void setProtein(UniprotProteinName names)
+  {
+    protName = names;
+  }
+
+  public UniprotProteinName getProtein()
+  {
+    return protName;
+  }
+
+  public void setName(Vector<String> na)
+  {
+    name = na;
+  }
+
+  public Vector<String> getName()
+  {
+    return name;
+  }
+
+  public UniprotSequence getUniprotSequence()
+  {
+    return sequence;
+  }
+
+  public void setUniprotSequence(UniprotSequence seq)
+  {
+    sequence = seq;
+  }
+
+  public Vector<PDBEntry> getDbReference()
+  {
+    return dbrefs;
+  }
+
+  public void setDbReference(Vector<PDBEntry> dbref)
+  {
+    this.dbrefs = dbref;
+  }
+
+}
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..4a359ff
--- /dev/null
@@ -0,0 +1,78 @@
+package jalview.datamodel.xdb.uniprot;
+
+/**
+ * A data model class for binding from Uniprot XML via uniprot_mapping.xml
+ */
+public class UniprotFeature
+{
+  private String type;
+
+  private String description;
+
+  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;
+  }
+}
diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotFile.java b/src/jalview/datamodel/xdb/uniprot/UniprotFile.java
new file mode 100755 (executable)
index 0000000..9cc0391
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * Data model of a retrieved Uniprot entry, as unmarshalled by Castor using a
+ * binding file (uniprot_mapping.xml)
+ */
+public class UniprotFile
+{
+  Vector<UniprotEntry> _items;
+
+  public void setUniprotEntries(Vector<UniprotEntry> items)
+  {
+    _items = items;
+  }
+
+  public Vector<UniprotEntry> getUniprotEntries()
+  {
+    return _items;
+  }
+}
diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java b/src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java
new file mode 100755 (executable)
index 0000000..2335e71
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+/**
+ * Data model for protein name returned from a Uniprot query
+ * 
+ * Protein names are read from the Uniprot XML element
+ * uniprot/entry/protein/recommendedName/fullName
+ * 
+ * @see uniprot_mapping.xml
+ */
+public class UniprotProteinName
+{
+  private Vector<String> names;
+
+  public void setName(Vector<String> names)
+  {
+    this.names = names;
+  }
+
+  public Vector<String> getName()
+  {
+    return names;
+  }
+
+}
diff --git a/src/jalview/datamodel/xdb/uniprot/UniprotSequence.java b/src/jalview/datamodel/xdb/uniprot/UniprotSequence.java
new file mode 100755 (executable)
index 0000000..bdba73f
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * Data model for the sequence returned by a Uniprot query
+ * 
+ * @see uniprot_mapping.xml
+ */
+public class UniprotSequence
+{
+  private String _content = "";
+
+  /**
+   * Sets the content string, omitting any space characters
+   * 
+   * @param seq
+   */
+  public void setContent(String seq)
+  {
+    if (seq != null)
+    {
+      StringBuilder sb = new StringBuilder(seq.length());
+      for (int i = 0; i < seq.length(); i++)
+      {
+        if (seq.charAt(i) != ' ')
+        {
+          sb.append(seq.charAt(i));
+        }
+      }
+      _content = sb.toString();
+    }
+  }
+
+  public String getContent()
+  {
+    return _content;
+  }
+
+}
index 4033dcc..26bd142 100644 (file)
@@ -19,8 +19,10 @@ package jalview.ext.android;
 /*
  * Copied to Jalview September 2016.
  * Only the members of this class required for SparseIntArray were copied.
- * Method binarySearch(short[] array, int size, short value) added to support
+ * Change Log:
+ * Sep 2016: Method binarySearch(short[] array, int size, short value) added to support
  * SparseShortArray.
+ * Jan 2017: EMPTY_DOUBLES added
  */
 class ContainerHelpers
 {
@@ -28,6 +30,8 @@ class ContainerHelpers
 
   static final int[] EMPTY_INTS = new int[0];
 
+  static final double[] EMPTY_DOUBLES = new double[0];
+
   static final long[] EMPTY_LONGS = new long[0];
 
   static final Object[] EMPTY_OBJECTS = new Object[0];
diff --git a/src/jalview/ext/android/SparseDoubleArray.java b/src/jalview/ext/android/SparseDoubleArray.java
new file mode 100644 (file)
index 0000000..eaf059c
--- /dev/null
@@ -0,0 +1,443 @@
+package jalview.ext.android;
+
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * SparseDoubleArray map integers to doubles. Unlike a normal array of integers,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integer to Double, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra
+ * entry object for each mapping.
+ *
+ * <p>
+ * Note that this container keeps its mappings in an array data structure, using
+ * a binary search to find keys. The implementation is not intended to be
+ * appropriate for data structures that may contain large numbers of items. It
+ * is generally slower than a traditional HashMap, since lookups require a
+ * binary search and adds and removes require inserting and deleting entries in
+ * the array. For containers holding up to hundreds of items, the performance
+ * difference is not significant, less than 50%.
+ * </p>
+ *
+ * <p>
+ * It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)<code>.
+ * </p>
+ */
+
+/*
+ * Change log:
+ * Jan 2017 cloned from SparseIntArray for Jalview to support SparseMatrix
+ * - SparseDoubleArray(double[]) constructor added
+ * - multiply() added for more efficient multiply (or divide) of a value
+ */
+public class SparseDoubleArray implements Cloneable
+{
+  private int[] mKeys;
+
+  private double[] mValues;
+
+  private int mSize;
+
+  /**
+   * Creates a new SparseDoubleArray containing no mappings.
+   */
+  public SparseDoubleArray()
+  {
+    this(10);
+  }
+
+  /**
+   * Creates a new SparseDoubleArray containing no mappings that will not
+   * require any additional memory allocation to store the specified number of
+   * mappings. If you supply an initial capacity of 0, the sparse array will be
+   * initialized with a light-weight representation not requiring any additional
+   * array allocations.
+   */
+  public SparseDoubleArray(int initialCapacity)
+  {
+    if (initialCapacity == 0)
+    {
+      mKeys = ContainerHelpers.EMPTY_INTS;
+      mValues = ContainerHelpers.EMPTY_DOUBLES;
+    }
+    else
+    {
+      initialCapacity = idealDoubleArraySize(initialCapacity);
+      mKeys = new int[initialCapacity];
+      mValues = new double[initialCapacity];
+    }
+    mSize = 0;
+  }
+
+  /**
+   * Constructor given an array of double values; stores the non-zero values
+   * 
+   * @param row
+   */
+  public SparseDoubleArray(double[] row)
+  {
+    this();
+    for (int i = 0; i < row.length; i++)
+    {
+      if (row[i] != 0d)
+      {
+        put(i, row[i]);
+      }
+    }
+  }
+
+  @Override
+  public SparseDoubleArray clone()
+  {
+    SparseDoubleArray clone = null;
+    try
+    {
+      clone = (SparseDoubleArray) super.clone();
+      clone.mKeys = mKeys.clone();
+      clone.mValues = mValues.clone();
+    } catch (CloneNotSupportedException cnse)
+    {
+      /* ignore */
+    }
+    return clone;
+  }
+
+  /**
+   * Gets the value mapped from the specified key, or <code>0</code> if no such
+   * mapping has been made.
+   */
+  public double get(int key)
+  {
+    return get(key, 0d);
+  }
+
+  /**
+   * Gets the int mapped from the specified key, or the specified value if no
+   * such mapping has been made.
+   */
+  public double get(int key, double valueIfKeyNotFound)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i < 0)
+    {
+      return valueIfKeyNotFound;
+    }
+    else
+    {
+      return mValues[i];
+    }
+  }
+
+  /**
+   * Removes the mapping from the specified key, if there was any.
+   */
+  public void delete(int key)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      removeAt(i);
+    }
+  }
+
+  /**
+   * Removes the mapping at the given index.
+   */
+  public void removeAt(int index)
+  {
+    System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+    System.arraycopy(mValues, index + 1, mValues, index, mSize
+            - (index + 1));
+    mSize--;
+  }
+
+  /**
+   * Adds a mapping from the specified key to the specified value, replacing the
+   * previous mapping from the specified key if there was one.
+   */
+  public void put(int key, double value)
+  {
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      mValues[i] = value;
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealDoubleArraySize(mSize + 1);
+        int[] nkeys = new int[n];
+        double[] nvalues = new double[n];
+        // Log.e("SparseDoubleArray", "grow " + mKeys.length + " to " + n);
+        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+        mKeys = nkeys;
+        mValues = nvalues;
+      }
+      if (mSize - i != 0)
+      {
+        // Log.e("SparseDoubleArray", "move " + (mSize - i));
+        System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+        System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+      }
+      mKeys[i] = key;
+      mValues[i] = value;
+      mSize++;
+    }
+  }
+
+  /**
+   * Returns the number of key-value mappings that this SparseDoubleArray
+   * currently stores.
+   */
+  public int size()
+  {
+    return mSize;
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the key from
+   * the <code>index</code>th key-value mapping that this SparseDoubleArray
+   * stores.
+   *
+   * <p>
+   * The keys corresponding to indices in ascending order are guaranteed to be
+   * in ascending order, e.g., <code>keyAt(0)</code> will return the smallest
+   * key and <code>keyAt(size()-1)</code> will return the largest key.
+   * </p>
+   */
+  public int keyAt(int index)
+  {
+    return mKeys[index];
+  }
+
+  /**
+   * Given an index in the range <code>0...size()-1</code>, returns the value
+   * from the <code>index</code>th key-value mapping that this SparseDoubleArray
+   * stores.
+   *
+   * <p>
+   * The values corresponding to indices in ascending order are guaranteed to be
+   * associated with keys in ascending order, e.g., <code>valueAt(0)</code> will
+   * return the value associated with the smallest key and
+   * <code>valueAt(size()-1)</code> will return the value associated with the
+   * largest key.
+   * </p>
+   */
+  public double valueAt(int index)
+  {
+    return mValues[index];
+  }
+
+  /**
+   * Returns the index for which {@link #keyAt} would return the specified key,
+   * or a negative number if the specified key is not mapped.
+   */
+  public int indexOfKey(int key)
+  {
+    return ContainerHelpers.binarySearch(mKeys, mSize, key);
+  }
+
+  /**
+   * Returns an index for which {@link #valueAt} would return the specified key,
+   * or a negative number if no keys map to the specified value. Beware that
+   * this is a linear search, unlike lookups by key, and that multiple keys can
+   * map to the same value and this will find only one of them.
+   */
+  public int indexOfValue(double value)
+  {
+    for (int i = 0; i < mSize; i++)
+    {
+      if (mValues[i] == value)
+      {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Removes all key-value mappings from this SparseDoubleArray.
+   */
+  public void clear()
+  {
+    mSize = 0;
+  }
+
+  /**
+   * Puts a key/value pair into the array, optimizing for the case where the key
+   * is greater than all existing keys in the array.
+   */
+  public void append(int key, double value)
+  {
+    if (mSize != 0 && key <= mKeys[mSize - 1])
+    {
+      put(key, value);
+      return;
+    }
+    int pos = mSize;
+    if (pos >= mKeys.length)
+    {
+      int n = idealDoubleArraySize(pos + 1);
+      int[] nkeys = new int[n];
+      double[] nvalues = new double[n];
+      // Log.e("SparseDoubleArray", "grow " + mKeys.length + " to " + n);
+      System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+      System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+      mKeys = nkeys;
+      mValues = nvalues;
+    }
+    mKeys[pos] = key;
+    mValues[pos] = value;
+    mSize = pos + 1;
+  }
+
+  /**
+   * Created by analogy with
+   * com.android.internal.util.ArrayUtils#idealLongArraySize
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealDoubleArraySize(int need)
+  {
+    return idealByteArraySize(need * 8) / 8;
+  }
+
+  /**
+   * Inlined here by copying from com.android.internal.util.ArrayUtils
+   * 
+   * @param i
+   * @return
+   */
+  public static int idealByteArraySize(int need)
+  {
+    for (int i = 4; i < 32; i++)
+    {
+      if (need <= (1 << i) - 12)
+      {
+        return (1 << i) - 12;
+      }
+    }
+
+    return need;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>
+   * This implementation composes a string by iterating over its mappings.
+   */
+  @Override
+  public String toString()
+  {
+    if (size() <= 0)
+    {
+      return "{}";
+    }
+    StringBuilder buffer = new StringBuilder(mSize * 28);
+    buffer.append('{');
+    for (int i = 0; i < mSize; i++)
+    {
+      if (i > 0)
+      {
+        buffer.append(", ");
+      }
+      int key = keyAt(i);
+      buffer.append(key);
+      buffer.append('=');
+      double value = valueAt(i);
+      buffer.append(value);
+    }
+    buffer.append('}');
+    return buffer.toString();
+  }
+
+  /**
+   * Method (copied from put) added for Jalview to efficiently increment a key's
+   * value if present, else add it with the given value. This avoids a double
+   * binary search (once to get the value, again to put the updated value).
+   * 
+   * @param key
+   * @oparam toAdd
+   * @return the new value for the key
+   */
+  public double add(int key, double toAdd)
+  {
+    double newValue = toAdd;
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      mValues[i] += toAdd;
+      newValue = mValues[i];
+    }
+    else
+    {
+      i = ~i;
+      if (mSize >= mKeys.length)
+      {
+        int n = idealDoubleArraySize(mSize + 1);
+        int[] nkeys = new int[n];
+        double[] nvalues = new double[n];
+        System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+        System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+        mKeys = nkeys;
+        mValues = nvalues;
+      }
+      if (mSize - i != 0)
+      {
+        System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+        System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+      }
+      mKeys[i] = key;
+      mValues[i] = toAdd;
+      mSize++;
+    }
+    return newValue;
+  }
+
+  /**
+   * Method added for Jalview to efficiently multiply a key's value if present,
+   * else do nothing. This avoids a double binary search (once to get the value,
+   * again to put the updated value).
+   * 
+   * @param key
+   * @oparam toAdd
+   * @return the new value for the key
+   */
+  public double divide(int key, double divisor)
+  {
+    double newValue = 0d;
+    if (divisor == 0d)
+    {
+      return newValue;
+    }
+    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+    if (i >= 0)
+    {
+      mValues[i] /= divisor;
+      newValue = mValues[i];
+    }
+    return newValue;
+  }
+}
index dc000c6..6d031b7 100644 (file)
@@ -24,6 +24,9 @@ import jalview.datamodel.SequenceFeature;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 
+import java.util.HashMap;
+import java.util.Map;
+
 import com.stevesoft.pat.Regex;
 
 /**
@@ -44,6 +47,13 @@ public class EnsemblCdna extends EnsemblSeqProxy
   private static final Regex ACCESSION_REGEX = new Regex(
           "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)");
 
+  private static Map<String, String> params = new HashMap<String, String>();
+
+  static
+  {
+    params.put("object_type", "transcript");
+  }
+
   /*
    * fetch exon features on genomic sequence (to identify the cdna regions)
    * and cds and variation features (to retain)
@@ -128,4 +138,14 @@ public class EnsemblCdna extends EnsemblSeqProxy
     return false;
   }
 
+  /**
+   * Parameter object_type=cdna added to ensure cdna and not peptide is returned
+   * (JAL-2529)
+   */
+  @Override
+  protected Map<String, String> getAdditionalParameters()
+  {
+    return params;
+  }
+
 }
index 24e3e95..915fa0a 100644 (file)
@@ -26,6 +26,7 @@ import jalview.datamodel.AlignmentI;
 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;
@@ -190,7 +191,22 @@ public class EnsemblGene extends EnsemblSeqProxy
           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 
@@ -267,22 +283,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);
     }
   }
 
@@ -332,6 +346,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     {
       splices = findFeatures(gene, SequenceOntologyI.CDS, parentId);
     }
+    SequenceFeatures.sortFeatures(splices, true);
 
     int transcriptLength = 0;
     final char[] geneChars = gene.getSequence();
@@ -381,7 +396,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     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);
 
     /*
@@ -422,19 +437,18 @@ public class EnsemblGene extends EnsemblSeqProxy
     List<SequenceFeature> transcriptFeatures = new ArrayList<SequenceFeature>();
 
     String parentIdentifier = GENE_PREFIX + accId;
-    SequenceFeature[] sfs = geneSequence.getSequenceFeatures();
+    // todo optimise here by transcript type!
+    List<SequenceFeature> sfs = geneSequence.getFeatures()
+            .getPositionalFeatures();
 
-    if (sfs != null)
+    for (SequenceFeature sf : sfs)
     {
-      for (SequenceFeature sf : sfs)
+      if (isTranscript(sf.getType()))
       {
-        if (isTranscript(sf.getType()))
+        String parent = (String) sf.getValue(PARENT);
+        if (parentIdentifier.equals(parent))
         {
-          String parent = (String) sf.getValue(PARENT);
-          if (parentIdentifier.equals(parent))
-          {
-            transcriptFeatures.add(sf);
-          }
+          transcriptFeatures.add(sf);
         }
       }
     }
index ab3b197..2437588 100644 (file)
@@ -57,13 +57,13 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   /*
    * update these constants when Jalview has been checked / updated for
-   * changes to Ensembl REST API
+   * 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 = "4.8";
+  private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "5.0";
 
-  private static final String LATEST_ENSEMBL_REST_VERSION = "4.8";
+  private static final String LATEST_ENSEMBL_REST_VERSION = "5.0";
 
   private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
 
@@ -76,6 +76,9 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   private final static long VERSION_RETEST_INTERVAL = 1000L * 3600; // 1 hr
 
+  private static final Regex PROTEIN_REGEX = new Regex(
+          "(ENS)([A-Z]{3}|)P[0-9]{11}$");
+
   private static final Regex TRANSCRIPT_REGEX = new Regex(
           "(ENS)([A-Z]{3}|)T[0-9]{11}$");
 
@@ -125,6 +128,18 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   /**
    * 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
@@ -513,9 +528,11 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
       boolean laterVersion = StringUtils.compareVersions(version, expected) == 1;
       if (laterVersion)
       {
-        System.err.println(String.format(
-                "Expected %s REST version %s but found %s, see %s",
-                getDbSource(), expected, version, REST_CHANGE_LOG));
+        System.err
+                .println(String
+                        .format("EnsemblRestClient expected %s REST version %s but found %s, see %s",
+                                getDbSource(), expected, version,
+                                REST_CHANGE_LOG));
       }
       info.restVersion = version;
     } catch (Throwable t)
index 233707b..79d6c0a 100644 (file)
@@ -30,6 +30,7 @@ import jalview.datamodel.DBRefSource;
 import jalview.datamodel.Mapping;
 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;
@@ -37,8 +38,8 @@ 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.IOException;
 import java.net.MalformedURLException;
@@ -46,8 +47,9 @@ 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 java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Base class for Ensembl sequence fetchers
@@ -468,11 +470,31 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     urlstring.append("?type=").append(getSourceEnsemblType().getType());
     urlstring.append(("&Accept=text/x-fasta"));
 
+    Map<String, String> params = getAdditionalParameters();
+    if (params != null)
+    {
+      for (Entry<String, String> entry : params.entrySet())
+      {
+        urlstring.append("&").append(entry.getKey()).append("=")
+                .append(entry.getValue());
+      }
+    }
+
     URL url = new URL(urlstring.toString());
     return url;
   }
 
   /**
+   * Override this method to add any additional x=y URL parameters needed
+   * 
+   * @return
+   */
+  protected Map<String, String> getAdditionalParameters()
+  {
+    return null;
+  }
+
+  /**
    * A sequence/id POST request currently allows up to 50 queries
    * 
    * @see http://rest.ensembl.org/documentation/info/sequence_id_post
@@ -536,8 +558,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
           String accId, int start)
   {
-    SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
-    if (sfs == null)
+    // SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
+    List<SequenceFeature> sfs = sourceSequence.getFeatures()
+            .getPositionalFeatures();
+    if (sfs.isEmpty())
     {
       return null;
     }
@@ -607,7 +631,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[] { start,
         start + mappedLength - 1 });
@@ -658,13 +683,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);
 
       /*
@@ -763,8 +790,10 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       return false;
     }
 
-    // long start = System.currentTimeMillis();
-    SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
+    long start = System.currentTimeMillis();
+    // SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
+    List<SequenceFeature> sfs = sourceSequence.getFeatures()
+            .getPositionalFeatures();
     MapList mapping = getGenomicRangesFromFeatures(sourceSequence,
             accessionId, targetSequence.getStart());
     if (mapping == null)
@@ -774,10 +803,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;
   }
 
@@ -786,13 +815,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();
@@ -802,10 +831,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))
       {
@@ -817,33 +846,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
@@ -885,35 +887,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>();
 
-    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.equals(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 2d2d10e..5de554b 100644 (file)
@@ -24,7 +24,7 @@ import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
@@ -35,6 +35,7 @@ import jalview.structure.AtomSpec;
 import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.MessageManager;
 
 import java.awt.Color;
 import java.awt.Container;
@@ -44,6 +45,7 @@ import java.io.File;
 import java.net.URL;
 import java.security.AccessControlException;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
@@ -76,8 +78,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   Hashtable<String, String> chainFile;
 
-  public String fileLoadingError;
-
   /*
    * the default or current model displayed if the model cannot be identified
    * from the selection message
@@ -164,13 +164,14 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   public void closeViewer()
   {
     // remove listeners for all structures in viewer
-    getSsm().removeStructureViewerListener(this, this.getPdbFile());
+    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
     viewer.dispose();
     lastCommand = null;
     viewer = null;
     releaseUIResources();
   }
 
+  @Override
   public void colourByChain()
   {
     colourBySequence = false;
@@ -180,6 +181,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     evalStateCommand("select *;color chain");
   }
 
+  @Override
   public void colourByCharge()
   {
     colourBySequence = false;
@@ -220,29 +222,19 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
    *          TODO
    */
   public void superposeStructures(AlignmentI alignment, int refStructure,
-          ColumnSelection hiddenCols)
+          HiddenColumns hiddenCols)
   {
     superposeStructures(new AlignmentI[] { alignment },
             new int[] { refStructure },
-            new ColumnSelection[] { hiddenCols });
+ new HiddenColumns[] { hiddenCols });
   }
 
   /**
-   * Construct and send a command to align structures against a reference
-   * structure, based on one or more sequence alignments
-   * 
-   * @param _alignment
-   *          an array of alignments to process
-   * @param _refStructure
-   *          an array of corresponding reference structures (index into pdb
-   *          file array); if a negative value is passed, the first PDB file
-   *          mapped to an alignment sequence is used as the reference for
-   *          superposition
-   * @param _hiddenCols
-   *          an array of corresponding hidden columns for each alignment
+   * {@inheritDoc}
    */
-  public void superposeStructures(AlignmentI[] _alignment,
-          int[] _refStructure, ColumnSelection[] _hiddenCols)
+  @Override
+  public String superposeStructures(AlignmentI[] _alignment,
+          int[] _refStructure, HiddenColumns[] _hiddenCols)
   {
     while (viewer.isScriptExecuting())
     {
@@ -258,10 +250,10 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
      * get the distinct structure files modelled
      * (a file with multiple chains may map to multiple sequences)
      */
-    String[] files = getPdbFile();
+    String[] files = getStructureFiles();
     if (!waitForFileLoad(files))
     {
-      return;
+      return null;
     }
 
     StringBuilder selectioncom = new StringBuilder(256);
@@ -277,6 +269,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       nSeconds = " " + (2.0 / files.length) + " ";
       // if (nSeconds).substring(0,5)+" ";
     }
+
     // see JAL-1345 - should really automatically turn off the animation for
     // large numbers of structures, but Jmol doesn't seem to allow that.
     // nSeconds = " ";
@@ -285,7 +278,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     {
       int refStructure = _refStructure[a];
       AlignmentI alignment = _alignment[a];
-      ColumnSelection hiddenCols = _hiddenCols[a];
+      HiddenColumns hiddenCols = _hiddenCols[a];
       if (a > 0
               && selectioncom.length() > 0
               && !selectioncom.substring(selectioncom.length() - 1).equals(
@@ -302,14 +295,16 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       }
 
       /*
-       * 'matched' array will hold 'true' for visible alignment columns where
+       * 'matched' bit j will be set for visible alignment columns j where
        * all sequences have a residue with a mapping to the PDB structure
        */
-      // TODO could use a BitSet for matched
-      boolean matched[] = new boolean[alignment.getWidth()];
-      for (int m = 0; m < matched.length; m++)
+      BitSet matched = new BitSet();
+      for (int m = 0; m < alignment.getWidth(); m++)
       {
-        matched[m] = (hiddenCols != null) ? hiddenCols.isVisible(m) : true;
+        if (hiddenCols == null || hiddenCols.isVisible(m))
+        {
+          matched.set(m);
+        }
       }
 
       SuperposeData[] structures = new SuperposeData[files.length];
@@ -334,17 +329,12 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       }
 
       String[] selcom = new String[files.length];
-      int nmatched = 0;
-      for (boolean b : matched)
-      {
-        if (b)
-        {
-          nmatched++;
-        }
-      }
+      int nmatched = matched.cardinality();
       if (nmatched < 4)
       {
-        // TODO: bail out here because superposition illdefined?
+        return (MessageManager.formatMessage(
+"label.insufficient_residues",
+                nmatched));
       }
 
       /*
@@ -359,35 +349,35 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
           boolean run = false;
           StringBuilder molsel = new StringBuilder();
           molsel.append("{");
-          for (int r = 0; r < matched.length; r++)
+
+          int nextColumnMatch = matched.nextSetBit(0);
+          while (nextColumnMatch != -1)
           {
-            if (matched[r])
+            int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch];
+            if (lpos != pdbResNo - 1)
             {
-              int pdbResNo = structures[pdbfnum].pdbResNo[r];
-              if (lpos != pdbResNo - 1)
+              // discontinuity
+              if (lpos != -1)
               {
-                // discontinuity
-                if (lpos != -1)
-                {
-                  molsel.append(lpos);
-                  molsel.append(chainCd);
-                  molsel.append("|");
-                }
-                run = false;
+                molsel.append(lpos);
+                molsel.append(chainCd);
+                molsel.append("|");
               }
-              else
+              run = false;
+            }
+            else
+            {
+              // continuous run - and lpos >-1
+              if (!run)
               {
-                // continuous run - and lpos >-1
-                if (!run)
-                {
-                  // at the beginning, so add dash
-                  molsel.append(lpos);
-                  molsel.append("-");
-                }
-                run = true;
+                // at the beginning, so add dash
+                molsel.append(lpos);
+                molsel.append("-");
               }
-              lpos = pdbResNo;
+              run = true;
             }
+            lpos = pdbResNo;
+            nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
           }
           /*
            * add final selection phrase
@@ -473,6 +463,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
               + selectioncom.toString() + "); cartoons; ");
       // evalStateCommand("select *; backbone; select "+selcom.toString()+"; cartoons; center "+selcom.toString());
     }
+
+    return null;
   }
 
   public void evalStateCommand(String command)
@@ -487,35 +479,15 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   }
 
   /**
-   * colour any structures associated with sequences in the given alignment
-   * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
-   * if colourBySequence is enabled.
+   * Sends a set of colour commands to the structure viewer
+   * 
+   * @param colourBySequenceCommands
    */
-  public void colourBySequence(AlignmentViewPanel alignmentv)
+  @Override
+  protected void colourBySequence(
+          StructureMappingcommandSet[] colourBySequenceCommands)
   {
-    boolean showFeatures = alignmentv.getAlignViewport()
-            .isShowSequenceFeatures();
-    if (!colourBySequence || !isLoadingFinished())
-    {
-      return;
-    }
-    if (getSsm() == null)
-    {
-      return;
-    }
-    String[] files = getPdbFile();
-
-    SequenceRenderer sr = getSequenceRenderer(alignmentv);
-
-    FeatureRenderer fr = null;
-    if (showFeatures)
-    {
-      fr = getFeatureRenderer(alignmentv);
-    }
-    AlignmentI alignment = alignmentv.getAlignment();
-
-    for (jalview.structure.StructureMappingcommandSet cpdbbyseq : getColourBySequenceCommands(
-            files, sr, fr, alignment))
+    for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
     {
       for (String cbyseq : cpdbbyseq.commands)
       {
@@ -527,16 +499,15 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   /**
    * @param files
    * @param sr
-   * @param fr
-   * @param alignment
+   * @param viewPanel
    * @return
    */
+  @Override
   protected StructureMappingcommandSet[] getColourBySequenceCommands(
-          String[] files, SequenceRenderer sr, FeatureRenderer fr,
-          AlignmentI alignment)
+          String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
   {
     return JmolCommands.getColourBySequenceCommand(getSsm(), files,
-            getSequence(), sr, fr, alignment);
+            getSequence(), sr, viewPanel);
   }
 
   /**
@@ -598,17 +569,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   }
 
   /**
-   * returns the current featureRenderer that should be used to colour the
-   * structures
-   * 
-   * @param alignment
-   * 
-   * @return
-   */
-  public abstract FeatureRenderer getFeatureRenderer(
-          AlignmentViewPanel alignment);
-
-  /**
    * instruct the Jalview binding to update the pdbentries vector if necessary
    * prior to matching the jmol view's contents to the list of structure files
    * Jalview knows about.
@@ -617,7 +577,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   private int getModelNum(String modelFileName)
   {
-    String[] mfn = getPdbFile();
+    String[] mfn = getStructureFiles();
     if (mfn == null)
     {
       return -1;
@@ -641,8 +601,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   // ////////////////////////////////
   // /StructureListener
-  @Override
-  public synchronized String[] getPdbFile()
+  // @Override
+  public synchronized String[] getPdbFilex()
   {
     if (viewer == null)
     {
@@ -707,6 +667,32 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return modelFileNames;
   }
 
+  @Override
+  public synchronized String[] getStructureFiles()
+  {
+    List<String> mset = new ArrayList<String>();
+    if (viewer == null)
+    {
+      return new String[0];
+    }
+
+    if (modelFileNames == null)
+    {
+      int modelCount = viewer.ms.mc;
+      String filePath = null;
+      for (int i = 0; i < modelCount; ++i)
+      {
+        filePath = viewer.ms.getModelFileName(i);
+        if (!mset.contains(filePath))
+        {
+          mset.add(filePath);
+        }
+      }
+      modelFileNames = mset.toArray(new String[mset.size()]);
+    }
+
+    return modelFileNames;
+  }
   /**
    * map from string to applet
    */
@@ -717,16 +703,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return null;
   }
 
-  /**
-   * returns the current sequenceRenderer that should be used to colour the
-   * structures
-   * 
-   * @param alignment
-   * 
-   * @return
-   */
-  public abstract SequenceRenderer getSequenceRenderer(
-          AlignmentViewPanel alignment);
+  
 
   // ///////////////////////////////
   // JmolStatusListener
@@ -1084,7 +1061,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     chainNames = new ArrayList<String>();
     chainFile = new Hashtable<String, String>();
     boolean notifyLoaded = false;
-    String[] modelfilenames = getPdbFile();
+    String[] modelfilenames = getStructureFiles();
     // first check if we've lost any structures
     if (oldmodels != null && oldmodels.length > 0)
     {
@@ -1286,6 +1263,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   }
 
+  @Override
   public void setJalviewColourScheme(ColourSchemeI cs)
   {
     colourBySequence = false;
@@ -1300,10 +1278,12 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     command.append("select *;color white;");
     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
             false);
-    for (String res : residueSet)
+    for (String resName : residueSet)
     {
-      Color col = cs.findColour(res.charAt(0));
-      command.append("select " + res + ";color[" + col.getRed() + ","
+      char res = resName.length() == 3 ? ResidueProperties
+              .getSingleCharacterCode(resName) : resName.charAt(0);
+      Color col = cs.findColour(res, 0, null, null, 0f);
+      command.append("select " + resName + ";color[" + col.getRed() + ","
               + col.getGreen() + "," + col.getBlue() + "];");
     }
 
@@ -1400,6 +1380,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   protected org.jmol.api.JmolAppConsoleInterface console = null;
 
+  @Override
   public void setBackgroundColour(java.awt.Color col)
   {
     jmolHistory(false);
index d5676c5..3e7ca59 100644 (file)
  */
 package jalview.ext.jmol;
 
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 
 import java.awt.Color;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Routines for generating Jmol commands for Jalview/Jmol binding another
@@ -50,11 +55,15 @@ public class JmolCommands
    */
   public static StructureMappingcommandSet[] getColourBySequenceCommand(
           StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
-          AlignmentI alignment)
+          SequenceI[][] sequence, SequenceRenderer sr,
+          AlignmentViewPanel viewPanel)
   {
-
-    ArrayList<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
+    AlignViewportI viewport = viewPanel.getAlignViewport();
+    HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
+    AlignmentI al = viewport.getAlignment();
+    List<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
 
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
@@ -74,9 +83,9 @@ public class JmolCommands
         for (int sp, m = 0; m < mapping.length; m++)
         {
           if (mapping[m].getSequence() == sequence[pdbfnum][s]
-                  && (sp = alignment.findIndex(sequence[pdbfnum][s])) > -1)
+                  && (sp = al.findIndex(sequence[pdbfnum][s])) > -1)
           {
-            SequenceI asp = alignment.getSequenceAt(sp);
+            SequenceI asp = al.getSequenceAt(sp);
             for (int r = 0; r < asp.getLength(); r++)
             {
               // no mapping to gaps in sequence
@@ -93,12 +102,17 @@ public class JmolCommands
 
               lastPos = pos;
 
-              Color col = sr.getResidueBoxColour(sequence[pdbfnum][s], r);
+              Color col = sr.getResidueColour(sequence[pdbfnum][s], r,
+                      finder);
 
-              if (fr != null)
+              /*
+               * shade hidden regions darker
+               */
+              if (!cs.isVisible(r))
               {
-                col = fr.findFeatureColour(col, sequence[pdbfnum][s], r);
+                col = Color.GRAY;
               }
+
               String newSelcom = (mapping[m].getChain() != " " ? ":"
                       + mapping[m].getChain() : "")
                       + "/"
index d62cc3c..f3c9c1e 100644 (file)
@@ -1,6 +1,6 @@
 package jalview.ext.rbvi.chimera;
 
-import jalview.util.RangeComparator;
+import jalview.util.IntRangeComparator;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -107,7 +107,7 @@ public class AtomSpecModel
         /*
          * 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];
index 07c0015..e9ce49b 100644 (file)
  */
 package jalview.ext.rbvi.chimera;
 
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
@@ -57,16 +61,16 @@ public class ChimeraCommands
    * @param sequence
    * @param sr
    * @param fr
-   * @param alignment
+   * @param viewPanel
    * @return
    */
-  public static StructureMappingcommandSet getColourBySequenceCommand(
+  public static StructureMappingcommandSet[] getColourBySequenceCommand(
           StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
-          AlignmentI alignment)
+          SequenceI[][] sequence, SequenceRenderer sr,
+          AlignmentViewPanel viewPanel)
   {
-    Map<Object, AtomSpecModel> colourMap = buildColoursMap(
-            ssm, files, sequence, sr, fr, alignment);
+    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
+            sequence, sr, viewPanel);
 
     List<String> colourCommands = buildColourCommands(colourMap);
 
@@ -74,7 +78,7 @@ public class ChimeraCommands
             ChimeraCommands.class, null,
             colourCommands.toArray(new String[colourCommands.size()]));
 
-    return cs;
+    return new StructureMappingcommandSet[] { cs };
   }
 
   /**
@@ -113,8 +117,7 @@ public class ChimeraCommands
       }
       sb.append("color ").append(colourCode).append(" ");
       firstColour = false;
-      final AtomSpecModel colourData = colourMap
-              .get(colour);
+      final AtomSpecModel colourData = colourMap.get(colour);
       sb.append(colourData.getAtomSpec());
     }
     commands.add(sb.toString());
@@ -174,9 +177,8 @@ public class ChimeraCommands
 
   /**
    * <pre>
-   * Build a data structure which maps contiguous subsequences for each colour. 
-   * This generates a data structure from which we can easily generate the 
-   * Chimera command for colour by sequence.
+   * Build a data structure which records contiguous subsequences for each colour. 
+   * From this we can easily generate the Chimera command for colour by sequence.
    * Color
    *     Model number
    *         Chain
@@ -186,11 +188,17 @@ public class ChimeraCommands
    */
   protected static Map<Object, AtomSpecModel> buildColoursMap(
           StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
-          AlignmentI alignment)
+          SequenceI[][] sequence, SequenceRenderer sr,
+          AlignmentViewPanel viewPanel)
   {
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
+    AlignViewportI viewport = viewPanel.getAlignViewport();
+    HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
+    AlignmentI al = viewport.getAlignment();
     Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<Object, AtomSpecModel>();
     Color lastColour = null;
+
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
@@ -208,9 +216,9 @@ public class ChimeraCommands
         {
           final SequenceI seq = sequence[pdbfnum][s];
           if (mapping[m].getSequence() == seq
-                  && (sp = alignment.findIndex(seq)) > -1)
+                  && (sp = al.findIndex(seq)) > -1)
           {
-            SequenceI asp = alignment.getSequenceAt(sp);
+            SequenceI asp = al.getSequenceAt(sp);
             for (int r = 0; r < asp.getLength(); r++)
             {
               // no mapping to gaps in sequence
@@ -225,7 +233,16 @@ public class ChimeraCommands
                 continue;
               }
 
-              Color colour = sr.getResidueColour(seq, r, fr);
+              Color colour = sr.getResidueColour(seq, r, finder);
+
+              /*
+               * darker colour for hidden regions
+               */
+              if (!cs.isVisible(r))
+              {
+                colour = Color.GRAY;
+              }
+
               final String chain = mapping[m].getChain();
 
               /*
@@ -240,7 +257,7 @@ public class ChimeraCommands
               {
                 if (startPos != -1)
                 {
-                  addRange(colourMap, lastColour, pdbfnum, startPos,
+                  addColourRange(colourMap, lastColour, pdbfnum, startPos,
                           lastPos, lastChain);
                 }
                 startPos = pos;
@@ -252,8 +269,8 @@ public class ChimeraCommands
             // final colour range
             if (lastColour != null)
             {
-              addRange(colourMap, lastColour, pdbfnum, startPos,
-                      lastPos, lastChain);
+              addColourRange(colourMap, lastColour, pdbfnum, startPos, lastPos,
+                      lastChain);
             }
             // break;
           }
@@ -273,7 +290,7 @@ public class ChimeraCommands
    * @param endPos
    * @param chain
    */
-  protected static void addRange(Map<Object, AtomSpecModel> map,
+  protected static void addColourRange(Map<Object, AtomSpecModel> map,
           Object key, int model, int startPos, int endPos, String chain)
   {
     /*
@@ -297,16 +314,15 @@ public class ChimeraCommands
    * @param ssm
    * @param files
    * @param seqs
-   * @param fr
-   * @param alignment
+   * @param viewPanel
    * @return
    */
   public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
           StructureSelectionManager ssm, String[] files,
-          SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
+          SequenceI[][] seqs, AlignmentViewPanel viewPanel)
   {
     Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
-            ssm, files, seqs, fr, alignment);
+            ssm, files, seqs, viewPanel);
 
     List<String> commands = buildSetAttributeCommands(featureMap);
 
@@ -326,22 +342,28 @@ public class ChimeraCommands
    * @param ssm
    * @param files
    * @param seqs
-   * @param fr
-   * @param alignment
+   * @param viewPanel
    * @return
    */
   protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
           StructureSelectionManager ssm, String[] files,
-          SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
+          SequenceI[][] seqs, AlignmentViewPanel viewPanel)
   {
     Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
 
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    if (fr == null)
+    {
+      return theMap;
+    }
+
     List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
     if (visibleFeatures.isEmpty())
     {
       return theMap;
     }
-    
+
+    AlignmentI alignment = viewPanel.getAlignment();
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
@@ -389,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();
@@ -405,7 +423,7 @@ public class ChimeraCommands
        */
       boolean isFromViewer = JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
               .equals(sf.getFeatureGroup());
-      if (isFromViewer || !visibleFeatures.contains(type))
+      if (isFromViewer)
       {
         continue;
       }
@@ -432,7 +450,7 @@ public class ChimeraCommands
         }
         for (int[] range : mappedRanges)
         {
-          addRange(featureValues, value, modelNumber, range[0], range[1],
+          addColourRange(featureValues, value, modelNumber, range[0], range[1],
                   mapping.getChain());
         }
       }
@@ -474,10 +492,13 @@ public class ChimeraCommands
         /*
          * for each distinct value recorded for this feature type,
          * add a command to set the attribute on the mapped residues
+         * Put values in single quotes, encoding any embedded single quotes
          */
         StringBuilder sb = new StringBuilder(128);
-        sb.append("setattr r ").append(attributeName).append(" \"")
-                .append(value.toString()).append("\" ");
+        String featureValue = value.toString();
+        featureValue = featureValue.replaceAll("\\'", "&#39;");
+        sb.append("setattr r ").append(attributeName).append(" '")
+                .append(featureValue).append("' ");
         sb.append(values.get(value).getAtomSpec());
         commands.add(sb.toString());
       }
index 75ddc9c..b954677 100644 (file)
 package jalview.ext.rbvi.chimera;
 
 import jalview.api.AlignmentViewPanel;
-import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SearchResultMatchI;
-import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.httpserver.AbstractRequestHandler;
@@ -49,6 +48,7 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.BindException;
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.Hashtable;
 import java.util.LinkedHashMap;
@@ -103,8 +103,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    */
   private boolean loadingFinished = true;
 
-  public String fileLoadingError;
-
   /*
    * Map of ChimeraModel objects keyed by PDB full local file name
    */
@@ -174,13 +172,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       if (getSsm() != null)
       {
         getSsm().addStructureViewerListener(this);
-        // ssm.addSelectionListener(this);
-        FeatureRenderer fr = getFeatureRenderer(null);
-        if (fr != null)
-        {
-          fr.featuresAdded();
-        }
-        refreshGUI();
       }
       return true;
     } catch (Exception q)
@@ -256,18 +247,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Construct a title string for the viewer window based on the data Jalview
-   * knows about
-   * 
-   * @param verbose
-   * @return
-   */
-  public String getViewerTitle(boolean verbose)
-  {
-    return getViewerTitle("Chimera", verbose);
-  }
-
-  /**
    * Tells Chimera to display only the specified chains
    * 
    * @param toshow
@@ -310,7 +289,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    */
   public void closeViewer(boolean closeChimera)
   {
-    getSsm().removeStructureViewerListener(this, this.getPdbFile());
+    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
     if (closeChimera)
     {
       viewer.exitChimera();
@@ -329,6 +308,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     releaseUIResources();
   }
 
+  @Override
   public void colourByChain()
   {
     colourBySequence = false;
@@ -344,6 +324,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * <li>all others - white</li>
    * </ul>
    */
+  @Override
   public void colourByCharge()
   {
     colourBySequence = false;
@@ -352,28 +333,18 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Construct and send a command to align structures against a reference
-   * structure, based on one or more sequence alignments
-   * 
-   * @param _alignment
-   *          an array of alignments to process
-   * @param _refStructure
-   *          an array of corresponding reference structures (index into pdb
-   *          file array); if a negative value is passed, the first PDB file
-   *          mapped to an alignment sequence is used as the reference for
-   *          superposition
-   * @param _hiddenCols
-   *          an array of corresponding hidden columns for each alignment
+   * {@inheritDoc}
    */
-  public void superposeStructures(AlignmentI[] _alignment,
-          int[] _refStructure, ColumnSelection[] _hiddenCols)
+  @Override
+  public String superposeStructures(AlignmentI[] _alignment,
+          int[] _refStructure, HiddenColumns[] _hiddenCols)
   {
     StringBuilder allComs = new StringBuilder(128);
-    String[] files = getPdbFile();
+    String[] files = getStructureFiles();
 
     if (!waitForFileLoad(files))
     {
-      return;
+      return null;
     }
 
     refreshPdbEntries();
@@ -382,7 +353,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     {
       int refStructure = _refStructure[a];
       AlignmentI alignment = _alignment[a];
-      ColumnSelection hiddenCols = _hiddenCols[a];
+      HiddenColumns hiddenCols = _hiddenCols[a];
 
       if (refStructure >= files.length)
       {
@@ -392,13 +363,16 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       }
 
       /*
-       * 'matched' array will hold 'true' for visible alignment columns where
+       * 'matched' bit i will be set for visible alignment columns i where
        * all sequences have a residue with a mapping to the PDB structure
        */
-      boolean matched[] = new boolean[alignment.getWidth()];
-      for (int m = 0; m < matched.length; m++)
+      BitSet matched = new BitSet();
+      for (int m = 0; m < alignment.getWidth(); m++)
       {
-        matched[m] = (hiddenCols != null) ? hiddenCols.isVisible(m) : true;
+        if (hiddenCols == null || hiddenCols.isVisible(m))
+        {
+          matched.set(m);
+        }
       }
 
       SuperposeData[] structures = new SuperposeData[files.length];
@@ -422,17 +396,11 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
         refStructure = candidateRefStructure;
       }
 
-      int nmatched = 0;
-      for (boolean b : matched)
-      {
-        if (b)
-        {
-          nmatched++;
-        }
-      }
+      int nmatched = matched.cardinality();
       if (nmatched < 4)
       {
-        // TODO: bail out here because superposition illdefined?
+        return MessageManager.formatMessage("label.insufficient_residues",
+                nmatched);
       }
 
       /*
@@ -445,41 +413,41 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
         int lpos = -1;
         boolean run = false;
         StringBuilder molsel = new StringBuilder();
-        for (int r = 0; r < matched.length; r++)
+
+        int nextColumnMatch = matched.nextSetBit(0);
+        while (nextColumnMatch != -1)
         {
-          if (matched[r])
+          int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
+          if (lpos != pdbResNum - 1)
           {
-            int pdbResNum = structures[pdbfnum].pdbResNo[r];
-            if (lpos != pdbResNum - 1)
+            /*
+             * discontiguous - append last residue now
+             */
+            if (lpos != -1)
             {
-              /*
-               * discontiguous - append last residue now
-               */
-              if (lpos != -1)
-              {
-                molsel.append(String.valueOf(lpos));
-                molsel.append(chainCd);
-                molsel.append(",");
-              }
-              run = false;
+              molsel.append(String.valueOf(lpos));
+              molsel.append(chainCd);
+              molsel.append(",");
             }
-            else
+            run = false;
+          }
+          else
+          {
+            /*
+             * extending a contiguous run
+             */
+            if (!run)
             {
               /*
-               * extending a contiguous run
+               * start the range selection
                */
-              if (!run)
-              {
-                /*
-                 * start the range selection
-                 */
-                molsel.append(String.valueOf(lpos));
-                molsel.append("-");
-              }
-              run = true;
+              molsel.append(String.valueOf(lpos));
+              molsel.append("-");
             }
-            lpos = pdbResNum;
+            run = true;
           }
+          lpos = pdbResNum;
+          nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
         }
 
         /*
@@ -555,6 +523,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
                 .append(";" + command.toString());
       }
     }
+
+    String error = null;
     if (selectioncom.length() > 0)
     {
       // TODO: visually distinguish regions that were superposed
@@ -568,9 +538,17 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       }
       allComs.append("; ~display all; chain @CA|P; ribbon ")
               .append(selectioncom.toString()).append("; focus");
-      sendChimeraCommand(allComs.toString(), false);
+      List<String> chimeraReplies = sendChimeraCommand(allComs.toString(),
+              true);
+      for (String reply : chimeraReplies)
+      {
+        if (reply.toLowerCase().contains("unequal numbers of atoms"))
+        {
+          error = reply;
+        }
+      }
     }
-
+    return error;
   }
 
   /**
@@ -599,7 +577,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
      * to the Chimera command 'list models type molecule', see
      * ChimeraManager.getModelList().
      */
-    List<ChimeraModel> maps = chimeraMaps.get(getPdbFile()[pdbfnum]);
+    List<ChimeraModel> maps = chimeraMaps.get(getStructureFiles()[pdbfnum]);
     boolean hasSubModels = maps != null && maps.size() > 1;
     return "#" + String.valueOf(pdbfnum) + (hasSubModels ? ".1" : "");
   }
@@ -692,39 +670,35 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
           String progressMsg);
 
   /**
-   * colour any structures associated with sequences in the given alignment
-   * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
-   * if colourBySequence is enabled.
+   * Sends a set of colour commands to the structure viewer
+   * 
+   * @param colourBySequenceCommands
    */
-  public void colourBySequence(boolean showFeatures,
-          jalview.api.AlignmentViewPanel alignmentv)
+  @Override
+  protected void colourBySequence(
+          StructureMappingcommandSet[] colourBySequenceCommands)
   {
-    if (!colourBySequence || !loadingFinished)
-    {
-      return;
-    }
-    if (getSsm() == null)
-    {
-      return;
-    }
-    String[] files = getPdbFile();
-
-    SequenceRenderer sr = getSequenceRenderer(alignmentv);
-
-    FeatureRenderer fr = null;
-    if (showFeatures)
+    for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
     {
-      fr = getFeatureRenderer(alignmentv);
+      for (String command : cpdbbyseq.commands)
+      {
+        sendAsynchronousCommand(command, COLOURING_CHIMERA);
+      }
     }
-    AlignmentI alignment = alignmentv.getAlignment();
+  }
 
-    StructureMappingcommandSet colourBySequenceCommands = ChimeraCommands
-            .getColourBySequenceCommand(getSsm(), files, getSequence(), sr,
-                    fr, alignment);
-    for (String command : colourBySequenceCommands.commands)
-    {
-      sendAsynchronousCommand(command, COLOURING_CHIMERA);
-    }
+  /**
+   * @param files
+   * @param sr
+   * @param viewPanel
+   * @return
+   */
+  @Override
+  protected StructureMappingcommandSet[] getColourBySequenceCommands(
+          String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
+  {
+    return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
+            getSequence(), sr, viewPanel);
   }
 
   /**
@@ -754,27 +728,24 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   // //////////////////////////
 
   /**
-   * returns the current featureRenderer that should be used to colour the
-   * structures
-   * 
-   * @param alignment
-   * 
-   * @return
-   */
-  public abstract FeatureRenderer getFeatureRenderer(
-          AlignmentViewPanel alignment);
-
-  /**
    * instruct the Jalview binding to update the pdbentries vector if necessary
    * prior to matching the viewer's contents to the list of structure files
    * Jalview knows about.
    */
   public abstract void refreshPdbEntries();
 
+  /**
+   * map between index of model filename returned from getPdbFile and the first
+   * index of models from this file in the viewer. Note - this is not trimmed -
+   * use getPdbFile to get number of unique models.
+   */
+  private int _modelFileNameMap[];
+
+
   // ////////////////////////////////
   // /StructureListener
   @Override
-  public synchronized String[] getPdbFile()
+  public synchronized String[] getStructureFiles()
   {
     if (viewer == null)
     {
@@ -786,17 +757,6 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * returns the current sequenceRenderer that should be used to colour the
-   * structures
-   * 
-   * @param alignment
-   * 
-   * @return
-   */
-  public abstract SequenceRenderer getSequenceRenderer(
-          AlignmentViewPanel alignment);
-
-  /**
    * Construct and send a command to highlight zero, one or more atoms. We do
    * this by sending an "rlabel" command to show the residue label at that
    * position.
@@ -953,6 +913,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     return loadNotifiesHandled;
   }
 
+  @Override
   public void setJalviewColourScheme(ColourSchemeI cs)
   {
     colourBySequence = false;
@@ -969,12 +930,14 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
 
     List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
             false);
-    for (String res : residueSet)
+    for (String resName : residueSet)
     {
-      Color col = cs.findColour(res.charAt(0));
+      char res = resName.length() == 3 ? ResidueProperties
+              .getSingleCharacterCode(resName) : resName.charAt(0);
+      Color col = cs.findColour(res, 0, null, null, 0f);
       command.append("color " + col.getRed() / normalise + ","
               + col.getGreen() / normalise + "," + col.getBlue()
-              / normalise + " ::" + res + ";");
+              / normalise + " ::" + resName + ";");
     }
 
     sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
@@ -1025,6 +988,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    *      .html
    * @param col
    */
+  @Override
   public void setBackgroundColour(Color col)
   {
     viewerCommandHistory(false);
@@ -1129,30 +1093,22 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * features visible in Jalview
    * 
    * @param avp
+   * @return
    */
-  public void sendFeaturesToViewer(AlignmentViewPanel avp)
+  public int sendFeaturesToViewer(AlignmentViewPanel avp)
   {
     // TODO refactor as required to pull up to an interface
     AlignmentI alignment = avp.getAlignment();
-    FeatureRenderer fr = getFeatureRenderer(avp);
-
-    /*
-     * fr is null if feature display is turned off
-     */
-    if (fr == null)
-    {
-      return;
-    }
 
-    String[] files = getPdbFile();
+    String[] files = getStructureFiles();
     if (files == null)
     {
-      return;
+      return 0;
     }
 
     StructureMappingcommandSet commandSet = ChimeraCommands
             .getSetAttributeCommandsForFeatures(getSsm(), files,
-                    getSequence(), fr, alignment);
+                    getSequence(), avp);
     String[] commands = commandSet.commands;
     if (commands.length > 10)
     {
@@ -1165,6 +1121,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
         sendAsynchronousCommand(command, null);
       }
     }
+    return commands.length;
   }
 
   /**
@@ -1297,7 +1254,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       /*
        * locate the mapped position in the alignment (if any)
        */
-      SearchResults sr = getSsm()
+      SearchResultsI sr = getSsm()
               .findAlignmentPositionsForStructurePositions(atoms);
 
       /*
index a41e10e..d65f1d5 100644 (file)
  */
 package jalview.ext.varna;
 
-import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 
@@ -47,7 +47,8 @@ public class VarnaCommands
    */
   public static String[] getColourBySequenceCommand(
           StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr, FeatureRenderer fr,
+          SequenceI[][] sequence, SequenceRenderer sr,
+          FeatureColourFinder finder,
           AlignmentI alignment)
   {
     ArrayList<String> str = new ArrayList<String>();
@@ -58,7 +59,9 @@ public class VarnaCommands
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
 
       if (mapping == null || mapping.length < 1)
+      {
         continue;
+      }
 
       int lastPos = -1;
       for (int s = 0; s < sequence[pdbfnum].length; s++)
@@ -79,14 +82,15 @@ public class VarnaCommands
               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
 
               if (pos < 1 || pos == lastPos)
+              {
                 continue;
+              }
 
               lastPos = pos;
 
-              Color col = sr.getResidueBoxColour(sequence[pdbfnum][s], r);
+              Color col = sr.getResidueColour(sequence[pdbfnum][s], r,
+                      finder);
 
-              if (fr != null)
-                col = fr.findFeatureColour(col, sequence[pdbfnum][s], r);
               String newSelcom = (mapping[m].getChain() != " " ? ":"
                       + mapping[m].getChain() : "")
                       + "/"
index f86c3bc..99c0c51 100644 (file)
@@ -137,4 +137,12 @@ public interface GFTSPanelI
    * @return
    */
   public Map<String, Integer> getTempUserPrefs();
+
+  /**
+   * Returns unique key used for storing an FTSs instance cache items in the
+   * cache data structure
+   * 
+   * @return
+   */
+  public String getCacheKey();
 }
index a69d9f8..f1db383 100644 (file)
@@ -28,6 +28,7 @@ import jalview.gui.Desktop;
 import jalview.gui.IProgressIndicator;
 import jalview.gui.JvSwingUtils;
 import jalview.gui.SequenceFetcher;
+import jalview.io.cache.JvCacheableInputBox;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -38,6 +39,8 @@ import java.awt.event.ActionListener;
 import java.awt.event.FocusAdapter;
 import java.awt.event.FocusEvent;
 import java.awt.event.FocusListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
 import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
@@ -61,7 +64,6 @@ import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
 import javax.swing.JTable;
-import javax.swing.JTextField;
 import javax.swing.Timer;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
@@ -70,6 +72,7 @@ import javax.swing.event.DocumentListener;
 import javax.swing.event.InternalFrameEvent;
 import javax.swing.table.DefaultTableModel;
 import javax.swing.table.TableColumn;
+import javax.swing.text.JTextComponent;
 
 /**
  * This class provides the swing GUI layout for FTS Panel and implements most of
@@ -95,7 +98,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   protected JButton btn_cancel = new JButton();
 
-  protected JTextField txt_search = new JTextField(30);
+  protected JvCacheableInputBox<String> txt_search;
 
   protected SequenceFetcher seqFetcher;
 
@@ -146,6 +149,10 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   protected HashSet<String> paginatorCart = new HashSet<String>();
 
+  private static final int MIN_WIDTH = 670;
+
+  private static final int MIN_HEIGHT = 300;
+
   protected static final DecimalFormat totalNumberformatter = new DecimalFormat(
           "###,###");
 
@@ -234,6 +241,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     try
     {
       jbInit();
+      mainFrame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
       mainFrame.addFocusListener(new FocusAdapter()
       {
         @Override
@@ -257,6 +265,9 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
    */
   private void jbInit() throws Exception
   {
+
+    txt_search = new JvCacheableInputBox<String>(getCacheKey());
+    populateCmbSearchTargetOptions();
     Integer width = getTempUserPrefs().get("FTSPanel.width") == null ? 800
             : getTempUserPrefs().get("FTSPanel.width");
     Integer height = getTempUserPrefs().get("FTSPanel.height") == null ? 400
@@ -448,49 +459,51 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     scrl_searchResult.setPreferredSize(new Dimension(width, height));
 
     cmb_searchTarget.setFont(new java.awt.Font("Verdana", 0, 12));
-    cmb_searchTarget.addActionListener(new ActionListener()
+    cmb_searchTarget.addItemListener(new ItemListener()
     {
       @Override
-      public void actionPerformed(ActionEvent e)
+      public void itemStateChanged(ItemEvent e)
       {
-        String tooltipText;
-        if ("all".equalsIgnoreCase(getCmbSearchTarget().getSelectedItem()
-                .toString()))
+        if (e.getStateChange() == ItemEvent.SELECTED)
         {
-          tooltipText = MessageManager.getString("label.search_all");
-        }
-        else if ("pdb id".equalsIgnoreCase(getCmbSearchTarget()
-                .getSelectedItem().toString()))
-        {
-          tooltipText = MessageManager
-                  .getString("label.separate_multiple_accession_ids");
-        }
-        else
-        {
-          tooltipText = MessageManager.formatMessage(
-                  "label.separate_multiple_query_values",
-                  new Object[] { getCmbSearchTarget().getSelectedItem()
-                          .toString() });
+          String tooltipText;
+          if ("all".equalsIgnoreCase(getCmbSearchTarget().getSelectedItem()
+                  .toString()))
+          {
+            tooltipText = MessageManager.getString("label.search_all");
+          }
+          else if ("pdb id".equalsIgnoreCase(getCmbSearchTarget()
+                  .getSelectedItem().toString()))
+          {
+            tooltipText = MessageManager
+                    .getString("label.separate_multiple_accession_ids");
+          }
+          else
+          {
+            tooltipText = MessageManager.formatMessage(
+                    "label.separate_multiple_query_values",
+                    new Object[] { getCmbSearchTarget().getSelectedItem()
+                            .toString() });
+          }
+          txt_search.setToolTipText(JvSwingUtils.wrapTooltip(true,
+                  tooltipText));
+          searchAction(true);
         }
-        txt_search.setToolTipText(JvSwingUtils.wrapTooltip(true,
-                tooltipText));
-        searchAction(true);
       }
     });
-
-    populateCmbSearchTargetOptions();
 
     txt_search.setFont(new java.awt.Font("Verdana", 0, 12));
 
-    txt_search.addKeyListener(new KeyAdapter()
+    txt_search.getEditor().getEditorComponent()
+            .addKeyListener(new KeyAdapter()
     {
       @Override
       public void keyPressed(KeyEvent e)
       {
         if (e.getKeyCode() == KeyEvent.VK_ENTER)
         {
-          if (txt_search.getText() == null
-                  || txt_search.getText().isEmpty())
+          if (getTypedText() == null || getTypedText().isEmpty())
           {
             return;
           }
@@ -499,27 +512,29 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
           if (primaryKeyName.equalsIgnoreCase(getCmbSearchTarget()
                   .getSelectedItem().toString()))
           {
-            transferToSequenceFetcher(txt_search.getText());
+            transferToSequenceFetcher(getTypedText());
           }
         }
       }
     });
-
     final DeferredTextInputListener listener = new DeferredTextInputListener(
             1500, new ActionListener()
             {
               @Override
               public void actionPerformed(ActionEvent e)
               {
-                if (!getTypedText().equalsIgnoreCase(lastSearchTerm))
+                String typed = getTypedText();
+                if (!typed.equalsIgnoreCase(lastSearchTerm))
                 {
                   searchAction(true);
                   paginatorCart.clear();
-                  lastSearchTerm = getTypedText();
+                  lastSearchTerm = typed;
                 }
               }
             }, false);
-    txt_search.getDocument().addDocumentListener(listener);
+    ((JTextComponent) txt_search.getEditor().getEditorComponent())
+            .getDocument().addDocumentListener(listener);
+
     txt_search.addFocusListener(new FocusListener()
     {
       @Override
@@ -635,16 +650,12 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   protected void closeAction()
   {
-    // System.out.println(">>>>>>>>>> closing internal frame!!!");
-    // System.out.println("width : " + this.getWidth());
-    // System.out.println("heigh : " + this.getHeight());
-    // System.out.println("x : " + mainFrame.getX());
-    // System.out.println("y : " + mainFrame.getY());
     getTempUserPrefs().put("FTSPanel.width", this.getWidth());
     getTempUserPrefs().put("FTSPanel.height", pnl_results.getHeight());
     getTempUserPrefs().put("FTSPanel.x", mainFrame.getX());
     getTempUserPrefs().put("FTSPanel.y", mainFrame.getY());
     mainFrame.dispose();
+    txt_search.persistCache();
   }
 
   public class DeferredTextInputListener implements DocumentListener
@@ -692,7 +703,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
   {
     if (previousWantedFields == null)
     {
-      return true;
+      return false;
     }
 
     return Arrays.equals(getFTSRestClient()
@@ -719,7 +730,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     return cmb_searchTarget;
   }
 
-  public JTextField getTxtSearch()
+  public JComboBox<String> getTxtSearch()
   {
     return txt_search;
   }
@@ -815,7 +826,6 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   public void transferToSequenceFetcher(String ids)
   {
-    // mainFrame.dispose();
     seqFetcher.getTextArea().setText(ids);
     Thread worker = new Thread(seqFetcher);
     worker.start();
@@ -824,7 +834,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
   @Override
   public String getTypedText()
   {
-    return txt_search.getText().trim();
+    return txt_search.getUserInput();
   }
 
   @Override
@@ -875,6 +885,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
   {
     lbl_blank.setVisible(!isSearchInProgress);
     lbl_loading.setVisible(isSearchInProgress);
+    txt_search.setEditable(!isSearchInProgress);
   }
 
   @Override
@@ -929,8 +940,6 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
               .toString();
       paginatorCart.add(idStr);
     }
-    // System.out.println("Paginator shopping cart size : "
-    // + paginatorCart.size());
   }
 
   public void updateSummaryTableSelections()
@@ -949,9 +958,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     {
       e.printStackTrace();
     }
-    // System.out.println(">>>>>> got here : 1");
     int totalRows = resultTable.getRowCount();
-    // resultTable.clearSelection();
     for (int row = 0; row < totalRows; row++)
     {
       String id = (String) resultTable.getValueAt(row, primaryKeyColIndex);
@@ -965,9 +972,6 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   public void refreshPaginatorState()
   {
-    // System.out.println("resultSet count : " + resultSetCount);
-    // System.out.println("offSet : " + offSet);
-    // System.out.println("page limit : " + pageLimit);
     setPrevPageButtonEnabled(false);
     setNextPageButtonEnabled(false);
     if (resultSetCount == 0 && pageLimit == 0)
@@ -989,4 +993,5 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     mainFrame.setTitle(getFTSFrameTitle());
   }
 
+
 }
index 1dfabce..b944b9b 100644 (file)
@@ -39,10 +39,11 @@ public class PDBFTSPanel extends GFTSPanel
   private static String defaultFTSFrameTitle = MessageManager
           .getString("label.pdb_sequence_fetcher");
 
-  private String ftsFrameTitle = defaultFTSFrameTitle;
 
   private static Map<String, Integer> tempUserPrefs = new HashMap<String, Integer>();
 
+  private static final String PDB_FTS_CACHE_KEY = "CACHE.PDB_FTS";
+
   public PDBFTSPanel(SequenceFetcher seqFetcher)
   {
     super();
@@ -55,6 +56,7 @@ public class PDBFTSPanel extends GFTSPanel
   @Override
   public void searchAction(boolean isFreshSearch)
   {
+    mainFrame.requestFocusInWindow();
     if (isFreshSearch)
     {
       offSet = 0;
@@ -64,7 +66,6 @@ public class PDBFTSPanel extends GFTSPanel
       @Override
       public void run()
       {
-        ftsFrameTitle = defaultFTSFrameTitle;
         reset();
         boolean allowEmptySequence = false;
         if (getTypedText().length() > 0)
@@ -76,7 +77,7 @@ public class PDBFTSPanel extends GFTSPanel
                   .getSelectedItem()).getCode();
           wantedFields = PDBFTSRestClient.getInstance()
                   .getAllDefaultDisplayedFTSDataColumns();
-          String searchTerm = decodeSearchTerm(txt_search.getText(),
+          String searchTerm = decodeSearchTerm(getTypedText(),
                   searchTarget);
 
           FTSRestRequest request = new FTSRestRequest();
@@ -143,6 +144,7 @@ public class PDBFTSPanel extends GFTSPanel
           refreshPaginatorState();
           updateSummaryTableSelections();
         }
+        txt_search.updateCache();
       }
     }.start();
   }
@@ -200,7 +202,7 @@ public class PDBFTSPanel extends GFTSPanel
       e.printStackTrace();
     }
     int[] selectedRows = getResultTable().getSelectedRows();
-    String searchTerm = txt_search.getText();
+    String searchTerm = getTypedText();
     for (int summaryRow : selectedRows)
     {
       String idStr = getResultTable().getValueAt(summaryRow,
@@ -261,7 +263,7 @@ public class PDBFTSPanel extends GFTSPanel
   @Override
   public String getFTSFrameTitle()
   {
-    return ftsFrameTitle;
+    return defaultFTSFrameTitle;
   }
 
   @Override
@@ -276,4 +278,12 @@ public class PDBFTSPanel extends GFTSPanel
     return tempUserPrefs;
   }
 
+
+  @Override
+  public String getCacheKey()
+  {
+    return PDB_FTS_CACHE_KEY;
+  }
+
+
 }
index 27bfca8..a23df4c 100644 (file)
@@ -157,14 +157,14 @@ public class UniProtFTSRestClient extends FTSRestClient
     if (foundDataRow != null && foundDataRow.length > 0)
     {
       result = new ArrayList<FTSData>();
-      String titleRow = getDataColumnsFieldsAsTabDelimitedString(uniprotRestRequest
-              .getWantedFields());
-      // System.out.println(">>>>Title row : " + titleRow);
+      boolean firstRow = true;
       for (String dataRow : foundDataRow)
       {
-        if (dataRow.equalsIgnoreCase(titleRow))
+        // The first data row is usually the header data. This should be
+        // filtered out from the rest of the data See: JAL-2485
+        if (firstRow)
         {
-          // System.out.println(">>>>>>>>>> matched!!!");
+          firstRow = false;
           continue;
         }
         // System.out.println(dataRow);
index f04e4fa..ace3600 100644 (file)
@@ -40,10 +40,11 @@ public class UniprotFTSPanel extends GFTSPanel
   private static String defaultFTSFrameTitle = MessageManager
           .getString("label.uniprot_sequence_fetcher");
 
-  private String ftsFrameTitle = defaultFTSFrameTitle;
 
   private static Map<String, Integer> tempUserPrefs = new HashMap<String, Integer>();
 
+  private static final String UNIPROT_FTS_CACHE_KEY = "CACHE.UNIPROT_FTS";
+
   public UniprotFTSPanel(SequenceFetcher seqFetcher)
   {
     super();
@@ -57,6 +58,7 @@ public class UniprotFTSPanel extends GFTSPanel
   @Override
   public void searchAction(boolean isFreshSearch)
   {
+    mainFrame.requestFocusInWindow();
     if (isFreshSearch)
     {
       offSet = 0;
@@ -66,20 +68,18 @@ public class UniprotFTSPanel extends GFTSPanel
       @Override
       public void run()
       {
-        ftsFrameTitle = defaultFTSFrameTitle;
         reset();
-        if (getTypedText().length() > 0)
+        String searchInput = getTypedText();
+        if (searchInput.length() > 0)
         {
           setSearchInProgress(true);
           long startTime = System.currentTimeMillis();
-
+          searchInput = getTypedText();
           String searchTarget = ((FTSDataColumnI) cmb_searchTarget
                   .getSelectedItem()).getAltCode();
-
           wantedFields = UniProtFTSRestClient.getInstance()
                   .getAllDefaultDisplayedFTSDataColumns();
-          String searchTerm = decodeSearchTerm(txt_search.getText(),
-                  searchTarget);
+          String searchTerm = decodeSearchTerm(searchInput, searchTarget);
 
           FTSRestRequest request = new FTSRestRequest();
           request.setFieldToSearchBy(searchTarget);
@@ -143,6 +143,7 @@ public class UniprotFTSPanel extends GFTSPanel
           refreshPaginatorState();
           updateSummaryTableSelections();
         }
+        txt_search.updateCache();
       }
     }.start();
 
@@ -226,7 +227,7 @@ public class UniprotFTSPanel extends GFTSPanel
   @Override
   public String getFTSFrameTitle()
   {
-    return ftsFrameTitle;
+    return defaultFTSFrameTitle;
   }
 
   @Override
@@ -235,4 +236,9 @@ public class UniprotFTSPanel extends GFTSPanel
     return tempUserPrefs;
   }
 
+  @Override
+  public String getCacheKey()
+  {
+    return UNIPROT_FTS_CACHE_KEY;
+  }
 }
index 370e649..a9a970f 100644 (file)
@@ -34,7 +34,7 @@ import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureSettingsControllerI;
 import jalview.api.SplitContainerI;
 import jalview.api.ViewStyleI;
-import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.commands.CommandI;
@@ -53,12 +53,14 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
 import jalview.datamodel.AlignmentView;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SeqCigar;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.gui.ColourMenuHelper.ColourChangeListener;
 import jalview.gui.ViewSelectionMenu.ViewSetProvider;
 import jalview.io.AlignmentProperties;
 import jalview.io.AnnotationFile;
@@ -68,6 +70,7 @@ import jalview.io.FileFormat;
 import jalview.io.FileFormatI;
 import jalview.io.FileFormats;
 import jalview.io.FileLoader;
+import jalview.io.FileParse;
 import jalview.io.FormatAdapter;
 import jalview.io.HtmlSvgOutput;
 import jalview.io.IdentifyFile;
@@ -76,28 +79,16 @@ import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.io.JnetAnnotationMaker;
 import jalview.io.NewickFile;
+import jalview.io.ScoreMatrixFile;
 import jalview.io.TCoffeeScoreFile;
 import jalview.jbgui.GAlignFrame;
-import jalview.schemes.Blosum62ColourScheme;
-import jalview.schemes.BuriedColourScheme;
-import jalview.schemes.ClustalxColourScheme;
 import jalview.schemes.ColourSchemeI;
-import jalview.schemes.ColourSchemeProperty;
-import jalview.schemes.HelixColourScheme;
-import jalview.schemes.HydrophobicColourScheme;
-import jalview.schemes.NucleotideColourScheme;
-import jalview.schemes.PIDColourScheme;
-import jalview.schemes.PurinePyrimidineColourScheme;
-import jalview.schemes.RNAHelicesColourChooser;
-import jalview.schemes.ResidueProperties;
-import jalview.schemes.StrandColourScheme;
+import jalview.schemes.ColourSchemes;
+import jalview.schemes.ResidueColourScheme;
 import jalview.schemes.TCoffeeColourScheme;
-import jalview.schemes.TaylorColourScheme;
-import jalview.schemes.TurnColourScheme;
-import jalview.schemes.UserColourScheme;
-import jalview.schemes.ZappoColourScheme;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.ViewportRanges;
 import jalview.ws.DBRefFetcher;
 import jalview.ws.DBRefFetcher.FetchFinishedListenerI;
 import jalview.ws.jws1.Discoverer;
@@ -126,7 +117,6 @@ import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
-import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.awt.print.PageFormat;
 import java.awt.print.PrinterJob;
@@ -149,7 +139,6 @@ import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
-import javax.swing.JRadioButtonMenuItem;
 import javax.swing.JScrollPane;
 import javax.swing.SwingUtilities;
 
@@ -160,7 +149,7 @@ import javax.swing.SwingUtilities;
  * @version $Revision$
  */
 public class AlignFrame extends GAlignFrame implements DropTargetListener,
-        IProgressIndicator, AlignViewControllerGuiI
+        IProgressIndicator, AlignViewControllerGuiI, ColourChangeListener
 {
 
   public static final int DEFAULT_WIDTH = 700;
@@ -174,6 +163,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
   AlignViewport viewport;
 
+  ViewportRanges vpRanges;
+
   public AlignViewControllerI avc;
 
   List<AlignmentPanel> alignPanels = new ArrayList<AlignmentPanel>();
@@ -243,7 +234,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * @param height
    *          height of frame.
    */
-  public AlignFrame(AlignmentI al, ColumnSelection hiddenColumns,
+  public AlignFrame(AlignmentI al, HiddenColumns hiddenColumns,
           int width, int height)
   {
     this(al, hiddenColumns, width, height, null);
@@ -260,7 +251,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * @param sequenceSetId
    *          (may be null)
    */
-  public AlignFrame(AlignmentI al, ColumnSelection hiddenColumns,
+  public AlignFrame(AlignmentI al, HiddenColumns hiddenColumns,
           int width, int height, String sequenceSetId)
   {
     this(al, hiddenColumns, width, height, sequenceSetId, null);
@@ -279,7 +270,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * @param viewId
    *          (may be null)
    */
-  public AlignFrame(AlignmentI al, ColumnSelection hiddenColumns,
+  public AlignFrame(AlignmentI al, HiddenColumns hiddenColumns,
           int width, int height, String sequenceSetId, String viewId)
   {
     setSize(width, height);
@@ -298,7 +289,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   public AlignFrame(AlignmentI al, SequenceI[] hiddenSeqs,
-          ColumnSelection hiddenColumns, int width, int height)
+          HiddenColumns hiddenColumns, int width, int height)
   {
     setSize(width, height);
 
@@ -345,11 +336,12 @@ 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)
     {
-      BLOSUM62Colour.setEnabled(false);
+      // BLOSUM62Colour.setEnabled(false);
       conservationMenuItem.setEnabled(false);
       modifyConservation.setEnabled(false);
       // PIDColour.setEnabled(false);
@@ -369,19 +361,28 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       sortPairwiseMenuItem_actionPerformed(null);
     }
 
-    if (Desktop.desktop != null)
-    {
-      this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
-      addServiceListeners();
-      setGUINucleotide(viewport.getAlignment().isNucleotide());
-    }
-
     this.alignPanel.av
             .setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
 
     setMenusFromViewport(viewport);
     buildSortByAnnotationScoresMenu();
-    buildTreeMenu();
+    calculateTree.addActionListener(new ActionListener()
+    {
+
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        openTreePcaDialog();
+      }
+    });
+    buildColourMenu();
+
+    if (Desktop.desktop != null)
+    {
+      this.setDropTarget(new java.awt.dnd.DropTarget(this, this));
+      addServiceListeners();
+      setGUINucleotide();
+    }
 
     if (viewport.getWrapAlignment())
     {
@@ -653,8 +654,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                   new String[] { (viewport.cursorMode ? "on" : "off") }));
           if (viewport.cursorMode)
           {
-            alignPanel.getSeqPanel().seqCanvas.cursorX = viewport.startRes;
-            alignPanel.getSeqPanel().seqCanvas.cursorY = viewport.startSeq;
+            alignPanel.getSeqPanel().seqCanvas.cursorX = vpRanges
+                    .getStartRes();
+            alignPanel.getSeqPanel().seqCanvas.cursorY = vpRanges
+                    .getStartSeq();
           }
           alignPanel.getSeqPanel().seqCanvas.repaint();
           break;
@@ -688,23 +691,21 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
         case KeyEvent.VK_PAGE_UP:
           if (viewport.getWrapAlignment())
           {
-            alignPanel.scrollUp(true);
+            vpRanges.scrollUp(true);
           }
           else
           {
-            alignPanel.setScrollValues(viewport.startRes, viewport.startSeq
-                    - viewport.endSeq + viewport.startSeq);
+            vpRanges.pageUp();
           }
           break;
         case KeyEvent.VK_PAGE_DOWN:
           if (viewport.getWrapAlignment())
           {
-            alignPanel.scrollUp(false);
+            vpRanges.scrollUp(false);
           }
           else
           {
-            alignPanel.setScrollValues(viewport.startRes, viewport.startSeq
-                    + viewport.endSeq - viewport.startSeq);
+            vpRanges.pageDown();
           }
           break;
         }
@@ -848,27 +849,23 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   /**
    * Configure menu items that vary according to whether the alignment is
    * nucleotide or protein
-   * 
-   * @param nucleotide
    */
-  public void setGUINucleotide(boolean nucleotide)
+  public void setGUINucleotide()
   {
+    AlignmentI al = getViewport().getAlignment();
+    boolean nucleotide = al.isNucleotide();
+
     showTranslation.setVisible(nucleotide);
     showReverse.setVisible(nucleotide);
     showReverseComplement.setVisible(nucleotide);
     conservationMenuItem.setEnabled(!nucleotide);
-    modifyConservation.setEnabled(!nucleotide);
+    modifyConservation.setEnabled(!nucleotide
+            && conservationMenuItem.isSelected());
     showGroupConservation.setEnabled(!nucleotide);
-    rnahelicesColour.setEnabled(nucleotide);
-    nucleotideColour.setEnabled(nucleotide);
-    purinePyrimidineColour.setEnabled(nucleotide);
-    RNAInteractionColour.setEnabled(nucleotide);
+
     showComplementMenuItem.setText(nucleotide ? MessageManager
             .getString("label.protein") : MessageManager
             .getString("label.nucleotide"));
-    setColourSelected(jalview.bin.Cache.getDefault(
-            nucleotide ? Preferences.DEFAULT_COLOUR_NUC
-                    : Preferences.DEFAULT_COLOUR_PROT, "None"));
   }
 
   /**
@@ -894,7 +891,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     padGapsMenuitem.setSelected(av.isPadGaps());
     colourTextMenuItem.setSelected(av.isShowColourText());
     abovePIDThreshold.setSelected(av.getAbovePIDThreshold());
+    modifyPID.setEnabled(abovePIDThreshold.isSelected());
     conservationMenuItem.setSelected(av.getConservationSelected());
+    modifyConservation.setEnabled(conservationMenuItem.isSelected());
     seqLimits.setSelected(av.getShowJVSuffix());
     idRightAlign.setSelected(av.isRightAlignIds());
     centreColumnLabelsMenuItem.setState(av.isCentreColumnLabels());
@@ -920,8 +919,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     showSequenceLogo.setSelected(av.isShowSequenceLogo());
     normaliseSequenceLogo.setSelected(av.isNormaliseSequenceLogo());
 
-    setColourSelected(ColourSchemeProperty.getColourName(av
-            .getGlobalColourScheme()));
+    ColourMenuHelper.setColourSelected(colourMenu,
+            av.getGlobalColourScheme());
 
     showSeqFeatures.setSelected(av.isShowSequenceFeatures());
     hiddenMarkers.setState(av.getShowHiddenMarkers());
@@ -931,9 +930,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     autoCalculate.setSelected(av.autoCalculateConsensus);
     sortByTree.setSelected(av.sortByTree);
     listenToViewSelections.setSelected(av.followSelection);
-    rnahelicesColour.setEnabled(av.getAlignment().hasRNAStructure());
-    rnahelicesColour
-            .setSelected(av.getGlobalColourScheme() instanceof jalview.schemes.RNAHelicesColour);
 
     showProducts.setEnabled(canShowProducts());
     setGroovyEnabled(Desktop.getGroovyConsole() != null);
@@ -1195,8 +1191,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
               exportData.getAlignment(), // class cast exceptions will
               // occur in the distant future
               exportData.getOmitHidden(), exportData.getStartEndPostions(),
-              f.getCacheSuffixDefault(format),
-              viewport.getColumnSelection());
+              f.getCacheSuffixDefault(format), viewport.getAlignment()
+                      .getHiddenColumns());
 
       if (output == null)
       {
@@ -1272,11 +1268,11 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     {
       FileFormatI format = fileFormat;
       cap.setText(new FormatAdapter(alignPanel, exportData.getSettings())
-              .formatSequences(format,
-                      exportData.getAlignment(),
+              .formatSequences(format, exportData.getAlignment(),
                       exportData.getOmitHidden(),
-                      exportData.getStartEndPostions(),
-                      viewport.getColumnSelection()));
+ exportData
+                              .getStartEndPostions(), viewport
+                              .getAlignment().getHiddenColumns()));
       Desktop.addInternalFrame(cap, MessageManager.formatMessage(
               "label.alignment_output_command",
               new Object[] { e.getActionCommand() }), 600, 500);
@@ -1325,8 +1321,9 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       alignmentToExport = viewport.getAlignment();
     }
     alignmentStartEnd = alignmentToExport
-            .getVisibleStartAndEndIndex(viewport.getColumnSelection()
-                    .getHiddenColumns());
+            .getVisibleStartAndEndIndex(viewport.getAlignment()
+                    .getHiddenColumns()
+                    .getHiddenRegions());
     AlignmentExportData ed = new AlignmentExportData(alignmentToExport,
             omitHidden, alignmentStartEnd, settings);
     return ed;
@@ -1864,8 +1861,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
 
     String output = new FormatAdapter().formatSequences(FileFormat.Fasta,
-            seqs,
-            omitHidden, null);
+            seqs, omitHidden, null);
 
     StringSelection ss = new StringSelection(output);
 
@@ -1891,7 +1887,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       hiddenColumns = new ArrayList<int[]>();
       int hiddenOffset = viewport.getSelectionGroup().getStartRes(), hiddenCutoff = viewport
               .getSelectionGroup().getEndRes();
-      for (int[] region : viewport.getColumnSelection().getHiddenColumns())
+      for (int[] region : viewport.getAlignment().getHiddenColumns()
+              .getHiddenRegions())
       {
         if (region[0] >= hiddenOffset && region[1] <= hiddenCutoff)
         {
@@ -2161,7 +2158,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       {
 
         // propagate alignment changed.
-        viewport.setEndSeq(alignment.getHeight());
+        vpRanges.setEndSeq(alignment.getHeight());
         if (annotationAdded)
         {
           // Duplicate sequence annotation in all views.
@@ -2565,7 +2562,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       {
         trimRegion = new TrimRegionCommand("Remove Left", true, seqs,
                 column, viewport.getAlignment());
-        viewport.setStartRes(0);
+        vpRanges.setStartRes(0);
       }
       else
       {
@@ -2632,13 +2629,13 @@ 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(viewport.startRes);
+    int startRes = seq.findPosition(vpRanges.getStartRes());
     // ShiftList shifts;
     // viewport.getAlignment().removeGaps(shifts=new ShiftList());
     // edit.alColumnChanges=shifts.getInverse();
     // if (viewport.hasHiddenColumns)
     // viewport.getColumnSelection().compensateForEdits(shifts);
-    viewport.setStartRes(seq.findIndex(startRes) - 1);
+    vpRanges.setStartRes(seq.findIndex(startRes) - 1);
     viewport.firePropertyChange("alignment", null, viewport.getAlignment()
             .getSequences());
 
@@ -2671,12 +2668,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(viewport.startRes);
+    int startRes = seq.findPosition(vpRanges.getStartRes());
 
     addHistoryItem(new RemoveGapsCommand("Remove Gaps", seqs, start, end,
             viewport.getAlignment()));
 
-    viewport.setStartRes(seq.findIndex(startRes) - 1);
+    vpRanges.setStartRes(seq.findIndex(startRes) - 1);
 
     viewport.firePropertyChange("alignment", null, viewport.getAlignment()
             .getSequences());
@@ -2764,6 +2761,14 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
      */
     newap.av.replaceMappings(viewport.getAlignment());
 
+    /*
+     * start up cDNA consensus (if applicable) now mappings are in place
+     */
+    if (newap.av.initComplementConsensus())
+    {
+      newap.refresh(true); // adjust layout of annotations
+    }
+
     newap.av.viewName = getNewViewName(viewTitle);
 
     addAlignmentPanel(newap, true);
@@ -3273,7 +3278,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     frame.setContentPane(overview);
     Desktop.addInternalFrame(frame, MessageManager.formatMessage(
             "label.overview_params", new Object[] { this.getTitle() }),
-            frame.getWidth(), frame.getHeight());
+            true, frame.getWidth(), frame.getHeight(), true, true);
     frame.pack();
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
     frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
@@ -3290,146 +3295,19 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   @Override
-  public void textColour_actionPerformed(ActionEvent e)
+  public void textColour_actionPerformed()
   {
     new TextColourChooser().chooseColour(alignPanel, null);
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  protected void noColourmenuItem_actionPerformed(ActionEvent e)
-  {
-    changeColour(null);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void clustalColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new ClustalxColourScheme(viewport.getAlignment(),
-            viewport.getHiddenRepSequences()));
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void zappoColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new ZappoColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void taylorColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new TaylorColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void hydrophobicityColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new HydrophobicColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void helixColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new HelixColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void strandColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new StrandColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void turnColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new TurnColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void buriedColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new BuriedColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void nucleotideColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new NucleotideColourScheme());
-  }
-
-  @Override
-  public void purinePyrimidineColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new PurinePyrimidineColourScheme());
-  }
-
   /*
-   * public void covariationColour_actionPerformed(ActionEvent e) {
+   * public void covariationColour_actionPerformed() {
    * changeColour(new
    * CovariationColourScheme(viewport.getAlignment().getAlignmentAnnotation
    * ()[0])); }
    */
   @Override
-  public void annotationColour_actionPerformed(ActionEvent e)
+  public void annotationColour_actionPerformed()
   {
     new AnnotationColourChooser(viewport, alignPanel);
   }
@@ -3440,56 +3318,56 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     new AnnotationColumnChooser(viewport, alignPanel);
   }
 
+  /**
+   * Action on the user checking or unchecking the option to apply the selected
+   * colour scheme to all groups. If unchecked, groups may have their own
+   * independent colour schemes.
+   * 
+   * @param selected
+   */
   @Override
-  public void rnahelicesColour_actionPerformed(ActionEvent e)
+  public void applyToAllGroups_actionPerformed(boolean selected)
   {
-    new RNAHelicesColourChooser(viewport, alignPanel);
+    viewport.setColourAppliesToAllGroups(selected);
   }
 
   /**
-   * DOCUMENT ME!
+   * Action on user selecting a colour from the colour menu
    * 
-   * @param e
-   *          DOCUMENT ME!
+   * @param name
+   *          the name (not the menu item label!) of the colour scheme
    */
   @Override
-  protected void applyToAllGroups_actionPerformed(ActionEvent e)
+  public void changeColour_actionPerformed(String name)
   {
-    viewport.setColourAppliesToAllGroups(applyToAllGroups.isSelected());
+    /*
+     * 'User Defined' opens a panel to configure or load a
+     * user-defined colour scheme
+     */
+    if (ResidueColourScheme.USER_DEFINED.equals(name))
+    {
+      new UserDefinedColours(alignPanel);
+      return;
+    }
+
+    /*
+     * otherwise set the chosen colour scheme (or null for 'None')
+     */
+    ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(name,
+            viewport.getAlignment(), viewport.getHiddenRepSequences());
+    changeColour(cs);
   }
 
   /**
-   * DOCUMENT ME!
+   * Actions on setting or changing the alignment colour scheme
    * 
    * @param cs
-   *          DOCUMENT ME!
    */
   @Override
   public void changeColour(ColourSchemeI cs)
   {
     // TODO: pull up to controller method
-
-    if (cs != null)
-    {
-      // Make sure viewport is up to date w.r.t. any sliders
-      if (viewport.getAbovePIDThreshold())
-      {
-        int threshold = SliderPanel.setPIDSliderSource(alignPanel, cs,
-                "Background");
-        viewport.setThreshold(threshold);
-      }
-
-      if (viewport.getConservationSelected())
-      {
-        cs.setConservationInc(SliderPanel.setConservationSlider(alignPanel,
-                cs, "Background"));
-      }
-      if (cs instanceof TCoffeeColourScheme)
-      {
-        tcoffeeColour.setEnabled(true);
-        tcoffeeColour.setSelected(true);
-      }
-    }
+    ColourMenuHelper.setColourSelected(colourMenu, cs);
 
     viewport.setGlobalColourScheme(cs);
 
@@ -3497,190 +3375,70 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Show the PID threshold slider panel
    */
   @Override
-  protected void modifyPID_actionPerformed(ActionEvent e)
+  protected void modifyPID_actionPerformed()
   {
-    if (viewport.getAbovePIDThreshold()
-            && viewport.getGlobalColourScheme() != null)
-    {
-      SliderPanel.setPIDSliderSource(alignPanel,
-              viewport.getGlobalColourScheme(), "Background");
-      SliderPanel.showPIDSlider();
-    }
+    SliderPanel.setPIDSliderSource(alignPanel,
+            viewport.getResidueShading(), alignPanel.getViewName());
+    SliderPanel.showPIDSlider();
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Show the Conservation slider panel
    */
   @Override
-  protected void modifyConservation_actionPerformed(ActionEvent e)
+  protected void modifyConservation_actionPerformed()
   {
-    if (viewport.getConservationSelected()
-            && viewport.getGlobalColourScheme() != null)
-    {
-      SliderPanel.setConservationSlider(alignPanel,
-              viewport.getGlobalColourScheme(), "Background");
-      SliderPanel.showConservationSlider();
-    }
+    SliderPanel.setConservationSlider(alignPanel,
+            viewport.getResidueShading(), alignPanel.getViewName());
+    SliderPanel.showConservationSlider();
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Action on selecting or deselecting (Colour) By Conservation
    */
   @Override
-  protected void conservationMenuItem_actionPerformed(ActionEvent e)
+  public void conservationMenuItem_actionPerformed(boolean selected)
   {
-    viewport.setConservationSelected(conservationMenuItem.isSelected());
-
-    viewport.setAbovePIDThreshold(false);
-    abovePIDThreshold.setSelected(false);
+    modifyConservation.setEnabled(selected);
+    viewport.setConservationSelected(selected);
+    viewport.getResidueShading().setConservationApplied(selected);
 
     changeColour(viewport.getGlobalColourScheme());
-
-    modifyConservation_actionPerformed(null);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void abovePIDThreshold_actionPerformed(ActionEvent e)
-  {
-    viewport.setAbovePIDThreshold(abovePIDThreshold.isSelected());
-
-    conservationMenuItem.setSelected(false);
-    viewport.setConservationSelected(false);
-
-    changeColour(viewport.getGlobalColourScheme());
-
-    modifyPID_actionPerformed(null);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void userDefinedColour_actionPerformed(ActionEvent e)
-  {
-    if (e.getActionCommand().equals(
-            MessageManager.getString("action.user_defined")))
+    if (selected)
     {
-      new UserDefinedColours(alignPanel, null);
+      modifyConservation_actionPerformed();
     }
     else
     {
-      UserColourScheme udc = (UserColourScheme) UserDefinedColours
-              .getUserColourSchemes().get(e.getActionCommand());
-
-      changeColour(udc);
+      SliderPanel.hideConservationSlider();
     }
   }
 
-  public void updateUserColourMenu()
+  /**
+   * Action on selecting or deselecting (Colour) Above PID Threshold
+   */
+  @Override
+  public void abovePIDThreshold_actionPerformed(boolean selected)
   {
+    modifyPID.setEnabled(selected);
+    viewport.setAbovePIDThreshold(selected);
+    if (!selected)
+    {
+      viewport.getResidueShading().setThreshold(0,
+              viewport.isIgnoreGapsConsensus());
+    }
 
-    Component[] menuItems = colourMenu.getMenuComponents();
-    int iSize = menuItems.length;
-    for (int i = 0; i < iSize; i++)
+    changeColour(viewport.getGlobalColourScheme());
+    if (selected)
     {
-      if (menuItems[i].getName() != null
-              && menuItems[i].getName().equals("USER_DEFINED"))
-      {
-        colourMenu.remove(menuItems[i]);
-        iSize--;
-      }
+      modifyPID_actionPerformed();
     }
-    if (jalview.gui.UserDefinedColours.getUserColourSchemes() != null)
+    else
     {
-      java.util.Enumeration userColours = jalview.gui.UserDefinedColours
-              .getUserColourSchemes().keys();
-
-      while (userColours.hasMoreElements())
-      {
-        final JRadioButtonMenuItem radioItem = new JRadioButtonMenuItem(
-                userColours.nextElement().toString());
-        radioItem.setName("USER_DEFINED");
-        radioItem.addMouseListener(new MouseAdapter()
-        {
-          @Override
-          public void mousePressed(MouseEvent evt)
-          {
-            if (evt.isPopupTrigger()) // Mac
-            {
-              offerRemoval(radioItem);
-            }
-          }
-
-          @Override
-          public void mouseReleased(MouseEvent evt)
-          {
-            if (evt.isPopupTrigger()) // Windows
-            {
-              offerRemoval(radioItem);
-            }
-          }
-
-          /**
-           * @param radioItem
-           */
-          void offerRemoval(final JRadioButtonMenuItem radioItem)
-          {
-            radioItem.removeActionListener(radioItem.getActionListeners()[0]);
-
-            int option = JvOptionPane.showInternalConfirmDialog(
-                    jalview.gui.Desktop.desktop, MessageManager
-                            .getString("label.remove_from_default_list"),
-                    MessageManager
-                            .getString("label.remove_user_defined_colour"),
-                    JvOptionPane.YES_NO_OPTION);
-            if (option == JvOptionPane.YES_OPTION)
-            {
-              jalview.gui.UserDefinedColours
-                      .removeColourFromDefaults(radioItem.getText());
-              colourMenu.remove(radioItem);
-            }
-            else
-            {
-              radioItem.addActionListener(new ActionListener()
-              {
-                @Override
-                public void actionPerformed(ActionEvent evt)
-                {
-                  userDefinedColour_actionPerformed(evt);
-                }
-              });
-            }
-          }
-        });
-        radioItem.addActionListener(new ActionListener()
-        {
-          @Override
-          public void actionPerformed(ActionEvent evt)
-          {
-            userDefinedColour_actionPerformed(evt);
-          }
-        });
-
-        colourMenu.insert(radioItem, 15);
-        colours.add(radioItem);
-      }
+      SliderPanel.hidePIDSlider();
     }
   }
 
@@ -3691,35 +3449,11 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    *          DOCUMENT ME!
    */
   @Override
-  public void PIDColour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new PIDColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void BLOSUM62Colour_actionPerformed(ActionEvent e)
-  {
-    changeColour(new Blosum62ColourScheme());
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
   public void sortPairwiseMenuItem_actionPerformed(ActionEvent e)
   {
     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
     AlignmentSorter.sortByPID(viewport.getAlignment(), viewport
-            .getAlignment().getSequenceAt(0), null);
+            .getAlignment().getSequenceAt(0));
     addHistoryItem(new OrderCommand("Pairwise Sort", oldOrder,
             viewport.getAlignment()));
     alignPanel.paintAlignment(true);
@@ -3813,35 +3547,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void PCAMenuItem_actionPerformed(ActionEvent e)
-  {
-    if (((viewport.getSelectionGroup() != null)
-            && (viewport.getSelectionGroup().getSize() < 4) && (viewport
-            .getSelectionGroup().getSize() > 0))
-            || (viewport.getAlignment().getHeight() < 4))
-    {
-      JvOptionPane
-              .showInternalMessageDialog(
-                      this,
-                      MessageManager
-                              .getString("label.principal_component_analysis_must_take_least_four_input_sequences"),
-                      MessageManager
-                              .getString("label.sequence_selection_insufficient"),
-                      JvOptionPane.WARNING_MESSAGE);
-
-      return;
-    }
-
-    new PCAPanel(alignPanel);
-  }
-
   @Override
   public void autoCalculate_actionPerformed(ActionEvent e)
   {
@@ -3866,83 +3571,24 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void averageDistanceTreeMenuItem_actionPerformed(ActionEvent e)
-  {
-    newTreePanel("AV", "PID", "Average distance tree using PID");
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void neighbourTreeMenuItem_actionPerformed(ActionEvent e)
-  {
-    newTreePanel("NJ", "PID", "Neighbour joining tree using PID");
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  protected void njTreeBlosumMenuItem_actionPerformed(ActionEvent e)
-  {
-    newTreePanel("NJ", "BL", "Neighbour joining tree using BLOSUM62");
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  @Override
-  protected void avTreeBlosumMenuItem_actionPerformed(ActionEvent e)
-  {
-    newTreePanel("AV", "BL", "Average distance tree using BLOSUM62");
-  }
-
-  /**
-   * DOCUMENT ME!
+   * Constructs a tree panel and adds it to the desktop
    * 
    * @param type
-   *          DOCUMENT ME!
-   * @param pwType
-   *          DOCUMENT ME!
-   * @param title
-   *          DOCUMENT ME!
+   *          tree type (NJ or AV)
+   * @param modelName
+   *          name of score model used to compute the tree
+   * @param options
+   *          parameters for the distance or similarity calculation
    */
-  void newTreePanel(String type, String pwType, String title)
+  void newTreePanel(String type, String modelName, SimilarityParamsI options)
   {
+    String frameTitle = "";
     TreePanel tp;
 
+    boolean onSelection = false;
     if (viewport.getSelectionGroup() != null
             && viewport.getSelectionGroup().getSize() > 0)
     {
-      if (viewport.getSelectionGroup().getSize() < 3)
-      {
-        JvOptionPane
-                .showMessageDialog(
-                        Desktop.desktop,
-                        MessageManager
-                                .getString("label.you_need_more_two_sequences_selected_build_tree"),
-                        MessageManager
-                                .getString("label.not_enough_sequences"),
-                        JvOptionPane.WARNING_MESSAGE);
-        return;
-      }
-
       SequenceGroup sg = viewport.getSelectionGroup();
 
       /* Decide if the selection is a column region */
@@ -3962,45 +3608,29 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           return;
         }
       }
-
-      title = title + " on region";
-      tp = new TreePanel(alignPanel, type, pwType);
+      onSelection = true;
     }
     else
     {
-      // are the visible sequences aligned?
-      if (!viewport.getAlignment().isAligned(false))
-      {
-        JvOptionPane
-                .showMessageDialog(
-                        Desktop.desktop,
-                        MessageManager
-                                .getString("label.sequences_must_be_aligned_before_creating_tree"),
-                        MessageManager
-                                .getString("label.sequences_not_aligned"),
-                        JvOptionPane.WARNING_MESSAGE);
-
-        return;
-      }
-
       if (viewport.getAlignment().getHeight() < 2)
       {
         return;
       }
-
-      tp = new TreePanel(alignPanel, type, pwType);
     }
 
-    title += " from ";
+    tp = new TreePanel(alignPanel, type, modelName, options);
+    frameTitle = tp.getPanelTitle() + (onSelection ? " on region" : "");
+
+    frameTitle += " from ";
 
     if (viewport.viewName != null)
     {
-      title += viewport.viewName + " of ";
+      frameTitle += viewport.viewName + " of ";
     }
 
-    title += this.title;
+    frameTitle += this.title;
 
-    Desktop.addInternalFrame(tp, title, 600, 500);
+    Desktop.addInternalFrame(tp, frameTitle, 600, 500);
   }
 
   /**
@@ -4122,48 +3752,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * call. Listeners are added to remove the menu item when the treePanel is
    * closed, and adjust the tree leaf to sequence mapping when the alignment is
    * modified.
-   * 
-   * @param treePanel
-   *          Displayed tree window.
-   * @param title
-   *          SortBy menu item title.
    */
   @Override
-  public void buildTreeMenu()
+  public void buildTreeSortMenu()
   {
-    calculateTree.removeAll();
-    // build the calculate menu
-
-    for (final String type : new String[] { "NJ", "AV" })
-    {
-      String treecalcnm = MessageManager.getString("label.tree_calc_"
-              + type.toLowerCase());
-      for (final String pwtype : ResidueProperties.scoreMatrices.keySet())
-      {
-        JMenuItem tm = new JMenuItem();
-        ScoreModelI sm = ResidueProperties.scoreMatrices.get(pwtype);
-        if (sm.isDNA() == viewport.getAlignment().isNucleotide()
-                || sm.isProtein() == !viewport.getAlignment()
-                        .isNucleotide())
-        {
-          String smn = MessageManager.getStringOrReturn(
-                  "label.score_model_", sm.getName());
-          final String title = MessageManager.formatMessage(
-                  "label.treecalc_title", treecalcnm, smn);
-          tm.setText(title);//
-          tm.addActionListener(new java.awt.event.ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-              newTreePanel(type, pwtype, title);
-            }
-          });
-          calculateTree.add(tm);
-        }
-
-      }
-    }
     sortByTreeMenu.removeAll();
 
     List<Component> comps = PaintRefresher.components.get(viewport
@@ -4314,13 +3906,13 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
-      String choice = chooser.getSelectedFile().getPath();
-      jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice);
-      jalview.io.NewickFile fin = null;
+      String filePath = chooser.getSelectedFile().getPath();
+      Cache.setProperty("LAST_DIRECTORY", filePath);
+      NewickFile fin = null;
       try
       {
-        fin = new NewickFile(choice, DataSourceType.FILE);
-        viewport.setCurrentTree(ShowNewickTree(fin, choice).getTree());
+        fin = new NewickFile(filePath, DataSourceType.FILE);
+        viewport.setCurrentTree(showNewickTree(fin, filePath).getTree());
       } catch (Exception ex)
       {
         JvOptionPane
@@ -4342,31 +3934,19 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
   }
 
-  @Override
-  protected void tcoffeeColorScheme_actionPerformed(ActionEvent e)
+  public TreePanel showNewickTree(NewickFile nf, String treeTitle)
   {
-    changeColour(new TCoffeeColourScheme(alignPanel.getAlignment()));
+    return showNewickTree(nf, treeTitle, 600, 500, 4, 5);
   }
 
-  public TreePanel ShowNewickTree(NewickFile nf, String title)
-  {
-    return ShowNewickTree(nf, title, 600, 500, 4, 5);
-  }
-
-  public TreePanel ShowNewickTree(NewickFile nf, String title,
-          AlignmentView input)
-  {
-    return ShowNewickTree(nf, title, input, 600, 500, 4, 5);
-  }
-
-  public TreePanel ShowNewickTree(NewickFile nf, String title, int w,
+  public TreePanel showNewickTree(NewickFile nf, String treeTitle, int w,
           int h, int x, int y)
   {
-    return ShowNewickTree(nf, title, null, w, h, x, y);
+    return showNewickTree(nf, treeTitle, null, w, h, x, y);
   }
 
   /**
-   * Add a treeviewer for the tree extracted from a newick file object to the
+   * Add a treeviewer for the tree extracted from a Newick file object to the
    * current alignment view
    * 
    * @param nf
@@ -4385,7 +3965,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    *          position
    * @return TreePanel handle
    */
-  public TreePanel ShowNewickTree(NewickFile nf, String title,
+  public TreePanel showNewickTree(NewickFile nf, String treeTitle,
           AlignmentView input, int w, int h, int x, int y)
   {
     TreePanel tp = null;
@@ -4396,7 +3976,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
       if (nf.getTree() != null)
       {
-        tp = new TreePanel(alignPanel, "FromFile", title, nf, input);
+        tp = new TreePanel(alignPanel, nf, treeTitle, input);
 
         tp.setSize(w, h);
 
@@ -4405,7 +3985,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           tp.setLocation(x, y);
         }
 
-        Desktop.addInternalFrame(tp, title, w, h);
+        Desktop.addInternalFrame(tp, treeTitle, w, h);
       }
     } catch (Exception ex)
     {
@@ -4978,10 +4558,13 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * Attempt to load a "dropped" file or URL string: First by testing whether
-   * it's an Annotation file, then a JNet file, and finally a features file. If
-   * all are false then the user may have dropped an alignment file onto this
-   * AlignFrame.
+   * Attempt to load a "dropped" file or URL string, by testing in turn for
+   * <ul>
+   * <li>an Annotation file</li>
+   * <li>a JNet file</li>
+   * <li>a features file</li>
+   * <li>else try to interpret as an alignment file</li>
+   * </ul>
    * 
    * @param file
    *          either a filename or a URL string.
@@ -5013,8 +4596,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           {
             if (tcf.annotateAlignment(viewport.getAlignment(), true))
             {
-              tcoffeeColour.setEnabled(true);
-              tcoffeeColour.setSelected(true);
+              buildColourMenu();
               changeColour(new TCoffeeColourScheme(viewport.getAlignment()));
               isAnnotation = true;
               statusBar
@@ -5056,18 +4638,28 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           {
             format = new IdentifyFile().identify(file, sourceType);
           }
-          if (FileFormat.Jnet.equals(format))
+          if (FileFormat.ScoreMatrix == format)
+          {
+            ScoreMatrixFile sm = new ScoreMatrixFile(new FileParse(file,
+                    sourceType));
+            sm.parse();
+            // todo: i18n this message
+            statusBar
+                    .setText(MessageManager.formatMessage(
+                            "label.successfully_loaded_matrix",
+                            sm.getMatrixName()));
+          }
+          else if (FileFormat.Jnet.equals(format))
           {
-            JPredFile predictions = new JPredFile(
-                    file, sourceType);
+            JPredFile predictions = new JPredFile(file, sourceType);
             new JnetAnnotationMaker();
             JnetAnnotationMaker.add_annotation(predictions,
                     viewport.getAlignment(), 0, false);
             SequenceI repseq = viewport.getAlignment().getSequenceAt(0);
             viewport.getAlignment().setSeqrep(repseq);
-            ColumnSelection cs = new ColumnSelection();
+            HiddenColumns cs = new HiddenColumns();
             cs.hideInsertionsFor(repseq);
-            viewport.setColumnSelection(cs);
+            viewport.getAlignment().setHiddenColumns(cs);
             isAnnotation = true;
           }
           // else if (IdentifyFile.FeaturesFile.equals(format))
@@ -5129,6 +4721,28 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
 
     /*
+     * 'focus' any colour slider that is open to the selected viewport
+     */
+    if (viewport.getConservationSelected())
+    {
+      SliderPanel.setConservationSlider(alignPanel,
+              viewport.getResidueShading(), alignPanel.getViewName());
+    }
+    else
+    {
+      SliderPanel.hideConservationSlider();
+    }
+    if (viewport.getAbovePIDThreshold())
+    {
+      SliderPanel.setPIDSliderSource(alignPanel,
+              viewport.getResidueShading(), alignPanel.getViewName());
+    }
+    else
+    {
+      SliderPanel.hidePIDSlider();
+    }
+
+    /*
      * If there is a frame linked to this one in a SplitPane, switch it to the
      * same view tab index. No infinite recursion of calls should happen, since
      * tabSelectionChanged() should not get invoked on setting the selected
@@ -5828,7 +5442,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
     AlignmentI cdna = new Alignment(cdnaSeqs.toArray(new SequenceI[cdnaSeqs
             .size()]));
-    AlignFrame alignFrame = new AlignFrame(cdna, AlignFrame.DEFAULT_WIDTH,
+    GAlignFrame alignFrame = new AlignFrame(cdna, AlignFrame.DEFAULT_WIDTH,
             AlignFrame.DEFAULT_HEIGHT);
     cdna.alignAs(alignment);
     String newtitle = "cDNA " + MessageManager.getString("label.for") + " "
@@ -5944,6 +5558,44 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             true,
             (actionEvent.getModifiers() & (ActionEvent.META_MASK | ActionEvent.CTRL_MASK)) != 0);
   }
+
+  /**
+   * Rebuilds the Colour menu, including any user-defined colours which have
+   * been loaded either on startup or during the session
+   */
+  public void buildColourMenu()
+  {
+    colourMenu.removeAll();
+
+    colourMenu.add(applyToAllGroups);
+    colourMenu.add(textColour);
+    colourMenu.addSeparator();
+
+    ColourMenuHelper.addMenuItems(colourMenu, this,
+            viewport.getAlignment(), false);
+
+    colourMenu.addSeparator();
+    colourMenu.add(conservationMenuItem);
+    colourMenu.add(modifyConservation);
+    colourMenu.add(abovePIDThreshold);
+    colourMenu.add(modifyPID);
+    colourMenu.add(annotationColour);
+
+    ColourSchemeI colourScheme = viewport.getGlobalColourScheme();
+    ColourMenuHelper.setColourSelected(colourMenu, colourScheme);
+  }
+
+  /**
+   * Open a dialog (if not already open) that allows the user to select and
+   * calculate PCA or Tree analysis
+   */
+  protected void openTreePcaDialog()
+  {
+    if (alignPanel.getCalculationDialog() == null)
+    {
+      new CalculationChooser(AlignFrame.this);
+    }
+  }
 }
 
 class PrintThread extends Thread
index 53d118b..86e1144 100644 (file)
@@ -22,7 +22,7 @@ package jalview.gui;
 
 import jalview.analysis.AlignmentUtils;
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
-import jalview.analysis.NJTree;
+import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureColourI;
@@ -35,15 +35,18 @@ import jalview.datamodel.AlignedCodonFrame;
 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.Sequence;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.ResidueShader;
+import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
+import jalview.schemes.ResidueColourScheme;
 import jalview.schemes.UserColourScheme;
-import jalview.structure.CommandListener;
 import jalview.structure.SelectionSource;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
@@ -54,6 +57,7 @@ import jalview.ws.params.AutoCalcSetting;
 import java.awt.Container;
 import java.awt.Dimension;
 import java.awt.Font;
+import java.awt.FontMetrics;
 import java.awt.Rectangle;
 import java.util.ArrayList;
 import java.util.Hashtable;
@@ -61,7 +65,6 @@ import java.util.List;
 import java.util.Vector;
 
 import javax.swing.JInternalFrame;
-import javax.swing.JOptionPane;
 
 /**
  * DOCUMENT ME!
@@ -70,11 +73,11 @@ import javax.swing.JOptionPane;
  * @version $Revision: 1.141 $
  */
 public class AlignViewport extends AlignmentViewport implements
-        SelectionSource, CommandListener
+        SelectionSource
 {
   Font font;
 
-  NJTree currentTree = null;
+  TreeModel currentTree = null;
 
   boolean cursorMode = false;
 
@@ -103,7 +106,7 @@ public class AlignViewport extends AlignmentViewport implements
    */
   public AlignViewport(AlignmentI al)
   {
-    setAlignment(al);
+    super(al);
     init();
   }
 
@@ -121,6 +124,7 @@ public class AlignViewport extends AlignmentViewport implements
 
   public AlignViewport(AlignmentI al, String seqsetid, String viewid)
   {
+    super(al);
     sequenceSetID = seqsetid;
     viewId = viewid;
     // TODO remove these once 2.4.VAMSAS release finished
@@ -133,8 +137,8 @@ public class AlignViewport extends AlignmentViewport implements
     {
       Cache.log.debug("Setting viewport's view id : " + viewId);
     }
-    setAlignment(al);
     init();
+
   }
 
   /**
@@ -145,12 +149,12 @@ public class AlignViewport extends AlignmentViewport implements
    * @param hiddenColumns
    *          ColumnSelection
    */
-  public AlignViewport(AlignmentI al, ColumnSelection hiddenColumns)
+  public AlignViewport(AlignmentI al, HiddenColumns hiddenColumns)
   {
-    setAlignment(al);
+    super(al);
     if (hiddenColumns != null)
     {
-      colSel = hiddenColumns;
+      al.setHiddenColumns(hiddenColumns);
     }
     init();
   }
@@ -163,7 +167,7 @@ public class AlignViewport extends AlignmentViewport implements
    * @param seqsetid
    *          (may be null)
    */
-  public AlignViewport(AlignmentI al, ColumnSelection hiddenColumns,
+  public AlignViewport(AlignmentI al, HiddenColumns hiddenColumns,
           String seqsetid)
   {
     this(al, hiddenColumns, seqsetid, null);
@@ -179,9 +183,10 @@ public class AlignViewport extends AlignmentViewport implements
    * @param viewid
    *          (may be null)
    */
-  public AlignViewport(AlignmentI al, ColumnSelection hiddenColumns,
+  public AlignViewport(AlignmentI al, HiddenColumns hiddenColumns,
           String seqsetid, String viewid)
   {
+    super(al);
     sequenceSetID = seqsetid;
     viewId = viewid;
     // TODO remove these once 2.4.VAMSAS release finished
@@ -194,10 +199,10 @@ public class AlignViewport extends AlignmentViewport implements
     {
       Cache.log.debug("Setting viewport's view id : " + viewId);
     }
-    setAlignment(al);
+
     if (hiddenColumns != null)
     {
-      colSel = hiddenColumns;
+      al.setHiddenColumns(hiddenColumns);
     }
     init();
   }
@@ -236,10 +241,6 @@ public class AlignViewport extends AlignmentViewport implements
 
   void init()
   {
-    this.startRes = 0;
-    this.endRes = alignment.getWidth() - 1;
-    this.startSeq = 0;
-    this.endSeq = alignment.getHeight() - 1;
     applyViewProperties();
 
     String fontName = Cache.getDefault("FONT_NAME", "SansSerif");
@@ -280,32 +281,33 @@ public class AlignViewport extends AlignmentViewport implements
               false);
       showGroupConsensus = Cache.getDefault("SHOW_GROUP_CONSENSUS", false);
       showConsensus = Cache.getDefault("SHOW_IDENTITY", true);
+
+      showOccupancy = Cache.getDefault(Preferences.SHOW_OCCUPANCY, true);
     }
     initAutoAnnotation();
     String colourProperty = alignment.isNucleotide() ? Preferences.DEFAULT_COLOUR_NUC
             : Preferences.DEFAULT_COLOUR_PROT;
-    String propertyValue = Cache.getProperty(colourProperty);
-    if (propertyValue == null)
+    String schemeName = Cache.getProperty(colourProperty);
+    if (schemeName == null)
     {
-      // fall back on this property for backwards compatibility
-      propertyValue = Cache.getProperty(Preferences.DEFAULT_COLOUR);
+      // only DEFAULT_COLOUR available in Jalview before 2.9
+      schemeName = Cache.getDefault(Preferences.DEFAULT_COLOUR,
+              ResidueColourScheme.NONE);
     }
-    if (propertyValue != null)
-    {
-      globalColourScheme = ColourSchemeProperty.getColour(alignment,
-              propertyValue);
+    ColourSchemeI colourScheme = ColourSchemeProperty.getColourScheme(
+            alignment, schemeName);
+    residueShading = new ResidueShader(colourScheme);
 
-      if (globalColourScheme instanceof UserColourScheme)
-      {
-        globalColourScheme = UserDefinedColours.loadDefaultColours();
-        ((UserColourScheme) globalColourScheme).setThreshold(0,
-                isIgnoreGapsConsensus());
-      }
+    if (colourScheme instanceof UserColourScheme)
+    {
+      residueShading = new ResidueShader(
+              UserDefinedColours.loadDefaultColours());
+      residueShading.setThreshold(0, isIgnoreGapsConsensus());
+    }
 
-      if (globalColourScheme != null)
-      {
-        globalColourScheme.setConsensus(hconsensus);
-      }
+    if (residueShading != null)
+    {
+      residueShading.setConsensus(hconsensus);
     }
   }
 
@@ -350,23 +352,19 @@ public class AlignViewport extends AlignmentViewport implements
   boolean validCharWidth;
 
   /**
-   * update view settings with the given font. You may need to call
-   * alignPanel.fontChanged to update the layout geometry
-   * 
-   * @param setGrid
-   *          when true, charWidth/height is set according to font mentrics
+   * {@inheritDoc}
    */
+  @Override
   public void setFont(Font f, boolean setGrid)
   {
     font = f;
 
     Container c = new Container();
 
-    java.awt.FontMetrics fm = c.getFontMetrics(font);
-    int w = viewStyle.getCharWidth(), ww = fm.charWidth('M'), h = viewStyle
-            .getCharHeight();
     if (setGrid)
     {
+      FontMetrics fm = c.getFontMetrics(font);
+      int ww = fm.charWidth('M');
       setCharHeight(fm.getHeight());
       setCharWidth(ww);
     }
@@ -383,7 +381,6 @@ public class AlignViewport extends AlignmentViewport implements
     super.setViewStyle(settingsForView);
     setFont(new Font(viewStyle.getFontName(), viewStyle.getFontStyle(),
             viewStyle.getFontSize()), false);
-
   }
 
   /**
@@ -494,7 +491,7 @@ public class AlignViewport extends AlignmentViewport implements
    * @param tree
    *          DOCUMENT ME!
    */
-  public void setCurrentTree(NJTree tree)
+  public void setCurrentTree(TreeModel tree)
   {
     currentTree = tree;
   }
@@ -504,7 +501,7 @@ public class AlignViewport extends AlignmentViewport implements
    * 
    * @return DOCUMENT ME!
    */
-  public NJTree getCurrentTree()
+  public TreeModel getCurrentTree()
   {
     return currentTree;
   }
@@ -530,7 +527,7 @@ public class AlignViewport extends AlignmentViewport implements
     {
       end = alignment.getWidth();
     }
-    viscontigs = colSel.getVisibleContigs(start, end);
+    viscontigs = alignment.getHiddenColumns().getVisibleContigs(start, end);
     return viscontigs;
   }
 
@@ -600,7 +597,9 @@ public class AlignViewport extends AlignmentViewport implements
     jalview.structure.StructureSelectionManager
             .getStructureSelectionManager(Desktop.instance).sendSelection(
                     new SequenceGroup(getSelectionGroup()),
-                    new ColumnSelection(getColumnSelection()), this);
+                    new ColumnSelection(getColumnSelection()),
+                    new HiddenColumns(getAlignment().getHiddenColumns()),
+                    this);
   }
 
   /**
@@ -855,7 +854,7 @@ public class AlignViewport extends AlignmentViewport implements
       }
     }
 
-    setEndSeq(getAlignment().getHeight());
+    ranges.setEndSeq(getAlignment().getHeight());
     firePropertyChange("alignment", null, getAlignment().getSequences());
   }
 
@@ -1050,8 +1049,9 @@ public class AlignViewport extends AlignmentViewport implements
       // TODO would like next line without cast but needs more refactoring...
       final AlignmentPanel complementPanel = ((AlignViewport) getCodingComplement())
               .getAlignPanel();
-      complementPanel.setDontScrollComplement(true);
+      complementPanel.setToScrollComplementPanel(false);
       complementPanel.scrollToCentre(sr, verticalOffset);
+      complementPanel.setToScrollComplementPanel(true);
     }
   }
 
index e61b042..72b1cc9 100644 (file)
@@ -25,16 +25,21 @@ import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 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;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -46,6 +51,8 @@ import java.awt.Graphics;
 import java.awt.Insets;
 import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
 import java.awt.print.PageFormat;
 import java.awt.print.Printable;
 import java.awt.print.PrinterException;
@@ -65,10 +72,13 @@ import javax.swing.SwingUtilities;
  * @version $Revision: 1.161 $
  */
 public class AlignmentPanel extends GAlignmentPanel implements
-        AdjustmentListener, Printable, AlignmentViewPanel
+        AdjustmentListener, Printable, AlignmentViewPanel,
+        ViewportListenerI
 {
   public AlignViewport av;
 
+  ViewportRanges vpRanges;
+
   OverviewPanel overviewPanel;
 
   private SeqPanel seqPanel;
@@ -91,18 +101,20 @@ public class AlignmentPanel extends GAlignmentPanel implements
   // this value is set false when selection area being dragged
   boolean fastPaint = true;
 
-  int hextent = 0;
+  private int hextent = 0;
 
-  int vextent = 0;
+  private int vextent = 0;
 
   /*
    * Flag set while scrolling to follow complementary cDNA/protein scroll. When
-   * true, suppresses invoking the same method recursively.
+   * false, suppresses invoking the same method recursively.
    */
-  private boolean dontScrollComplement;
+  private boolean scrollComplementaryPanel = true;
 
   private PropertyChangeListener propertyChangeListener;
 
+  private CalculationChooser calculationDialog;
+
   /**
    * Creates a new AlignmentPanel object.
    * 
@@ -113,6 +125,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
   {
     alignFrame = af;
     this.av = av;
+    vpRanges = av.getRanges();
     setSeqPanel(new SeqPanel(av, this));
     setIdPanel(new IdPanel(av, this));
 
@@ -136,6 +149,34 @@ public class AlignmentPanel extends GAlignmentPanel implements
     hscroll.addAdjustmentListener(this);
     vscroll.addAdjustmentListener(this);
 
+    addComponentListener(new ComponentAdapter()
+    {
+      @Override
+      public void componentResized(ComponentEvent evt)
+      {
+        // reset the viewport ranges when the alignment panel is resized
+        // in particular, this initialises the end residue value when Jalview
+        // is initialised
+        if (av.getWrapAlignment())
+        {
+          int widthInRes = getSeqPanel().seqCanvas
+                  .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+          vpRanges.setViewportWidth(widthInRes);
+        }
+        else
+        {
+          int widthInRes = getSeqPanel().seqCanvas.getWidth()
+                  / av.getCharWidth();
+          int heightInSeq = getSeqPanel().seqCanvas.getHeight()
+                  / av.getCharHeight();
+          
+          vpRanges.setViewportWidth(widthInRes);
+          vpRanges.setViewportHeight(heightInSeq);
+        }
+      }
+
+    });
+
     final AlignmentPanel ap = this;
     propertyChangeListener = new PropertyChangeListener()
     {
@@ -150,6 +191,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
       }
     };
     av.addPropertyChangeListener(propertyChangeListener);
+
+    av.getRanges().addPropertyChangeListener(this);
     fontChanged();
     adjustAnnotationHeight();
     updateLayout();
@@ -165,6 +208,11 @@ public class AlignmentPanel extends GAlignmentPanel implements
   {
     av.alignmentChanged(this);
 
+    if (getCalculationDialog() != null)
+    {
+      getCalculationDialog().validateCalcTypes();
+    }
+
     alignFrame.updateEditMenuBar();
 
     paintAlignment(true);
@@ -195,10 +243,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
     getIdPanel().getIdCanvas().setPreferredSize(d);
     hscrollFillerPanel.setPreferredSize(d);
 
-    if (overviewPanel != null)
-    {
-      overviewPanel.setBoxPosition();
-    }
     if (this.alignFrame.getSplitViewContainer() != null)
     {
       ((SplitFrame) this.alignFrame.getSplitViewContainer()).adjustLayout();
@@ -367,9 +411,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
       }
       int start = r[0];
       int end = r[1];
-      // DEBUG
-      // System.err.println(this.av.viewName + " Seq : " + seqIndex
-      // + " Scroll to " + start + "," + end);
 
       /*
        * To centre results, scroll to positions half the visible width
@@ -377,7 +418,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
        */
       if (centre)
       {
-        int offset = (av.getEndRes() - av.getStartRes() + 1) / 2 - 1;
+        int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2
+                - 1;
         start = Math.max(start - offset, 0);
         end = end + offset - 1;
       }
@@ -391,11 +433,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
       }
       if (av.hasHiddenColumns())
       {
-        start = av.getColumnSelection().findColumnPosition(start);
-        end = av.getColumnSelection().findColumnPosition(end);
+        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+        start = hidden.findColumnPosition(start);
+        end = hidden.findColumnPosition(end);
         if (start == end)
         {
-          if (!av.getColumnSelection().isVisible(r[0]))
+          if (!hidden.isVisible(r[0]))
           {
             // don't scroll - position isn't visible
             return false;
@@ -408,40 +451,36 @@ public class AlignmentPanel extends GAlignmentPanel implements
        */
       seqIndex = Math.max(0, seqIndex - verticalOffset);
 
-      // System.out.println("start=" + start + ", end=" + end + ", startv="
-      // + av.getStartRes() + ", endv=" + av.getEndRes() + ", starts="
-      // + av.getStartSeq() + ", ends=" + av.getEndSeq());
       if (!av.getWrapAlignment())
       {
-        if ((startv = av.getStartRes()) >= start)
+        if ((startv = vpRanges.getStartRes()) >= start)
         {
           /*
            * Scroll left to make start of search results visible
            */
-          // setScrollValues(start - 1, seqIndex); // plus one residue
           setScrollValues(start, seqIndex);
         }
-        else if ((endv = av.getEndRes()) <= end)
+        else if ((endv = vpRanges.getEndRes()) <= end)
         {
           /*
            * Scroll right to make end of search results visible
            */
-          // setScrollValues(startv + 1 + end - endv, seqIndex); // plus one
           setScrollValues(startv + end - endv, seqIndex);
         }
-        else if ((starts = av.getStartSeq()) > seqIndex)
+        else if ((starts = vpRanges.getStartSeq()) > seqIndex)
         {
           /*
            * Scroll up to make start of search results visible
            */
-          setScrollValues(av.getStartRes(), seqIndex);
+          setScrollValues(vpRanges.getStartRes(), seqIndex);
         }
-        else if ((ends = av.getEndSeq()) <= seqIndex)
+        else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
         {
           /*
            * Scroll down to make end of search results visible
            */
-          setScrollValues(av.getStartRes(), starts + seqIndex - ends + 1);
+          setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends
+                  + 1);
         }
         /*
          * Else results are already visible - no need to scroll
@@ -449,29 +488,14 @@ public class AlignmentPanel extends GAlignmentPanel implements
       }
       else
       {
-        scrollToWrappedVisible(start);
+        vpRanges.scrollToWrappedVisible(start);
       }
     }
-    if (redrawOverview && overviewPanel != null)
-    {
-      overviewPanel.setBoxPosition();
-    }
+
     paintAlignment(redrawOverview);
     return true;
   }
 
-  void scrollToWrappedVisible(int res)
-  {
-    int cwidth = getSeqPanel().seqCanvas
-            .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
-    if (res < av.getStartRes() || res >= (av.getStartRes() + cwidth))
-    {
-      vscroll.setValue((res / cwidth));
-      av.startRes = vscroll.getValue() * cwidth;
-    }
-
-  }
-
   /**
    * DOCUMENT ME!
    * 
@@ -591,7 +615,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     fontChanged();
     setAnnotationVisible(av.isShowAnnotation());
     boolean wrap = av.getWrapAlignment();
-    av.startSeq = 0;
+    vpRanges.setStartSeq(0);
     scalePanelHolder.setVisible(!wrap);
     hscroll.setVisible(!wrap);
     idwidthAdjuster.setVisible(!wrap);
@@ -607,75 +631,28 @@ public class AlignmentPanel extends GAlignmentPanel implements
       annotationSpaceFillerHolder.setVisible(true);
     }
 
-    idSpaceFillerPanel1.setVisible(!wrap);
-
-    repaint();
-  }
-
-  // return value is true if the scroll is valid
-  public boolean scrollUp(boolean up)
-  {
-    if (up)
+    if (wrap)
     {
-      if (vscroll.getValue() < 1)
-      {
-        return false;
-      }
-
-      fastPaint = false;
-      vscroll.setValue(vscroll.getValue() - 1);
+      int widthInRes = getSeqPanel().seqCanvas
+              .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+      vpRanges.setViewportWidth(widthInRes);
     }
     else
     {
-      if ((vextent + vscroll.getValue()) >= av.getAlignment().getHeight())
-      {
-        return false;
-      }
+      int widthInRes = (getSeqPanel().seqCanvas.getWidth() / av
+              .getCharWidth()) - 1;
+      int heightInSeq = (getSeqPanel().seqCanvas.getHeight() / av
+              .getCharHeight()) - 1;
 
-      fastPaint = false;
-      vscroll.setValue(vscroll.getValue() + 1);
+      vpRanges.setViewportWidth(widthInRes);
+      vpRanges.setViewportHeight(heightInSeq);
     }
 
-    fastPaint = true;
+    idSpaceFillerPanel1.setVisible(!wrap);
 
-    return true;
+    repaint();
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param right
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public boolean scrollRight(boolean right)
-  {
-    if (!right)
-    {
-      if (hscroll.getValue() < 1)
-      {
-        return false;
-      }
-
-      fastPaint = false;
-      hscroll.setValue(hscroll.getValue() - 1);
-    }
-    else
-    {
-      if ((hextent + hscroll.getValue()) >= av.getAlignment().getWidth())
-      {
-        return false;
-      }
-
-      fastPaint = false;
-      hscroll.setValue(hscroll.getValue() + 1);
-    }
-
-    fastPaint = true;
-
-    return true;
-  }
 
   /**
    * Adjust row/column scrollers to show a visible position in the alignment.
@@ -686,172 +663,170 @@ public class AlignmentPanel extends GAlignmentPanel implements
    *          visible row to scroll to
    * 
    */
-  public void setScrollValues(int x, int y)
+  public void setScrollValues(int xpos, int ypos)
   {
-    // System.err.println("Scroll " + this.av.viewName + " to " + x + "," + y);
+    int x = xpos;
+    int y = ypos;
+
     if (av == null || av.getAlignment() == null)
     {
       return;
     }
-    int width = av.getAlignment().getWidth();
-    int height = av.getAlignment().getHeight();
 
-    if (av.hasHiddenColumns())
+    if (av.getWrapAlignment())
     {
-      width = av.getColumnSelection().findColumnPosition(width);
+      setScrollingForWrappedPanel(x);
     }
+    else
+    {
+      int width = av.getAlignment().getWidth();
+      int height = av.getAlignment().getHeight();
 
-    av.setEndRes((x + (getSeqPanel().seqCanvas.getWidth() / av
-            .getCharWidth())) - 1);
+      if (av.hasHiddenColumns())
+      {
+        // reset the width to exclude hidden columns
+        width = av.getAlignment().getHiddenColumns().findColumnPosition(width);
+      }
 
-    hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
-    vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
+      hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
+      vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
 
-    if (hextent > width)
-    {
-      hextent = width;
-    }
+      if (hextent > width)
+      {
+        hextent = width;
+      }
 
-    if (vextent > height)
-    {
-      vextent = height;
-    }
+      if (vextent > height)
+      {
+        vextent = height;
+      }
 
-    if ((hextent + x) > width)
-    {
-      x = width - hextent;
-    }
+      if ((hextent + x) > width)
+      {
+        x = width - hextent;
+      }
 
-    if ((vextent + y) > height)
-    {
-      y = height - vextent;
-    }
+      if ((vextent + y) > height)
+      {
+        y = height - vextent;
+      }
 
-    if (y < 0)
-    {
-      y = 0;
-    }
+      if (y < 0)
+      {
+        y = 0;
+      }
 
-    if (x < 0)
-    {
-      x = 0;
-    }
+      if (x < 0)
+      {
+        x = 0;
+      }
 
-    /*
-     * each scroll adjustment triggers adjustmentValueChanged, which resets the
-     * 'do not scroll complement' flag; ensure it is the same for both
-     * operations
-     */
-    boolean flag = isDontScrollComplement();
-    hscroll.setValues(x, hextent, 0, width);
-    setDontScrollComplement(flag);
-    vscroll.setValues(y, vextent, 0, height);
+      // update the scroll values
+      hscroll.setValues(x, hextent, 0, width);
+      vscroll.setValues(y, vextent, 0, height);
+    }
   }
 
   /**
-   * DOCUMENT ME!
+   * Respond to adjustment event when horizontal or vertical scrollbar is
+   * changed
    * 
    * @param evt
-   *          DOCUMENT ME!
+   *          adjustment event encoding whether hscroll or vscroll changed
    */
   @Override
   public void adjustmentValueChanged(AdjustmentEvent evt)
   {
-    int oldX = av.getStartRes();
-    int oldY = av.getStartSeq();
+    int oldX = vpRanges.getStartRes();
+    int oldwidth = vpRanges.getViewportWidth();
+    int oldY = vpRanges.getStartSeq();
+    int oldheight = vpRanges.getViewportHeight();
 
-    if (evt.getSource() == hscroll)
-    {
-      int x = hscroll.getValue();
-      av.setStartRes(x);
-      av.setEndRes((x + (getSeqPanel().seqCanvas.getWidth() / av
-              .getCharWidth())) - 1);
-    }
-
-    if (evt.getSource() == vscroll)
+    if (av.getWrapAlignment())
     {
-      int offy = vscroll.getValue();
-
-      if (av.getWrapAlignment())
+      if (evt.getSource() == hscroll)
+      {
+        return; // no horizontal scroll when wrapped
+      }
+      else if (evt.getSource() == vscroll)
       {
-        if (offy > -1)
+        int offy = vscroll.getValue();
+        int rowSize = getSeqPanel().seqCanvas
+                .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+
+        // if we're scrolling to the position we're already at, stop
+        // this prevents infinite recursion of events when the scroll/viewport
+        // ranges values are the same
+        if ((offy * rowSize == oldX) && (oldwidth == rowSize))
         {
-          int rowSize = getSeqPanel().seqCanvas
-                  .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
-          av.setStartRes(offy * rowSize);
-          av.setEndRes((offy + 1) * rowSize);
+          return;
         }
-        else
+        else if (offy > -1)
         {
-          // This is only called if file loaded is a jar file that
-          // was wrapped when saved and user has wrap alignment true
-          // as preference setting
-          SwingUtilities.invokeLater(new Runnable()
-          {
-            @Override
-            public void run()
-            {
-              setScrollValues(av.getStartRes(), av.getStartSeq());
-            }
-          });
+          vpRanges.setViewportStartAndWidth(offy * rowSize, rowSize);
         }
       }
       else
       {
-        av.setStartSeq(offy);
-        av.setEndSeq(offy
-                + (getSeqPanel().seqCanvas.getHeight() / av.getCharHeight()));
+        // This is only called if file loaded is a jar file that
+        // was wrapped when saved and user has wrap alignment true
+        // as preference setting
+        SwingUtilities.invokeLater(new Runnable()
+        {
+          @Override
+          public void run()
+        {
+            // When updating scrolling to use ViewportChange events, this code
+            // could not be validated and it is not clear if it is now being
+            // called. Log warning here in case it is called and unforeseen
+            // problems occur
+            Cache.log
+                    .warn("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);
+          }
+        });
       }
-    }
-
-    if (overviewPanel != null)
-    {
-      overviewPanel.setBoxPosition();
-    }
-
-    int scrollX = av.startRes - oldX;
-    int scrollY = av.startSeq - oldY;
-
-    if (av.getWrapAlignment() || !fastPaint)
-    {
       repaint();
     }
     else
     {
-      // Make sure we're not trying to draw a panel
-      // larger than the visible window
-      if (scrollX > av.endRes - av.startRes)
+      // horizontal scroll
+      if (evt.getSource() == hscroll)
       {
-        scrollX = av.endRes - av.startRes;
-      }
-      else if (scrollX < av.startRes - av.endRes)
-      {
-        scrollX = av.startRes - av.endRes;
-      }
+        int x = hscroll.getValue();
+        int width = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
 
-      if (scrollX != 0 || scrollY != 0)
+        // if we're scrolling to the position we're already at, stop
+        // this prevents infinite recursion of events when the scroll/viewport
+        // ranges values are the same
+        if ((x == oldX) && (width == oldwidth))
+        {
+          return;
+        }
+        vpRanges.setViewportStartAndWidth(x, width);
+      }
+      else if (evt.getSource() == vscroll)
       {
-        getIdPanel().getIdCanvas().fastPaint(scrollY);
-        getSeqPanel().seqCanvas.fastPaint(scrollX, scrollY);
-        getScalePanel().repaint();
-
-        if (av.isShowAnnotation() && scrollX != 0)
+        int y = vscroll.getValue();
+        int height = getSeqPanel().seqCanvas.getHeight()
+                / av.getCharHeight();
+
+        // if we're scrolling to the position we're already at, stop
+        // this prevents infinite recursion of events when the scroll/viewport
+        // ranges values are the same
+        if ((y == oldY) && (height == oldheight))
         {
-          getAnnotationPanel().fastPaint(scrollX);
+          return;
         }
+        vpRanges.setViewportStartAndHeight(y, height);
+      }
+      if (!fastPaint)
+      {
+        repaint();
       }
-    }
-    /*
-     * If there is one, scroll the (Protein/cDNA) complementary alignment to
-     * match, unless we are ourselves doing that.
-     */
-    if (isDontScrollComplement())
-    {
-      setDontScrollComplement(false);
-    }
-    else
-    {
-      av.scrollComplementaryAlignment();
     }
   }
 
@@ -897,36 +872,39 @@ public class AlignmentPanel extends GAlignmentPanel implements
     validate();
 
     /*
-     * set scroll bar positions; first suppress this being 'followed' in any
-     * complementary split pane
+     * set scroll bar positions
      */
-    setDontScrollComplement(true);
+    setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
+  }
 
-    if (av.getWrapAlignment())
+  /*
+   * Set vertical scroll bar parameters for wrapped panel
+   * @param res 
+   *    the residue to scroll to
+   */
+  private void setScrollingForWrappedPanel(int res)
+  {
+    // get the width of the alignment in residues
+    int maxwidth = av.getAlignment().getWidth();
+    if (av.hasHiddenColumns())
     {
-      int maxwidth = av.getAlignment().getWidth();
-
-      if (av.hasHiddenColumns())
-      {
-        maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
-      }
-
-      int canvasWidth = getSeqPanel().seqCanvas
-              .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
-      if (canvasWidth > 0)
-      {
-        int max = maxwidth
-                / getSeqPanel().seqCanvas
-                        .getWrappedCanvasWidth(getSeqPanel().seqCanvas
-                                .getWidth()) + 1;
-        vscroll.setMaximum(max);
-        vscroll.setUnitIncrement(1);
-        vscroll.setVisibleAmount(1);
-      }
+        maxwidth = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(maxwidth) - 1;
     }
-    else
+
+    // get the width of the canvas in residues
+    int canvasWidth = getSeqPanel().seqCanvas
+            .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
+    if (canvasWidth > 0)
     {
-      setScrollValues(av.getStartRes(), av.getStartSeq());
+      // position we want to scroll to is number of canvasWidth's to get there
+      int current = res / canvasWidth;
+
+      // max scroll position: add one because extent is 1 and scrollbar value
+      // can only be set to at most max - extent
+      int max = maxwidth / canvasWidth + 1;
+      vscroll.setUnitIncrement(1);
+      vscroll.setValues(current, 1, 0, max);
     }
   }
 
@@ -1196,7 +1174,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     int resWidth = getSeqPanel().seqCanvas.getWrappedCanvasWidth(pwidth
@@ -1390,7 +1369,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth);
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth);
     }
 
     int height = ((av.getAlignment().getHeight() + 1) * av.getCharHeight())
@@ -1460,33 +1440,32 @@ 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) + "");
+                      .getCharAt(column) + "");
             }
             else
             {
-              triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(res)
+              triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(column)
                       + "");
             }
 
@@ -1495,84 +1474,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());
             }
           }
         }
@@ -1620,7 +1588,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     int height = ((maxwidth / chunkWidth) + 1) * cHeight;
@@ -1638,6 +1607,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     PaintRefresher.RemoveComponent(getIdPanel().getIdCanvas());
     PaintRefresher.RemoveComponent(this);
 
+    closeChildFrames();
+
     /*
      * try to ensure references are nulled
      */
@@ -1669,6 +1640,17 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   /**
+   * Close any open dialogs that would be orphaned when this one is closed
+   */
+  protected void closeChildFrames()
+  {
+    if (calculationDialog != null)
+    {
+      calculationDialog.closeFrame();
+    }
+  }
+
+  /**
    * hides or shows dynamic annotation rows based on groups and av state flags
    */
   public void updateAnnotation()
@@ -1866,13 +1848,82 @@ public class AlignmentPanel extends GAlignmentPanel implements
    * 
    * @param b
    */
-  protected void setDontScrollComplement(boolean b)
+  protected void setToScrollComplementPanel(boolean b)
+  {
+    this.scrollComplementaryPanel = b;
+  }
+
+  /**
+   * Get whether to scroll complement panel
+   * 
+   * @return true if cDNA/protein complement panels should be scrolled
+   */
+  protected boolean isSetToScrollComplementPanel()
+  {
+    return this.scrollComplementaryPanel;
+  }
+
+  /**
+   * Redraw sensibly.
+   * 
+   * @adjustHeight if true, try to recalculate panel height for visible
+   *               annotations
+   */
+  protected void refresh(boolean adjustHeight)
   {
-    this.dontScrollComplement = b;
+    validateAnnotationDimensions(adjustHeight);
+    addNotify();
+    if (adjustHeight)
+    {
+      // sort, repaint, update overview
+      paintAlignment(true);
+    }
+    else
+    {
+      // lightweight repaint
+      repaint();
+    }
   }
 
-  protected boolean isDontScrollComplement()
+  @Override
+  /**
+   * Property change event fired when a change is made to the viewport ranges 
+   * object associated with this alignment panel's viewport
+   */
+  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();
+    setScrollValues(x, y);
+
+    // now update any complementary alignment (its viewport ranges object
+    // is different so does not get automatically updated)
+    if (isSetToScrollComplementPanel())
+    {
+      setToScrollComplementPanel(false);
+      av.scrollComplementaryAlignment();
+      setToScrollComplementPanel(true);
+    }
+  }
+
+  /**
+   * Set the reference to the PCA/Tree chooser dialog for this panel. This
+   * reference should be nulled when the dialog is closed.
+   * 
+   * @param calculationChooser
+   */
+  public void setCalculationDialog(CalculationChooser calculationChooser)
+  {
+    calculationDialog = calculationChooser;
+  }
+
+  /**
+   * Returns the reference to the PCA/Tree chooser dialog for this panel (null
+   * if none is open)
+   */
+  public CalculationChooser getCalculationDialog()
   {
-    return this.dontScrollComplement;
+    return calculationDialog;
   }
 }
index 39cde03..253a7ec 100644 (file)
@@ -21,6 +21,8 @@
 package jalview.gui;
 
 import jalview.bin.Cache;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.GraphLine;
 import jalview.datamodel.SequenceGroup;
 import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.ColourSchemeI;
@@ -35,9 +37,11 @@ import java.awt.event.ActionListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.util.Hashtable;
+import java.util.Vector;
 
 import javax.swing.BorderFactory;
 import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JColorChooser;
 import javax.swing.JComboBox;
 import javax.swing.JInternalFrame;
@@ -49,27 +53,25 @@ import net.miginfocom.swing.MigLayout;
 @SuppressWarnings("serial")
 public class AnnotationColourChooser extends AnnotationRowFilter
 {
+  private static final int ONETHOUSAND = 1000;
 
-  ColourSchemeI oldcs;
+  private ColourSchemeI oldcs;
 
-  Hashtable<SequenceGroup, ColourSchemeI> oldgroupColours;
+  private JButton defColours;
 
-  /**
-   * enabled if the user is dragging the slider - try to keep updates to a
-   * minimun
-   */
+  private Hashtable<SequenceGroup, ColourSchemeI> oldgroupColours;
 
-  JComboBox<String> annotations;
+  private JCheckBox useOriginalColours = new JCheckBox();
 
-  JButton defColours = new JButton();
+  private JPanel minColour = new JPanel();
 
-  JPanel jPanel1 = new JPanel();
+  private JPanel maxColour = new JPanel();
 
-  JPanel jPanel2 = new JPanel();
+  private JCheckBox thresholdIsMin = new JCheckBox();
 
-  BorderLayout borderLayout1 = new BorderLayout();
+  protected static final int MIN_WIDTH = 500;
 
-  private JComboBox<String> threshold = new JComboBox<String>();
+  protected static final int MIN_HEIGHT = 240;
 
   public AnnotationColourChooser(AlignViewport av, final AlignmentPanel ap)
   {
@@ -80,9 +82,9 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       oldgroupColours = new Hashtable<SequenceGroup, ColourSchemeI>();
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
-        if (sg.cs != null)
+        if (sg.getColourScheme() != null)
         {
-          oldgroupColours.put(sg, sg.cs);
+          oldgroupColours.put(sg, sg.getColourScheme());
         }
       }
     }
@@ -92,7 +94,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     Desktop.addInternalFrame(frame,
             MessageManager.getString("label.colour_by_annotation"), 520,
             215);
-
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
     addSliderChangeListener();
     addSliderMouseListeners();
 
@@ -108,7 +110,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     if (oldcs instanceof AnnotationColourGradient)
     {
       AnnotationColourGradient acg = (AnnotationColourGradient) oldcs;
-      currentColours.setSelected(acg.isPredefinedColours()
+      useOriginalColours.setSelected(acg.isPredefinedColours()
               || acg.getBaseColour() != null);
       if (!acg.isPredefinedColours() && acg.getBaseColour() == null)
       {
@@ -118,15 +120,17 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       seqAssociated.setSelected(acg.isSeqAssociated());
 
     }
-    annotations = new JComboBox<String>(
-            getAnnotationItems(seqAssociated.isSelected()));
+    Vector<String> annotItems = getAnnotationItems(seqAssociated
+            .isSelected());
+    annotations = new JComboBox<String>(annotItems);
 
     populateThresholdComboBox(threshold);
 
     if (oldcs instanceof AnnotationColourGradient)
     {
       AnnotationColourGradient acg = (AnnotationColourGradient) oldcs;
-      annotations.setSelectedItem(acg.getAnnotation());
+      String label = getAnnotationMenuLabel(acg.getAnnotation());
+      annotations.setSelectedItem(label);
       switch (acg.getAboveThreshold())
       {
       case AnnotationColourGradient.NO_THRESHOLD:
@@ -143,16 +147,11 @@ public class AnnotationColourChooser extends AnnotationRowFilter
                 MessageManager
                         .getString("error.implementation_error_dont_know_about_threshold_setting"));
       }
-      thresholdIsMin.setSelected(acg.thresholdIsMinMax);
+      thresholdIsMin.setSelected(acg.isThresholdIsMinMax());
       thresholdValue.setText("" + acg.getAnnotationThreshold());
     }
 
-    try
-    {
-      jbInit();
-    } catch (Exception ex)
-    {
-    }
+    jbInit();
     adjusting = false;
 
     updateView();
@@ -160,19 +159,11 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     frame.pack();
   }
 
-  public AnnotationColourChooser()
+  @Override
+  protected void jbInit()
   {
-    try
-    {
-      jbInit();
-    } catch (Exception ex)
-    {
-      ex.printStackTrace();
-    }
-  }
+    super.jbInit();
 
-  private void jbInit() throws Exception
-  {
     minColour.setFont(JvSwingUtils.getLabelFont());
     minColour.setBorder(BorderFactory.createEtchedBorder());
     minColour.setPreferredSize(new Dimension(40, 20));
@@ -203,26 +194,8 @@ public class AnnotationColourChooser extends AnnotationRowFilter
         }
       }
     });
-    ok.setOpaque(false);
-    ok.setText(MessageManager.getString("action.ok"));
-    ok.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        ok_actionPerformed();
-      }
-    });
-    cancel.setOpaque(false);
-    cancel.setText(MessageManager.getString("action.cancel"));
-    cancel.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        cancel_actionPerformed();
-      }
-    });
+
+    defColours = new JButton();
     defColours.setOpaque(false);
     defColours.setText(MessageManager.getString("action.set_defaults"));
     defColours.setToolTipText(MessageManager
@@ -237,48 +210,16 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       }
     });
 
-    annotations.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        annotations_actionPerformed();
-      }
-    });
-    getThreshold().addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        threshold_actionPerformed();
-      }
-    });
-    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));
-    thresholdValue.setEnabled(false);
-    thresholdValue.setColumns(7);
-    currentColours.setFont(JvSwingUtils.getLabelFont());
-    currentColours.setOpaque(false);
-    currentColours.setText(MessageManager
+    useOriginalColours.setFont(JvSwingUtils.getLabelFont());
+    useOriginalColours.setOpaque(false);
+    useOriginalColours.setText(MessageManager
             .getString("label.use_original_colours"));
-    currentColours.addActionListener(new ActionListener()
+    useOriginalColours.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        currentColours_actionPerformed();
+        originalColours_actionPerformed();
       }
     });
     thresholdIsMin.setBackground(Color.white);
@@ -307,7 +248,9 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       }
     });
 
-    this.setLayout(borderLayout1);
+    this.setLayout(new BorderLayout());
+    JPanel jPanel1 = new JPanel();
+    JPanel jPanel2 = new JPanel();
     jPanel2.setLayout(new MigLayout("", "[left][center][right]", "[][][]"));
     jPanel1.setBackground(Color.white);
     jPanel2.setBackground(Color.white);
@@ -316,7 +259,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     jPanel1.add(cancel);
     jPanel2.add(annotations, "grow, wrap");
     jPanel2.add(seqAssociated);
-    jPanel2.add(currentColours);
+    jPanel2.add(useOriginalColours);
     JPanel colpanel = new JPanel(new FlowLayout());
     colpanel.setBackground(Color.white);
     colpanel.add(minColour);
@@ -381,7 +324,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
 
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
-        sg.cs = oldgroupColours.get(sg);
+        sg.setColourScheme(oldgroupColours.get(sg));
       }
     }
   }
@@ -391,7 +334,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
   {
     if (slider.isEnabled())
     {
-      if (currentColours.isSelected()
+      if (useOriginalColours.isSelected()
               && !(av.getGlobalColourScheme() instanceof AnnotationColourGradient))
       {
         updateView();
@@ -403,24 +346,16 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     }
   }
 
-  public JComboBox<String> getThreshold()
+  public void originalColours_actionPerformed()
   {
-    return threshold;
-  }
-
-  public void setThreshold(JComboBox<String> threshold)
-  {
-    this.threshold = threshold;
-  }
-
-  public void currentColours_actionPerformed()
-  {
-    if (currentColours.isSelected())
+    boolean selected = useOriginalColours.isSelected();
+    if (selected)
     {
       reset();
     }
-    maxColour.setEnabled(!currentColours.isSelected());
-    minColour.setEnabled(!currentColours.isSelected());
+    maxColour.setEnabled(!selected);
+    minColour.setEnabled(!selected);
+    thresholdIsMin.setEnabled(!selected);
     updateView();
   }
 
@@ -441,7 +376,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
 
     slider.setEnabled(true);
     thresholdValue.setEnabled(true);
-    thresholdIsMin.setEnabled(true);
+    thresholdIsMin.setEnabled(!useOriginalColours.isSelected());
 
     if (selectedThresholdItem == AnnotationColourGradient.NO_THRESHOLD)
     {
@@ -455,7 +390,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     {
       getCurrentAnnotation()
               .setThreshold(
-                      new jalview.datamodel.GraphLine(
+                      new GraphLine(
                               (getCurrentAnnotation().graphMax - getCurrentAnnotation().graphMin) / 2f,
                               "Threshold", Color.black));
     }
@@ -463,19 +398,19 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     if (selectedThresholdItem != AnnotationColourGradient.NO_THRESHOLD)
     {
       adjusting = true;
-      float range = getCurrentAnnotation().graphMax * 1000
-              - getCurrentAnnotation().graphMin * 1000;
+      float range = getCurrentAnnotation().graphMax * ONETHOUSAND
+              - getCurrentAnnotation().graphMin * ONETHOUSAND;
 
-      slider.setMinimum((int) (getCurrentAnnotation().graphMin * 1000));
-      slider.setMaximum((int) (getCurrentAnnotation().graphMax * 1000));
-      slider.setValue((int) (getCurrentAnnotation().threshold.value * 1000));
+      slider.setMinimum((int) (getCurrentAnnotation().graphMin * ONETHOUSAND));
+      slider.setMaximum((int) (getCurrentAnnotation().graphMax * ONETHOUSAND));
+      slider.setValue((int) (getCurrentAnnotation().threshold.value * ONETHOUSAND));
       thresholdValue.setText(getCurrentAnnotation().threshold.value + "");
       slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
       adjusting = false;
     }
-    colorAlignmContaining(getCurrentAnnotation(), selectedThresholdItem);
+    colorAlignmentContaining(getCurrentAnnotation(), selectedThresholdItem);
 
     ap.alignmentChanged();
     // ensure all associated views (overviews, structures, etc) are notified of
@@ -483,4 +418,60 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     ap.paintAlignment(true);
   }
 
+  protected boolean colorAlignmentContaining(AlignmentAnnotation currentAnn, int selectedThresholdOption)
+  {
+  
+    AnnotationColourGradient acg = null;
+    if (useOriginalColours.isSelected())
+    {
+      acg = new AnnotationColourGradient(currentAnn,
+              av.getGlobalColourScheme(), selectedThresholdOption);
+    }
+    else
+    {
+      acg = new AnnotationColourGradient(currentAnn,
+              minColour.getBackground(), maxColour.getBackground(),
+              selectedThresholdOption);
+    }
+    acg.setSeqAssociated(seqAssociated.isSelected());
+  
+    if (currentAnn.graphMin == 0f && currentAnn.graphMax == 0f)
+    {
+      acg.setPredefinedColours(true);
+    }
+  
+    acg.setThresholdIsMinMax(thresholdIsMin.isSelected());
+  
+    av.setGlobalColourScheme(acg);
+  
+    if (av.getAlignment().getGroups() != null)
+    {
+  
+      for (SequenceGroup sg : ap.av.getAlignment().getGroups())
+      {
+        if (sg.cs == null)
+        {
+          continue;
+        }
+  
+        if (useOriginalColours.isSelected())
+        {
+          sg.setColourScheme(new AnnotationColourGradient(currentAnn, sg
+                  .getColourScheme(), selectedThresholdOption));
+          ((AnnotationColourGradient) sg.cs).setSeqAssociated(seqAssociated
+                  .isSelected());
+        }
+        else
+        {
+          sg.setColourScheme(new AnnotationColourGradient(currentAnn,
+                  minColour.getBackground(), maxColour.getBackground(),
+                  selectedThresholdOption));
+          ((AnnotationColourGradient) sg.cs).setSeqAssociated(seqAssociated
+                  .isSelected());
+        }
+      }
+    }
+    return false;
+  }
+
 }
index 1290d70..9c2a1b9 100644 (file)
@@ -21,8 +21,8 @@
 
 package jalview.gui;
 
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+import jalview.io.cache.JvCacheableInputBox;
 import jalview.schemes.AnnotationColourGradient;
 import jalview.util.MessageManager;
 import jalview.viewmodel.annotationfilter.AnnotationFilterParameter;
@@ -35,6 +35,7 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
 import java.util.Iterator;
 
 import javax.swing.ButtonGroup;
@@ -44,10 +45,7 @@ import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
-import javax.swing.JTextField;
 import javax.swing.border.TitledBorder;
-import javax.swing.event.DocumentEvent;
-import javax.swing.event.DocumentListener;
 
 import net.miginfocom.swing.MigLayout;
 
@@ -55,28 +53,10 @@ import net.miginfocom.swing.MigLayout;
 public class AnnotationColumnChooser extends AnnotationRowFilter implements
         ItemListener
 {
-
-  private JComboBox<String> annotations;
-
-  private JPanel actionPanel = new JPanel();
-
-  private JPanel thresholdPanel = new JPanel();
-
   private JPanel switchableViewsPanel = new JPanel(new CardLayout());
 
-  private CardLayout switchableViewsLayout = (CardLayout) (switchableViewsPanel
-          .getLayout());
-
-  private JPanel noGraphFilterView = new JPanel();
-
-  private JPanel graphFilterView = new JPanel();
-
   private JPanel annotationComboBoxPanel = new JPanel();
 
-  private BorderLayout borderLayout1 = new BorderLayout();
-
-  private JComboBox<String> threshold = new JComboBox<String>();
-
   private StructureFilterPanel gStructureFilterPanel;
 
   private StructureFilterPanel ngStructureFilterPanel;
@@ -105,18 +85,11 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
   private int actionOption = ACTION_OPTION_SELECT;
 
-  private ColumnSelection oldColumnSelection;
+  private HiddenColumns oldHiddenColumns;
 
-  public AnnotationColumnChooser()
-  {
-    try
-    {
-      jbInit();
-    } catch (Exception ex)
-    {
-      ex.printStackTrace();
-    }
-  }
+  protected int MIN_WIDTH = 420;
+
+  protected int MIN_HEIGHT = 430;
 
   public AnnotationColumnChooser(AlignViewport av, final AlignmentPanel ap)
   {
@@ -127,6 +100,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     Desktop.addInternalFrame(frame,
             MessageManager.getString("label.select_by_annotation"), 520,
             215);
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
     addSliderChangeListener();
     addSliderMouseListeners();
@@ -135,25 +109,28 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     {
       return;
     }
-    setOldColumnSelection(av.getColumnSelection());
+    setOldHiddenColumns(av.getAlignment().getHiddenColumns());
     adjusting = true;
 
-    setAnnotations(new JComboBox<String>(getAnnotationItems(false)));
+    setAnnotations(new JComboBox<>(getAnnotationItems(false)));
     populateThresholdComboBox(threshold);
-
+    AnnotationColumnChooser lastChooser = av
+            .getAnnotationColumnSelectionState();
     // restore Object state from the previous session if one exists
-    if (av.getAnnotationColumnSelectionState() != null)
+    if (lastChooser != null)
     {
-      currentSearchPanel = av.getAnnotationColumnSelectionState()
+      currentSearchPanel = lastChooser
               .getCurrentSearchPanel();
-      currentStructureFilterPanel = av.getAnnotationColumnSelectionState()
+      currentStructureFilterPanel = lastChooser
               .getCurrentStructureFilterPanel();
-      annotations.setSelectedIndex(av.getAnnotationColumnSelectionState()
+      annotations.setSelectedIndex(lastChooser
               .getAnnotations().getSelectedIndex());
-      threshold.setSelectedIndex(av.getAnnotationColumnSelectionState()
+      threshold.setSelectedIndex(lastChooser
               .getThreshold().getSelectedIndex());
-      actionOption = av.getAnnotationColumnSelectionState()
+      actionOption = lastChooser
               .getActionOption();
+      percentThreshold.setSelected(lastChooser.percentThreshold
+              .isSelected());
     }
 
     try
@@ -169,72 +146,30 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     frame.pack();
   }
 
-  private void jbInit() throws Exception
+  @Override
+  protected void jbInit()
   {
-    ok.setOpaque(false);
-    ok.setText(MessageManager.getString("action.ok"));
-    ok.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        ok_actionPerformed();
-      }
-    });
-
-    cancel.setOpaque(false);
-    cancel.setText(MessageManager.getString("action.cancel"));
-    cancel.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        cancel_actionPerformed();
-      }
-    });
-
-    annotations.addItemListener(this);
-    annotations.setToolTipText(MessageManager
-            .getString("info.select_annotation_row"));
-    threshold.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        threshold_actionPerformed();
-      }
-    });
-
-    thresholdValue.setEnabled(false);
-    thresholdValue.setColumns(7);
-    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));
+    super.jbInit();
 
+    JPanel thresholdPanel = new JPanel();
     thresholdPanel.setBorder(new TitledBorder(MessageManager
             .getString("label.threshold_filter")));
     thresholdPanel.setBackground(Color.white);
     thresholdPanel.setFont(JvSwingUtils.getLabelFont());
     thresholdPanel.setLayout(new MigLayout("", "[left][right]", "[][]"));
 
+    percentThreshold.setBackground(Color.white);
+    percentThreshold.setFont(JvSwingUtils.getLabelFont());
+
+    JPanel actionPanel = new JPanel();
     actionPanel.setBackground(Color.white);
     actionPanel.setFont(JvSwingUtils.getLabelFont());
 
+    JPanel graphFilterView = new JPanel();
     graphFilterView.setLayout(new MigLayout("", "[left][right]", "[][]"));
     graphFilterView.setBackground(Color.white);
 
+    JPanel noGraphFilterView = new JPanel();
     noGraphFilterView.setLayout(new MigLayout("", "[left][right]", "[][]"));
     noGraphFilterView.setBackground(Color.white);
 
@@ -249,8 +184,9 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     ngStructureFilterPanel = new StructureFilterPanel(this);
 
     thresholdPanel.add(getThreshold());
-    thresholdPanel.add(thresholdValue, "wrap");
-    thresholdPanel.add(slider, "grow, span, wrap");
+    thresholdPanel.add(percentThreshold, "wrap");
+    thresholdPanel.add(slider, "grow");
+    thresholdPanel.add(thresholdValue, "span, wrap");
 
     actionPanel.add(ok);
     actionPanel.add(cancel);
@@ -270,7 +206,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     switchableViewsPanel.add(graphFilterView,
             AnnotationColumnChooser.GRAPH_VIEW);
 
-    this.setLayout(borderLayout1);
+    this.setLayout(new BorderLayout());
     this.add(annotationComboBoxPanel, java.awt.BorderLayout.PAGE_START);
     this.add(switchableViewsPanel, java.awt.BorderLayout.CENTER);
     this.add(actionPanel, java.awt.BorderLayout.SOUTH);
@@ -280,7 +216,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     this.validate();
   }
 
-  public void updateThresholdPanelToolTip()
+  protected void updateThresholdPanelToolTip()
   {
     thresholdValue.setToolTipText("");
     slider.setToolTipText("");
@@ -297,28 +233,28 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
   }
 
   @Override
-  public void reset()
+  protected void reset()
   {
-    if (this.getOldColumnSelection() != null)
+    if (this.getOldHiddenColumns() != null)
     {
       av.getColumnSelection().clear();
 
       if (av.getAnnotationColumnSelectionState() != null)
       {
-        ColumnSelection oldSelection = av
+        HiddenColumns oldHidden = av
                 .getAnnotationColumnSelectionState()
-                .getOldColumnSelection();
-        if (oldSelection != null && oldSelection.getHiddenColumns() != null
-                && !oldSelection.getHiddenColumns().isEmpty())
+                .getOldHiddenColumns();
+        if (oldHidden != null && oldHidden.getHiddenRegions() != null
+                && !oldHidden.getHiddenRegions().isEmpty())
         {
-          for (Iterator<int[]> itr = oldSelection.getHiddenColumns()
+          for (Iterator<int[]> itr = oldHidden.getHiddenRegions()
                   .iterator(); itr.hasNext();)
           {
             int positions[] = itr.next();
             av.hideColumns(positions[0], positions[1]);
           }
         }
-        av.setColumnSelection(oldSelection);
+        av.getAlignment().setHiddenColumns(oldHidden);
       }
       ap.paintAlignment(true);
     }
@@ -338,26 +274,6 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     }
   }
 
-  public JComboBox<String> getThreshold()
-  {
-    return threshold;
-  }
-
-  public void setThreshold(JComboBox<String> threshold)
-  {
-    this.threshold = threshold;
-  }
-
-  public JComboBox<String> getAnnotations()
-  {
-    return annotations;
-  }
-
-  public void setAnnotations(JComboBox<String> annotations)
-  {
-    this.annotations = annotations;
-  }
-
   @Override
   public void updateView()
   {
@@ -377,12 +293,14 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
     slider.setEnabled(true);
     thresholdValue.setEnabled(true);
+    percentThreshold.setEnabled(true);
 
     if (selectedThresholdItem == AnnotationColourGradient.NO_THRESHOLD)
     {
       slider.setEnabled(false);
       thresholdValue.setEnabled(false);
       thresholdValue.setText("");
+      percentThreshold.setEnabled(false);
       // build filter params
     }
     else if (selectedThresholdItem != AnnotationColourGradient.NO_THRESHOLD)
@@ -403,7 +321,9 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       slider.setMinimum((int) (getCurrentAnnotation().graphMin * 1000));
       slider.setMaximum((int) (getCurrentAnnotation().graphMax * 1000));
       slider.setValue((int) (getCurrentAnnotation().threshold.value * 1000));
-      thresholdValue.setText(getCurrentAnnotation().threshold.value + "");
+      
+      setThresholdValueText();
+
       slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
@@ -412,7 +332,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       // build filter params
       filterParams
               .setThresholdType(AnnotationFilterParameter.ThresholdType.NO_THRESHOLD);
-      if (getCurrentAnnotation().graph != AlignmentAnnotation.NO_GRAPH)
+      if (getCurrentAnnotation().isQuantitative())
       {
         filterParams
                 .setThresholdValue(getCurrentAnnotation().threshold.value);
@@ -449,12 +369,10 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
     if (currentSearchPanel != null)
     {
-
-      if (!currentSearchPanel.searchBox.getText().isEmpty())
+      if (!currentSearchPanel.searchBox.getUserInput().isEmpty())
       {
-        currentSearchPanel.description.setEnabled(true);
-        currentSearchPanel.displayName.setEnabled(true);
-        filterParams.setRegexString(currentSearchPanel.searchBox.getText());
+        filterParams.setRegexString(currentSearchPanel.searchBox
+                .getUserInput());
         if (currentSearchPanel.displayName.isSelected())
         {
           filterParams
@@ -466,17 +384,15 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
                   .addRegexSearchField(AnnotationFilterParameter.SearchableAnnotationField.DESCRIPTION);
         }
       }
-      else
-      {
-        currentSearchPanel.description.setEnabled(false);
-        currentSearchPanel.displayName.setEnabled(false);
-      }
     }
 
+    // show hidden columns here, before changing the column selection in
+    // filterAnnotations, because showing hidden columns has the side effect of
+    // adding them to the selection
+    av.showAllHiddenColumns();
     av.getColumnSelection().filterAnnotations(
             getCurrentAnnotation().annotations, filterParams);
 
-    av.showAllHiddenColumns();
     if (getActionOption() == ACTION_OPTION_HIDE)
     {
       av.hideSelectedColumns();
@@ -487,16 +403,16 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     ap.paintAlignment(true);
   }
 
-  public ColumnSelection getOldColumnSelection()
+  public HiddenColumns getOldHiddenColumns()
   {
-    return oldColumnSelection;
+    return oldHiddenColumns;
   }
 
-  public void setOldColumnSelection(ColumnSelection currentColumnSelection)
+  public void setOldHiddenColumns(HiddenColumns currentHiddenColumns)
   {
-    if (currentColumnSelection != null)
+    if (currentHiddenColumns != null)
     {
-      this.oldColumnSelection = new ColumnSelection(currentColumnSelection);
+      this.oldHiddenColumns = new HiddenColumns(currentHiddenColumns);
     }
   }
 
@@ -568,15 +484,17 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     selectedAnnotationChanged();
   }
 
+  @Override
   public void selectedAnnotationChanged()
   {
     String currentView = AnnotationColumnChooser.NO_GRAPH_VIEW;
-    if (av.getAlignment().getAlignmentAnnotation()[annmap[getAnnotations()
-            .getSelectedIndex()]].graph != AlignmentAnnotation.NO_GRAPH)
+    if (av.getAlignment()
+            .getAlignmentAnnotation()[annmap[getAnnotations()
+            .getSelectedIndex()]].isQuantitative())
     {
       currentView = AnnotationColumnChooser.GRAPH_VIEW;
     }
-
+    saveCache();
     gSearchPanel.syncState();
     gFurtherActionPanel.syncState();
     gStructureFilterPanel.syncState();
@@ -585,6 +503,8 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     ngFurtherActionPanel.syncState();
     ngStructureFilterPanel.syncState();
 
+    CardLayout switchableViewsLayout = (CardLayout) switchableViewsPanel
+            .getLayout();
     switchableViewsLayout.show(switchableViewsPanel, currentView);
     updateView();
   }
@@ -805,7 +725,10 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
 
     private JCheckBox description = new JCheckBox();
 
-    private JTextField searchBox = new JTextField(10);
+    private static final String FILTER_BY_ANN_CACHE_KEY = "CACHE.SELECT_FILTER_BY_ANNOT";
+
+    public JvCacheableInputBox<String> searchBox = new JvCacheableInputBox<String>(
+            FILTER_BY_ANN_CACHE_KEY);
 
     public SearchPanel(AnnotationColumnChooser aColChooser)
     {
@@ -815,32 +738,26 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       this.setBorder(new TitledBorder(MessageManager
               .getString("label.search_filter")));
 
-      JvSwingUtils.jvInitComponent(searchBox);
+      searchBox.setPrototypeDisplayValue("XXXXXXXXXXXXXXXXXXXXXXX");
       searchBox.setToolTipText(MessageManager
               .getString("info.enter_search_text_here"));
-      searchBox.getDocument().addDocumentListener(new DocumentListener()
-      {
-        @Override
-        public void insertUpdate(DocumentEvent e)
-        {
-          searchStringAction();
-        }
+      searchBox.getEditor().getEditorComponent()
+              .addKeyListener(new java.awt.event.KeyAdapter()
+              {
+                @Override
+                public void keyPressed(KeyEvent e)
+                {
+                  if (e.getKeyCode() == KeyEvent.VK_ENTER)
+                  {
+                    e.consume();
+                    searchStringAction();
+                  }
+                }
+              });
 
-        @Override
-        public void removeUpdate(DocumentEvent e)
-        {
-          searchStringAction();
-        }
 
-        @Override
-        public void changedUpdate(DocumentEvent e)
-        {
-          searchStringAction();
-        }
-      });
 
       JvSwingUtils.jvInitComponent(displayName, "label.label");
-      displayName.setEnabled(false);
       displayName.addActionListener(new ActionListener()
       {
         @Override
@@ -851,7 +768,6 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       });
 
       JvSwingUtils.jvInitComponent(description, "label.description");
-      description.setEnabled(false);
       description.addActionListener(new ActionListener()
       {
         @Override
@@ -884,6 +800,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       aColChooser.setCurrentSearchPanel(this);
       aColChooser.updateView();
       updateSearchPanelToolTips();
+      searchBox.updateCache();
     }
 
     public void syncState()
@@ -897,7 +814,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
         displayName.setEnabled(sp.displayName.isEnabled());
         displayName.setSelected(sp.displayName.isSelected());
 
-        searchBox.setText(sp.searchBox.getText());
+        searchBox.setSelectedItem(sp.searchBox.getUserInput());
       }
       updateSearchPanelToolTips();
     }
@@ -919,4 +836,25 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     }
   }
 
+  @Override
+  public void ok_actionPerformed()
+  {
+    saveCache();
+    super.ok_actionPerformed();
+  }
+
+  @Override
+  public void cancel_actionPerformed()
+  {
+    saveCache();
+    super.cancel_actionPerformed();
+  }
+
+  private void saveCache()
+  {
+    gSearchPanel.searchBox.persistCache();
+    ngSearchPanel.searchBox.persistCache();
+    gSearchPanel.searchBox.updateCache();
+    ngSearchPanel.searchBox.updateCache();
+  }
 }
index 0d47e36..42913de 100644 (file)
@@ -34,6 +34,7 @@ import java.awt.Color;
 import java.awt.FlowLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.util.List;
 import java.util.Map;
 
 import javax.swing.BorderFactory;
@@ -156,28 +157,22 @@ public class AnnotationExporter extends JPanel
             .getString("label.no_features_on_alignment");
     if (features)
     {
-      Map<String, FeatureColourI> displayedFeatureColours = ap
-              .getFeatureRenderer().getDisplayedFeatureCols();
       FeaturesFile formatter = new FeaturesFile();
       SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
       Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
               .getDisplayedFeatureCols();
+      List<String> featureGroups = ap.getFeatureRenderer()
+              .getDisplayedFeatureGroups();
       boolean includeNonPositional = ap.av.isShowNPFeats();
       if (GFFFormat.isSelected())
       {
-        text = new FeaturesFile().printGffFormat(ap.av.getAlignment()
-                .getDataset().getSequencesArray(), displayedFeatureColours,
-                true, ap.av.isShowNPFeats());
-        text = formatter.printGffFormat(sequences, featureColours, true,
-                includeNonPositional);
+        text = formatter.printGffFormat(sequences, featureColours,
+                featureGroups, 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);
+                featureGroups, includeNonPositional);
       }
     }
     else
index 4b774d6..8ca1a4e 100755 (executable)
@@ -292,33 +292,11 @@ public class AnnotationLabels extends JPanel implements MouseListener,
       aa[selectedRow].scaleColLabel = !aa[selectedRow].scaleColLabel;
     }
 
-    refresh(fullRepaint);
+    ap.refresh(fullRepaint);
 
   }
 
   /**
-   * Redraw sensibly.
-   * 
-   * @adjustHeight if true, try to recalculate panel height for visible
-   *               annotations
-   */
-  protected void refresh(boolean adjustHeight)
-  {
-    ap.validateAnnotationDimensions(adjustHeight);
-    ap.addNotify();
-    if (adjustHeight)
-    {
-      // sort, repaint, update overview
-      ap.paintAlignment(true);
-    }
-    else
-    {
-      // lightweight repaint
-      ap.repaint();
-    }
-  }
-
-  /**
    * DOCUMENT ME!
    * 
    * @param e
@@ -420,7 +398,7 @@ public class AnnotationLabels extends JPanel implements MouseListener,
             // ann.visible = false;
             // }
             // }
-            refresh(true);
+            ap.refresh(true);
           }
         });
         pop.add(hideType);
@@ -969,12 +947,14 @@ public class AnnotationLabels extends JPanel implements MouseListener,
     Alignment ds = new Alignment(dseqs);
     if (av.hasHiddenColumns())
     {
-      omitHidden = av.getColumnSelection().getVisibleSequenceStrings(0,
+      omitHidden = av.getAlignment().getHiddenColumns()
+              .getVisibleSequenceStrings(0,
               sq.getLength(), seqs);
     }
 
     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
-    List<int[]> hiddenCols = av.getColumnSelection().getHiddenColumns();
+    List<int[]> hiddenCols = av.getAlignment().getHiddenColumns()
+            .getHiddenRegions();
     if (hiddenCols != null)
     {
       alignmentStartEnd = av.getAlignment().getVisibleStartAndEndIndex(
@@ -990,7 +970,8 @@ public class AnnotationLabels extends JPanel implements MouseListener,
     if (av.hasHiddenColumns())
     {
       hiddenColumns = new ArrayList<int[]>();
-      for (int[] region : av.getColumnSelection().getHiddenColumns())
+      for (int[] region : av.getAlignment().getHiddenColumns()
+              .getHiddenRegions())
       {
         hiddenColumns.add(new int[] { region[0], region[1] });
       }
index b1f0edb..452f002 100755 (executable)
@@ -23,12 +23,14 @@ package jalview.gui;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.AnnotationRenderer;
 import jalview.renderer.AwtRenderPanelI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
 import jalview.util.MessageManager;
+import jalview.viewmodel.ViewportListenerI;
 
 import java.awt.AlphaComposite;
 import java.awt.Color;
@@ -49,13 +51,13 @@ import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
 import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeEvent;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 import javax.swing.JColorChooser;
 import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.Scrollable;
@@ -70,7 +72,7 @@ import javax.swing.ToolTipManager;
  */
 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
         MouseListener, MouseWheelListener, MouseMotionListener,
-        ActionListener, AdjustmentListener, Scrollable
+        ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
 {
   String HELIX = MessageManager.getString("label.helix");
 
@@ -156,6 +158,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     // and then set our own listener to consume all mousewheel events
     ap.annotationScroller.addMouseWheelListener(this);
     renderer = new AnnotationRenderer();
+
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   public AnnotationPanel(AlignViewport av)
@@ -172,11 +176,11 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       e.consume();
       if (e.getWheelRotation() > 0)
       {
-        ap.scrollRight(true);
+        av.getRanges().scrollRight(true);
       }
       else
       {
-        ap.scrollRight(false);
+        av.getRanges().scrollRight(false);
       }
     }
     else
@@ -291,7 +295,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     {
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (av.getColumnSelection().isVisible(index))
+        if (av.getAlignment().getHiddenColumns().isVisible(index))
         {
           anot[index] = null;
         }
@@ -315,7 +319,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
 
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (!av.getColumnSelection().isVisible(index))
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
         {
           continue;
         }
@@ -338,7 +342,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
 
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (!av.getColumnSelection().isVisible(index))
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
         {
           continue;
         }
@@ -398,7 +402,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       }
       for (int index : av.getColumnSelection().getSelected())
       {
-        if (!av.getColumnSelection().isVisible(index))
+        if (!av.getAlignment().getHiddenColumns().isVisible(index))
         {
           continue;
         }
@@ -441,6 +445,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     StringBuilder collatedInput = new StringBuilder(64);
     String last = "";
     ColumnSelection viscols = av.getColumnSelection();
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
 
     /*
      * the selection list (read-only view) is in selection order, not
@@ -451,7 +456,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     for (int index : selected)
     {
       // always check for current display state - just in case
-      if (!viscols.isVisible(index))
+      if (!hidden.isVisible(index))
       {
         continue;
       }
@@ -708,11 +713,13 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       return;
     }
 
-    int column = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int column = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
 
     if (av.hasHiddenColumns())
     {
-      column = av.getColumnSelection().adjustForHiddenColumns(column);
+      column = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(column);
     }
 
     AlignmentAnnotation ann = aa[row];
@@ -905,7 +912,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
         return;
       }
     }
-    imgWidth = (av.endRes - av.startRes + 1) * av.getCharWidth();
+    imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes() + 1)
+            * av.getCharWidth();
     if (imgWidth < 1)
     {
       return;
@@ -946,7 +954,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       imageFresh = true;
     }
 
-    drawComponent(gg, av.startRes, av.endRes + 1);
+    drawComponent(gg, av.getRanges().getStartRes(), av.getRanges()
+            .getEndRes() + 1);
     imageFresh = false;
     g.drawImage(image, 0, 0, this);
   }
@@ -976,8 +985,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     gg.copyArea(0, 0, imgWidth, getHeight(),
             -horizontal * av.getCharWidth(), 0);
     long mtime = System.currentTimeMillis();
-    int sr = av.startRes;
-    int er = av.endRes + 1;
+    int sr = av.getRanges().getStartRes();
+    int er = av.getRanges().getEndRes() + 1;
     int transX = 0;
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
@@ -1154,4 +1163,15 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       renderer.dispose();
     }
   }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport range changes (e.g. alignment panel was scrolled)
+    if (evt.getPropertyName().equals("startres")
+            || evt.getPropertyName().equals("endres"))
+    {
+      fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
+    }
+  }
 }
index c8bd69c..a3ce528 100644 (file)
@@ -22,12 +22,21 @@ package jalview.gui;
 
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.GraphLine;
-import jalview.datamodel.SequenceGroup;
 import jalview.schemes.AnnotationColourGradient;
 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.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.util.HashMap;
+import java.util.Map;
 import java.util.Vector;
 
 import javax.swing.JButton;
@@ -49,21 +58,11 @@ public abstract class AnnotationRowFilter extends JPanel
 
   protected int[] annmap;
 
-  protected boolean enableSeqAss = false;
-
-  private AlignmentAnnotation currentAnnotation;
-
   protected boolean adjusting = false;
 
-  protected JCheckBox currentColours = new JCheckBox();
-
-  protected JPanel minColour = new JPanel();
-
-  protected JPanel maxColour = new JPanel();
-
   protected JCheckBox seqAssociated = new JCheckBox();
 
-  protected JCheckBox thresholdIsMin = new JCheckBox();
+  protected JCheckBox percentThreshold = new JCheckBox();
 
   protected JSlider slider = new JSlider();
 
@@ -81,6 +80,38 @@ public abstract class AnnotationRowFilter extends JPanel
    */
   protected boolean sliderDragging = false;
 
+  protected JComboBox<String> threshold = new JComboBox<String>();
+
+  protected JComboBox<String> annotations;
+
+  /*
+   * map from annotation to its menu item display label
+   * - so we know which item to pre-select on restore
+   */
+  private Map<AlignmentAnnotation, String> annotationLabels;
+
+  private AlignmentAnnotation currentAnnotation;
+
+  /**
+   * Constructor
+   * 
+   * @param viewport
+   * @param alignPanel
+   */
+  public AnnotationRowFilter(AlignViewport viewport, final AlignmentPanel alignPanel)
+  {
+    this.av = viewport;
+    this.ap = alignPanel;
+    thresholdValue.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
+  }
+
   protected void addSliderChangeListener()
   {
 
@@ -91,13 +122,32 @@ public abstract class AnnotationRowFilter extends JPanel
       {
         if (!adjusting)
         {
-          thresholdValue.setText((slider.getValue() / 1000f) + "");
+          setThresholdValueText();
           valueChanged(!sliderDragging);
         }
       }
     });
   }
 
+  /**
+   * update the text field from the threshold slider. preserves state of
+   * 'adjusting' so safe to call in init.
+   */
+  protected void setThresholdValueText()
+  {
+    boolean oldadj = adjusting;
+    adjusting = true;
+    if (percentThreshold.isSelected())
+    {
+      thresholdValue.setText("" + (slider.getValue() - slider.getMinimum())
+              * 100f / (slider.getMaximum() - slider.getMinimum()));
+    }
+    else
+    {
+      thresholdValue.setText((slider.getValue() / 1000f) + "");
+    }
+    adjusting = oldadj;
+  }
   protected void addSliderMouseListeners()
   {
 
@@ -130,25 +180,27 @@ public abstract class AnnotationRowFilter extends JPanel
     });
   }
 
-  public AnnotationRowFilter(AlignViewport av, final AlignmentPanel ap)
-  {
-    this.av = av;
-    this.ap = ap;
-  }
-
-  public AnnotationRowFilter()
-  {
-
-  }
-
+/**
+ * Builds and returns a list of menu items (display text) for choice of
+ * annotation. Also builds maps between annotations, their positions in the
+ * list, and their display labels in the list.
+ * 
+ * @param isSeqAssociated
+ * @return
+ */
   public Vector<String> getAnnotationItems(boolean isSeqAssociated)
   {
+    annotationLabels = new HashMap<AlignmentAnnotation, String>();
+
     Vector<String> list = new Vector<String>();
     int index = 1;
     int[] anmap = new int[av.getAlignment().getAlignmentAnnotation().length];
+    seqAssociated.setEnabled(false);
     for (int i = 0; i < av.getAlignment().getAlignmentAnnotation().length; i++)
     {
-      if (av.getAlignment().getAlignmentAnnotation()[i].sequenceRef == null)
+      AlignmentAnnotation annotation = av.getAlignment()
+              .getAlignmentAnnotation()[i];
+      if (annotation.sequenceRef == null)
       {
         if (isSeqAssociated)
         {
@@ -157,30 +209,29 @@ public abstract class AnnotationRowFilter extends JPanel
       }
       else
       {
-        enableSeqAss = true;
+        seqAssociated.setEnabled(true);
       }
-      String label = av.getAlignment().getAlignmentAnnotation()[i].label;
+      String label = annotation.label;
       // add associated sequence ID if available
-      if (!isSeqAssociated
-              && av.getAlignment().getAlignmentAnnotation()[i].sequenceRef != null)
+      if (!isSeqAssociated && annotation.sequenceRef != null)
       {
-        label = label
-                + "_"
-                + av.getAlignment().getAlignmentAnnotation()[i].sequenceRef
-                        .getName();
+        label = label + "_" + annotation.sequenceRef.getName();
       }
       // make label unique
       if (!list.contains(label))
       {
         anmap[list.size()] = i;
         list.add(label);
+        annotationLabels.put(annotation, label);
       }
       else
       {
         if (!isSeqAssociated)
         {
           anmap[list.size()] = i;
-          list.add(label + "_" + (index++));
+          label = label + "_" + (index++);
+          list.add(label);
+          annotationLabels.put(annotation, label);
         }
       }
     }
@@ -203,11 +254,6 @@ public abstract class AnnotationRowFilter extends JPanel
     return selectedThresholdItem;
   }
 
-  public void modelChanged()
-  {
-    seqAssociated.setEnabled(enableSeqAss);
-  }
-
   public void ok_actionPerformed()
   {
     try
@@ -230,54 +276,75 @@ public abstract class AnnotationRowFilter extends JPanel
     }
   }
 
-  public void thresholdCheck_actionPerformed()
+  protected void thresholdCheck_actionPerformed()
   {
     updateView();
   }
 
-  public void annotations_actionPerformed()
+  protected void selectedAnnotationChanged()
   {
     updateView();
   }
 
-  public void threshold_actionPerformed()
+  protected void threshold_actionPerformed()
   {
     updateView();
   }
 
-  public void thresholdValue_actionPerformed()
+  protected void thresholdValue_actionPerformed()
   {
     try
     {
       float f = Float.parseFloat(thresholdValue.getText());
-      slider.setValue((int) (f * 1000));
+      if (percentThreshold.isSelected())
+      {
+        slider.setValue(slider.getMinimum()
+                + ((int) ((f / 100f) * (slider.getMaximum() - slider
+                        .getMinimum()))));
+      }
+      else
+      {
+        slider.setValue((int) (f * 1000));
+      }
       updateView();
     } catch (NumberFormatException ex)
     {
     }
   }
 
-  public void thresholdIsMin_actionPerformed()
+  protected void percentageValue_actionPerformed()
+  {
+    setThresholdValueText();
+  }
+
+  protected void thresholdIsMin_actionPerformed()
   {
     updateView();
   }
 
-  protected void populateThresholdComboBox(JComboBox<String> threshold)
+  protected void populateThresholdComboBox(JComboBox<String> thresh)
   {
-    threshold.addItem(MessageManager
+    thresh.addItem(MessageManager
             .getString("label.threshold_feature_no_threshold"));
-    threshold.addItem(MessageManager
+    thresh.addItem(MessageManager
             .getString("label.threshold_feature_above_threshold"));
-    threshold.addItem(MessageManager
+    thresh.addItem(MessageManager
             .getString("label.threshold_feature_below_threshold"));
   }
 
-  protected void seqAssociated_actionPerformed(JComboBox<String> annotations)
+  /**
+   * Rebuilds the drop-down list of annotations to choose from when the 'per
+   * sequence only' checkbox is checked or unchecked.
+   * 
+   * @param anns
+   */
+  protected void seqAssociated_actionPerformed(JComboBox<String> anns)
   {
     adjusting = true;
-    String cursel = (String) annotations.getSelectedItem();
-    boolean isvalid = false, isseqs = seqAssociated.isSelected();
-    annotations.removeAllItems();
+    String cursel = (String) anns.getSelectedItem();
+    boolean isvalid = false;
+    boolean isseqs = seqAssociated.isSelected();
+    anns.removeAllItems();
     for (String anitem : getAnnotationItems(seqAssociated.isSelected()))
     {
       if (anitem.equals(cursel) || (isseqs && cursel.startsWith(anitem)))
@@ -285,20 +352,22 @@ public abstract class AnnotationRowFilter extends JPanel
         isvalid = true;
         cursel = anitem;
       }
-      annotations.addItem(anitem);
+      anns.addItem(anitem);
     }
-    adjusting = false;
     if (isvalid)
     {
-      annotations.setSelectedItem(cursel);
+      anns.setSelectedItem(cursel);
     }
     else
     {
-      if (annotations.getItemCount() > 0)
+      if (anns.getItemCount() > 0)
       {
-        annotations.setSelectedIndex(0);
+        anns.setSelectedIndex(0);
       }
     }
+    adjusting = false;
+
+    updateView();
   }
 
   protected void propagateSeqAssociatedThreshold(boolean allAnnotation,
@@ -329,79 +398,119 @@ public abstract class AnnotationRowFilter extends JPanel
     }
   }
 
-  protected boolean colorAlignmContaining(AlignmentAnnotation currentAnn,
-          int selectedThresholdOption)
+  public AlignmentAnnotation getCurrentAnnotation()
   {
+    return currentAnnotation;
+  }
 
-    AnnotationColourGradient acg = null;
-    if (currentColours.isSelected())
-    {
-      acg = new AnnotationColourGradient(currentAnn,
-              av.getGlobalColourScheme(), selectedThresholdOption);
-    }
-    else
-    {
-      acg = new AnnotationColourGradient(currentAnn,
-              minColour.getBackground(), maxColour.getBackground(),
-              selectedThresholdOption);
-    }
-    acg.setSeqAssociated(seqAssociated.isSelected());
+  protected void setCurrentAnnotation(AlignmentAnnotation annotation)
+  {
+    this.currentAnnotation = annotation;
+  }
 
-    if (currentAnn.graphMin == 0f && currentAnn.graphMax == 0f)
-    {
-      acg.setPredefinedColours(true);
-    }
+  protected abstract void valueChanged(boolean updateAllAnnotation);
 
-    acg.thresholdIsMinMax = thresholdIsMin.isSelected();
+  protected abstract void updateView();
 
-    av.setGlobalColourScheme(acg);
+  protected abstract void reset();
 
-    if (av.getAlignment().getGroups() != null)
+  protected String getAnnotationMenuLabel(AlignmentAnnotation ann)
+  {
+    return annotationLabels.get(ann);
+  }
+
+  protected void jbInit()
+  {
+    ok.setOpaque(false);
+    ok.setText(MessageManager.getString("action.ok"));
+    ok.addActionListener(new ActionListener()
     {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        ok_actionPerformed();
+      }
+    });
 
-      for (SequenceGroup sg : ap.av.getAlignment().getGroups())
+    cancel.setOpaque(false);
+    cancel.setText(MessageManager.getString("action.cancel"));
+    cancel.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
       {
-        if (sg.cs == null)
-        {
-          continue;
-        }
+        cancel_actionPerformed();
+      }
+    });
 
-        if (currentColours.isSelected())
-        {
-          sg.cs = new AnnotationColourGradient(currentAnn, sg.cs,
-                  selectedThresholdOption);
-          ((AnnotationColourGradient) sg.cs).setSeqAssociated(seqAssociated
-                  .isSelected());
+    annotations.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        selectedAnnotationChanged();
+      }
+    });
+    annotations.setToolTipText(MessageManager
+            .getString("info.select_annotation_row"));
 
-        }
-        else
+    threshold.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        threshold_actionPerformed();
+      }
+    });
+
+    thresholdValue.setEnabled(false);
+    thresholdValue.setColumns(7);
+    thresholdValue.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
+
+    percentThreshold.setText(MessageManager.getString("label.as_percentage"));
+    percentThreshold.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (!adjusting)
         {
-          sg.cs = new AnnotationColourGradient(currentAnn,
-                  minColour.getBackground(), maxColour.getBackground(),
-                  selectedThresholdOption);
-          ((AnnotationColourGradient) sg.cs).setSeqAssociated(seqAssociated
-                  .isSelected());
+          percentageValue_actionPerformed();
         }
-
       }
-    }
-    return false;
+    });
+    slider.setPaintLabels(false);
+    slider.setPaintTicks(true);
+    slider.setBackground(Color.white);
+    slider.setEnabled(false);
+    slider.setOpaque(false);
+    slider.setPreferredSize(new Dimension(100, 32));
   }
 
-  public jalview.datamodel.AlignmentAnnotation getCurrentAnnotation()
+  public JComboBox<String> getThreshold()
   {
-    return currentAnnotation;
+    return threshold;
   }
 
-  public void setCurrentAnnotation(
-          jalview.datamodel.AlignmentAnnotation currentAnnotation)
+  public void setThreshold(JComboBox<String> thresh)
   {
-    this.currentAnnotation = currentAnnotation;
+    this.threshold = thresh;
   }
 
-  public abstract void valueChanged(boolean updateAllAnnotation);
-
-  public abstract void updateView();
+  public JComboBox<String> getAnnotations()
+  {
+    return annotations;
+  }
 
-  public abstract void reset();
+  public void setAnnotations(JComboBox<String> anns)
+  {
+    this.annotations = anns;
+  }
 }
index a30f6d3..68a847e 100644 (file)
 package jalview.gui;
 
 import jalview.bin.Cache;
-import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.StructureViewer.ViewerType;
-import jalview.io.JalviewFileChooser;
-import jalview.io.JalviewFileView;
-import jalview.schemes.BuriedColourScheme;
-import jalview.schemes.ColourSchemeI;
-import jalview.schemes.HelixColourScheme;
-import jalview.schemes.HydrophobicColourScheme;
-import jalview.schemes.PurinePyrimidineColourScheme;
-import jalview.schemes.StrandColourScheme;
-import jalview.schemes.TaylorColourScheme;
-import jalview.schemes.TurnColourScheme;
-import jalview.schemes.ZappoColourScheme;
 import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.BrowserLauncher;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.ws.dbsources.Pdb;
@@ -50,28 +38,18 @@ import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Vector;
 
 import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JColorChooser;
-import javax.swing.JMenu;
+import javax.swing.JInternalFrame;
 import javax.swing.JPanel;
 import javax.swing.JSplitPane;
 import javax.swing.SwingUtilities;
 import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
-import javax.swing.event.MenuEvent;
-import javax.swing.event.MenuListener;
 
 public class AppJmol extends StructureViewerBase
 {
@@ -90,8 +68,6 @@ public class AppJmol extends StructureViewerBase
 
   RenderPanel renderPanel;
 
-  ViewSelectionMenu seqColourBy;
-
   /**
    * 
    * @param files
@@ -135,6 +111,7 @@ public class AppJmol extends StructureViewerBase
     {
       useAlignmentPanelForSuperposition(ap);
     }
+    initMenus();
     if (leaveColouringToJmol || !usetoColour)
     {
       jmb.setColourBySequence(false);
@@ -149,7 +126,6 @@ public class AppJmol extends StructureViewerBase
       viewerColour.setSelected(false);
     }
     this.setBounds(bounds);
-    initMenus();
     setViewId(viewid);
     // jalview.gui.Desktop.addInternalFrame(this, "Loading File",
     // bounds.width,bounds.height);
@@ -163,84 +139,19 @@ public class AppJmol extends StructureViewerBase
       }
     });
     initJmol(loadStatus); // pdbentry, seq, JBPCHECK!
-
   }
 
-  private void initMenus()
+  @Override
+  protected void initMenus()
   {
-    seqColour.setSelected(jmb.isColourBySequence());
-    viewerColour.setSelected(!jmb.isColourBySequence());
-    if (_colourwith == null)
-    {
-      _colourwith = new Vector<AlignmentPanel>();
-    }
-    if (_alignwith == null)
-    {
-      _alignwith = new Vector<AlignmentPanel>();
-    }
-
-    seqColourBy = new ViewSelectionMenu(
-            MessageManager.getString("label.colour_by"), this, _colourwith,
-            new ItemListener()
-            {
+    super.initMenus();
 
-              @Override
-              public void itemStateChanged(ItemEvent e)
-              {
-                if (!seqColour.isSelected())
-                {
-                  seqColour.doClick();
-                }
-                else
-                {
-                  // update the jmol display now.
-                  seqColour_actionPerformed(null);
-                }
-              }
-            });
-    viewMenu.add(seqColourBy);
-    final ItemListener handler;
-    JMenu alpanels = new ViewSelectionMenu(
-            MessageManager.getString("label.superpose_with"), this,
-            _alignwith, handler = new ItemListener()
-            {
-
-              @Override
-              public void itemStateChanged(ItemEvent e)
-              {
-                alignStructs.setEnabled(_alignwith.size() > 0);
-                alignStructs.setToolTipText(MessageManager
-                        .formatMessage(
-                                "label.align_structures_using_linked_alignment_views",
-                                new String[] { new Integer(_alignwith
-                                        .size()).toString() }));
-              }
-            });
-    handler.itemStateChanged(null);
-    viewerActionMenu.add(alpanels);
-    viewerActionMenu.addMenuListener(new MenuListener()
-    {
-
-      @Override
-      public void menuSelected(MenuEvent e)
-      {
-        handler.itemStateChanged(null);
-      }
-
-      @Override
-      public void menuDeselected(MenuEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
+    viewerActionMenu.setText(MessageManager.getString("label.jmol"));
 
-      @Override
-      public void menuCanceled(MenuEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-    });
+    viewerColour
+            .setText(MessageManager.getString("label.colour_with_jmol"));
+    viewerColour.setToolTipText(MessageManager
+            .getString("label.let_jmol_manage_structure_colours"));
   }
 
   IProgressIndicator progressBar = null;
@@ -283,15 +194,6 @@ public class AppJmol extends StructureViewerBase
     openNewJmol(ap, new PDBEntry[] { pdbentry }, new SequenceI[][] { seq });
   }
 
-  /**
-   * Answers true if this viewer already involves the given PDB ID
-   */
-  @Override
-  protected boolean hasPdbId(String pdbId)
-  {
-    return jmb.hasPdbId(pdbId);
-  }
-
   private void openNewJmol(AlignmentPanel ap, PDBEntry[] pdbentrys,
           SequenceI[][] seqs)
   {
@@ -300,6 +202,7 @@ public class AppJmol extends StructureViewerBase
             pdbentrys, seqs, null);
     addAlignmentPanel(ap);
     useAlignmentPanelForColourbyseq(ap);
+
     if (pdbentrys.length > 1)
     {
       alignAddedStructures = true;
@@ -336,6 +239,30 @@ public class AppJmol extends StructureViewerBase
     openNewJmol(ap, 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)
   {
     jmb.setFinishedInit(false);
@@ -483,8 +410,8 @@ public class AppJmol extends StructureViewerBase
     int waitFor = 35;
     int waitTotal = 0;
     while (addingStructures ? lastnotify >= jmb.getLoadNotifiesHandled()
-            : !(jmb.isFinishedInit() && jmb.getPdbFile() != null && jmb
-                    .getPdbFile().length == files.size()))
+            : !(jmb.isFinishedInit() && jmb.getStructureFiles() != null && jmb
+                    .getStructureFiles().length == files.size()))
     {
       try
       {
@@ -502,7 +429,7 @@ public class AppJmol extends StructureViewerBase
 //        System.err.println("finished: " + jmb.isFinishedInit()
 //                + "; loaded: " + Arrays.toString(jmb.getPdbFile())
 //                + "; files: " + files.toString());
-        jmb.getPdbFile();
+        jmb.getStructureFiles();
         break;
       }
     }
@@ -567,7 +494,7 @@ public class AppJmol extends StructureViewerBase
     String pdbid = "";
     try
     {
-      String[] filesInViewer = jmb.getPdbFile();
+      String[] filesInViewer = jmb.getStructureFiles();
       // TODO: replace with reference fetching/transfer code (validate PDBentry
       // as a DBRef?)
       Pdb pdbclient = new Pdb();
@@ -659,76 +586,6 @@ public class AppJmol extends StructureViewerBase
   }
 
   @Override
-  public void pdbFile_actionPerformed(ActionEvent actionEvent)
-  {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
-
-    chooser.setFileView(new JalviewFileView());
-    chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
-    chooser.setToolTipText(MessageManager.getString("action.save"));
-
-    int value = chooser.showSaveDialog(this);
-
-    if (value == JalviewFileChooser.APPROVE_OPTION)
-    {
-      BufferedReader in = null;
-      try
-      {
-        // TODO: cope with multiple PDB files in view
-        in = new BufferedReader(new FileReader(jmb.getPdbFile()[0]));
-        File outFile = chooser.getSelectedFile();
-
-        PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
-        String data;
-        while ((data = in.readLine()) != null)
-        {
-          if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
-          {
-            out.println(data);
-          }
-        }
-        out.close();
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
-      } finally
-      {
-        if (in != null)
-        {
-          try
-          {
-            in.close();
-          } catch (IOException e)
-          {
-            // ignore
-          }
-        }
-      }
-    }
-  }
-
-  @Override
-  public void viewMapping_actionPerformed(ActionEvent actionEvent)
-  {
-    jalview.gui.CutAndPasteTransfer cap = new jalview.gui.CutAndPasteTransfer();
-    try
-    {
-      cap.appendText(jmb.printMappings());
-    } catch (OutOfMemoryError e)
-    {
-      new OOMWarning(
-              "composing sequence-structure alignments for display in text box.",
-              e);
-      cap.dispose();
-      return;
-    }
-    jalview.gui.Desktop.addInternalFrame(cap,
-            MessageManager.getString("label.pdb_sequence_mapping"), 550,
-            600);
-  }
-
-  @Override
   public void eps_actionPerformed(ActionEvent e)
   {
     makePDBImage(jalview.util.ImageMaker.TYPE.EPS);
@@ -775,135 +632,11 @@ public class AppJmol extends StructureViewerBase
   }
 
   @Override
-  public void viewerColour_actionPerformed(ActionEvent actionEvent)
-  {
-    if (viewerColour.isSelected())
-    {
-      // disable automatic sequence colouring.
-      jmb.setColourBySequence(false);
-    }
-  }
-
-  @Override
-  public void seqColour_actionPerformed(ActionEvent actionEvent)
-  {
-    jmb.setColourBySequence(seqColour.isSelected());
-    if (_colourwith == null)
-    {
-      _colourwith = new Vector<AlignmentPanel>();
-    }
-    if (jmb.isColourBySequence())
-    {
-      if (!jmb.isLoadingFromArchive())
-      {
-        if (_colourwith.size() == 0 && getAlignmentPanel() != null)
-        {
-          // Make the currently displayed alignment panel the associated view
-          _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
-        }
-      }
-      // Set the colour using the current view for the associated alignframe
-      for (AlignmentPanel ap : _colourwith)
-      {
-        jmb.colourBySequence(ap);
-      }
-    }
-  }
-
-  @Override
-  public void chainColour_actionPerformed(ActionEvent actionEvent)
-  {
-    chainColour.setSelected(true);
-    jmb.colourByChain();
-  }
-
-  @Override
-  public void chargeColour_actionPerformed(ActionEvent actionEvent)
-  {
-    chargeColour.setSelected(true);
-    jmb.colourByCharge();
-  }
-
-  @Override
-  public void zappoColour_actionPerformed(ActionEvent actionEvent)
-  {
-    zappoColour.setSelected(true);
-    jmb.setJalviewColourScheme(new ZappoColourScheme());
-  }
-
-  @Override
-  public void taylorColour_actionPerformed(ActionEvent actionEvent)
-  {
-    taylorColour.setSelected(true);
-    jmb.setJalviewColourScheme(new TaylorColourScheme());
-  }
-
-  @Override
-  public void hydroColour_actionPerformed(ActionEvent actionEvent)
-  {
-    hydroColour.setSelected(true);
-    jmb.setJalviewColourScheme(new HydrophobicColourScheme());
-  }
-
-  @Override
-  public void helixColour_actionPerformed(ActionEvent actionEvent)
-  {
-    helixColour.setSelected(true);
-    jmb.setJalviewColourScheme(new HelixColourScheme());
-  }
-
-  @Override
-  public void strandColour_actionPerformed(ActionEvent actionEvent)
-  {
-    strandColour.setSelected(true);
-    jmb.setJalviewColourScheme(new StrandColourScheme());
-  }
-
-  @Override
-  public void turnColour_actionPerformed(ActionEvent actionEvent)
-  {
-    turnColour.setSelected(true);
-    jmb.setJalviewColourScheme(new TurnColourScheme());
-  }
-
-  @Override
-  public void buriedColour_actionPerformed(ActionEvent actionEvent)
-  {
-    buriedColour.setSelected(true);
-    jmb.setJalviewColourScheme(new BuriedColourScheme());
-  }
-
-  @Override
-  public void purinePyrimidineColour_actionPerformed(ActionEvent actionEvent)
-  {
-    setJalviewColourScheme(new PurinePyrimidineColourScheme());
-  }
-
-  @Override
-  public void userColour_actionPerformed(ActionEvent actionEvent)
-  {
-    userColour.setSelected(true);
-    new UserDefinedColours(this, null);
-  }
-
-  @Override
-  public void backGround_actionPerformed(ActionEvent actionEvent)
-  {
-    java.awt.Color col = JColorChooser
-            .showDialog(this, MessageManager
-                    .getString("label.select_backgroud_colour"), null);
-    if (col != null)
-    {
-      jmb.setBackgroundColour(col);
-    }
-  }
-
-  @Override
   public void showHelp_actionPerformed(ActionEvent actionEvent)
   {
     try
     {
-      jalview.util.BrowserLauncher
+      BrowserLauncher
               .openURL("http://jmol.sourceforge.net/docs/JmolUserGuide/");
     } catch (Exception ex)
     {
@@ -952,7 +685,7 @@ public class AppJmol extends StructureViewerBase
     {
       getSize(currentSize);
 
-      if (jmb != null && jmb.fileLoadingError != null)
+      if (jmb != null && jmb.hasFileLoadingError())
       {
         g.setColor(Color.black);
         g.fillRect(0, 0, currentSize.width, currentSize.height);
@@ -995,104 +728,6 @@ public class AppJmol extends StructureViewerBase
     }
   }
 
-  public void updateTitleAndMenus()
-  {
-    if (jmb.fileLoadingError != null && jmb.fileLoadingError.length() > 0)
-    {
-      repaint();
-      return;
-    }
-    setChainMenuItems(jmb.getChainNames());
-
-    this.setTitle(jmb.getViewerTitle());
-    if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
-    {
-      viewerActionMenu.setVisible(true);
-    }
-    if (!jmb.isLoadingFromArchive())
-    {
-      seqColour_actionPerformed(null);
-    }
-  }
-
-  /*
-   * (non-Javadoc)
-   * 
-   * @see
-   * jalview.jbgui.GStructureViewer#alignStructs_actionPerformed(java.awt.event
-   * .ActionEvent)
-   */
-  @Override
-  protected void alignStructs_actionPerformed(ActionEvent actionEvent)
-  {
-    alignStructs_withAllAlignPanels();
-  }
-
-  private void alignStructs_withAllAlignPanels()
-  {
-    if (getAlignmentPanel() == null)
-    {
-      return;
-    }
-    ;
-    if (_alignwith.size() == 0)
-    {
-      _alignwith.add(getAlignmentPanel());
-    }
-    ;
-    try
-    {
-      AlignmentI[] als = new Alignment[_alignwith.size()];
-      ColumnSelection[] alc = new ColumnSelection[_alignwith.size()];
-      int[] alm = new int[_alignwith.size()];
-      int a = 0;
-
-      for (AlignmentPanel ap : _alignwith)
-      {
-        als[a] = ap.av.getAlignment();
-        alm[a] = -1;
-        alc[a++] = ap.av.getColumnSelection();
-      }
-      jmb.superposeStructures(als, alm, alc);
-    } catch (Exception e)
-    {
-      StringBuffer sp = new StringBuffer();
-      for (AlignmentPanel ap : _alignwith)
-      {
-        sp.append("'" + ap.alignFrame.getTitle() + "' ");
-      }
-      Cache.log.info("Couldn't align structures with the " + sp.toString()
-              + "associated alignment panels.", e);
-
-    }
-
-  }
-
-  @Override
-  public void setJalviewColourScheme(ColourSchemeI ucs)
-  {
-    jmb.setJalviewColourScheme(ucs);
-
-  }
-
-  /**
-   * 
-   * @param alignment
-   * @return first alignment panel displaying given alignment, or the default
-   *         alignment panel
-   */
-  public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment)
-  {
-    for (AlignmentPanel ap : getAllAlignmentPanels())
-    {
-      if (ap.av.getAlignment() == alignment)
-      {
-        return ap;
-      }
-    }
-    return getAlignmentPanel();
-  }
-
   @Override
   public AAStructureBindingModel getBinding()
   {
@@ -1112,9 +747,8 @@ public class AppJmol extends StructureViewerBase
   }
 
   @Override
-  protected AAStructureBindingModel getBindingModel()
+  protected String getViewerName()
   {
-    return jmb;
+    return "Jmol";
   }
-
 }
index 75e0c5e..f822358 100644 (file)
@@ -40,8 +40,6 @@ public class AppJmolBinding extends JalviewJmolBinding
 {
   private AppJmol appJmolWindow;
 
-  private FeatureRenderer fr = null;
-
   public AppJmolBinding(AppJmol appJmol, StructureSelectionManager sSm,
           PDBEntry[] pdbentry, SequenceI[][] sequenceIs, DataSourceType protocol)
   {
@@ -50,26 +48,6 @@ public class AppJmolBinding extends JalviewJmolBinding
   }
 
   @Override
-  public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment)
-  {
-    AlignmentPanel ap = (alignment == null) ? appJmolWindow
-            .getAlignmentPanel() : (AlignmentPanel) alignment;
-    if (ap.av.isShowSequenceFeatures())
-    {
-      if (fr == null)
-      {
-        fr = (jalview.gui.FeatureRenderer) ap.cloneFeatureRenderer();
-      }
-      else
-      {
-        ap.updateFeatureRenderer(fr);
-      }
-    }
-
-    return fr;
-  }
-
-  @Override
   public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
   {
     return new SequenceRenderer(((AlignmentPanel) alignment).av);
@@ -215,4 +193,18 @@ public class AppJmolBinding extends JalviewJmolBinding
   {
     return appJmolWindow;
   }
+
+  @Override
+  public jalview.api.FeatureRenderer getFeatureRenderer(
+          AlignmentViewPanel alignment)
+  {
+    AlignmentPanel ap = (alignment == null) ? appJmolWindow
+            .getAlignmentPanel() : (AlignmentPanel) alignment;
+    if (ap.av.isShowSequenceFeatures())
+    {
+      return ap.av.getAlignPanel().getSeqPanel().seqCanvas.fr;
+    }
+
+    return null;
+  }
 }
index f4a4f4d..a50de77 100644 (file)
@@ -23,6 +23,7 @@ package jalview.gui;
 import jalview.analysis.AlignSeq;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.RnaViewerModel;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -400,7 +401,7 @@ public class AppVarna extends JInternalFrame implements SelectionListener,
 
   @Override
   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
-          SelectionSource source)
+          HiddenColumns hidden, SelectionSource source)
   {
     if (source != ap.av)
     {
index 8ab1e61..829fc3e 100644 (file)
@@ -384,7 +384,7 @@ public class AppVarnaBinding extends JalviewVarnaBinding
   }
 
   @Override
-  public String[] getPdbFile()
+  public String[] getStructureFiles()
   {
     return null;
   }
diff --git a/src/jalview/gui/CalculationChooser.java b/src/jalview/gui/CalculationChooser.java
new file mode 100644 (file)
index 0000000..8a95594
--- /dev/null
@@ -0,0 +1,593 @@
+/*
+ * 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.analysis.TreeBuilder;
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.analysis.scoremodels.SimilarityParams;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.SequenceGroup;
+import jalview.util.MessageManager;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.beans.PropertyVetoException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JInternalFrame;
+import javax.swing.JLabel;
+import javax.swing.JLayeredPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+/**
+ * A dialog where a user can choose and action Tree or PCA calculation options
+ */
+public class CalculationChooser extends JPanel
+{
+  /*
+   * flag for whether gap matches residue in the PID calculation for a Tree
+   * - true gives Jalview 2.10.1 behaviour
+   * - set to false (using Groovy) for a more correct tree
+   * (JAL-374)
+   */
+  private static boolean treeMatchGaps = true;
+
+  private static final Font VERDANA_11PT = new Font("Verdana", 0, 11);
+
+  private static final int MIN_TREE_SELECTION = 3;
+
+  private static final int MIN_PCA_SELECTION = 4;
+
+  AlignFrame af;
+
+  JRadioButton pca;
+
+  JRadioButton neighbourJoining;
+
+  JRadioButton averageDistance;
+
+  JComboBox<String> modelNames;
+
+  JButton calculate;
+
+  private JInternalFrame frame;
+
+  private JCheckBox includeGaps;
+
+  private JCheckBox matchGaps;
+
+  private JCheckBox includeGappedColumns;
+
+  private JCheckBox shorterSequence;
+
+  final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
+
+  List<String> tips = new ArrayList<String>();
+
+  /**
+   * Constructor
+   * 
+   * @param af
+   */
+  public CalculationChooser(AlignFrame alignFrame)
+  {
+    this.af = alignFrame;
+    init();
+    af.alignPanel.setCalculationDialog(this);
+  }
+
+  /**
+   * Lays out the panel and adds it to the desktop
+   */
+  void init()
+  {
+    setLayout(new BorderLayout());
+    frame = new JInternalFrame();
+    frame.setContentPane(this);
+    this.setBackground(Color.white);
+    frame.addFocusListener(new FocusListener()
+    {
+
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+      }
+
+      @Override
+      public void focusGained(FocusEvent e)
+      {
+        validateCalcTypes();
+      }
+    });
+    /*
+     * Layout consists of 3 or 4 panels:
+     * - first with choice of PCA or tree method NJ or AV
+     * - second with choice of score model
+     * - third with score model parameter options [suppressed]
+     * - fourth with OK and Cancel
+     */
+    pca = new JRadioButton(
+            MessageManager.getString("label.principal_component_analysis"));
+    pca.setOpaque(false);
+    neighbourJoining = new JRadioButton(
+            MessageManager.getString("label.tree_calc_nj"));
+    neighbourJoining.setSelected(true);
+    averageDistance = new JRadioButton(
+            MessageManager.getString("label.tree_calc_av"));
+    neighbourJoining.setOpaque(false);
+
+    JPanel calcChoicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    calcChoicePanel.setOpaque(false);
+
+    // first create the Tree calculation's border panel
+    JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    treePanel.setOpaque(false);
+
+    treePanel.setBorder(BorderFactory.createTitledBorder(MessageManager
+            .getString("label.tree")));
+
+    // then copy the inset dimensions for the border-less PCA panel
+    JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    Insets b = treePanel.getBorder().getBorderInsets(treePanel);
+    pcaBorderless.setBorder(BorderFactory.createEmptyBorder(2, b.left, 2,
+            b.right));
+    pcaBorderless.setOpaque(false);
+
+    pcaBorderless.add(pca, FlowLayout.LEFT);
+    calcChoicePanel.add(pcaBorderless, FlowLayout.LEFT);
+
+    treePanel.add(neighbourJoining);
+    treePanel.add(averageDistance);
+
+    calcChoicePanel.add(treePanel);
+
+    ButtonGroup calcTypes = new ButtonGroup();
+    calcTypes.add(pca);
+    calcTypes.add(neighbourJoining);
+    calcTypes.add(averageDistance);
+    
+    ActionListener calcChanged = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        validateCalcTypes();
+      }
+    };
+    pca.addActionListener(calcChanged);
+    neighbourJoining.addActionListener(calcChanged);
+    averageDistance.addActionListener(calcChanged);
+
+    /*
+     * score models drop-down - with added tooltips!
+     */
+    modelNames = buildModelOptionsList();
+
+    JPanel scoreModelPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
+    scoreModelPanel.setOpaque(false);
+    scoreModelPanel.add(modelNames);
+
+    /*
+     * score model parameters
+     */
+    JPanel paramsPanel = new JPanel(new GridLayout(5, 1));
+    paramsPanel.setOpaque(false);
+    includeGaps = new JCheckBox("Include gaps");
+    matchGaps = new JCheckBox("Match gaps");
+    includeGappedColumns = new JCheckBox("Include gapped columns");
+    shorterSequence = new JCheckBox("Match on shorter sequence");
+    paramsPanel.add(new JLabel("Pairwise sequence scoring options"));
+    paramsPanel.add(includeGaps);
+    paramsPanel.add(matchGaps);
+    paramsPanel.add(includeGappedColumns);
+    paramsPanel.add(shorterSequence);
+
+    /*
+     * OK / Cancel buttons
+     */
+    calculate = new JButton(MessageManager.getString("action.calculate"));
+    calculate.setFont(VERDANA_11PT);
+    calculate.addActionListener(new java.awt.event.ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        calculate_actionPerformed();
+      }
+    });
+    JButton close = new JButton(MessageManager.getString("action.close"));
+    close.setFont(VERDANA_11PT);
+    close.addActionListener(new java.awt.event.ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        close_actionPerformed();
+      }
+    });
+    JPanel actionPanel = new JPanel();
+    actionPanel.setOpaque(false);
+    actionPanel.add(calculate);
+    actionPanel.add(close);
+
+    boolean includeParams = false;
+    this.add(calcChoicePanel, BorderLayout.CENTER);
+    calcChoicePanel.add(scoreModelPanel);
+    if (includeParams)
+    {
+      scoreModelPanel.add(paramsPanel);
+    }
+    this.add(actionPanel, BorderLayout.SOUTH);
+
+    int width = 350;
+    int height = includeParams ? 420 : 240;
+
+    setMinimumSize(new Dimension(325, height - 10));
+    String title = MessageManager.getString("label.choose_calculation");
+    if (af.getViewport().viewName != null)
+    {
+      title = title + " (" + af.getViewport().viewName + ")";
+    }
+
+    Desktop.addInternalFrame(frame, title, width, height, false);
+    calcChoicePanel.doLayout();
+    revalidate();
+    /*
+     * null the AlignmentPanel's reference to the dialog when it is closed
+     */
+    frame.addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosed(InternalFrameEvent evt)
+      {
+        af.alignPanel.setCalculationDialog(null);
+      };
+    });
+
+    frame.setLayer(JLayeredPane.PALETTE_LAYER);
+  }
+
+  /**
+   * enable calculations applicable for the current alignment or selection.
+   */
+  protected void validateCalcTypes()
+  {
+    int size = af.getViewport().getAlignment().getHeight();
+    if (af.getViewport().getSelectionGroup() != null)
+    {
+      size = af.getViewport().getSelectionGroup().getSize();
+    }
+
+    /*
+     * disable calc options for which there is insufficient input data
+     * return value of true means enabled and selected
+     */
+    boolean checkPca = checkEnabled(pca, size, MIN_PCA_SELECTION);
+    boolean checkNeighbourJoining = checkEnabled(neighbourJoining, size,
+            MIN_TREE_SELECTION);
+    boolean checkAverageDistance = checkEnabled(averageDistance, size,
+            MIN_TREE_SELECTION);
+
+    if (checkPca || checkNeighbourJoining || checkAverageDistance)
+    {
+      calculate.setToolTipText(null);
+      calculate.setEnabled(true);
+    }
+    else
+    {
+      calculate.setEnabled(false);
+    }
+    updateScoreModels(modelNames, tips);
+  }
+
+  /**
+   * Check the input and disable a calculation's radio button if necessary. A
+   * tooltip is shown for disabled calculations.
+   * 
+   * @param calc
+   *          - radio button for the calculation being validated
+   * @param size
+   *          - size of input to calculation
+   * @param minsize
+   *          - minimum size for calculation
+   * @return true if size >= minsize and calc.isSelected
+   */
+  private boolean checkEnabled(JRadioButton calc, int size, int minsize)
+  {
+    String ttip = MessageManager.formatMessage(
+            "label.you_need_at_least_n_sequences", minsize);
+
+    calc.setEnabled(size >= minsize);
+    if (!calc.isEnabled())
+    {
+      calc.setToolTipText(ttip);
+    }
+    else
+    {
+      calc.setToolTipText(null);
+    }
+    if (calc.isSelected())
+    {
+      modelNames.setEnabled(calc.isEnabled());
+      if (calc.isEnabled())
+      {
+        return true;
+      }
+      else
+      {
+        calculate.setToolTipText(ttip);
+      }
+    }
+    return false;
+  }
+
+  /**
+   * A rather elaborate helper method (blame Swing, not me) that builds a
+   * drop-down list of score models (by name) with descriptions as tooltips.
+   * There is also a tooltip shown for the currently selected item when hovering
+   * over it (without opening the list).
+   */
+  protected JComboBox<String> buildModelOptionsList()
+  {
+    final JComboBox<String> scoreModelsCombo = new JComboBox<String>();
+    scoreModelsCombo.setRenderer(renderer);
+
+    /*
+     * show tooltip on mouse over the combobox
+     * note the listener has to be on the components that make up
+     * the combobox, doesn't work if just on the combobox
+     */
+    final MouseAdapter mouseListener = new MouseAdapter()
+    {
+      @Override
+      public void mouseEntered(MouseEvent e)
+      {
+        scoreModelsCombo.setToolTipText(tips.get(scoreModelsCombo.getSelectedIndex()));
+      }
+
+      @Override
+      public void mouseExited(MouseEvent e)
+      {
+        scoreModelsCombo.setToolTipText(null);
+      }
+    };
+    for (Component c : scoreModelsCombo.getComponents())
+    {
+      c.addMouseListener(mouseListener);
+    }
+
+    updateScoreModels(scoreModelsCombo, tips);
+
+    /*
+     * set the list of tooltips on the combobox's renderer
+     */
+    renderer.setTooltips(tips);
+
+    return scoreModelsCombo;
+  }
+
+  private void updateScoreModels(JComboBox<String> comboBox,
+          List<String> toolTips)
+  {
+    Object curSel = comboBox.getSelectedItem();
+    toolTips.clear();
+    DefaultComboBoxModel<String> model = new DefaultComboBoxModel<String>();
+
+    /*
+     * now we can actually add entries to the combobox,
+     * remembering their descriptions for tooltips
+     */
+    ScoreModels scoreModels = ScoreModels.getInstance();
+    boolean selectedIsPresent = false;
+    for (ScoreModelI sm : scoreModels.getModels())
+    {
+      boolean nucleotide = af.getViewport().getAlignment().isNucleotide();
+      if (sm.isDNA() && nucleotide || sm.isProtein() && !nucleotide)
+      {
+        if (curSel != null && sm.getName().equals(curSel))
+        {
+          selectedIsPresent = true;
+          curSel = sm.getName();
+        }
+        model.addElement(sm.getName());
+
+        /*
+         * tooltip is description if provided, else text lookup with
+         * fallback on the model name
+         */
+        String tooltip = sm.getDescription();
+        if (tooltip == null)
+        {
+          tooltip = MessageManager.getStringOrReturn("label.score_model_",
+                  sm.getName());
+        }
+        toolTips.add(tooltip);
+      }
+    }
+    if (selectedIsPresent)
+    {
+      model.setSelectedItem(curSel);
+    }
+    // finally, update the model
+    comboBox.setModel(model);
+  }
+
+  /**
+   * Open and calculate the selected tree or PCA on 'OK'
+   */
+  protected void calculate_actionPerformed()
+  {
+    boolean doPCA = pca.isSelected();
+    String modelName = modelNames.getSelectedItem().toString();
+    SimilarityParamsI params = getSimilarityParameters(doPCA);
+
+    if (doPCA)
+    {
+      openPcaPanel(modelName, params);
+    }
+    else
+    {
+      openTreePanel(modelName, params);
+    }
+
+    // closeFrame();
+  }
+
+  /**
+   * Open a new Tree panel on the desktop
+   * 
+   * @param modelName
+   * @param params
+   */
+  protected void openTreePanel(String modelName, SimilarityParamsI params)
+  {
+    /*
+     * gui validation shouldn't allow insufficient sequences here, but leave
+     * this check in in case this method gets exposed programmatically in future
+     */
+    AlignViewport viewport = af.getViewport();
+    SequenceGroup sg = viewport.getSelectionGroup();
+    if (sg != null && sg.getSize() < MIN_TREE_SELECTION)
+    {
+      JvOptionPane
+              .showMessageDialog(
+                      Desktop.desktop,
+                      MessageManager
+              .formatMessage("label.you_need_at_least_n_sequences",
+                      MIN_TREE_SELECTION),
+                      MessageManager
+                              .getString("label.not_enough_sequences"),
+                      JvOptionPane.WARNING_MESSAGE);
+      return;
+    }
+
+    String treeType = neighbourJoining.isSelected() ? TreeBuilder.NEIGHBOUR_JOINING
+            : TreeBuilder.AVERAGE_DISTANCE;
+    af.newTreePanel(treeType, modelName, params);
+  }
+
+  /**
+   * Open a new PCA panel on the desktop
+   * 
+   * @param modelName
+   * @param params
+   */
+  protected void openPcaPanel(String modelName, SimilarityParamsI params)
+  {
+    AlignViewport viewport = af.getViewport();
+
+    /*
+     * gui validation shouldn't allow insufficient sequences here, but leave
+     * this check in in case this method gets exposed programmatically in future
+     */
+    if (((viewport.getSelectionGroup() != null)
+            && (viewport.getSelectionGroup().getSize() < MIN_PCA_SELECTION) && (viewport
+            .getSelectionGroup().getSize() > 0))
+            || (viewport.getAlignment().getHeight() < MIN_PCA_SELECTION))
+    {
+      JvOptionPane.showInternalMessageDialog(this, MessageManager
+              .formatMessage("label.you_need_at_least_n_sequences",
+                      MIN_PCA_SELECTION), MessageManager
+              .getString("label.sequence_selection_insufficient"),
+              JvOptionPane.WARNING_MESSAGE);
+      return;
+    }
+    new PCAPanel(af.alignPanel, modelName, params);
+  }
+
+  /**
+   * 
+   */
+  protected void closeFrame()
+  {
+    try
+    {
+      frame.setClosed(true);
+    } catch (PropertyVetoException ex)
+    {
+    }
+  }
+
+  /**
+   * Returns a data bean holding parameters for similarity (or distance) model
+   * calculation
+   * 
+   * @param doPCA
+   * @return
+   */
+  protected SimilarityParamsI getSimilarityParameters(boolean doPCA)
+  {
+    // commented out: parameter choices read from gui widgets
+    // SimilarityParamsI params = new SimilarityParams(
+    // includeGappedColumns.isSelected(), matchGaps.isSelected(),
+    // includeGaps.isSelected(), shorterSequence.isSelected());
+
+    boolean includeGapGap = true;
+    boolean includeGapResidue = true;
+    boolean matchOnShortestLength = false;
+
+    /*
+     * 'matchGaps' flag is only used in the PID calculation
+     * - set to false for PCA so that PCA using PID reproduces SeqSpace PCA
+     * - set to true for Tree to reproduce Jalview 2.10.1 calculation
+     * - set to false for Tree for a more correct calculation (JAL-374)
+     */
+    boolean matchGap = doPCA ? false : treeMatchGaps;
+
+    return new SimilarityParams(includeGapGap, matchGap, includeGapResidue, matchOnShortestLength);
+  }
+
+  /**
+   * Closes dialog on Close button press
+   */
+  protected void close_actionPerformed()
+  {
+    try
+    {
+      frame.setClosed(true);
+    } catch (Exception ex)
+    {
+    }
+  }
+}
index bc247ca..eadc2ad 100644 (file)
  */
 package jalview.gui;
 
+import jalview.api.FeatureRenderer;
 import jalview.bin.Cache;
-import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.ChimeraCommands;
 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
-import jalview.io.JalviewFileChooser;
-import jalview.io.JalviewFileView;
 import jalview.io.StructureFile;
-import jalview.schemes.BuriedColourScheme;
-import jalview.schemes.ColourSchemeI;
-import jalview.schemes.HelixColourScheme;
-import jalview.schemes.HydrophobicColourScheme;
-import jalview.schemes.PurinePyrimidineColourScheme;
-import jalview.schemes.StrandColourScheme;
-import jalview.schemes.TaylorColourScheme;
-import jalview.schemes.TurnColourScheme;
-import jalview.schemes.ZappoColourScheme;
 import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.BrowserLauncher;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.ws.dbsources.Pdb;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Random;
-import java.util.Vector;
 
 import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JColorChooser;
 import javax.swing.JInternalFrame;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
 import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
-import javax.swing.event.MenuEvent;
-import javax.swing.event.MenuListener;
 
 /**
  * GUI elements for handling an external chimera display
@@ -87,8 +66,6 @@ public class ChimeraViewFrame extends StructureViewerBase
 {
   private JalviewChimeraBinding jmb;
 
-  private boolean allChainsSelected = false;
-
   private IProgressIndicator progressBar = null;
 
   /*
@@ -100,120 +77,64 @@ public class ChimeraViewFrame extends StructureViewerBase
 
   private Random random = new Random();
 
+  private int myWidth = 500;
+
+  private int myHeight = 150;
+
   /**
    * Initialise menu options.
    */
-  private void initMenus()
+  @Override
+  protected void initMenus()
   {
+    super.initMenus();
+
     viewerActionMenu.setText(MessageManager.getString("label.chimera"));
+
     viewerColour.setText(MessageManager
             .getString("label.colour_with_chimera"));
     viewerColour.setToolTipText(MessageManager
             .getString("label.let_chimera_manage_structure_colours"));
-    helpItem.setText(MessageManager.getString("label.chimera_help"));
-    seqColour.setSelected(jmb.isColourBySequence());
-    viewerColour.setSelected(!jmb.isColourBySequence());
-    if (_colourwith == null)
-    {
-      _colourwith = new Vector<AlignmentPanel>();
-    }
-    if (_alignwith == null)
-    {
-      _alignwith = new Vector<AlignmentPanel>();
-    }
-
-    // save As not yet implemented
-    savemenu.setVisible(false);
 
-    ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
-            MessageManager.getString("label.colour_by"), this, _colourwith,
-            new ItemListener()
-            {
-              @Override
-              public void itemStateChanged(ItemEvent e)
-              {
-                if (!seqColour.isSelected())
-                {
-                  seqColour.doClick();
-                }
-                else
-                {
-                  // update the Chimera display now.
-                  seqColour_actionPerformed(null);
-                }
-              }
-            });
-    viewMenu.add(seqColourBy);
+    helpItem.setText(MessageManager.getString("label.chimera_help"));
+    savemenu.setVisible(false); // not yet implemented
     viewMenu.add(fitToWindow);
 
-    final ItemListener handler;
-    JMenu alpanels = new ViewSelectionMenu(
-            MessageManager.getString("label.superpose_with"), this,
-            _alignwith, handler = new ItemListener()
-            {
-              @Override
-              public void itemStateChanged(ItemEvent e)
-              {
-                alignStructs.setEnabled(_alignwith.size() > 0);
-                alignStructs.setToolTipText(MessageManager
-                        .formatMessage(
-                                "label.align_structures_using_linked_alignment_views",
-                                new Object[] { new Integer(_alignwith
-                                        .size()).toString() }));
-              }
-            });
-    handler.itemStateChanged(null);
-    viewerActionMenu.add(alpanels);
-    viewerActionMenu.addMenuListener(new MenuListener()
+    /*
+     * exchange of Jalview features and Chimera attributes is for now
+     * an optionally enabled experimental feature
+     */
+    if (Desktop.instance.showExperimental())
     {
-
-      @Override
-      public void menuSelected(MenuEvent e)
-      {
-        handler.itemStateChanged(null);
-      }
-
-      @Override
-      public void menuDeselected(MenuEvent e)
+      JMenuItem writeFeatures = new JMenuItem(
+              MessageManager.getString("label.create_chimera_attributes"));
+      writeFeatures.setToolTipText(MessageManager
+              .getString("label.create_chimera_attributes_tip"));
+      writeFeatures.addActionListener(new ActionListener()
       {
-        // TODO Auto-generated method stub
-      }
-
-      @Override
-      public void menuCanceled(MenuEvent e)
-      {
-        // TODO Auto-generated method stub
-      }
-    });
-
-    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("Fetch Chimera attributes");
-    fetchAttributes
-            .setToolTipText("Copy Chimera attribute to Jalview feature");
-    fetchAttributes.addMouseListener(new MouseAdapter()
-    {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          sendFeaturesToChimera();
+        }
+      });
+      viewerActionMenu.add(writeFeatures);
 
-      @Override
-      public void mouseEntered(MouseEvent e)
+      final JMenu fetchAttributes = new JMenu(
+              MessageManager.getString("label.fetch_chimera_attributes"));
+      fetchAttributes.setToolTipText(MessageManager
+              .getString("label.fetch_chimera_attributes_tip"));
+      fetchAttributes.addMouseListener(new MouseAdapter()
       {
-        buildAttributesMenu(fetchAttributes);
-      }
-    });
-    viewerActionMenu.add(fetchAttributes);
 
+        @Override
+        public void mouseEntered(MouseEvent e)
+        {
+          buildAttributesMenu(fetchAttributes);
+        }
+      });
+      viewerActionMenu.add(fetchAttributes);
+    }
   }
 
   /**
@@ -275,7 +196,9 @@ public class ChimeraViewFrame extends StructureViewerBase
    */
   protected void sendFeaturesToChimera()
   {
-    jmb.sendFeaturesToViewer(getAlignmentPanel());
+    int count = jmb.sendFeaturesToViewer(getAlignmentPanel());
+    statusBar.setText(MessageManager.formatMessage("label.attributes_set",
+            count));
   }
 
   /**
@@ -329,15 +252,6 @@ public class ChimeraViewFrame extends StructureViewerBase
     }
   }
 
-  /**
-   * Answers true if this viewer already involves the given PDB ID
-   */
-  @Override
-  protected boolean hasPdbId(String pdbId)
-  {
-    return jmb.hasPdbId(pdbId);
-  }
-
   private void openNewChimera(AlignmentPanel ap, PDBEntry[] pdbentrys,
           SequenceI[][] seqs)
   {
@@ -346,13 +260,14 @@ public class ChimeraViewFrame extends StructureViewerBase
             ap.getStructureSelectionManager(), pdbentrys, seqs, null);
     addAlignmentPanel(ap);
     useAlignmentPanelForColourbyseq(ap);
+
     if (pdbentrys.length > 1)
     {
       alignAddedStructures = true;
       useAlignmentPanelForSuperposition(ap);
     }
     jmb.setColourBySequence(true);
-    setSize(400, 400); // probably should be a configurable/dynamic default here
+    setSize(myWidth, myHeight);
     initMenus();
 
     addingStructures = false;
@@ -434,6 +349,28 @@ public class ChimeraViewFrame extends StructureViewerBase
     setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
   }
 
+  /**
+   * 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
@@ -441,8 +378,10 @@ public class ChimeraViewFrame extends StructureViewerBase
    */
   void initChimera()
   {
-    Desktop.addInternalFrame(this, jmb.getViewerTitle("Chimera", true),
-            getBounds().width, getBounds().height);
+    jmb.setFinishedInit(false);
+    Desktop.addInternalFrame(this,
+            jmb.getViewerTitle(getViewerName(), true), getBounds().width,
+            getBounds().height);
 
     if (!jmb.launchChimera())
     {
@@ -469,61 +408,6 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   /**
-   * If the list is not empty, add menu items for 'All' and each individual
-   * chain to the "View | Show Chain" sub-menu. Multiple selections are allowed.
-   * 
-   * @param chainNames
-   */
-  @Override
-  void setChainMenuItems(List<String> chainNames)
-  {
-    chainMenu.removeAll();
-    if (chainNames == null || chainNames.isEmpty())
-    {
-      return;
-    }
-    JMenuItem menuItem = new JMenuItem(
-            MessageManager.getString("label.all"));
-    menuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent evt)
-      {
-        allChainsSelected = true;
-        for (int i = 0; i < chainMenu.getItemCount(); i++)
-        {
-          if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
-          {
-            ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
-          }
-        }
-        showSelectedChains();
-        allChainsSelected = false;
-      }
-    });
-
-    chainMenu.add(menuItem);
-
-    for (String chainName : chainNames)
-    {
-      menuItem = new JCheckBoxMenuItem(chainName, true);
-      menuItem.addItemListener(new ItemListener()
-      {
-        @Override
-        public void itemStateChanged(ItemEvent evt)
-        {
-          if (!allChainsSelected)
-          {
-            showSelectedChains();
-          }
-        }
-      });
-
-      chainMenu.add(menuItem);
-    }
-  }
-
-  /**
    * Show only the selected chain(s) in the viewer
    */
   @Override
@@ -549,47 +433,36 @@ public class ChimeraViewFrame extends StructureViewerBase
    * option to close the associated Chimera window (process). They may wish to
    * keep it open until they have had an opportunity to save any work.
    * 
-   * @param forceCloseChimera
+   * @param closeChimera
    *          if true, close any linked Chimera process; if false, prompt first
    */
   @Override
-  public void closeViewer(boolean forceCloseChimera)
+  public void closeViewer(boolean closeChimera)
   {
-    if (jmb != null)
+    if (jmb != null && jmb.isChimeraRunning())
     {
-      if (jmb.isChimeraRunning())
+      if (!closeChimera)
       {
+        String prompt = MessageManager.formatMessage(
+                "label.confirm_close_chimera",
+                        new Object[] { jmb.getViewerTitle(getViewerName(),
+                                false) });
+        prompt = JvSwingUtils.wrapTooltip(true, prompt);
+        int confirm = JvOptionPane.showConfirmDialog(this, prompt,
+                MessageManager.getString("label.close_viewer"),
+                JvOptionPane.YES_NO_CANCEL_OPTION);
         /*
-         * force close, or prompt to close, Chimera
+         * abort closure if user hits escape or Cancel
          */
-        if (!forceCloseChimera)
+        if (confirm == JvOptionPane.CANCEL_OPTION
+                || confirm == JvOptionPane.CLOSED_OPTION)
         {
-          String prompt = MessageManager.formatMessage(
-                  "label.confirm_close_chimera",
-                  new Object[] { jmb.getViewerTitle("Chimera", false) });
-          prompt = JvSwingUtils.wrapTooltip(true, prompt);
-          int confirm = JvOptionPane.showConfirmDialog(this, prompt,
-                  MessageManager.getString("label.close_viewer"),
-                  JvOptionPane.YES_NO_CANCEL_OPTION);
-          /*
-           * abort closure if user hits escape or Cancel
-           */
-          if (confirm == JvOptionPane.CANCEL_OPTION
-                  || confirm == JvOptionPane.CLOSED_OPTION)
-          {
-            return;
-          }
-          forceCloseChimera = confirm == JvOptionPane.YES_OPTION;
+          return;
         }
+        closeChimera = confirm == JvOptionPane.YES_OPTION;
       }
-
-      /*
-       * close the viewer plus any side-effects e.g. remove mappings
-       * note we do this also if closing Chimera triggered this method
-       */
-      jmb.closeViewer(forceCloseChimera);
+      jmb.closeViewer(closeChimera);
     }
-
     setAlignmentPanel(null);
     _aps.clear();
     _alignwith.clear();
@@ -617,7 +490,7 @@ public class ChimeraViewFrame extends StructureViewerBase
     StructureFile pdb = null;
     try
     {
-      String[] curfiles = jmb.getPdbFile(); // files currently in viewer
+      String[] curfiles = jmb.getStructureFiles(); // files currently in viewer
       // TODO: replace with reference fetching/transfer code (validate PDBentry
       // as a DBRef?)
       for (int pi = 0; pi < jmb.getPdbCount(); pi++)
@@ -704,7 +577,7 @@ public class ChimeraViewFrame extends StructureViewerBase
           try
           {
             int pos = filePDBpos.get(num).intValue();
-            long startTime = startProgressBar("Chimera "
+            long startTime = startProgressBar(getViewerName() + " "
                     + MessageManager.getString("status.opening_file_for")
                     + " " + pe.getId());
             jmb.openFile(pe);
@@ -747,6 +620,16 @@ public class ChimeraViewFrame extends StructureViewerBase
       jmb.setFinishedInit(true);
       jmb.setLoadingFromArchive(false);
 
+      /*
+       * ensure that any newly discovered features (e.g. RESNUM)
+       * are added to any open feature settings dialog
+       */
+      FeatureRenderer fr = getBinding().getFeatureRenderer(null);
+      if (fr != null)
+      {
+        fr.featuresAdded();
+      }
+
       // refresh the sequence colours for the new structure(s)
       for (AlignmentPanel ap : _colourwith)
       {
@@ -870,76 +753,6 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   @Override
-  public void pdbFile_actionPerformed(ActionEvent actionEvent)
-  {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
-
-    chooser.setFileView(new JalviewFileView());
-    chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
-    chooser.setToolTipText(MessageManager.getString("action.save"));
-
-    int value = chooser.showSaveDialog(this);
-
-    if (value == JalviewFileChooser.APPROVE_OPTION)
-    {
-      BufferedReader in = null;
-      try
-      {
-        // TODO: cope with multiple PDB files in view
-        in = new BufferedReader(new FileReader(jmb.getPdbFile()[0]));
-        File outFile = chooser.getSelectedFile();
-
-        PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
-        String data;
-        while ((data = in.readLine()) != null)
-        {
-          if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
-          {
-            out.println(data);
-          }
-        }
-        out.close();
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
-      } finally
-      {
-        if (in != null)
-        {
-          try
-          {
-            in.close();
-          } catch (IOException e)
-          {
-            e.printStackTrace();
-          }
-        }
-      }
-    }
-  }
-
-  @Override
-  public void viewMapping_actionPerformed(ActionEvent actionEvent)
-  {
-    jalview.gui.CutAndPasteTransfer cap = new jalview.gui.CutAndPasteTransfer();
-    try
-    {
-      cap.appendText(jmb.printMappings());
-    } catch (OutOfMemoryError e)
-    {
-      new OOMWarning(
-              "composing sequence-structure alignments for display in text box.",
-              e);
-      cap.dispose();
-      return;
-    }
-    jalview.gui.Desktop.addInternalFrame(cap,
-            MessageManager.getString("label.pdb_sequence_mapping"), 550,
-            600);
-  }
-
-  @Override
   public void eps_actionPerformed(ActionEvent e)
   {
     throw new Error(
@@ -956,238 +769,18 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   @Override
-  public void viewerColour_actionPerformed(ActionEvent actionEvent)
-  {
-    if (viewerColour.isSelected())
-    {
-      // disable automatic sequence colouring.
-      jmb.setColourBySequence(false);
-    }
-  }
-
-  @Override
-  public void seqColour_actionPerformed(ActionEvent actionEvent)
-  {
-    jmb.setColourBySequence(seqColour.isSelected());
-    if (_colourwith == null)
-    {
-      _colourwith = new Vector<AlignmentPanel>();
-    }
-    if (jmb.isColourBySequence())
-    {
-      if (!jmb.isLoadingFromArchive())
-      {
-        if (_colourwith.size() == 0 && getAlignmentPanel() != null)
-        {
-          // Make the currently displayed alignment panel the associated view
-          _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
-        }
-      }
-      // Set the colour using the current view for the associated alignframe
-      for (AlignmentPanel ap : _colourwith)
-      {
-        jmb.colourBySequence(ap.av.isShowSequenceFeatures(), ap);
-      }
-    }
-  }
-
-  @Override
-  public void chainColour_actionPerformed(ActionEvent actionEvent)
-  {
-    chainColour.setSelected(true);
-    jmb.colourByChain();
-  }
-
-  @Override
-  public void chargeColour_actionPerformed(ActionEvent actionEvent)
-  {
-    chargeColour.setSelected(true);
-    jmb.colourByCharge();
-  }
-
-  @Override
-  public void zappoColour_actionPerformed(ActionEvent actionEvent)
-  {
-    zappoColour.setSelected(true);
-    jmb.setJalviewColourScheme(new ZappoColourScheme());
-  }
-
-  @Override
-  public void taylorColour_actionPerformed(ActionEvent actionEvent)
-  {
-    taylorColour.setSelected(true);
-    jmb.setJalviewColourScheme(new TaylorColourScheme());
-  }
-
-  @Override
-  public void hydroColour_actionPerformed(ActionEvent actionEvent)
-  {
-    hydroColour.setSelected(true);
-    jmb.setJalviewColourScheme(new HydrophobicColourScheme());
-  }
-
-  @Override
-  public void helixColour_actionPerformed(ActionEvent actionEvent)
-  {
-    helixColour.setSelected(true);
-    jmb.setJalviewColourScheme(new HelixColourScheme());
-  }
-
-  @Override
-  public void strandColour_actionPerformed(ActionEvent actionEvent)
-  {
-    strandColour.setSelected(true);
-    jmb.setJalviewColourScheme(new StrandColourScheme());
-  }
-
-  @Override
-  public void turnColour_actionPerformed(ActionEvent actionEvent)
-  {
-    turnColour.setSelected(true);
-    jmb.setJalviewColourScheme(new TurnColourScheme());
-  }
-
-  @Override
-  public void buriedColour_actionPerformed(ActionEvent actionEvent)
-  {
-    buriedColour.setSelected(true);
-    jmb.setJalviewColourScheme(new BuriedColourScheme());
-  }
-
-  @Override
-  public void purinePyrimidineColour_actionPerformed(ActionEvent actionEvent)
-  {
-    setJalviewColourScheme(new PurinePyrimidineColourScheme());
-  }
-
-  @Override
-  public void userColour_actionPerformed(ActionEvent actionEvent)
-  {
-    userColour.setSelected(true);
-    new UserDefinedColours(this, null);
-  }
-
-  @Override
-  public void backGround_actionPerformed(ActionEvent actionEvent)
-  {
-    java.awt.Color col = JColorChooser
-            .showDialog(this, MessageManager
-                    .getString("label.select_backgroud_colour"), null);
-    if (col != null)
-    {
-      jmb.setBackgroundColour(col);
-    }
-  }
-
-  @Override
   public void showHelp_actionPerformed(ActionEvent actionEvent)
   {
     try
     {
-      jalview.util.BrowserLauncher
+      BrowserLauncher
               .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
-    } catch (Exception ex)
-    {
-    }
-  }
-
-  public void updateTitleAndMenus()
-  {
-    if (jmb.fileLoadingError != null && jmb.fileLoadingError.length() > 0)
-    {
-      repaint();
-      return;
-    }
-    setChainMenuItems(jmb.getChainNames());
-
-    this.setTitle(jmb.getViewerTitle("Chimera", true));
-    // if (jmb.getPdbFile().length > 1 && jmb.getSequence().length > 1)
-    // {
-      viewerActionMenu.setVisible(true);
-    // }
-    if (!jmb.isLoadingFromArchive())
-    {
-      seqColour_actionPerformed(null);
-    }
-  }
-
-  /*
-   * (non-Javadoc)
-   * 
-   * @see
-   * jalview.jbgui.GStructureViewer#alignStructs_actionPerformed(java.awt.event
-   * .ActionEvent)
-   */
-  @Override
-  protected void alignStructs_actionPerformed(ActionEvent actionEvent)
-  {
-    alignStructs_withAllAlignPanels();
-  }
-
-  private void alignStructs_withAllAlignPanels()
-  {
-    if (getAlignmentPanel() == null)
-    {
-      return;
-    }
-
-    if (_alignwith.size() == 0)
-    {
-      _alignwith.add(getAlignmentPanel());
-    }
-
-    try
+    } catch (IOException ex)
     {
-      AlignmentI[] als = new Alignment[_alignwith.size()];
-      ColumnSelection[] alc = new ColumnSelection[_alignwith.size()];
-      int[] alm = new int[_alignwith.size()];
-      int a = 0;
-
-      for (AlignmentPanel ap : _alignwith)
-      {
-        als[a] = ap.av.getAlignment();
-        alm[a] = -1;
-        alc[a++] = ap.av.getColumnSelection();
-      }
-      jmb.superposeStructures(als, alm, alc);
-    } catch (Exception e)
-    {
-      StringBuffer sp = new StringBuffer();
-      for (AlignmentPanel ap : _alignwith)
-      {
-        sp.append("'" + ap.alignFrame.getTitle() + "' ");
-      }
-      Cache.log.info("Couldn't align structures with the " + sp.toString()
-              + "associated alignment panels.", e);
     }
   }
 
   @Override
-  public void setJalviewColourScheme(ColourSchemeI ucs)
-  {
-    jmb.setJalviewColourScheme(ucs);
-
-  }
-
-  /**
-   * 
-   * @param alignment
-   * @return first alignment panel displaying given alignment, or the default
-   *         alignment panel
-   */
-  public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment)
-  {
-    for (AlignmentPanel ap : getAllAlignmentPanels())
-    {
-      if (ap.av.getAlignment() == alignment)
-      {
-        return ap;
-      }
-    }
-    return getAlignmentPanel();
-  }
-
-  @Override
   public AAStructureBindingModel getBinding()
   {
     return jmb;
@@ -1276,8 +869,24 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   @Override
-  protected AAStructureBindingModel getBindingModel()
+  protected String getViewerName()
   {
-    return jmb;
+    return "Chimera";
+  }
+
+  /**
+   * Sends commands to align structures according to associated alignment(s).
+   * 
+   * @return
+   */
+  @Override
+  protected String alignStructs_withAllAlignPanels()
+  {
+    String reply = super.alignStructs_withAllAlignPanels();
+    if (reply != null)
+    {
+      statusBar.setText("Superposition failed: " + reply);
+    }
+    return reply;
   }
 }
diff --git a/src/jalview/gui/ColourMenuHelper.java b/src/jalview/gui/ColourMenuHelper.java
new file mode 100644 (file)
index 0000000..b2b9574
--- /dev/null
@@ -0,0 +1,297 @@
+package jalview.gui;
+
+import jalview.bin.Cache;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemeLoader;
+import jalview.schemes.ColourSchemes;
+import jalview.schemes.ResidueColourScheme;
+import jalview.schemes.UserColourScheme;
+import jalview.util.MessageManager;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JMenu;
+import javax.swing.JRadioButtonMenuItem;
+
+public class ColourMenuHelper
+{
+  public interface ColourChangeListener
+  {
+    /**
+     * Change colour scheme to the selected scheme
+     * 
+     * @param name
+     *          the registered (unique) name of a colour scheme
+     */
+    void changeColour_actionPerformed(String name);
+  }
+
+  /**
+   * Adds items to the colour menu, as mutually exclusive members of a button
+   * group. The callback handler is responsible for the action on selecting any
+   * of these options. The callback method receives the name of the selected
+   * colour, or "None" or "User Defined". This method returns the ButtonGroup to
+   * which items were added.
+   * <ul>
+   * <li>None</li>
+   * <li>Clustal</li>
+   * <li>...other 'built-in' colours</li>
+   * <li>...any user-defined colours</li>
+   * <li>User Defined..(only for AlignFrame menu)</li>
+   * </ul>
+   * 
+   * @param colourMenu
+   *          the menu to attach items to
+   * @param client
+   *          a callback to handle menu selection
+   * @param coll
+   *          the data the menu is being built for
+   * @param simpleOnly
+   *          if true, only simple per-residue colour schemes are included
+   */
+  public static ButtonGroup addMenuItems(final JMenu colourMenu,
+          final ColourChangeListener client, AnnotatedCollectionI coll,
+          boolean simpleOnly)
+  {
+    /*
+     * ButtonGroup groups those items whose 
+     * selection is mutually exclusive
+     */
+    ButtonGroup colours = new ButtonGroup();
+
+    if (!simpleOnly)
+    {
+      JRadioButtonMenuItem noColourmenuItem = new JRadioButtonMenuItem(
+              MessageManager.getString("label.none"));
+      noColourmenuItem.setName(ResidueColourScheme.NONE);
+      noColourmenuItem.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          client.changeColour_actionPerformed(ResidueColourScheme.NONE);
+        }
+      });
+      colourMenu.add(noColourmenuItem);
+      colours.add(noColourmenuItem);
+    }
+
+    /*
+     * scan registered colour schemes (built-in or user-defined)
+     * and add them to the menu (in the order they were registered)
+     */
+    Iterable<ColourSchemeI> colourSchemes = ColourSchemes.getInstance()
+            .getColourSchemes();
+    for (ColourSchemeI scheme : colourSchemes)
+    {
+      if (simpleOnly && !scheme.isSimple())
+      {
+        continue;
+      }
+
+      /*
+       * button text is i18n'd but the name is the canonical name of
+       * the colour scheme (inspected in setColourSelected())
+       */
+      final String name = scheme.getSchemeName();
+      String label = MessageManager.getStringOrReturn("label.colourScheme_"
+              + name.toLowerCase().replace(" ", "_"), name);
+      final JRadioButtonMenuItem radioItem = new JRadioButtonMenuItem(label);
+      radioItem.setName(name);
+      radioItem.setEnabled(scheme.isApplicableTo(coll));
+      if (scheme instanceof UserColourScheme)
+      {
+        /*
+         * user-defined colour scheme loaded on startup or during the
+         * Jalview session; right-click on this offers the option to
+         * remove it as a colour choice (unless currently selected)
+         */
+        radioItem.addMouseListener(new MouseAdapter()
+        {
+          @Override
+          public void mousePressed(MouseEvent evt)
+          {
+            if (evt.isPopupTrigger() && !radioItem.isSelected()) // Mac
+            {
+              offerRemoval();
+            }
+          }
+
+          @Override
+          public void mouseReleased(MouseEvent evt)
+          {
+            if (evt.isPopupTrigger() && !radioItem.isSelected()) // Windows
+            {
+              offerRemoval();
+            }
+          }
+
+          void offerRemoval()
+          {
+            ActionListener al = radioItem.getActionListeners()[0];
+            radioItem.removeActionListener(al);
+            int option = JvOptionPane.showInternalConfirmDialog(
+                    Desktop.desktop, MessageManager
+                            .getString("label.remove_from_default_list"),
+                    MessageManager
+                            .getString("label.remove_user_defined_colour"),
+                    JvOptionPane.YES_NO_OPTION);
+            if (option == JvOptionPane.YES_OPTION)
+            {
+              ColourSchemes.getInstance().removeColourScheme(
+                      radioItem.getName());
+              colourMenu.remove(radioItem);
+              updatePreferences();
+            }
+            else
+            {
+              radioItem.addActionListener(al);
+            }
+          }
+        });
+      }
+      radioItem.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent evt)
+        {
+          client.changeColour_actionPerformed(name);
+        }
+      });
+      colourMenu.add(radioItem);
+      colours.add(radioItem);
+    }
+
+    /*
+     * only add the option to load/configure a user-defined colour
+     * to the AlignFrame colour menu
+     */
+    if (client instanceof AlignFrame)
+    {
+      final String label = MessageManager.getString("action.user_defined");
+      JRadioButtonMenuItem userDefinedColour = new JRadioButtonMenuItem(
+              label);
+      userDefinedColour.setName(ResidueColourScheme.USER_DEFINED);
+      userDefinedColour.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          client.changeColour_actionPerformed(ResidueColourScheme.USER_DEFINED);
+        }
+      });
+      colourMenu.add(userDefinedColour);
+      colours.add(userDefinedColour);
+    }
+
+    return colours;
+  }
+
+  /**
+   * Marks as selected the colour menu item matching the given colour scheme, or
+   * the first item ('None') if no match is found. If the colour scheme is a
+   * user defined scheme, but not in the menu (this arises if a new scheme is
+   * defined and applied but not saved to file), then menu option
+   * "User Defined.." is selected.
+   * 
+   * @param colourMenu
+   * @param cs
+   */
+  public static void setColourSelected(JMenu colourMenu, ColourSchemeI cs)
+  {
+    String colourName = cs == null ? ResidueColourScheme.NONE : cs
+            .getSchemeName();
+
+    JRadioButtonMenuItem none = null;
+    JRadioButtonMenuItem userDefined = null;
+
+    /*
+     * select the radio button whose name matches the colour name
+     * (not the button text, as it may be internationalised)
+     */
+    for (Component menuItem : colourMenu.getMenuComponents())
+    {
+      if (menuItem instanceof JRadioButtonMenuItem)
+      {
+        JRadioButtonMenuItem radioButton = (JRadioButtonMenuItem) menuItem;
+        String buttonName = radioButton.getName();
+        if (buttonName.equals(colourName))
+        {
+          radioButton.setSelected(true);
+          return;
+        }
+        if (ResidueColourScheme.NONE.equals(buttonName))
+        {
+          none = radioButton;
+        }
+        if (ResidueColourScheme.USER_DEFINED.equals(buttonName))
+        {
+          userDefined = radioButton;
+        }
+      }
+    }
+
+    /*
+     * no match by name; select User Defined.. if current scheme is a 
+     * user defined one, else select None
+     */
+    if (cs instanceof UserColourScheme && userDefined != null)
+    {
+      userDefined.setSelected(true);
+    }
+    else if (none != null)
+    {
+      none.setSelected(true);
+    }
+  }
+
+  /**
+   * Updates the USER_DEFINE_COLOURS preference to remove any de-registered
+   * colour scheme
+   */
+  static void updatePreferences()
+  {
+    StringBuilder coloursFound = new StringBuilder();
+    String[] files = Cache.getProperty("USER_DEFINED_COLOURS").split("\\|");
+
+    /*
+     * the property does not include the scheme name, it is in the file;
+     * so just load the colour schemes and discard any whose name is not
+     * registered
+     */
+    for (String file : files)
+    {
+      try
+      {
+        UserColourScheme ucs = ColourSchemeLoader.loadColourScheme(file);
+        if (ucs != null
+                && ColourSchemes.getInstance().nameExists(ucs.getName()))
+        {
+          if (coloursFound.length() > 0)
+          {
+            coloursFound.append("|");
+          }
+          coloursFound.append(file);
+        }
+      } catch (Exception ex)
+      {
+        System.out.println("Error loading User ColourFile\n" + ex);
+      }
+    }
+
+    if (coloursFound.toString().length() > 1)
+    {
+      Cache.setProperty("USER_DEFINED_COLOURS", coloursFound.toString());
+    }
+    else
+    {
+      Cache.applicationProperties.remove("USER_DEFINED_COLOURS");
+    }
+  }
+}
diff --git a/src/jalview/gui/ComboBoxTooltipRenderer.java b/src/jalview/gui/ComboBoxTooltipRenderer.java
new file mode 100644 (file)
index 0000000..b776757
--- /dev/null
@@ -0,0 +1,42 @@
+package jalview.gui;
+
+import java.awt.Component;
+import java.util.List;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JComponent;
+import javax.swing.JList;
+
+/**
+ * A helper class to render a combobox with tooltips
+ * 
+ * @see http
+ *      ://stackoverflow.com/questions/480261/java-swing-mouseover-text-on-jcombobox
+ *      -items
+ */
+public class ComboBoxTooltipRenderer extends DefaultListCellRenderer
+{
+  private static final long serialVersionUID = 1L;
+
+  List<String> tooltips;
+
+  @Override
+  public Component getListCellRendererComponent(JList list, Object value,
+          int index, boolean isSelected, boolean cellHasFocus)
+  {
+
+    JComponent comp = (JComponent) super.getListCellRendererComponent(list,
+            value, index, isSelected, cellHasFocus);
+
+    if (-1 < index && null != value && null != tooltips)
+    {
+      list.setToolTipText(tooltips.get(index));
+    }
+    return comp;
+  }
+
+  public void setTooltips(List<String> tips)
+  {
+    this.tooltips = tips;
+  }
+}
index 280b4c0..de7574c 100644 (file)
@@ -84,6 +84,10 @@ public class Console extends WindowAdapter implements WindowListener,
   // are we attached to some parent Desktop
   Desktop parent = null;
 
+  private int MIN_WIDTH = 300;
+
+  private int MIN_HEIGHT = 250;
+
   public Console()
   {
     // create all components and add them
@@ -243,7 +247,9 @@ public class Console extends WindowAdapter implements WindowListener,
             .getLocalGraphicsEnvironment();
     String[] fontNames = ge.getAvailableFontFamilyNames();
     for (int n = 0; n < fontNames.length; n++)
+    {
       System.out.println(fontNames[n]);
+    }
     // Testing part: simple an error thrown anywhere in this JVM will be printed
     // on the Console
     // We do it with a seperate Thread becasue we don't wan't to break a Thread
@@ -259,9 +265,13 @@ public class Console extends WindowAdapter implements WindowListener,
     JFrame frame = new JFrame(string);
     frame.setName(string);
     if (x == -1)
-      x = (int) (i / 2);
+    {
+      x = i / 2;
+    }
     if (y == -1)
-      y = (int) (j / 2);
+    {
+      y = j / 2;
+    }
     frame.setBounds(x, y, i, j);
     return frame;
   }
@@ -298,6 +308,7 @@ public class Console extends WindowAdapter implements WindowListener,
       frame = initFrame("Jalview Java Console", bounds.width,
               bounds.height, bounds.x, bounds.y);
     }
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
     // desktop.add(frame);
     initConsole(false);
     JalviewAppender jappender = new JalviewAppender();
@@ -345,6 +356,7 @@ public class Console extends WindowAdapter implements WindowListener,
     // System.exit(0);
   }
 
+  @Override
   public synchronized void windowClosed(WindowEvent evt)
   {
     frame.setVisible(false);
@@ -365,6 +377,7 @@ public class Console extends WindowAdapter implements WindowListener,
     }
   }
 
+  @Override
   public synchronized void windowClosing(WindowEvent evt)
   {
     frame.setVisible(false); // default behaviour of JFrame
@@ -373,12 +386,14 @@ public class Console extends WindowAdapter implements WindowListener,
     // frame.dispose();
   }
 
+  @Override
   public synchronized void actionPerformed(ActionEvent evt)
   {
     trimBuffer(true);
     // textArea.setText("");
   }
 
+  @Override
   public synchronized void run()
   {
     try
@@ -410,7 +425,9 @@ public class Console extends WindowAdapter implements WindowListener,
           // lines++;
         }
         if (quit)
+        {
           return;
+        }
       }
 
       while (Thread.currentThread() == reader2)
@@ -439,7 +456,9 @@ public class Console extends WindowAdapter implements WindowListener,
           // lines++;
         }
         if (quit)
+        {
           return;
+        }
       }
       while (Thread.currentThread() == textAppender)
       {
@@ -531,6 +550,7 @@ public class Console extends WindowAdapter implements WindowListener,
     long time = System.nanoTime();
     javax.swing.SwingUtilities.invokeLater(new Runnable()
     {
+      @Override
       public void run()
       {
         displayPipe.append(input); // change to stringBuffer
@@ -598,7 +618,9 @@ public class Console extends WindowAdapter implements WindowListener,
     {
       int available = in.available();
       if (available == 0)
+      {
         break;
+      }
       byte b[] = new byte[available];
       in.read(b);
       input = input + new String(b, 0, b.length);
index 7a0b0af..3eced2f 100644 (file)
@@ -28,7 +28,7 @@ import jalview.api.FeaturesDisplayedI;
 import jalview.api.FeaturesSourceI;
 import jalview.bin.Jalview;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.io.AlignmentFileReaderI;
 import jalview.io.AppletFormatAdapter;
@@ -284,8 +284,8 @@ public class CutAndPasteTransfer extends GCutAndPasteTransfer
         AlignFrame af;
         if (source instanceof ComplexAlignFile)
         {
-          ColumnSelection colSel = ((ComplexAlignFile) source)
-                  .getColumnSelection();
+          HiddenColumns hidden = ((ComplexAlignFile) source)
+                  .getHiddenColumns();
           SequenceI[] hiddenSeqs = ((ComplexAlignFile) source)
                   .getHiddenSequences();
           boolean showSeqFeatures = ((ComplexAlignFile) source)
@@ -294,10 +294,11 @@ public class CutAndPasteTransfer extends GCutAndPasteTransfer
                   .getGlobalColourScheme();
           FeaturesDisplayedI fd = ((ComplexAlignFile) source)
                   .getDisplayedFeatures();
-          af = new AlignFrame(al, hiddenSeqs, colSel,
+          af = new AlignFrame(al, hiddenSeqs, hidden,
                   AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
           af.getViewport().setShowSequenceFeatures(showSeqFeatures);
           af.getViewport().setFeaturesDisplayed(fd);
+          af.setMenusForViewport();
           ColourSchemeI cs = ColourSchemeMapper.getJalviewColourScheme(
                   colourSchemeName, al);
           if (cs != null)
index 8c8f228..c5ec067 100644 (file)
@@ -453,7 +453,7 @@ public class DasSourceBrowser extends GDasSourceBrowser implements
     pane12.add(nametf, BorderLayout.EAST);
     panel.add(pane12, BorderLayout.NORTH);
     pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.url")),
+    pane12.add(new JLabel(MessageManager.getString("label.url:")),
             BorderLayout.NORTH);
     pane12.add(seqs, BorderLayout.SOUTH);
     pane12.add(urltf, BorderLayout.EAST);
index 4ce42dc..d076ba0 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.gui;
 
-import static jalview.util.UrlConstants.EMBLEBI_STRING;
 import static jalview.util.UrlConstants.SEQUENCE_ID;
 
 import jalview.api.AlignViewportI;
@@ -39,11 +38,14 @@ import jalview.io.JalviewFileView;
 import jalview.jbgui.GSplitFrame;
 import jalview.jbgui.GStructureViewer;
 import jalview.structure.StructureSelectionManager;
+import jalview.urls.IdOrgSettings;
 import jalview.util.ImageMaker;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.UrlConstants;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.ws.params.ParamManager;
+import jalview.ws.utils.UrlDownloadClient;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -78,6 +80,7 @@ import java.beans.PropertyChangeListener;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Hashtable;
@@ -126,6 +129,15 @@ public class Desktop extends jalview.jbgui.GDesktop implements
         DropTargetListener, ClipboardOwner, IProgressIndicator,
         jalview.api.StructureSelectionManagerProvider
 {
+  private static int DEFAULT_MIN_WIDTH = 300;
+
+  private static int DEFAULT_MIN_HEIGHT = 250;
+
+  private static int ALIGN_FRAME_DEFAULT_MIN_WIDTH = 600;
+
+  private static int ALIGN_FRAME_DEFAULT_MIN_HEIGHT = 70;
+
+  private static final String EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES";
 
   private JalviewChangeSupport changeSupport = new JalviewChangeSupport();
 
@@ -324,19 +336,6 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     instance = this;
     doVamsasClientCheck();
 
-    groovyShell = new JMenuItem();
-    groovyShell.setText(MessageManager.getString("label.groovy_console"));
-    groovyShell.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        groovyShell_actionPerformed();
-      }
-    });
-    toolsMenu.add(groovyShell);
-    groovyShell.setVisible(true);
-
     doConfigureStructurePrefs();
     setTitle("Jalview " + jalview.bin.Cache.getProperty("VERSION"));
     setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
@@ -391,6 +390,10 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     showConsole(showjconsole);
 
     showNews.setVisible(false);
+    
+    experimentalFeatures.setSelected(showExperimental());
+
+    getIdentifiersOrgData();
 
     checkURLLinks();
 
@@ -485,6 +488,19 @@ public class Desktop extends jalview.jbgui.GDesktop implements
             });
   }
 
+  /**
+   * Answers true if user preferences to enable experimental features is True
+   * (on), else false
+   * 
+   * @return
+   */
+  public boolean showExperimental()
+  {
+    String experimental = Cache.getDefault(EXPERIMENTAL_FEATURES,
+            Boolean.FALSE.toString());
+    return Boolean.valueOf(experimental).booleanValue();
+  }
+
   public void doConfigureStructurePrefs()
   {
     // configure services
@@ -525,6 +541,29 @@ public class Desktop extends jalview.jbgui.GDesktop implements
     });
   }
 
+  public void getIdentifiersOrgData()
+  {
+    // Thread off the identifiers fetcher
+    addDialogThread(new Runnable()
+    {
+      @Override
+      public void run()
+      {
+        Cache.log.debug("Downloading data from identifiers.org");
+        UrlDownloadClient client = new UrlDownloadClient();
+        try
+        {
+          client.download(IdOrgSettings.getUrl(),
+                  IdOrgSettings.getDownloadLocation());
+        } catch (IOException e)
+        {
+          Cache.log.debug("Exception downloading identifiers.org data"
+                  + e.getMessage());
+        }
+      }
+    });
+  }
+
   @Override
   protected void showNews_actionPerformed(ActionEvent e)
   {
@@ -714,7 +753,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
   public static synchronized void addInternalFrame(
           final JInternalFrame frame, String title, int w, int h)
   {
-    addInternalFrame(frame, title, true, w, h, true);
+    addInternalFrame(frame, title, true, w, h, true, false);
   }
 
   /**
@@ -736,7 +775,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
           final JInternalFrame frame, String title, boolean makeVisible,
           int w, int h)
   {
-    addInternalFrame(frame, title, makeVisible, w, h, true);
+    addInternalFrame(frame, title, makeVisible, w, h, true, false);
   }
 
   /**
@@ -757,7 +796,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
           final JInternalFrame frame, String title, int w, int h,
           boolean resizable)
   {
-    addInternalFrame(frame, title, true, w, h, resizable);
+    addInternalFrame(frame, title, true, w, h, resizable, false);
   }
 
   /**
@@ -776,10 +815,12 @@ public class Desktop extends jalview.jbgui.GDesktop implements
    *          height
    * @param resizable
    *          Allow resize
+   * @param ignoreMinSize
+   *          Do not set the default minimum size for frame
    */
   public static synchronized void addInternalFrame(
           final JInternalFrame frame, String title, boolean makeVisible,
-          int w, int h, boolean resizable)
+          int w, int h, boolean resizable, boolean ignoreMinSize)
   {
 
     // TODO: allow callers to determine X and Y position of frame (eg. via
@@ -805,6 +846,23 @@ public class Desktop extends jalview.jbgui.GDesktop implements
 
     openFrameCount++;
 
+    if (!ignoreMinSize)
+    {
+      frame.setMinimumSize(new Dimension(DEFAULT_MIN_WIDTH,
+              DEFAULT_MIN_HEIGHT));
+
+      // Set default dimension for Alignment Frame window.
+      // The Alignment Frame window could be added from a number of places,
+      // hence,
+      // I did this here in order not to miss out on any Alignment frame.
+      if (frame instanceof AlignFrame)
+      {
+        frame.setMinimumSize(new Dimension(ALIGN_FRAME_DEFAULT_MIN_WIDTH,
+                ALIGN_FRAME_DEFAULT_MIN_HEIGHT));
+      }
+    }
+
+
     frame.setVisible(makeVisible);
     frame.setClosable(true);
     frame.setResizable(resizable);
@@ -1531,8 +1589,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
   @Override
   public void saveState_actionPerformed(ActionEvent e)
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "jvp", "Jalview Project");
+    JalviewFileChooser chooser = new JalviewFileChooser("jvp",
+            "Jalview Project");
 
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(MessageManager.getString("label.save_state"));
@@ -2185,8 +2243,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
   {
     if (v_client != null)
     {
-      JalviewFileChooser chooser = new JalviewFileChooser(
-              Cache.getProperty("LAST_DIRECTORY"), "vdj",// TODO: VAMSAS DOCUMENT EXTENSION is VDJ
+   // TODO: VAMSAS DOCUMENT EXTENSION is VDJ
+      JalviewFileChooser chooser = new JalviewFileChooser("vdj",
               "Vamsas Document");
 
       chooser.setFileView(new JalviewFileView());
@@ -2290,7 +2348,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
         {
           // check what the actual links are - if it's just the default don't
           // bother with the warning
-          Vector<String> links = Preferences.sequenceURLLinks;
+          List<String> links = Preferences.sequenceUrlLinks
+                  .getLinksForMenu();
 
           // only need to check links if there is one with a
           // SEQUENCE_ID which is not the default EMBL_EBI link
@@ -2300,7 +2359,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
           while (li.hasNext())
           {
             String link = li.next();
-            if (link.contains(SEQUENCE_ID) && !link.equals(EMBLEBI_STRING))
+            if (link.contains(SEQUENCE_ID)
+                    && !link.equals(UrlConstants.DEFAULT_STRING))
             {
               check = true;
               int barPos = link.indexOf("|");
@@ -2462,8 +2522,6 @@ public class Desktop extends jalview.jbgui.GDesktop implements
 
   }
 
-  protected JMenuItem groovyShell;
-
   /**
    * Accessor method to quickly get all the AlignmentFrames loaded.
    * 
@@ -2549,6 +2607,7 @@ public class Desktop extends jalview.jbgui.GDesktop implements
   /**
    * Add Groovy Support to Jalview
    */
+  @Override
   public void groovyShell_actionPerformed()
   {
     try
@@ -2824,13 +2883,8 @@ public class Desktop extends jalview.jbgui.GDesktop implements
 
     if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
     {
-      if (jalview.ws.jws2.Jws2Discoverer.getDiscoverer().isRunning())
-      {
-        jalview.ws.jws2.Jws2Discoverer.getDiscoverer().setAborted(true);
-      }
       t2 = jalview.ws.jws2.Jws2Discoverer.getDiscoverer().startDiscoverer(
               changeSupport);
-
     }
     Thread t3 = null;
     {
@@ -3368,4 +3422,14 @@ public class Desktop extends jalview.jbgui.GDesktop implements
       }
     }
   }
+
+  /**
+   * Sets the Preferences property for experimental features to True or False
+   * depending on the state of the controlling menu item
+   */
+  @Override
+  protected void showExperimental_actionPerformed(boolean selected)
+  {
+    Cache.setProperty(EXPERIMENTAL_FEATURES, Boolean.toString(selected));
+  }
 }
index ae911ed..ac56590 100644 (file)
@@ -25,8 +25,9 @@ import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.io.FeaturesFile;
 import jalview.schemes.FeatureColour;
-import jalview.schemes.UserColourScheme;
+import jalview.util.ColorUtils;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -42,17 +43,19 @@ import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.List;
 
 import javax.swing.JColorChooser;
 import javax.swing.JComboBox;
 import javax.swing.JLabel;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JSpinner;
 import javax.swing.JTextArea;
 import javax.swing.JTextField;
 import javax.swing.SwingConstants;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
 
 /**
  * DOCUMENT ME!
@@ -61,56 +64,116 @@ import javax.swing.SwingConstants;
  * @version $Revision$
  */
 public class FeatureRenderer extends
-        jalview.renderer.seqfeatures.FeatureRenderer implements
-        jalview.api.FeatureRenderer
+        jalview.renderer.seqfeatures.FeatureRenderer
 {
+  /*
+   * defaults for creating a new feature are the last created
+   * feature type and group
+   */
+  static String lastFeatureAdded = "feature_1";
+
+  static String lastFeatureGroupAdded = "Jalview";
+
   Color resBoxColour;
 
   AlignmentPanel ap;
 
   /**
-   * Creates a new FeatureRenderer object.
+   * Creates a new FeatureRenderer object
    * 
-   * @param av
-   *          DOCUMENT ME!
+   * @param alignPanel
    */
-  public FeatureRenderer(AlignmentPanel ap)
+  public FeatureRenderer(AlignmentPanel alignPanel)
   {
-    super(ap.av);
-    this.ap = ap;
-    if (ap != null && ap.getSeqPanel() != null
-            && ap.getSeqPanel().seqCanvas != null
-            && ap.getSeqPanel().seqCanvas.fr != null)
+    super(alignPanel.av);
+    this.ap = alignPanel;
+    if (alignPanel.getSeqPanel() != null
+            && alignPanel.getSeqPanel().seqCanvas != null
+            && alignPanel.getSeqPanel().seqCanvas.fr != null)
     {
-      transferSettings(ap.getSeqPanel().seqCanvas.fr);
+      transferSettings(alignPanel.getSeqPanel().seqCanvas.fr);
     }
   }
 
-  // // /////////////
-  // // Feature Editing Dialog
-  // // Will be refactored in next release.
-
-  static String lastFeatureAdded;
-
-  static String lastFeatureGroupAdded;
-
-  static String lastDescriptionAdded;
-
   FeatureColourI oldcol, fcol;
 
   int featureIndex = 0;
 
-  boolean amendFeatures(final SequenceI[] sequences,
-          final SequenceFeature[] features, boolean newFeatures,
-          final AlignmentPanel ap)
+  /**
+   * Presents a dialog allowing the user to add new features, or amend or delete
+   * existing features. Currently this can be on
+   * <ul>
+   * <li>double-click on a sequence - Amend/Delete features at position</li>
+   * <li>Create sequence feature from pop-up menu on selected region</li>
+   * <li>Create features for pattern matches from Find</li>
+   * </ul>
+   * If the supplied feature type is null, show (and update on confirm) the type
+   * and group of the last new feature created (with initial defaults of
+   * "feature_1" and "Jalview").
+   * 
+   * @param sequences
+   *          the sequences features are to be created on (if creating
+   *          features), or a single sequence (if amending features)
+   * @param features
+   *          the current features at the position (if amending), or template
+   *          new feature(s) with start/end position set (if creating)
+   * @param create
+   *          true to create features, false to amend or delete
+   * @param alignPanel
+   * @return
+   */
+  protected boolean amendFeatures(final List<SequenceI> sequences,
+          final List<SequenceFeature> features, boolean create,
+          final AlignmentPanel alignPanel)
   {
-
     featureIndex = 0;
 
-    final JPanel bigPanel = new JPanel(new BorderLayout());
-    final JComboBox overlaps;
+    final JPanel mainPanel = new JPanel(new BorderLayout());
+
     final JTextField name = new JTextField(25);
-    final JTextField source = new JTextField(25);
+    name.getDocument().addDocumentListener(new DocumentListener()
+    {
+      @Override
+      public void insertUpdate(DocumentEvent e)
+      {
+        warnIfTypeHidden(mainPanel, name.getText());
+      }
+
+      @Override
+      public void removeUpdate(DocumentEvent e)
+      {
+        warnIfTypeHidden(mainPanel, name.getText());
+      }
+
+      @Override
+      public void changedUpdate(DocumentEvent e)
+      {
+        warnIfTypeHidden(mainPanel, name.getText());
+      }
+    });
+
+    final JTextField group = new JTextField(25);
+    group.getDocument().addDocumentListener(new DocumentListener()
+    {
+      @Override
+      public void insertUpdate(DocumentEvent e)
+      {
+        warnIfGroupHidden(mainPanel, group.getText());
+      }
+
+      @Override
+      public void removeUpdate(DocumentEvent e)
+      {
+        warnIfGroupHidden(mainPanel, group.getText());
+      }
+
+      @Override
+      public void changedUpdate(DocumentEvent e)
+      {
+        warnIfGroupHidden(mainPanel, group.getText());
+      }
+    });
+
     final JTextArea description = new JTextArea(3, 25);
     final JSpinner start = new JSpinner();
     final JSpinner end = new JSpinner();
@@ -136,14 +199,15 @@ public class FeatureRenderer extends
           if (col != null)
           {
             fcol = new FeatureColour(col);
-            updateColourButton(bigPanel, colour, new FeatureColour(col));
+            updateColourButton(mainPanel, colour, fcol);
           }
         }
         else
         {
           if (fcc == null)
           {
-            final String type = features[featureIndex].getType();
+            final String ft = features.get(featureIndex).getType();
+            final String type = ft == null ? lastFeatureAdded : ft;
             fcc = new FeatureColourChooser(me, type);
             fcc.setRequestFocusEnabled(true);
             fcc.requestFocus();
@@ -157,7 +221,7 @@ public class FeatureRenderer extends
                 fcol = fcc.getLastColour();
                 fcc = null;
                 setColour(type, fcol);
-                updateColourButton(bigPanel, colour, fcol);
+                updateColourButton(mainPanel, colour, fcol);
               }
             });
 
@@ -165,26 +229,27 @@ public class FeatureRenderer extends
         }
       }
     });
-    JPanel tmp = new JPanel();
-    JPanel panel = new JPanel(new GridLayout(3, 1));
+    JPanel gridPanel = new JPanel(new GridLayout(3, 1));
 
-    // /////////////////////////////////////
-    // /MULTIPLE FEATURES AT SELECTED RESIDUE
-    if (!newFeatures && features.length > 1)
+    if (!create && features.size() > 1)
     {
-      panel = new JPanel(new GridLayout(4, 1));
-      tmp = new JPanel();
-      tmp.add(new JLabel(MessageManager.getString("label.select_feature")
+      /*
+       * more than one feature at selected position - add a drop-down
+       * to choose the feature to amend
+       */
+      gridPanel = new JPanel(new GridLayout(4, 1));
+      JPanel choosePanel = new JPanel();
+      choosePanel.add(new JLabel(MessageManager
+              .getString("label.select_feature")
               + ":"));
-      overlaps = new JComboBox();
-      for (int i = 0; i < features.length; i++)
+      final JComboBox<String> overlaps = new JComboBox<String>();
+      for (SequenceFeature sf : features)
       {
-        overlaps.addItem(features[i].getType() + "/"
-                + features[i].getBegin() + "-" + features[i].getEnd()
-                + " (" + features[i].getFeatureGroup() + ")");
+        String text = sf.getType() + "/" + sf.getBegin() + "-"
+                + sf.getEnd() + " (" + sf.getFeatureGroup() + ")";
+        overlaps.addItem(text);
       }
-
-      tmp.add(overlaps);
+      choosePanel.add(overlaps);
 
       overlaps.addItemListener(new ItemListener()
       {
@@ -195,52 +260,51 @@ public class FeatureRenderer extends
           if (index != -1)
           {
             featureIndex = index;
-            name.setText(features[index].getType());
-            description.setText(features[index].getDescription());
-            source.setText(features[index].getFeatureGroup());
-            start.setValue(new Integer(features[index].getBegin()));
-            end.setValue(new Integer(features[index].getEnd()));
+            SequenceFeature sf = features.get(index);
+            name.setText(sf.getType());
+            description.setText(sf.getDescription());
+            group.setText(sf.getFeatureGroup());
+            start.setValue(new Integer(sf.getBegin()));
+            end.setValue(new Integer(sf.getEnd()));
 
             SearchResultsI highlight = new SearchResults();
-            highlight.addResult(sequences[0], features[index].getBegin(),
-                    features[index].getEnd());
+            highlight.addResult(sequences.get(0), sf.getBegin(),
+                    sf.getEnd());
 
-            ap.getSeqPanel().seqCanvas.highlightSearchResults(highlight);
+            alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(highlight);
 
           }
           FeatureColourI col = getFeatureStyle(name.getText());
           if (col == null)
           {
-            col = new FeatureColour(UserColourScheme
+            col = new FeatureColour(ColorUtils
                     .createColourFromName(name.getText()));
           }
           oldcol = fcol = col;
-          updateColourButton(bigPanel, colour, col);
+          updateColourButton(mainPanel, colour, col);
         }
       });
 
-      panel.add(tmp);
+      gridPanel.add(choosePanel);
     }
-    // ////////
-    // ////////////////////////////////////
 
-    tmp = new JPanel();
-    panel.add(tmp);
-    tmp.add(new JLabel(MessageManager.getString("label.name:"),
+    JPanel namePanel = new JPanel();
+    gridPanel.add(namePanel);
+    namePanel.add(new JLabel(MessageManager.getString("label.name:"),
             JLabel.RIGHT));
-    tmp.add(name);
+    namePanel.add(name);
 
-    tmp = new JPanel();
-    panel.add(tmp);
-    tmp.add(new JLabel(MessageManager.getString("label.group:"),
+    JPanel groupPanel = new JPanel();
+    gridPanel.add(groupPanel);
+    groupPanel.add(new JLabel(MessageManager.getString("label.group:"),
             JLabel.RIGHT));
-    tmp.add(source);
+    groupPanel.add(group);
 
-    tmp = new JPanel();
-    panel.add(tmp);
-    tmp.add(new JLabel(MessageManager.getString("label.colour"),
+    JPanel colourPanel = new JPanel();
+    gridPanel.add(colourPanel);
+    colourPanel.add(new JLabel(MessageManager.getString("label.colour"),
             JLabel.RIGHT));
-    tmp.add(colour);
+    colourPanel.add(colour);
     colour.setPreferredSize(new Dimension(150, 15));
     colour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 9));
     colour.setForeground(Color.black);
@@ -248,163 +312,175 @@ public class FeatureRenderer extends
     colour.setVerticalAlignment(SwingConstants.CENTER);
     colour.setHorizontalTextPosition(SwingConstants.CENTER);
     colour.setVerticalTextPosition(SwingConstants.CENTER);
-    bigPanel.add(panel, BorderLayout.NORTH);
+    mainPanel.add(gridPanel, BorderLayout.NORTH);
 
-    panel = new JPanel();
-    panel.add(new JLabel(MessageManager.getString("label.description:"),
+    JPanel descriptionPanel = new JPanel();
+    descriptionPanel.add(new JLabel(MessageManager
+            .getString("label.description:"),
             JLabel.RIGHT));
     description.setFont(JvSwingUtils.getTextAreaFont());
     description.setLineWrap(true);
-    panel.add(new JScrollPane(description));
+    descriptionPanel.add(new JScrollPane(description));
 
-    if (!newFeatures)
+    if (!create)
     {
-      bigPanel.add(panel, BorderLayout.SOUTH);
+      mainPanel.add(descriptionPanel, BorderLayout.SOUTH);
 
-      panel = new JPanel();
-      panel.add(new JLabel(MessageManager.getString("label.start"),
+      JPanel startEndPanel = new JPanel();
+      startEndPanel.add(new JLabel(MessageManager.getString("label.start"),
               JLabel.RIGHT));
-      panel.add(start);
-      panel.add(new JLabel(MessageManager.getString("label.end"),
+      startEndPanel.add(start);
+      startEndPanel.add(new JLabel(MessageManager.getString("label.end"),
               JLabel.RIGHT));
-      panel.add(end);
-      bigPanel.add(panel, BorderLayout.CENTER);
-    }
-    else
-    {
-      bigPanel.add(panel, BorderLayout.CENTER);
-    }
-
-    if (lastFeatureAdded == null)
-    {
-      if (features[0].type != null)
-      {
-        lastFeatureAdded = features[0].type;
-      }
-      else
-      {
-        lastFeatureAdded = "feature_1";
-      }
-    }
-
-    if (lastFeatureGroupAdded == null)
-    {
-      if (features[0].featureGroup != null)
-      {
-        lastFeatureGroupAdded = features[0].featureGroup;
-      }
-      else
-      {
-        lastFeatureGroupAdded = "Jalview";
-      }
-    }
-
-    if (newFeatures)
-    {
-      name.setText(lastFeatureAdded);
-      source.setText(lastFeatureGroupAdded);
+      startEndPanel.add(end);
+      mainPanel.add(startEndPanel, BorderLayout.CENTER);
     }
     else
     {
-      name.setText(features[0].getType());
-      source.setText(features[0].getFeatureGroup());
+      mainPanel.add(descriptionPanel, BorderLayout.CENTER);
     }
 
-    start.setValue(new Integer(features[0].getBegin()));
-    end.setValue(new Integer(features[0].getEnd()));
-    description.setText(features[0].getDescription());
-    updateColourButton(bigPanel, colour,
-            (oldcol = fcol = getFeatureStyle(name.getText())));
+    /*
+     * default feature type and group to that of the first feature supplied,
+     * or to the last feature created if not supplied (null value) 
+     */
+    SequenceFeature firstFeature = features.get(0);
+    boolean useLastDefaults = firstFeature.getType() == null;
+    final String featureType = useLastDefaults ? lastFeatureAdded
+            : firstFeature.getType();
+    final String featureGroup = useLastDefaults ? lastFeatureGroupAdded
+            : firstFeature.getFeatureGroup();
+    name.setText(featureType);
+    group.setText(featureGroup);
+
+    start.setValue(new Integer(firstFeature.getBegin()));
+    end.setValue(new Integer(firstFeature.getEnd()));
+    description.setText(firstFeature.getDescription());
+    updateColourButton(mainPanel, colour,
+            (oldcol = fcol = getFeatureStyle(featureType)));
     Object[] options;
-    if (!newFeatures)
+    if (!create)
     {
-      options = new Object[] { "Amend", "Delete", "Cancel" };
+      options = new Object[] { MessageManager.getString("label.amend"),
+          MessageManager.getString("action.delete"),
+          MessageManager.getString("action.cancel") };
     }
     else
     {
-      options = new Object[] { "OK", "Cancel" };
+      options = new Object[] { MessageManager.getString("action.ok"),
+          MessageManager.getString("action.cancel") };
     }
 
-    String title = newFeatures ? MessageManager
+    String title = create ? MessageManager
             .getString("label.create_new_sequence_features")
             : MessageManager.formatMessage("label.amend_delete_features",
-                    new String[] { sequences[0].getName() });
+                    new String[] { sequences.get(0).getName() });
 
+    /*
+     * show the dialog
+     */
     int reply = JvOptionPane.showInternalOptionDialog(Desktop.desktop,
-            bigPanel, title, JvOptionPane.YES_NO_CANCEL_OPTION,
+            mainPanel, title, JvOptionPane.YES_NO_CANCEL_OPTION,
             JvOptionPane.QUESTION_MESSAGE, null, options,
             MessageManager.getString("action.ok"));
 
-    jalview.io.FeaturesFile ffile = new jalview.io.FeaturesFile();
+    FeaturesFile ffile = new FeaturesFile();
 
-    if (reply == JvOptionPane.OK_OPTION && name.getText().length() > 0)
+    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)
     {
-      // This ensures that the last sequence
-      // is refreshed and new features are rendered
-      lastSeq = null;
-      lastFeatureAdded = name.getText().trim();
-      lastFeatureGroupAdded = source.getText().trim();
-      lastDescriptionAdded = description.getText().replaceAll("\n", " ");
-      // TODO: determine if the null feature group is valid
-      if (lastFeatureGroupAdded.length() < 1)
+      /*
+       * update default values only if creating using default values
+       */
+      if (useLastDefaults)
       {
-        lastFeatureGroupAdded = null;
+        lastFeatureAdded = enteredType;
+        lastFeatureGroupAdded = enteredGroup;
+        // TODO: determine if the null feature group is valid
+        if (lastFeatureGroupAdded.length() < 1)
+        {
+          lastFeatureGroupAdded = null;
+        }
       }
     }
 
-    if (!newFeatures)
+    if (!create)
     {
-      SequenceFeature sf = features[featureIndex];
+      SequenceFeature sf = features.get(featureIndex);
 
       if (reply == JvOptionPane.NO_OPTION)
       {
-        sequences[0].getDatasetSequence().deleteFeature(sf);
+        /*
+         * NO_OPTION corresponds to the Delete button
+         */
+        sequences.get(0).getDatasetSequence().deleteFeature(sf);
+        // update Feature Settings for removal of feature / group
+        featuresAdded();
       }
       else if (reply == JvOptionPane.YES_OPTION)
       {
-        sf.type = lastFeatureAdded;
-        sf.featureGroup = lastFeatureGroupAdded;
-        sf.description = lastDescriptionAdded;
-
-        setColour(sf.type, fcol);
-        getFeaturesDisplayed().setVisible(sf.type);
-
+        /*
+         * YES_OPTION corresponds to the Amend button
+         * 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
+         */
+        boolean refreshSettings = (!featureType.equals(enteredType) || !featureGroup
+                .equals(enteredGroup));
+        refreshSettings |= (fcol != oldcol);
+        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, newBegin, newEnd,
+                enteredGroup, sf.getScore());
+        sf.setDescription(enteredDescription);
+        ffile.parseDescriptionHTML(newSf, false);
+        // amend features dialog only updates one sequence at a time
+        sequences.get(0).addSequenceFeature(newSf);
+
+        if (refreshSettings)
+        {
+          featuresAdded();
+        }
       }
     }
     else
     // NEW FEATURES ADDED
     {
-      if (reply == JvOptionPane.OK_OPTION && lastFeatureAdded.length() > 0)
+      if (reply == JvOptionPane.OK_OPTION && enteredType.length() > 0)
       {
-        for (int i = 0; i < sequences.length; i++)
+        for (int i = 0; i < sequences.size(); i++)
         {
-          features[i].type = lastFeatureAdded;
-          // fix for JAL-1538 - always set feature group here
-          features[i].featureGroup = lastFeatureGroupAdded;
-          features[i].description = lastDescriptionAdded;
-          sequences[i].addSequenceFeature(features[i]);
-          ffile.parseDescriptionHTML(features[i], false);
+          SequenceFeature sf = features.get(i);
+          SequenceFeature sf2 = new SequenceFeature(enteredType,
+                  enteredDescription, sf.getBegin(), sf.getEnd(),
+                  enteredGroup);
+          ffile.parseDescriptionHTML(sf2, false);
+          sequences.get(i).addSequenceFeature(sf2);
         }
 
-        if (lastFeatureGroupAdded != null)
-        {
-          setGroupVisibility(lastFeatureGroupAdded, true);
-        }
-        setColour(lastFeatureAdded, fcol);
-        setVisible(lastFeatureAdded);
+        setColour(enteredType, fcol);
 
-        findAllFeatures(false);
+        featuresAdded();
 
-        ap.paintAlignment(true);
+        alignPanel.paintAlignment(true);
 
         return true;
       }
@@ -414,12 +490,48 @@ public class FeatureRenderer extends
       }
     }
 
-    ap.paintAlignment(true);
+    alignPanel.paintAlignment(true);
 
     return true;
   }
 
   /**
+   * Show a warning message if the entered type is one that is currently hidden
+   * 
+   * @param panel
+   * @param type
+   */
+  protected void warnIfTypeHidden(JPanel panel, String type)
+  {
+    if (getRenderOrder().contains(type))
+    {
+      if (!showFeatureOfType(type))
+      {
+        String msg = MessageManager.formatMessage("label.warning_hidden",
+                MessageManager.getString("label.feature_type"), type);
+        JvOptionPane.showMessageDialog(panel, msg, "",
+                JvOptionPane.OK_OPTION);
+      }
+    }
+  }
+
+  /**
+   * Show a warning message if the entered group is one that is currently hidden
+   * 
+   * @param panel
+   * @param group
+   */
+  protected void warnIfGroupHidden(JPanel panel, String group)
+  {
+    if (featureGroups.containsKey(group) && !featureGroups.get(group))
+    {
+      String msg = MessageManager.formatMessage("label.warning_hidden",
+              MessageManager.getString("label.group"), group);
+      JvOptionPane.showMessageDialog(panel, msg, "", JvOptionPane.OK_OPTION);
+    }
+  }
+
+  /**
    * update the amend feature button dependent on the given style
    * 
    * @param bigPanel
index bb5f13c..45b6b0d 100644 (file)
@@ -23,7 +23,7 @@ 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.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
@@ -40,6 +40,7 @@ import jalview.ws.dbsources.das.api.jalviewSourceI;
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
+import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
@@ -60,6 +61,7 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
@@ -126,6 +128,15 @@ public class FeatureSettings extends JPanel implements
 
   JPanel transPanel = new JPanel(new GridLayout(1, 2));
 
+  private static final int MIN_WIDTH = 400;
+
+  private static final int MIN_HEIGHT = 400;
+
+  /**
+   * Constructor
+   * 
+   * @param af
+   */
   public FeatureSettings(AlignFrame af)
   {
     this.af = af;
@@ -278,6 +289,7 @@ public class FeatureSettings extends JPanel implements
               MessageManager.getString("label.sequence_feature_settings"),
               400, 450);
     }
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
     frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
     {
@@ -463,50 +475,26 @@ public class FeatureSettings extends JPanel implements
   private boolean handlingUpdate = false;
 
   /**
-   * contains a float[3] for each feature type string. created by setTableData
+   * holds {featureCount, totalExtent} for each feature type
    */
   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<String>();
+    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++;
       }
     }
 
@@ -525,27 +513,15 @@ public class FeatureSettings extends JPanel implements
   {
     boolean visible = fr.checkGroupVisibility(group, true);
 
-    if (groupPanel == null)
-    {
-      groupPanel = new JPanel();
-    }
-
-    boolean alreadyAdded = false;
     for (int g = 0; g < groupPanel.getComponentCount(); g++)
     {
       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
       {
-        alreadyAdded = true;
         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
-        break;
+        return visible;
       }
     }
 
-    if (alreadyAdded)
-    {
-
-      return visible;
-    }
     final String grp = group;
     final JCheckBox check = new JCheckBox(group, visible);
     check.setFont(new Font("Serif", Font.BOLD, 12));
@@ -572,7 +548,7 @@ public class FeatureSettings extends JPanel implements
 
   synchronized void resetTable(String[] groupChanged)
   {
-    if (resettingTable == true)
+    if (resettingTable)
     {
       return;
     }
@@ -580,69 +556,59 @@ public class FeatureSettings extends JPanel implements
     typeWidth = new Hashtable<String, float[]>();
     // 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>();
-
-    // Find out which features should be visible depending on which groups
-    // are selected / deselected
-    // and recompute average width ordering
+
+    Set<String> displayableTypes = new HashSet<String>();
+    Set<String> foundGroups = new HashSet<String>();
+
+    /*
+     * 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<String>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-
-        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());
+          visibleGroups.add(group);
         }
-        avWidth[0]++;
-        if (tmpfeatures[index].getBegin() > tmpfeatures[index].getEnd())
-        {
-          avWidth[1] += 1 + tmpfeatures[index].getBegin()
-                  - tmpfeatures[index].getEnd();
-        }
-        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()][3];
     int dataIndex = 0;
 
     if (fr.hasRenderOrder())
@@ -658,9 +624,9 @@ public class FeatureSettings extends JPanel implements
       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;
         }
@@ -670,16 +636,17 @@ public class FeatureSettings extends JPanel implements
         data[dataIndex][2] = 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();
+      String type = displayableTypes.iterator().next();
       data[dataIndex][0] = type;
 
       data[dataIndex][1] = fr.getFeatureStyle(type);
@@ -692,6 +659,7 @@ public class FeatureSettings extends JPanel implements
 
       data[dataIndex][2] = new Boolean(true);
       dataIndex++;
+      displayableTypes.remove(type);
     }
 
     if (originalData == null)
@@ -702,24 +670,105 @@ public class FeatureSettings extends JPanel implements
         System.arraycopy(data[i], 0, originalData[i], 0, 3);
       }
     }
+    else
+    {
+      updateOriginalData(data);
+    }
 
     table.setModel(new FeatureTableModel(data));
     table.getColumnModel().getColumn(0).setPreferredWidth(200);
 
-    if (groupPanel != null)
-    {
-      groupPanel.setLayout(new GridLayout(
-              fr.getFeatureGroupsSize() / 4 + 1, 4));
-
-      groupPanel.validate();
-      bigPanel.add(groupPanel, BorderLayout.NORTH);
-    }
+    groupPanel.setLayout(new GridLayout(fr.getFeatureGroupsSize() / 4 + 1,
+            4));
+    pruneGroups(foundGroups);
+    groupPanel.validate();
 
     updateFeatureRenderer(data, groupChanged != null);
     resettingTable = false;
   }
 
   /**
+   * 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>
+   * </ul>
+   * 
+   * @param foundData
+   */
+  protected void updateOriginalData(Object[][] foundData)
+  {
+    // todo LinkedHashMap instead of Object[][] would be nice
+
+    Object[][] currentData = ((FeatureTableModel) table.getModel())
+            .getData();
+    for (Object[] row : foundData)
+    {
+      String type = (String) row[0];
+      boolean found = false;
+      for (Object[] current : currentData)
+      {
+        if (type.equals(current[0]))
+        {
+          found = true;
+          /*
+           * currently dependent on object equality here;
+           * really need an equals method on FeatureColour
+           */
+          if (!row[1].equals(current[1]))
+          {
+            /*
+             * feature colour has changed externally - update originalData
+             */
+            for (Object[] original : originalData)
+            {
+              if (type.equals(original[0]))
+              {
+                original[1] = row[1];
+                break;
+              }
+            }
+          }
+          break;
+        }
+      }
+      if (!found)
+      {
+        /*
+         * new feature detected - add to original data (on top)
+         */
+        Object[][] newData = new Object[originalData.length + 1][3];
+        for (int i = 0; i < originalData.length; i++)
+        {
+          System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
+        }
+        newData[0] = row;
+        originalData = newData;
+      }
+    }
+  }
+
+  /**
+   * 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.
+   * 
+   * @param foundGroups
+   */
+  protected void pruneGroups(Set<String> foundGroups)
+  {
+    for (int g = 0; g < groupPanel.getComponentCount(); g++)
+    {
+      JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g);
+      if (!foundGroups.contains(checkbox.getText()))
+      {
+        groupPanel.remove(checkbox);
+      }
+    }
+  }
+
+  /**
    * reorder data based on the featureRenderers global priority list.
    * 
    * @param data
@@ -748,8 +797,7 @@ public class FeatureSettings extends JPanel implements
 
   void load()
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "fc",
+    JalviewFileChooser chooser = new JalviewFileChooser("fc",
             "Sequence Feature Colours");
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(MessageManager
@@ -841,8 +889,7 @@ public class FeatureSettings extends JPanel implements
 
   void save()
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "fc",
+    JalviewFileChooser chooser = new JalviewFileChooser("fc",
             "Sequence Feature Colours");
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(MessageManager
@@ -1061,6 +1108,10 @@ public class FeatureSettings extends JPanel implements
     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()
index af23ceb..5c917ae 100755 (executable)
@@ -28,8 +28,11 @@ import jalview.jbgui.GFinder;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
 
+import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Vector;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
@@ -38,8 +41,8 @@ import javax.swing.AbstractAction;
 import javax.swing.JComponent;
 import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
-import javax.swing.JOptionPane;
 import javax.swing.KeyStroke;
+import javax.swing.event.InternalFrameEvent;
 
 /**
  * Performs the menu option for searching the alignment, for the next or all
@@ -54,14 +57,18 @@ import javax.swing.KeyStroke;
  */
 public class Finder extends GFinder
 {
-  private static final int HEIGHT = 110;
+  private static final int MY_HEIGHT = 120;
 
-  private static final int WIDTH = 340;
+  private static final int MY_WIDTH = 400;
 
   AlignmentViewport av;
 
   AlignmentPanel ap;
 
+  private static final int MIN_WIDTH = 350;
+
+  private static final int MIN_HEIGHT = 120;
+
   JInternalFrame frame;
 
   int seqIndex = 0;
@@ -94,11 +101,19 @@ public class Finder extends GFinder
     frame = new JInternalFrame();
     frame.setContentPane(this);
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
+    frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosing(InternalFrameEvent e)
+      {
+        closeAction();
+      }
+    });
     addEscapeHandler();
     Desktop.addInternalFrame(frame, MessageManager.getString("label.find"),
-            WIDTH, HEIGHT);
-
-    textfield.requestFocus();
+            MY_WIDTH, MY_HEIGHT);
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
+    searchBox.requestFocus();
   }
 
   /**
@@ -113,19 +128,11 @@ public class Finder extends GFinder
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        escapeActionPerformed();
+        closeAction();
       }
     });
   }
 
-  /**
-   * Close the panel on Escape key press
-   */
-  protected void escapeActionPerformed()
-  {
-    setVisible(false);
-    frame.dispose();
-  }
 
   /**
    * Performs the 'Find Next' action.
@@ -185,11 +192,11 @@ public class Finder extends GFinder
     JInternalFrame[] frames = Desktop.desktop.getAllFrames();
     for (int f = 0; f < frames.length; f++)
     {
-      JInternalFrame frame = frames[f];
-      if (frame != null && frame instanceof AlignFrame)
+      JInternalFrame alignFrame = frames[f];
+      if (alignFrame != null && alignFrame instanceof AlignFrame)
       {
-        av = ((AlignFrame) frame).viewport;
-        ap = ((AlignFrame) frame).alignPanel;
+        av = ((AlignFrame) alignFrame).viewport;
+        ap = ((AlignFrame) alignFrame).alignPanel;
         return true;
       }
     }
@@ -197,32 +204,37 @@ public class Finder extends GFinder
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Opens a dialog that allows the user to create sequence features for the
+   * find match results.
    */
   @Override
-  public void createNewGroup_actionPerformed(ActionEvent e)
+  public void createFeatures_actionPerformed()
   {
-    SequenceI[] seqs = new SequenceI[searchResults.getSize()];
-    SequenceFeature[] features = new SequenceFeature[searchResults
-            .getSize()];
+    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+
+    String searchString = searchBox.getEditor().getItem().toString().trim();
+    String desc = "Search Results";
 
-    int i = 0;
+    /*
+     * assemble dataset sequences, and template new sequence features,
+     * for the amend features dialog
+     */
     for (SearchResultMatchI match : searchResults.getResults())
     {
-      seqs[i] = match.getSequence().getDatasetSequence();
-
-      features[i] = new SequenceFeature(textfield.getText().trim(),
-              "Search Results", null, match.getStart(), match.getEnd(),
-              "Search Results");
-      i++;
+      seqs.add(match.getSequence().getDatasetSequence());
+      features.add(new SequenceFeature(searchString, desc,
+              match
+              .getStart(), match.getEnd(), desc));
     }
 
     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
             features, true, ap))
     {
+      /*
+       * ensure feature display is turned on to show the new features,
+       * and remove them as highlighted regions
+       */
       ap.alignFrame.showSeqFeatures.setSelected(true);
       av.setShowSequenceFeatures(true);
       ap.highlightSearchResults(null);
@@ -233,13 +245,13 @@ public class Finder extends GFinder
    * Search the alignment for the next or all matches. If 'all matches', a
    * dialog is shown with the number of sequence ids and subsequences matched.
    * 
-   * @param findAll
+   * @param doFindAll
    */
-  void doSearch(boolean findAll)
+  void doSearch(boolean doFindAll)
   {
-    createNewGroup.setEnabled(false);
+    createFeatures.setEnabled(false);
 
-    String searchString = textfield.getText().trim();
+    String searchString = searchBox.getUserInput().trim();
 
     if (isInvalidSearchString(searchString))
     {
@@ -254,7 +266,7 @@ public class Finder extends GFinder
     finder.setCaseSensitive(caseSensitive.isSelected());
     finder.setIncludeDescription(searchDescription.isSelected());
 
-    finder.setFindAll(findAll);
+    finder.setFindAll(doFindAll);
 
     finder.find(searchString); // returns true if anything was actually found
 
@@ -279,7 +291,7 @@ public class Finder extends GFinder
     if (searchResults.getSize() > 0)
     {
       haveResults = true;
-      createNewGroup.setEnabled(true);
+      createFeatures.setEnabled(true);
     }
     else
     {
@@ -301,7 +313,7 @@ public class Finder extends GFinder
     }
     else
     {
-      if (findAll)
+      if (doFindAll)
       {
         // then we report the matches that were found
         String message = (idMatch.size() > 0) ? "" + idMatch.size()
@@ -321,7 +333,7 @@ public class Finder extends GFinder
         seqIndex = 0;
       }
     }
-
+    searchBox.updateCache();
   }
 
   /**
@@ -373,4 +385,15 @@ public class Finder extends GFinder
     }
     return error;
   }
+
+  protected void closeAction()
+  {
+    frame.setVisible(false);
+    frame.dispose();
+    searchBox.persistCache();
+    if (getFocusedViewport())
+    {
+      ap.alignFrame.requestFocus();
+    }
+  }
 }
index 8220aea..06f29e9 100755 (executable)
@@ -26,12 +26,10 @@ import jalview.util.MessageManager;
 
 import java.awt.Font;
 import java.awt.FontMetrics;
-import java.awt.event.ActionEvent;
 import java.awt.geom.Rectangle2D;
 
 import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
-import javax.swing.JOptionPane;
 
 /**
  * DOCUMENT ME!
@@ -50,8 +48,22 @@ public class FontChooser extends GFontChooser
    */
   Font oldFont;
 
+  /*
+   * The font on opening the dialog (to be restored on Cancel)
+   * on the other half of a split frame (if applicable)
+   */
+  Font oldComplementFont;
+
+  /*
+   * the state of 'scale protein as cDNA' on opening the dialog
+   */
   boolean oldProteinScale;
 
+  /*
+   * the state of 'same font for protein and cDNA' on opening the dialog
+   */
+  boolean oldMirrorFont;
+
   boolean init = true;
 
   JInternalFrame frame;
@@ -63,34 +75,37 @@ public class FontChooser extends GFontChooser
 
   private boolean lastSelMono = false;
 
+  private boolean oldSmoothFont;
+
+  private boolean oldComplementSmooth;
+
   /**
-   * Creates a new FontChooser object.
+   * Creates a new FontChooser for a tree panel
    * 
-   * @param ap
-   *          DOCUMENT ME!
+   * @param treePanel
    */
-  public FontChooser(TreePanel tp)
+  public FontChooser(TreePanel treePanel)
   {
-    this.tp = tp;
-    ap = tp.treeCanvas.ap;
-    oldFont = tp.getTreeFont();
+    this.tp = treePanel;
+    ap = treePanel.treeCanvas.ap;
+    oldFont = treePanel.getTreeFont();
     defaultButton.setVisible(false);
     smoothFont.setEnabled(false);
     init();
   }
 
   /**
-   * Creates a new FontChooser object.
+   * Creates a new FontChooser for an alignment panel
    * 
-   * @param ap
-   *          DOCUMENT ME!
+   * @param alignPanel
    */
-  public FontChooser(AlignmentPanel ap)
+  public FontChooser(AlignmentPanel alignPanel)
   {
-    oldFont = ap.av.getFont();
-    oldProteinScale = ap.av.isScaleProteinAsCdna();
-
-    this.ap = ap;
+    oldFont = alignPanel.av.getFont();
+    oldProteinScale = alignPanel.av.isScaleProteinAsCdna();
+    oldMirrorFont = alignPanel.av.isProteinFontAsCdna();
+    oldSmoothFont = alignPanel.av.antiAlias;
+    this.ap = alignPanel;
     init();
   }
 
@@ -103,14 +118,19 @@ public class FontChooser extends GFontChooser
 
     /*
      * Enable 'scale protein as cDNA' in a SplitFrame view. The selection is
-     * stored in the ViewStyle of both dna and protein Viewport
+     * stored in the ViewStyle of both dna and protein Viewport. Also enable
+     * checkbox for copy font changes to other half of split frame.
      */
-    scaleAsCdna.setEnabled(false);
-    if (ap.av.getCodingComplement() != null)
+    boolean inSplitFrame = ap.av.getCodingComplement() != null;
+    if (inSplitFrame)
     {
-      scaleAsCdna.setEnabled(true);
+      oldComplementFont = ((AlignViewport) ap.av.getCodingComplement())
+              .getFont();
+      oldComplementSmooth = ((AlignViewport) ap.av.getCodingComplement()).antiAlias;
       scaleAsCdna.setVisible(true);
       scaleAsCdna.setSelected(ap.av.isScaleProteinAsCdna());
+      fontAsCdna.setVisible(true);
+      fontAsCdna.setSelected(ap.av.isProteinFontAsCdna());
     }
 
     if (tp != null)
@@ -122,7 +142,7 @@ public class FontChooser extends GFontChooser
     else
     {
       Desktop.addInternalFrame(frame,
-              MessageManager.getString("action.change_font"), 380, 200,
+              MessageManager.getString("action.change_font"), 380, 220,
               false);
     }
 
@@ -158,11 +178,19 @@ public class FontChooser extends GFontChooser
   }
 
   @Override
-  public void smoothFont_actionPerformed(ActionEvent e)
+  protected void smoothFont_actionPerformed()
   {
     ap.av.antiAlias = smoothFont.isSelected();
     ap.getAnnotationPanel().image = null;
     ap.paintAlignment(true);
+    if (ap.av.getCodingComplement() != null && ap.av.isProteinFontAsCdna())
+    {
+      ((AlignViewport) ap.av.getCodingComplement()).antiAlias = ap.av.antiAlias;
+      SplitFrame sv = (SplitFrame) ap.alignFrame.getSplitViewContainer();
+      sv.adjustLayout();
+      sv.repaint();
+    }
+
   }
 
   /**
@@ -172,7 +200,7 @@ public class FontChooser extends GFontChooser
    *          DOCUMENT ME!
    */
   @Override
-  protected void ok_actionPerformed(ActionEvent e)
+  protected void ok_actionPerformed()
   {
     try
     {
@@ -197,26 +225,32 @@ public class FontChooser extends GFontChooser
    *          DOCUMENT ME!
    */
   @Override
-  protected void cancel_actionPerformed(ActionEvent e)
+  protected void cancel_actionPerformed()
   {
     if (ap != null)
     {
       ap.av.setFont(oldFont, true);
       ap.av.setScaleProteinAsCdna(oldProteinScale);
+      ap.av.setProteinFontAsCdna(oldMirrorFont);
+      ap.av.antiAlias = oldSmoothFont;
       ap.paintAlignment(true);
-      if (scaleAsCdna.isEnabled())
+
+      if (scaleAsCdna.isVisible() && scaleAsCdna.isEnabled())
       {
-        ap.av.setScaleProteinAsCdna(oldProteinScale);
         ap.av.getCodingComplement().setScaleProteinAsCdna(oldProteinScale);
+        ap.av.getCodingComplement().setProteinFontAsCdna(oldMirrorFont);
+        ((AlignViewport) ap.av.getCodingComplement()).antiAlias = oldComplementSmooth;
+        ap.av.getCodingComplement().setFont(oldComplementFont, true);
+        SplitFrame splitFrame = (SplitFrame) ap.alignFrame
+                .getSplitViewContainer();
+        splitFrame.adjustLayout();
+        splitFrame.repaint();
       }
     }
     else if (tp != null)
     {
       tp.setTreeFont(oldFont);
     }
-    fontName.setSelectedItem(oldFont.getName());
-    fontSize.setSelectedItem(oldFont.getSize());
-    fontStyle.setSelectedIndex(oldFont.getStyle());
 
     try
     {
@@ -287,6 +321,29 @@ public class FontChooser extends GFontChooser
     {
       ap.av.setFont(newFont, true);
       ap.fontChanged();
+
+      /*
+       * adjust other half of split frame if any, if either same
+       * font, or proportionate scaling, is selected
+       */
+      if (fontAsCdna.isEnabled())
+      {
+        if (fontAsCdna.isSelected())
+        {
+          /*
+           * copy the font
+           */
+          ap.av.getCodingComplement().setFont(newFont, true);
+        }
+
+        /*
+         * adjust layout for font change / reset / sizing
+         */
+        SplitFrame splitFrame = (SplitFrame) ap.alignFrame
+                .getSplitViewContainer();
+        splitFrame.adjustLayout();
+        splitFrame.repaint();
+      }
     }
 
     monospaced.setSelected(mw == iw);
@@ -299,13 +356,10 @@ public class FontChooser extends GFontChooser
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Updates on change of selected font name
    */
   @Override
-  protected void fontName_actionPerformed(ActionEvent e)
+  protected void fontName_actionPerformed()
   {
     if (init)
     {
@@ -316,13 +370,10 @@ public class FontChooser extends GFontChooser
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Updates on change of selected font size
    */
   @Override
-  protected void fontSize_actionPerformed(ActionEvent e)
+  protected void fontSize_actionPerformed()
   {
     if (init)
     {
@@ -333,13 +384,10 @@ public class FontChooser extends GFontChooser
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Updates on change of selected font style
    */
   @Override
-  protected void fontStyle_actionPerformed(ActionEvent e)
+  protected void fontStyle_actionPerformed()
   {
     if (init)
     {
@@ -352,11 +400,9 @@ public class FontChooser extends GFontChooser
   /**
    * Make selected settings the defaults by storing them (via Cache class) in
    * the .jalview_properties file (the file is only written when Jalview exits)
-   * 
-   * @param e
    */
   @Override
-  public void defaultButton_actionPerformed(ActionEvent e)
+  public void defaultButton_actionPerformed()
   {
     Cache.setProperty("FONT_NAME", fontName.getSelectedItem().toString());
     Cache.setProperty("FONT_STYLE", fontStyle.getSelectedIndex() + "");
@@ -372,7 +418,7 @@ public class FontChooser extends GFontChooser
    * characters
    */
   @Override
-  protected void scaleAsCdna_actionPerformed(ActionEvent e)
+  protected void scaleAsCdna_actionPerformed()
   {
     ap.av.setScaleProteinAsCdna(scaleAsCdna.isSelected());
     ap.av.getCodingComplement().setScaleProteinAsCdna(
@@ -381,7 +427,28 @@ public class FontChooser extends GFontChooser
             .getSplitViewContainer();
     splitFrame.adjustLayout();
     splitFrame.repaint();
-    // ap.paintAlignment(true);
-    // TODO would like to repaint
+  }
+
+  /**
+   * Turn on/off mirroring of font across split frame. If turning on, also
+   * copies the current font across the split frame. If turning off, restores
+   * the other half of the split frame to its initial font.
+   */
+  @Override
+  protected void mirrorFonts_actionPerformed()
+  {
+    boolean selected = fontAsCdna.isSelected();
+    ap.av.setProteinFontAsCdna(selected);
+    ap.av.getCodingComplement().setProteinFontAsCdna(selected);
+
+    /*
+     * reset other half of split frame if turning option off
+     */
+    if (!selected)
+    {
+      ap.av.getCodingComplement().setFont(oldComplementFont, true);
+    }
+
+    changeFont();
   }
 }
index 37be8bc..5ce36cb 100755 (executable)
@@ -21,6 +21,8 @@
 package jalview.gui;
 
 import jalview.datamodel.SequenceI;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -30,6 +32,7 @@ import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
 import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
 import javax.swing.JPanel;
@@ -40,7 +43,7 @@ import javax.swing.JPanel;
  * @author $author$
  * @version $Revision$
  */
-public class IdCanvas extends JPanel
+public class IdCanvas extends JPanel implements ViewportListenerI
 {
   protected AlignViewport av;
 
@@ -79,6 +82,7 @@ public class IdCanvas extends JPanel
     setLayout(new BorderLayout());
     this.av = av;
     PaintRefresher.Register(this, av.getSequenceSetId());
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   /**
@@ -158,33 +162,35 @@ public class IdCanvas extends JPanel
       return;
     }
 
+    ViewportRanges ranges = av.getRanges();
+
     gg.copyArea(0, 0, getWidth(), imgHeight, 0,
             -vertical * av.getCharHeight());
 
-    int ss = av.startSeq;
-    int es = av.endSeq;
+    int ss = ranges.getStartSeq();
+    int es = ranges.getEndSeq();
     int transY = 0;
 
     if (vertical > 0) // scroll down
     {
       ss = es - vertical;
 
-      if (ss < av.startSeq)
+      if (ss < ranges.getStartSeq())
       { // ie scrolling too fast, more than a page at a time
-        ss = av.startSeq;
+        ss = ranges.getStartSeq();
       }
       else
       {
-        transY = imgHeight - (vertical * av.getCharHeight());
+        transY = imgHeight - ((vertical + 1) * av.getCharHeight());
       }
     }
-    else if (vertical < 0)
+    else if (vertical < 0) // scroll up
     {
       es = ss - vertical;
 
-      if (es > av.endSeq)
+      if (es > ranges.getEndSeq())
       {
-        es = av.endSeq;
+        es = ranges.getEndSeq();
       }
     }
 
@@ -240,7 +246,7 @@ public class IdCanvas extends JPanel
     gg.setColor(Color.white);
     gg.fillRect(0, 0, getWidth(), imgHeight);
 
-    drawIds(av.getStartSeq(), av.endSeq);
+    drawIds(av.getRanges().getStartSeq(), av.getRanges().getEndSeq());
 
     g.drawImage(image, 0, 0, this);
   }
@@ -287,7 +293,8 @@ public class IdCanvas extends JPanel
 
       if (av.hasHiddenColumns())
       {
-        maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+        maxwidth = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(maxwidth) - 1;
       }
 
       int annotationHeight = 0;
@@ -314,10 +321,11 @@ public class IdCanvas extends JPanel
 
       int cHeight = alheight * av.getCharHeight() + hgap + annotationHeight;
 
-      int rowSize = av.getEndRes() - av.getStartRes();
+      int rowSize = av.getRanges().getEndRes()
+              - av.getRanges().getStartRes();
 
       // Draw the rest of the panels
-      for (int ypos = hgap, row = av.startRes; (ypos <= getHeight())
+      for (int ypos = hgap, row = av.getRanges().getStartRes(); (ypos <= getHeight())
               && (row < maxwidth); ypos += cHeight, row += rowSize)
       {
         for (int i = starty; i < alheight; i++)
@@ -354,7 +362,7 @@ public class IdCanvas extends JPanel
 
       SequenceI sequence;
       // Now draw the id strings
-      for (int i = starty; i < endy; i++)
+      for (int i = starty; i <= endy; i++)
       {
         sequence = av.getAlignment().getSequenceAt(i);
 
@@ -511,4 +519,15 @@ public class IdCanvas extends JPanel
   {
     this.idfont = idfont;
   }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport range changes (e.g. alignment panel was scrolled)
+    if (evt.getPropertyName().equals("startseq")
+            || evt.getPropertyName().equals("endseq"))
+    {
+      fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
+    }
+  }
 }
index 59d12d9..29bb522 100755 (executable)
@@ -27,7 +27,6 @@ import jalview.datamodel.SequenceI;
 import jalview.io.SequenceAnnotationReport;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.util.UrlLink;
 import jalview.viewmodel.AlignmentViewport;
 
 import java.awt.BorderLayout;
@@ -37,9 +36,7 @@ import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
 import java.util.List;
-import java.util.Vector;
 
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.SwingUtilities;
 import javax.swing.ToolTipManager;
@@ -155,22 +152,22 @@ public class IdPanel extends JPanel implements MouseListener,
     {
       if (e.isShiftDown())
       {
-        alignPanel.scrollRight(true);
+        av.getRanges().scrollRight(true);
       }
       else
       {
-        alignPanel.scrollUp(false);
+        av.getRanges().scrollUp(false);
       }
     }
     else
     {
       if (e.isShiftDown())
       {
-        alignPanel.scrollRight(false);
+        av.getRanges().scrollRight(false);
       }
       else
       {
-        alignPanel.scrollUp(true);
+        av.getRanges().scrollUp(true);
       }
     }
   }
@@ -199,56 +196,10 @@ public class IdPanel extends JPanel implements MouseListener,
       return;
     }
 
-    Vector links = Preferences.sequenceURLLinks;
-    if (links == null || links.size() < 1)
-    {
-      return;
-    }
-
     int seq = alignPanel.getSeqPanel().findSeq(e);
-    String url = null;
-    int i = 0;
     String id = av.getAlignment().getSequenceAt(seq).getName();
-    while (url == null && i < links.size())
-    {
-      // DEFAULT LINK IS FIRST IN THE LINK LIST
-      // BUT IF ITS A REGEX AND DOES NOT MATCH THE NEXT ONE WILL BE TRIED
-      url = links.elementAt(i++).toString();
-      jalview.util.UrlLink urlLink = null;
-      try
-      {
-        urlLink = new UrlLink(url);
-      } catch (Exception foo)
-      {
-        jalview.bin.Cache.log.error("Exception for URLLink '" + url + "'",
-                foo);
-        url = null;
-        continue;
-      }
-
-      if (urlLink.usesDBAccession())
-      {
-        // this URL requires an accession id, not the name of a sequence
-        url = null;
-        continue;
-      }
+    String url = Preferences.sequenceUrlLinks.getPrimaryUrl(id);
 
-      if (!urlLink.isValid())
-      {
-        jalview.bin.Cache.log.error(urlLink.getInvalidMessage());
-        url = null;
-        continue;
-      }
-
-      String urls[] = urlLink.makeUrls(id, true);
-      if (urls == null || urls[0] == null || urls[0].length() < 4)
-      {
-        url = null;
-        continue;
-      }
-      // just take first URL made from regex
-      url = urls[1];
-    }
     try
     {
       jalview.util.BrowserLauncher.openURL(url);
@@ -291,13 +242,14 @@ public class IdPanel extends JPanel implements MouseListener,
       return;
     }
 
-    if (mouseDragging && (e.getY() < 0) && (av.getStartSeq() > 0))
+    if (mouseDragging && (e.getY() < 0)
+            && (av.getRanges().getStartSeq() > 0))
     {
       scrollThread = new ScrollThread(true);
     }
 
     if (mouseDragging && (e.getY() >= getHeight())
-            && (av.getAlignment().getHeight() > av.getEndSeq()))
+            && (av.getAlignment().getHeight() > av.getRanges().getEndSeq()))
     {
       scrollThread = new ScrollThread(false);
     }
@@ -373,23 +325,19 @@ public class IdPanel extends JPanel implements MouseListener,
   {
     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
-    Vector<String> nlinks = new Vector<String>(Preferences.sequenceURLLinks);
-    SequenceFeature sfs[] = sq == null ? null : sq.getSequenceFeatures();
-    if (sfs != null)
+
+    /*
+     *  build a new links menu based on the current links
+     *  and any non-positional features
+     */
+    List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
+    for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
     {
-      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.addElement(sf.links.elementAt(l));
-            }
-          }
+          nlinks.add(link);
         }
       }
     }
@@ -491,9 +439,10 @@ public class IdPanel extends JPanel implements MouseListener,
     int index = av.getAlignment().findIndex(list.get(0));
 
     // do we need to scroll the panel?
-    if ((av.getStartSeq() > index) || (av.getEndSeq() < index))
+    if ((av.getRanges().getStartSeq() > index)
+            || (av.getRanges().getEndSeq() < index))
     {
-      alignPanel.setScrollValues(av.getStartRes(), index);
+      av.getRanges().setStartSeq(index);
     }
   }
 
@@ -532,14 +481,14 @@ public class IdPanel extends JPanel implements MouseListener,
 
       while (running)
       {
-        if (alignPanel.scrollUp(up))
+        if (av.getRanges().scrollUp(up))
         {
           // scroll was ok, so add new sequence to selection
-          int seq = av.getStartSeq();
+          int seq = av.getRanges().getStartSeq();
 
           if (!up)
           {
-            seq = av.getEndSeq();
+            seq = av.getRanges().getEndSeq();
           }
 
           if (seq < lastid)
index 2a3d788..d92f6c0 100644 (file)
@@ -88,9 +88,12 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
 
   private JLabel dbstatus, dbstatex;
 
+  private JPanel mainPanel = new JPanel(new BorderLayout());
+
   public JDatabaseTree(jalview.ws.SequenceFetcher sfetch)
   {
-    initDialogFrame(this, true, false,
+    mainPanel.add(this);
+    initDialogFrame(mainPanel, true, false,
             MessageManager
                     .getString("label.select_database_retrieval_source"),
             650, 490);
@@ -148,19 +151,19 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
     }
     // and sort the tree
     sortTreeNodes(root);
-    svp = new JScrollPane();
-    // svp.setAutoscrolls(true);
     dbviews = new JTree(new DefaultTreeModel(root, false));
     dbviews.setCellRenderer(new DbTreeRenderer(this));
 
     dbviews.getSelectionModel().setSelectionMode(
             TreeSelectionModel.SINGLE_TREE_SELECTION);
-    svp.getViewport().setView(dbviews);
-    // svp.getViewport().setMinimumSize(new Dimension(300,200));
-    // svp.setSize(300,250);
-    // JPanel panel=new JPanel();
-    // panel.setSize(new Dimension(350,220));
-    // panel.add(svp);
+    svp = new JScrollPane(dbviews);
+    svp.setMinimumSize(new Dimension(100, 200));
+    svp.setPreferredSize(new Dimension(200, 400));
+    svp.setMaximumSize(new Dimension(300, 600));
+
+    JPanel panel = new JPanel(new BorderLayout());
+    panel.setSize(new Dimension(350, 220));
+    panel.add(svp);
     dbviews.addTreeSelectionListener(new TreeSelectionListener()
     {
 
@@ -200,7 +203,6 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
     dbstat.add(dbstatex);
     jc.add(dbstat, BorderLayout.SOUTH);
     jc.validate();
-    // j.setPreferredSize(new Dimension(300,50));
     add(jc, BorderLayout.CENTER);
     ok.setEnabled(false);
     j.add(ok);
index 35db33f..358cca6 100644 (file)
@@ -29,8 +29,10 @@ import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 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;
@@ -39,6 +41,7 @@ import jalview.ext.varna.RnaModel;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemabinding.version2.AlcodMap;
 import jalview.schemabinding.version2.AlcodonFrame;
 import jalview.schemabinding.version2.Annotation;
@@ -76,7 +79,6 @@ import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
 import jalview.schemes.FeatureColour;
-import jalview.schemes.ResidueColourScheme;
 import jalview.schemes.ResidueProperties;
 import jalview.schemes.UserColourScheme;
 import jalview.structure.StructureSelectionManager;
@@ -86,6 +88,7 @@ import jalview.util.Platform;
 import jalview.util.StringUtils;
 import jalview.util.jarInputStreamProvider;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.ViewportRanges;
 import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
 import jalview.viewmodel.seqfeatures.FeaturesDisplayed;
 import jalview.ws.jws2.Jws2Discoverer;
@@ -129,7 +132,6 @@ import java.util.jar.JarInputStream;
 import java.util.jar.JarOutputStream;
 
 import javax.swing.JInternalFrame;
-import javax.swing.JOptionPane;
 import javax.swing.SwingUtilities;
 
 import org.exolab.castor.xml.Marshaller;
@@ -755,6 +757,7 @@ public class Jalview2XML
     List<UserColourScheme> userColours = new ArrayList<UserColourScheme>();
 
     AlignViewport av = ap.av;
+    ViewportRanges vpRanges = av.getRanges();
 
     JalviewModel object = new JalviewModel();
     object.setVamsasModel(new jalview.schemabinding.version2.VamsasModel());
@@ -880,48 +883,43 @@ 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)
+        {
+          String key;
+          Iterator<String> keys = sf.otherDetails.keySet().iterator();
+          while (keys.hasNext())
           {
-            String key;
-            Iterator<String> keys = sf[index].otherDetails.keySet()
-                    .iterator();
-            while (keys.hasNext())
-            {
-              key = keys.next();
-              OtherData keyValue = new OtherData();
-              keyValue.setKey(key);
-              keyValue.setValue(sf[index].otherDetails.get(key).toString());
-              features.addOtherData(keyValue);
-            }
+            key = keys.next();
+            OtherData keyValue = new OtherData();
+            keyValue.setKey(key);
+            keyValue.setValue(sf.otherDetails.get(key).toString());
+            features.addOtherData(keyValue);
           }
-
-          jseq.addFeatures(features);
-          index++;
         }
+
+        jseq.addFeatures(features);
       }
 
       if (jdatasq.getAllPDBEntries() != null)
@@ -1105,7 +1103,7 @@ public class Jalview2XML
               Tree tree = new Tree();
               tree.setTitle(tp.getTitle());
               tree.setCurrentTree((av.currentTree == tp.getTree()));
-              tree.setNewick(tp.getTree().toString());
+              tree.setNewick(tp.getTree().print());
               tree.setThreshold(tp.treeCanvas.threshold);
 
               tree.setFitToWindow(tp.fitToWindow.getState());
@@ -1175,38 +1173,43 @@ public class Jalview2XML
           // group has references so set its ID field
           jGroup.setId(groupRefs.get(sg));
         }
-        if (sg.cs != null)
+        ColourSchemeI colourScheme = sg.getColourScheme();
+        if (colourScheme != null)
         {
-          if (sg.cs.conservationApplied())
+          ResidueShaderI groupColourScheme = sg
+                  .getGroupColourScheme();
+          if (groupColourScheme.conservationApplied())
           {
-            jGroup.setConsThreshold(sg.cs.getConservationInc());
+            jGroup.setConsThreshold(groupColourScheme.getConservationInc());
 
-            if (sg.cs instanceof jalview.schemes.UserColourScheme)
+            if (colourScheme instanceof jalview.schemes.UserColourScheme)
             {
-              jGroup.setColour(setUserColourScheme(sg.cs, userColours, jms));
+              jGroup.setColour(setUserColourScheme(colourScheme,
+                      userColours, jms));
             }
             else
             {
-              jGroup.setColour(ColourSchemeProperty.getColourName(sg.cs));
+              jGroup.setColour(colourScheme.getSchemeName());
             }
           }
-          else if (sg.cs instanceof jalview.schemes.AnnotationColourGradient)
+          else if (colourScheme instanceof jalview.schemes.AnnotationColourGradient)
           {
             jGroup.setColour("AnnotationColourGradient");
             jGroup.setAnnotationColours(constructAnnotationColours(
-                    (jalview.schemes.AnnotationColourGradient) sg.cs,
+                    (jalview.schemes.AnnotationColourGradient) colourScheme,
                     userColours, jms));
           }
-          else if (sg.cs instanceof jalview.schemes.UserColourScheme)
+          else if (colourScheme instanceof jalview.schemes.UserColourScheme)
           {
-            jGroup.setColour(setUserColourScheme(sg.cs, userColours, jms));
+            jGroup.setColour(setUserColourScheme(colourScheme,
+                    userColours, jms));
           }
           else
           {
-            jGroup.setColour(ColourSchemeProperty.getColourName(sg.cs));
+            jGroup.setColour(colourScheme.getSchemeName());
           }
 
-          jGroup.setPidThreshold(sg.cs.getThreshold());
+          jGroup.setPidThreshold(groupColourScheme.getThreshold());
         }
 
         jGroup.setOutlineColour(sg.getOutlineColour().getRGB());
@@ -1265,8 +1268,8 @@ public class Jalview2XML
       view.setWidth(size.width);
       view.setHeight(size.height);
 
-      view.setStartRes(av.startRes);
-      view.setStartSeq(av.startSeq);
+      view.setStartRes(vpRanges.getStartRes());
+      view.setStartSeq(vpRanges.getStartSeq());
 
       if (av.getGlobalColourScheme() instanceof jalview.schemes.UserColourScheme)
       {
@@ -1289,23 +1292,20 @@ public class Jalview2XML
                 .getGlobalColourScheme()));
       }
 
+      ResidueShaderI vcs = av.getResidueShading();
       ColourSchemeI cs = av.getGlobalColourScheme();
 
       if (cs != null)
       {
-        if (cs.conservationApplied())
+        if (vcs.conservationApplied())
         {
-          view.setConsThreshold(cs.getConservationInc());
+          view.setConsThreshold(vcs.getConservationInc());
           if (cs instanceof jalview.schemes.UserColourScheme)
           {
             view.setBgColour(setUserColourScheme(cs, userColours, jms));
           }
         }
-
-        if (cs instanceof ResidueColourScheme)
-        {
-          view.setPidThreshold(cs.getThreshold());
-        }
+        view.setPidThreshold(vcs.getThreshold());
       }
 
       view.setConservationSelected(av.getConservationSelected());
@@ -1409,17 +1409,18 @@ public class Jalview2XML
 
       if (av.hasHiddenColumns())
       {
-        if (av.getColumnSelection() == null
-                || av.getColumnSelection().getHiddenColumns() == null)
+        jalview.datamodel.HiddenColumns hidden = av.getAlignment()
+                .getHiddenColumns();
+        if (hidden == null || hidden.getHiddenRegions() == null)
         {
           warn("REPORT BUG: avoided null columnselection bug (DMAM reported). Please contact Jim about this.");
         }
         else
         {
-          for (int c = 0; c < av.getColumnSelection().getHiddenColumns()
+          for (int c = 0; c < hidden.getHiddenRegions()
                   .size(); c++)
           {
-            int[] region = av.getColumnSelection().getHiddenColumns()
+            int[] region = hidden.getHiddenRegions()
                     .get(c);
             HiddenColumns hc = new HiddenColumns();
             hc.setStart(region[0]);
@@ -1709,6 +1710,15 @@ public class Jalview2XML
     return matchedFile;
   }
 
+  /**
+   * Populates the AnnotationColours xml for save. This captures the settings of
+   * the options in the 'Colour by Annotation' dialog.
+   * 
+   * @param acg
+   * @param userColours
+   * @param jms
+   * @return
+   */
   private AnnotationColours constructAnnotationColours(
           AnnotationColourGradient acg, List<UserColourScheme> userColours,
           JalviewModelSequence jms)
@@ -1716,16 +1726,16 @@ public class Jalview2XML
     AnnotationColours ac = new AnnotationColours();
     ac.setAboveThreshold(acg.getAboveThreshold());
     ac.setThreshold(acg.getAnnotationThreshold());
-    ac.setAnnotation(acg.getAnnotation());
-    if (acg.getBaseColour() instanceof jalview.schemes.UserColourScheme)
+    // 2.10.2 save annotationId (unique) not annotation label
+    ac.setAnnotation(acg.getAnnotation().annotationId);
+    if (acg.getBaseColour() instanceof UserColourScheme)
     {
       ac.setColourScheme(setUserColourScheme(acg.getBaseColour(),
               userColours, jms));
     }
     else
     {
-      ac.setColourScheme(ColourSchemeProperty.getColourName(acg
-              .getBaseColour()));
+      ac.setColourScheme(ColourSchemeProperty.getColourName(acg.getBaseColour()));
     }
 
     ac.setMaxColour(acg.getMaxColour().getRGB());
@@ -2638,10 +2648,12 @@ public class Jalview2XML
           @Override
           public void run()
           {
-            JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-                    finalErrorMessage, "Error "
-                            + (saving ? "saving" : "loading")
-                            + " Jalview file", JvOptionPane.WARNING_MESSAGE);
+            JvOptionPane
+                    .showInternalMessageDialog(Desktop.desktop,
+                            finalErrorMessage, "Error "
+                                    + (saving ? "saving" : "loading")
+                                    + " Jalview file",
+                            JvOptionPane.WARNING_MESSAGE);
           }
         });
       }
@@ -2970,12 +2982,11 @@ 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());
-
-            sf.setScore(features[f].getScore());
+            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());
             for (int od = 0; od < features[f].getOtherDataCount(); od++)
             {
               OtherData keyValue = features[f].getOtherData(od);
@@ -3342,18 +3353,13 @@ public class Jalview2XML
                   && jGroup.getAnnotationColours() != null)
           {
             addAnnotSchemeGroup = true;
-            cs = null;
           }
           else
           {
-            cs = ColourSchemeProperty.getColour(al, jGroup.getColour());
-          }
-
-          if (cs != null)
-          {
-            cs.setThreshold(jGroup.getPidThreshold(), true);
+            cs = ColourSchemeProperty.getColourScheme(al, jGroup.getColour());
           }
         }
+        int pidThreshold = jGroup.getPidThreshold();
 
         Vector<SequenceI> seqs = new Vector<SequenceI>();
 
@@ -3376,7 +3382,8 @@ public class Jalview2XML
         SequenceGroup sg = new SequenceGroup(seqs, jGroup.getName(), cs,
                 jGroup.getDisplayBoxes(), jGroup.getDisplayText(),
                 jGroup.getColourText(), jGroup.getStart(), jGroup.getEnd());
-
+        sg.getGroupColourScheme().setThreshold(pidThreshold, true);
+        sg.getGroupColourScheme().setConservationInc(jGroup.getConsThreshold());
         sg.setOutlineColour(new java.awt.Color(jGroup.getOutlineColour()));
 
         sg.textColour = new java.awt.Color(jGroup.getTextCol1());
@@ -3442,8 +3449,8 @@ public class Jalview2XML
         if (addAnnotSchemeGroup)
         {
           // reconstruct the annotation colourscheme
-          sg.cs = constructAnnotationColour(jGroup.getAnnotationColours(),
-                  null, al, jms, false);
+          sg.setColourScheme(constructAnnotationColour(
+                  jGroup.getAnnotationColours(), null, al, jms, false));
         }
       }
     }
@@ -3669,7 +3676,7 @@ public class Jalview2XML
         TreePanel tp = (TreePanel) retrieveExistingObj(tree.getId());
         if (tp == null)
         {
-          tp = af.ShowNewickTree(
+          tp = af.showNewickTree(
                   new jalview.io.NewickFile(tree.getNewick()),
                   tree.getTitle(), tree.getWidth(), tree.getHeight(),
                   tree.getXpos(), tree.getYpos());
@@ -4422,10 +4429,12 @@ public class Jalview2XML
 
     af.viewport.setShowAnnotation(view.getShowAnnotation());
     af.viewport.setAbovePIDThreshold(view.getPidSelected());
+    af.viewport.setThreshold(view.getPidThreshold());
 
     af.viewport.setColourText(view.getShowColourText());
 
     af.viewport.setConservationSelected(view.getConservationSelected());
+    af.viewport.setIncrement(view.getConsThreshold());
     af.viewport.setShowJVSuffix(view.getShowFullId());
     af.viewport.setRightAlignIds(view.getRightAlignIds());
     af.viewport.setFont(
@@ -4449,8 +4458,8 @@ public class Jalview2XML
     af.viewport.setThresholdTextColour(view.getTextColThreshold());
     af.viewport.setShowUnconserved(view.hasShowUnconserved() ? view
             .isShowUnconserved() : false);
-    af.viewport.setStartRes(view.getStartRes());
-    af.viewport.setStartSeq(view.getStartSeq());
+    af.viewport.getRanges().setStartRes(view.getStartRes());
+    // startSeq set in af.alignPanel.updateLayout below
     af.alignPanel.updateLayout();
     ColourSchemeI cs = null;
     // apply colourschemes
@@ -4470,22 +4479,21 @@ public class Jalview2XML
       }
       else
       {
-        cs = ColourSchemeProperty.getColour(al, view.getBgColour());
-      }
-
-      if (cs != null)
-      {
-        cs.setThreshold(view.getPidThreshold(), true);
-        cs.setConsensus(af.viewport.getSequenceConsensusHash());
+        cs = ColourSchemeProperty.getColourScheme(al, view.getBgColour());
       }
     }
 
     af.viewport.setGlobalColourScheme(cs);
+    af.viewport.getResidueShading().setThreshold(
+            view.getPidThreshold(), true);
+    af.viewport.getResidueShading().setConsensus(
+            af.viewport.getSequenceConsensusHash());
     af.viewport.setColourAppliesToAllGroups(false);
 
     if (view.getConservationSelected() && cs != null)
     {
-      cs.setConservationInc(view.getConsThreshold());
+      af.viewport.getResidueShading().setConservationInc(
+              view.getConsThreshold());
     }
 
     af.changeColour(cs);
@@ -4691,12 +4699,21 @@ public class Jalview2XML
     return af;
   }
 
+  /**
+   * Reads saved data to restore Colour by Annotation settings
+   * 
+   * @param viewAnnColour
+   * @param af
+   * @param al
+   * @param jms
+   * @param checkGroupAnnColour
+   * @return
+   */
   private ColourSchemeI constructAnnotationColour(
           AnnotationColours viewAnnColour, AlignFrame af, AlignmentI al,
           JalviewModelSequence jms, boolean checkGroupAnnColour)
   {
     boolean propagateAnnColour = false;
-    ColourSchemeI cs = null;
     AlignmentI annAlignment = af != null ? af.viewport.getAlignment() : al;
     if (checkGroupAnnColour && al.getGroups() != null
             && al.getGroups().size() > 0)
@@ -4704,113 +4721,92 @@ public class Jalview2XML
       // pre 2.8.1 behaviour
       // check to see if we should transfer annotation colours
       propagateAnnColour = true;
-      for (jalview.datamodel.SequenceGroup sg : al.getGroups())
+      for (SequenceGroup sg : al.getGroups())
       {
-        if (sg.cs instanceof AnnotationColourGradient)
+        if (sg.getColourScheme() instanceof AnnotationColourGradient)
         {
           propagateAnnColour = false;
         }
       }
     }
-    // int find annotation
-    if (annAlignment.getAlignmentAnnotation() != null)
+
+    /*
+     * 2.10.2- : saved annotationId is AlignmentAnnotation.annotationId
+     */
+    String annotationId = viewAnnColour.getAnnotation();
+    AlignmentAnnotation matchedAnnotation = annotationIds.get(annotationId);
+
+    /*
+     * pre 2.10.2: saved annotationId is AlignmentAnnotation.label
+     */
+    if (matchedAnnotation == null && annAlignment.getAlignmentAnnotation() != null)
     {
       for (int i = 0; i < annAlignment.getAlignmentAnnotation().length; i++)
       {
-        if (annAlignment.getAlignmentAnnotation()[i].label
-                .equals(viewAnnColour.getAnnotation()))
+        if (annotationId
+                .equals(annAlignment.getAlignmentAnnotation()[i].label))
         {
-          if (annAlignment.getAlignmentAnnotation()[i].getThreshold() == null)
-          {
-            annAlignment.getAlignmentAnnotation()[i]
-                    .setThreshold(new jalview.datamodel.GraphLine(
-                            viewAnnColour.getThreshold(), "Threshold",
-                            java.awt.Color.black)
-
-                    );
-          }
-
-          if (viewAnnColour.getColourScheme().equals("None"))
-          {
-            cs = new AnnotationColourGradient(
-                    annAlignment.getAlignmentAnnotation()[i],
-                    new java.awt.Color(viewAnnColour.getMinColour()),
-                    new java.awt.Color(viewAnnColour.getMaxColour()),
-                    viewAnnColour.getAboveThreshold());
-          }
-          else if (viewAnnColour.getColourScheme().startsWith("ucs"))
-          {
-            cs = new AnnotationColourGradient(
-                    annAlignment.getAlignmentAnnotation()[i],
-                    getUserColourScheme(jms,
-                            viewAnnColour.getColourScheme()),
-                    viewAnnColour.getAboveThreshold());
-          }
-          else
-          {
-            cs = new AnnotationColourGradient(
-                    annAlignment.getAlignmentAnnotation()[i],
-                    ColourSchemeProperty.getColour(al,
-                            viewAnnColour.getColourScheme()),
-                    viewAnnColour.getAboveThreshold());
-          }
-          if (viewAnnColour.hasPerSequence())
-          {
-            ((AnnotationColourGradient) cs).setSeqAssociated(viewAnnColour
-                    .isPerSequence());
-          }
-          if (viewAnnColour.hasPredefinedColours())
-          {
-            ((AnnotationColourGradient) cs)
-                    .setPredefinedColours(viewAnnColour
-                            .isPredefinedColours());
-          }
-          if (propagateAnnColour && al.getGroups() != null)
-          {
-            // Also use these settings for all the groups
-            for (int g = 0; g < al.getGroups().size(); g++)
-            {
-              jalview.datamodel.SequenceGroup sg = al.getGroups().get(g);
-
-              if (sg.cs == null)
-              {
-                continue;
-              }
+          matchedAnnotation = annAlignment.getAlignmentAnnotation()[i];
+          break;
+        }
+      }
+    }
+    if (matchedAnnotation == null)
+    {
+      System.err.println("Failed to match annotation colour scheme for "
+              + annotationId);
+      return null;
+    }
+    if (matchedAnnotation.getThreshold() == null)
+    {
+      matchedAnnotation.setThreshold(new GraphLine(viewAnnColour.getThreshold(),
+              "Threshold", Color.black));
+    }
 
-              /*
-               * if (viewAnnColour.getColourScheme().equals("None" )) { sg.cs =
-               * new AnnotationColourGradient(
-               * annAlignment.getAlignmentAnnotation()[i], new
-               * java.awt.Color(viewAnnColour. getMinColour()), new
-               * java.awt.Color(viewAnnColour. getMaxColour()),
-               * viewAnnColour.getAboveThreshold()); } else
-               */
-              {
-                sg.cs = new AnnotationColourGradient(
-                        annAlignment.getAlignmentAnnotation()[i], sg.cs,
-                        viewAnnColour.getAboveThreshold());
-                if (cs instanceof AnnotationColourGradient)
-                {
-                  if (viewAnnColour.hasPerSequence())
-                  {
-                    ((AnnotationColourGradient) cs)
-                            .setSeqAssociated(viewAnnColour.isPerSequence());
-                  }
-                  if (viewAnnColour.hasPredefinedColours())
-                  {
-                    ((AnnotationColourGradient) cs)
-                            .setPredefinedColours(viewAnnColour
-                                    .isPredefinedColours());
-                  }
-                }
-              }
+    AnnotationColourGradient cs = null;
+    if (viewAnnColour.getColourScheme().equals("None"))
+    {
+      cs = new AnnotationColourGradient(matchedAnnotation, new Color(
+              viewAnnColour.getMinColour()), new Color(
+              viewAnnColour.getMaxColour()),
+              viewAnnColour.getAboveThreshold());
+    }
+    else if (viewAnnColour.getColourScheme().startsWith("ucs"))
+    {
+      cs = new AnnotationColourGradient(matchedAnnotation, getUserColourScheme(
+              jms, viewAnnColour.getColourScheme()),
+              viewAnnColour.getAboveThreshold());
+    }
+    else
+    {
+      cs = new AnnotationColourGradient(matchedAnnotation,
+              ColourSchemeProperty.getColourScheme(al,
+                      viewAnnColour.getColourScheme()),
+              viewAnnColour.getAboveThreshold());
+    }
 
-            }
-          }
+    boolean perSequenceOnly = viewAnnColour.isPerSequence();
+    boolean useOriginalColours = viewAnnColour.isPredefinedColours();
+    cs.setSeqAssociated(perSequenceOnly);
+    cs.setPredefinedColours(useOriginalColours);
 
-          break;
+    if (propagateAnnColour && al.getGroups() != null)
+    {
+      // Also use these settings for all the groups
+      for (int g = 0; g < al.getGroups().size(); g++)
+      {
+        SequenceGroup sg = al.getGroups().get(g);
+        if (sg.getGroupColourScheme() == null)
+        {
+          continue;
         }
 
+        AnnotationColourGradient groupScheme = new AnnotationColourGradient(
+                matchedAnnotation, sg.getColourScheme(),
+                viewAnnColour.getAboveThreshold());
+        sg.setColourScheme(groupScheme);
+        groupScheme.setSeqAssociated(perSequenceOnly);
+        groupScheme.setPredefinedColours(useOriginalColours);
       }
     }
     return cs;
index 52f61b1..614efd2 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;
@@ -52,8 +53,6 @@ import java.util.Vector;
 import java.util.jar.JarEntry;
 import java.util.jar.JarInputStream;
 
-import javax.swing.JOptionPane;
-
 /**
  * DOCUMENT ME!
  * 
@@ -226,11 +225,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);
         }
       }
@@ -333,15 +331,10 @@ public class Jalview2XML_V1
           }
           else
           {
-            cs = ColourSchemeProperty.getColour(al, groups[i].getColour());
-          }
-
-          if (cs != null)
-          {
-            cs.setThreshold(groups[i].getPidThreshold(), true);
+            cs = ColourSchemeProperty.getColourScheme(al, groups[i].getColour());
           }
-
         }
+        int pidThreshold = groups[i].getPidThreshold();
 
         Vector seqs = new Vector();
         int[] ids = groups[i].getSeq();
@@ -355,6 +348,7 @@ public class Jalview2XML_V1
                 seqs, groups[i].getName(), cs, groups[i].getDisplayBoxes(),
                 groups[i].getDisplayText(), groups[i].getColourText(),
                 groups[i].getStart(), groups[i].getEnd());
+        sg.getGroupColourScheme().setThreshold(pidThreshold, true);
 
         sg.setOutlineColour(new java.awt.Color(groups[i].getOutlineColour()));
 
@@ -373,8 +367,8 @@ public class Jalview2XML_V1
 
     af.setBounds(view.getXpos(), view.getYpos(), view.getWidth(),
             view.getHeight());
-    af.viewport.setStartRes(view.getStartRes());
-    af.viewport.setStartSeq(view.getStartSeq());
+    af.viewport.getRanges().setStartRes(view.getStartRes());
+    // startSeq set in af.alignPanel.updateLayout below
     af.viewport.setShowAnnotation(view.getShowAnnotation());
     af.viewport.setAbovePIDThreshold(view.getPidSelected());
     af.viewport.setColourText(view.getShowColourText());
@@ -401,23 +395,27 @@ public class Jalview2XML_V1
       }
       else
       {
-        cs = ColourSchemeProperty.getColour(al, view.getBgColour());
+        cs = ColourSchemeProperty.getColourScheme(al, view.getBgColour());
       }
 
-      if (cs != null)
-      {
-        cs.setThreshold(view.getPidThreshold(), true);
-        cs.setConsensus(af.viewport.getSequenceConsensusHash());
-      }
+      // if (cs != null)
+      // {
+      // cs.setThreshold(view.getPidThreshold(), true);
+      // cs.setConsensus(af.viewport.getSequenceConsensusHash());
+      // }
     }
 
-    af.viewport.setGlobalColourScheme(cs);
+    af.viewport.getResidueShading().setThreshold(
+            view.getPidThreshold(), true);
+    af.viewport.getResidueShading().setConsensus(
+            af.viewport.getSequenceConsensusHash());
     af.viewport.setColourAppliesToAllGroups(false);
     af.alignPanel.updateLayout();
     af.changeColour(cs);
     if (view.getConservationSelected() && cs != null)
     {
-      cs.setConservationInc(view.getConsThreshold());
+      af.viewport.getResidueShading().setConservationInc(
+              view.getConsThreshold());
     }
 
     af.viewport.setColourAppliesToAllGroups(true);
@@ -466,7 +464,7 @@ public class Jalview2XML_V1
 
           Tree tree = jms.getTree(t);
 
-          TreePanel tp = af.ShowNewickTree(
+          TreePanel tp = af.showNewickTree(
                   new jalview.io.NewickFile(tree.getNewick()),
                   tree.getTitle(), tree.getWidth(), tree.getHeight(),
                   tree.getXpos(), tree.getYpos());
index bd890d9..c9b35d8 100644 (file)
@@ -34,9 +34,6 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
 {
   private ChimeraViewFrame cvf;
 
-  private FeatureRenderer fr = null;
-
-
   public JalviewChimeraBindingModel(ChimeraViewFrame chimeraViewFrame,
           StructureSelectionManager ssm, PDBEntry[] pdbentry,
           SequenceI[][] sequenceIs, DataSourceType protocol)
@@ -52,17 +49,10 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
             : (AlignmentPanel) alignment;
     if (ap.av.isShowSequenceFeatures())
     {
-      if (fr == null)
-      {
-        fr = (jalview.gui.FeatureRenderer) ap.cloneFeatureRenderer();
-      }
-      else
-      {
-        ap.updateFeatureRenderer(fr);
-      }
+      return ap.getSeqPanel().seqCanvas.fr;
     }
 
-    return fr;
+    return null;
   }
 
   @Override
@@ -97,7 +87,7 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
     }
     if (!isLoadingFromArchive())
     {
-      colourBySequence(ap.av.isShowSequenceFeatures(), ap);
+      colourBySequence(ap);
     }
   }
 
index 8742253..05f5ffc 100644 (file)
@@ -23,6 +23,7 @@ package jalview.gui;
 import jalview.util.MessageManager;
 
 import java.awt.Container;
+import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -89,6 +90,9 @@ public abstract class JalviewDialog extends JPanel
     {
       frame.setSize(width, height);
     }
+    int minWidth = width - 100;
+    int minHeight = height - 100;
+    frame.setMinimumSize(new Dimension(minWidth, minHeight));
     frame.setContentPane(content);
     this.block = block;
 
diff --git a/src/jalview/gui/OverviewCanvas.java b/src/jalview/gui/OverviewCanvas.java
new file mode 100644 (file)
index 0000000..6f9fbbf
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * 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.AlignViewportI;
+import jalview.renderer.OverviewRenderer;
+import jalview.viewmodel.OverviewDimensions;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+
+import javax.swing.JComponent;
+
+public class OverviewCanvas extends JComponent
+{
+  private static final Color TRANS_GREY = new Color(100, 100, 100, 25);
+
+  // This is set true if the alignment view changes whilst
+  // the overview is being calculated
+  private volatile boolean restart = false;
+
+  private volatile boolean updaterunning = false;
+
+  private BufferedImage miniMe;
+
+  private BufferedImage lastMiniMe = null;
+
+  // Can set different properties in this seqCanvas than
+  // main visible SeqCanvas
+  private SequenceRenderer sr;
+
+  private jalview.renderer.seqfeatures.FeatureRenderer fr;
+
+  private OverviewDimensions od;
+
+  private AlignViewportI av;
+
+  public OverviewCanvas(OverviewDimensions overviewDims,
+          AlignViewportI alignvp)
+  {
+    od = overviewDims;
+    av = alignvp;
+
+    sr = new SequenceRenderer(av);
+    sr.renderGaps = false;
+    sr.forOverview = true;
+    fr = new jalview.renderer.seqfeatures.FeatureRenderer(av);
+  }
+
+  /**
+   * Update the overview dimensions object used by the canvas (e.g. if we change
+   * from showing hidden columns to hiding them or vice versa)
+   * 
+   * @param overviewDims
+   */
+  public void resetOviewDims(OverviewDimensions overviewDims)
+  {
+    od = overviewDims;
+  }
+
+  /**
+   * Signals to drawing code that the associated alignment viewport has changed
+   * and a redraw will be required
+   */
+  public boolean restartDraw()
+  {
+    synchronized (this)
+    {
+      if (updaterunning)
+      {
+        restart = true;
+      }
+      else
+      {
+        updaterunning = true;
+      }
+      return restart;
+    }
+  }
+
+  /**
+   * Draw the overview sequences
+   * 
+   * @param showSequenceFeatures
+   *          true if sequence features are to be shown
+   * @param showAnnotation
+   *          true if the annotation is to be shown
+   * @param transferRenderer
+   *          the renderer to transfer feature colouring from
+   */
+  public void draw(boolean showSequenceFeatures, boolean showAnnotation,
+          FeatureRenderer transferRenderer)
+  {
+    miniMe = null;
+
+    if (showSequenceFeatures)
+    {
+      fr.transferSettings(transferRenderer);
+    }
+
+    setPreferredSize(new Dimension(od.getWidth(), od.getHeight()));
+
+    OverviewRenderer or = new OverviewRenderer(sr, fr, od);
+    miniMe = or.draw(od.getRows(av.getAlignment()),
+            od.getColumns(av.getAlignment()));
+
+    Graphics mg = miniMe.getGraphics();
+
+    if (showAnnotation)
+    {
+      mg.translate(0, od.getSequencesHeight());
+      or.drawGraph(mg, av.getAlignmentConservationAnnotation(),
+              av.getCharWidth(), od.getGraphHeight(),
+              od.getColumns(av.getAlignment()));
+      mg.translate(0, -od.getSequencesHeight());
+    }
+    System.gc();
+
+    if (restart)
+    {
+      restart = false;
+      draw(showSequenceFeatures, showAnnotation, transferRenderer);
+    }
+    else
+    {
+      updaterunning = false;
+      lastMiniMe = miniMe;
+    }
+  }
+
+  @Override
+  public void paintComponent(Graphics g)
+  {
+    if (restart)
+    {
+      if (lastMiniMe == null)
+      {
+        g.setColor(Color.white);
+        g.fillRect(0, 0, getWidth(), getHeight());
+      }
+      else
+      {
+        g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
+      }
+      g.setColor(TRANS_GREY);
+      g.fillRect(0, 0, getWidth(), getHeight());
+    }
+    else if (lastMiniMe != null)
+    {
+      g.drawImage(lastMiniMe, 0, 0, this);
+      if (lastMiniMe != miniMe)
+      {
+        g.setColor(TRANS_GREY);
+        g.fillRect(0, 0, getWidth(), getHeight());
+      }
+    }
+
+    g.setColor(Color.red);
+    od.drawBox(g);
+  }
+
+}
index 1c48690..3fa674e 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.renderer.AnnotationRenderer;
-
-import java.awt.Color;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.viewmodel.OverviewDimensions;
+import jalview.viewmodel.OverviewDimensionsHideHidden;
+import jalview.viewmodel.OverviewDimensionsShowHidden;
+import jalview.viewmodel.ViewportListenerI;
+
+import java.awt.BorderLayout;
 import java.awt.Dimension;
-import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseMotionAdapter;
-import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeEvent;
 
+import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.SwingUtilities;
 
 /**
- * DOCUMENT ME!
+ * Panel displaying an overview of the full alignment, with an interactive box
+ * representing the viewport onto the alignment.
  * 
  * @author $author$
  * @version $Revision$
  */
-public class OverviewPanel extends JPanel implements Runnable
+public class OverviewPanel extends JPanel implements Runnable,
+        ViewportListenerI
 {
-  BufferedImage miniMe;
-
-  AlignViewport av;
-
-  AlignmentPanel ap;
-
-  final AnnotationRenderer renderer = new AnnotationRenderer();
-
-  float scalew = 1f;
-
-  float scaleh = 1f;
+  private OverviewDimensions od;
 
-  int width;
+  private OverviewCanvas oviewCanvas;
 
-  int sequencesHeight;
+  private AlignViewport av;
 
-  int graphHeight = 20;
+  private AlignmentPanel ap;
 
-  int boxX = -1;
+  private JCheckBoxMenuItem displayToggle;
 
-  int boxY = -1;
-
-  int boxWidth = -1;
-
-  int boxHeight = -1;
-
-  boolean resizing = false;
-
-  // Can set different properties in this seqCanvas than
-  // main visible SeqCanvas
-  SequenceRenderer sr;
-
-  jalview.renderer.seqfeatures.FeatureRenderer fr;
+  private boolean showHidden = true;
 
   /**
    * Creates a new OverviewPanel object.
    * 
-   * @param ap
-   *          DOCUMENT ME!
+   * @param alPanel
+   *          The alignment panel which is shown in the overview panel
    */
-  public OverviewPanel(AlignmentPanel ap)
+  public OverviewPanel(AlignmentPanel alPanel)
   {
-    this.av = ap.av;
-    this.ap = ap;
-    setLayout(null);
+    this.av = alPanel.av;
+    this.ap = alPanel;
 
-    sr = new SequenceRenderer(av);
-    sr.renderGaps = false;
-    sr.forOverview = true;
-    fr = new FeatureRenderer(ap);
+    od = new OverviewDimensionsShowHidden(av.getRanges(),
+            (av.isShowAnnotation() && av
+                    .getAlignmentConservationAnnotation() != null));
 
-    // scale the initial size of overviewpanel to shape of alignment
-    float initialScale = (float) av.getAlignment().getWidth()
-            / (float) av.getAlignment().getHeight();
+    setSize(od.getWidth(), od.getHeight());
 
-    if (av.getAlignmentConservationAnnotation() == null)
-    {
-      graphHeight = 0;
-    }
+    oviewCanvas = new OverviewCanvas(od, av);
+    setLayout(new BorderLayout());
+    add(oviewCanvas, BorderLayout.CENTER);
 
-    if (av.getAlignment().getWidth() > av.getAlignment().getHeight())
-    {
-      // wider
-      width = 400;
-      sequencesHeight = (int) (400f / initialScale);
-      if (sequencesHeight < 40)
-      {
-        sequencesHeight = 40;
-      }
-    }
-    else
-    {
-      // taller
-      width = (int) (400f * initialScale);
-      sequencesHeight = 300;
-
-      if (width < 120)
-      {
-        width = 120;
-      }
-    }
+    av.getRanges().addPropertyChangeListener(this);
 
     addComponentListener(new ComponentAdapter()
     {
       @Override
       public void componentResized(ComponentEvent evt)
       {
-        if ((getWidth() != width)
-                || (getHeight() != (sequencesHeight + graphHeight)))
+        if ((getWidth() != od.getWidth())
+                || (getHeight() != (od.getHeight())))
         {
           updateOverviewImage();
+          setBoxPosition();
         }
       }
     });
@@ -142,13 +107,13 @@ public class OverviewPanel extends JPanel implements Runnable
       @Override
       public void mouseDragged(MouseEvent evt)
       {
-        if (!av.getWrapAlignment())
+        if (!SwingUtilities.isRightMouseButton(evt)
+                && !av.getWrapAlignment())
         {
-          // TODO: feature: jv2.5 detect shift drag and update selection from
-          // it.
-          boxX = evt.getX();
-          boxY = evt.getY();
-          checkValid();
+          od.updateViewportFromMouse(evt.getX(), evt.getY(), av
+                  .getAlignment().getHiddenSequences(), av.getAlignment()
+                  .getHiddenColumns());
+
         }
       }
     });
@@ -158,364 +123,133 @@ public class OverviewPanel extends JPanel implements Runnable
       @Override
       public void mousePressed(MouseEvent evt)
       {
-        if (!av.getWrapAlignment())
+        if (SwingUtilities.isRightMouseButton(evt))
+        {
+          if (!Platform.isAMac())
+          {
+            showPopupMenu(evt);
+          }
+        }
+        else if (!av.getWrapAlignment())
+        {
+          od.updateViewportFromMouse(evt.getX(), evt.getY(), av
+                  .getAlignment().getHiddenSequences(), av.getAlignment()
+                  .getHiddenColumns());
+        }
+      }
+
+      @Override
+      public void mouseClicked(MouseEvent evt)
+      {
+        if (SwingUtilities.isRightMouseButton(evt))
         {
-          boxX = evt.getX();
-          boxY = evt.getY();
-          checkValid();
+          showPopupMenu(evt);
         }
       }
     });
 
+
     updateOverviewImage();
   }
 
-  /**
-   * DOCUMENT ME!
+  /*
+   * Displays the popup menu and acts on user input
    */
-  void checkValid()
+  private void showPopupMenu(MouseEvent e)
   {
-    if (boxY < 0)
-    {
-      boxY = 0;
-    }
-
-    if (boxY > (sequencesHeight - boxHeight))
+    JPopupMenu popup = new JPopupMenu();
+    ActionListener menuListener = new ActionListener()
     {
-      boxY = sequencesHeight - boxHeight + 1;
-    }
-
-    if (boxX < 0)
-    {
-      boxX = 0;
-    }
-
-    if (boxX > (width - boxWidth))
-    {
-      if (av.hasHiddenColumns())
-      {
-        // Try smallest possible box
-        boxWidth = (int) ((av.endRes - av.startRes + 1) * av.getCharWidth() * scalew);
-      }
-      boxX = width - boxWidth;
-    }
-
-    int col = (int) (boxX / scalew / av.getCharWidth());
-    int row = (int) (boxY / scaleh / av.getCharHeight());
+      @Override
+      public void actionPerformed(ActionEvent event)
+      {
+        // switch on/off the hidden columns view
+        toggleHiddenColumns();
+        displayToggle.setSelected(showHidden);
+      }
+    };
+    displayToggle = new JCheckBoxMenuItem(
+            MessageManager.getString("label.togglehidden"));
+    displayToggle.setEnabled(true);
+    displayToggle.setSelected(showHidden);
+    popup.add(displayToggle);
+    displayToggle.addActionListener(menuListener);
+    popup.show(this, e.getX(), e.getY());
+  }
 
-    if (av.hasHiddenColumns())
+  /*
+   * Toggle overview display between showing hidden columns and hiding hidden columns
+   */
+  private void toggleHiddenColumns()
+  {
+    if (showHidden)
     {
-      if (!av.getColumnSelection().isVisible(col))
-      {
-        return;
-      }
-
-      col = av.getColumnSelection().findColumnPosition(col);
+      showHidden = false;
+      od = new OverviewDimensionsHideHidden(av.getRanges(),
+              (av.isShowAnnotation() && av
+                      .getAlignmentConservationAnnotation() != null));
     }
-
-    if (av.hasHiddenRows())
+    else
     {
-      row = av.getAlignment().getHiddenSequences()
-              .findIndexWithoutHiddenSeqs(row);
+      showHidden = true;
+      od = new OverviewDimensionsShowHidden(av.getRanges(),
+              (av.isShowAnnotation() && av
+                      .getAlignmentConservationAnnotation() != null));
     }
-
-    ap.setScrollValues(col, row);
-
+    oviewCanvas.resetOviewDims(od);
+    updateOverviewImage();
+    setBoxPosition();
   }
 
   /**
-   * DOCUMENT ME!
+   * Updates the overview image when the related alignment panel is updated
    */
   public void updateOverviewImage()
   {
-    if (resizing)
+    if ((getWidth() > 0) && (getHeight() > 0))
     {
-      resizeAgain = true;
-      return;
+      od.setWidth(getWidth());
+      od.setHeight(getHeight());
     }
+    
+    setPreferredSize(new Dimension(od.getWidth(), od.getHeight()));
 
-    resizing = true;
-
-    if ((getWidth() > 0) && (getHeight() > 0))
+    if (oviewCanvas.restartDraw())
     {
-      width = getWidth();
-      sequencesHeight = getHeight() - graphHeight;
+      return;
     }
 
-    setPreferredSize(new Dimension(width, sequencesHeight + graphHeight));
-
     Thread thread = new Thread(this);
     thread.start();
     repaint();
-  }
 
-  // This is set true if the user resizes whilst
-  // the overview is being calculated
-  boolean resizeAgain = false;
+  }
 
-  /**
-   * DOCUMENT ME!
-   */
   @Override
   public void run()
   {
-    miniMe = null;
-
-    if (av.isShowSequenceFeatures())
-    {
-      fr.transferSettings(ap.getSeqPanel().seqCanvas.getFeatureRenderer());
-    }
-
-    int alwidth = av.getAlignment().getWidth();
-    int alheight = av.getAlignment().getHeight()
-            + av.getAlignment().getHiddenSequences().getSize();
-
-    setPreferredSize(new Dimension(width, sequencesHeight + graphHeight));
-
-    int fullsizeWidth = alwidth * av.getCharWidth();
-    int fullsizeHeight = alheight * av.getCharHeight();
-
-    scalew = (float) width / (float) fullsizeWidth;
-    scaleh = (float) sequencesHeight / (float) fullsizeHeight;
-
-    miniMe = new BufferedImage(width, sequencesHeight + graphHeight,
-            BufferedImage.TYPE_INT_RGB);
-
-    Graphics mg = miniMe.getGraphics();
-    mg.setColor(Color.orange);
-    mg.fillRect(0, 0, width, miniMe.getHeight());
-
-    float sampleCol = (float) alwidth / (float) width;
-    float sampleRow = (float) alheight / (float) sequencesHeight;
-
-    int lastcol = -1, lastrow = -1;
-    int color = Color.white.getRGB();
-    int row, col;
-    jalview.datamodel.SequenceI seq;
-    final boolean hasHiddenRows = av.hasHiddenRows(), hasHiddenCols = av
-            .hasHiddenColumns();
-    boolean hiddenRow = false;
-    // get hidden row and hidden column map once at beginning.
-    // clone featureRenderer settings to avoid race conditions... if state is
-    // updated just need to refresh again
-    for (row = 0; row < sequencesHeight; row++)
-    {
-      if (resizeAgain)
-      {
-        break;
-      }
-      if ((int) (row * sampleRow) == lastrow)
-      {
-        // No need to recalculate the colours,
-        // Just copy from the row above
-        for (col = 0; col < width; col++)
-        {
-          if (resizeAgain)
-          {
-            break;
-          }
-          miniMe.setRGB(col, row, miniMe.getRGB(col, row - 1));
-        }
-        continue;
-      }
-
-      lastrow = (int) (row * sampleRow);
-
-      hiddenRow = false;
-      if (hasHiddenRows)
-      {
-        seq = av.getAlignment().getHiddenSequences()
-                .getHiddenSequence(lastrow);
-        if (seq == null)
-        {
-          int index = av.getAlignment().getHiddenSequences()
-                  .findIndexWithoutHiddenSeqs(lastrow);
-
-          seq = av.getAlignment().getSequenceAt(index);
-        }
-        else
-        {
-          hiddenRow = true;
-        }
-      }
-      else
-      {
-        seq = av.getAlignment().getSequenceAt(lastrow);
-      }
-
-      if (seq == null)
-      {
-        System.out.println(lastrow + " null");
-        continue;
-      }
-
-      for (col = 0; col < width; col++)
-      {
-        if (resizeAgain)
-        {
-          break;
-        }
-        if ((int) (col * sampleCol) == lastcol
-                && (int) (row * sampleRow) == lastrow)
-        {
-          miniMe.setRGB(col, row, color);
-          continue;
-        }
-
-        lastcol = (int) (col * sampleCol);
-
-        if (seq.getLength() > lastcol)
-        {
-          color = sr.getResidueBoxColour(seq, lastcol).getRGB();
-
-          if (av.isShowSequenceFeatures())
-          {
-            color = fr.findFeatureColour(color, seq, lastcol);
-          }
-        }
-        else
-        {
-          color = -1; // White
-        }
-
-        if (hiddenRow
-                || (hasHiddenCols && !av.getColumnSelection().isVisible(
-                        lastcol)))
-        {
-          color = new Color(color).darker().darker().getRGB();
-        }
-
-        miniMe.setRGB(col, row, color);
-
-      }
-    }
-
-    if (av.getAlignmentConservationAnnotation() != null)
-    {
-      renderer.updateFromAlignViewport(av);
-      for (col = 0; col < width; col++)
-      {
-        if (resizeAgain)
-        {
-          break;
-        }
-        lastcol = (int) (col * sampleCol);
-        {
-          mg.translate(col, sequencesHeight);
-          renderer.drawGraph(mg, av.getAlignmentConservationAnnotation(),
-                  av.getAlignmentConservationAnnotation().annotations,
-                  (int) (sampleCol) + 1, graphHeight,
-                  (int) (col * sampleCol), (int) (col * sampleCol) + 1);
-          mg.translate(-col, -sequencesHeight);
-        }
-      }
-    }
-    System.gc();
-
-    resizing = false;
-
-    if (resizeAgain)
-    {
-      resizeAgain = false;
-      updateOverviewImage();
-    }
-    else
-    {
-      lastMiniMe = miniMe;
-    }
-
+    oviewCanvas.draw(av.isShowSequenceFeatures(),
+            (av.isShowAnnotation() && av
+                    .getAlignmentConservationAnnotation() != null), ap
+                    .getSeqPanel().seqCanvas.getFeatureRenderer());
     setBoxPosition();
   }
 
   /**
-   * DOCUMENT ME!
+   * Update the overview panel box when the associated alignment panel is
+   * changed
+   * 
    */
-  public void setBoxPosition()
+  private void setBoxPosition()
   {
-    int fullsizeWidth = av.getAlignment().getWidth() * av.getCharWidth();
-    int fullsizeHeight = (av.getAlignment().getHeight() + av.getAlignment()
-            .getHiddenSequences().getSize())
-            * av.getCharHeight();
-
-    int startRes = av.getStartRes();
-    int endRes = av.getEndRes();
-
-    if (av.hasHiddenColumns())
-    {
-      startRes = av.getColumnSelection().adjustForHiddenColumns(startRes);
-      endRes = av.getColumnSelection().adjustForHiddenColumns(endRes);
-    }
-
-    int startSeq = av.startSeq;
-    int endSeq = av.endSeq;
-
-    if (av.hasHiddenRows())
-    {
-      startSeq = av.getAlignment().getHiddenSequences()
-              .adjustForHiddenSeqs(startSeq);
-
-      endSeq = av.getAlignment().getHiddenSequences()
-              .adjustForHiddenSeqs(endSeq);
-
-    }
-
-    scalew = (float) width / (float) fullsizeWidth;
-    scaleh = (float) sequencesHeight / (float) fullsizeHeight;
-
-    boxX = (int) (startRes * av.getCharWidth() * scalew);
-    boxY = (int) (startSeq * av.getCharHeight() * scaleh);
-
-    if (av.hasHiddenColumns())
-    {
-      boxWidth = (int) ((endRes - startRes + 1) * av.getCharWidth() * scalew);
-    }
-    else
-    {
-      boxWidth = (int) ((endRes - startRes + 1) * av.getCharWidth() * scalew);
-    }
-
-    boxHeight = (int) ((endSeq - startSeq) * av.getCharHeight() * scaleh);
-
+    od.setBoxPosition(av.getAlignment().getHiddenSequences(), av
+            .getAlignment().getHiddenColumns());
     repaint();
   }
 
-  private BufferedImage lastMiniMe = null;
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param g
-   *          DOCUMENT ME!
-   */
   @Override
-  public void paintComponent(Graphics g)
+  public void propertyChange(PropertyChangeEvent evt)
   {
-    if (resizing || resizeAgain)
-    {
-      if (lastMiniMe == null)
-      {
-        g.setColor(Color.white);
-        g.fillRect(0, 0, getWidth(), getHeight());
-      }
-      else
-      {
-        g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
-      }
-      g.setColor(new Color(100, 100, 100, 25));
-      g.fillRect(0, 0, getWidth(), getHeight());
-    }
-    else if (lastMiniMe != null)
-    {
-      g.drawImage(lastMiniMe, 0, 0, this);
-      if (lastMiniMe != miniMe)
-      {
-        g.setColor(new Color(100, 100, 100, 25));
-        g.fillRect(0, 0, getWidth(), getHeight());
-      }
-    }
-    // TODO: render selected regions
-    g.setColor(Color.red);
-    g.drawRect(boxX, boxY, boxWidth, boxHeight);
-    g.drawRect(boxX + 1, boxY + 1, boxWidth - 2, boxHeight - 2);
+    setBoxPosition();
   }
 }
index aa01bf5..d8e6b06 100644 (file)
  */
 package jalview.gui;
 
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.analysis.scoremodels.SimilarityParams;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
-import jalview.datamodel.ColumnSelection;
-import jalview.datamodel.SeqCigar;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.jbgui.GPCAPanel;
-import jalview.schemes.ResidueProperties;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.PCAModel;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Dimension;
 import java.awt.Graphics;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -70,29 +73,54 @@ public class PCAPanel extends GPCAPanel implements Runnable,
 
   PCAModel pcaModel;
 
+  private static final int MIN_WIDTH = 470;
+
+  private static final int MIN_HEIGHT = 250;
+
   int top = 0;
 
   /**
-   * Creates a new PCAPanel object.
+   * Creates a new PCAPanel object using default score model and parameters
    * 
-   * @param av
-   *          DOCUMENT ME!
-   * @param s
-   *          DOCUMENT ME!
+   * @param alignPanel
    */
-  public PCAPanel(AlignmentPanel ap)
+  public PCAPanel(AlignmentPanel alignPanel)
+  {
+    this(alignPanel, ScoreModels.getInstance()
+            .getDefaultModel(!alignPanel.av.getAlignment().isNucleotide())
+            .getName(), SimilarityParams.SeqSpace);
+  }
+
+  /**
+   * Constructor given sequence data, a similarity (or distance) score model
+   * name, and score calculation parameters
+   * 
+   * @param alignPanel
+   * @param modelName
+   * @param params
+   */
+  public PCAPanel(AlignmentPanel alignPanel, String modelName,
+          SimilarityParamsI params)
   {
     super();
-    this.av = ap.av;
-    this.ap = ap;
+    this.av = alignPanel.av;
+    this.ap = alignPanel;
+    boolean nucleotide = av.getAlignment().isNucleotide();
 
     progressBar = new ProgressBar(statusPanel, statusBar);
 
-    boolean sameLength = true;
+    addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosed(InternalFrameEvent e)
+      {
+        close_actionPerformed();
+      }
+    });
+
     boolean selected = av.getSelectionGroup() != null
             && av.getSelectionGroup().getSize() > 0;
     AlignmentView seqstrings = av.getAlignmentView(selected);
-    boolean nucleotide = av.getAlignment().isNucleotide();
     SequenceI[] seqs;
     if (!selected)
     {
@@ -102,41 +130,14 @@ public class PCAPanel extends GPCAPanel implements Runnable,
     {
       seqs = av.getSelectionGroup().getSequencesInOrder(av.getAlignment());
     }
-    SeqCigar sq[] = seqstrings.getSequences();
-    int length = sq[0].getWidth();
-
-    for (int i = 0; i < seqs.length; i++)
-    {
-      if (sq[i].getWidth() != length)
-      {
-        sameLength = false;
-        break;
-      }
-    }
 
-    if (!sameLength)
-    {
-      JvOptionPane.showMessageDialog(Desktop.desktop,
-              MessageManager.getString("label.pca_sequences_not_aligned"),
-              MessageManager.getString("label.sequences_not_aligned"),
-              JvOptionPane.WARNING_MESSAGE);
-
-      return;
-    }
-
-    addInternalFrameListener(new InternalFrameAdapter()
-    {
-      @Override
-      public void internalFrameClosed(InternalFrameEvent e)
-      {
-        close_actionPerformed();
-      }
-    });
-
-    pcaModel = new PCAModel(seqstrings, seqs, nucleotide);
+    ScoreModelI scoreModel = ScoreModels.getInstance().getScoreModel(
+            modelName, ap);
+    pcaModel = new PCAModel(seqstrings, seqs, nucleotide, scoreModel,
+            params);
     PaintRefresher.Register(this, av.getSequenceSetId());
 
-    rc = new RotatableCanvas(ap);
+    rc = new RotatableCanvas(alignPanel);
     this.getContentPane().add(rc, BorderLayout.CENTER);
     Thread worker = new Thread(this);
     worker.start();
@@ -151,40 +152,52 @@ public class PCAPanel extends GPCAPanel implements Runnable,
     pcaModel = null;
   }
 
+  /**
+   * Repopulate the options and actions under the score model menu when it is
+   * selected. Options will depend on whether 'nucleotide' or 'peptide'
+   * modelling is selected (and also possibly on whether any additional score
+   * models have been added).
+   */
   @Override
-  protected void scoreMatrix_menuSelected()
+  protected void scoreModel_menuSelected()
   {
-    scoreMatrixMenu.removeAll();
-    for (final String sm : ResidueProperties.scoreMatrices.keySet())
-    {
-      if (ResidueProperties.getScoreMatrix(sm) != null)
+    scoreModelMenu.removeAll();
+    for (final ScoreModelI sm : ScoreModels.getInstance().getModels())
+    {
+      final String name = sm.getName();
+      JCheckBoxMenuItem jm = new JCheckBoxMenuItem(name);
+
+      /*
+       * if the score model doesn't provide a description, try to look one
+       * up in the text bundle, falling back on its name
+       */
+      String tooltip = sm.getDescription();
+      if (tooltip == null)
       {
-        // create an entry for this score matrix for use in PCA
-        JCheckBoxMenuItem jm = new JCheckBoxMenuItem();
-        jm.setText(MessageManager.getStringOrReturn("label.score_model_",
-                sm));
-        jm.setSelected(pcaModel.getScore_matrix().equals(sm));
-        if ((ResidueProperties.scoreMatrices.get(sm).isDNA() && ResidueProperties.scoreMatrices
-                .get(sm).isProtein())
-                || pcaModel.isNucleotide() == ResidueProperties.scoreMatrices
-                        .get(sm).isDNA())
+        tooltip = MessageManager.getStringOrReturn("label.score_model_",
+                name);
+      }
+      jm.setToolTipText(tooltip);
+      jm.setSelected(pcaModel.getScoreModelName().equals(name));
+      if ((pcaModel.isNucleotide() && sm.isDNA())
+              || (!pcaModel.isNucleotide() && sm.isProtein()))
+      {
+        jm.addActionListener(new ActionListener()
         {
-          final PCAPanel us = this;
-          jm.addActionListener(new ActionListener()
+          @Override
+          public void actionPerformed(ActionEvent e)
           {
-            @Override
-            public void actionPerformed(ActionEvent e)
+            if (!pcaModel.getScoreModelName().equals(name))
             {
-              if (!pcaModel.getScore_matrix().equals(sm))
-              {
-                pcaModel.setScore_matrix(sm);
-                Thread worker = new Thread(us);
-                worker.start();
-              }
+              ScoreModelI sm2 = ScoreModels.getInstance().getScoreModel(
+                      name, ap);
+              pcaModel.setScoreModel(sm2);
+              Thread worker = new Thread(PCAPanel.this);
+              worker.start();
             }
-          });
-          scoreMatrixMenu.add(jm);
-        }
+          }
+        });
+        scoreModelMenu.add(jm);
       }
     }
   }
@@ -193,7 +206,7 @@ public class PCAPanel extends GPCAPanel implements Runnable,
   public void bgcolour_actionPerformed(ActionEvent e)
   {
     Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_backgroud_colour"),
+            MessageManager.getString("label.select_background_colour"),
             rc.bgColour);
 
     if (col != null)
@@ -231,7 +244,6 @@ public class PCAPanel extends GPCAPanel implements Runnable,
       // rc.invalidate();
       nuclSetting.setSelected(pcaModel.isNucleotide());
       protSetting.setSelected(!pcaModel.isNucleotide());
-      jvVersionSetting.setSelected(pcaModel.isJvCalcMode());
       top = pcaModel.getTop();
 
     } catch (OutOfMemoryError er)
@@ -249,6 +261,7 @@ public class PCAPanel extends GPCAPanel implements Runnable,
       addKeyListener(rc);
       Desktop.addInternalFrame(this, MessageManager
               .getString("label.principal_component_analysis"), 475, 450);
+      this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
     }
   }
 
@@ -258,7 +271,8 @@ public class PCAPanel extends GPCAPanel implements Runnable,
     if (!pcaModel.isNucleotide())
     {
       pcaModel.setNucleotide(true);
-      pcaModel.setScore_matrix("DNA");
+      pcaModel.setScoreModel(ScoreModels.getInstance().getDefaultModel(
+              false));
       Thread worker = new Thread(this);
       worker.start();
     }
@@ -272,20 +286,13 @@ public class PCAPanel extends GPCAPanel implements Runnable,
     if (pcaModel.isNucleotide())
     {
       pcaModel.setNucleotide(false);
-      pcaModel.setScore_matrix("BLOSUM62");
+      pcaModel.setScoreModel(ScoreModels.getInstance()
+              .getDefaultModel(true));
       Thread worker = new Thread(this);
       worker.start();
     }
   }
 
-  @Override
-  protected void jvVersionSetting_actionPerfomed(ActionEvent arg0)
-  {
-    pcaModel.setJvCalcMode(jvVersionSetting.isSelected());
-    Thread worker = new Thread(this);
-    worker.start();
-  }
-
   /**
    * DOCUMENT ME!
    */
@@ -399,7 +406,7 @@ public class PCAPanel extends GPCAPanel implements Runnable,
     }
     ;
     Object[] alAndColsel = pcaModel.getSeqtrings()
-            .getAlignmentAndColumnSelection(gc);
+            .getAlignmentAndHiddenColumns(gc);
 
     if (alAndColsel != null && alAndColsel[0] != null)
     {
@@ -416,8 +423,8 @@ public class PCAPanel extends GPCAPanel implements Runnable,
       if (true)
       {
         // make a new frame!
-        AlignFrame af = new AlignFrame(al,
-                (ColumnSelection) alAndColsel[1], AlignFrame.DEFAULT_WIDTH,
+        AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
+                AlignFrame.DEFAULT_WIDTH,
                 AlignFrame.DEFAULT_HEIGHT);
 
         // >>>This is a fix for the moment, until a better solution is
index e36204c..a8e2f7e 100644 (file)
@@ -31,31 +31,22 @@ import jalview.commands.EditCommand.Action;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
-import jalview.datamodel.ColumnSelection;
 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;
+import jalview.gui.ColourMenuHelper.ColourChangeListener;
 import jalview.io.FileFormatI;
 import jalview.io.FileFormats;
 import jalview.io.FormatAdapter;
 import jalview.io.SequenceAnnotationReport;
-import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.Blosum62ColourScheme;
-import jalview.schemes.BuriedColourScheme;
-import jalview.schemes.ClustalxColourScheme;
-import jalview.schemes.HelixColourScheme;
-import jalview.schemes.HydrophobicColourScheme;
-import jalview.schemes.NucleotideColourScheme;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemes;
 import jalview.schemes.PIDColourScheme;
-import jalview.schemes.PurinePyrimidineColourScheme;
-import jalview.schemes.StrandColourScheme;
-import jalview.schemes.TaylorColourScheme;
-import jalview.schemes.TurnColourScheme;
-import jalview.schemes.UserColourScheme;
-import jalview.schemes.ZappoColourScheme;
 import jalview.util.GroupUrlLink;
 import jalview.util.GroupUrlLink.UrlStringTooLongException;
 import jalview.util.MessageManager;
@@ -64,23 +55,24 @@ import jalview.util.UrlLink;
 import java.awt.Color;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.Vector;
 
-import javax.swing.ButtonGroup;
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JColorChooser;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
 import javax.swing.JPopupMenu;
-import javax.swing.JRadioButtonMenuItem;
 
 /**
  * DOCUMENT ME!
@@ -88,44 +80,20 @@ import javax.swing.JRadioButtonMenuItem;
  * @author $author$
  * @version $Revision: 1.118 $
  */
-public class PopupMenu extends JPopupMenu
+public class PopupMenu extends JPopupMenu implements ColourChangeListener
 {
   JMenu groupMenu = new JMenu();
 
   JMenuItem groupName = new JMenuItem();
 
-  protected JRadioButtonMenuItem clustalColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem zappoColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem taylorColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem hydrophobicityColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem helixColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem strandColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem turnColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem buriedColour = new JRadioButtonMenuItem();
-
   protected JCheckBoxMenuItem abovePIDColour = new JCheckBoxMenuItem();
 
-  protected JRadioButtonMenuItem userDefinedColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem PIDColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem BLOSUM62Colour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem purinePyrimidineColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem RNAInteractionColour = new JRadioButtonMenuItem();
-
-  JRadioButtonMenuItem noColourmenuItem = new JRadioButtonMenuItem();
+  protected JMenuItem modifyPID = new JMenuItem();
 
   protected JCheckBoxMenuItem conservationMenuItem = new JCheckBoxMenuItem();
 
+  protected JMenuItem modifyConservation = new JMenuItem();
+
   AlignmentPanel ap;
 
   JMenu sequenceMenu = new JMenu();
@@ -148,8 +116,6 @@ public class PopupMenu extends JPopupMenu
 
   JMenuItem outline = new JMenuItem();
 
-  JRadioButtonMenuItem nucleotideMenuItem = new JRadioButtonMenuItem();
-
   JMenu colourMenu = new JMenu();
 
   JCheckBoxMenuItem showBoxes = new JCheckBoxMenuItem();
@@ -238,27 +204,11 @@ public class PopupMenu extends JPopupMenu
     this.ap = ap;
     sequence = seq;
 
-    ButtonGroup colours = new ButtonGroup();
-    colours.add(noColourmenuItem);
-    colours.add(clustalColour);
-    colours.add(zappoColour);
-    colours.add(taylorColour);
-    colours.add(hydrophobicityColour);
-    colours.add(helixColour);
-    colours.add(strandColour);
-    colours.add(turnColour);
-    colours.add(buriedColour);
-    colours.add(userDefinedColour);
-    colours.add(PIDColour);
-    colours.add(BLOSUM62Colour);
-    colours.add(purinePyrimidineColour);
-    colours.add(RNAInteractionColour);
-
     for (String ff : FileFormats.getInstance().getWritableFormats(true))
     {
       JMenuItem item = new JMenuItem(ff);
 
-      item.addActionListener(new java.awt.event.ActionListener()
+      item.addActionListener(new ActionListener()
       {
         @Override
         public void actionPerformed(ActionEvent e)
@@ -342,7 +292,7 @@ public class PopupMenu extends JPopupMenu
               menuItem.setText(MessageManager.formatMessage(
                       "label.2d_rna_structure_line",
                       new Object[] { aa.label }));
-              menuItem.addActionListener(new java.awt.event.ActionListener()
+              menuItem.addActionListener(new ActionListener()
               {
                 @Override
                 public void actionPerformed(ActionEvent e)
@@ -370,7 +320,7 @@ public class PopupMenu extends JPopupMenu
               menuItem.setText(MessageManager.formatMessage(
                       "label.2d_rna_sequence_name",
                       new Object[] { seq.getName() }));
-              menuItem.addActionListener(new java.awt.event.ActionListener()
+              menuItem.addActionListener(new ActionListener()
               {
                 @Override
                 public void actionPerformed(ActionEvent e)
@@ -391,7 +341,7 @@ public class PopupMenu extends JPopupMenu
 
       menuItem = new JMenuItem(
               MessageManager.getString("action.hide_sequences"));
-      menuItem.addActionListener(new java.awt.event.ActionListener()
+      menuItem.addActionListener(new ActionListener()
       {
         @Override
         public void actionPerformed(ActionEvent e)
@@ -407,7 +357,7 @@ public class PopupMenu extends JPopupMenu
         menuItem = new JMenuItem(MessageManager.formatMessage(
                 "label.represent_group_with",
                 new Object[] { seq.getName() }));
-        menuItem.addActionListener(new java.awt.event.ActionListener()
+        menuItem.addActionListener(new ActionListener()
         {
           @Override
           public void actionPerformed(ActionEvent e)
@@ -464,7 +414,6 @@ public class PopupMenu extends JPopupMenu
 
         add(menuItem);
       }
-
     }
 
     SequenceGroup sg = ap.av.getSelectionGroup();
@@ -476,75 +425,23 @@ public class PopupMenu extends JPopupMenu
       groupName.setText(MessageManager
               .getString("label.edit_name_and_description_current_group"));
 
-      if (sg.cs instanceof ZappoColourScheme)
-      {
-        zappoColour.setSelected(true);
-      }
-      else if (sg.cs instanceof TaylorColourScheme)
-      {
-        taylorColour.setSelected(true);
-      }
-      else if (sg.cs instanceof PIDColourScheme)
-      {
-        PIDColour.setSelected(true);
-      }
-      else if (sg.cs instanceof Blosum62ColourScheme)
-      {
-        BLOSUM62Colour.setSelected(true);
-      }
-      else if (sg.cs instanceof UserColourScheme)
-      {
-        userDefinedColour.setSelected(true);
-      }
-      else if (sg.cs instanceof HydrophobicColourScheme)
-      {
-        hydrophobicityColour.setSelected(true);
-      }
-      else if (sg.cs instanceof HelixColourScheme)
-      {
-        helixColour.setSelected(true);
-      }
-      else if (sg.cs instanceof StrandColourScheme)
-      {
-        strandColour.setSelected(true);
-      }
-      else if (sg.cs instanceof TurnColourScheme)
-      {
-        turnColour.setSelected(true);
-      }
-      else if (sg.cs instanceof BuriedColourScheme)
-      {
-        buriedColour.setSelected(true);
-      }
-      else if (sg.cs instanceof ClustalxColourScheme)
-      {
-        clustalColour.setSelected(true);
-      }
-      else if (sg.cs instanceof PurinePyrimidineColourScheme)
-      {
-        purinePyrimidineColour.setSelected(true);
-      }
+      ColourMenuHelper.setColourSelected(colourMenu, sg.getColourScheme());
 
-      /*
-       * else if (sg.cs instanceof CovariationColourScheme) {
-       * covariationColour.setSelected(true); }
-       */
-      else
-      {
-        noColourmenuItem.setSelected(true);
-      }
+      conservationMenuItem.setEnabled(!sg.isNucleotide());
 
       if (sg.cs != null)
       {
         if (sg.cs.conservationApplied())
         {
-        conservationMenuItem.setSelected(true);
+          conservationMenuItem.setSelected(true);
         }
         if (sg.cs.getThreshold() > 0)
         {
           abovePIDColour.setSelected(true);
         }
       }
+      modifyConservation.setEnabled(conservationMenuItem.isSelected());
+      modifyPID.setEnabled(abovePIDColour.isSelected());
       displayNonconserved.setSelected(sg.getShowNonconserved());
       showText.setSelected(sg.getDisplayText());
       showColourText.setSelected(sg.getColourText());
@@ -668,8 +565,6 @@ public class PopupMenu extends JPopupMenu
 
   }
 
-
-
   /**
    * Add annotation types to 'Show annotations' and/or 'Hide annotations' menus.
    * "All" is added first, followed by a separator. Then add any annotation
@@ -785,7 +680,7 @@ public class PopupMenu extends JPopupMenu
     label = label.substring(1, label.length() - 1); // a, b, c
     final JMenuItem item = new JMenuItem(label);
     item.setToolTipText(calcId);
-    item.addActionListener(new java.awt.event.ActionListener()
+    item.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -967,7 +862,7 @@ public class PopupMenu extends JPopupMenu
     JMenuItem item = new JMenuItem(label);
     item.setToolTipText(MessageManager.formatMessage(
             "label.open_url_param", new Object[] { url }));
-    item.addActionListener(new java.awt.event.ActionListener()
+    item.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -1044,7 +939,7 @@ public class PopupMenu extends JPopupMenu
   {
     groupMenu.setText(MessageManager.getString("label.selection"));
     groupName.setText(MessageManager.getString("label.name"));
-    groupName.addActionListener(new java.awt.event.ActionListener()
+    groupName.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -1055,7 +950,7 @@ public class PopupMenu extends JPopupMenu
     sequenceMenu.setText(MessageManager.getString("label.sequence"));
     sequenceName.setText(MessageManager
             .getString("label.edit_name_description"));
-    sequenceName.addActionListener(new java.awt.event.ActionListener()
+    sequenceName.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -1065,7 +960,7 @@ public class PopupMenu extends JPopupMenu
     });
     chooseAnnotations.setText(MessageManager
             .getString("action.choose_annotations"));
-    chooseAnnotations.addActionListener(new java.awt.event.ActionListener()
+    chooseAnnotations.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -1075,7 +970,7 @@ public class PopupMenu extends JPopupMenu
     });
     sequenceDetails.setText(MessageManager
             .getString("label.sequence_details"));
-    sequenceDetails.addActionListener(new java.awt.event.ActionListener()
+    sequenceDetails.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -1085,19 +980,18 @@ public class PopupMenu extends JPopupMenu
     });
     sequenceSelDetails.setText(MessageManager
             .getString("label.sequence_details"));
-    sequenceSelDetails
-            .addActionListener(new java.awt.event.ActionListener()
-            {
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                sequenceSelectionDetails_actionPerformed();
-              }
-            });
-    PIDColour.setFocusPainted(false);
+    sequenceSelDetails.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        sequenceSelectionDetails_actionPerformed();
+      }
+    });
+
     unGroupMenuItem
             .setText(MessageManager.getString("action.remove_group"));
-    unGroupMenuItem.addActionListener(new java.awt.event.ActionListener()
+    unGroupMenuItem.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -1107,36 +1001,24 @@ public class PopupMenu extends JPopupMenu
     });
     createGroupMenuItem.setText(MessageManager
             .getString("action.create_group"));
-    createGroupMenuItem
-            .addActionListener(new java.awt.event.ActionListener()
-            {
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                createGroupMenuItem_actionPerformed();
-              }
-            });
-
-    outline.setText(MessageManager.getString("action.border_colour"));
-    outline.addActionListener(new java.awt.event.ActionListener()
+    createGroupMenuItem.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        outline_actionPerformed();
+        createGroupMenuItem_actionPerformed();
       }
     });
-    nucleotideMenuItem
-            .setText(MessageManager.getString("label.nucleotide"));
-    nucleotideMenuItem.addActionListener(new ActionListener()
+
+    outline.setText(MessageManager.getString("action.border_colour"));
+    outline.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        nucleotideMenuItem_actionPerformed();
+        outline_actionPerformed();
       }
     });
-    colourMenu.setText(MessageManager.getString("label.group_colour"));
     showBoxes.setText(MessageManager.getString("action.boxes"));
     showBoxes.setState(true);
     showBoxes.addActionListener(new ActionListener()
@@ -1167,7 +1049,7 @@ public class PopupMenu extends JPopupMenu
       }
     });
     displayNonconserved.setText(MessageManager
-            .getString("label.show_non_conversed"));
+            .getString("label.show_non_conserved"));
     displayNonconserved.setState(true);
     displayNonconserved.addActionListener(new ActionListener()
     {
@@ -1243,15 +1125,6 @@ public class PopupMenu extends JPopupMenu
         sequenceFeature_actionPerformed();
       }
     });
-    textColour.setText(MessageManager.getString("label.text_colour"));
-    textColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        textColour_actionPerformed();
-      }
-    });
     jMenu1.setText(MessageManager.getString("label.group"));
     pdbStructureDialog.setText(MessageManager
             .getString("label.show_pdbstruct_dialog"));
@@ -1306,12 +1179,7 @@ public class PopupMenu extends JPopupMenu
         hideInsertions_actionPerformed(e);
       }
     });
-    /*
-     * annotationMenuItem.setText("By Annotation");
-     * annotationMenuItem.addActionListener(new ActionListener() { public void
-     * actionPerformed(ActionEvent actionEvent) {
-     * annotationMenuItem_actionPerformed(actionEvent); } });
-     */
+
     groupMenu.add(sequenceSelDetails);
     add(groupMenu);
     add(sequenceMenu);
@@ -1343,49 +1211,10 @@ public class PopupMenu extends JPopupMenu
     sequenceMenu.add(sequenceName);
     sequenceMenu.add(sequenceDetails);
     sequenceMenu.add(makeReferenceSeq);
-    colourMenu.add(textColour);
-    colourMenu.add(noColourmenuItem);
-    colourMenu.add(clustalColour);
-    colourMenu.add(BLOSUM62Colour);
-    colourMenu.add(PIDColour);
-    colourMenu.add(zappoColour);
-    colourMenu.add(taylorColour);
-    colourMenu.add(hydrophobicityColour);
-    colourMenu.add(helixColour);
-    colourMenu.add(strandColour);
-    colourMenu.add(turnColour);
-    colourMenu.add(buriedColour);
-    colourMenu.add(nucleotideMenuItem);
-    if (ap.getAlignment().isNucleotide())
-    {
-      // JBPNote - commented since the colourscheme isn't functional
-      colourMenu.add(purinePyrimidineColour);
-    }
-    colourMenu.add(userDefinedColour);
 
-    if (jalview.gui.UserDefinedColours.getUserColourSchemes() != null)
-    {
-      java.util.Enumeration userColours = jalview.gui.UserDefinedColours
-              .getUserColourSchemes().keys();
+    initColourMenu();
+    buildColourMenu();
 
-      while (userColours.hasMoreElements())
-      {
-        JMenuItem item = new JMenuItem(userColours.nextElement().toString());
-        item.addActionListener(new ActionListener()
-        {
-          @Override
-          public void actionPerformed(ActionEvent evt)
-          {
-            userDefinedColour_actionPerformed(evt);
-          }
-        });
-        colourMenu.add(item);
-      }
-    }
-
-    colourMenu.addSeparator();
-    colourMenu.add(abovePIDColour);
-    colourMenu.add(conservationMenuItem);
     editMenu.add(copy);
     editMenu.add(cut);
     editMenu.add(editSequence);
@@ -1403,160 +1232,119 @@ public class PopupMenu extends JPopupMenu
     jMenu1.add(showColourText);
     jMenu1.add(outline);
     jMenu1.add(displayNonconserved);
-    noColourmenuItem.setText(MessageManager.getString("label.none"));
-    noColourmenuItem.addActionListener(new java.awt.event.ActionListener()
+  }
+  
+  /**
+   * Constructs the entries for the colour menu
+   */
+  protected void initColourMenu()
+  {
+    colourMenu.setText(MessageManager.getString("label.group_colour"));
+    textColour.setText(MessageManager.getString("label.text_colour"));
+    textColour.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        noColourmenuItem_actionPerformed();
+        textColour_actionPerformed();
       }
     });
 
-    clustalColour.setText(MessageManager
-            .getString("label.clustalx_colours"));
-    clustalColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        clustalColour_actionPerformed();
-      }
-    });
-    zappoColour.setText(MessageManager.getString("label.zappo"));
-    zappoColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        zappoColour_actionPerformed();
-      }
-    });
-    taylorColour.setText(MessageManager.getString("label.taylor"));
-    taylorColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        taylorColour_actionPerformed();
-      }
-    });
-    hydrophobicityColour.setText(MessageManager
-            .getString("label.hydrophobicity"));
-    hydrophobicityColour
-            .addActionListener(new java.awt.event.ActionListener()
-            {
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                hydrophobicityColour_actionPerformed();
-              }
-            });
-    helixColour.setText(MessageManager.getString("label.helix_propensity"));
-    helixColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        helixColour_actionPerformed();
-      }
-    });
-    strandColour.setText(MessageManager
-            .getString("label.strand_propensity"));
-    strandColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        strandColour_actionPerformed();
-      }
-    });
-    turnColour.setText(MessageManager.getString("label.turn_propensity"));
-    turnColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        turnColour_actionPerformed();
-      }
-    });
-    buriedColour.setText(MessageManager.getString("label.buried_index"));
-    buriedColour.addActionListener(new java.awt.event.ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        buriedColour_actionPerformed();
-      }
-    });
     abovePIDColour.setText(MessageManager
-            .getString("label.above_identity_percentage"));
-    abovePIDColour.addActionListener(new java.awt.event.ActionListener()
+            .getString("label.above_identity_threshold"));
+    abovePIDColour.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        abovePIDColour_actionPerformed();
+        abovePIDColour_actionPerformed(abovePIDColour.isSelected());
       }
     });
-    userDefinedColour.setText(MessageManager
-            .getString("action.user_defined"));
-    userDefinedColour.addActionListener(new java.awt.event.ActionListener()
+
+    modifyPID.setText(MessageManager
+            .getString("label.modify_identity_threshold"));
+    modifyPID.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        userDefinedColour_actionPerformed(e);
+        modifyPID_actionPerformed();
       }
     });
-    PIDColour
-            .setText(MessageManager.getString("label.percentage_identity"));
-    PIDColour.addActionListener(new java.awt.event.ActionListener()
+
+    conservationMenuItem.setText(MessageManager
+            .getString("action.by_conservation"));
+    conservationMenuItem.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        PIDColour_actionPerformed();
+        conservationMenuItem_actionPerformed(conservationMenuItem
+                .isSelected());
       }
     });
-    BLOSUM62Colour.setText(MessageManager.getString("label.blosum62"));
-    BLOSUM62Colour.addActionListener(new java.awt.event.ActionListener()
+
+    modifyConservation.setText(MessageManager
+            .getString("label.modify_conservation_threshold"));
+    modifyConservation.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        BLOSUM62Colour_actionPerformed();
+        modifyConservation_actionPerformed();
       }
     });
-    purinePyrimidineColour.setText(MessageManager
-            .getString("label.purine_pyrimidine"));
-    purinePyrimidineColour
-            .addActionListener(new java.awt.event.ActionListener()
-            {
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                purinePyrimidineColour_actionPerformed();
-              }
-            });
+  }
 
-    /*
-     * covariationColour.addActionListener(new java.awt.event.ActionListener() {
-     * public void actionPerformed(ActionEvent e) {
-     * covariationColour_actionPerformed(); } });
-     */
+  /**
+   * Builds the group colour sub-menu, including any user-defined colours which
+   * were loaded at startup or during the Jalview session
+   */
+  protected void buildColourMenu()
+  {
+    SequenceGroup sg = ap.av.getSelectionGroup();
+    if (sg == null)
+    {
+      /*
+       * popup menu with no sequence group scope
+       */
+      return;
+    }
+    colourMenu.removeAll();
+    colourMenu.add(textColour);
+    colourMenu.addSeparator();
 
-    conservationMenuItem.setText(MessageManager
-            .getString("label.conservation"));
-    conservationMenuItem
-            .addActionListener(new java.awt.event.ActionListener()
-            {
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                conservationMenuItem_actionPerformed();
-              }
-            });
+    ColourMenuHelper.addMenuItems(colourMenu, this, sg, false);
+
+    colourMenu.addSeparator();
+    colourMenu.add(conservationMenuItem);
+    colourMenu.add(modifyConservation);
+    colourMenu.add(abovePIDColour);
+    colourMenu.add(modifyPID);
+  }
+
+  protected void modifyConservation_actionPerformed()
+  {
+    SequenceGroup sg = getGroup();
+    if (sg.cs != null)
+    {
+      SliderPanel.setConservationSlider(ap, sg.cs, sg.getName());
+      SliderPanel.showConservationSlider();
+    }
+  }
+
+  protected void modifyPID_actionPerformed()
+  {
+    SequenceGroup sg = getGroup();
+    if (sg.cs != null)
+    {
+      // int threshold = SliderPanel.setPIDSliderSource(ap, sg.cs, getGroup()
+      // .getName());
+      // sg.cs.setThreshold(threshold, ap.av.isIgnoreGapsConsensus());
+      SliderPanel.setPIDSliderSource(ap, sg.cs, getGroup()
+              .getName());
+      SliderPanel.showPIDSlider();
+    }
   }
 
   /**
@@ -1581,7 +1369,7 @@ public class PopupMenu extends JPopupMenu
      * Temporary store to hold distinct calcId / type pairs for the tooltip.
      * Using TreeMap means calcIds are shown in alphabetical order.
      */
-    Map<String, String> tipEntries = new TreeMap<String, String>();
+    SortedMap<String, String> tipEntries = new TreeMap<String, String>();
     final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<SequenceI, List<AlignmentAnnotation>>();
     AlignmentI al = this.ap.av.getAlignment();
     AlignmentUtils.findAddableReferenceAnnotations(forSequences,
@@ -1657,16 +1445,59 @@ public class PopupMenu extends JPopupMenu
 
   protected void hideInsertions_actionPerformed(ActionEvent actionEvent)
   {
-    if (sequence != null)
+
+    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())
     {
-      ColumnSelection cs = ap.av.getColumnSelection();
-      if (cs == null)
+      ap.av.getAlignment().getHiddenColumns().markHiddenRegions(mask);
+    }
+
+    boolean markedPopup = false;
+    // mark inserts in current selection
+    if (ap.av.getSelectionGroup() != null)
+    {
+      // 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);
+
+      // now clear columns without gaps
+      for (SequenceI sq : ap.av.getSelectionGroup().getSequences())
       {
-        cs = new ColumnSelection();
+        if (sq == sequence)
+        {
+          markedPopup = true;
+        }
+        inserts.and(sq.getInsertionsAsBits());
       }
-      cs.hideInsertionsFor(sequence);
-      ap.av.setColumnSelection(cs);
     }
+    else
+    {
+      // initially, mark all columns to be hidden
+      inserts.set(0, ap.av.getAlignment().getWidth());
+
+      // and clear out old hidden regions completely
+      mask.clear();
+    }
+
+    // now mark for sequence under popup if we haven't already done it
+    if (!markedPopup && sequence != null)
+    {
+      inserts.and(sequence.getInsertionsAsBits());
+    }
+
+    // finally, preserve hidden regions outside selection
+    inserts.or(mask);
+
+    // and set hidden columns accordingly
+    hidden.hideMarkedBits(inserts);
+
+    ap.av.getAlignment().setHiddenColumns(hidden);
     refresh();
   }
 
@@ -1730,121 +1561,6 @@ public class PopupMenu extends JPopupMenu
     PaintRefresher.Refresh(this, ap.av.getSequenceSetId());
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void clustalColour_actionPerformed()
-  {
-    SequenceGroup sg = getGroup();
-    sg.cs = new ClustalxColourScheme(sg, ap.av.getHiddenRepSequences());
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void zappoColour_actionPerformed()
-  {
-    getGroup().cs = new ZappoColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void taylorColour_actionPerformed()
-  {
-    getGroup().cs = new TaylorColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void hydrophobicityColour_actionPerformed()
-  {
-    getGroup().cs = new HydrophobicColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void helixColour_actionPerformed()
-  {
-    getGroup().cs = new HelixColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void strandColour_actionPerformed()
-  {
-    getGroup().cs = new StrandColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void turnColour_actionPerformed()
-  {
-    getGroup().cs = new TurnColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void buriedColour_actionPerformed()
-  {
-    getGroup().cs = new BuriedColourScheme();
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  public void nucleotideMenuItem_actionPerformed()
-  {
-    getGroup().cs = new NucleotideColourScheme();
-    refresh();
-  }
-
-  protected void purinePyrimidineColour_actionPerformed()
-  {
-    getGroup().cs = new PurinePyrimidineColourScheme();
-    refresh();
-  }
-
   /*
    * protected void covariationColour_actionPerformed() { getGroup().cs = new
    * CovariationColourScheme(sequence.getAnnotation()[0]); refresh(); }
@@ -1852,10 +1568,12 @@ public class PopupMenu extends JPopupMenu
   /**
    * DOCUMENT ME!
    * 
+   * @param selected
+   * 
    * @param e
    *          DOCUMENT ME!
    */
-  protected void abovePIDColour_actionPerformed()
+  public void abovePIDColour_actionPerformed(boolean selected)
   {
     SequenceGroup sg = getGroup();
     if (sg.cs == null)
@@ -1863,13 +1581,14 @@ public class PopupMenu extends JPopupMenu
       return;
     }
 
-    if (abovePIDColour.isSelected())
+    if (selected)
     {
       sg.cs.setConsensus(AAFrequency.calculate(
               sg.getSequences(ap.av.getHiddenRepSequences()),
               sg.getStartRes(), sg.getEndRes() + 1));
 
-      int threshold = SliderPanel.setPIDSliderSource(ap, sg.cs, getGroup()
+      int threshold = SliderPanel.setPIDSliderSource(ap,
+              sg.getGroupColourScheme(), getGroup()
               .getName());
 
       sg.cs.setThreshold(threshold, ap.av.isIgnoreGapsConsensus());
@@ -1880,36 +1599,14 @@ public class PopupMenu extends JPopupMenu
     // remove PIDColouring
     {
       sg.cs.setThreshold(0, ap.av.isIgnoreGapsConsensus());
+      SliderPanel.hidePIDSlider();
     }
+    modifyPID.setEnabled(selected);
 
     refresh();
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void userDefinedColour_actionPerformed(ActionEvent e)
-  {
-    SequenceGroup sg = getGroup();
-
-    if (e.getSource().equals(userDefinedColour))
-    {
-      new UserDefinedColours(ap, sg);
-    }
-    else
-    {
-      UserColourScheme udc = (UserColourScheme) UserDefinedColours
-              .getUserColourSchemes().get(e.getActionCommand());
-
-      sg.cs = udc;
-    }
-    refresh();
-  }
-
-  /**
    * Open a panel where the user can choose which types of sequence annotation
    * to show or hide.
    * 
@@ -1927,54 +1624,7 @@ public class PopupMenu extends JPopupMenu
    * @param e
    *          DOCUMENT ME!
    */
-  protected void PIDColour_actionPerformed()
-  {
-    SequenceGroup sg = getGroup();
-    sg.cs = new PIDColourScheme();
-    sg.cs.setConsensus(AAFrequency.calculate(
-            sg.getSequences(ap.av.getHiddenRepSequences()),
-            sg.getStartRes(), sg.getEndRes() + 1));
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void BLOSUM62Colour_actionPerformed()
-  {
-    SequenceGroup sg = getGroup();
-
-    sg.cs = new Blosum62ColourScheme();
-
-    sg.cs.setConsensus(AAFrequency.calculate(
-            sg.getSequences(ap.av.getHiddenRepSequences()),
-            sg.getStartRes(), sg.getEndRes() + 1));
-
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void noColourmenuItem_actionPerformed()
-  {
-    getGroup().cs = null;
-    refresh();
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void conservationMenuItem_actionPerformed()
+  public void conservationMenuItem_actionPerformed(boolean selected)
   {
     SequenceGroup sg = getGroup();
     if (sg.cs == null)
@@ -1982,7 +1632,7 @@ public class PopupMenu extends JPopupMenu
       return;
     }
 
-    if (conservationMenuItem.isSelected())
+    if (selected)
     {
       // JBPNote: Conservation name shouldn't be i18n translated
       Conservation c = new Conservation("Group", sg.getSequences(ap.av
@@ -1991,35 +1641,19 @@ public class PopupMenu extends JPopupMenu
 
       c.calculate();
       c.verdict(false, ap.av.getConsPercGaps());
-
       sg.cs.setConservation(c);
 
-      SliderPanel.setConservationSlider(ap, sg.cs, sg.getName());
+      SliderPanel.setConservationSlider(ap, sg.getGroupColourScheme(),
+              sg.getName());
       SliderPanel.showConservationSlider();
     }
     else
     // remove ConservationColouring
     {
       sg.cs.setConservation(null);
+      SliderPanel.hideConservationSlider();
     }
-
-    refresh();
-  }
-
-  public void annotationMenuItem_actionPerformed(ActionEvent actionEvent)
-  {
-    SequenceGroup sg = getGroup();
-    if (sg == null)
-    {
-      return;
-    }
-
-    AnnotationColourGradient acg = new AnnotationColourGradient(
-            sequence.getAnnotation()[0], null,
-            AnnotationColourGradient.NO_THRESHOLD);
-
-    acg.setPredefinedColours(true);
-    sg.cs = acg;
+    modifyConservation.setEnabled(selected);
 
     refresh();
   }
@@ -2279,8 +1913,7 @@ public class PopupMenu extends JPopupMenu
     // or we simply trust the user wants
     // wysiwig behaviour
 
-    FileFormatI fileFormat = FileFormats.getInstance().forName(
-            e.getActionCommand());
+    FileFormatI fileFormat = FileFormats.getInstance().forName(e.getActionCommand());
     cap.setText(new FormatAdapter(ap).formatSequences(fileFormat, ap, true));
   }
 
@@ -2292,28 +1925,25 @@ public class PopupMenu extends JPopupMenu
       return;
     }
 
-    int rsize = 0, gSize = sg.getSize();
-    SequenceI[] rseqs, seqs = new SequenceI[gSize];
-    SequenceFeature[] tfeatures, features = new SequenceFeature[gSize];
+    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
 
+    /*
+     * assemble dataset sequences, and template new sequence features,
+     * for the amend features dialog
+     */
+    int gSize = sg.getSize();
     for (int i = 0; i < gSize; i++)
     {
       int start = sg.getSequenceAt(i).findPosition(sg.getStartRes());
       int end = sg.findEndRes(sg.getSequenceAt(i));
       if (start <= end)
       {
-        seqs[rsize] = sg.getSequenceAt(i).getDatasetSequence();
-        features[rsize] = new SequenceFeature(null, null, null, start, end,
-                "Jalview");
-        rsize++;
+        seqs.add(sg.getSequenceAt(i).getDatasetSequence());
+        features.add(new SequenceFeature(null, null, start, end, null));
       }
     }
-    rseqs = new SequenceI[rsize];
-    tfeatures = new SequenceFeature[rsize];
-    System.arraycopy(seqs, 0, rseqs, 0, rsize);
-    System.arraycopy(features, 0, tfeatures, 0, rsize);
-    features = tfeatures;
-    seqs = rseqs;
+
     if (ap.getSeqPanel().seqCanvas.getFeatureRenderer().amendFeatures(seqs,
             features, true, ap))
     {
@@ -2385,4 +2015,32 @@ public class PopupMenu extends JPopupMenu
     }
   }
 
+  /**
+   * Action on user selecting an item from the colour menu (that does not have
+   * its bespoke action handler)
+   * 
+   * @return
+   */
+  @Override
+  public void changeColour_actionPerformed(String colourSchemeName)
+  {
+    SequenceGroup sg = getGroup();
+    /*
+     * switch to the chosen colour scheme (or null for None)
+     */
+    ColourSchemeI colourScheme = ColourSchemes.getInstance()
+            .getColourScheme(colourSchemeName, sg,
+                    ap.av.getHiddenRepSequences());
+    sg.setColourScheme(colourScheme);
+    if (colourScheme instanceof Blosum62ColourScheme
+            || colourScheme instanceof PIDColourScheme)
+    {
+      sg.cs.setConsensus(AAFrequency.calculate(
+              sg.getSequences(ap.av.getHiddenRepSequences()),
+              sg.getStartRes(), sg.getEndRes() + 1));
+    }
+
+    refresh();
+  }
+
 }
index 422745a..cccdd2e 100755 (executable)
  */
 package jalview.gui;
 
-import static jalview.util.UrlConstants.DB_ACCESSION;
-import static jalview.util.UrlConstants.EMBLEBI_STRING;
-import static jalview.util.UrlConstants.SEQUENCE_ID;
-import static jalview.util.UrlConstants.SRS_STRING;
-
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.bin.Cache;
 import jalview.gui.Help.HelpId;
@@ -34,13 +29,21 @@ import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.jbgui.GPreferences;
 import jalview.jbgui.GSequenceLink;
-import jalview.schemes.ColourSchemeProperty;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemes;
+import jalview.schemes.ResidueColourScheme;
+import jalview.urls.UrlLinkTableModel;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.UrlConstants;
 import jalview.ws.sifts.SiftsSettings;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.event.ActionEvent;
@@ -49,14 +52,24 @@ import java.awt.event.MouseEvent;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.StringTokenizer;
-import java.util.Vector;
 
 import javax.help.HelpSetException;
 import javax.swing.JColorChooser;
 import javax.swing.JFileChooser;
 import javax.swing.JInternalFrame;
 import javax.swing.JPanel;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.RowSorter;
+import javax.swing.SortOrder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
 
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
 
@@ -94,6 +107,8 @@ public class Preferences extends GPreferences
 
   public static final String SHOW_AUTOCALC_ABOVE = "SHOW_AUTOCALC_ABOVE";
 
+  public static final String SHOW_OCCUPANCY = "SHOW_OCCUPANCY";
+
   private static final int MIN_FONT_SIZE = 1;
 
   private static final int MAX_FONT_SIZE = 30;
@@ -102,7 +117,9 @@ public class Preferences extends GPreferences
    * Holds name and link separated with | character. Sequence ID must be
    * $SEQUENCE_ID$ or $SEQUENCE_ID=/.possible | chars ./=$
    */
-  public static Vector<String> sequenceURLLinks;
+  public static UrlProviderI sequenceUrlLinks;
+
+  public static UrlLinkTableModel dataModel;
 
   /**
    * Holds name and link separated with | character. Sequence IDS and Sequences
@@ -115,40 +132,23 @@ public class Preferences extends GPreferences
   public static List<String> groupURLLinks;
   static
   {
-    String string = Cache.getDefault("SEQUENCE_LINKS", EMBLEBI_STRING);
-    sequenceURLLinks = new Vector<String>();
-
-    try
-    {
-      StringTokenizer st = new StringTokenizer(string, "|");
-      while (st.hasMoreElements())
-      {
-        String name = st.nextToken();
-        String url = st.nextToken();
-        // check for '|' within a regex
-        int rxstart = url.indexOf("$" + DB_ACCESSION + "$");
-        if (rxstart == -1)
-        {
-          rxstart = url.indexOf("$" + SEQUENCE_ID + "$");
-        }
-        while (rxstart == -1 && url.indexOf("/=$") == -1)
-        {
-          url = url + "|" + st.nextToken();
-        }
-        sequenceURLLinks.addElement(name + "|" + url);
-      }
-    } catch (Exception ex)
+    // get links selected to be in the menu (SEQUENCE_LINKS)
+    // and links entered by the user but not selected (STORED_LINKS)
+    String inMenuString = Cache.getDefault("SEQUENCE_LINKS", "");
+    String notInMenuString = Cache.getDefault("STORED_LINKS", "");
+    String defaultUrl = Cache.getDefault("DEFAULT_URL",
+            UrlConstants.DEFAULT_LABEL);
+
+    // if both links lists are empty, add the DEFAULT_URL link
+    // otherwise we assume the default link is in one of the lists
+    if (inMenuString.isEmpty() && notInMenuString.isEmpty())
     {
-      System.out.println(ex + "\nError parsing sequence links");
-    }
-    {
-      // upgrade old SRS link
-      int srsPos = sequenceURLLinks.indexOf(SRS_STRING);
-      if (srsPos > -1)
-      {
-        sequenceURLLinks.setElementAt(EMBLEBI_STRING, srsPos);
-      }
+      inMenuString = UrlConstants.DEFAULT_STRING;
     }
+    UrlProviderFactoryI factory = new DesktopUrlProviderFactory(defaultUrl,
+            inMenuString, notInMenuString);
+    sequenceUrlLinks = factory.createUrlProvider();
+    dataModel = new UrlLinkTableModel(sequenceUrlLinks);
 
     /**
      * TODO: reformulate groupURL encoding so two or more can be stored in the
@@ -158,8 +158,6 @@ public class Preferences extends GPreferences
     groupURLLinks = new ArrayList<String>();
   }
 
-  Vector<String> nameLinks, urlLinks;
-
   JInternalFrame frame;
 
   DasSourceBrowser dasSource;
@@ -214,6 +212,7 @@ public class Preferences extends GPreferences
     openoverv.setSelected(Cache.getDefault("SHOW_OVERVIEW", false));
     showUnconserved
             .setSelected(Cache.getDefault("SHOW_UNCONSERVED", false));
+    showOccupancy.setSelected(Cache.getDefault(SHOW_OCCUPANCY, false));
     showGroupConsensus.setSelected(Cache.getDefault("SHOW_GROUP_CONSENSUS",
             false));
     showGroupConservation.setSelected(Cache.getDefault(
@@ -290,12 +289,16 @@ public class Preferences extends GPreferences
     /*
      * Set Colours tab defaults
      */
-    for (int i = ColourSchemeProperty.FIRST_COLOUR; i <= ColourSchemeProperty.LAST_COLOUR; i++)
+    protColour.addItem(ResidueColourScheme.NONE);
+    nucColour.addItem(ResidueColourScheme.NONE);
+    for (ColourSchemeI cs : ColourSchemes.getInstance().getColourSchemes())
     {
-      protColour.addItem(ColourSchemeProperty.getColourName(i));
-      nucColour.addItem(ColourSchemeProperty.getColourName(i));
+      String name = cs.getSchemeName();
+      protColour.addItem(name);
+      nucColour.addItem(name);
     }
-    String oldProp = Cache.getDefault(DEFAULT_COLOUR, "None");
+    String oldProp = Cache.getDefault(DEFAULT_COLOUR,
+            ResidueColourScheme.NONE);
     String newProp = Cache.getDefault(DEFAULT_COLOUR_PROT, null);
     protColour.setSelectedItem(newProp != null ? newProp : oldProp);
     newProp = Cache.getDefault(DEFAULT_COLOUR_NUC, null);
@@ -343,20 +346,128 @@ public class Preferences extends GPreferences
     /*
      * Set Connections tab defaults
      */
-    nameLinks = new Vector<String>();
-    urlLinks = new Vector<String>();
-    for (int i = 0; i < sequenceURLLinks.size(); i++)
+
+    // set up sorting
+    linkUrlTable.setModel(dataModel);
+    final TableRowSorter<TableModel> sorter = new TableRowSorter<>(
+            linkUrlTable.getModel());
+    linkUrlTable.setRowSorter(sorter);
+    List<RowSorter.SortKey> sortKeys = new ArrayList<>();
+
+    UrlLinkTableModel m = (UrlLinkTableModel) linkUrlTable.getModel();
+    sortKeys.add(new RowSorter.SortKey(m.getPrimaryColumn(),
+            SortOrder.DESCENDING));
+    sortKeys.add(new RowSorter.SortKey(m.getSelectedColumn(),
+            SortOrder.DESCENDING));
+    sortKeys.add(new RowSorter.SortKey(m.getNameColumn(),
+            SortOrder.ASCENDING));
+
+    sorter.setSortKeys(sortKeys);
+    sorter.sort();
+    
+    // set up filtering
+    ActionListener onReset;
+    onReset = new ActionListener()
     {
-      String link = sequenceURLLinks.elementAt(i).toString();
-      nameLinks.addElement(link.substring(0, link.indexOf("|")));
-      urlLinks.addElement(link.substring(link.indexOf("|") + 1));
-    }
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filterTB.setText("");
+        sorter.setRowFilter(RowFilter.regexFilter(""));
+      }
+
+    };
+    doReset.addActionListener(onReset);
+
+    // filter to display only custom urls
+    final RowFilter<TableModel, Object> customUrlFilter = new RowFilter<TableModel, Object>()
+    {
+      @Override
+      public boolean include(
+              Entry<? extends TableModel, ? extends Object> entry)
+      {
+        return ((UrlLinkTableModel) entry.getModel()).isUserEntry(entry);
+      }
+    };
+
+    final TableRowSorter<TableModel> customSorter = new TableRowSorter<>(
+            linkUrlTable.getModel());
+    customSorter.setRowFilter(customUrlFilter);
+
+    ActionListener onCustomOnly;
+    onCustomOnly = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filterTB.setText("");
+        sorter.setRowFilter(customUrlFilter);
+      }
+    };
+    userOnly.addActionListener(onCustomOnly);
+
+    filterTB.getDocument().addDocumentListener(new DocumentListener()
+    {
+      String caseInsensitiveFlag = "(?i)";
+
+      @Override
+      public void changedUpdate(DocumentEvent e)
+      {
+        sorter.setRowFilter(RowFilter.regexFilter(caseInsensitiveFlag
+                + filterTB.getText()));
+      }
+
+      @Override
+      public void removeUpdate(DocumentEvent e)
+      {
+        sorter.setRowFilter(RowFilter.regexFilter(caseInsensitiveFlag
+                + filterTB.getText()));
+      }
+
+      @Override
+      public void insertUpdate(DocumentEvent e)
+      {
+        sorter.setRowFilter(RowFilter.regexFilter(caseInsensitiveFlag
+                + filterTB.getText()));
+      }
+    });
 
-    updateLinkData();
+    // set up list selection functionality
+    linkUrlTable.getSelectionModel().addListSelectionListener(
+            new UrlListSelectionHandler());
+
+    // set up radio buttons
+    int onClickCol = ((UrlLinkTableModel) linkUrlTable.getModel())
+            .getPrimaryColumn();
+    String onClickName = linkUrlTable.getColumnName(onClickCol);
+    linkUrlTable.getColumn(onClickName).setCellRenderer(
+               new RadioButtonRenderer());
+    linkUrlTable.getColumn(onClickName)
+            .setCellEditor(new RadioButtonEditor());
+
+    // get boolean columns and resize those to min possible
+    for (int column = 0; column < linkUrlTable.getColumnCount(); column++)
+    {
+      if (linkUrlTable.getModel().getColumnClass(column)
+              .equals(Boolean.class))
+      {
+        TableColumn tableColumn = linkUrlTable.getColumnModel().getColumn(
+                column);
+        int preferredWidth = tableColumn.getMinWidth();
+
+        TableCellRenderer cellRenderer = linkUrlTable.getCellRenderer(0,
+                column);
+        Component c = linkUrlTable.prepareRenderer(cellRenderer, 0, column);
+        int cwidth = c.getPreferredSize().width
+                + linkUrlTable.getIntercellSpacing().width;
+        preferredWidth = Math.max(preferredWidth, cwidth);
+
+        tableColumn.setPreferredWidth(preferredWidth);
+      }
+    }
 
     useProxy.setSelected(Cache.getDefault("USE_PROXY", false));
-    proxyServerTB.setEnabled(useProxy.isSelected());
-    proxyPortTB.setEnabled(useProxy.isSelected());
+    useProxy_actionPerformed(); // make sure useProxy is correctly initialised
     proxyServerTB.setText(Cache.getDefault("PROXY_SERVER", ""));
     proxyPortTB.setText(Cache.getDefault("PROXY_PORT", ""));
 
@@ -464,6 +575,8 @@ public class Preferences extends GPreferences
             Boolean.toString(idItalics.isSelected()));
     Cache.applicationProperties.setProperty("SHOW_UNCONSERVED",
             Boolean.toString(showUnconserved.isSelected()));
+    Cache.applicationProperties.setProperty(SHOW_OCCUPANCY,
+            Boolean.toString(showOccupancy.isSelected()));
     Cache.applicationProperties.setProperty("SHOW_GROUP_CONSENSUS",
             Boolean.toString(showGroupConsensus.isSelected()));
     Cache.applicationProperties.setProperty("SHOW_GROUP_CONSERVATION",
@@ -548,28 +661,32 @@ public class Preferences extends GPreferences
 
     jalview.util.BrowserLauncher.resetBrowser();
 
-    if (nameLinks.size() > 0)
+    // save user-defined and selected links
+    String menuLinks = sequenceUrlLinks.writeUrlsAsString(true);
+    if (menuLinks.isEmpty())
+    {
+      Cache.applicationProperties.remove("SEQUENCE_LINKS");
+    }
+    else
     {
-      StringBuffer links = new StringBuffer();
-      sequenceURLLinks = new Vector<String>();
-      for (int i = 0; i < nameLinks.size(); i++)
-      {
-        sequenceURLLinks.addElement(nameLinks.elementAt(i) + "|"
-                + urlLinks.elementAt(i));
-        links.append(sequenceURLLinks.elementAt(i).toString());
-        links.append("|");
-      }
-      // remove last "|"
-      links.setLength(links.length() - 1);
       Cache.applicationProperties.setProperty("SEQUENCE_LINKS",
-              links.toString());
+              menuLinks.toString());
+    }
+
+    String nonMenuLinks = sequenceUrlLinks.writeUrlsAsString(false);
+    if (nonMenuLinks.isEmpty())
+    {
+      Cache.applicationProperties.remove("STORED_LINKS");
     }
     else
     {
-      Cache.applicationProperties.remove("SEQUENCE_LINKS");
-      sequenceURLLinks.clear();
+      Cache.applicationProperties.setProperty("STORED_LINKS",
+              nonMenuLinks.toString());
     }
 
+    Cache.applicationProperties.setProperty("DEFAULT_URL",
+            sequenceUrlLinks.getPrimaryUrlId());
+
     Cache.applicationProperties.setProperty("USE_PROXY",
             Boolean.toString(useProxy.isSelected()));
 
@@ -739,6 +856,7 @@ public class Preferences extends GPreferences
     conservation.setEnabled(annotations.isSelected());
     quality.setEnabled(annotations.isSelected());
     identity.setEnabled(annotations.isSelected());
+    showOccupancy.setEnabled(annotations.isSelected());
     showGroupConsensus.setEnabled(annotations.isSelected());
     showGroupConservation.setEnabled(annotations.isSelected());
     showConsensHistogram.setEnabled(annotations.isSelected()
@@ -750,7 +868,6 @@ public class Preferences extends GPreferences
   @Override
   public void newLink_actionPerformed(ActionEvent e)
   {
-
     GSequenceLink link = new GSequenceLink();
     boolean valid = false;
     while (!valid)
@@ -761,10 +878,18 @@ public class Preferences extends GPreferences
       {
         if (link.checkValid())
         {
-          nameLinks.addElement(link.getName());
-          urlLinks.addElement(link.getURL());
-          updateLinkData();
-          valid = true;
+          if (((UrlLinkTableModel) linkUrlTable.getModel())
+                  .isUniqueName(link.getName()))
+          {
+            ((UrlLinkTableModel) linkUrlTable.getModel()).insertRow(
+                    link.getName(), link.getURL());
+            valid = true;
+          }
+          else
+          {
+            link.notifyDuplicate();
+            continue;
+          }
         }
       }
       else
@@ -779,36 +904,46 @@ public class Preferences extends GPreferences
   {
     GSequenceLink link = new GSequenceLink();
 
-    int index = linkNameList.getSelectedIndex();
+    int index = linkUrlTable.getSelectedRow();
     if (index == -1)
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString("label.no_link_selected"),
-              MessageManager.getString("label.no_link_selected"),
-              JvOptionPane.WARNING_MESSAGE);
+      // button no longer enabled if row is not selected
+      Cache.log.debug("Edit with no row selected in linkUrlTable");
       return;
     }
 
-    link.setName(nameLinks.elementAt(index).toString());
-    link.setURL(urlLinks.elementAt(index).toString());
+    int nameCol = ((UrlLinkTableModel) linkUrlTable.getModel())
+            .getNameColumn();
+    int urlCol = ((UrlLinkTableModel) linkUrlTable.getModel())
+            .getUrlColumn();
+    String oldName = linkUrlTable.getValueAt(index, nameCol).toString();
+    link.setName(oldName);
+    link.setURL(linkUrlTable.getValueAt(index, urlCol).toString());
 
     boolean valid = false;
     while (!valid)
     {
-
       if (JvOptionPane.showInternalConfirmDialog(Desktop.desktop, link,
-              MessageManager.getString("label.new_sequence_url_link"),
+              MessageManager.getString("label.edit_sequence_url_link"),
               JvOptionPane.OK_CANCEL_OPTION, -1, null) == JvOptionPane.OK_OPTION)
       {
         if (link.checkValid())
         {
-          nameLinks.setElementAt(link.getName(), index);
-          urlLinks.setElementAt(link.getURL(), index);
-          updateLinkData();
-          valid = true;
+          if ((oldName.equals(link.getName()))
+                  || (((UrlLinkTableModel) linkUrlTable.getModel())
+                          .isUniqueName(link.getName())))
+          {
+            linkUrlTable.setValueAt(link.getName(), index, nameCol);
+            linkUrlTable.setValueAt(link.getURL(), index, urlCol);
+            valid = true;
+          }
+          else
+          {
+            link.notifyDuplicate();
+            continue;
+          }
         }
       }
-
       else
       {
         break;
@@ -819,26 +954,24 @@ public class Preferences extends GPreferences
   @Override
   public void deleteLink_actionPerformed(ActionEvent e)
   {
-    int index = linkNameList.getSelectedIndex();
+    int index = linkUrlTable.getSelectedRow();
+    int modelIndex = -1;
     if (index == -1)
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString("label.no_link_selected"),
-              MessageManager.getString("label.no_link_selected"),
-              JvOptionPane.WARNING_MESSAGE);
+      // button no longer enabled if row is not selected
+      Cache.log.debug("Delete with no row selected in linkUrlTable");
       return;
     }
-    nameLinks.removeElementAt(index);
-    urlLinks.removeElementAt(index);
-    updateLinkData();
-  }
+    else
+    {
+      modelIndex = linkUrlTable.convertRowIndexToModel(index);
+    }
 
-  void updateLinkData()
-  {
-    linkNameList.setListData(nameLinks);
-    linkURLList.setListData(urlLinks);
+    // make sure we use the model index to delete, and not the table index
+    ((UrlLinkTableModel) linkUrlTable.getModel()).removeRow(modelIndex);
   }
 
+
   @Override
   public void defaultBrowser_mouseClicked(MouseEvent e)
   {
@@ -1055,4 +1188,45 @@ public class Preferences extends GPreferences
       return name.hashCode() + code.hashCode();
     }
   }
+  
+  private class UrlListSelectionHandler implements ListSelectionListener
+  {
+
+    @Override
+    public void valueChanged(ListSelectionEvent e)
+    {
+      ListSelectionModel lsm = (ListSelectionModel) e.getSource();
+
+      int index = lsm.getMinSelectionIndex();
+      if (index == -1)
+      {
+        // no selection, so disable delete/edit buttons
+        editLink.setEnabled(false);
+        deleteLink.setEnabled(false);
+        return;
+      }
+      int modelIndex = linkUrlTable.convertRowIndexToModel(index);
+
+      // enable/disable edit and delete link buttons
+      if (((UrlLinkTableModel) linkUrlTable.getModel())
+              .isRowDeletable(modelIndex))
+      {
+        deleteLink.setEnabled(true);
+      }
+      else
+      {
+        deleteLink.setEnabled(false);
+      }
+
+      if (((UrlLinkTableModel) linkUrlTable.getModel())
+              .isRowEditable(modelIndex))
+      {
+        editLink.setEnabled(true);
+      }
+      else
+      {
+        editLink.setEnabled(false);
+      }
+    }
+}
 }
index a9d2690..cbbcf70 100755 (executable)
@@ -82,6 +82,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
 
     slider.addChangeListener(new ChangeListener()
     {
+      @Override
       public void stateChanged(ChangeEvent evt)
       {
         valueField.setText(slider.getValue() + "");
@@ -105,6 +106,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
             false);
     frame.addInternalFrameListener(new InternalFrameAdapter()
     {
+      @Override
       public void internalFrameClosing(InternalFrameEvent evt)
       {
         ap.getIdPanel().getIdCanvas().setHighlighted(null);
@@ -125,6 +127,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public void run()
   {
     JProgressBar progress = new JProgressBar();
@@ -207,6 +210,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
    * @param e
    *          DOCUMENT ME!
    */
+  @Override
   public void applyButton_actionPerformed(ActionEvent e)
   {
     Vector del = new Vector();
@@ -271,6 +275,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
    * @param e
    *          DOCUMENT ME!
    */
+  @Override
   public void undoButton_actionPerformed(ActionEvent e)
   {
     if (historyList == null || historyList.isEmpty())
@@ -297,22 +302,4 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
     }
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  public void valueField_actionPerformed(ActionEvent e)
-  {
-    try
-    {
-      int i = Integer.parseInt(valueField.getText());
-      slider.setValue(i);
-    } catch (Exception ex)
-    {
-      valueField.setText(slider.getValue() + "");
-    }
-  }
-
 }
index 00d465a..cb19539 100755 (executable)
 package jalview.gui;
 
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.viewmodel.ViewportListenerI;
 
 import java.awt.Color;
 import java.awt.FontMetrics;
@@ -38,6 +40,7 @@ import java.awt.event.ActionListener;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
 import javax.swing.JMenuItem;
@@ -51,7 +54,7 @@ import javax.swing.ToolTipManager;
  * supports a range of mouse operations to select, hide or reveal columns.
  */
 public class ScalePanel extends JPanel implements MouseMotionListener,
-        MouseListener
+        MouseListener, ViewportListenerI
 {
   protected int offy = 4;
 
@@ -90,6 +93,8 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
 
     addMouseListener(this);
     addMouseMotionListener(this);
+
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   /**
@@ -101,12 +106,12 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
   @Override
   public void mousePressed(MouseEvent evt)
   {
-    int x = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int x = (evt.getX() / av.getCharWidth()) + av.getRanges().getStartRes();
     final int res;
 
     if (av.hasHiddenColumns())
     {
-      x = av.getColumnSelection().adjustForHiddenColumns(x);
+      x = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(x);
     }
 
     if (x >= av.getAlignment().getWidth())
@@ -172,7 +177,7 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
       });
       pop.add(item);
 
-      if (av.getColumnSelection().hasHiddenColumns())
+      if (av.getAlignment().getHiddenColumns().hasHiddenColumns())
       {
         item = new JMenuItem(MessageManager.getString("action.reveal_all"));
         item.addActionListener(new ActionListener()
@@ -282,11 +287,13 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
   {
     mouseDragging = false;
 
-    int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int res = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
 
     if (av.hasHiddenColumns())
     {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
+      res = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(res);
     }
 
     if (res >= av.getAlignment().getWidth())
@@ -326,77 +333,31 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Action on dragging the mouse in the scale panel is to expand or shrink the
+   * selection group range (including any hidden columns that it spans)
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseDragged(MouseEvent evt)
   {
     mouseDragging = true;
+    ColumnSelection cs = av.getColumnSelection();
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
 
-    int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
-    if (res < 0)
-    {
-      res = 0;
-    }
-
-    if (av.hasHiddenColumns())
-    {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
-    }
-
-    if (res >= av.getAlignment().getWidth())
-    {
-      res = av.getAlignment().getWidth() - 1;
-    }
-
-    if (res < min)
-    {
-      min = res;
-    }
-
-    if (res > max)
-    {
-      max = res;
-    }
+    int res = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
+    res = Math.max(0, res);
+    res = hidden.adjustForHiddenColumns(res);
+    res = Math.min(res, av.getAlignment().getWidth() - 1);
+    min = Math.min(res, min);
+    max = Math.max(res, max);
 
     SequenceGroup sg = av.getSelectionGroup();
-
     if (sg != null)
     {
       stretchingGroup = true;
-
-      if (!av.getColumnSelection().contains(res))
-      {
-        av.getColumnSelection().addElement(res);
-      }
-
-      if (res > sg.getStartRes())
-      {
-        sg.setEndRes(res);
-      }
-      if (res < sg.getStartRes())
-      {
-        sg.setStartRes(res);
-      }
-
-      int col;
-      for (int i = min; i <= max; i++)
-      {
-        col = i; // av.getColumnSelection().adjustForHiddenColumns(i);
-
-        if ((col < sg.getStartRes()) || (col > sg.getEndRes()))
-        {
-          av.getColumnSelection().removeElement(col);
-        }
-        else
-        {
-          av.getColumnSelection().addElement(col);
-        }
-      }
-
+      cs.stretchGroup(res, sg, min, max);
       ap.paintAlignment(false);
     }
   }
@@ -437,13 +398,15 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
       return;
     }
 
-    int res = (evt.getX() / av.getCharWidth()) + av.getStartRes();
+    int res = (evt.getX() / av.getCharWidth())
+            + av.getRanges().getStartRes();
 
-    res = av.getColumnSelection().adjustForHiddenColumns(res);
+    res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(res);
 
-    if (av.getColumnSelection().getHiddenColumns() != null)
+    if (av.getAlignment().getHiddenColumns().getHiddenRegions() != null)
     {
-      for (int[] region : av.getColumnSelection().getHiddenColumns())
+      for (int[] region : av.getAlignment().getHiddenColumns()
+              .getHiddenRegions())
       {
         if (res + 1 == region[0] || res - 1 == region[1])
         {
@@ -467,7 +430,8 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
   @Override
   public void paintComponent(Graphics g)
   {
-    drawScale(g, av.getStartRes(), av.getEndRes(), getWidth(), getHeight());
+    drawScale(g, av.getRanges().getStartRes(), av.getRanges().getEndRes(),
+            getWidth(), getHeight());
   }
 
   // scalewidth will normally be screenwidth,
@@ -490,7 +454,9 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
 
     // Fill the selected columns
     ColumnSelection cs = av.getColumnSelection();
-    int avCharWidth = av.getCharWidth(), avCharHeight = av.getCharHeight();
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+    int avCharWidth = av.getCharWidth();
+    int avCharHeight = av.getCharHeight();
 
     if (cs != null)
     {
@@ -503,9 +469,9 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
 
         if (av.hasHiddenColumns())
         {
-          if (cs.isVisible(sel))
+          if (hidden.isVisible(sel))
           {
-            sel = cs.findColumnPosition(sel);
+            sel = hidden.findColumnPosition(sel);
           }
           else
           {
@@ -532,13 +498,13 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
       // draw any hidden column markers
       gg.setColor(Color.blue);
       int res;
-      if (av.getShowHiddenMarkers()
-              && av.getColumnSelection().getHiddenColumns() != null)
+
+      if (av.getShowHiddenMarkers() && hidden.getHiddenRegions() != null)
       {
-        for (int i = 0; i < av.getColumnSelection().getHiddenColumns()
+        for (int i = 0; i < hidden.getHiddenRegions()
                 .size(); i++)
         {
-          res = av.getColumnSelection().findHiddenRegionPosition(i)
+          res = hidden.findHiddenRegionPosition(i)
                   - startx;
 
           if (res < 0 || res > widthx)
@@ -586,4 +552,11 @@ public class ScalePanel extends JPanel implements MouseMotionListener,
     }
   }
 
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    // Respond to viewport change events (e.g. alignment panel was scrolled)
+    repaint();
+  }
+
 }
index d015292..0e5e1b8 100755 (executable)
 package jalview.gui;
 
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
+import jalview.viewmodel.ViewportListenerI;
+import jalview.viewmodel.ViewportRanges;
 
 import java.awt.BasicStroke;
 import java.awt.BorderLayout;
@@ -36,6 +39,7 @@ import java.awt.Graphics2D;
 import java.awt.RenderingHints;
 import java.awt.Shape;
 import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeEvent;
 import java.util.List;
 
 import javax.swing.JComponent;
@@ -46,7 +50,7 @@ import javax.swing.JComponent;
  * @author $author$
  * @version $Revision$
  */
-public class SeqCanvas extends JComponent
+public class SeqCanvas extends JComponent implements ViewportListenerI
 {
   final FeatureRenderer fr;
 
@@ -87,6 +91,8 @@ public class SeqCanvas extends JComponent
     setLayout(new BorderLayout());
     PaintRefresher.Register(this, av.getSequenceSetId());
     setBackground(Color.white);
+
+    av.getRanges().addPropertyChangeListener(this);
   }
 
   public SequenceRenderer getSequenceRenderer()
@@ -164,14 +170,17 @@ public class SeqCanvas extends JComponent
 
     if (av.hasHiddenColumns())
     {
-      startx = av.getColumnSelection().adjustForHiddenColumns(startx);
-      endx = av.getColumnSelection().adjustForHiddenColumns(endx);
+      startx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(startx);
+      endx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(endx);
     }
 
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     // WEST SCALE
@@ -223,7 +232,8 @@ public class SeqCanvas extends JComponent
 
     if (av.hasHiddenColumns())
     {
-      endx = av.getColumnSelection().adjustForHiddenColumns(endx);
+      endx = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(endx);
     }
 
     SequenceI seq;
@@ -279,43 +289,43 @@ public class SeqCanvas extends JComponent
     gg.copyArea(horizontal * charWidth, vertical * charHeight, imgWidth,
             imgHeight, -horizontal * charWidth, -vertical * charHeight);
 
-    int sr = av.startRes;
-    int er = av.endRes;
-    int ss = av.startSeq;
-    int es = av.endSeq;
+    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;
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
     {
-      er++;
       transX = (er - sr - horizontal) * charWidth;
       sr = er - horizontal;
     }
     else if (horizontal < 0)
     {
-      er = sr - horizontal - 1;
+      er = sr - horizontal;
     }
     else if (vertical > 0) // scroll down
     {
       ss = es - vertical;
 
-      if (ss < av.startSeq)
+      if (ss < ranges.getStartSeq())
       { // ie scrolling too fast, more than a page at a time
-        ss = av.startSeq;
+        ss = ranges.getStartSeq();
       }
       else
       {
-        transY = imgHeight - (vertical * charHeight);
+        transY = imgHeight - ((vertical + 1) * charHeight);
       }
     }
     else if (vertical < 0)
     {
       es = ss - vertical;
 
-      if (es > av.endSeq)
+      if (es > ranges.getEndSeq())
       {
-        es = av.endSeq;
+        es = ranges.getEndSeq();
       }
     }
 
@@ -395,13 +405,15 @@ public class SeqCanvas extends JComponent
     gg.setColor(Color.white);
     gg.fillRect(0, 0, imgWidth, imgHeight);
 
+    ViewportRanges ranges = av.getRanges();
     if (av.getWrapAlignment())
     {
-      drawWrappedPanel(gg, getWidth(), getHeight(), av.startRes);
+      drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
     }
     else
     {
-      drawPanel(gg, av.startRes, av.endRes, av.startSeq, av.endSeq, 0);
+      drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
+              ranges.getStartSeq(), ranges.getEndSeq(), 0);
     }
 
     g.drawImage(lcimg, 0, 0, this);
@@ -503,7 +515,7 @@ public class SeqCanvas extends JComponent
 
     av.setWrappedWidth(cWidth);
 
-    av.endRes = av.startRes + cWidth;
+    av.getRanges().setEndRes(av.getRanges().getStartRes() + cWidth - 1);
 
     int endx;
     int ypos = hgap;
@@ -511,7 +523,8 @@ public class SeqCanvas extends JComponent
 
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getColumnSelection().findColumnPosition(maxwidth) - 1;
+      maxwidth = av.getAlignment().getHiddenColumns()
+              .findColumnPosition(maxwidth) - 1;
     }
 
     while ((ypos <= canvasHeight) && (startRes < maxwidth))
@@ -549,11 +562,10 @@ public class SeqCanvas extends JComponent
       {
         g.setColor(Color.blue);
         int res;
-        for (int i = 0; i < av.getColumnSelection().getHiddenColumns()
-                .size(); i++)
+        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+        for (int i = 0; i < hidden.getHiddenRegions().size(); i++)
         {
-          res = av.getColumnSelection().findHiddenRegionPosition(i)
-                  - startRes;
+          res = hidden.findHiddenRegionPosition(i) - startRes;
 
           if (res < 0 || res > endx - startRes)
           {
@@ -584,7 +596,7 @@ public class SeqCanvas extends JComponent
                 (int) clip.getBounds().getHeight());
       }
 
-      drawPanel(g, startRes, endx, 0, al.getHeight(), ypos);
+      drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
 
       if (av.isShowAnnotation())
       {
@@ -650,7 +662,8 @@ public class SeqCanvas extends JComponent
     }
     else
     {
-      List<int[]> regions = av.getColumnSelection().getHiddenColumns();
+      List<int[]> regions = av.getAlignment().getHiddenColumns()
+              .getHiddenRegions();
 
       int screenY = 0;
       int blockStart = startRes;
@@ -679,7 +692,7 @@ public class SeqCanvas extends JComponent
 
           g1.drawLine((blockEnd - blockStart + 1) * charWidth - 1,
                   0 + offset, (blockEnd - blockStart + 1) * charWidth - 1,
-                  (endSeq - startSeq) * charHeight + offset);
+                  (endSeq - startSeq + 1) * charHeight + offset);
         }
 
         g1.translate(-screenY * charWidth, 0);
@@ -718,7 +731,7 @@ public class SeqCanvas extends JComponent
 
     // / First draw the sequences
     // ///////////////////////////
-    for (int i = startSeq; i < endSeq; i++)
+    for (int i = startSeq; i <= endSeq; i++)
     {
       nextSeq = av.getAlignment().getSequenceAt(i);
       if (nextSeq == null)
@@ -733,7 +746,7 @@ public class SeqCanvas extends JComponent
       if (av.isShowSequenceFeatures())
       {
         fr.drawSequence(g, nextSeq, startRes, endRes, offset
-                + ((i - startSeq) * charHeight));
+                + ((i - startSeq) * charHeight), false);
       }
 
       // / Highlight search Results once all sequences have been drawn
@@ -802,7 +815,7 @@ public class SeqCanvas extends JComponent
         int top = -1;
         int bottom = -1;
 
-        for (i = startSeq; i < endSeq; i++)
+        for (i = startSeq; i <= endSeq; i++)
         {
           sx = (group.getStartRes() - startRes) * charWidth;
           sy = offset + ((i - startSeq) * charHeight);
@@ -971,4 +984,34 @@ public class SeqCanvas extends JComponent
 
     repaint();
   }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    if (!av.getWrapAlignment())
+    {
+      if (evt.getPropertyName().equals("startres")
+              || evt.getPropertyName().equals("endres"))
+      {
+        // Make sure we're not trying to draw a panel
+        // larger than the visible window
+        ViewportRanges vpRanges = av.getRanges();
+        int scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
+        if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
+        {
+          scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
+        }
+        else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
+        {
+          scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
+        }
+        fastPaint(scrollX, 0);
+      }
+      else if (evt.getPropertyName().equals("startseq")
+              || evt.getPropertyName().equals("endseq"))
+      {
+        fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+      }
+    }
+  }
 }
index 5b87445..5634ead 100644 (file)
@@ -27,6 +27,7 @@ import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -35,6 +36,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.io.SequenceAnnotationReport;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemes.ResidueProperties;
 import jalview.structure.SelectionListener;
 import jalview.structure.SelectionSource;
@@ -58,6 +60,7 @@ 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 javax.swing.JPanel;
@@ -81,6 +84,16 @@ public class SeqPanel extends JPanel implements MouseListener,
   /** 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;
@@ -167,6 +180,9 @@ public class SeqPanel extends JPanel implements MouseListener,
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
+
+    lastMouseColumn = -1;
+    lastMouseSeq = -1;
   }
 
   int startWrapBlock = -1;
@@ -180,7 +196,7 @@ public class SeqPanel extends JPanel implements MouseListener,
    * @param evt
    * @return
    */
-  int findRes(MouseEvent evt)
+  int findColumn(MouseEvent evt)
   {
     int res = 0;
     int x = evt.getX();
@@ -208,7 +224,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
 
       wrappedBlock = y / cHeight;
-      wrappedBlock += av.getStartRes() / cwidth;
+      wrappedBlock += av.getRanges().getStartRes() / cwidth;
 
       res = wrappedBlock * cwidth + x / av.getCharWidth();
 
@@ -221,17 +237,18 @@ public class SeqPanel extends JPanel implements MouseListener,
         // right-hand gutter
         x = seqCanvas.getX() + seqCanvas.getWidth();
       }
-      res = (x / av.getCharWidth()) + av.getStartRes();
-      if (res > av.getEndRes())
+      res = (x / av.getCharWidth()) + av.getRanges().getStartRes();
+      if (res > av.getRanges().getEndRes())
       {
         // moused off right
-        res = av.getEndRes();
+        res = av.getRanges().getEndRes();
       }
     }
 
     if (av.hasHiddenColumns())
     {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
+      res = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(res);
     }
 
     return res;
@@ -261,7 +278,9 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
     else
     {
-      seq = Math.min((y / av.getCharHeight()) + av.getStartSeq(), av
+      seq = Math.min((y / av.getCharHeight())
+              + av.getRanges().getStartSeq(),
+              av
               .getAlignment().getHeight() - 1);
     }
 
@@ -335,20 +354,23 @@ public class SeqPanel extends JPanel implements MouseListener,
   {
     seqCanvas.cursorX += dx;
     seqCanvas.cursorY += dy;
+
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+
     if (av.hasHiddenColumns()
-            && !av.getColumnSelection().isVisible(seqCanvas.cursorX))
+ && !hidden.isVisible(seqCanvas.cursorX))
     {
       int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
 
-      while (!av.getColumnSelection().isVisible(seqCanvas.cursorX)
+      while (!hidden.isVisible(seqCanvas.cursorX)
               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
       {
         seqCanvas.cursorX += dx;
       }
 
       if (seqCanvas.cursorX >= maxWidth
-              || !av.getColumnSelection().isVisible(seqCanvas.cursorX))
+              || !hidden.isVisible(seqCanvas.cursorX))
       {
         seqCanvas.cursorX = original;
       }
@@ -380,37 +402,11 @@ public class SeqPanel extends JPanel implements MouseListener,
     endEditing();
     if (av.getWrapAlignment())
     {
-      ap.scrollToWrappedVisible(seqCanvas.cursorX);
+      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
     }
     else
     {
-      while (seqCanvas.cursorY < av.startSeq)
-      {
-        ap.scrollUp(true);
-      }
-      while (seqCanvas.cursorY + 1 > av.endSeq)
-      {
-        ap.scrollUp(false);
-      }
-      if (!av.getWrapAlignment())
-      {
-        while (seqCanvas.cursorX < av.getColumnSelection()
-                .adjustForHiddenColumns(av.startRes))
-        {
-          if (!ap.scrollRight(false))
-          {
-            break;
-          }
-        }
-        while (seqCanvas.cursorX > av.getColumnSelection()
-                .adjustForHiddenColumns(av.endRes))
-        {
-          if (!ap.scrollRight(true))
-          {
-            break;
-          }
-        }
-      }
+      av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY);
     }
     setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
             seqCanvas.cursorX, seqCanvas.cursorY);
@@ -580,6 +576,7 @@ public class SeqPanel extends JPanel implements MouseListener,
   @Override
   public void mouseReleased(MouseEvent evt)
   {
+    boolean didDrag = mouseDragging; // did we come here after a drag
     mouseDragging = false;
     mouseWheelPressed = false;
 
@@ -592,7 +589,7 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (!editingSeqs)
     {
-      doMouseReleasedDefineMode(evt);
+      doMouseReleasedDefineMode(evt, didDrag);
       return;
     }
 
@@ -632,7 +629,7 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
 
     int seq = findSeq(evt);
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (seq < 0 || res < 0)
     {
@@ -686,17 +683,16 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (av.isFollowHighlight())
     {
-      /*
-       * if scrollToPosition requires a scroll adjustment, this flag prevents
-       * another scroll event being propagated back to the originator
-       * 
-       * @see AlignmentPanel#adjustmentValueChanged
-       */
-      ap.setDontScrollComplement(true);
+      // don't allow highlight of protein/cDNA to also scroll a complementary
+      // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
+      // 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))
       {
         seqCanvas.revalidate();
       }
+      ap.setToScrollComplementPanel(true);
     }
     setStatusMessage(results);
     seqCanvas.highlightSearchResults(results);
@@ -731,25 +727,38 @@ public class SeqPanel extends JPanel implements MouseListener,
       mouseDragged(evt);
     }
 
-    int res = findRes(evt);
+    final int column = findColumn(evt);
     int seq = findSeq(evt);
-    int pos;
-    if (res < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+    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);
 
-    if (res >= sequence.getLength())
+    if (column >= sequence.getLength())
     {
       return;
     }
 
-    pos = setStatusMessage(sequence, res, seq);
-    if (ssm != null && pos > -1)
+    /*
+     * set status bar message, returning residue position in sequence
+     */
+    boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
+    final int pos = setStatusMessage(sequence, column, seq);
+    if (ssm != null && !isGapped)
     {
-      mouseOverSequence(sequence, res, pos);
+      mouseOverSequence(sequence, column, pos);
     }
 
     tooltipText.setLength(6); // Cuts the buffer back to <html>
@@ -759,7 +768,8 @@ public class SeqPanel extends JPanel implements MouseListener,
     {
       for (int g = 0; g < groups.length; g++)
       {
-        if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
+        if (groups[g].getStartRes() <= column
+                && groups[g].getEndRes() >= column)
         {
           if (!groups[g].getName().startsWith("JTreeGroup")
                   && !groups[g].getName().startsWith("JGroup"))
@@ -775,14 +785,16 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
     }
 
-    // use aa to see if the mouse pointer is on a
+    /*
+     * add any features at the position to the tooltip; if over a gap, only
+     * add features that straddle the gap (pos may be the residue before or
+     * after the gap)
+     */
     if (av.isShowSequenceFeatures())
     {
-      int rpos;
       List<SequenceFeature> features = ap.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(),
-                      rpos = sequence.findPosition(res));
-      seqARep.appendFeatures(tooltipText, rpos, features,
+              .findFeaturesAtColumn(sequence, column + 1);
+      seqARep.appendFeatures(tooltipText, pos, features,
               this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
     }
     if (tooltipText.length() == 6) // <html>
@@ -792,18 +804,15 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
     else
     {
-      if (lastTooltip == null
-              || !lastTooltip.equals(tooltipText.toString()))
+      String textString = tooltipText.toString();
+      if (lastTooltip == null || !lastTooltip.equals(textString))
       {
-        String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
-                tooltipText.toString());
-        // String formatedTooltipText = tooltipText.toString();
-        setToolTipText(formatedTooltipText);
-        lastTooltip = tooltipText.toString();
+        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
+                textString);
+        setToolTipText(formattedTooltipText);
+        lastTooltip = textString;
       }
-
     }
-
   }
 
   private Point lastp = null;
@@ -849,59 +858,88 @@ public class SeqPanel extends JPanel implements MouseListener,
   // avcontroller or viewModel
 
   /**
-   * Set status message in alignment panel
+   * Sets the status message in alignment panel, showing the sequence number
+   * (index) and id, and residue and residue position if not at a gap, for the
+   * given sequence and column position. Returns the residue position returned
+   * by Sequence.findPosition. Note this may be for the nearest adjacent residue
+   * if at a gapped position.
    * 
    * @param sequence
    *          aligned sequence object
-   * @param res
+   * @param column
    *          alignment column
-   * @param seq
+   * @param seqIndex
    *          index of sequence in alignment
-   * @return position of res in sequence
+   * @return sequence position of residue at column, or adjacent residue if at a
+   *         gap
+   */
+  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)
    */
-  int setStatusMessage(SequenceI sequence, int res, int seq)
+  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());
 
     String residue = null;
+
     /*
      * Try to translate the display character to residue name (null for gap).
      */
-    final String displayChar = String.valueOf(sequence.getCharAt(res));
-    if (av.getAlignment().isNucleotide())
+    boolean isGapped = Comparison.isGap(sequenceChar);
+
+    if (!isGapped)
     {
-      residue = ResidueProperties.nucleotideName.get(displayChar);
-      if (residue != null)
+      boolean nucleotide = av.getAlignment().isNucleotide();
+      String displayChar = String.valueOf(sequenceChar);
+      if (nucleotide)
       {
-        text.append(" Nucleotide: ").append(residue);
+        residue = ResidueProperties.nucleotideName.get(displayChar);
       }
-    }
-    else
-    {
-      residue = "X".equalsIgnoreCase(displayChar) ? "X" : ("*"
-              .equals(displayChar) ? "STOP" : ResidueProperties.aa2Triplet
-              .get(displayChar));
-      if (residue != null)
+      else
       {
-        text.append(" Residue: ").append(residue);
+        residue = "X".equalsIgnoreCase(displayChar) ? "X" : ("*"
+                .equals(displayChar) ? "STOP"
+                : ResidueProperties.aa2Triplet.get(displayChar));
       }
-    }
+      text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
+              .append(": ").append(residue == null ? displayChar : residue);
 
-    int pos = -1;
-    if (residue != null)
-    {
-      pos = sequence.findPosition(res);
-      text.append(" (").append(Integer.toString(pos)).append(")");
+      text.append(" (").append(Integer.toString(residuePos)).append(")");
     }
     ap.alignFrame.statusBar.setText(text.toString());
-    return pos;
   }
 
   /**
@@ -929,42 +967,45 @@ public class SeqPanel extends JPanel implements MouseListener,
 
       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;
       }
     }
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param evt
-   *          DOCUMENT ME!
+   * {@inheritDoc}
    */
   @Override
   public void mouseDragged(MouseEvent evt)
   {
     if (mouseWheelPressed)
     {
+      boolean inSplitFrame = ap.av.getCodingComplement() != null;
+      boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
+
       int oldWidth = av.getCharWidth();
 
       // Which is bigger, left-right or up-down?
       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math.abs(evt
               .getX() - lastMousePress.getX()))
       {
+        /*
+         * on drag up or down, decrement or increment font size
+         */
         int fontSize = av.font.getSize();
+        boolean fontChanged = false;
 
         if (evt.getY() < lastMousePress.getY())
         {
+          fontChanged = true;
           fontSize--;
         }
         else if (evt.getY() > lastMousePress.getY())
         {
+          fontChanged = true;
           fontSize++;
         }
 
@@ -973,24 +1014,56 @@ public class SeqPanel extends JPanel implements MouseListener,
           fontSize = 1;
         }
 
-        av.setFont(
-                new Font(av.font.getName(), av.font.getStyle(), fontSize),
-                true);
-        av.setCharWidth(oldWidth);
-        ap.fontChanged();
+        if (fontChanged)
+        {
+          Font newFont = new Font(av.font.getName(), av.font.getStyle(),
+                  fontSize);
+          av.setFont(newFont, true);
+          av.setCharWidth(oldWidth);
+          ap.fontChanged();
+          if (copyChanges)
+          {
+            ap.av.getCodingComplement().setFont(newFont, true);
+            SplitFrame splitFrame = (SplitFrame) ap.alignFrame
+                    .getSplitViewContainer();
+            splitFrame.adjustLayout();
+            splitFrame.repaint();
+          }
+        }
       }
       else
       {
+        /*
+         * on drag left or right, decrement or increment character width
+         */
+        int newWidth = 0;
         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
         {
-          av.setCharWidth(av.getCharWidth() - 1);
+          newWidth = av.getCharWidth() - 1;
+          av.setCharWidth(newWidth);
         }
         else if (evt.getX() > lastMousePress.getX())
         {
-          av.setCharWidth(av.getCharWidth() + 1);
+          newWidth = av.getCharWidth() + 1;
+          av.setCharWidth(newWidth);
+        }
+        if (newWidth > 0)
+        {
+          ap.paintAlignment(false);
+          if (copyChanges)
+          {
+            /*
+             * need to ensure newWidth is set on cdna, regardless of which
+             * panel the mouse drag happened in; protein will compute its 
+             * character width as 1:1 or 3:1
+             */
+            av.getCodingComplement().setCharWidth(newWidth);
+            SplitFrame splitFrame = (SplitFrame) ap.alignFrame
+                    .getSplitViewContainer();
+            splitFrame.adjustLayout();
+            splitFrame.repaint();
+          }
         }
-
-        ap.paintAlignment(false);
       }
 
       FontMetrics fm = getFontMetrics(av.getFont());
@@ -1007,7 +1080,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       return;
     }
 
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (res < 0)
     {
@@ -1140,8 +1213,10 @@ public class SeqPanel extends JPanel implements MouseListener,
     if (av.hasHiddenColumns())
     {
       fixedColumns = true;
-      int y1 = av.getColumnSelection().getHiddenBoundaryLeft(startres);
-      int y2 = av.getColumnSelection().getHiddenBoundaryRight(startres);
+      int y1 = av.getAlignment().getHiddenColumns()
+              .getHiddenBoundaryLeft(startres);
+      int y2 = av.getAlignment().getHiddenColumns()
+              .getHiddenBoundaryRight(startres);
 
       if ((insertGap && startres > y1 && lastres < y1)
               || (!insertGap && startres < y2 && lastres > y2))
@@ -1216,8 +1291,8 @@ public class SeqPanel extends JPanel implements MouseListener,
         {
           if (sg.getSize() == av.getAlignment().getHeight())
           {
-            if ((av.hasHiddenColumns() && startres < av
-                    .getColumnSelection().getHiddenBoundaryRight(startres)))
+            if ((av.hasHiddenColumns() && startres < av.getAlignment()
+                    .getHiddenColumns().getHiddenBoundaryRight(startres)))
             {
               endEditing();
               return;
@@ -1481,6 +1556,11 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
   }
 
+  /**
+   * Handler for double-click on a position with one or more sequence features.
+   * Opens the Amend Features dialog to allow feature details to be amended, or
+   * the feature deleted.
+   */
   @Override
   public void mouseClicked(MouseEvent evt)
   {
@@ -1495,24 +1575,32 @@ public class SeqPanel extends JPanel implements MouseListener,
         av.setSelectionGroup(null);
       }
 
+      int column = findColumn(evt);
+
+      /*
+       * 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(findRes(evt)));
+              .findFeaturesAtColumn(sequence, column + 1);
 
-      if (features != null && features.size() > 0)
+      if (!features.isEmpty())
       {
+        /*
+         * 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);
-      }
-      if (features != null && features.size() > 0)
-      {
-        seqCanvas.getFeatureRenderer().amendFeatures(
-                new SequenceI[] { sequence },
-                features.toArray(new SequenceFeature[features.size()]),
-                false, ap);
 
+        /*
+         * open the Amend Features dialog; clear highlighting afterwards,
+         * whether changes were made or not
+         */
+        List<SequenceI> seqs = Collections.singletonList(sequence);
+        seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
+                ap);
         seqCanvas.highlightSearchResults(null);
       }
     }
@@ -1526,23 +1614,23 @@ public class SeqPanel extends JPanel implements MouseListener,
     {
       if (e.isShiftDown())
       {
-        ap.scrollRight(true);
+        av.getRanges().scrollRight(true);
 
       }
       else
       {
-        ap.scrollUp(false);
+        av.getRanges().scrollUp(false);
       }
     }
     else
     {
       if (e.isShiftDown())
       {
-        ap.scrollRight(false);
+        av.getRanges().scrollRight(false);
       }
       else
       {
-        ap.scrollUp(true);
+        av.getRanges().scrollUp(true);
       }
     }
     // TODO Update tooltip for new position.
@@ -1556,7 +1644,7 @@ public class SeqPanel extends JPanel implements MouseListener,
    */
   public void doMousePressedDefineMode(MouseEvent evt)
   {
-    final int res = findRes(evt);
+    final int res = findColumn(evt);
     final int seq = findSeq(evt);
     oldSeq = seq;
     needOverviewUpdate = false;
@@ -1586,34 +1674,15 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     stretchGroup = av.getSelectionGroup();
 
-    if (stretchGroup == null)
+    if (stretchGroup == null || !stretchGroup.contains(sequence, res))
     {
       stretchGroup = av.getAlignment().findGroup(sequence, res);
-      av.setSelectionGroup(stretchGroup);
-    }
-    if (stretchGroup == null
-            || !stretchGroup.getSequences(null).contains(sequence)
-            || (stretchGroup.getStartRes() > res)
-            || (stretchGroup.getEndRes() < res))
-    {
-      stretchGroup = null;
-
-      SequenceGroup[] allGroups = av.getAlignment().findAllGroups(sequence);
-
-      if (allGroups != null)
+      if (stretchGroup != null)
       {
-        for (int i = 0; i < allGroups.length; i++)
-        {
-          if ((allGroups[i].getStartRes() <= res)
-                  && (allGroups[i].getEndRes() >= res))
-          {
-            stretchGroup = allGroups[i];
-            break;
-          }
-        }
+        // only update the current selection if the popup menu has a group to
+        // focus on
+        av.setSelectionGroup(stretchGroup);
       }
-
-      av.setSelectionGroup(stretchGroup);
     }
 
     if (evt.isPopupTrigger()) // Mac: mousePressed
@@ -1634,7 +1703,7 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (av.cursorMode)
     {
-      seqCanvas.cursorX = findRes(evt);
+      seqCanvas.cursorX = findColumn(evt);
       seqCanvas.cursorY = findSeq(evt);
       seqCanvas.repaint();
       return;
@@ -1654,15 +1723,16 @@ public class SeqPanel extends JPanel implements MouseListener,
 
       if (av.getConservationSelected())
       {
-        SliderPanel.setConservationSlider(ap, av.getGlobalColourScheme(),
-                "Background");
+        SliderPanel.setConservationSlider(ap, av.getResidueShading(),
+                ap.getViewName());
       }
 
       if (av.getAbovePIDThreshold())
       {
-        SliderPanel.setPIDSliderSource(ap, av.getGlobalColourScheme(),
-                "Background");
+        SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
+                ap.getViewName());
       }
+      // TODO: stretchGroup will always be not null. Is this a merge error ?
       if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
       {
         // Edit end res position of selected group
@@ -1685,17 +1755,16 @@ public class SeqPanel extends JPanel implements MouseListener,
    * 
    * @param evt
    * @param res
-   * @param sequence
+   * @param sequences
    */
   void showPopupMenu(MouseEvent evt)
   {
-    final int res = findRes(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<String>();
+            .findFeaturesAtColumn(sequence, column + 1);
+    List<String> links = new ArrayList<>();
     for (SequenceFeature sf : allFeatures)
     {
       if (sf.links != null)
@@ -1712,12 +1781,15 @@ public class SeqPanel extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Update the display after mouse up on a selection or group
    * 
    * @param evt
-   *          DOCUMENT ME!
+   *          mouse released event details
+   * @param afterDrag
+   *          true if this event is happening after a mouse drag (rather than a
+   *          mouse down)
    */
-  public void doMouseReleasedDefineMode(MouseEvent evt)
+  public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
   {
     if (stretchGroup == null)
     {
@@ -1726,21 +1798,22 @@ public class SeqPanel extends JPanel implements MouseListener,
     // 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();
+    needOverviewUpdate |= vischange && av.isSelectionDefinedGroup()
+            && afterDrag;
     if (stretchGroup.cs != null)
     {
       stretchGroup.cs.alignmentChanged(stretchGroup,
               av.getHiddenRepSequences());
 
+      ResidueShaderI groupColourScheme = stretchGroup.getGroupColourScheme();
+      String name = stretchGroup.getName();
       if (stretchGroup.cs.conservationApplied())
       {
-        SliderPanel.setConservationSlider(ap, stretchGroup.cs,
-                stretchGroup.getName());
+        SliderPanel.setConservationSlider(ap, groupColourScheme, name);
       }
-      else
+      if (stretchGroup.cs.getThreshold() > 0)
       {
-        SliderPanel.setPIDSliderSource(ap, stretchGroup.cs,
-                stretchGroup.getName());
+        SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
       }
     }
     PaintRefresher.Refresh(this, av.getSequenceSetId());
@@ -1760,7 +1833,7 @@ public class SeqPanel extends JPanel implements MouseListener,
    */
   public void doMouseDraggedDefineMode(MouseEvent evt)
   {
-    int res = findRes(evt);
+    int res = findColumn(evt);
     int y = findSeq(evt);
 
     if (wrappedBlock != startWrapBlock)
@@ -1789,9 +1862,9 @@ public class SeqPanel extends JPanel implements MouseListener,
       changeStartRes = true;
     }
 
-    if (res < av.getStartRes())
+    if (res < av.getRanges().getStartRes())
     {
-      res = av.getStartRes();
+      res = av.getRanges().getStartRes();
     }
 
     if (changeEndRes)
@@ -1925,24 +1998,26 @@ public class SeqPanel extends JPanel implements MouseListener,
       {
         if (evt != null)
         {
-          if (mouseDragging && (evt.getY() < 0) && (av.getStartSeq() > 0))
+          if (mouseDragging && (evt.getY() < 0)
+                  && (av.getRanges().getStartSeq() > 0))
           {
-            running = ap.scrollUp(true);
+            running = av.getRanges().scrollUp(true);
           }
 
           if (mouseDragging && (evt.getY() >= getHeight())
-                  && (av.getAlignment().getHeight() > av.getEndSeq()))
+                  && (av.getAlignment().getHeight() > av.getRanges()
+                          .getEndSeq()))
           {
-            running = ap.scrollUp(false);
+            running = av.getRanges().scrollUp(false);
           }
 
           if (mouseDragging && (evt.getX() < 0))
           {
-            running = ap.scrollRight(false);
+            running = av.getRanges().scrollRight(false);
           }
           else if (mouseDragging && (evt.getX() >= getWidth()))
           {
-            running = ap.scrollRight(true);
+            running = av.getRanges().scrollRight(true);
           }
         }
 
@@ -1961,7 +2036,7 @@ public class SeqPanel extends JPanel implements MouseListener,
    */
   @Override
   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
-          SelectionSource source)
+          HiddenColumns hidden, SelectionSource source)
   {
     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
     // handles selection messages...
@@ -1969,7 +2044,20 @@ public class SeqPanel extends JPanel implements MouseListener,
     // shared between viewports.
     boolean iSentTheSelection = (av == source || (source instanceof AlignViewport && ((AlignmentViewport) source)
             .getSequenceSetId().equals(av.getSequenceSetId())));
-    if (iSentTheSelection || !av.followSelection)
+
+    if (iSentTheSelection)
+    {
+      // respond to our own event by updating dependent dialogs
+      if (ap.getCalculationDialog() != null)
+      {
+        ap.getCalculationDialog().validateCalcTypes();
+      }
+
+      return;
+    }
+
+    // process further ?
+    if (!av.followSelection)
     {
       return;
     }
@@ -1986,7 +2074,7 @@ public class SeqPanel extends JPanel implements MouseListener,
      * Check for selection in a view of which this one is a dna/protein
      * complement.
      */
-    if (selectionFromTranslation(seqsel, colsel, source))
+    if (selectionFromTranslation(seqsel, colsel, hidden, source))
     {
       return;
     }
@@ -2049,7 +2137,8 @@ public class SeqPanel extends JPanel implements MouseListener,
         }
         else
         {
-          av.getColumnSelection().setElementsFrom(colsel);
+          av.getColumnSelection().setElementsFrom(colsel,
+                  av.getAlignment().getHiddenColumns());
         }
       }
       av.isColSelChanged(true);
@@ -2058,8 +2147,8 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (copycolsel
             && av.hasHiddenColumns()
-            && (av.getColumnSelection() == null || av.getColumnSelection()
-                    .getHiddenColumns() == null))
+            && (av.getAlignment().getHiddenColumns() == null || av
+                    .getAlignment().getHiddenColumns().getHiddenRegions() == null))
     {
       System.err.println("Bad things");
     }
@@ -2069,6 +2158,13 @@ public class SeqPanel extends JPanel implements MouseListener,
       PaintRefresher.Refresh(this, av.getSequenceSetId());
       // ap.paintAlignment(false);
     }
+
+    // lastly, update dependent dialogs
+    if (ap.getCalculationDialog() != null)
+    {
+      ap.getCalculationDialog().validateCalcTypes();
+    }
+
   }
 
   /**
@@ -2081,7 +2177,8 @@ public class SeqPanel extends JPanel implements MouseListener,
    * @param source
    */
   protected boolean selectionFromTranslation(SequenceGroup seqsel,
-          ColumnSelection colsel, SelectionSource source)
+          ColumnSelection colsel, HiddenColumns hidden,
+          SelectionSource source)
   {
     if (!(source instanceof AlignViewportI))
     {
@@ -2104,9 +2201,19 @@ public class SeqPanel extends JPanel implements MouseListener,
     /*
      * Map column selection
      */
-    ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
-            av);
+    // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
+    // av);
+    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns hs = new HiddenColumns();
+    MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
     av.setColumnSelection(cs);
+    av.getAlignment().setHiddenColumns(hs);
+
+    // lastly, update any dependent dialogs
+    if (ap.getCalculationDialog() != null)
+    {
+      ap.getCalculationDialog().validateCalcTypes();
+    }
 
     PaintRefresher.Refresh(this, av.getSequenceSetId());
 
index 8e1d549..da026b5 100755 (executable)
@@ -52,7 +52,6 @@ import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTextArea;
@@ -1007,15 +1006,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;
             }
-
           }
         }
 
index 64fe053..36825ea 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.api.FeatureRenderer;
+import jalview.api.AlignViewportI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.ColourSchemeI;
+import jalview.renderer.ResidueShaderI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.util.Comparison;
 
 import java.awt.Color;
 import java.awt.FontMetrics;
@@ -33,7 +35,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
 {
   final static int CHAR_TO_UPPER = 'A' - 'a';
 
-  AlignViewport av;
+  AlignViewportI av;
 
   FontMetrics fm;
 
@@ -52,14 +54,13 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
   boolean forOverview = false;
 
   /**
-   * Creates a new SequenceRenderer object.
+   * Creates a new SequenceRenderer object
    * 
-   * @param av
-   *          DOCUMENT ME!
+   * @param viewport
    */
-  public SequenceRenderer(AlignViewport av)
+  public SequenceRenderer(AlignViewportI viewport)
   {
-    this.av = av;
+    this.av = viewport;
   }
 
   /**
@@ -82,8 +83,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     this.renderGaps = renderGaps;
   }
 
-  @Override
-  public Color getResidueBoxColour(SequenceI seq, int i)
+  protected Color getResidueBoxColour(SequenceI seq, int i)
   {
     // rate limiting step when rendering overview for lots of groups
     allGroups = av.getAlignment().findAllGroups(seq);
@@ -92,12 +92,12 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     {
       if (currentSequenceGroup.getDisplayBoxes())
       {
-        getBoxColour(currentSequenceGroup.cs, seq, i);
+        getBoxColour(currentSequenceGroup.getGroupColourScheme(), seq, i);
       }
     }
     else if (av.getShowBoxes())
     {
-      getBoxColour(av.getGlobalColourScheme(), seq, i);
+      getBoxColour(av.getResidueShading(), seq, i);
     }
 
     return resBoxColour;
@@ -110,20 +110,18 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
    * 
    * @param seq
    * @param position
-   * @param fr
+   * @param finder
    * @return
    */
   @Override
   public Color getResidueColour(final SequenceI seq, int position,
-          FeatureRenderer fr)
+          FeatureColourFinder finder)
   {
-    // TODO replace 8 or so code duplications with calls to this method
-    // (refactored as needed)
     Color col = getResidueBoxColour(seq, position);
 
-    if (fr != null)
+    if (finder != null)
     {
-      col = fr.findFeatureColour(col, seq, position);
+      col = finder.findFeatureColour(col, seq, position);
     }
     return col;
   }
@@ -131,21 +129,21 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
   /**
    * DOCUMENT ME!
    * 
-   * @param cs
+   * @param shader
    *          DOCUMENT ME!
    * @param seq
    *          DOCUMENT ME!
    * @param i
    *          DOCUMENT ME!
    */
-  void getBoxColour(ColourSchemeI cs, SequenceI seq, int i)
+  void getBoxColour(ResidueShaderI shader, SequenceI seq, int i)
   {
-    if (cs != null)
+    if (shader.getColourScheme() != null)
     {
-      resBoxColour = cs.findColour(seq.getCharAt(i), i, seq);
+      resBoxColour = shader.findColour(seq.getCharAt(i),
+              i, seq);
     }
-    else if (forOverview
-            && !jalview.util.Comparison.isGap(seq.getCharAt(i)))
+    else if (forOverview && !Comparison.isGap(seq.getCharAt(i)))
     {
       resBoxColour = Color.lightGray;
     }
@@ -184,7 +182,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
 
     drawBoxes(seq, start, end, y1);
 
-    if (av.validCharWidth)
+    if (av.isValidCharWidth())
     {
       drawText(seq, start, end, y1);
     }
@@ -234,14 +232,14 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
         {
           if (currentSequenceGroup.getDisplayBoxes())
           {
-            getBoxColour(currentSequenceGroup.cs, seq, i);
+            getBoxColour(currentSequenceGroup.getGroupColourScheme(), seq,
+                    i);
           }
         }
         else if (av.getShowBoxes())
         {
-          getBoxColour(av.getGlobalColourScheme(), seq, i);
+          getBoxColour(av.getResidueShading(), seq, i);
         }
-
       }
 
       if (resBoxColour != tempColour)
@@ -345,7 +343,8 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
                   || currentSequenceGroup.getColourText())
           {
             getboxColour = true;
-            getBoxColour(currentSequenceGroup.cs, seq, i);
+            getBoxColour(currentSequenceGroup.getGroupColourScheme(), seq,
+                    i);
 
             if (currentSequenceGroup.getColourText())
             {
@@ -385,7 +384,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
           if (av.getColourText())
           {
             getboxColour = true;
-            getBoxColour(av.getGlobalColourScheme(), seq, i);
+            getBoxColour(av.getResidueShading(), seq, i);
 
             if (av.getShowBoxes())
             {
@@ -401,7 +400,7 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
           {
             if (!getboxColour)
             {
-              getBoxColour(av.getGlobalColourScheme(), seq, i);
+              getBoxColour(av.getResidueShading(), seq, i);
             }
 
             if (resBoxColour.getRed() + resBoxColour.getBlue()
index a381e8b..0c4e03e 100755 (executable)
@@ -22,18 +22,19 @@ package jalview.gui;
 
 import jalview.datamodel.SequenceGroup;
 import jalview.jbgui.GSliderPanel;
-import jalview.schemes.ColourSchemeI;
+import jalview.renderer.ResidueShaderI;
 import jalview.util.MessageManager;
 
-import java.awt.event.ActionEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.util.Iterator;
+import java.beans.PropertyVetoException;
 
 import javax.swing.JInternalFrame;
 import javax.swing.JLayeredPane;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
 
 /**
  * DOCUMENT ME!
@@ -43,6 +44,8 @@ import javax.swing.event.ChangeListener;
  */
 public class SliderPanel extends GSliderPanel
 {
+  private static final String BACKGROUND = "Background";
+
   static JInternalFrame conservationSlider;
 
   static JInternalFrame PIDSlider;
@@ -51,7 +54,25 @@ public class SliderPanel extends GSliderPanel
 
   boolean forConservation = true;
 
-  ColourSchemeI cs;
+  ResidueShaderI cs;
+
+  /**
+   * Returns the currently displayed slider panel (or null if none).
+   * 
+   * @return
+   */
+  public static SliderPanel getSliderPanel()
+  {
+    if (conservationSlider != null && conservationSlider.isVisible())
+    {
+      return (SliderPanel) conservationSlider.getContentPane();
+    }
+    if (PIDSlider != null && PIDSlider.isVisible())
+    {
+      return (SliderPanel) PIDSlider.getContentPane();
+    }
+    return null;
+  }
 
   /**
    * Creates a new SliderPanel object.
@@ -62,14 +83,14 @@ public class SliderPanel extends GSliderPanel
    *          DOCUMENT ME!
    * @param forConserve
    *          DOCUMENT ME!
-   * @param cs
+   * @param scheme
    *          DOCUMENT ME!
    */
   public SliderPanel(final AlignmentPanel ap, int value,
-          boolean forConserve, ColourSchemeI cs)
+          boolean forConserve, ResidueShaderI scheme)
   {
     this.ap = ap;
-    this.cs = cs;
+    this.cs = scheme;
     forConservation = forConserve;
     undoButton.setVisible(false);
     applyButton.setVisible(false);
@@ -91,6 +112,7 @@ public class SliderPanel extends GSliderPanel
 
     slider.addChangeListener(new ChangeListener()
     {
+      @Override
       public void stateChanged(ChangeEvent evt)
       {
         valueField.setText(slider.getValue() + "");
@@ -100,6 +122,7 @@ public class SliderPanel extends GSliderPanel
 
     slider.addMouseListener(new MouseAdapter()
     {
+      @Override
       public void mouseReleased(MouseEvent evt)
       {
         ap.paintAlignment(true);
@@ -111,74 +134,104 @@ public class SliderPanel extends GSliderPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Method to 'set focus' of the Conservation slider panel
    * 
    * @param ap
-   *          DOCUMENT ME!
-   * @param cs
-   *          DOCUMENT ME!
+   *          the panel to repaint on change of slider
+   * @param rs
+   *          the colour scheme to update on change of slider
    * @param source
-   *          DOCUMENT ME!
+   *          a text description for the panel's title
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
   public static int setConservationSlider(AlignmentPanel ap,
-          ColourSchemeI cs, String source)
+          ResidueShaderI rs, String source)
   {
-    SliderPanel sp = null;
+    SliderPanel sliderPanel = null;
 
     if (conservationSlider == null)
     {
-      sp = new SliderPanel(ap, cs.getConservationInc(), true, cs);
+      sliderPanel = new SliderPanel(ap, rs.getConservationInc(), true, rs);
       conservationSlider = new JInternalFrame();
-      conservationSlider.setContentPane(sp);
+      conservationSlider.setContentPane(sliderPanel);
       conservationSlider.setLayer(JLayeredPane.PALETTE_LAYER);
     }
     else
     {
-      sp = (SliderPanel) conservationSlider.getContentPane();
-      sp.cs = cs;
+      sliderPanel = (SliderPanel) conservationSlider.getContentPane();
+      sliderPanel.valueField.setText(String.valueOf(rs.getConservationInc()));
+      sliderPanel.cs = rs;
+      sliderPanel.ap = ap;
+      sliderPanel.slider.setValue(rs.getConservationInc());
     }
 
-    conservationSlider
-            .setTitle(MessageManager.formatMessage(
-                    "label.conservation_colour_increment",
-                    new String[] { source }));
+    conservationSlider.setTitle(MessageManager.formatMessage(
+            "label.conservation_colour_increment",
+            new String[] { source == null ? BACKGROUND : source }));
 
     if (ap.av.getAlignment().getGroups() != null)
     {
-      sp.setAllGroupsCheckEnabled(true);
+      sliderPanel.setAllGroupsCheckEnabled(true);
     }
     else
     {
-      sp.setAllGroupsCheckEnabled(false);
+      sliderPanel.setAllGroupsCheckEnabled(false);
     }
 
-    return sp.getValue();
+    return sliderPanel.getValue();
   }
 
   /**
-   * DOCUMENT ME!
+   * Hides the PID slider panel if it is shown
    */
-  public static void showConservationSlider()
+  public static void hidePIDSlider()
   {
-    try
+    if (PIDSlider != null)
     {
-      PIDSlider.setClosed(true);
-      PIDSlider = null;
-    } catch (Exception ex)
+      try
+      {
+        PIDSlider.setClosed(true);
+        PIDSlider = null;
+      } catch (PropertyVetoException ex)
+      {
+      }
+    }
+  }
+
+  /**
+   * Hides the conservation slider panel if it is shown
+   */
+  public static void hideConservationSlider()
+  {
+    if (conservationSlider != null)
     {
+      try
+      {
+        conservationSlider.setClosed(true);
+        conservationSlider = null;
+      } catch (PropertyVetoException ex)
+      {
+      }
     }
+  }
+
+  /**
+   * DOCUMENT ME!
+   */
+  public static void showConservationSlider()
+  {
+    hidePIDSlider();
 
     if (!conservationSlider.isVisible())
     {
       Desktop.addInternalFrame(conservationSlider,
               conservationSlider.getTitle(), 420, 90, false);
       conservationSlider
-              .addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
+              .addInternalFrameListener(new InternalFrameAdapter()
               {
-                public void internalFrameClosed(
-                        javax.swing.event.InternalFrameEvent e)
+                @Override
+                public void internalFrameClosed(InternalFrameEvent e)
                 {
                   conservationSlider = null;
                 }
@@ -188,127 +241,103 @@ public class SliderPanel extends GSliderPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Method to 'set focus' of the PID slider panel
    * 
    * @param ap
-   *          DOCUMENT ME!
-   * @param cs
-   *          DOCUMENT ME!
+   *          the panel to repaint on change of slider
+   * @param rs
+   *          the colour scheme to update on change of slider
    * @param source
-   *          DOCUMENT ME!
+   *          a text description for the panel's title
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
-  public static int setPIDSliderSource(AlignmentPanel ap, ColourSchemeI cs,
-          String source)
+  public static int setPIDSliderSource(AlignmentPanel ap,
+          ResidueShaderI rs, String source)
   {
-    SliderPanel pid = null;
+    int threshold = rs.getThreshold();
 
-    int threshold = cs.getThreshold();
+    SliderPanel sliderPanel = null;
 
     if (PIDSlider == null)
     {
-      pid = new SliderPanel(ap, threshold, false, cs);
+      sliderPanel = new SliderPanel(ap, threshold, false, rs);
       PIDSlider = new JInternalFrame();
-      PIDSlider.setContentPane(pid);
+      PIDSlider.setContentPane(sliderPanel);
       PIDSlider.setLayer(JLayeredPane.PALETTE_LAYER);
     }
     else
     {
-      pid = (SliderPanel) PIDSlider.getContentPane();
-      pid.cs = cs;
+      sliderPanel = (SliderPanel) PIDSlider.getContentPane();
+      sliderPanel.cs = rs;
+      sliderPanel.ap = ap;
+      sliderPanel.valueField.setText(String.valueOf(rs.getThreshold()));
+      sliderPanel.slider.setValue(rs.getThreshold());
     }
 
-    PIDSlider
-            .setTitle(MessageManager.formatMessage(
-                    "label.percentage_identity_threshold",
-                    new String[] { source }));
+    PIDSlider.setTitle(MessageManager.formatMessage(
+            "label.percentage_identity_threshold",
+            new String[] { source == null ? BACKGROUND : source }));
 
     if (ap.av.getAlignment().getGroups() != null)
     {
-      pid.setAllGroupsCheckEnabled(true);
+      sliderPanel.setAllGroupsCheckEnabled(true);
     }
     else
     {
-      pid.setAllGroupsCheckEnabled(false);
+      sliderPanel.setAllGroupsCheckEnabled(false);
     }
 
-    return pid.getValue();
+    return sliderPanel.getValue();
   }
 
   /**
    * DOCUMENT ME!
+   * 
+   * @return
    */
-  public static void showPIDSlider()
+  public static JInternalFrame showPIDSlider()
   {
-    try
-    {
-      conservationSlider.setClosed(true);
-      conservationSlider = null;
-    } catch (Exception ex)
-    {
-    }
+    hideConservationSlider();
 
     if (!PIDSlider.isVisible())
     {
       Desktop.addInternalFrame(PIDSlider, PIDSlider.getTitle(), 420, 90,
               false);
       PIDSlider.setLayer(JLayeredPane.PALETTE_LAYER);
-      PIDSlider
-              .addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
-              {
-                public void internalFrameClosed(
-                        javax.swing.event.InternalFrameEvent e)
-                {
-                  PIDSlider = null;
-                }
-              });
+      PIDSlider.addInternalFrameListener(new InternalFrameAdapter()
+      {
+        @Override
+        public void internalFrameClosed(InternalFrameEvent e)
+        {
+          PIDSlider = null;
+        }
+      });
       PIDSlider.setLayer(JLayeredPane.PALETTE_LAYER);
     }
+    return PIDSlider;
   }
 
   /**
-   * DOCUMENT ME!
+   * Updates the colour scheme with the current (identity threshold or
+   * conservation) percentage value. Also updates all groups if 'apply to all
+   * groups' is selected.
    * 
-   * @param i
-   *          DOCUMENT ME!
+   * @param percent
    */
-  public void valueChanged(int i)
+  public void valueChanged(int percent)
   {
-    if (cs == null)
+    if (!forConservation)
     {
-      return;
+      ap.av.setThreshold(percent);
     }
-
-    ColourSchemeI toChange = cs;
-    Iterator<SequenceGroup> allGroups = null;
+    updateColourScheme(percent, cs);
 
     if (allGroupsCheck.isSelected())
     {
-      allGroups = ap.av.getAlignment().getGroups().listIterator();
-    }
-
-    while (toChange != null)
-    {
-      if (forConservation)
-      {
-        toChange.setConservationInc(i);
-      }
-      else
-      {
-        toChange.setThreshold(i, ap.av.isIgnoreGapsConsensus());
-      }
-      if (allGroups != null && allGroups.hasNext())
-      {
-        while ((toChange = allGroups.next().cs) == null
-                && allGroups.hasNext())
-        {
-          ;
-        }
-      }
-      else
+      for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
-        toChange = null;
+        updateColourScheme(percent, sg.getGroupColourScheme());
       }
     }
 
@@ -316,6 +345,29 @@ public class SliderPanel extends GSliderPanel
   }
 
   /**
+   * Updates the colour scheme (if not null) with the current (identity
+   * threshold or conservation) percentage value
+   * 
+   * @param percent
+   * @param scheme
+   */
+  protected void updateColourScheme(int percent, ResidueShaderI scheme)
+  {
+    if (scheme == null)
+    {
+      return;
+    }
+    if (forConservation)
+    {
+      scheme.setConservationInc(percent);
+    }
+    else
+    {
+      scheme.setThreshold(percent, ap.av.isIgnoreGapsConsensus());
+    }
+  }
+
+  /**
    * DOCUMENT ME!
    * 
    * @param b
@@ -332,7 +384,8 @@ public class SliderPanel extends GSliderPanel
    * @param e
    *          DOCUMENT ME!
    */
-  public void valueField_actionPerformed(ActionEvent e)
+  @Override
+  public void valueField_actionPerformed()
   {
     try
     {
@@ -365,6 +418,7 @@ public class SliderPanel extends GSliderPanel
     return Integer.parseInt(valueField.getText());
   }
 
+  @Override
   public void slider_mouseReleased(MouseEvent e)
   {
     if (ap.overviewPanel != null)
@@ -373,4 +427,53 @@ public class SliderPanel extends GSliderPanel
     }
   }
 
+  public static int getConservationValue()
+  {
+    return getValue(conservationSlider);
+  }
+
+  static int getValue(JInternalFrame slider)
+  {
+    return slider == null ? 0 : ((SliderPanel) slider.getContentPane())
+            .getValue();
+  }
+
+  public static int getPIDValue()
+  {
+    return getValue(PIDSlider);
+  }
+
+  /**
+   * Answers true if the SliderPanel is for Conservation, false if it is for PID
+   * threshold
+   * 
+   * @return
+   */
+  public boolean isForConservation()
+  {
+    return forConservation;
+  }
+
+  /**
+   * Answers the title for the slider panel; this may include 'Background' if
+   * for the alignment, or the group id if for a group
+   * 
+   * @return
+   */
+  public String getTitle()
+  {
+    String title = null;
+    if (isForConservation())
+    {
+      if (conservationSlider != null)
+      {
+        title = conservationSlider.getTitle();
+      }
+    }
+    else if (PIDSlider != null)
+    {
+      title = PIDSlider.getTitle();
+    }
+    return title;
+  }
 }
index 6c849c3..1d929e6 100644 (file)
@@ -21,7 +21,6 @@
 package jalview.gui;
 
 import jalview.api.SplitContainerI;
-import jalview.api.ViewStyleI;
 import jalview.datamodel.AlignmentI;
 import jalview.jbgui.GAlignFrame;
 import jalview.jbgui.GSplitFrame;
@@ -189,10 +188,8 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
             : (!bottomAlignment.isNucleotide() ? bottomViewport : null);
     if (protein != null && cdna != null)
     {
-      ViewStyleI vs = protein.getViewStyle();
-      int scale = vs.isScaleProteinAsCdna() ? 3 : 1;
-      vs.setCharWidth(scale * cdna.getViewStyle().getCharWidth());
-      protein.setViewStyle(vs);
+      int scale = protein.isScaleProteinAsCdna() ? 3 : 1;
+      protein.setCharWidth(scale * cdna.getViewStyle().getCharWidth());
     }
   }
 
index ee22ae4..3e516a6 100644 (file)
@@ -53,7 +53,6 @@ import java.util.Vector;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JLabel;
-import javax.swing.JOptionPane;
 import javax.swing.table.AbstractTableModel;
 
 /**
@@ -542,26 +541,26 @@ public class StructureChooser extends GStructureChooser implements
     if (haveData)
     {
       cmb_filterOption.addItem(new FilterOption("Best Quality",
-              "overall_quality", VIEWS_FILTER));
+              "overall_quality", VIEWS_FILTER, false));
       cmb_filterOption.addItem(new FilterOption("Best Resolution",
-              "resolution", VIEWS_FILTER));
+              "resolution", VIEWS_FILTER, false));
       cmb_filterOption.addItem(new FilterOption("Most Protein Chain",
-              "number_of_protein_chains", VIEWS_FILTER));
+              "number_of_protein_chains", VIEWS_FILTER, false));
       cmb_filterOption.addItem(new FilterOption("Most Bound Molecules",
-              "number_of_bound_molecules", VIEWS_FILTER));
+              "number_of_bound_molecules", VIEWS_FILTER, false));
       cmb_filterOption.addItem(new FilterOption("Most Polymer Residues",
-              "number_of_polymer_residues", VIEWS_FILTER));
+              "number_of_polymer_residues", VIEWS_FILTER, true));
     }
     cmb_filterOption.addItem(new FilterOption("Enter PDB Id", "-",
-            VIEWS_ENTER_ID));
+            VIEWS_ENTER_ID, false));
     cmb_filterOption.addItem(new FilterOption("From File", "-",
-            VIEWS_FROM_FILE));
-    FilterOption cachedOption = new FilterOption("Cached PDB Entries", "-",
-            VIEWS_LOCAL_PDB);
-    cmb_filterOption.addItem(cachedOption);
+            VIEWS_FROM_FILE, false));
 
-    if (/*!haveData &&*/cachedPDBExists)
+    if (cachedPDBExists)
     {
+      FilterOption cachedOption = new FilterOption("Cached PDB Entries",
+              "-", VIEWS_LOCAL_PDB, false);
+      cmb_filterOption.addItem(cachedOption);
       cmb_filterOption.setSelectedItem(cachedOption);
     }
 
index 19291ac..e73ce07 100644 (file)
  */
 package jalview.gui;
 
+import jalview.bin.Cache;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.gui.ViewSelectionMenu.ViewSetProvider;
 import jalview.io.DataSourceType;
+import jalview.io.JalviewFileChooser;
+import jalview.io.JalviewFileView;
 import jalview.jbgui.GStructureViewer;
-import jalview.structure.StructureSelectionManager;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemes;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MessageManager;
 
+import java.awt.Color;
 import java.awt.Component;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Vector;
 
+import javax.swing.ButtonGroup;
 import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JInternalFrame;
+import javax.swing.JColorChooser;
+import javax.swing.JMenu;
 import javax.swing.JMenuItem;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.event.MenuEvent;
+import javax.swing.event.MenuListener;
 
 /**
  * Base class with common functionality for JMol, Chimera or other structure
@@ -53,6 +72,13 @@ import javax.swing.JMenuItem;
 public abstract class StructureViewerBase extends GStructureViewer
         implements Runnable, ViewSetProvider
 {
+  /*
+   * names for colour options (additional to Jalview colour schemes)
+   */
+  enum ViewerColour
+  {
+    BySequence, ByChain, ChargeCysteine, ByViewer
+  }
 
   /**
    * list of sequenceSet ids associated with the view
@@ -84,6 +110,15 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   protected boolean allChainsSelected = false;
 
+  protected JMenu viewSelectionMenu;
+
+  /**
+   * Default constructor
+   */
+  public StructureViewerBase()
+  {
+    super();
+  }
   /**
    * 
    * @param ap2
@@ -274,8 +309,6 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   public abstract ViewerType getViewerType();
 
-  protected abstract AAStructureBindingModel getBindingModel();
-
   /**
    * add a new structure (with associated sequences and chains) to this viewer,
    * retrieving it if necessary first.
@@ -321,7 +354,7 @@ public abstract class StructureViewerBase extends GStructureViewer
       }
     }
     // otherwise, start adding the structure.
-    getBindingModel().addSequenceAndChain(new PDBEntry[] { pdbentry },
+    getBinding().addSequenceAndChain(new PDBEntry[] { pdbentry },
             new SequenceI[][] { seqs }, new String[][] { chains });
     addingStructures = true;
     _started = false;
@@ -351,32 +384,14 @@ public abstract class StructureViewerBase extends GStructureViewer
     return option;
   }
 
-  protected abstract boolean hasPdbId(String pdbId);
-
-  /**
-   * Returns a list of any structure viewers of the same type. The list is
-   * restricted to those linked to the given alignment panel if it is not null.
-   */
-  protected List<StructureViewerBase> getViewersFor(AlignmentPanel alp){
-
-    List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
-    JInternalFrame[] frames = Desktop.instance.getAllFrames();
-
-    for (JInternalFrame frame : frames)
-    {
-      if (this.getClass().isAssignableFrom(frame.getClass()))
-      {
-        if (alp == null
-                || ((StructureViewerBase) frame).isLinkedWith(alp))
-        {
-          result.add((StructureViewerBase) frame);
-        }
-      }
-    }
-    return result;
-  
+  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
@@ -460,7 +475,7 @@ public abstract class StructureViewerBase extends GStructureViewer
     // JBPNOTE: this looks like a binding routine, rather than a gui routine
     for (StructureViewerBase viewer : getViewersFor(null))
     {
-      AAStructureBindingModel bindingModel = viewer.getBindingModel();
+      AAStructureBindingModel bindingModel = viewer.getBinding();
       for (int pe = 0; pe < bindingModel.getPdbCount(); pe++)
       {
         if (bindingModel.getPdbEntry(pe).getFile().equals(pdbFilename))
@@ -495,8 +510,8 @@ public abstract class StructureViewerBase extends GStructureViewer
           final AlignmentPanel apanel, String pdbId)
   {
     boolean finished = false;
-    StructureSelectionManager ssm = apanel.getStructureSelectionManager();
-    String alreadyMapped = ssm.alreadyMappedToFile(pdbId);
+    String alreadyMapped = apanel.getStructureSelectionManager()
+            .alreadyMappedToFile(pdbId);
 
     if (alreadyMapped != null)
     {
@@ -574,4 +589,430 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   abstract void showSelectedChains();
 
+  /**
+   * Action on selecting one of Jalview's registered colour schemes
+   */
+  @Override
+  public void changeColour_actionPerformed(String colourSchemeName)
+  {
+    AlignmentI al = getAlignmentPanel().av.getAlignment();
+    ColourSchemeI cs = ColourSchemes.getInstance().getColourScheme(
+            colourSchemeName, al, null);
+    getBinding().setJalviewColourScheme(cs);
+  }
+
+  /**
+   * Builds the colour menu
+   */
+  protected void buildColourMenu()
+  {
+    colourMenu.removeAll();
+    AlignmentI al = getAlignmentPanel().av.getAlignment();
+
+    /*
+     * add colour by sequence, by chain, by charge and cysteine
+     */
+    colourMenu.add(seqColour);
+    colourMenu.add(chainColour);
+    colourMenu.add(chargeColour);
+    chargeColour.setEnabled(!al.isNucleotide());
+
+    /*
+     * add all 'simple' (per-residue) colour schemes registered to Jalview
+     */
+    ButtonGroup itemGroup = ColourMenuHelper.addMenuItems(colourMenu, this,
+            al, true);
+
+    /*
+     * add 'colour by viewer' (menu item text is set in subclasses)
+     */
+    viewerColour.setSelected(false);
+    viewerColour.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent actionEvent)
+      {
+        viewerColour_actionPerformed(actionEvent);
+      }
+    });
+    colourMenu.add(viewerColour);
+
+    /*
+     * add 'set background colour'
+     */
+    JMenuItem backGround = new JMenuItem();
+    backGround
+            .setText(MessageManager.getString("action.background_colour"));
+    backGround.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent actionEvent)
+      {
+        background_actionPerformed(actionEvent);
+      }
+    });
+    colourMenu.add(backGround);
+
+    /*
+     * add colour buttons to a group so their selection is
+     * mutually exclusive (background colour is a separate option)
+     */
+    itemGroup.add(seqColour);
+    itemGroup.add(chainColour);
+    itemGroup.add(chargeColour);
+    itemGroup.add(viewerColour);
+  }
+
+  /**
+   * Construct menu items
+   */
+  protected void initMenus()
+  {
+    AAStructureBindingModel binding = getBinding();
+
+    seqColour = new JRadioButtonMenuItem();
+    seqColour.setText(MessageManager.getString("action.by_sequence"));
+    seqColour.setName(ViewerColour.BySequence.name());
+    seqColour.setSelected(binding.isColourBySequence());
+    seqColour.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent actionEvent)
+      {
+        seqColour_actionPerformed(actionEvent);
+      }
+    });
+
+    chainColour = new JRadioButtonMenuItem();
+    chainColour.setText(MessageManager.getString("action.by_chain"));
+    chainColour.setName(ViewerColour.ByChain.name());
+    chainColour.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent actionEvent)
+      {
+        chainColour_actionPerformed(actionEvent);
+      }
+    });
+
+    chargeColour = new JRadioButtonMenuItem();
+    chargeColour.setText(MessageManager.getString("label.charge_cysteine"));
+    chargeColour.setName(ViewerColour.ChargeCysteine.name());
+    chargeColour.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent actionEvent)
+      {
+        chargeColour_actionPerformed(actionEvent);
+      }
+    });
+
+    viewerColour = new JRadioButtonMenuItem();
+    // text is set in overrides of this method
+    viewerColour.setName(ViewerColour.ByViewer.name());
+    viewerColour.setSelected(!binding.isColourBySequence());
+
+    if (_colourwith == null)
+    {
+      _colourwith = new Vector<AlignmentPanel>();
+    }
+    if (_alignwith == null)
+    {
+      _alignwith = new Vector<AlignmentPanel>();
+    }
+
+    ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
+            MessageManager.getString("label.colour_by"), this, _colourwith,
+            new ItemListener()
+            {
+              @Override
+              public void itemStateChanged(ItemEvent e)
+              {
+                if (!seqColour.isSelected())
+                {
+                  seqColour.doClick();
+                }
+                else
+                {
+                  // update the Chimera display now.
+                  seqColour_actionPerformed(null);
+                }
+              }
+            });
+    viewMenu.add(seqColourBy);
+
+    final ItemListener handler = new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        alignStructs.setEnabled(!_alignwith.isEmpty());
+        alignStructs.setToolTipText(MessageManager.formatMessage(
+                "label.align_structures_using_linked_alignment_views",
+                _alignwith.size()));
+      }
+    };
+    viewSelectionMenu = new ViewSelectionMenu(
+            MessageManager.getString("label.superpose_with"), this,
+            _alignwith, handler);
+    handler.itemStateChanged(null);
+    viewerActionMenu.add(viewSelectionMenu, 0);
+    viewerActionMenu.addMenuListener(new MenuListener()
+    {
+      @Override
+      public void menuSelected(MenuEvent e)
+      {
+        handler.itemStateChanged(null);
+      }
+
+      @Override
+      public void menuDeselected(MenuEvent e)
+      {
+      }
+
+      @Override
+      public void menuCanceled(MenuEvent e)
+      {
+      }
+    });
+
+    buildColourMenu();
+  }
+
+  @Override
+  public void setJalviewColourScheme(ColourSchemeI cs) {
+    getBinding().setJalviewColourScheme(cs);
+  }
+
+  /**
+   * Sends commands to the structure viewer to superimpose structures based on
+   * currently associated alignments. May optionally return an error message for
+   * the operation.
+   */
+  @Override
+  protected String alignStructs_actionPerformed(
+          ActionEvent actionEvent)
+  {
+    return alignStructs_withAllAlignPanels();
+  }
+
+  protected String alignStructs_withAllAlignPanels()
+  {
+    if (getAlignmentPanel() == null)
+    {
+      return null;
+    }
+  
+    if (_alignwith.size() == 0)
+    {
+      _alignwith.add(getAlignmentPanel());
+    }
+  
+    String reply = null;
+    try
+    {
+      AlignmentI[] als = new Alignment[_alignwith.size()];
+      HiddenColumns[] alc = new HiddenColumns[_alignwith.size()];
+      int[] alm = new int[_alignwith.size()];
+      int a = 0;
+  
+      for (AlignmentPanel ap : _alignwith)
+      {
+        als[a] = ap.av.getAlignment();
+        alm[a] = -1;
+        alc[a++] = ap.av.getAlignment().getHiddenColumns();
+      }
+      reply = getBinding().superposeStructures(als, alm, alc);
+      if (reply != null)
+      {
+        String text = MessageManager.formatMessage(
+                "error.superposition_failed", reply);
+        statusBar.setText(text);
+      }
+    } catch (Exception e)
+    {
+      StringBuffer sp = new StringBuffer();
+      for (AlignmentPanel ap : _alignwith)
+      {
+        sp.append("'" + ap.alignFrame.getTitle() + "' ");
+      }
+      Cache.log.info("Couldn't align structures with the " + sp.toString()
+              + "associated alignment panels.", e);
+    }
+    return reply;
+  }
+
+  @Override
+  public void background_actionPerformed(ActionEvent actionEvent)
+  {
+    Color col = JColorChooser.showDialog(this,
+            MessageManager.getString("label.select_background_colour"),
+            null);
+    if (col != null)
+    {
+      getBinding().setBackgroundColour(col);
+    }
+  }
+  @Override
+  public void viewerColour_actionPerformed(ActionEvent actionEvent)
+  {
+    if (viewerColour.isSelected())
+    {
+      // disable automatic sequence colouring.
+      getBinding().setColourBySequence(false);
+    }
+  }
+  @Override
+  public void chainColour_actionPerformed(ActionEvent actionEvent)
+  {
+    chainColour.setSelected(true);
+    getBinding().colourByChain();
+  }
+  @Override
+  public void chargeColour_actionPerformed(ActionEvent actionEvent)
+  {
+    chargeColour.setSelected(true);
+    getBinding().colourByCharge();
+  }
+  @Override
+  public void seqColour_actionPerformed(ActionEvent actionEvent)
+  {
+    AAStructureBindingModel binding = getBinding();
+    binding.setColourBySequence(seqColour.isSelected());
+    if (_colourwith == null)
+    {
+      _colourwith = new Vector<AlignmentPanel>();
+    }
+    if (binding.isColourBySequence())
+    {
+      if (!binding.isLoadingFromArchive())
+      {
+        if (_colourwith.size() == 0 && getAlignmentPanel() != null)
+        {
+          // Make the currently displayed alignment panel the associated view
+          _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
+        }
+      }
+      // Set the colour using the current view for the associated alignframe
+      for (AlignmentPanel ap : _colourwith)
+      {
+        binding.colourBySequence(ap);
+      }
+    }
+  }
+  @Override
+  public void pdbFile_actionPerformed(ActionEvent actionEvent)
+  {
+    JalviewFileChooser chooser = new JalviewFileChooser(
+            Cache.getProperty("LAST_DIRECTORY"));
+  
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
+    chooser.setToolTipText(MessageManager.getString("action.save"));
+  
+    int value = chooser.showSaveDialog(this);
+  
+    if (value == JalviewFileChooser.APPROVE_OPTION)
+    {
+      BufferedReader in = null;
+      try
+      {
+        // TODO: cope with multiple PDB files in view
+        in = new BufferedReader(
+                new FileReader(getBinding().getStructureFiles()[0]));
+        File outFile = chooser.getSelectedFile();
+  
+        PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
+        String data;
+        while ((data = in.readLine()) != null)
+        {
+          if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
+          {
+            out.println(data);
+          }
+        }
+        out.close();
+      } catch (Exception ex)
+      {
+        ex.printStackTrace();
+      } finally
+      {
+        if (in != null)
+        {
+          try
+          {
+            in.close();
+          } catch (IOException e)
+          {
+            // ignore
+          }
+        }
+      }
+    }
+  }
+  @Override
+  public void viewMapping_actionPerformed(ActionEvent actionEvent)
+  {
+    CutAndPasteTransfer cap = new CutAndPasteTransfer();
+    try
+    {
+      cap.appendText(getBinding().printMappings());
+    } catch (OutOfMemoryError e)
+    {
+      new OOMWarning(
+              "composing sequence-structure alignments for display in text box.",
+              e);
+      cap.dispose();
+      return;
+    }
+    Desktop.addInternalFrame(cap,
+            MessageManager.getString("label.pdb_sequence_mapping"), 550,
+            600);
+  }
+
+  protected abstract String getViewerName();
+
+  /**
+   * Configures the title and menu items of the viewer panel.
+   */
+  public void updateTitleAndMenus()
+  {
+    AAStructureBindingModel binding = getBinding();
+    if (binding.hasFileLoadingError())
+    {
+      repaint();
+      return;
+    }
+    setChainMenuItems(binding.getChainNames());
+  
+    this.setTitle(binding.getViewerTitle(getViewerName(), true));
+
+    /*
+     * enable 'Superpose with' if more than one mapped structure
+     */
+    viewSelectionMenu.setEnabled(false);
+    if (getBinding().getStructureFiles().length > 1
+            && getBinding().getSequence().length > 1)
+    {
+      viewSelectionMenu.setEnabled(true);
+    }
+
+    /*
+     * Show action menu if it has any enabled items
+     */
+    viewerActionMenu.setVisible(false);
+    for (int i = 0; i < viewerActionMenu.getItemCount(); i++)
+    {
+      if (viewerActionMenu.getItem(i).isEnabled())
+      {
+        viewerActionMenu.setVisible(true);
+        break;
+      }
+    }
+
+    if (!binding.isLoadingFromArchive())
+    {
+      seqColour_actionPerformed(null);
+    }
+  }
 }
index 49fdaf7..91e05c6 100644 (file)
@@ -28,11 +28,12 @@ import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.swing.BorderFactory;
 import javax.swing.JColorChooser;
 import javax.swing.JLabel;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JSlider;
 import javax.swing.event.ChangeEvent;
@@ -44,36 +45,43 @@ public class TextColourChooser
 
   SequenceGroup sg;
 
-  public void chooseColour(AlignmentPanel ap, SequenceGroup sg)
+  Color original1, original2;
+
+  int originalThreshold;
+
+  Map<SequenceGroup, Color> groupColour1;
+
+  Map<SequenceGroup, Color> groupColour2;
+
+  Map<SequenceGroup, Integer> groupThreshold;
+
+  /**
+   * Show a dialogue which allows the user to select two text colours and adjust
+   * a slider for the cross-over point
+   * 
+   * @param alignPanel
+   *          the AlignmentPanel context
+   * @param sequenceGroup
+   *          the SequenceGroup context (only for group pop-menu option)
+   */
+  public void chooseColour(AlignmentPanel alignPanel, SequenceGroup sequenceGroup)
   {
-    this.ap = ap;
-    this.sg = sg;
+    this.ap = alignPanel;
+    this.sg = sequenceGroup;
 
-    int original1, original2, originalThreshold;
-    if (sg == null)
-    {
-      original1 = ap.av.getTextColour().getRGB();
-      original2 = ap.av.getTextColour2().getRGB();
-      originalThreshold = ap.av.getThresholdTextColour();
-    }
-    else
-    {
-      original1 = sg.textColour.getRGB();
-      original2 = sg.textColour2.getRGB();
-      originalThreshold = sg.thresholdTextColour;
-    }
+    saveInitialSettings();
 
     final JSlider slider = new JSlider(0, 750, originalThreshold);
     final JPanel col1 = new JPanel();
     col1.setPreferredSize(new Dimension(40, 20));
     col1.setBorder(BorderFactory.createEtchedBorder());
     col1.setToolTipText(MessageManager.getString("label.dark_colour"));
-    col1.setBackground(new Color(original1));
+    col1.setBackground(original1);
     final JPanel col2 = new JPanel();
     col2.setPreferredSize(new Dimension(40, 20));
     col2.setBorder(BorderFactory.createEtchedBorder());
     col2.setToolTipText(MessageManager.getString("label.light_colour"));
-    col2.setBackground(new Color(original2));
+    col2.setBackground(original2);
     final JPanel bigpanel = new JPanel(new BorderLayout());
     JPanel panel = new JPanel();
     bigpanel.add(panel, BorderLayout.CENTER);
@@ -130,7 +138,7 @@ public class TextColourChooser
 
     int reply = JvOptionPane
             .showInternalOptionDialog(
-                    ap,
+                    alignPanel,
                     bigpanel,
                     MessageManager
                             .getString("label.adjunst_foreground_text_colour_threshold"),
@@ -139,19 +147,81 @@ public class TextColourChooser
 
     if (reply == JvOptionPane.CANCEL_OPTION)
     {
-      if (sg == null)
-      {
-        ap.av.setTextColour(new Color(original1));
-        ap.av.setTextColour2(new Color(original2));
-        ap.av.setThresholdTextColour(originalThreshold);
-      }
-      else
+      restoreInitialSettings();
+    }
+  }
+
+  /**
+   * Restore initial settings on Cancel
+   */
+  protected void restoreInitialSettings()
+  {
+    if (sg == null)
+    {
+      ap.av.setTextColour(original1);
+      ap.av.setTextColour2(original2);
+      ap.av.setThresholdTextColour(originalThreshold);
+    }
+    else
+    {
+      sg.textColour = original1;
+      sg.textColour2 = original2;
+      sg.thresholdTextColour = originalThreshold;
+    }
+
+    /*
+     * if 'Apply To All Groups' was in force, there will be 
+     * group-specific settings to restore as well
+     */
+    for (SequenceGroup group : this.groupColour1.keySet())
+    {
+      group.textColour = groupColour1.get(group);
+      group.textColour2 = groupColour2.get(group);
+      group.thresholdTextColour = groupThreshold.get(group);
+    }
+  }
+
+  /**
+   * Save settings on entry, for restore on Cancel
+   */
+  protected void saveInitialSettings()
+  {
+    groupColour1 = new HashMap<SequenceGroup, Color>();
+    groupColour2 = new HashMap<SequenceGroup, Color>();
+    groupThreshold = new HashMap<SequenceGroup, Integer>();
+
+    if (sg == null)
+    {
+      /*
+       * alignment scope
+       */
+      original1 = ap.av.getTextColour();
+      original2 = ap.av.getTextColour2();
+      originalThreshold = ap.av.getThresholdTextColour();
+      if (ap.av.getColourAppliesToAllGroups()
+              && ap.av.getAlignment().getGroups() != null)
       {
-        sg.textColour = new Color(original1);
-        sg.textColour2 = new Color(original2);
-        sg.thresholdTextColour = originalThreshold;
+        /*
+         * if applying changes to all groups, need to be able to 
+         * restore group settings as well
+         */
+        for (SequenceGroup group : ap.av.getAlignment().getGroups())
+        {
+          groupColour1.put(group, group.textColour);
+          groupColour2.put(group, group.textColour2);
+          groupThreshold.put(group, group.thresholdTextColour);
+        }
       }
     }
+    else
+    {
+      /*
+       * Sequence group scope
+       */
+      original1 = sg.textColour;
+      original2 = sg.textColour2;
+      originalThreshold = sg.thresholdTextColour;
+    }
   }
 
   void colour1Changed(Color col)
@@ -215,11 +285,11 @@ public class TextColourChooser
       return;
     }
 
-    for (SequenceGroup sg : ap.av.getAlignment().getGroups())
+    for (SequenceGroup group : ap.av.getAlignment().getGroups())
     {
-      sg.textColour = ap.av.getTextColour();
-      sg.textColour2 = ap.av.getTextColour2();
-      sg.thresholdTextColour = ap.av.getThresholdTextColour();
+      group.textColour = ap.av.getTextColour();
+      group.textColour2 = ap.av.getTextColour2();
+      group.thresholdTextColour = ap.av.getThresholdTextColour();
     }
   }
 
index 84fd82f..e60ac8e 100755 (executable)
@@ -21,7 +21,7 @@
 package jalview.gui;
 
 import jalview.analysis.Conservation;
-import jalview.analysis.NJTree;
+import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
@@ -53,6 +53,7 @@ import java.awt.print.PrinterException;
 import java.awt.print.PrinterJob;
 import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Vector;
 
 import javax.swing.JColorChooser;
@@ -73,7 +74,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
   /** DOCUMENT ME!! */
   public static final String PLACEHOLDER = " * ";
 
-  NJTree tree;
+  TreeModel tree;
 
   JScrollPane scrollPane;
 
@@ -168,7 +169,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
    * @param tree
    *          DOCUMENT ME!
    */
-  public void setTree(NJTree tree)
+  public void setTree(TreeModel tree)
   {
     this.tree = tree;
     tree.findHeight(tree.getTopNode());
@@ -207,7 +208,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
    *          DOCUMENT ME!
    * @param chunk
    *          DOCUMENT ME!
-   * @param scale
+   * @param wscale
    *          DOCUMENT ME!
    * @param width
    *          DOCUMENT ME!
@@ -217,7 +218,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
    *          DOCUMENT ME!
    */
   public void drawNode(Graphics g, SequenceNode node, float chunk,
-          float scale, int width, int offx, int offy)
+          double wscale, int width, int offx, int offy)
   {
     if (node == null)
     {
@@ -227,11 +228,11 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
     if ((node.left() == null) && (node.right() == null))
     {
       // Drawing leaf node
-      float height = node.height;
-      float dist = node.dist;
+      double height = node.height;
+      double dist = node.dist;
 
-      int xstart = (int) ((height - dist) * scale) + offx;
-      int xend = (int) (height * scale) + offx;
+      int xstart = (int) ((height - dist) * wscale) + offx;
+      int xend = (int) (height * wscale) + offx;
 
       int ypos = (int) (node.ycount * chunk) + offy;
 
@@ -306,16 +307,16 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
     }
     else
     {
-      drawNode(g, (SequenceNode) node.left(), chunk, scale, width, offx,
+      drawNode(g, (SequenceNode) node.left(), chunk, wscale, width, offx,
               offy);
-      drawNode(g, (SequenceNode) node.right(), chunk, scale, width, offx,
+      drawNode(g, (SequenceNode) node.right(), chunk, wscale, width, offx,
               offy);
 
-      float height = node.height;
-      float dist = node.dist;
+      double height = node.height;
+      double dist = node.dist;
 
-      int xstart = (int) ((height - dist) * scale) + offx;
-      int xend = (int) (height * scale) + offx;
+      int xstart = (int) ((height - dist) * wscale) + offx;
+      int xend = (int) (height * wscale) + offx;
       int ypos = (int) (node.ycount * chunk) + offy;
 
       g.setColor(node.color.darker());
@@ -331,16 +332,17 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
         g.fillRect(xend - 2, ypos - 2, 4, 4);
       }
 
-      int ystart = (int) (((SequenceNode) node.left()).ycount * chunk)
-              + offy;
-      int yend = (int) (((SequenceNode) node.right()).ycount * chunk)
+      int ystart = (node.left() == null ? 0 : (int) (((SequenceNode) node
+              .left()).ycount * chunk)) + offy;
+      int yend = (node.right() == null ? 0 : (int) (((SequenceNode) node
+              .right()).ycount * chunk))
               + offy;
 
       Rectangle pos = new Rectangle(xend - 2, ypos - 2, 5, 5);
       nodeHash.put(node, pos);
 
-      g.drawLine((int) (height * scale) + offx, ystart,
-              (int) (height * scale) + offx, yend);
+      g.drawLine((int) (height * wscale) + offx, ystart,
+              (int) (height * wscale) + offx, yend);
 
       String nodeLabel = "";
 
@@ -422,7 +424,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
 
     SequenceNode top = tree.getTopNode();
 
-    float wscale = (float) ((width * .8) - (offx * 2))
+    double wscale = ((width * .8) - (offx * 2))
             / tree.getMaxHeight();
 
     if (top.count == 0)
@@ -445,7 +447,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
    *          DOCUMENT ME!
    * @param chunk
    *          DOCUMENT ME!
-   * @param scale
+   * @param wscale
    *          DOCUMENT ME!
    * @param width
    *          DOCUMENT ME!
@@ -455,7 +457,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
    *          DOCUMENT ME!
    */
   public void pickNode(Rectangle pickBox, SequenceNode node, float chunk,
-          float scale, int width, int offx, int offy)
+          double wscale, int width, int offx, int offy)
   {
     if (node == null)
     {
@@ -464,11 +466,11 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
 
     if ((node.left() == null) && (node.right() == null))
     {
-      float height = node.height;
-      float dist = node.dist;
+      double height = node.height;
+      double dist = node.dist;
 
-      int xstart = (int) ((height - dist) * scale) + offx;
-      int xend = (int) (height * scale) + offx;
+      int xstart = (int) ((height - dist) * wscale) + offx;
+      int xend = (int) (height * wscale) + offx;
 
       int ypos = (int) (node.ycount * chunk) + offy;
 
@@ -488,9 +490,9 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
     }
     else
     {
-      pickNode(pickBox, (SequenceNode) node.left(), chunk, scale, width,
+      pickNode(pickBox, (SequenceNode) node.left(), chunk, wscale, width,
               offx, offy);
-      pickNode(pickBox, (SequenceNode) node.right(), chunk, scale, width,
+      pickNode(pickBox, (SequenceNode) node.right(), chunk, wscale, width,
               offx, offy);
     }
   }
@@ -549,7 +551,16 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
   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);
 
@@ -715,7 +726,8 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
 
     labelLength = fm.stringWidth(longestName) + 20; // 20 allows for scrollbar
 
-    float wscale = (width - labelLength - (offx * 2)) / tree.getMaxHeight();
+    double wscale = (width - labelLength - (offx * 2))
+            / tree.getMaxHeight();
 
     SequenceNode top = tree.getTopNode();
 
@@ -928,8 +940,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
         threshold = (float) (x - offx)
                 / (float) (getWidth() - labelLength - (2 * offx));
 
-        tree.getGroups().removeAllElements();
-        tree.groupNodes(tree.getTopNode(), threshold);
+        List<SequenceNode> groups = tree.groupNodes(threshold);
         setColor(tree.getTopNode(), Color.black);
 
         AlignmentPanel[] aps = getAssociatedPanels();
@@ -949,7 +960,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
             aps[a].av.getCodingComplement().clearSequenceColours();
           }
         }
-        colourGroups();
+        colourGroups(groups);
       }
 
       PaintRefresher.Refresh(tp, ap.av.getSequenceSetId());
@@ -958,17 +969,16 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
 
   }
 
-  void colourGroups()
+  void colourGroups(List<SequenceNode> groups)
   {
     AlignmentPanel[] aps = getAssociatedPanels();
-    for (int i = 0; i < tree.getGroups().size(); i++)
+    for (int i = 0; i < groups.size(); i++)
     {
       Color col = new Color((int) (Math.random() * 255),
               (int) (Math.random() * 255), (int) (Math.random() * 255));
-      setColor(tree.getGroups().elementAt(i), col.brighter());
+      setColor(groups.get(i), col.brighter());
 
-      Vector<SequenceNode> l = tree.findLeaves(tree.getGroups()
-              .elementAt(i));
+      Vector<SequenceNode> l = tree.findLeaves(groups.get(i));
 
       Vector<SequenceI> sequences = new Vector<SequenceI>();
 
@@ -997,17 +1007,21 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
         }
         else
         {
-          cs = ColourSchemeProperty.getColour(sg, ColourSchemeProperty
-                  .getColourName(av.getGlobalColourScheme()));
+          cs = ColourSchemeProperty.getColourScheme(sg,
+                  ColourSchemeProperty.getColourName(av
+                          .getGlobalColourScheme()));
         }
         // cs is null if shading is an annotationColourGradient
-        if (cs != null)
-        {
-          cs.setThreshold(av.getGlobalColourScheme().getThreshold(),
-                  av.isIgnoreGapsConsensus());
-        }
+        // if (cs != null)
+        // {
+        // cs.setThreshold(av.getViewportColourScheme().getThreshold(),
+        // av.isIgnoreGapsConsensus());
+        // }
       }
-      sg.cs = cs;
+      sg.setColourScheme(cs);
+      sg.getGroupColourScheme().setThreshold(
+              av.getResidueShading().getThreshold(),
+              av.isIgnoreGapsConsensus());
       // sg.recalcConservation();
       sg.setName("JTreeGroup:" + sg.hashCode());
       sg.setIdColour(col);
@@ -1015,7 +1029,8 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
       for (int a = 0; a < aps.length; a++)
       {
         if (aps[a].av.getGlobalColourScheme() != null
-                && aps[a].av.getGlobalColourScheme().conservationApplied())
+                && aps[a].av.getResidueShading()
+                        .conservationApplied())
         {
           Conservation c = new Conservation("Group", sg.getSequences(null),
                   sg.getStartRes(), sg.getEndRes());
index 6fa4493..32c5702 100755 (executable)
 package jalview.gui;
 
 import jalview.analysis.AlignmentSorter;
+import jalview.analysis.AverageDistanceTree;
 import jalview.analysis.NJTree;
+import jalview.analysis.TreeBuilder;
+import jalview.analysis.TreeModel;
+import jalview.analysis.scoremodels.ScoreModels;
 import jalview.api.analysis.ScoreModelI;
-import jalview.api.analysis.ViewBasedAnalysisI;
+import jalview.api.analysis.SimilarityParamsI;
 import jalview.bin.Cache;
 import jalview.commands.CommandI;
 import jalview.commands.OrderCommand;
@@ -31,8 +35,8 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
 import jalview.datamodel.BinaryNode;
-import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.NodeTransformI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -41,7 +45,6 @@ import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.io.NewickFile;
 import jalview.jbgui.GTreePanel;
-import jalview.schemes.ResidueProperties;
 import jalview.util.ImageMaker;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
@@ -71,68 +74,46 @@ import org.jibble.epsgraphics.EpsGraphics2D;
  */
 public class TreePanel extends GTreePanel
 {
-  String type;
+  String treeType;
 
-  String pwtype;
+  String scoreModelName; // if tree computed
+
+  String treeTitle; // if tree loaded
+
+  SimilarityParamsI similarityParams;
 
   TreeCanvas treeCanvas;
 
-  NJTree tree;
+  TreeModel tree;
 
   AlignViewport av;
 
   /**
    * Creates a new TreePanel object.
    * 
-   * @param av
-   *          DOCUMENT ME!
-   * @param seqVector
-   *          DOCUMENT ME!
+   * @param ap
    * @param type
-   *          DOCUMENT ME!
-   * @param pwtype
-   *          DOCUMENT ME!
-   * @param s
-   *          DOCUMENT ME!
-   * @param e
-   *          DOCUMENT ME!
+   * @param modelName
+   * @param options
    */
-  public TreePanel(AlignmentPanel ap, String type, String pwtype)
+  public TreePanel(AlignmentPanel ap, String type, String modelName,
+          SimilarityParamsI options)
   {
     super();
-    initTreePanel(ap, type, pwtype, null, null);
+    this.similarityParams = options;
+    initTreePanel(ap, type, modelName, null, null);
 
     // We know this tree has distances. JBPNote TODO: prolly should add this as
     // a userdefined default
     // showDistances(true);
   }
 
-  /**
-   * Creates a new TreePanel object.
-   * 
-   * @param av
-   *          DOCUMENT ME!
-   * @param seqVector
-   *          DOCUMENT ME!
-   * @param newtree
-   *          DOCUMENT ME!
-   * @param type
-   *          DOCUMENT ME!
-   * @param pwtype
-   *          DOCUMENT ME!
-   */
-  public TreePanel(AlignmentPanel ap, String type, String pwtype,
-          NewickFile newtree)
-  {
-    super();
-    initTreePanel(ap, type, pwtype, newtree, null);
-  }
-
-  public TreePanel(AlignmentPanel av, String type, String pwtype,
-          NewickFile newtree, AlignmentView inputData)
+  public TreePanel(AlignmentPanel alignPanel, NewickFile newtree,
+          String theTitle, AlignmentView inputData)
   {
     super();
-    initTreePanel(av, type, pwtype, newtree, inputData);
+    this.treeTitle = theTitle;
+    initTreePanel(alignPanel, null, null, newtree, inputData);
   }
 
   public AlignmentI getAlignment()
@@ -145,13 +126,13 @@ public class TreePanel extends GTreePanel
     return treeCanvas.av;
   }
 
-  void initTreePanel(AlignmentPanel ap, String type, String pwtype,
+  void initTreePanel(AlignmentPanel ap, String type, String modelName,
           NewickFile newTree, AlignmentView inputData)
   {
 
     av = ap.av;
-    this.type = type;
-    this.pwtype = pwtype;
+    this.treeType = type;
+    this.scoreModelName = modelName;
 
     treeCanvas = new TreeCanvas(this, ap, scrollPane);
     scrollPane.setViewportView(treeCanvas);
@@ -181,7 +162,7 @@ public class TreePanel extends GTreePanel
                     .println("new alignment sequences vector value is null");
           }
 
-          tree.UpdatePlaceHolders((List<SequenceI>) evt.getNewValue());
+          tree.updatePlaceHolders((List<SequenceI>) evt.getNewValue());
           treeCanvas.nameHash.clear(); // reset the mapping between canvas
           // rectangles and leafnodes
           repaint();
@@ -189,11 +170,7 @@ public class TreePanel extends GTreePanel
       }
     });
 
-    TreeLoader tl = new TreeLoader(newTree);
-    if (inputData != null)
-    {
-      tl.odata = inputData;
-    }
+    TreeLoader tl = new TreeLoader(newTree, inputData);
     tl.start();
 
   }
@@ -265,19 +242,21 @@ public class TreePanel extends GTreePanel
 
   class TreeLoader extends Thread
   {
-    NewickFile newtree;
+    private NewickFile newtree;
 
-    jalview.datamodel.AlignmentView odata = null;
+    private AlignmentView odata = null;
 
-    public TreeLoader(NewickFile newtree)
+    public TreeLoader(NewickFile newickFile, AlignmentView inputData)
     {
-      this.newtree = newtree;
-      if (newtree != null)
+      this.newtree = newickFile;
+      this.odata = inputData;
+
+      if (newickFile != null)
       {
         // Must be outside run(), as Jalview2XML tries to
         // update distance/bootstrap visibility at the same time
-        showBootstrap(newtree.HasBootstrap());
-        showDistances(newtree.HasDistances());
+        showBootstrap(newickFile.HasBootstrap());
+        showDistances(newickFile.HasDistances());
       }
     }
 
@@ -287,60 +266,21 @@ public class TreePanel extends GTreePanel
 
       if (newtree != null)
       {
-        if (odata == null)
+        tree = new TreeModel(av.getAlignment().getSequencesArray(), odata,
+                newtree);
+        if (tree.getOriginalData() == null)
         {
-          tree = new NJTree(av.getAlignment().getSequencesArray(), newtree);
-        }
-        else
-        {
-          tree = new NJTree(av.getAlignment().getSequencesArray(), odata,
-                  newtree);
-        }
-        if (!tree.hasOriginalSequenceData())
-        {
-          allowOriginalSeqData(false);
+          originalSeqData.setVisible(false);
         }
       }
       else
       {
-        int start, end;
-        SequenceI[] seqs;
-        boolean selview = av.getSelectionGroup() != null
-                && av.getSelectionGroup().getSize() > 1;
-        AlignmentView seqStrings = av.getAlignmentView(selview);
-        if (!selview)
-        {
-          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());
-        }
-        ScoreModelI sm = ResidueProperties.getScoreModel(pwtype);
-        if (sm instanceof ViewBasedAnalysisI)
-        {
-          try
-          {
-            sm = sm.getClass().newInstance();
-            ((ViewBasedAnalysisI) sm)
-                    .configureFromAlignmentView(treeCanvas.ap);
-          } catch (Exception q)
-          {
-            Cache.log.error("Couldn't create a scoremodel instance for "
-                    + sm.getName());
-          }
-          tree = new NJTree(seqs, seqStrings, type, pwtype, sm, start, end);
-        }
-        else
-        {
-          tree = new NJTree(seqs, seqStrings, type, pwtype, null, start,
-                  end);
-        }
+        ScoreModelI sm = ScoreModels.getInstance().getScoreModel(
+                scoreModelName, treeCanvas.ap);
+        TreeBuilder njtree = treeType.equals(TreeBuilder.NEIGHBOUR_JOINING) ? new NJTree(
+                av, sm, similarityParams) : new AverageDistanceTree(av, sm,
+                similarityParams);
+        tree = new TreeModel(njtree);
         showDistances(true);
       }
 
@@ -374,17 +314,12 @@ public class TreePanel extends GTreePanel
     treeCanvas.setMarkPlaceholders(b);
   }
 
-  private void allowOriginalSeqData(boolean b)
-  {
-    originalSeqData.setVisible(b);
-  }
-
   /**
    * DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
-  public NJTree getTree()
+  public TreeModel getTree()
   {
     return tree;
   }
@@ -400,33 +335,14 @@ public class TreePanel extends GTreePanel
   {
     CutAndPasteTransfer cap = new CutAndPasteTransfer();
 
-    StringBuffer buffer = new StringBuffer();
+    String newTitle = getPanelTitle();
 
-    if (type.equals("AV"))
-    {
-      buffer.append("Average distance tree using ");
-    }
-    else
-    {
-      buffer.append("Neighbour joining tree using ");
-    }
-
-    if (pwtype.equals("BL"))
-    {
-      buffer.append("BLOSUM62");
-    }
-    else
-    {
-      buffer.append("PID");
-    }
-
-    jalview.io.NewickFile fout = new jalview.io.NewickFile(
-            tree.getTopNode());
+    NewickFile fout = new NewickFile(tree.getTopNode());
     try
     {
-      cap.setText(fout.print(tree.isHasBootstrap(), tree.isHasDistances(),
-              tree.isHasRootDistance()));
-      Desktop.addInternalFrame(cap, buffer.toString(), 500, 100);
+      cap.setText(fout.print(tree.hasBootstrap(), tree.hasDistances(),
+              tree.hasRootDistance()));
+      Desktop.addInternalFrame(cap, newTitle, 500, 100);
     } catch (OutOfMemoryError oom)
     {
       new OOMWarning("generating newick tree file", oom);
@@ -463,8 +379,8 @@ public class TreePanel extends GTreePanel
       {
         jalview.io.NewickFile fout = new jalview.io.NewickFile(
                 tree.getTopNode());
-        String output = fout.print(tree.isHasBootstrap(),
-                tree.isHasDistances(), tree.isHasRootDistance());
+        String output = fout.print(tree.hasBootstrap(),
+                tree.hasDistances(), tree.hasRootDistance());
         java.io.PrintWriter out = new java.io.PrintWriter(
                 new java.io.FileWriter(choice));
         out.println(output);
@@ -492,7 +408,8 @@ public class TreePanel extends GTreePanel
   @Override
   public void originalSeqData_actionPerformed(ActionEvent e)
   {
-    if (!tree.hasOriginalSequenceData())
+    AlignmentView originalData = tree.getOriginalData();
+    if (originalData == null)
     {
       jalview.bin.Cache.log
               .info("Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
@@ -514,8 +431,8 @@ public class TreePanel extends GTreePanel
     } catch (Exception ex)
     {
     }
-    ;
-    Object[] alAndColsel = tree.seqData.getAlignmentAndColumnSelection(gc);
+
+    Object[] alAndColsel = originalData.getAlignmentAndHiddenColumns(gc);
 
     if (alAndColsel != null && alAndColsel[0] != null)
     {
@@ -532,8 +449,8 @@ public class TreePanel extends GTreePanel
       if (true)
       {
         // make a new frame!
-        AlignFrame af = new AlignFrame(al,
-                (ColumnSelection) alAndColsel[1], AlignFrame.DEFAULT_WIDTH,
+        AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
+                AlignFrame.DEFAULT_WIDTH,
                 AlignFrame.DEFAULT_HEIGHT);
 
         // >>>This is a fix for the moment, until a better solution is
@@ -632,11 +549,11 @@ public class TreePanel extends GTreePanel
 
   public CommandI sortAlignmentIn(AlignmentPanel ap)
   {
-    AlignmentViewport av = ap.av;
-    SequenceI[] oldOrder = av.getAlignment().getSequencesArray();
-    AlignmentSorter.sortByTree(av.getAlignment(), tree);
+    AlignmentViewport viewport = ap.av;
+    SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
+    AlignmentSorter.sortByTree(viewport.getAlignment(), tree);
     CommandI undo;
-    undo = new OrderCommand("Tree Sort", oldOrder, av.getAlignment());
+    undo = new OrderCommand("Tree Sort", oldOrder, viewport.getAlignment());
 
     ap.paintAlignment(true);
     return undo;
@@ -664,11 +581,11 @@ public class TreePanel extends GTreePanel
     return treeCanvas.font;
   }
 
-  public void setTreeFont(Font font)
+  public void setTreeFont(Font f)
   {
     if (treeCanvas != null)
     {
-      treeCanvas.setFont(font);
+      treeCanvas.setFont(f);
     }
   }
 
@@ -749,7 +666,6 @@ public class TreePanel extends GTreePanel
     try
     {
       JalviewFileChooser chooser = new JalviewFileChooser(
-              Cache.getProperty("LAST_DIRECTORY"),
               ImageMaker.EPS_EXTENSION, ImageMaker.EPS_EXTENSION);
       chooser.setFileView(new JalviewFileView());
       chooser.setDialogTitle(MessageManager
@@ -796,7 +712,6 @@ public class TreePanel extends GTreePanel
     try
     {
       JalviewFileChooser chooser = new JalviewFileChooser(
-              Cache.getProperty("LAST_DIRECTORY"),
               ImageMaker.PNG_EXTENSION, ImageMaker.PNG_DESCRIPTION);
 
       chooser.setFileView(new jalview.io.JalviewFileView());
@@ -874,31 +789,63 @@ 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();
                 }
               }
             }
           }
           if (newname != null)
           {
-            String oldname = ((SequenceNode) node).getName();
-            // TODO : save in the undo object for this modification.
+            // String oldname = ((SequenceNode) node).getName();
+            // TODO : save oldname in the undo object for this modification.
             ((SequenceNode) node).setName(newname);
           }
         }
       }
     });
   }
+
+  /**
+   * Formats a localised title for the tree panel, like
+   * <p>
+   * Neighbour Joining Using BLOSUM62
+   * <p>
+   * For a tree loaded from file, just uses the file name
+   * @return
+   */
+  public String getPanelTitle()
+  {
+    if (treeTitle != null)
+    {
+      return treeTitle;
+    }
+
+    /*
+     * i18n description of Neighbour Joining or Average Distance method
+     */
+    String treecalcnm = MessageManager.getString("label.tree_calc_"
+            + treeType.toLowerCase());
+
+    /*
+     * short score model name (long description can be too long)
+     */
+    String smn = scoreModelName;
+
+    /*
+     * put them together as <method> Using <model>
+     */
+    final String ttl = MessageManager.formatMessage("label.treecalc_title",
+            treecalcnm, smn);
+    return ttl;
+  }
 }
index 0df23e0..f75a0a3 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
-import jalview.datamodel.SequenceGroup;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.jbgui.GUserDefinedColours;
+import jalview.schemabinding.version2.Colour;
+import jalview.schemabinding.version2.JalviewUserColours;
 import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemeLoader;
+import jalview.schemes.ColourSchemes;
 import jalview.schemes.ResidueProperties;
 import jalview.schemes.UserColourScheme;
 import jalview.util.ColorUtils;
+import jalview.util.Format;
 import jalview.util.MessageManager;
 
 import java.awt.Color;
 import java.awt.Font;
-import java.awt.event.ActionEvent;
+import java.awt.Insets;
+import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Hashtable;
-import java.util.StringTokenizer;
+import java.util.List;
 
 import javax.swing.JButton;
 import javax.swing.JInternalFrame;
@@ -61,7 +62,14 @@ import javax.swing.event.ChangeListener;
 public class UserDefinedColours extends GUserDefinedColours implements
         ChangeListener
 {
-  private static final int MY_FRAME_HEIGHT = 420;
+  private static final Font VERDANA_BOLD_10 = new Font("Verdana",
+          Font.BOLD, 10);
+
+  public static final String USER_DEFINED_COLOURS = "USER_DEFINED_COLOURS";
+
+  private static final String LAST_DIRECTORY = "LAST_DIRECTORY";
+
+  private static final int MY_FRAME_HEIGHT = 440;
 
   private static final int MY_FRAME_WIDTH = 810;
 
@@ -69,49 +77,42 @@ public class UserDefinedColours extends GUserDefinedColours implements
 
   AlignmentPanel ap;
 
-  SequenceGroup seqGroup;
-
-  ArrayList<JButton> selectedButtons;
-
+  /*
+   * the colour scheme when the dialog was opened, or
+   * the scheme last saved to file
+   */
   ColourSchemeI oldColourScheme;
 
-  JInternalFrame frame;
+  /*
+   * flag is true if the colour scheme has been changed since the
+   * dialog was opened, or the changes last saved to file
+   */
+  boolean changed;
 
-  JalviewStructureDisplayI jmol;
+  JInternalFrame frame;
 
-  ArrayList<JButton> upperCaseButtons;
+  List<JButton> upperCaseButtons;
 
-  ArrayList<JButton> lowerCaseButtons;
+  List<JButton> lowerCaseButtons;
 
   /**
-   * Creates a new UserDefinedColours object.
+   * Creates and displays a new UserDefinedColours panel
    * 
-   * @param ap
-   *          DOCUMENT ME!
-   * @param sg
-   *          DOCUMENT ME!
+   * @param alignPanel
    */
-  public UserDefinedColours(AlignmentPanel ap, SequenceGroup sg)
+  public UserDefinedColours(AlignmentPanel alignPanel)
   {
-    super();
+    this();
 
     lcaseColour.setEnabled(false);
 
-    this.ap = ap;
-    seqGroup = sg;
+    this.ap = alignPanel;
 
-    if (seqGroup != null)
-    {
-      oldColourScheme = seqGroup.cs;
-    }
-    else
-    {
-      oldColourScheme = ap.av.getGlobalColourScheme();
-    }
+    oldColourScheme = alignPanel.av.getGlobalColourScheme();
 
     if (oldColourScheme instanceof UserColourScheme)
     {
-      schemeName.setText(((UserColourScheme) oldColourScheme).getName());
+      schemeName.setText(oldColourScheme.getSchemeName());
       if (((UserColourScheme) oldColourScheme).getLowerCaseColours() != null)
       {
         caseSensitive.setSelected(true);
@@ -131,25 +132,10 @@ public class UserDefinedColours extends GUserDefinedColours implements
     showFrame();
   }
 
-  public UserDefinedColours(JalviewStructureDisplayI jmol,
-          ColourSchemeI oldcs)
+  UserDefinedColours()
   {
     super();
-    this.jmol = jmol;
-
-    colorChooser.getSelectionModel().addChangeListener(this);
-
-    oldColourScheme = oldcs;
-
-    if (oldColourScheme instanceof UserColourScheme)
-    {
-      schemeName.setText(((UserColourScheme) oldColourScheme).getName());
-    }
-
-    resetButtonPanel(false);
-
-    showFrame();
-
+    selectedButtons = new ArrayList<JButton>();
   }
 
   void showFrame()
@@ -160,14 +146,17 @@ public class UserDefinedColours extends GUserDefinedColours implements
     Desktop.addInternalFrame(frame,
             MessageManager.getString("label.user_defined_colours"),
             MY_FRAME_WIDTH, MY_FRAME_HEIGHT, true);
-
-    if (seqGroup != null)
-    {
-      frame.setTitle(frame.getTitle() + " (" + seqGroup.getName() + ")");
-    }
   }
 
-  void resetButtonPanel(boolean caseSensitive)
+  /**
+   * Rebuilds the panel with coloured buttons for residues. If not case
+   * sensitive colours, show 3-letter amino acid code as button text. If case
+   * sensitive, just show the single letter code, in order to make space for the
+   * additional buttons.
+   * 
+   * @param isCaseSensitive
+   */
+  void resetButtonPanel(boolean isCaseSensitive)
   {
     buttonPanel.removeAll();
 
@@ -176,23 +165,13 @@ public class UserDefinedColours extends GUserDefinedColours implements
       upperCaseButtons = new ArrayList<JButton>();
     }
 
-    JButton button;
-    String label;
     for (int i = 0; i < 20; i++)
     {
-      if (caseSensitive)
-      {
-        label = ResidueProperties.aa[i];
-      }
-      else
-      {
-        label = ResidueProperties.aa2Triplet.get(ResidueProperties.aa[i])
-                .toString();
-      }
-
-      button = makeButton(label, ResidueProperties.aa[i], upperCaseButtons,
-              i);
-
+      String label = isCaseSensitive ? ResidueProperties.aa[i]
+              : ResidueProperties.aa2Triplet.get(ResidueProperties.aa[i])
+                      .toString();
+      JButton button = makeButton(label, ResidueProperties.aa[i],
+              upperCaseButtons, i);
       buttonPanel.add(button);
     }
 
@@ -201,7 +180,7 @@ public class UserDefinedColours extends GUserDefinedColours implements
     buttonPanel.add(makeButton("X", "X", upperCaseButtons, 22));
     buttonPanel.add(makeButton("Gap", "-", upperCaseButtons, 23));
 
-    if (!caseSensitive)
+    if (!isCaseSensitive)
     {
       gridLayout.setRows(6);
       gridLayout.setColumns(4);
@@ -221,14 +200,14 @@ public class UserDefinedColours extends GUserDefinedColours implements
       {
         int row = i / cols + 1;
         int index = (row * cols) + i;
-        button = makeButton(ResidueProperties.aa[i].toLowerCase(),
+        JButton button = makeButton(ResidueProperties.aa[i].toLowerCase(),
                 ResidueProperties.aa[i].toLowerCase(), lowerCaseButtons, i);
 
         buttonPanel.add(button, index);
       }
     }
 
-    if (caseSensitive)
+    if (isCaseSensitive)
     {
       buttonPanel.add(makeButton("b", "b", lowerCaseButtons, 20));
       buttonPanel.add(makeButton("z", "z", lowerCaseButtons, 21));
@@ -239,7 +218,7 @@ public class UserDefinedColours extends GUserDefinedColours implements
     // codes
     if (this.frame != null)
     {
-      int newWidth = caseSensitive ? MY_FRAME_WIDTH_CASE_SENSITIVE
+      int newWidth = isCaseSensitive ? MY_FRAME_WIDTH_CASE_SENSITIVE
               : MY_FRAME_WIDTH;
       this.frame.setSize(newWidth, this.frame.getHeight());
     }
@@ -249,35 +228,38 @@ public class UserDefinedColours extends GUserDefinedColours implements
   }
 
   /**
-   * DOCUMENT ME!
+   * ChangeListener handler for when a colour is picked in the colour chooser.
+   * The action is to apply the colour to all selected buttons as their
+   * background colour. Foreground colour (text) is set to a lighter shade in
+   * order to highlight which buttons are selected. If 'Lower Case Colour' is
+   * active, then the colour is applied to all lower case buttons (as well as
+   * the Lower Case Colour button itself).
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void stateChanged(ChangeEvent evt)
   {
-    if (selectedButtons != null)
+    JButton button = null;
+    final Color newColour = colorChooser.getColor();
+    if (lcaseColour.isSelected())
     {
-      JButton button = null;
-      final Color newColour = colorChooser.getColor();
-      for (int i = 0; i < selectedButtons.size(); i++)
+      selectedButtons.clear();
+      for (int i = 0; i < lowerCaseButtons.size(); i++)
       {
-        button = selectedButtons.get(i);
+        button = lowerCaseButtons.get(i);
         button.setBackground(newColour);
-        button.setForeground(ColorUtils.brighterThan(newColour));
-      }
-      if (button == lcaseColour)
-      {
-        for (int i = 0; i < lowerCaseButtons.size(); i++)
-        {
-          button = lowerCaseButtons.get(i);
-          button.setBackground(newColour);
-          button.setForeground(ColorUtils.brighterThan(button
-                  .getBackground()));
-        }
+        button.setForeground(ColorUtils.brighterThan(button.getBackground()));
       }
     }
+    for (int i = 0; i < selectedButtons.size(); i++)
+    {
+      button = selectedButtons.get(i);
+      button.setBackground(newColour);
+      button.setForeground(ColorUtils.brighterThan(newColour));
+    }
+
+    changed = true;
   }
 
   /**
@@ -300,11 +282,6 @@ public class UserDefinedColours extends GUserDefinedColours implements
    */
   public void colourButtonPressed(MouseEvent e)
   {
-    if (selectedButtons == null)
-    {
-      selectedButtons = new ArrayList<JButton>();
-    }
-
     JButton pressed = (JButton) e.getSource();
 
     if (e.isShiftDown())
@@ -384,28 +361,35 @@ public class UserDefinedColours extends GUserDefinedColours implements
   }
 
   /**
-   * DOCUMENT ME!
+   * A helper method to update or make a colour button, whose background colour
+   * is the associated colour, and text colour a darker shade of the same. If
+   * the button is already in the list, then its text and margins are updated,
+   * if not then it is created and added. This method supports toggling between
+   * case-sensitive and case-insensitive button panels. The case-sensitive
+   * version has abbreviated button text in order to fit in more buttons.
    * 
    * @param label
-   *          DOCUMENT ME!
-   * @param aa
-   *          DOCUMENT ME!
+   * @param residue
+   * @param the
+   *          list of buttons
+   * @param buttonIndex
+   *          the button's position in the list
    */
-  JButton makeButton(String label, String aa,
-          ArrayList<JButton> caseSensitiveButtons, int buttonIndex)
+  JButton makeButton(String label, String residue, List<JButton> buttons,
+          int buttonIndex)
   {
     final JButton button;
     Color col;
 
-    if (buttonIndex < caseSensitiveButtons.size())
+    if (buttonIndex < buttons.size())
     {
-      button = caseSensitiveButtons.get(buttonIndex);
+      button = buttons.get(buttonIndex);
       col = button.getBackground();
     }
     else
     {
       button = new JButton();
-      button.addMouseListener(new java.awt.event.MouseAdapter()
+      button.addMouseListener(new MouseAdapter()
       {
         @Override
         public void mouseClicked(MouseEvent e)
@@ -414,46 +398,45 @@ public class UserDefinedColours extends GUserDefinedColours implements
         }
       });
 
-      caseSensitiveButtons.add(button);
+      buttons.add(button);
 
+      /*
+       * make initial button colour that of the current colour scheme,
+       * if it is a simple per-residue colouring, else white
+       */
       col = Color.white;
-      if (oldColourScheme != null)
+      if (oldColourScheme != null && oldColourScheme.isSimple())
       {
-        try
-        {
-          col = oldColourScheme.findColour(aa.charAt(0), -1, null);
-        } catch (Exception ex)
-        {
-        }
+        col = oldColourScheme.findColour(residue.charAt(0), 0, null, null,
+                0f);
       }
     }
 
     if (caseSensitive.isSelected())
     {
-      button.setMargin(new java.awt.Insets(2, 2, 2, 2));
+      button.setMargin(new Insets(2, 2, 2, 2));
     }
     else
     {
-      button.setMargin(new java.awt.Insets(2, 14, 2, 14));
+      button.setMargin(new Insets(2, 14, 2, 14));
     }
 
     button.setOpaque(true); // required for the next line to have effect
     button.setBackground(col);
     button.setText(label);
     button.setForeground(ColorUtils.darkerThan(col));
-    button.setFont(new java.awt.Font("Verdana", Font.BOLD, 10));
+    button.setFont(VERDANA_BOLD_10);
 
     return button;
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * On 'OK', check that at least one colour has been assigned to a residue (and
+   * if not issue a warning), and apply the chosen colour scheme and close the
+   * panel.
    */
   @Override
-  protected void okButton_actionPerformed(ActionEvent e)
+  protected void okButton_actionPerformed()
   {
     if (isNoSelectionMade())
     {
@@ -464,7 +447,16 @@ public class UserDefinedColours extends GUserDefinedColours implements
     }
     else
     {
-      applyButton_actionPerformed(null);
+      /*
+       * OK is treated as 'apply colours and close'
+       */
+      applyButton_actionPerformed();
+
+      /*
+       * If editing a named colour scheme, warn if changes
+       * have not been saved
+       */
+      warnIfUnsavedChanges();
 
       try
       {
@@ -476,6 +468,57 @@ public class UserDefinedColours extends GUserDefinedColours implements
   }
 
   /**
+   * If we have made changes to an existing user defined colour scheme but not
+   * saved them, show a dialog with the option to save. If the user chooses to
+   * save, do so, else clear the colour scheme name to indicate a new colour
+   * scheme.
+   */
+  protected void warnIfUnsavedChanges()
+  {
+    if (!changed)
+    {
+      return;
+    }
+
+    String name = schemeName.getText().trim();
+    if (oldColourScheme != null && !"".equals(name)
+            && name.equals(oldColourScheme.getSchemeName()))
+    {
+      String message = MessageManager.formatMessage("label.scheme_changed",
+              name);
+      String title = MessageManager.getString("label.save_changes");
+      String[] options = new String[] { title,
+          MessageManager.getString("label.dont_save_changes"), };
+      final String question = JvSwingUtils.wrapTooltip(true, message);
+      int response = JvOptionPane.showOptionDialog(Desktop.desktop,
+              question, title, JvOptionPane.DEFAULT_OPTION,
+              JvOptionPane.PLAIN_MESSAGE, null, options, options[0]);
+
+      boolean saved = false;
+      if (response == 0)
+      {
+        /*
+         * prompt to save changes to file
+         */
+        saved = savebutton_actionPerformed();
+      }
+
+      /*
+       * if user chooses not to save (either in this dialog or in the
+       * save as dialogs), treat this as a new user defined colour scheme
+       */
+      if (!saved)
+      {
+        /*
+         * clear scheme name and re-apply as an anonymous scheme
+         */
+        schemeName.setText("");
+        applyButton_actionPerformed();
+      }
+    }
+  }
+
+  /**
    * Returns true if the user has not made any colour selection (including if
    * 'case-sensitive' selected and no lower-case colour chosen).
    * 
@@ -493,13 +536,10 @@ public class UserDefinedColours extends GUserDefinedColours implements
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Applies the current colour scheme to the alignment or sequence group
    */
   @Override
-  protected void applyButton_actionPerformed(ActionEvent e)
+  protected void applyButton_actionPerformed()
   {
     if (isNoSelectionMade())
     {
@@ -510,23 +550,16 @@ public class UserDefinedColours extends GUserDefinedColours implements
 
     }
     UserColourScheme ucs = getSchemeFromButtons();
-    ucs.setName(schemeName.getText());
 
-    if (seqGroup != null)
-    {
-      seqGroup.cs = ucs;
-      ap.paintAlignment(true);
-    }
-    else if (ap != null)
-    {
-      ap.alignFrame.changeColour(ucs);
-    }
-    else if (jmol != null)
-    {
-      jmol.setJalviewColourScheme(ucs);
-    }
+    ap.alignFrame.changeColour(ucs);
   }
 
+  /**
+   * Constructs an instance of UserColourScheme with the residue colours
+   * currently set on the buttons on the panel
+   * 
+   * @return
+   */
   UserColourScheme getSchemeFromButtons()
   {
 
@@ -552,6 +585,7 @@ public class UserDefinedColours extends GUserDefinedColours implements
     }
 
     UserColourScheme ucs = new UserColourScheme(newColours);
+    ucs.setName(schemeName.getText());
 
     if (caseSensitive.isSelected())
     {
@@ -577,28 +611,24 @@ public class UserDefinedColours extends GUserDefinedColours implements
       ucs.setLowerCaseColours(newColours);
     }
 
-    if (ap != null)
-    {
-      ucs.setThreshold(0, ap.av.isIgnoreGapsConsensus());
-    }
-
     return ucs;
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Action on clicking Load scheme button.
+   * <ul>
+   * <li>Open a file chooser to browse for files with extension .jc</li>
+   * <li>Load in the colour scheme and transfer it to this panel's buttons</li>
+   * <li>Register the loaded colour scheme</li>
+   * </ul>
    */
   @Override
-  protected void loadbutton_actionPerformed(ActionEvent e)
+  protected void loadbutton_actionPerformed()
   {
     upperCaseButtons = new ArrayList<JButton>();
     lowerCaseButtons = new ArrayList<JButton>();
 
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "jc",
+    JalviewFileChooser chooser = new JalviewFileChooser("jc",
             "Jalview User Colours");
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(MessageManager
@@ -607,443 +637,258 @@ public class UserDefinedColours extends GUserDefinedColours implements
 
     int value = chooser.showOpenDialog(this);
 
-    if (value == JalviewFileChooser.APPROVE_OPTION)
+    if (value != JalviewFileChooser.APPROVE_OPTION)
     {
-      File choice = chooser.getSelectedFile();
-      jalview.bin.Cache.setProperty("LAST_DIRECTORY", choice.getParent());
-      String defaultColours = jalview.bin.Cache.getDefault(
-              "USER_DEFINED_COLOURS", choice.getPath());
-      if (defaultColours.indexOf(choice.getPath()) == -1)
-      {
-        defaultColours = defaultColours.concat("|")
-                .concat(choice.getPath());
-      }
-
-      jalview.bin.Cache.setProperty("USER_DEFINED_COLOURS", defaultColours);
-
-      UserColourScheme ucs = loadColours(choice.getAbsolutePath());
-      Color[] colors = ucs.getColours();
-      schemeName.setText(ucs.getName());
-
-      if (ucs.getLowerCaseColours() != null)
-      {
-        caseSensitive.setSelected(true);
-        lcaseColour.setEnabled(true);
-        resetButtonPanel(true);
-        for (int i = 0; i < lowerCaseButtons.size(); i++)
-        {
-          JButton button = lowerCaseButtons.get(i);
-          button.setBackground(ucs.getLowerCaseColours()[i]);
-        }
+      return;
+    }
+    File choice = chooser.getSelectedFile();
+    Cache.setProperty(LAST_DIRECTORY, choice.getParent());
 
-      }
-      else
-      {
-        caseSensitive.setSelected(false);
-        lcaseColour.setEnabled(false);
-        resetButtonPanel(false);
-      }
+    UserColourScheme ucs = ColourSchemeLoader.loadColourScheme(choice
+            .getAbsolutePath());
+    Color[] colors = ucs.getColours();
+    schemeName.setText(ucs.getSchemeName());
 
-      for (int i = 0; i < upperCaseButtons.size(); i++)
+    if (ucs.getLowerCaseColours() != null)
+    {
+      caseSensitive.setSelected(true);
+      lcaseColour.setEnabled(true);
+      resetButtonPanel(true);
+      for (int i = 0; i < lowerCaseButtons.size(); i++)
       {
-        JButton button = upperCaseButtons.get(i);
-        button.setBackground(colors[i]);
+        JButton button = lowerCaseButtons.get(i);
+        button.setBackground(ucs.getLowerCaseColours()[i]);
       }
+    }
+    else
+    {
+      caseSensitive.setSelected(false);
+      lcaseColour.setEnabled(false);
+      resetButtonPanel(false);
+    }
 
+    for (int i = 0; i < upperCaseButtons.size(); i++)
+    {
+      JButton button = upperCaseButtons.get(i);
+      button.setBackground(colors[i]);
     }
+
+    addNewColourScheme(choice.getPath());
   }
 
   /**
-   * DOCUMENT ME!
+   * Loads the user-defined colour scheme from the first file listed in property
+   * "USER_DEFINED_COLOURS". If this fails, returns an all-white colour scheme.
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
   public static UserColourScheme loadDefaultColours()
   {
     UserColourScheme ret = null;
 
-    String colours = jalview.bin.Cache.getProperty("USER_DEFINED_COLOURS");
+    String colours = Cache.getProperty(USER_DEFINED_COLOURS);
     if (colours != null)
     {
       if (colours.indexOf("|") > -1)
       {
         colours = colours.substring(0, colours.indexOf("|"));
       }
-
-      ret = loadColours(colours);
+      ret = ColourSchemeLoader.loadColourScheme(colours);
     }
 
     if (ret == null)
     {
-      Color[] newColours = new Color[24];
-      for (int i = 0; i < 24; i++)
-      {
-        newColours[i] = Color.white;
-      }
-      ret = new UserColourScheme(newColours);
+      ret = new UserColourScheme("white");
     }
 
     return ret;
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param file
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  static UserColourScheme loadColours(String file)
-  {
-    UserColourScheme ucs = null;
-    Color[] newColours = null;
-    try
-    {
-      InputStreamReader in = new InputStreamReader(
-              new FileInputStream(file), "UTF-8");
-
-      jalview.schemabinding.version2.JalviewUserColours jucs = new jalview.schemabinding.version2.JalviewUserColours();
-
-      org.exolab.castor.xml.Unmarshaller unmar = new org.exolab.castor.xml.Unmarshaller(
-              jucs);
-      jucs = (jalview.schemabinding.version2.JalviewUserColours) unmar
-              .unmarshal(in);
-
-      newColours = new Color[24];
-
-      Color[] lowerCase = null;
-      boolean caseSensitive = false;
-
-      String name;
-      int index;
-      for (int i = 0; i < jucs.getColourCount(); i++)
-      {
-        name = jucs.getColour(i).getName();
-        if (ResidueProperties.aa3Hash.containsKey(name))
-        {
-          index = ResidueProperties.aa3Hash.get(name).intValue();
-        }
-        else
-        {
-          index = ResidueProperties.aaIndex[name.charAt(0)];
-        }
-        if (index == -1)
-        {
-          continue;
-        }
-
-        if (name.toLowerCase().equals(name))
-        {
-          if (lowerCase == null)
-          {
-            lowerCase = new Color[23];
-          }
-          caseSensitive = true;
-          lowerCase[index] = new Color(Integer.parseInt(jucs.getColour(i)
-                  .getRGB(), 16));
-        }
-        else
-        {
-          newColours[index] = new Color(Integer.parseInt(jucs.getColour(i)
-                  .getRGB(), 16));
-        }
-      }
-
-      if (newColours != null)
-      {
-        ucs = new UserColourScheme(newColours);
-        ucs.setName(jucs.getSchemeName());
-        if (caseSensitive)
-        {
-          ucs.setLowerCaseColours(lowerCase);
-        }
-      }
-
-    } catch (Exception ex)
-    {
-      // Could be Archive Jalview format
-      try
-      {
-        InputStreamReader in = new InputStreamReader(new FileInputStream(
-                file), "UTF-8");
-
-        jalview.binding.JalviewUserColours jucs = new jalview.binding.JalviewUserColours();
-
-        jucs = jucs.unmarshal(in);
-
-        newColours = new Color[jucs.getColourCount()];
-
-        for (int i = 0; i < 24; i++)
-        {
-          newColours[i] = new Color(Integer.parseInt(jucs.getColour(i)
-                  .getRGB(), 16));
-        }
-        if (newColours != null)
-        {
-          ucs = new UserColourScheme(newColours);
-          ucs.setName(jucs.getSchemeName());
-        }
-      } catch (Exception ex2)
-      {
-        ex2.printStackTrace();
-      }
-
-      if (newColours == null)
-      {
-        System.out.println("Error loading User ColourFile\n" + ex);
-      }
-    }
-
-    return ucs;
-  }
-
-  /**
-   * DOCUMENT ME!
+   * Action on pressing the Save button.
+   * <ul>
+   * <li>Check a name has been entered</li>
+   * <li>Warn if the name already exists, remove any existing scheme of the same
+   * name if overwriting</li>
+   * <li>Do the standard file chooser thing to write with extension .jc</li>
+   * <li>If saving changes (possibly not yet applied) to the currently selected
+   * colour scheme, then apply the changes, as it is too late to back out now</li>
+   * <li>Don't apply the changes if the currently selected scheme is different,
+   * to allow a new scheme to be configured and saved but not applied</li>
+   * </ul>
+   * Returns true if the scheme is saved to file, false if it is not
    * 
-   * @param e
-   *          DOCUMENT ME!
+   * @return
    */
   @Override
-  protected void savebutton_actionPerformed(ActionEvent e)
+  protected boolean savebutton_actionPerformed()
   {
-    if (schemeName.getText().trim().length() < 1)
+    String name = schemeName.getText().trim();
+    if (name.length() < 1)
     {
       JvOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
               .getString("label.user_colour_scheme_must_have_name"),
               MessageManager.getString("label.no_name_colour_scheme"),
               JvOptionPane.WARNING_MESSAGE);
-      return;
+      return false;
     }
 
-    if (userColourSchemes != null
-            && userColourSchemes.containsKey(schemeName.getText()))
+    if (ColourSchemes.getInstance().nameExists(name))
     {
       int reply = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
               MessageManager.formatMessage(
                       "label.colour_scheme_exists_overwrite", new Object[] {
-                          schemeName.getText(), schemeName.getText() }),
+                          name, name }),
               MessageManager.getString("label.duplicate_scheme_name"),
               JvOptionPane.YES_NO_OPTION);
       if (reply != JvOptionPane.YES_OPTION)
       {
-        return;
+        return false;
       }
-
-      userColourSchemes.remove(schemeName.getText());
     }
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "jc",
+    JalviewFileChooser chooser = new JalviewFileChooser("jc",
             "Jalview User Colours");
 
-    chooser.setFileView(new JalviewFileView());
+    JalviewFileView fileView = new JalviewFileView();
+    chooser.setFileView(fileView);
     chooser.setDialogTitle(MessageManager
             .getString("label.save_colour_scheme"));
     chooser.setToolTipText(MessageManager.getString("action.save"));
 
     int value = chooser.showSaveDialog(this);
 
-    if (value == JalviewFileChooser.APPROVE_OPTION)
+    if (value != JalviewFileChooser.APPROVE_OPTION)
     {
-      String choice = chooser.getSelectedFile().getPath();
-      String defaultColours = jalview.bin.Cache.getDefault(
-              "USER_DEFINED_COLOURS", choice);
-      if (defaultColours.indexOf(choice) == -1)
-      {
-        if (defaultColours.length() > 0)
-        {
-          defaultColours = defaultColours.concat("|");
-        }
-        defaultColours = defaultColours.concat(choice);
-      }
-
-      userColourSchemes.put(schemeName.getText(), getSchemeFromButtons());
-
-      ap.alignFrame.updateUserColourMenu();
-
-      jalview.bin.Cache.setProperty("USER_DEFINED_COLOURS", defaultColours);
+      return false;
+    }
 
-      jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
+    File file = chooser.getSelectedFile();
+    UserColourScheme updatedScheme = addNewColourScheme(file.getPath());
+    saveToFile(file);
+    changed = false;
 
-      ucs.setSchemeName(schemeName.getText());
-      try
-      {
-        PrintWriter out = new PrintWriter(new OutputStreamWriter(
-                new FileOutputStream(choice), "UTF-8"));
-
-        for (int i = 0; i < buttonPanel.getComponentCount(); i++)
-        {
-          JButton button = (JButton) buttonPanel.getComponent(i);
-          jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
-          col.setName(button.getText());
-          col.setRGB(jalview.util.Format.getHexString(button
-                  .getBackground()));
-          ucs.addColour(col);
-        }
-
-        ucs.marshal(out);
-        out.close();
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
-      }
+    /*
+     * changes saved - apply to alignment if we are changing 
+     * the currently selected colour scheme; also make the updated
+     * colours the 'backout' scheme on Cancel
+     */
+    if (oldColourScheme != null
+            && name.equals(oldColourScheme.getSchemeName()))
+    {
+      oldColourScheme = updatedScheme;
+      applyButton_actionPerformed();
     }
+    return true;
   }
 
   /**
-   * DOCUMENT ME!
+   * Adds the current colour scheme to the Jalview properties file so it is
+   * loaded on next startup, and updates the Colour menu in the parent
+   * AlignFrame (if there is one). Note this action does not including applying
+   * the colour scheme.
    * 
-   * @param e
-   *          DOCUMENT ME!
+   * @param filePath
+   * @return
    */
-  @Override
-  protected void cancelButton_actionPerformed(ActionEvent e)
+  protected UserColourScheme addNewColourScheme(String filePath)
   {
-    if (ap != null)
+    /*
+     * update the delimited list of user defined colour files in
+     * Jalview property USER_DEFINED_COLOURS
+     */
+    String defaultColours = Cache
+            .getDefault(USER_DEFINED_COLOURS, filePath);
+    if (defaultColours.indexOf(filePath) == -1)
     {
-      if (seqGroup != null)
+      if (defaultColours.length() > 0)
       {
-        seqGroup.cs = oldColourScheme;
+        defaultColours = defaultColours.concat("|");
       }
-      else if (ap != null)
-      {
-        ap.av.setGlobalColourScheme(oldColourScheme);
-      }
-      ap.paintAlignment(true);
+      defaultColours = defaultColours.concat(filePath);
     }
+    Cache.setProperty(USER_DEFINED_COLOURS, defaultColours);
 
-    if (jmol != null)
-    {
-      jmol.setJalviewColourScheme(oldColourScheme);
-    }
+    /*
+     * construct and register the colour scheme
+     */
+    UserColourScheme ucs = getSchemeFromButtons();
+    ColourSchemes.getInstance().registerColourScheme(ucs);
 
-    try
-    {
-      frame.setClosed(true);
-    } catch (Exception ex)
+    /*
+     * update the Colour menu items
+     */
+    if (ap != null)
     {
+      ap.alignFrame.buildColourMenu();
     }
-  }
-
-  static Hashtable userColourSchemes;
 
-  public static Hashtable getUserColourSchemes()
-  {
-    return userColourSchemes;
+    return ucs;
   }
 
-  public static void initUserColourSchemes(String files)
+  /**
+   * Saves the colour scheme to file in XML format
+   * 
+   * @param path
+   */
+  protected void saveToFile(File toFile)
   {
-    userColourSchemes = new Hashtable();
-
-    if (files == null || files.length() == 0)
+    /*
+     * build a Java model of colour scheme as XML, and 
+     * marshal to file
+     */
+    JalviewUserColours ucs = new JalviewUserColours();
+    String name = schemeName.getText();
+    ucs.setSchemeName(name);
+    try
     {
-      return;
-    }
+      PrintWriter out = new PrintWriter(new OutputStreamWriter(
+              new FileOutputStream(toFile), "UTF-8"));
 
-    // In case colours can't be loaded, we'll remove them
-    // from the default list here.
-    StringBuffer coloursFound = new StringBuffer();
-    StringTokenizer st = new StringTokenizer(files, "|");
-    while (st.hasMoreElements())
-    {
-      String file = st.nextToken();
-      try
+      for (int i = 0; i < buttonPanel.getComponentCount(); i++)
       {
-        UserColourScheme ucs = loadColours(file);
-        if (ucs != null)
-        {
-          if (coloursFound.length() > 0)
-          {
-            coloursFound.append("|");
-          }
-          coloursFound.append(file);
-          userColourSchemes.put(ucs.getName(), ucs);
-        }
-      } catch (Exception ex)
-      {
-        System.out.println("Error loading User ColourFile\n" + ex);
+        JButton button = (JButton) buttonPanel.getComponent(i);
+        Colour col = new Colour();
+        col.setName(button.getText());
+        col.setRGB(Format.getHexString(button.getBackground()));
+        ucs.addColour(col);
       }
-    }
-    if (!files.equals(coloursFound.toString()))
+      ucs.marshal(out);
+      out.close();
+    } catch (Exception ex)
     {
-      if (coloursFound.toString().length() > 1)
-      {
-        jalview.bin.Cache.setProperty("USER_DEFINED_COLOURS",
-                coloursFound.toString());
-      }
-      else
-      {
-        jalview.bin.Cache.applicationProperties
-                .remove("USER_DEFINED_COLOURS");
-      }
+      ex.printStackTrace();
     }
   }
 
-  public static void removeColourFromDefaults(String target)
+  /**
+   * On cancel, restores the colour scheme that was selected before the dialogue
+   * was opened
+   */
+  @Override
+  protected void cancelButton_actionPerformed()
   {
-    // The only way to find colours by name is to load them in
-    // In case colours can't be loaded, we'll remove them
-    // from the default list here.
-
-    userColourSchemes = new Hashtable();
+    ap.alignFrame.changeColour(oldColourScheme);
+    ap.paintAlignment(true);
 
-    StringBuffer coloursFound = new StringBuffer();
-    StringTokenizer st = new StringTokenizer(
-            jalview.bin.Cache.getProperty("USER_DEFINED_COLOURS"), "|");
-
-    while (st.hasMoreElements())
-    {
-      String file = st.nextToken();
-      try
-      {
-        UserColourScheme ucs = loadColours(file);
-        if (ucs != null && !ucs.getName().equals(target))
-        {
-          if (coloursFound.length() > 0)
-          {
-            coloursFound.append("|");
-          }
-          coloursFound.append(file);
-          userColourSchemes.put(ucs.getName(), ucs);
-        }
-      } catch (Exception ex)
-      {
-        System.out.println("Error loading User ColourFile\n" + ex);
-      }
-    }
-
-    if (coloursFound.toString().length() > 1)
+    try
     {
-      jalview.bin.Cache.setProperty("USER_DEFINED_COLOURS",
-              coloursFound.toString());
-    }
-    else
+      frame.setClosed(true);
+    } catch (Exception ex)
     {
-      jalview.bin.Cache.applicationProperties
-              .remove("USER_DEFINED_COLOURS");
     }
-
-  }
-
-  @Override
-  public void caseSensitive_actionPerformed(ActionEvent e)
-  {
-    resetButtonPanel(caseSensitive.isSelected());
-    lcaseColour.setEnabled(caseSensitive.isSelected());
   }
 
+  /**
+   * Action on selecting or deselecting the Case Sensitive option. When
+   * selected, separate buttons are shown for lower case residues, and the panel
+   * is resized to accommodate them. Also, the checkbox for 'apply colour to all
+   * lower case' is enabled.
+   */
   @Override
-  public void lcaseColour_actionPerformed(ActionEvent e)
+  public void caseSensitive_actionPerformed()
   {
-    if (selectedButtons == null)
-    {
-      selectedButtons = new ArrayList<JButton>();
-    }
-    else
-    {
-      selectedButtons.clear();
-    }
-    selectedButtons.add(lcaseColour);
+    boolean selected = caseSensitive.isSelected();
+    resetButtonPanel(selected);
+    lcaseColour.setEnabled(selected);
   }
 }
index 75ddba5..d58cb5a 100644 (file)
@@ -23,6 +23,7 @@ package jalview.gui;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.io.VamsasAppDatastore;
@@ -43,7 +44,6 @@ import java.util.IdentityHashMap;
 import java.util.Iterator;
 
 import javax.swing.JInternalFrame;
-import javax.swing.JOptionPane;
 
 import uk.ac.vamsas.client.ClientHandle;
 import uk.ac.vamsas.client.IClient;
@@ -821,7 +821,7 @@ public class VamsasApplication implements SelectionSource, VamsasSource
               {
                 // TODO: rationalise : can only clear a selection over a
                 // referred to object
-                ssm.sendSelection(null, null, me);
+                ssm.sendSelection(null, null, null, me);
                 return;
               }
               Class type = null;
@@ -955,7 +955,7 @@ public class VamsasApplication implements SelectionSource, VamsasSource
               }
               if (send)
               {
-                ssm.sendSelection(jselection, colsel, me);
+                ssm.sendSelection(jselection, colsel, null, me);
               }
               // discard message.
               for (int c = 0; c < jvobjs.length; c++)
@@ -1004,7 +1004,8 @@ public class VamsasApplication implements SelectionSource, VamsasSource
 
           @Override
           public void selection(SequenceGroup seqsel,
-                  ColumnSelection colsel, SelectionSource source)
+                  ColumnSelection colsel, HiddenColumns hidden,
+                  SelectionSource source)
           {
             if (vobj2jv == null)
             {
@@ -1079,7 +1080,9 @@ public class VamsasApplication implements SelectionSource, VamsasSource
                   }
                   else
                   {
-                    int[] intervals = colsel.getVisibleContigs(
+                    // 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)
                     {
index b260e1b..1aa0803 100644 (file)
@@ -183,8 +183,7 @@ public class WsParamSetManager implements ParamManager
     }
     if (filename == null)
     {
-      JalviewFileChooser chooser = new JalviewFileChooser(
-              Cache.getProperty("LAST_DIRECTORY"), "wsparams",
+      JalviewFileChooser chooser = new JalviewFileChooser("wsparams",
               "Web Service Parameter File");
       chooser.setFileView(new JalviewFileView());
       chooser.setDialogTitle(MessageManager
index 165e8f2..32671d5 100644 (file)
@@ -454,7 +454,7 @@ public class WsPreferences extends GWsPreferences
     JTextField urltf = new JTextField(url, 40);
     JPanel panel = new JPanel(new BorderLayout());
     JPanel pane12 = new JPanel(new BorderLayout());
-    pane12.add(new JLabel(MessageManager.getString("label.url")),
+    pane12.add(new JLabel(MessageManager.getString("label.url:")),
             BorderLayout.CENTER);
     pane12.add(urltf, BorderLayout.EAST);
     panel.add(pane12, BorderLayout.NORTH);
@@ -574,6 +574,7 @@ public class WsPreferences extends GWsPreferences
     new Thread(new Runnable()
     {
 
+      @Override
       public void run()
       {
         // force a refresh.
@@ -599,6 +600,7 @@ public class WsPreferences extends GWsPreferences
       new Thread(new Runnable()
       {
 
+        @Override
         public void run()
         {
           progressBar.setVisible(true);
@@ -624,6 +626,7 @@ public class WsPreferences extends GWsPreferences
       new Thread(new Runnable()
       {
 
+        @Override
         public void run()
         {
           long ct = System.currentTimeMillis();
@@ -681,6 +684,7 @@ public class WsPreferences extends GWsPreferences
     new Thread(new Runnable()
     {
 
+      @Override
       public void run()
       {
         updateWsMenuConfig(false);
index 82e71b5..c3e71da 100755 (executable)
@@ -27,13 +27,15 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.GraphLine;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
-import jalview.schemes.UserColourScheme;
+import jalview.util.ColorUtils;
 
+import java.awt.Color;
 import java.io.BufferedReader;
 import java.io.FileReader;
 import java.io.InputStreamReader;
@@ -108,23 +110,22 @@ public class AnnotationFile
    */
   public class ViewDef
   {
-    public String viewname;
+    // TODO this class is not used - remove?
+    public final String viewname;
 
-    public HiddenSequences hidseqs;
+    public final HiddenSequences hidseqs;
 
-    public ColumnSelection hiddencols;
+    public final HiddenColumns hiddencols;
 
-    public Vector visibleGroups;
+    public final Hashtable hiddenRepSeqs;
 
-    public Hashtable hiddenRepSeqs;
-
-    public ViewDef(String viewname, HiddenSequences hidseqs,
-            ColumnSelection hiddencols, Hashtable hiddenRepSeqs)
+    public ViewDef(String vname, HiddenSequences hseqs,
+            HiddenColumns hcols, Hashtable hRepSeqs)
     {
-      this.viewname = viewname;
-      this.hidseqs = hidseqs;
-      this.hiddencols = hiddencols;
-      this.hiddenRepSeqs = hiddenRepSeqs;
+      this.viewname = vname;
+      this.hidseqs = hseqs;
+      this.hiddencols = hcols;
+      this.hiddenRepSeqs = hRepSeqs;
     }
   }
 
@@ -140,7 +141,8 @@ public class AnnotationFile
    */
   public String printAnnotations(AlignmentAnnotation[] annotations,
           List<SequenceGroup> list, Hashtable properties,
-          ColumnSelection cs, AlignmentI al, ViewDef view)
+ HiddenColumns cs,
+          AlignmentI al, ViewDef view)
   {
     if (view != null)
     {
@@ -150,7 +152,7 @@ public class AnnotationFile
       }
       if (list == null)
       {
-        list = view.visibleGroups;
+        // list = view.visibleGroups;
       }
       if (cs == null)
       {
@@ -169,7 +171,7 @@ public class AnnotationFile
     if (cs != null && cs.hasHiddenColumns())
     {
       text.append("VIEW_HIDECOLS\t");
-      List<int[]> hc = cs.getHiddenColumns();
+      List<int[]> hc = cs.getHiddenRegions();
       boolean comma = false;
       for (int[] r : hc)
       {
@@ -535,7 +537,7 @@ public class AnnotationFile
     return false;
   }
 
-  public void printGroups(List<SequenceGroup> list)
+  protected void printGroups(List<SequenceGroup> list)
   {
     SequenceI seqrep = null;
     for (SequenceGroup sg : list)
@@ -581,7 +583,8 @@ public class AnnotationFile
       if (sg.cs != null)
       {
         text.append("colour=");
-        text.append(ColourSchemeProperty.getColourName(sg.cs));
+        text.append(ColourSchemeProperty.getColourName(sg.cs
+                .getColourScheme()));
         text.append("\t");
         if (sg.cs.getThreshold() != 0)
         {
@@ -663,15 +666,21 @@ public class AnnotationFile
           String file, DataSourceType protocol)
   {
     ColumnSelection colSel = viewport.getColumnSelection();
+    HiddenColumns hidden = viewport.getAlignment().getHiddenColumns();
     if (colSel == null)
     {
       colSel = new ColumnSelection();
     }
-    boolean rslt = readAnnotationFile(viewport.getAlignment(), colSel,
+    if (hidden == null)
+    {
+      hidden = new HiddenColumns();
+    }
+    boolean rslt = readAnnotationFile(viewport.getAlignment(), hidden,
             file, protocol);
-    if (rslt && (colSel.hasSelectedColumns() || colSel.hasHiddenColumns()))
+    if (rslt && (colSel.hasSelectedColumns() || hidden.hasHiddenColumns()))
     {
       viewport.setColumnSelection(colSel);
+      viewport.getAlignment().setHiddenColumns(hidden);
     }
 
     return rslt;
@@ -683,7 +692,7 @@ public class AnnotationFile
     return readAnnotationFile(al, null, file, sourceType);
   }
 
-  public boolean readAnnotationFile(AlignmentI al, ColumnSelection colSel,
+  public boolean readAnnotationFile(AlignmentI al, HiddenColumns hidden,
           String file, DataSourceType sourceType)
   {
     BufferedReader in = null;
@@ -712,7 +721,7 @@ public class AnnotationFile
       }
       if (in != null)
       {
-        return parseAnnotationFrom(al, colSel, in);
+        return parseAnnotationFrom(al, hidden, in);
       }
 
     } catch (Exception ex)
@@ -735,7 +744,7 @@ public class AnnotationFile
 
   private static String GRAPHLINE = "GRAPHLINE", COMBINE = "COMBINE";
 
-  public boolean parseAnnotationFrom(AlignmentI al, ColumnSelection colSel,
+  public boolean parseAnnotationFrom(AlignmentI al, HiddenColumns hidden,
           BufferedReader in) throws Exception
   {
     nlinesread = 0;
@@ -946,11 +955,11 @@ public class AnnotationFile
         {
           if (st.hasMoreTokens())
           {
-            if (colSel == null)
+            if (hidden == null)
             {
-              colSel = new ColumnSelection();
+              hidden = new HiddenColumns();
             }
-            parseHideCols(colSel, st.nextToken());
+            parseHideCols(hidden, st.nextToken());
           }
           modified = true;
           continue;
@@ -964,7 +973,7 @@ public class AnnotationFile
           }
           if (sr != null)
           {
-            if (colSel == null)
+            if (hidden == null)
             {
               System.err
                       .println("Cannot process HIDE_INSERTIONS without an alignment view: Ignoring line: "
@@ -973,7 +982,7 @@ public class AnnotationFile
             else
             {
               // consider deferring this till after the file has been parsed ?
-              colSel.hideInsertionsFor(sr);
+              hidden.hideInsertionsFor(sr);
             }
           }
           modified = true;
@@ -1179,7 +1188,7 @@ public class AnnotationFile
     return modified;
   }
 
-  private void parseHideCols(ColumnSelection colSel, String nextToken)
+  private void parseHideCols(HiddenColumns hidden, String nextToken)
   {
     StringTokenizer inval = new StringTokenizer(nextToken, ",");
     while (inval.hasMoreTokens())
@@ -1191,7 +1200,7 @@ public class AnnotationFile
         from = to = Integer.parseInt(range);
         if (from >= 0)
         {
-          colSel.hideColumns(from, to);
+          hidden.hideColumns(from, to);
         }
       }
       else
@@ -1207,7 +1216,7 @@ public class AnnotationFile
         }
         if (from > 0 && to >= from)
         {
-          colSel.hideColumns(from, to);
+          hidden.hideColumns(from, to);
         }
       }
     }
@@ -1223,29 +1232,21 @@ public class AnnotationFile
 
   Annotation parseAnnotation(String string, int graphStyle)
   {
-    boolean hasSymbols = (graphStyle == AlignmentAnnotation.NO_GRAPH); // don't
-    // do the
-    // glyph
-    // test
-    // if we
-    // don't
-    // want
-    // secondary
-    // structure
+    // don't do the glyph test if we don't want secondary structure
+    boolean hasSymbols = (graphStyle == AlignmentAnnotation.NO_GRAPH);
     String desc = null, displayChar = null;
     char ss = ' '; // secondaryStructure
     float value = 0;
     boolean parsedValue = false, dcset = false;
 
     // find colour here
-    java.awt.Color colour = null;
+    Color colour = null;
     int i = string.indexOf("[");
     int j = string.indexOf("]");
     if (i > -1 && j > -1)
     {
-      UserColourScheme ucs = new UserColourScheme();
-
-      colour = ucs.getColourFromString(string.substring(i + 1, j));
+      colour = ColorUtils.parseColourString(string.substring(i + 1,
+              j));
       if (i > 0 && string.charAt(i - 1) == ',')
       {
         // clip the preceding comma as well
@@ -1347,7 +1348,7 @@ public class AnnotationFile
 
   void colourAnnotations(AlignmentI al, String label, String colour)
   {
-    UserColourScheme ucs = new UserColourScheme(colour);
+    Color awtColour = ColorUtils.parseColourString(colour);
     Annotation[] annotations;
     for (int i = 0; i < al.getAlignmentAnnotation().length; i++)
     {
@@ -1358,7 +1359,7 @@ public class AnnotationFile
         {
           if (annotations[j] != null)
           {
-            annotations[j].colour = ucs.findColour('A');
+            annotations[j].colour = awtColour;
           }
         }
       }
@@ -1428,15 +1429,22 @@ public class AnnotationFile
           SequenceGroup groupRef)
   {
     String group = st.nextToken();
-    AlignmentAnnotation annotation = null, alannot[] = al
-            .getAlignmentAnnotation();
-    float value = new Float(st.nextToken()).floatValue();
+    AlignmentAnnotation[] alannot = al.getAlignmentAnnotation();
+    String nextToken = st.nextToken();
+    float value = 0f;
+    try
+    {
+      value = Float.valueOf(nextToken);
+    } catch (NumberFormatException e)
+    {
+      System.err.println("line " + nlinesread + ": Threshold '" + nextToken
+              + "' invalid, setting to zero");
+    }
     String label = st.hasMoreTokens() ? st.nextToken() : null;
-    java.awt.Color colour = null;
+    Color colour = null;
     if (st.hasMoreTokens())
     {
-      UserColourScheme ucs = new UserColourScheme(st.nextToken());
-      colour = ucs.findColour('A');
+      colour = ColorUtils.parseColourString(st.nextToken());
     }
     if (alannot != null)
     {
@@ -1450,10 +1458,6 @@ public class AnnotationFile
         }
       }
     }
-    if (annotation == null)
-    {
-      return;
-    }
   }
 
   void addGroup(AlignmentI al, StringTokenizer st)
@@ -1613,8 +1617,7 @@ public class AnnotationFile
     if (sg != null)
     {
       String keyValue, key, value;
-      ColourSchemeI def = sg.cs;
-      sg.cs = null;
+      ColourSchemeI def = sg.getColourScheme();
       while (st.hasMoreTokens())
       {
         keyValue = st.nextToken();
@@ -1627,7 +1630,8 @@ public class AnnotationFile
         }
         else if (key.equalsIgnoreCase("colour"))
         {
-          sg.cs = ColourSchemeProperty.getColour(al, value);
+          sg.cs.setColourScheme(ColourSchemeProperty
+                  .getColourScheme(al, value));
         }
         else if (key.equalsIgnoreCase("pidThreshold"))
         {
@@ -1648,7 +1652,7 @@ public class AnnotationFile
         }
         else if (key.equalsIgnoreCase("outlineColour"))
         {
-          sg.setOutlineColour(new UserColourScheme(value).findColour('A'));
+          sg.setOutlineColour(ColorUtils.parseColourString(value));
         }
         else if (key.equalsIgnoreCase("displayBoxes"))
         {
@@ -1668,11 +1672,11 @@ public class AnnotationFile
         }
         else if (key.equalsIgnoreCase("textCol1"))
         {
-          sg.textColour = new UserColourScheme(value).findColour('A');
+          sg.textColour = ColorUtils.parseColourString(value);
         }
         else if (key.equalsIgnoreCase("textCol2"))
         {
-          sg.textColour2 = new UserColourScheme(value).findColour('A');
+          sg.textColour2 = ColorUtils.parseColourString(value);
         }
         else if (key.equalsIgnoreCase("textColThreshold"))
         {
@@ -1680,9 +1684,8 @@ public class AnnotationFile
         }
         else if (key.equalsIgnoreCase("idColour"))
         {
-          // consider warning if colour doesn't resolve to a real colour
-          sg.setIdColour((def = new UserColourScheme(value))
-                  .findColour('A'));
+          Color idColour = ColorUtils.parseColourString(value);
+          sg.setIdColour(idColour == null ? Color.black : idColour);
         }
         else if (key.equalsIgnoreCase("hide"))
         {
@@ -1696,9 +1699,9 @@ public class AnnotationFile
         }
         sg.recalcConservation();
       }
-      if (sg.cs == null)
+      if (sg.getColourScheme() == null)
       {
-        sg.cs = def;
+        sg.setColourScheme(def);
       }
     }
   }
@@ -1792,7 +1795,7 @@ public class AnnotationFile
     return printAnnotations(viewport.isShowAnnotation() ? viewport
             .getAlignment().getAlignmentAnnotation() : null, viewport
             .getAlignment().getGroups(), viewport.getAlignment()
-            .getProperties(), viewport.getColumnSelection(),
+            .getProperties(), viewport.getAlignment().getHiddenColumns(),
             viewport.getAlignment(), null);
   }
 
index c5a80e3..907ff46 100755 (executable)
@@ -407,15 +407,26 @@ public class AppletFormatAdapter
     return null;
   }
 
-  public static DataSourceType checkProtocol(String file)
+  /**
+   * Determines the protocol (i.e DataSourceType.{FILE|PASTE|URL}) for the input
+   * data
+   *
+   * @param data
+   * @return the protocol for the input data
+   */
+  public static DataSourceType checkProtocol(String data)
   {
-    DataSourceType protocol = DataSourceType.FILE;
-    String ft = file.toLowerCase().trim();
+    DataSourceType protocol = DataSourceType.PASTE;
+    String ft = data.toLowerCase().trim();
     if (ft.indexOf("http:") == 0 || ft.indexOf("https:") == 0
             || ft.indexOf("file:") == 0)
     {
       protocol = DataSourceType.URL;
     }
+    else if (new File(data).exists())
+    {
+      protocol = DataSourceType.FILE;
+    }
     return protocol;
   }
 
index 6af0cdf..e1fccaf 100755 (executable)
@@ -35,16 +35,18 @@ import jalview.io.gff.GffHelperBase;
 import jalview.io.gff.GffHelperFactory;
 import jalview.io.gff.GffHelperI;
 import jalview.schemes.FeatureColour;
-import jalview.schemes.UserColourScheme;
+import jalview.util.ColorUtils;
 import jalview.util.MapList;
 import jalview.util.ParseHtmlBodyAndLinks;
 import jalview.util.StringUtils;
 
+import java.awt.Color;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -75,6 +77,19 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
   protected static final String GFF_VERSION = "##gff-version";
 
+  private static final Comparator<String> SORT_NULL_LAST = new Comparator<String>()
+  {
+    @Override
+    public int compare(String o1, String o2)
+    {
+      if (o1 == null)
+      {
+        return o2 == null ? 0 : 1;
+      }
+      return (o2 == null ? -1 : o1.compareTo(o2));
+    }
+  };
+
   private AlignmentI lastmatchedAl = null;
 
   private SequenceIdMatcher matcher = null;
@@ -281,7 +296,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
      */
     for (SequenceI newseq : newseqs)
     {
-      if (newseq.getSequenceFeatures() != null)
+      if (newseq.getFeatures().hasFeatures())
       {
         align.addSequence(newseq);
       }
@@ -355,23 +370,26 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
        * Perhaps an old style groups file with no colours -
        * synthesize a colour from the feature type
        */
-      UserColourScheme ucs = new UserColourScheme(ft);
-      featureColours.put(ft, new FeatureColour(ucs.findColour('A')));
+      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);
@@ -471,219 +489,191 @@ 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 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,
+          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()]);
+
+    /*
+     * sort groups alphabetically, and ensure that features with a
+     * null or empty group are output after those in named groups
+     */
+    List<String> sortedGroups = new ArrayList<String>(visibleFeatureGroups);
+    sortedGroups.remove(null);
+    sortedGroups.remove("");
+    Collections.sort(sortedGroups);
+    sortedGroups.add(null);
+    sortedGroups.add("");
 
-    SequenceFeature[] features;
-    for (int i = 0; i < sequences.length; i++)
+    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;
-          }
-
-          if (features[j].featureGroup != null
-                  && !groups.contains(features[j].featureGroup))
-          {
-            groups.add(features[j].featureGroup);
-          }
+          foundSome = true;
+          out.append(formatJalviewFeature(sequenceName, feature));
         }
       }
     }
 
-    String group = null;
-    do
+    for (String group : sortedGroups)
     {
-      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(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<SequenceFeature>();
+        if (types.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, types));
+        }
 
-            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(group);
         out.append(newline);
-        groupIndex++;
       }
-      else
+    }
+
+    return foundSome ? out.toString() : "No Features Visible";
+  }
+
+  /**
+   * @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();
   }
@@ -741,102 +731,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<SequenceFeature>();
+      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);
       }
     }
 
@@ -1097,10 +1075,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 a11147c..3354b88 100644 (file)
@@ -256,6 +256,21 @@ public enum FileFormat implements FileFormatI
       return new FeaturesFile();
     }
   },
+  ScoreMatrix("Substitution matrix", "", false, false)
+  {
+    @Override
+    public AlignmentFileReaderI getReader(FileParse source)
+            throws IOException
+    {
+      return new ScoreMatrixFile(source);
+    }
+
+    @Override
+    public AlignmentFileWriterI getWriter(AlignmentI al)
+    {
+      return null;
+    }
+  },
   PDB("PDB", "pdb,ent", true, false)
   {
     @Override
index 158489e..19a61cf 100644 (file)
@@ -74,7 +74,9 @@ public class FileFormats
    */
   public void registerFileFormat(FileFormatI format)
   {
-    registerFileFormat(format, false);
+    boolean isIdentifiable = format instanceof FileFormat
+            && ((FileFormat) format).isIdentifiable();
+    registerFileFormat(format, isIdentifiable);
   }
 
   protected void registerFileFormat(FileFormatI format,
index 4f83ab1..9d5fd93 100755 (executable)
@@ -27,7 +27,7 @@ import jalview.api.FeaturesSourceI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
@@ -39,7 +39,10 @@ import jalview.json.binding.biojson.v1.ColourSchemeMapper;
 import jalview.schemes.ColourSchemeI;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.MessageManager;
+import jalview.ws.utils.UrlDownloadClient;
 
+import java.io.File;
+import java.io.IOException;
 import java.util.StringTokenizer;
 import java.util.Vector;
 
@@ -205,6 +208,12 @@ public class FileLoader implements Runnable
       // refer to it as.
       return;
     }
+    if (file != null
+            && file.indexOf(System.getProperty("java.io.tmpdir")) > -1)
+    {
+      // ignore files loaded from the system's temporary directory
+      return;
+    }
     String type = protocol == DataSourceType.FILE ? "RECENT_FILE"
             : "RECENT_URL";
 
@@ -325,9 +334,27 @@ public class FileLoader implements Runnable
 
             // open a new source and read from it
             FormatAdapter fa = new FormatAdapter();
-            al = fa.readFile(file, protocol, format);
-            source = fa.getAlignFile(); // keep reference for later if
-                                        // necessary.
+            boolean downloadStructureFile = format.isStructureFile()
+                    && protocol.equals(DataSourceType.URL);
+            if (downloadStructureFile)
+            {
+              String structExt = format.getExtensions().split(",")[0];
+              String urlLeafName = file.substring(file.lastIndexOf(System
+                      .getProperty("file.separator")), file
+                      .lastIndexOf("."));
+              String tempStructureFileStr = createNamedJvTempFile(
+                      urlLeafName, structExt);
+              UrlDownloadClient.download(file, tempStructureFileStr);
+              al = fa.readFile(tempStructureFileStr, DataSourceType.FILE,
+                      format);
+              source = fa.getAlignFile();
+            }
+            else
+            {
+              al = fa.readFile(file, protocol, format);
+              source = fa.getAlignFile(); // keep reference for later if
+                                          // necessary.
+            }
           }
         } catch (java.io.IOException ex)
         {
@@ -372,8 +399,8 @@ public class FileLoader implements Runnable
 
             if (source instanceof ComplexAlignFile)
             {
-              ColumnSelection colSel = ((ComplexAlignFile) source)
-                      .getColumnSelection();
+              HiddenColumns colSel = ((ComplexAlignFile) source)
+                      .getHiddenColumns();
               SequenceI[] hiddenSeqs = ((ComplexAlignFile) source)
                       .getHiddenSequences();
               String colourSchemeName = ((ComplexAlignFile) source)
@@ -556,6 +583,29 @@ public class FileLoader implements Runnable
 
   }
 
+  /**
+   * This method creates the file -
+   * {tmpdir}/jalview/{current_timestamp}/fileName.exetnsion using the supplied
+   * file name and extension
+   * 
+   * @param fileName
+   *          the name of the temp file to be created
+   * @param extension
+   *          the extension of the temp file to be created
+   * @return
+   */
+  private static String createNamedJvTempFile(String fileName,
+          String extension) throws IOException
+  {
+    String seprator = System.getProperty("file.separator");
+    String jvTempDir = System.getProperty("java.io.tmpdir") + "jalview"
+            + seprator + System.currentTimeMillis();
+    File tempStructFile = new File(jvTempDir + seprator + fileName + "."
+            + extension);
+    tempStructFile.mkdirs();
+    return tempStructFile.toString();
+  }
+
   /*
    * (non-Javadoc)
    * 
index d9dd79d..f09e8a0 100755 (executable)
@@ -26,7 +26,7 @@ import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -165,14 +165,14 @@ public class FormatAdapter extends AppletFormatAdapter
   }
 
   public String formatSequences(FileFormatI format, AlignmentI alignment,
-          String[] omitHidden, int[] exportRange, ColumnSelection colSel)
+          String[] omitHidden, int[] exportRange, HiddenColumns hidden)
   {
     return formatSequences(format, alignment, omitHidden, exportRange,
-            getCacheSuffixDefault(format), colSel, null);
+            getCacheSuffixDefault(format), hidden, null);
   }
 
   /**
-   * hack function to replace seuqences with visible sequence strings before
+   * hack function to replace sequences with visible sequence strings before
    * generating a string of the alignment in the given format.
    * 
    * @param format
@@ -185,15 +185,15 @@ public class FormatAdapter extends AppletFormatAdapter
    */
   public String formatSequences(FileFormatI format, AlignmentI alignment,
           String[] omitHidden, int[] exportRange, boolean suffix,
-          ColumnSelection colSel)
+          HiddenColumns hidden)
   {
     return formatSequences(format, alignment, omitHidden, exportRange,
-            suffix, colSel, null);
+            suffix, hidden, null);
   }
 
   public String formatSequences(FileFormatI format, AlignmentI alignment,
           String[] omitHidden, int[] exportRange, boolean suffix,
-          ColumnSelection colSel, SequenceGroup selgp)
+          HiddenColumns hidden, SequenceGroup selgp)
   {
     if (omitHidden != null)
     {
@@ -211,12 +211,12 @@ public class FormatAdapter extends AppletFormatAdapter
           AlignmentAnnotation na = new AlignmentAnnotation(ala[i]);
           if (selgp != null)
           {
-            colSel.makeVisibleAnnotation(selgp.getStartRes(),
+            hidden.makeVisibleAnnotation(selgp.getStartRes(),
                     selgp.getEndRes(), na);
           }
           else
           {
-            colSel.makeVisibleAnnotation(na);
+            hidden.makeVisibleAnnotation(na);
           }
           alv.addAnnotation(na);
         }
index b0ca25b..77006db 100755 (executable)
@@ -1,7 +1,6 @@
 package jalview.io;
 
 import jalview.api.AlignExportSettingI;
-import jalview.bin.Cache;
 import jalview.datamodel.AlignmentExportData;
 import jalview.exceptions.NoFileSelectedException;
 import jalview.gui.AlignmentPanel;
@@ -92,8 +91,8 @@ public abstract class HTMLOutput implements Runnable
     String bioJSON = new FormatAdapter(ap, exportData.getSettings())
             .formatSequences(FileFormat.Json, exportData.getAlignment(),
                     exportData.getOmitHidden(), exportData
-                            .getStartEndPostions(), ap.getAlignViewport()
-                            .getColumnSelection());
+.getStartEndPostions(), ap.getAlignViewport()
+                            .getAlignment().getHiddenColumns());
     return bioJSON;
   }
 
@@ -243,8 +242,8 @@ public abstract class HTMLOutput implements Runnable
               pSessionId);
     }
 
-    JalviewFileChooser jvFileChooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "html", "HTML files");
+    JalviewFileChooser jvFileChooser = new JalviewFileChooser("html",
+            "HTML files");
     jvFileChooser.setFileView(new JalviewFileView());
 
     jvFileChooser.setDialogTitle(MessageManager
index af3fb5d..9256278 100644 (file)
@@ -24,7 +24,7 @@ package jalview.io;
 import jalview.api.ComplexAlignFile;
 import jalview.api.FeatureSettingsModelI;
 import jalview.api.FeaturesDisplayedI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 
 import java.io.IOException;
@@ -44,7 +44,7 @@ public class HtmlFile extends AlignFile implements ComplexAlignFile
 
   private boolean showSeqFeatures;
 
-  private ColumnSelection columnSelection;
+  private HiddenColumns hiddenColumns;
 
   private SequenceI[] hiddenSequences;
 
@@ -111,7 +111,7 @@ public class HtmlFile extends AlignFile implements ComplexAlignFile
       this.showSeqFeatures = jsonFile.isShowSeqFeatures();
       this.globalColourScheme = jsonFile.getGlobalColourScheme();
       this.hiddenSequences = jsonFile.getHiddenSequences();
-      this.columnSelection = jsonFile.getColumnSelection();
+      this.hiddenColumns = jsonFile.getHiddenColumns();
       this.displayedFeatures = jsonFile.getDisplayedFeatures();
     } catch (Exception e)
     {
@@ -149,14 +149,14 @@ public class HtmlFile extends AlignFile implements ComplexAlignFile
   }
 
   @Override
-  public ColumnSelection getColumnSelection()
+  public HiddenColumns getHiddenColumns()
   {
-    return columnSelection;
+    return hiddenColumns;
   }
 
-  public void setColumnSelection(ColumnSelection columnSelection)
+  public void setHiddenColumns(HiddenColumns hidden)
   {
-    this.columnSelection = columnSelection;
+    this.hiddenColumns = hidden;
   }
 
   @Override
index 0556e76..be0df21 100755 (executable)
@@ -98,12 +98,15 @@ public class IdentifyFile
     boolean lineswereskipped = false;
     boolean isBinary = false; // true if length is non-zero and non-printable
     // characters are encountered
+
     try
     {
       if (!closeSource)
       {
         source.mark();
       }
+      boolean aaIndexHeaderRead = false;
+
       while ((data = source.nextLine()) != null)
       {
         bytesRead += data.length();
@@ -141,6 +144,20 @@ public class IdentifyFile
         }
         data = data.toUpperCase();
 
+        if (data.startsWith(ScoreMatrixFile.SCOREMATRIX))
+        {
+          reply = FileFormat.ScoreMatrix;
+          break;
+        }
+        if (data.startsWith("H ") && !aaIndexHeaderRead)
+        {
+          aaIndexHeaderRead = true;
+        }
+        if (data.startsWith("D ") && aaIndexHeaderRead)
+        {
+          reply = FileFormat.ScoreMatrix;
+          break;
+        }
         if (data.startsWith("##GFF-VERSION"))
         {
           // GFF - possibly embedded in a Jalview features file!
@@ -257,6 +274,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
@@ -274,11 +296,6 @@ public class IdentifyFile
           }
         }
 
-        if (data.indexOf("{\"") > -1)
-        {
-          reply = FileFormat.Json;
-          break;
-        }
         if ((data.length() < 1) || (data.indexOf("#") == 0))
         {
           lineswereskipped = true;
index 7a12076..14574d0 100644 (file)
@@ -32,7 +32,7 @@ import jalview.bin.BuildDetails;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
@@ -46,8 +46,12 @@ import jalview.json.binding.biojson.v1.ColourSchemeMapper;
 import jalview.json.binding.biojson.v1.SequenceFeaturesPojo;
 import jalview.json.binding.biojson.v1.SequenceGrpPojo;
 import jalview.json.binding.biojson.v1.SequencePojo;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.schemes.ColourSchemeProperty;
-import jalview.schemes.UserColourScheme;
+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;
@@ -81,9 +85,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
 
   private FeatureRenderer fr;
 
-  private List<int[]> hiddenColumns;
-
-  private ColumnSelection columnSelection;
+  private HiddenColumns hiddenColumns;
 
   private List<String> hiddenSeqRefs;
 
@@ -215,17 +217,19 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
       {
         // These color schemes require annotation, disable them if annotations
         // are not exported
-        if (globalColourScheme.equalsIgnoreCase("RNA Helices")
-                || globalColourScheme.equalsIgnoreCase("T-COFFEE SCORES"))
+        if (globalColourScheme
+                .equalsIgnoreCase(JalviewColourScheme.RNAHelices.toString())
+                || globalColourScheme
+                        .equalsIgnoreCase(JalviewColourScheme.TCoffee
+                                .toString()))
         {
-          jsonAlignmentPojo.setGlobalColorScheme("None");
+          jsonAlignmentPojo.setGlobalColorScheme(ResidueColourScheme.NONE);
         }
       }
 
       if (exportSettings.isExportFeatures())
       {
-        jsonAlignmentPojo
-                .setSeqFeatures(sequenceFeatureToJsonPojo(sqs, fr));
+        jsonAlignmentPojo.setSeqFeatures(sequenceFeatureToJsonPojo(sqs));
       }
 
       if (exportSettings.isExportGroups() && seqGroups != null
@@ -236,7 +240,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
           SequenceGrpPojo seqGrpPojo = new SequenceGrpPojo();
           seqGrpPojo.setGroupName(seqGrp.getName());
           seqGrpPojo.setColourScheme(ColourSchemeProperty
-                  .getColourName(seqGrp.cs));
+                  .getColourName(seqGrp.getColourScheme()));
           seqGrpPojo.setColourText(seqGrp.getColourText());
           seqGrpPojo.setDescription(seqGrp.getDescription());
           seqGrpPojo.setDisplayBoxes(seqGrp.getDisplayBoxes());
@@ -275,8 +279,9 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
     // hidden column business
     if (getViewport().hasHiddenColumns())
     {
-      List<int[]> hiddenCols = getViewport().getColumnSelection()
-              .getHiddenColumns();
+      List<int[]> hiddenCols = getViewport().getAlignment()
+              .getHiddenColumns()
+              .getHiddenRegions();
       StringBuilder hiddenColsBuilder = new StringBuilder();
       for (int[] range : hiddenCols)
       {
@@ -314,8 +319,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<SequenceFeaturesPojo>();
@@ -324,41 +329,40 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
       return sequenceFeaturesPojo;
     }
 
-    for (SequenceI seq : sqs)
-    {
-      SequenceI dataSetSequence = seq.getDatasetSequence();
-      SequenceFeature[] seqFeatures = (dataSetSequence == null) ? null
-              : seq.getDatasetSequence().getSequenceFeatures();
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
 
-      seqFeatures = (seqFeatures == null) ? seq.getSequenceFeatures()
-              : seqFeatures;
-      if (seqFeatures == null)
-      {
-        continue;
-      }
+    String[] visibleFeatureTypes = displayedFeatures == null ? null
+            : displayedFeatures.getVisibleFeatures().toArray(
+                    new String[displayedFeatures.getVisibleFeatureCount()]);
 
+    for (SequenceI seq : sqs)
+    {
+      /*
+       * 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(fr.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;
@@ -524,8 +528,8 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
         }
         SequenceGroup seqGrp = new SequenceGroup(grpSeqs, grpName, null,
                 displayBoxes, displayText, colourText, startRes, endRes);
-        seqGrp.cs = ColourSchemeMapper.getJalviewColourScheme(colourScheme,
-                seqGrp);
+        seqGrp.setColourScheme(ColourSchemeMapper.getJalviewColourScheme(
+                colourScheme, seqGrp));
         seqGrp.setShowNonconserved(showNonconserved);
         seqGrp.setDescription(description);
         this.seqGroups.add(seqGrp);
@@ -563,7 +567,7 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
             annotations[count] = new Annotation(displayChar, desc, ss, val);
             if (annot.get("colour") != null)
             {
-              Color color = UserColourScheme.getColourFromString(annot.get(
+              Color color = ColorUtils.parseColourString(annot.get(
                       "colour").toString());
               annotations[count].colour = color;
             }
@@ -659,12 +663,12 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
     String hiddenCols = (String) jvSettingsJson.get("hiddenCols");
     if (hiddenCols != null && !hiddenCols.isEmpty())
     {
-      columnSelection = new ColumnSelection();
+      hiddenColumns = new HiddenColumns();
       String[] rangeStrings = hiddenCols.split(";");
       for (String rangeString : rangeStrings)
       {
         String[] range = rangeString.split("-");
-        columnSelection.hideColumns(Integer.valueOf(range[0]),
+        hiddenColumns.hideColumns(Integer.valueOf(range[0]),
                 Integer.valueOf(range[1]));
       }
     }
@@ -684,12 +688,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)
         {
@@ -700,12 +715,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);
       }
@@ -783,20 +793,15 @@ public class JSONFile extends AlignFile implements ComplexAlignFile
     return annotations;
   }
 
-  public List<int[]> getHiddenColumns()
-  {
-    return hiddenColumns;
-  }
-
   @Override
-  public ColumnSelection getColumnSelection()
+  public HiddenColumns getHiddenColumns()
   {
-    return columnSelection;
+    return hiddenColumns;
   }
 
-  public void setColumnSelection(ColumnSelection columnSelection)
+  public void setHiddenColumns(HiddenColumns hidden)
   {
-    this.columnSelection = columnSelection;
+    this.hiddenColumns = hidden;
   }
 
   @Override
index 2a0f8b1..98cd162 100755 (executable)
@@ -21,6 +21,7 @@
 //////////////////////////////////////////////////////////////////
 package jalview.io;
 
+import jalview.bin.Cache;
 import jalview.gui.JvOptionPane;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
@@ -124,16 +125,13 @@ public class JalviewFileChooser extends JFileChooser
   /**
    * Constructor for a single choice of file extension and description
    * 
-   * @param dir
    * @param extension
    * @param desc
    */
-  public JalviewFileChooser(String dir, String extension, String desc)
+  public JalviewFileChooser(String extension, String desc)
   {
-    // TODO inline dir as Cache.getProperty("LAST_DIRECTORY") ? if applet
-    // builds ok
-    this(dir, new String[] { extension }, new String[] { desc }, desc,
-            true);
+    this(Cache.getProperty("LAST_DIRECTORY"), new String[] { extension },
+            new String[] { desc }, desc, true);
   }
 
   JalviewFileChooser(String dir, String[] extensions, String[] descs,
diff --git a/src/jalview/io/ScoreMatrixFile.java b/src/jalview/io/ScoreMatrixFile.java
new file mode 100644 (file)
index 0000000..6b2f891
--- /dev/null
@@ -0,0 +1,433 @@
+package jalview.io;
+
+import jalview.analysis.scoremodels.ScoreMatrix;
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.datamodel.SequenceI;
+
+import java.io.IOException;
+import java.util.StringTokenizer;
+
+/**
+ * A class that can parse a file containing a substitution matrix and register
+ * it for use in Jalview
+ * <p>
+ * Accepts 'NCBI' format (e.g.
+ * https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt), with the
+ * addition of a header line to provide a matrix name, e.g.
+ * 
+ * <pre>
+ * ScoreMatrix BLOSUM62
+ * </pre>
+ * 
+ * Also accepts 'AAindex' format (as described at
+ * http://www.genome.jp/aaindex/aaindex_help.html) with the minimum data
+ * required being
+ * 
+ * <pre>
+ * H accession number (used as score matrix identifier in Jalview)
+ * D description (used for tooltip in Jalview)
+ * M rows = symbolList
+ * and the substitution scores
+ * </pre>
+ */
+public class ScoreMatrixFile extends AlignFile implements
+        AlignmentFileReaderI
+{
+  // first non-comment line identifier - also checked in IdentifyFile
+  public static final String SCOREMATRIX = "SCOREMATRIX";
+
+  private static final String DELIMITERS = " ,\t";
+
+  private static final String COMMENT_CHAR = "#";
+
+  private String matrixName;
+
+  /*
+   * aaindex format has scores for diagonal and below only
+   */
+  boolean isLowerDiagonalOnly;
+
+  /*
+   * ncbi format has symbols as first column on score rows
+   */
+  boolean hasGuideColumn;
+
+  /**
+   * Constructor
+   * 
+   * @param source
+   * @throws IOException
+   */
+  public ScoreMatrixFile(FileParse source) throws IOException
+  {
+    super(false, source);
+  }
+
+  @Override
+  public String print(SequenceI[] sqs, boolean jvsuffix)
+  {
+    return null;
+  }
+
+  /**
+   * Parses the score matrix file, and if successful registers the matrix so it
+   * will be shown in Jalview menus. This method is not thread-safe (a separate
+   * instance of this class should be used by each thread).
+   */
+  @Override
+  public void parse() throws IOException
+  {
+    ScoreMatrix sm = parseMatrix();
+
+    ScoreModels.getInstance().registerScoreModel(sm);
+  }
+
+  /**
+   * Parses the score matrix file and constructs a ScoreMatrix object. If an
+   * error is found in parsing, it is thrown as FileFormatException. Any
+   * warnings are written to syserr.
+   * 
+   * @return
+   * @throws IOException
+   */
+  public ScoreMatrix parseMatrix() throws IOException
+  {
+    ScoreMatrix sm = null;
+    int lineNo = 0;
+    String name = null;
+    char[] alphabet = null;
+    float[][] scores = null;
+    int size = 0;
+    int row = 0;
+    String err = null;
+    String data;
+    isLowerDiagonalOnly = false;
+
+    while ((data = nextLine()) != null)
+    {
+      lineNo++;
+      data = data.trim();
+      if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
+      {
+        continue;
+      }
+      if (data.toUpperCase().startsWith(SCOREMATRIX))
+      {
+        /*
+         * Parse name from ScoreMatrix <name>
+         * we allow any delimiter after ScoreMatrix then take the rest of the line
+         */
+        if (name != null)
+        {
+          throw new FileFormatException(
+                  "Error: 'ScoreMatrix' repeated in file at line "
+                          + lineNo);
+        }
+        StringTokenizer nameLine = new StringTokenizer(data, DELIMITERS);
+        if (nameLine.countTokens() < 2)
+        {
+          err = "Format error: expected 'ScoreMatrix <name>', found '"
+                  + data + "' at line " + lineNo;
+          throw new FileFormatException(err);
+        }
+        nameLine.nextToken(); // 'ScoreMatrix'
+        name = nameLine.nextToken(); // next field
+        name = data.substring(1).substring(data.substring(1).indexOf(name));
+        continue;
+      }
+      else if (data.startsWith("H ") && name == null)
+      {
+        /*
+         * AAindex identifier 
+         */
+        return parseAAIndexFormat(lineNo, data);
+      }
+      else if (name == null)
+      {
+        err = "Format error: 'ScoreMatrix <name>' should be the first non-comment line";
+        throw new FileFormatException(err);
+      }
+
+      /*
+       * next non-comment line after ScoreMatrix should be the 
+       * column header line with the alphabet of scored symbols
+       */
+      if (alphabet == null)
+      {
+        StringTokenizer columnHeadings = new StringTokenizer(data,
+                DELIMITERS);
+        size = columnHeadings.countTokens();
+        alphabet = new char[size];
+        int col = 0;
+        while (columnHeadings.hasMoreTokens())
+        {
+          alphabet[col++] = columnHeadings.nextToken().charAt(0);
+        }
+        scores = new float[size][];
+        continue;
+      }
+
+      /*
+       * too much information
+       */
+      if (row >= size)
+      {
+        err = "Unexpected extra input line in score model file: '" + data
+                + "'";
+        throw new FileFormatException(err);
+      }
+
+      parseValues(data, lineNo, scores, row, alphabet);
+      row++;
+    }
+
+    /*
+     * out of data - check we found enough
+     */
+    if (row < size)
+    {
+      err = String
+              .format("Expected %d rows of score data in score matrix but only found %d",
+                      size, row);
+      throw new FileFormatException(err);
+    }
+
+    /*
+     * If we get here, then name, alphabet and scores have been parsed successfully
+     */
+    sm = new ScoreMatrix(name, alphabet, scores);
+    matrixName = name;
+
+    return sm;
+  }
+
+  /**
+   * Parse input as AAIndex format, starting from the header line with the
+   * accession id
+   * 
+   * @param lineNo
+   * @param data
+   * @return
+   * @throws IOException
+   */
+  protected ScoreMatrix parseAAIndexFormat(int lineNo, String data)
+          throws IOException
+  {
+    String name = data.substring(2).trim();
+    String description = null;
+
+    float[][] scores = null;
+    char[] alphabet = null;
+    int row = 0;
+    int size = 0;
+
+    while ((data = nextLine()) != null)
+    {
+      lineNo++;
+      data = data.trim();
+      if (skipAAindexLine(data))
+      {
+        continue;
+      }
+      if (data.startsWith("D "))
+      {
+        description = data.substring(2).trim();
+      }
+      else if (data.startsWith("M "))
+      {
+        alphabet = parseAAindexRowsColumns(lineNo, data);
+        size = alphabet.length;
+        scores = new float[size][size];
+      }
+      else if (scores == null)
+      {
+        throw new FileFormatException(
+                "No alphabet specified in matrix file");
+      }
+      else if (row >= size)
+      {
+        throw new FileFormatException("Too many data rows in matrix file");
+      }
+      else
+      {
+        parseValues(data, lineNo, scores, row, alphabet);
+        row++;
+      }
+    }
+
+    ScoreMatrix sm = new ScoreMatrix(name, description, alphabet, scores);
+    matrixName = name;
+
+    return sm;
+  }
+
+  /**
+   * Parse one row of score values, delimited by whitespace or commas. The line
+   * may optionally include the symbol from which the scores are defined. Values
+   * may be present for all columns, or only up to the diagonal (in which case
+   * upper diagonal values are set symmetrically).
+   * 
+   * @param data
+   *          the line to be parsed
+   * @param lineNo
+   * @param scores
+   *          the score matrix to add data to
+   * @param row
+   *          the row number / alphabet index position
+   * @param alphabet
+   * @return
+   * @throws exception
+   *           if invalid, or too few, or too many values
+   */
+  protected void parseValues(String data, int lineNo, float[][] scores,
+          int row, char[] alphabet) throws FileFormatException
+  {
+    String err;
+    int size = alphabet.length;
+    StringTokenizer scoreLine = new StringTokenizer(data, DELIMITERS);
+
+    int tokenCount = scoreLine.countTokens();
+
+    /*
+     * inspect first row to see if it includes the symbol in the first column,
+     * and to see if it is lower diagonal values only (i.e. just one score)
+     */
+    if (row == 0)
+    {
+      if (data.startsWith(String.valueOf(alphabet[0])))
+      {
+        hasGuideColumn = true;
+      }
+      if (tokenCount == (hasGuideColumn ? 2 : 1))
+      {
+        isLowerDiagonalOnly = true;
+      }
+    }
+
+    if (hasGuideColumn)
+    {
+      /*
+       * check 'guide' symbol is the row'th letter of the alphabet
+       */
+      String symbol = scoreLine.nextToken();
+      if (symbol.length() > 1 || symbol.charAt(0) != alphabet[row])
+      {
+        err = String
+                .format("Error parsing score matrix at line %d, expected '%s' but found '%s'",
+                        lineNo, alphabet[row], symbol);
+        throw new FileFormatException(err);
+      }
+      tokenCount = scoreLine.countTokens(); // excluding guide symbol
+    }
+
+    /*
+     * check the right number of values (lower diagonal or full format)
+     */
+    if (isLowerDiagonalOnly && tokenCount != row + 1)
+    {
+      err = String.format(
+              "Expected %d scores at line %d: '%s' but found %d", row + 1,
+              lineNo, data, tokenCount);
+        throw new FileFormatException(err);
+    }
+
+    if (!isLowerDiagonalOnly && tokenCount != size)
+    {
+      err = String.format(
+              "Expected %d scores at line %d: '%s' but found %d", size,
+              lineNo, data, scoreLine.countTokens());
+      throw new FileFormatException(err);
+    }
+
+    /*
+     * parse and set the values, setting the symmetrical value
+     * as well if lower diagonal format data
+     */
+    scores[row] = new float[size];
+    int col = 0;
+    String value = null;
+    while (scoreLine.hasMoreTokens())
+    {
+      try
+      {
+        value = scoreLine.nextToken();
+        scores[row][col] = Float.valueOf(value);
+        if (isLowerDiagonalOnly)
+        {
+          scores[col][row] = scores[row][col];
+        }
+        col++;
+      } catch (NumberFormatException e)
+      {
+        err = String.format(
+                "Invalid score value '%s' at line %d column %d", value,
+                lineNo, col);
+        throw new FileFormatException(err);
+      }
+    }
+  }
+
+  /**
+   * Parse the line in an aaindex file that looks like
+   * 
+   * <pre>
+   * M rows = ARNDCQEGHILKMFPSTWYV, cols = ARNDCQEGHILKMFPSTWYV
+   * </pre>
+   * 
+   * rejecting it if rows and cols do not match. Returns the string of
+   * characters in the row/cols alphabet.
+   * 
+   * @param lineNo
+   * @param data
+   * @return
+   * @throws FileFormatException
+   */
+  protected char[] parseAAindexRowsColumns(int lineNo, String data)
+          throws FileFormatException
+  {
+    String err = "Unexpected aaIndex score matrix data at line " + lineNo
+            + ": " + data;
+    
+    try
+    {
+      String[] toks = data.split(",");
+      String rowsAlphabet = toks[0].split("=")[1].trim();
+      String colsAlphabet = toks[1].split("=")[1].trim();
+      if (!rowsAlphabet.equals(colsAlphabet))
+      {
+        throw new FileFormatException("rows != cols");
+      }
+      return rowsAlphabet.toCharArray();
+    } catch (Throwable t)
+    {
+      throw new FileFormatException(err + " " + t.getMessage());
+    }
+  }
+
+  /**
+   * Answers true if line is one we are not interested in from AAindex format
+   * file
+   * 
+   * @param data
+   * @return
+   */
+  protected boolean skipAAindexLine(String data)
+  {
+    if (data.startsWith(COMMENT_CHAR) || data.length() == 0)
+    {
+      return true;
+    }
+    if (data.startsWith("*") || data.startsWith("R ")
+            || data.startsWith("A ") || data.startsWith("T ")
+            || data.startsWith("J ") || data.startsWith("//"))
+    {
+      return true;
+    }
+    return false;
+  }
+
+  public String getMatrixName()
+  {
+    return matrixName;
+  }
+}
index 6c8f40f..c3b076c 100644 (file)
@@ -57,7 +57,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
    */
   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
   {
@@ -356,100 +357,121 @@ 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, minmax, 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,
index 2061f29..936d2b9 100644 (file)
@@ -33,6 +33,7 @@ import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.schemes.ResidueProperties;
+import jalview.util.Comparison;
 import jalview.util.Format;
 import jalview.util.MessageManager;
 
@@ -45,7 +46,6 @@ import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.StringTokenizer;
 import java.util.Vector;
 
 import com.stevesoft.pat.Regex;
@@ -74,12 +74,14 @@ 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("(>|\\])", ")");
 
-  private static final Regex DETECT_BRACKETS = new Regex(
-          "(<|>|\\[|\\]|\\(|\\))");
+  public static final Regex DETECT_BRACKETS = new Regex(
+          "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 
   StringBuffer out; // output buffer
 
@@ -366,6 +368,11 @@ public class StockholmFile extends AlignFile
 
               // add alignment annotation for this feature
               String key = type2id(type);
+
+              /*
+               * have we added annotation rows for this type ?
+               */
+              boolean annotsAdded = false;
               if (key != null)
               {
                 if (accAnnotations != null
@@ -374,6 +381,7 @@ public class StockholmFile extends AlignFile
                   Vector vv = (Vector) accAnnotations.get(key);
                   for (int ii = 0; ii < vv.size(); ii++)
                   {
+                    annotsAdded = true;
                     AlignmentAnnotation an = (AlignmentAnnotation) vv
                             .elementAt(ii);
                     seqO.addAlignmentAnnotation(an);
@@ -386,6 +394,11 @@ public class StockholmFile extends AlignFile
               while (j.hasMoreElements())
               {
                 String desc = j.nextElement().toString();
+                if (ANNOTATION.equals(desc) && annotsAdded)
+                {
+                  // don't add features if we already added an annotation row
+                  continue;
+                }
                 String ns = content.get(desc).toString();
                 char[] byChar = ns.toCharArray();
                 for (int k = 0; k < byChar.length; k++)
@@ -401,7 +414,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);
                   }
@@ -572,22 +585,11 @@ public class StockholmFile extends AlignFile
           {
             String acc = s.stringMatched(1);
             String type = s.stringMatched(2);
-            String seq = new String(s.stringMatched(3));
-            String description = null;
-            // Check for additional information about the current annotation
-            // We use a simple string tokenizer here for speed
-            StringTokenizer sep = new StringTokenizer(seq, " \t");
-            description = sep.nextToken();
-            if (sep.hasMoreTokens())
-            {
-              seq = sep.nextToken();
-            }
-            else
-            {
-              seq = description;
-              description = new String();
-            }
-            // sequence id with from-to fields
+            String oseq = s.stringMatched(3);
+            /*
+             * copy of annotation field that may be processed into whitespace chunks
+             */
+            String seq = new String(oseq);
 
             Hashtable ann;
             // Get an object with all the annotations for this sequence
@@ -602,8 +604,12 @@ public class StockholmFile extends AlignFile
               ann = new Hashtable();
               seqAnn.put(acc, ann);
             }
+
+            // // start of block for appending annotation lines for wrapped
+            // stokchholm file
             // TODO test structure, call parseAnnotationRow with vector from
             // hashtable for specific sequence
+
             Hashtable features;
             // Get an object with all the content for an annotation
             if (ann.containsKey("features"))
@@ -631,15 +637,18 @@ public class StockholmFile extends AlignFile
               content = new Hashtable();
               features.put(this.id2type(type), content);
             }
-            String ns = (String) content.get(description);
+            String ns = (String) content.get(ANNOTATION);
+
             if (ns == null)
             {
               ns = "";
             }
+            // finally, append the annotation line
             ns += seq;
-            content.put(description, ns);
+            content.put(ANNOTATION, ns);
+            // // end of wrapped annotation block.
+            // // Now a new row is created with the current set of data
 
-            // if(type.equals("SS")){
             Hashtable strucAnn;
             if (seqAnn.containsKey(acc))
             {
@@ -656,7 +665,8 @@ public class StockholmFile extends AlignFile
             {
               alan.visible = false;
             }
-            // annotations.addAll(newStruc);
+            // new annotation overwrites any existing annotation...
+
             strucAnn.put(type, newStruc);
             seqAnn.put(acc, strucAnn);
           }
@@ -800,9 +810,9 @@ public class StockholmFile extends AlignFile
   {
     String convert1, convert2 = null;
 
-    convert1 = OPEN_PAREN.replaceAll(annots);
-    convert2 = CLOSE_PAREN.replaceAll(convert1);
-    annots = convert2;
+    // convert1 = OPEN_PAREN.replaceAll(annots);
+    // convert2 = CLOSE_PAREN.replaceAll(convert1);
+    // annots = convert2;
 
     String type = label;
     if (label.contains("_cons"))
@@ -810,12 +820,16 @@ public class StockholmFile extends AlignFile
       type = (label.indexOf("_cons") == label.length() - 5) ? label
               .substring(0, label.length() - 5) : label;
     }
-    boolean ss = false;
+    boolean ss = false, posterior = false;
     type = id2type(type);
-    if (type.equals("secondary structure"))
+    if (type.equalsIgnoreCase("secondary structure"))
     {
       ss = true;
     }
+    if (type.equalsIgnoreCase("posterior probability"))
+    {
+      posterior = true;
+    }
     // decide on secondary structure or not.
     Annotation[] els = new Annotation[annots.length()];
     for (int i = 0; i < annots.length(); i++)
@@ -831,12 +845,12 @@ public class StockholmFile extends AlignFile
           if (DETECT_BRACKETS.search(pos))
           {
             ann.secondaryStructure = Rna.getRNASecStrucState(pos).charAt(0);
+            ann.displayCharacter = "" + pos.charAt(0);
           }
           else
           {
             ann.secondaryStructure = ResidueProperties.getDssp3state(pos)
                     .charAt(0);
-          }
 
           if (ann.secondaryStructure == pos.charAt(0))
           {
@@ -846,9 +860,29 @@ public class StockholmFile extends AlignFile
           {
             ann.displayCharacter = " " + ann.displayCharacter;
           }
+          }
         }
 
       }
+      if (posterior && !ann.isWhitespace()
+              && !Comparison.isGap(pos.charAt(0)))
+      {
+        float val = 0;
+        // symbol encodes values - 0..*==0..10
+        if (pos.charAt(0) == '*')
+        {
+          val = 10;
+        }
+        else
+        {
+          val = pos.charAt(0) - '0';
+          if (val > 9)
+          {
+            val = 10;
+          }
+        }
+        ann.value = val;
+      }
 
       els[i] = ann;
     }
@@ -966,43 +1000,39 @@ public class StockholmFile extends AlignFile
     // output annotations
     while (i < s.length && s[i] != null)
     {
-      if (s[i].getDatasetSequence() != null)
+      AlignmentAnnotation[] alAnot = s[i].getAnnotation();
+      if (alAnot != null)
       {
-        SequenceI ds = s[i].getDatasetSequence();
-        AlignmentAnnotation[] alAnot;
         Annotation[] ann;
-        Annotation annot;
-        alAnot = s[i].getAnnotation();
-        String feature = "";
-        if (alAnot != null)
+        for (int j = 0; j < alAnot.length; j++)
         {
-          for (int j = 0; j < alAnot.length; j++)
+
+          String key = type2id(alAnot[j].label);
+          boolean isrna = alAnot[j].isValidStruc();
+
+          if (isrna)
+          {
+            // hardwire to secondary structure if there is RNA secondary
+            // structure on the annotation
+            key = "SS";
+          }
+          if (key == null)
           {
-            if (ds.getSequenceFeatures() != null)
-            {
-              feature = ds.getSequenceFeatures()[0].type;
-            }
-            // ?bug - feature may still have previous loop value
-            String key = type2id(feature);
 
-            if (key == null)
-            {
-              continue;
-            }
+            continue;
+          }
 
-            // out.append("#=GR ");
-            out.append(new Format("%-" + maxid + "s").form("#=GR "
-                    + printId(s[i], jvSuffix) + " " + key + " "));
-            ann = alAnot[j].annotations;
-            boolean isrna = alAnot[j].isValidStruc();
-            String seq = "";
-            for (int k = 0; k < ann.length; k++)
-            {
-              seq += outputCharacter(key, k, isrna, ann, s[i]);
-            }
-            out.append(seq);
-            out.append(newline);
+          // out.append("#=GR ");
+          out.append(new Format("%-" + maxid + "s").form("#=GR "
+                  + printId(s[i], jvSuffix) + " " + key + " "));
+          ann = alAnot[j].annotations;
+          String seq = "";
+          for (int k = 0; k < ann.length; k++)
+          {
+            seq += outputCharacter(key, k, isrna, ann, s[i]);
           }
+          out.append(seq);
+          out.append(newline);
         }
       }
 
@@ -1088,8 +1118,8 @@ public class StockholmFile extends AlignFile
     {
       if (annot == null)
       {
-        // sensible gap character if one is available or make one up
-        return sequenceI == null ? '-' : sequenceI.getCharAt(k);
+        // sensible gap character
+        return ' ';
       }
       else
       {
@@ -1135,10 +1165,10 @@ public class StockholmFile extends AlignFile
     if (typeIds == null)
     {
       typeIds = new Hashtable();
-      typeIds.put("SS", "secondary structure");
-      typeIds.put("SA", "surface accessibility");
+      typeIds.put("SS", "Secondary Structure");
+      typeIds.put("SA", "Surface Accessibility");
       typeIds.put("TM", "transmembrane");
-      typeIds.put("PP", "posterior probability");
+      typeIds.put("PP", "Posterior Probability");
       typeIds.put("LI", "ligand binding");
       typeIds.put("AS", "active site");
       typeIds.put("IN", "intron");
@@ -1149,7 +1179,7 @@ public class StockholmFile extends AlignFile
       typeIds.put("DE", "description");
       typeIds.put("DR", "reference");
       typeIds.put("LO", "look");
-      typeIds.put("RF", "reference positions");
+      typeIds.put("RF", "Reference Positions");
 
     }
   }
@@ -1172,7 +1202,7 @@ public class StockholmFile extends AlignFile
     while (e.hasMoreElements())
     {
       Object ll = e.nextElement();
-      if (typeIds.get(ll).toString().equals(type))
+      if (typeIds.get(ll).toString().equalsIgnoreCase(type))
       {
         key = (String) ll;
         break;
index 7fe17c8..ab220f0 100644 (file)
@@ -406,7 +406,8 @@ public abstract class StructureFile extends AlignFile
    * make a friendly ID string.
    * 
    * @param dataName
-   * @return truncated dataName to after last '/'
+   * @return truncated dataName to after last '/' and pruned .extension if
+   *         present
    */
   protected String safeName(String dataName)
   {
@@ -415,6 +416,9 @@ public abstract class StructureFile extends AlignFile
     {
       dataName = dataName.substring(p + 1);
     }
+    if(dataName.indexOf(".") > -1){
+      dataName = dataName.substring(0, dataName.lastIndexOf("."));
+    }
     return dataName;
   }
 
index c3ec951..648954f 100644 (file)
@@ -37,10 +37,10 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
- * A file parse for T-Coffee score ascii format. This file contains the
- * alignment consensus for each resude in any sequence.
+ * A file parser for T-Coffee score ascii format. This file contains the
+ * alignment consensus for each residue in any sequence.
  * <p>
- * This file is procuded by <code>t_coffee</code> providing the option
+ * This file is produced by <code>t_coffee</code> providing the option
  * <code>-output=score_ascii </code> to the program command line
  * 
  * An example file is the following
@@ -91,17 +91,26 @@ import java.util.regex.Pattern;
  */
 public class TCoffeeScoreFile extends AlignFile
 {
-  public TCoffeeScoreFile(String inFile, DataSourceType fileSourceType)
-          throws IOException
-  {
-    super(inFile, fileSourceType);
 
-  }
+  /**
+   * TCOFFEE score colourscheme
+   */
+  static final Color[] colors = { new Color(102, 102, 255), // 0: lilac #6666FF
+      new Color(0, 255, 0), // 1: green #00FF00
+      new Color(102, 255, 0), // 2: lime green #66FF00
+      new Color(204, 255, 0), // 3: greeny yellow #CCFF00
+      new Color(255, 255, 0), // 4: yellow #FFFF00
+      new Color(255, 204, 0), // 5: orange #FFCC00
+      new Color(255, 153, 0), // 6: deep orange #FF9900
+      new Color(255, 102, 0), // 7: ochre #FF6600
+      new Color(255, 51, 0), // 8: red #FF3300
+      new Color(255, 34, 0) // 9: redder #FF2000
+  };
 
-  public TCoffeeScoreFile(FileParse source) throws IOException
-  {
-    super(source);
-  }
+  public final static String TCOFFEE_SCORE = "TCoffeeScore";
+
+  static Pattern SCORES_WITH_RESIDUE_NUMS = Pattern
+          .compile("^\\d+\\s([^\\s]+)\\s+\\d+$");
 
   /** The {@link Header} structure holder */
   Header header;
@@ -114,6 +123,18 @@ public class TCoffeeScoreFile extends AlignFile
 
   Integer fWidth;
 
+  public TCoffeeScoreFile(String inFile, DataSourceType fileSourceType)
+          throws IOException
+  {
+    super(inFile, fileSourceType);
+
+  }
+
+  public TCoffeeScoreFile(FileParse source) throws IOException
+  {
+    super(source);
+  }
+
   /**
    * Parse the provided reader for the T-Coffee scores file format
    * 
@@ -399,9 +420,6 @@ public class TCoffeeScoreFile extends AlignFile
     }
   }
 
-  static Pattern SCORES_WITH_RESIDUE_NUMS = Pattern
-          .compile("^\\d+\\s([^\\s]+)\\s+\\d+$");
-
   /**
    * Read a scores block ihe provided stream.
    * 
@@ -525,23 +543,6 @@ public class TCoffeeScoreFile extends AlignFile
   }
 
   /**
-   * TCOFFEE score colourscheme
-   */
-  static final Color[] colors = { new Color(102, 102, 255), // #6666FF
-      new Color(0, 255, 0), // #00FF00
-      new Color(102, 255, 0), // #66FF00
-      new Color(204, 255, 0), // #CCFF00
-      new Color(255, 255, 0), // #FFFF00
-      new Color(255, 204, 0), // #FFCC00
-      new Color(255, 153, 0), // #FF9900
-      new Color(255, 102, 0), // #FF6600
-      new Color(255, 51, 0), // #FF3300
-      new Color(255, 34, 0) // #FF2000
-  };
-
-  public final static String TCOFFEE_SCORE = "TCoffeeScore";
-
-  /**
    * generate annotation for this TCoffee score set on the given alignment
    * 
    * @param al
index 2c35547..1cab8ca 100644 (file)
@@ -1942,7 +1942,7 @@ public class VamsasAppDatastore
                 TreePanel tp = null;
                 if (vstree.isValidTree())
                 {
-                  tp = alignFrame.ShowNewickTree(vstree.getNewickTree(),
+                  tp = alignFrame.showNewickTree(vstree.getNewickTree(),
                           vstree.getTitle(), vstree.getInputData(), 600,
                           500, t * 20 + 50, t * 20 + 50);
 
diff --git a/src/jalview/io/cache/AppCache.java b/src/jalview/io/cache/AppCache.java
new file mode 100644 (file)
index 0000000..091d30e
--- /dev/null
@@ -0,0 +1,153 @@
+package jalview.io.cache;
+
+
+import jalview.bin.Cache;
+
+import java.util.Hashtable;
+import java.util.LinkedHashSet;
+
+/**
+ * A singleton class used for querying and persisting cache items.
+ * 
+ * @author tcnofoegbu
+ *
+ */
+public class AppCache
+{
+  public static final String DEFAULT_LIMIT = "99";
+
+  public static final String CACHE_DELIMITER = ";";
+
+  private static AppCache instance = null;
+
+  private static final String DEFAULT_LIMIT_KEY = ".DEFAULT_LIMIT";
+
+
+
+  private Hashtable<String, LinkedHashSet<String>> cacheItems;
+
+  private AppCache()
+  {
+    cacheItems = new Hashtable<String, LinkedHashSet<String>>();
+  }
+
+  /**
+   * Method to obtain all the cache items for a given cache key
+   * 
+   * @param cacheKey
+   * @return
+   */
+  public LinkedHashSet<String> getAllCachedItemsFor(String cacheKey)
+  {
+    LinkedHashSet<String> foundCache = cacheItems.get(cacheKey);
+    if (foundCache == null)
+    {
+      foundCache = new LinkedHashSet<String>();
+      cacheItems.put(cacheKey, foundCache);
+    }
+    return foundCache;
+  }
+
+
+  /**
+   * Returns a singleton instance of AppCache
+   * 
+   * @return
+   */
+  public static AppCache getInstance()
+  {
+    if (instance == null)
+    {
+      instance = new AppCache();
+    }
+    return instance;
+  }
+
+
+
+  /**
+   * Method for persisting cache items for a given cache key
+   * 
+   * @param cacheKey
+   */
+  public void persistCache(String cacheKey)
+  {
+    LinkedHashSet<String> foundCacheItems = getAllCachedItemsFor(cacheKey);
+    StringBuffer delimitedCacheBuf = new StringBuffer();
+    for (String cacheItem : foundCacheItems)
+    {
+      delimitedCacheBuf.append(CACHE_DELIMITER).append(cacheItem);
+    }
+    if (delimitedCacheBuf.length() > 0)
+    {
+      delimitedCacheBuf.deleteCharAt(0);
+    }
+    String delimitedCacheString = delimitedCacheBuf.toString();
+
+    Cache.setProperty(cacheKey, delimitedCacheString);
+  }
+
+  /**
+   * Method for deleting cached items for a given cache key
+   * 
+   * @param cacheKey
+   *          the cache key
+   */
+  public void deleteCacheItems(String cacheKey)
+  {
+    cacheItems.put(cacheKey, new LinkedHashSet<String>());
+    persistCache(cacheKey);
+  }
+
+  /**
+   * Method for obtaining the preset maximum cache limit for a given cache key
+   * 
+   * @param cacheKey
+   *          the cache key
+   * @return the max number of items that could be cached
+   */
+  public String getCacheLimit(String cacheKey)
+  {
+    String uniqueKey = cacheKey + DEFAULT_LIMIT_KEY;
+    return Cache.getDefault(uniqueKey, DEFAULT_LIMIT);
+  }
+
+  /**
+   * Method for updating the preset maximum cache limit for a given cache key
+   * 
+   * @param cacheKey
+   *          the cache key
+   * @param newLimit
+   *          the max number of items that could be cached for the given cache
+   *          key
+   * @return
+   */
+  public int updateCacheLimit(String cacheKey, int newUserLimit)
+  {
+    String newLimit = String.valueOf(newUserLimit);
+    String uniqueKey = cacheKey + DEFAULT_LIMIT_KEY;
+    String formerLimit = getCacheLimit(cacheKey);
+    if (newLimit != null && !newLimit.isEmpty()
+            && !formerLimit.equals(newLimit))
+    {
+      Cache.setProperty(uniqueKey, newLimit);
+      formerLimit = newLimit;
+    }
+    return Integer.valueOf(formerLimit);
+  }
+
+  /**
+   * Method for inserting cache items for given cache key into the cache data
+   * structure
+   * 
+   * @param cacheKey
+   *          the cache key
+   * @param cacheItems
+   *          the items to add to the cache
+   */
+  public void putCache(String cacheKey, LinkedHashSet<String> newCacheItems)
+  {
+    cacheItems.put(cacheKey, newCacheItems);
+  }
+
+}
diff --git a/src/jalview/io/cache/JvCacheableInputBox.java b/src/jalview/io/cache/JvCacheableInputBox.java
new file mode 100644 (file)
index 0000000..444670b
--- /dev/null
@@ -0,0 +1,287 @@
+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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+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();
+
+  public JvCacheableInputBox(String newCacheKey)
+  {
+    super();
+    this.cacheKey = newCacheKey;
+    setEditable(true);
+    setPrototypeDisplayValue("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
+    appCache = AppCache.getInstance();
+    initCachePopupMenu();
+    initCache(newCacheKey);
+    updateCache();
+  }
+
+  /**
+   * Method for initialising cache items for a given cache key and populating
+   * the in-memory cache with persisted cache items
+   * 
+   * @param cacheKey
+   */
+  private void initCache(String cacheKey)
+  {
+    // obtain persisted cache items from properties file as a delimited string
+    String delimitedCacheStr = Cache.getProperty(cacheKey);
+    if (delimitedCacheStr == null || delimitedCacheStr.isEmpty())
+    {
+      return;
+    }
+    // convert delimited cache items to a list of strings
+    List<String> persistedCacheItems = Arrays.asList(delimitedCacheStr
+            .split(AppCache.CACHE_DELIMITER));
+
+    LinkedHashSet<String> foundCacheItems = appCache
+            .getAllCachedItemsFor(cacheKey);
+    if (foundCacheItems == null)
+    {
+      foundCacheItems = new LinkedHashSet<String>();
+    }
+    // populate memory cache
+    for (String cacheItem : persistedCacheItems)
+    {
+      foundCacheItems.add(cacheItem);
+    }
+    appCache.putCache(cacheKey, foundCacheItems);
+  }
+
+  /**
+   * Initialise this cache's pop-up menu
+   */
+  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()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        // System.out.println(">>>>> Clear cache items");
+        setSelectedItem("");
+        appCache.deleteCacheItems(cacheKey);
+        updateCache();
+      }
+    });
+
+    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
+   * 
+   * @param text
+   * @return
+   */
+  static boolean isInteger(String text)
+  {
+    try
+    {
+      Integer.parseInt(text);
+      return true;
+    } catch (NumberFormatException e)
+    {
+      return false;
+    }
+  }
+
+  /**
+   * Method called to update the cache with the last user input
+   */
+  public void updateCache()
+  {
+    SwingUtilities.invokeLater(new Runnable()
+    {
+      @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);
+        String userInput = getUserInput();
+        if (userInput != null && !userInput.isEmpty())
+        {
+          LinkedHashSet<String> foundCache = appCache
+                  .getAllCachedItemsFor(cacheKey);
+          // remove old cache item so as to place current input at the top of
+          // the result
+          foundCache.remove(userInput);
+          foundCache.add(userInput);
+          appCache.putCache(cacheKey, foundCache);
+        }
+
+        String lastSearch = userInput;
+        if (getItemCount() > 0)
+        {
+          removeAllItems();
+        }
+        Set<String> cacheItems = appCache.getAllCachedItemsFor(cacheKey);
+        List<String> reversedCacheItems = new ArrayList<String>();
+        reversedCacheItems.addAll(cacheItems);
+        cacheItems = null;
+        Collections.reverse(reversedCacheItems);
+        if (lastSearch.isEmpty())
+        {
+          addItem("");
+        }
+
+        if (reversedCacheItems != null && !reversedCacheItems.isEmpty())
+        {
+          LinkedHashSet<String> foundCache = appCache
+                  .getAllCachedItemsFor(cacheKey);
+          boolean prune = reversedCacheItems.size() > cacheLimit;
+          int count = 1;
+          boolean limitExceeded = false;
+          for (String cacheItem : reversedCacheItems)
+          {
+            limitExceeded = (count++ > cacheLimit);
+            if (prune)
+            {
+              if (limitExceeded)
+              {
+                foundCache.remove(cacheItem);
+              }
+              else
+              {
+                addItem(cacheItem);
+              }
+            }
+            else
+            {
+              addItem(cacheItem);
+            }
+          }
+          appCache.putCache(cacheKey, foundCache);
+        }
+        setSelectedItem(lastSearch.isEmpty() ? "" : lastSearch);
+      }
+    });
+  }
+
+
+  /**
+   * This method should be called to persist the in-memory cache when this
+   * components parent frame is closed / exited
+   */
+  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);
+  }
+
+  /**
+   * Method to obtain input text from the cache box
+   * 
+   * @return
+   */
+  public String getUserInput()
+  {
+    return getEditor().getItem() == null ? "" : getEditor().getItem()
+            .toString().trim();
+  }
+
+}
index eb74eea..873fd27 100644 (file)
@@ -352,12 +352,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 82e5313..28941f5 100644 (file)
@@ -310,10 +310,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);
 
         /*
@@ -362,9 +361,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)
     {
index 48c33e5..41f141b 100644 (file)
@@ -337,6 +337,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]);
@@ -355,8 +368,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 e1334e1..0aa3b74 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 c1ca1b7..9f84c16 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.io.packed;
 
+import jalview.analysis.TreeModel;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
@@ -151,8 +152,7 @@ public class JalviewDataset
       {
         // the following works because all trees are already had node/SequenceI
         // associations created.
-        jalview.analysis.NJTree njt = new jalview.analysis.NJTree(
-                al.getSequencesArray(), nf);
+        TreeModel njt = new TreeModel(al.getSequencesArray(), null, nf);
         // this just updates the displayed leaf name on the tree according to
         // the SequenceIs.
         njt.renameAssociatedNodes();
index 9db7a8e..e1340e2 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;
@@ -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;
@@ -128,25 +133,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;
@@ -217,6 +218,7 @@ public class Datasetsequence extends DatastoreItem
     return modifiedtheseq;
   }
 
+  @Override
   public void conflict()
   {
     log.warn("Conflict in dataset sequence update to document. Overwriting document");
@@ -226,6 +228,7 @@ public class Datasetsequence extends DatastoreItem
 
   boolean modified = false;
 
+  @Override
   public void updateToDoc()
   {
     SequenceI sq = (SequenceI) jvobj;
index 61491b2..363f6f1 100644 (file)
@@ -284,9 +284,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();
@@ -302,11 +332,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());
         }
index a3781a7..d800d20 100644 (file)
@@ -20,7 +20,8 @@
  */
 package jalview.io.vamsas;
 
-import jalview.analysis.NJTree;
+import jalview.analysis.TreeBuilder;
+import jalview.analysis.TreeModel;
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
@@ -219,15 +220,17 @@ public class Tree extends DatastoreItem
     prov.getEntry(0).setUser(provEntry.getUser());
     prov.getEntry(0).setApp(provEntry.getApp());
     prov.getEntry(0).setDate(provEntry.getDate());
-    if (tp.getTree().hasOriginalSequenceData())
+
+    AlignmentView originalData = tp.getTree().getOriginalData();
+    if (originalData != null)
     {
       Input vInput = new Input();
       // LATER: check to see if tree input data is contained in this alignment -
       // or just correctly resolve the tree's seqData to the correct alignment
       // in
       // the document.
-      Vector alsqrefs = getjv2vObjs(findAlignmentSequences(jal,
-              tp.getTree().seqData.getSequences()));
+      Vector alsqrefs = getjv2vObjs(findAlignmentSequences(jal, tp
+              .getTree().getOriginalData().getSequences()));
       Object[] alsqs = new Object[alsqrefs.size()];
       alsqrefs.copyInto(alsqs);
       vInput.setObjRef(alsqs);
@@ -239,12 +242,13 @@ public class Tree extends DatastoreItem
       prov.getEntry(0).addParam(new Param());
       prov.getEntry(0).getParam(0).setName("treeType");
       prov.getEntry(0).getParam(0).setType("utf8");
-      prov.getEntry(0).getParam(0).setContent("NJ"); // TODO: type of tree is a
-      // general parameter
-      int ranges[] = tp.getTree().seqData.getVisibleContigs();
+      prov.getEntry(0).getParam(0)
+              .setContent(TreeBuilder.NEIGHBOUR_JOINING);
+      // TODO: type of tree is a general parameter
+      int ranges[] = originalData.getVisibleContigs();
       // VisibleContigs are with respect to alignment coordinates. Still need
       // offsets
-      int start = tp.getTree().seqData.getAlignmentOrigin();
+      int start = tp.getTree().getOriginalData().getAlignmentOrigin();
       for (int r = 0; r < ranges.length; r += 2)
       {
         Seg visSeg = new Seg();
@@ -370,13 +374,14 @@ public class Tree extends DatastoreItem
   /**
    * construct treenode mappings for mapped sequences
    * 
-   * @param ntree
+   * @param treeModel
    * @param newick
    * @return
    */
-  public Treenode[] makeTreeNodes(NJTree ntree, Newick newick)
+  public Treenode[] makeTreeNodes(TreeModel treeModel, Newick newick)
   {
-    Vector<SequenceNode> leaves = ntree.findLeaves(ntree.getTopNode());
+    Vector<SequenceNode> leaves = treeModel.findLeaves(treeModel
+            .getTopNode());
     Vector tnv = new Vector();
     Enumeration l = leaves.elements();
     Hashtable nodespecs = new Hashtable();
@@ -496,7 +501,7 @@ public class Tree extends DatastoreItem
     bindjvvobj(tp, tree);
     tree.setTitle(tp.getTitle());
     Newick newick = new Newick();
-    newick.setContent(tp.getTree().toString());
+    newick.setContent(tp.getTree().print());
     newick.setTitle(tp.getTitle());
     tree.addNewick(newick);
     tree.setProvenance(makeTreeProvenance(jal, tp));
index dc08b59..fdf8b58 100644 (file)
@@ -23,6 +23,7 @@ package jalview.javascript;
 import jalview.appletgui.AlignFrame;
 import jalview.bin.JalviewLite;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceGroup;
 import jalview.structure.SelectionSource;
 
@@ -44,7 +45,7 @@ public class JsSelectionSender extends JSFunctionExec implements
 
   @Override
   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
-          SelectionSource source)
+          HiddenColumns hidden, SelectionSource source)
   {
     // System.err.println("Testing selection event relay to jsfunction:"+_listener);
     try
index 4f833bc..c390b17 100644 (file)
@@ -126,7 +126,7 @@ public class MouseOverStructureListener extends JSFunctionExec implements
   }
 
   @Override
-  public String[] getPdbFile()
+  public String[] getStructureFiles()
   {
     return modelSet;
   }
@@ -221,8 +221,8 @@ public class MouseOverStructureListener extends JSFunctionExec implements
       ArrayList<String[]> ccomands = new ArrayList<String[]>();
       ArrayList<String> pdbfn = new ArrayList<String>();
       StructureMappingcommandSet[] colcommands = JmolCommands
-              .getColourBySequenceCommand(ssm, modelSet, sequence, sr, fr,
-                      ((AlignmentViewPanel) source).getAlignment());
+              .getColourBySequenceCommand(ssm, modelSet, sequence, sr,
+                      (AlignmentViewPanel) source);
       if (colcommands == null)
       {
         return;
@@ -298,6 +298,7 @@ public class MouseOverStructureListener extends JSFunctionExec implements
     return _listenerfn;
   }
 
+  @Override
   public void finalize() throws Throwable
   {
     jvlite = null;
index dc17397..88cc0a8 100755 (executable)
@@ -23,12 +23,11 @@ package jalview.jbgui;
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.api.SplitContainerI;
 import jalview.bin.Cache;
-import jalview.gui.JvOptionPane;
 import jalview.gui.JvSwingUtils;
 import jalview.gui.Preferences;
 import jalview.io.FileFormats;
-import jalview.schemes.ColourSchemeProperty;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
@@ -66,15 +65,13 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenuItem closeMenuItem = new JMenuItem();
 
-  protected JMenu colourMenu = new JMenu();
-
   protected JMenu webService = new JMenu();
 
   protected JMenuItem webServiceNoServices;
 
-  public JCheckBoxMenuItem viewBoxesMenuItem = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem viewBoxesMenuItem = new JCheckBoxMenuItem();
 
-  public JCheckBoxMenuItem viewTextMenuItem = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem viewTextMenuItem = new JCheckBoxMenuItem();
 
   protected JMenu sortByAnnotScore = new JMenu();
 
@@ -82,58 +79,19 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenu outputTextboxMenu = new JMenu();
 
-  protected JRadioButtonMenuItem clustalColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem zappoColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem taylorColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem hydrophobicityColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem helixColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem strandColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem turnColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem buriedColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem userDefinedColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem PIDColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem BLOSUM62Colour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem nucleotideColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem purinePyrimidineColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem RNAInteractionColour = new JRadioButtonMenuItem();
+  protected JCheckBoxMenuItem annotationPanelMenuItem = new JCheckBoxMenuItem();
 
-  // protected JRadioButtonMenuItem covariationColour = new
-  // JRadioButtonMenuItem();
+  protected JCheckBoxMenuItem colourTextMenuItem = new JCheckBoxMenuItem();
 
-  protected JRadioButtonMenuItem tcoffeeColour = new JRadioButtonMenuItem();
-
-  public JCheckBoxMenuItem annotationPanelMenuItem = new JCheckBoxMenuItem();
-
-  public JCheckBoxMenuItem colourTextMenuItem = new JCheckBoxMenuItem();
-
-  public JCheckBoxMenuItem showNonconservedMenuItem = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem showNonconservedMenuItem = new JCheckBoxMenuItem();
 
   protected JMenuItem undoMenuItem = new JMenuItem();
 
   protected JMenuItem redoMenuItem = new JMenuItem();
 
-  public JCheckBoxMenuItem conservationMenuItem = new JCheckBoxMenuItem();
-
-  JRadioButtonMenuItem noColourmenuItem = new JRadioButtonMenuItem();
+  protected JCheckBoxMenuItem wrapMenuItem = new JCheckBoxMenuItem();
 
-  public JCheckBoxMenuItem wrapMenuItem = new JCheckBoxMenuItem();
-
-  public JCheckBoxMenuItem renderGapsMenuItem = new JCheckBoxMenuItem();
-
-  public JCheckBoxMenuItem abovePIDThreshold = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem renderGapsMenuItem = new JCheckBoxMenuItem();
 
   public JCheckBoxMenuItem showSeqFeatures = new JCheckBoxMenuItem();
 
@@ -143,23 +101,35 @@ public class GAlignFrame extends JInternalFrame
 
   JMenu pasteMenu = new JMenu();
 
-  public JCheckBoxMenuItem applyToAllGroups = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem seqLimits = new JCheckBoxMenuItem();
+
+  protected JCheckBoxMenuItem scaleAbove = new JCheckBoxMenuItem();
 
-  public JCheckBoxMenuItem seqLimits = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem scaleLeft = new JCheckBoxMenuItem();
 
-  public JCheckBoxMenuItem scaleAbove = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem scaleRight = new JCheckBoxMenuItem();
 
-  public JCheckBoxMenuItem scaleLeft = new JCheckBoxMenuItem();
+  protected JCheckBoxMenuItem applyToAllGroups;
 
-  public JCheckBoxMenuItem scaleRight = new JCheckBoxMenuItem();
+  protected JMenu colourMenu = new JMenu();
+
+  protected JMenuItem textColour;
+
+  protected JCheckBoxMenuItem conservationMenuItem;
+
+  protected JMenuItem modifyConservation;
 
-  protected JMenuItem modifyConservation = new JMenuItem();
+  protected JCheckBoxMenuItem abovePIDThreshold;
+
+  protected JMenuItem modifyPID;
+
+  protected JMenuItem annotationColour;
 
   protected JMenu sortByTreeMenu = new JMenu();
 
   protected JMenu sort = new JMenu();
 
-  protected JMenu calculateTree = new JMenu();
+  protected JMenuItem calculateTree = new JMenuItem();
 
   protected JCheckBoxMenuItem padGapsMenuitem = new JCheckBoxMenuItem();
 
@@ -167,8 +137,6 @@ public class GAlignFrame extends JInternalFrame
 
   protected JCheckBoxMenuItem showDbRefsMenuitem = new JCheckBoxMenuItem();
 
-  protected ButtonGroup colours = new ButtonGroup();
-
   protected JMenuItem showTranslation = new JMenuItem();
 
   protected JMenuItem showReverse = new JMenuItem();
@@ -179,8 +147,6 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenuItem runGroovy = new JMenuItem();
 
-  protected JMenuItem rnahelicesColour = new JMenuItem();
-
   protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
 
   protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem();
@@ -265,7 +231,7 @@ public class GAlignFrame extends JInternalFrame
       System.err.println(e.toString());
     }
 
-    if (!jalview.util.Platform.isAMac())
+    if (!Platform.isAMac())
     {
       closeMenuItem.setMnemonic('C');
       outputTextboxMenu.setMnemonic('T');
@@ -276,201 +242,12 @@ public class GAlignFrame extends JInternalFrame
       pasteMenu.setMnemonic('P');
       reload.setMnemonic('R');
     }
-
-    if (jalview.gui.UserDefinedColours.getUserColourSchemes() != null)
-    {
-      java.util.Enumeration userColours = jalview.gui.UserDefinedColours
-              .getUserColourSchemes().keys();
-
-      while (userColours.hasMoreElements())
-      {
-        final JRadioButtonMenuItem radioItem = new JRadioButtonMenuItem(
-                userColours.nextElement().toString());
-        radioItem.setName("USER_DEFINED");
-        radioItem.addMouseListener(new MouseAdapter()
-        {
-          @Override
-          public void mousePressed(MouseEvent evt)
-          {
-            if (evt.isPopupTrigger()) // Mac
-            {
-              offerRemoval(radioItem);
-            }
-          }
-
-          @Override
-          public void mouseReleased(MouseEvent evt)
-          {
-            if (evt.isPopupTrigger()) // Windows
-            {
-              offerRemoval(radioItem);
-            }
-          }
-
-          /**
-           * @param radioItem
-           */
-          void offerRemoval(final JRadioButtonMenuItem radioItem)
-          {
-            radioItem.removeActionListener(radioItem.getActionListeners()[0]);
-
-            int option = JvOptionPane.showInternalConfirmDialog(
-                    jalview.gui.Desktop.desktop, MessageManager
-                            .getString("label.remove_from_default_list"),
-                    MessageManager
-                            .getString("label.remove_user_defined_colour"),
-                    JvOptionPane.YES_NO_OPTION);
-            if (option == JvOptionPane.YES_OPTION)
-            {
-              jalview.gui.UserDefinedColours
-                      .removeColourFromDefaults(radioItem.getText());
-              colourMenu.remove(radioItem);
-            }
-            else
-            {
-              radioItem.addActionListener(new ActionListener()
-              {
-                @Override
-                public void actionPerformed(ActionEvent evt)
-                {
-                  userDefinedColour_actionPerformed(evt);
-                }
-              });
-            }
-          }
-        });
-        radioItem.addActionListener(new ActionListener()
-        {
-          @Override
-          public void actionPerformed(ActionEvent evt)
-          {
-            userDefinedColour_actionPerformed(evt);
-          }
-        });
-        colourMenu.insert(radioItem, 15);
-        colours.add(radioItem);
-      }
-    }
-    colours.add(noColourmenuItem);
-    colours.add(clustalColour);
-    colours.add(zappoColour);
-    colours.add(taylorColour);
-    colours.add(hydrophobicityColour);
-    colours.add(helixColour);
-    colours.add(strandColour);
-    colours.add(turnColour);
-    colours.add(buriedColour);
-    colours.add(userDefinedColour);
-    colours.add(PIDColour);
-    colours.add(BLOSUM62Colour);
-    colours.add(nucleotideColour);
-    colours.add(purinePyrimidineColour);
-    // colours.add(covariationColour);
-    colours.add(tcoffeeColour);
-    colours.add(RNAInteractionColour);
-    setColourSelected(jalview.bin.Cache.getDefault(
-            Preferences.DEFAULT_COLOUR, "None"));
-  }
-
-  public void setColourSelected(String defaultColour)
-  {
-
-    if (defaultColour != null)
-    {
-      int index = ColourSchemeProperty
-              .getColourIndexFromName(defaultColour);
-
-      switch (index)
-      {
-      case ColourSchemeProperty.CLUSTAL:
-        clustalColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.BLOSUM:
-        BLOSUM62Colour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.PID:
-        PIDColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.ZAPPO:
-        zappoColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.TAYLOR:
-        taylorColour.setSelected(true);
-        break;
-
-      case ColourSchemeProperty.HYDROPHOBIC:
-        hydrophobicityColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.HELIX:
-        helixColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.STRAND:
-        strandColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.TURN:
-        turnColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.BURIED:
-        buriedColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.NUCLEOTIDE:
-        nucleotideColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.TCOFFEE:
-        tcoffeeColour.setSelected(true);
-        break;
-
-      case ColourSchemeProperty.PURINEPYRIMIDINE:
-        purinePyrimidineColour.setSelected(true);
-
-        break;
-
-      case ColourSchemeProperty.RNAINTERACTION:
-        RNAInteractionColour.setSelected(true);
-
-        break;
-      /*
-       * case ColourSchemeProperty.COVARIATION:
-       * covariationColour.setSelected(true);
-       * 
-       * break;
-       */
-      case ColourSchemeProperty.USER_DEFINED:
-        userDefinedColour.setSelected(true);
-
-        break;
-      case ColourSchemeProperty.NONE:
-      default:
-        noColourmenuItem.setSelected(true);
-        break;
-
-      }
-    }
-
   }
 
   private void jbInit() throws Exception
   {
+    initColourMenu();
+
     JMenuItem saveAs = new JMenuItem(
             MessageManager.getString("action.save_as"));
     ActionListener al = new ActionListener()
@@ -481,6 +258,8 @@ public class GAlignFrame extends JInternalFrame
         saveAs_actionPerformed(e);
       }
     };
+
+    // FIXME getDefaultToolkit throws an exception in Headless mode
     KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit
             .getDefaultToolkit().getMenuShortcutKeyMask()
             | KeyEvent.SHIFT_MASK, false);
@@ -669,7 +448,7 @@ public class GAlignFrame extends JInternalFrame
       }
     });
     showNonconservedMenuItem.setText(MessageManager
-            .getString("label.show_non_conversed"));
+            .getString("label.show_non_conserved"));
     showNonconservedMenuItem.setState(false);
     showNonconservedMenuItem.addActionListener(new ActionListener()
     {
@@ -744,36 +523,6 @@ public class GAlignFrame extends JInternalFrame
         pairwiseAlignmentMenuItem_actionPerformed(e);
       }
     });
-    JMenuItem PCAMenuItem = new JMenuItem(
-            MessageManager.getString("label.principal_component_analysis"));
-    PCAMenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        PCAMenuItem_actionPerformed(e);
-      }
-    });
-    JMenuItem averageDistanceTreeMenuItem = new JMenuItem(
-            MessageManager.getString("label.average_distance_identity"));
-    averageDistanceTreeMenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        averageDistanceTreeMenuItem_actionPerformed(e);
-      }
-    });
-    JMenuItem neighbourTreeMenuItem = new JMenuItem(
-            MessageManager.getString("label.neighbour_joining_identity"));
-    neighbourTreeMenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        neighbourTreeMenuItem_actionPerformed(e);
-      }
-    });
 
     this.getContentPane().setLayout(new BorderLayout());
     alignFrameMenuBar.setFont(new java.awt.Font("Verdana", 0, 11));
@@ -783,167 +532,7 @@ public class GAlignFrame extends JInternalFrame
     statusBar.setText(MessageManager.getString("label.status_bar"));
     outputTextboxMenu.setText(MessageManager
             .getString("label.out_to_textbox"));
-    clustalColour.setText(MessageManager.getString("label.clustalx"));
-    clustalColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        clustalColour_actionPerformed(e);
-      }
-    });
-    zappoColour.setText(MessageManager.getString("label.zappo"));
-    zappoColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        zappoColour_actionPerformed(e);
-      }
-    });
-    taylorColour.setText(MessageManager.getString("label.taylor"));
-    taylorColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        taylorColour_actionPerformed(e);
-      }
-    });
-    hydrophobicityColour.setText(MessageManager
-            .getString("label.hydrophobicity"));
-    hydrophobicityColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        hydrophobicityColour_actionPerformed(e);
-      }
-    });
-    helixColour.setText(MessageManager.getString("label.helix_propensity"));
-    helixColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        helixColour_actionPerformed(e);
-      }
-    });
-    strandColour.setText(MessageManager
-            .getString("label.strand_propensity"));
-    strandColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        strandColour_actionPerformed(e);
-      }
-    });
-    turnColour.setText(MessageManager.getString("label.turn_propensity"));
-    turnColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        turnColour_actionPerformed(e);
-      }
-    });
-    buriedColour.setText(MessageManager.getString("label.buried_index"));
-    buriedColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        buriedColour_actionPerformed(e);
-      }
-    });
-    userDefinedColour.setText(MessageManager
-            .getString("action.user_defined"));
-    userDefinedColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        userDefinedColour_actionPerformed(e);
-      }
-    });
-    PIDColour
-            .setText(MessageManager.getString("label.percentage_identity"));
-    PIDColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        PIDColour_actionPerformed(e);
-      }
-    });
-    BLOSUM62Colour
-            .setText(MessageManager.getString("label.blosum62_score"));
-    BLOSUM62Colour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        BLOSUM62Colour_actionPerformed(e);
-      }
-    });
-    nucleotideColour.setText(MessageManager.getString("label.nucleotide"));
-    nucleotideColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        nucleotideColour_actionPerformed(e);
-      }
-    });
-
-    purinePyrimidineColour.setText(MessageManager
-            .getString("label.purine_pyrimidine"));
-    purinePyrimidineColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        purinePyrimidineColour_actionPerformed(e);
-      }
-    });
-
-    RNAInteractionColour.setText("RNA Interaction type");
-    RNAInteractionColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        RNAInteractionColour_actionPerformed(e);
-      }
-    });
-    /*
-     * covariationColour.setText("Covariation");
-     * covariationColour.addActionListener(new ActionListener() { public void
-     * actionPerformed(ActionEvent e) { covariationColour_actionPerformed(e); }
-     * });
-     */
 
-    JMenuItem avDistanceTreeBlosumMenuItem = new JMenuItem(
-            MessageManager.getString("label.average_distance_bloslum62"));
-    avDistanceTreeBlosumMenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        avTreeBlosumMenuItem_actionPerformed(e);
-      }
-    });
-    JMenuItem njTreeBlosumMenuItem = new JMenuItem(
-            MessageManager.getString("label.neighbour_blosum62"));
-    njTreeBlosumMenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        njTreeBlosumMenuItem_actionPerformed(e);
-      }
-    });
     annotationPanelMenuItem.setActionCommand("");
     annotationPanelMenuItem.setText(MessageManager
             .getString("label.show_annotations"));
@@ -1039,7 +628,8 @@ public class GAlignFrame extends JInternalFrame
         sortAnnotations_actionPerformed();
       }
     });
-    colourTextMenuItem.setText(MessageManager
+    colourTextMenuItem = new JCheckBoxMenuItem(
+            MessageManager
             .getString("label.colour_text"));
     colourTextMenuItem.addActionListener(new ActionListener()
     {
@@ -1111,25 +701,6 @@ public class GAlignFrame extends JInternalFrame
     };
     addMenuActionAndAccelerator(keyStroke, redoMenuItem, al);
 
-    conservationMenuItem.setText(MessageManager
-            .getString("action.by_conservation"));
-    conservationMenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        conservationMenuItem_actionPerformed(e);
-      }
-    });
-    noColourmenuItem.setText(MessageManager.getString("label.none"));
-    noColourmenuItem.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        noColourmenuItem_actionPerformed(e);
-      }
-    });
     wrapMenuItem.setText(MessageManager.getString("label.wrap"));
     wrapMenuItem.addActionListener(new ActionListener()
     {
@@ -1182,16 +753,6 @@ public class GAlignFrame extends JInternalFrame
     };
     addMenuActionAndAccelerator(keyStroke, findMenuItem, al);
 
-    abovePIDThreshold.setText(MessageManager
-            .getString("label.above_identity_threshold"));
-    abovePIDThreshold.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        abovePIDThreshold_actionPerformed(e);
-      }
-    });
     showSeqFeatures.setText(MessageManager
             .getString("label.show_sequence_features"));
     showSeqFeatures.addActionListener(new ActionListener()
@@ -1337,28 +898,6 @@ public class GAlignFrame extends JInternalFrame
       }
     });
 
-    nucleotideColour.setText(MessageManager.getString("label.nucleotide"));
-    nucleotideColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        nucleotideColour_actionPerformed(e);
-      }
-    });
-
-    tcoffeeColour.setText(MessageManager.getString("label.tcoffee_scores"));
-    tcoffeeColour.setEnabled(false);
-    tcoffeeColour.addActionListener(new ActionListener()
-    {
-
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        tcoffeeColorScheme_actionPerformed(e);
-      }
-    });
-
     JMenuItem deleteGroups = new JMenuItem(
             MessageManager.getString("action.undefine_groups"));
     keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_U, Toolkit
@@ -1373,8 +912,19 @@ public class GAlignFrame extends JInternalFrame
     };
     addMenuActionAndAccelerator(keyStroke, deleteGroups, al);
 
+    JMenuItem annotationColumn = new JMenuItem(
+            MessageManager.getString("action.select_by_annotation"));
+    annotationColumn.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        annotationColumn_actionPerformed(e);
+      }
+    });
+
     JMenuItem createGroup = new JMenuItem(
-            MessageManager.getString("action.create_groups"));
+            MessageManager.getString("action.create_group"));
     keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G, Toolkit
             .getDefaultToolkit().getMenuShortcutKeyMask(), false);
     al = new ActionListener()
@@ -1470,16 +1020,6 @@ public class GAlignFrame extends JInternalFrame
     };
     addMenuActionAndAccelerator(keyStroke, pasteThis, al);
 
-    applyToAllGroups.setText(MessageManager
-            .getString("label.apply_colour_to_all_groups"));
-    applyToAllGroups.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        applyToAllGroups_actionPerformed(e);
-      }
-    });
     JMenuItem createPNG = new JMenuItem("PNG");
     createPNG.addActionListener(new ActionListener()
     {
@@ -1604,26 +1144,6 @@ public class GAlignFrame extends JInternalFrame
 
     });
 
-    JMenuItem modifyPID = new JMenuItem(
-            MessageManager.getString("label.modify_identity_threshold"));
-    modifyPID.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        modifyPID_actionPerformed(e);
-      }
-    });
-    modifyConservation.setText(MessageManager
-            .getString("label.modify_conservation_threshold"));
-    modifyConservation.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        modifyConservation_actionPerformed(e);
-      }
-    });
     sortByTreeMenu
             .setText(MessageManager.getString("action.by_tree_order"));
     sort.setText(MessageManager.getString("action.sort"));
@@ -1632,7 +1152,7 @@ public class GAlignFrame extends JInternalFrame
       @Override
       public void menuSelected(MenuEvent e)
       {
-        buildTreeMenu();
+        buildTreeSortMenu();
       }
 
       @Override
@@ -1669,8 +1189,8 @@ public class GAlignFrame extends JInternalFrame
     });
     sortByAnnotScore.setVisible(false);
 
-    calculateTree
-            .setText(MessageManager.getString("action.calculate_tree"));
+    calculateTree.setText(MessageManager
+            .getString("action.calculate_tree_pca"));
 
     padGapsMenuitem.setText(MessageManager.getString("label.pad_gaps"));
     padGapsMenuitem.setState(jalview.bin.Cache
@@ -1773,39 +1293,6 @@ public class GAlignFrame extends JInternalFrame
       }
     });
 
-    JMenuItem annotationColour = new JMenuItem(
-            MessageManager.getString("action.by_annotation"));
-    annotationColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        annotationColour_actionPerformed(e);
-      }
-    });
-
-    JMenuItem annotationColumn = new JMenuItem(
-            MessageManager.getString("action.select_by_annotation"));
-    annotationColumn.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        annotationColumn_actionPerformed(e);
-      }
-    });
-
-    rnahelicesColour.setText(MessageManager
-            .getString("action.by_rna_helixes"));
-    rnahelicesColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        rnahelicesColour_actionPerformed(e);
-      }
-    });
-
     JMenuItem associatedData = new JMenuItem(
             MessageManager.getString("label.load_features_annotations"));
     associatedData.addActionListener(new ActionListener()
@@ -2112,18 +1599,10 @@ public class GAlignFrame extends JInternalFrame
     tabbedPane.setToolTipText("<html><i>"
             + MessageManager.getString("label.rename_tab_eXpand_reGroup")
             + "</i></html>");
-    JMenuItem textColour = new JMenuItem(
-            MessageManager.getString("action.set_text_colour"));
-    textColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        textColour_actionPerformed(e);
-      }
-    });
+
     formatMenu.setText(MessageManager.getString("action.format"));
     JMenu selectMenu = new JMenu(MessageManager.getString("action.select"));
+
     idRightAlign.setText(MessageManager
             .getString("label.right_align_sequence_id"));
     idRightAlign.addActionListener(new ActionListener()
@@ -2303,33 +1782,6 @@ public class GAlignFrame extends JInternalFrame
     autoAnnMenu.add(showGroupConsensus);
     annotationsMenu.add(autoAnnMenu);
 
-    colourMenu.add(applyToAllGroups);
-    colourMenu.add(textColour);
-    colourMenu.addSeparator();
-    colourMenu.add(noColourmenuItem);
-    colourMenu.add(clustalColour);
-    colourMenu.add(BLOSUM62Colour);
-    colourMenu.add(PIDColour);
-    colourMenu.add(zappoColour);
-    colourMenu.add(taylorColour);
-    colourMenu.add(hydrophobicityColour);
-    colourMenu.add(helixColour);
-    colourMenu.add(strandColour);
-    colourMenu.add(turnColour);
-    colourMenu.add(buriedColour);
-    colourMenu.add(nucleotideColour);
-    colourMenu.add(purinePyrimidineColour);
-    // colourMenu.add(RNAInteractionColour);
-    // colourMenu.add(covariationColour);
-    colourMenu.add(tcoffeeColour);
-    colourMenu.add(userDefinedColour);
-    colourMenu.addSeparator();
-    colourMenu.add(conservationMenuItem);
-    colourMenu.add(modifyConservation);
-    colourMenu.add(abovePIDThreshold);
-    colourMenu.add(modifyPID);
-    colourMenu.add(annotationColour);
-    colourMenu.add(rnahelicesColour);
 
     sort.add(sortIDMenuItem);
     sort.add(sortLengthMenuItem);
@@ -2340,7 +1792,6 @@ public class GAlignFrame extends JInternalFrame
     calculateMenu.add(calculateTree);
     calculateMenu.addSeparator();
     calculateMenu.add(pairwiseAlignmentMenuItem);
-    calculateMenu.add(PCAMenuItem);
     calculateMenu.addSeparator();
     calculateMenu.add(showTranslation);
     calculateMenu.add(showReverse);
@@ -2402,6 +1853,90 @@ public class GAlignFrame extends JInternalFrame
     // selectMenu.add(listenToViewSelections);
   }
 
+  /**
+   * Constructs the entries on the Colour menu (but does not add them to the
+   * menu).
+   */
+  protected void initColourMenu()
+  {
+    applyToAllGroups = new JCheckBoxMenuItem(
+            MessageManager.getString("label.apply_colour_to_all_groups"));
+    applyToAllGroups.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        applyToAllGroups_actionPerformed(applyToAllGroups.isSelected());
+      }
+    });
+
+    textColour = new JMenuItem(
+            MessageManager.getString("label.text_colour"));
+    textColour.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        textColour_actionPerformed();
+      }
+    });
+
+    conservationMenuItem = new JCheckBoxMenuItem(
+            MessageManager.getString("action.by_conservation"));
+    conservationMenuItem.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        conservationMenuItem_actionPerformed(conservationMenuItem
+                .isSelected());
+      }
+    });
+
+    abovePIDThreshold = new JCheckBoxMenuItem(
+            MessageManager.getString("label.above_identity_threshold"));
+    abovePIDThreshold.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        abovePIDThreshold_actionPerformed(abovePIDThreshold.isSelected());
+      }
+    });
+    modifyPID = new JMenuItem(
+            MessageManager.getString("label.modify_identity_threshold"));
+    modifyPID.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        modifyPID_actionPerformed();
+      }
+    });
+    modifyConservation = new JMenuItem(
+            MessageManager
+            .getString("label.modify_conservation_threshold"));
+    modifyConservation.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        modifyConservation_actionPerformed();
+      }
+    });
+
+    annotationColour = new JMenuItem(
+            MessageManager.getString("action.by_annotation"));
+    annotationColour.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        annotationColour_actionPerformed();
+      }
+    });
+  }
+
   protected void selectHighlightedColumns_actionPerformed(
           ActionEvent actionEvent)
   {
@@ -2737,87 +2272,11 @@ public class GAlignFrame extends JInternalFrame
   {
   }
 
-  protected void PCAMenuItem_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void averageDistanceTreeMenuItem_actionPerformed(ActionEvent e)
-  {
-  }
-
   protected void neighbourTreeMenuItem_actionPerformed(ActionEvent e)
   {
   }
 
-  protected void njTreeBlosumMenuItem_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void avTreeBlosumMenuItem_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void clustalColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void zappoColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void taylorColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void hydrophobicityColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void helixColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void strandColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void turnColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void buriedColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void userDefinedColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void PIDColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void BLOSUM62Colour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void purinePyrimidineColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void RNAInteractionColour_actionPerformed(ActionEvent e)
-  {
-  }
-
-  /*
-   * protected void covariationColour_actionPerformed(ActionEvent e) { }
-   */
-
-  protected void noColourmenuItem_actionPerformed(ActionEvent e)
-  {
-  }
-
-  protected void conservationMenuItem_actionPerformed(ActionEvent e)
+  protected void conservationMenuItem_actionPerformed(boolean selected)
   {
   }
 
@@ -2833,7 +2292,7 @@ public class GAlignFrame extends JInternalFrame
   {
   }
 
-  protected void abovePIDThreshold_actionPerformed(ActionEvent e)
+  protected void abovePIDThreshold_actionPerformed(boolean selected)
   {
   }
 
@@ -2841,10 +2300,6 @@ public class GAlignFrame extends JInternalFrame
   {
   }
 
-  protected void nucleotideColour_actionPerformed(ActionEvent e)
-  {
-  }
-
   protected void deleteGroups_actionPerformed(ActionEvent e)
   {
   }
@@ -2877,7 +2332,7 @@ public class GAlignFrame extends JInternalFrame
   {
   }
 
-  protected void applyToAllGroups_actionPerformed(ActionEvent e)
+  protected void applyToAllGroups_actionPerformed(boolean selected)
   {
   }
 
@@ -2925,19 +2380,6 @@ public class GAlignFrame extends JInternalFrame
 
   }
 
-  /**
-   * Template method to handle the 'Color T-Coffee scores' menu event.
-   * <p>
-   * Subclasses override this method to provide a custom action.
-   * 
-   * @param event
-   *          The raised event
-   */
-  protected void tcoffeeColorScheme_actionPerformed(ActionEvent event)
-  {
-
-  }
-
   protected void jpred_actionPerformed(ActionEvent e)
   {
   }
@@ -2954,11 +2396,11 @@ public class GAlignFrame extends JInternalFrame
   {
   }
 
-  protected void modifyPID_actionPerformed(ActionEvent e)
+  protected void modifyPID_actionPerformed()
   {
   }
 
-  protected void modifyConservation_actionPerformed(ActionEvent e)
+  protected void modifyConservation_actionPerformed()
   {
   }
 
@@ -3000,19 +2442,12 @@ public class GAlignFrame extends JInternalFrame
 
   }
 
-  public void annotationColour_actionPerformed(ActionEvent e)
+  public void annotationColour_actionPerformed()
   {
-
   }
 
   public void annotationColumn_actionPerformed(ActionEvent e)
   {
-
-  }
-
-  public void rnahelicesColour_actionPerformed(ActionEvent e)
-  {
-
   }
 
   public void associatedData_actionPerformed(ActionEvent e)
@@ -3105,7 +2540,7 @@ public class GAlignFrame extends JInternalFrame
 
   }
 
-  public void textColour_actionPerformed(ActionEvent e)
+  public void textColour_actionPerformed()
   {
 
   }
@@ -3125,7 +2560,7 @@ public class GAlignFrame extends JInternalFrame
 
   }
 
-  public void buildTreeMenu()
+  public void buildTreeSortMenu()
   {
 
   }
index 63ecdaf..3e3691c 100755 (executable)
@@ -23,6 +23,7 @@ package jalview.jbgui;
 import jalview.api.AlignmentViewPanel;
 import jalview.io.FileFormatException;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
 
 import java.awt.FlowLayout;
 import java.awt.Toolkit;
@@ -99,6 +100,10 @@ public class GDesktop extends JFrame
 
   JMenuItem garbageCollect = new JMenuItem();
 
+  protected JMenuItem groovyShell;
+
+  protected JCheckBoxMenuItem experimentalFeatures;
+
   protected JCheckBoxMenuItem showConsole = new JCheckBoxMenuItem();
 
   protected JCheckBoxMenuItem showNews = new JCheckBoxMenuItem();
@@ -119,7 +124,7 @@ public class GDesktop extends JFrame
       e.printStackTrace();
     }
 
-    if (!new jalview.util.Platform().isAMac())
+    if (!Platform.isAMac())
     {
       FileMenu.setMnemonic('F');
       inputLocalFileMenuItem.setMnemonic('L');
@@ -374,6 +379,30 @@ public class GDesktop extends JFrame
         showNews_actionPerformed(e);
       }
     });
+    groovyShell = new JMenuItem();
+    groovyShell.setText(MessageManager.getString("label.groovy_console"));
+    groovyShell.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        groovyShell_actionPerformed();
+      }
+    });
+    experimentalFeatures = new JCheckBoxMenuItem();
+    experimentalFeatures.setText(MessageManager
+            .getString("label.show_experimental"));
+    experimentalFeatures.setToolTipText(MessageManager
+            .getString("label.show_experimental_tip"));
+    experimentalFeatures.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        showExperimental_actionPerformed(experimentalFeatures.isSelected());
+      }
+    });
+
     snapShotWindow.setText(MessageManager.getString("label.take_snapshot"));
     snapShotWindow.addActionListener(new ActionListener()
     {
@@ -410,6 +439,8 @@ public class GDesktop extends JFrame
     toolsMenu.add(showConsole);
     toolsMenu.add(showNews);
     toolsMenu.add(garbageCollect);
+    toolsMenu.add(groovyShell);
+    toolsMenu.add(experimentalFeatures);
     // toolsMenu.add(snapShotWindow);
     inputMenu.add(inputLocalFileMenuItem);
     inputMenu.add(inputURLMenuItem);
@@ -421,6 +452,14 @@ public class GDesktop extends JFrame
     // inputMenu.add(vamsasLoad);
   }
 
+  protected void showExperimental_actionPerformed(boolean selected)
+  {
+  }
+
+  protected void groovyShell_actionPerformed()
+  {
+  }
+
   protected void snapShotWindow_actionPerformed(ActionEvent e)
   {
     // TODO Auto-generated method stub
index fef4568..c335b33 100755 (executable)
@@ -24,6 +24,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
 import jalview.io.FormatAdapter;
+import jalview.io.cache.JvCacheableInputBox;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -38,12 +39,11 @@ import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
-import javax.swing.JScrollPane;
-import javax.swing.JTextArea;
 import javax.swing.SwingConstants;
 import javax.swing.SwingUtilities;
 import javax.swing.event.CaretEvent;
 import javax.swing.event.CaretListener;
+import javax.swing.text.JTextComponent;
 
 public class GFinder extends JPanel
 {
@@ -57,11 +57,9 @@ public class GFinder extends JPanel
 
   GridLayout gridLayout1 = new GridLayout();
 
-  protected JButton createNewGroup = new JButton();
+  protected JButton createFeatures = new JButton();
 
-  JScrollPane jScrollPane1 = new JScrollPane();
-
-  protected JTextArea textfield = new JTextArea();
+  protected JvCacheableInputBox<String> searchBox = new JvCacheableInputBox<String>(getCacheKey());
 
   BorderLayout mainBorderLayout = new BorderLayout();
 
@@ -81,6 +79,8 @@ public class GFinder extends JPanel
 
   GridLayout optionsGridLayout = new GridLayout();
 
+  private static final String FINDER_CACHE_KEY = "CACHE.FINDER";
+
   public GFinder()
   {
     try
@@ -121,22 +121,21 @@ public class GFinder extends JPanel
     gridLayout1.setHgap(0);
     gridLayout1.setRows(3);
     gridLayout1.setVgap(2);
-    createNewGroup.setEnabled(false);
-    createNewGroup.setFont(new java.awt.Font("Verdana", 0, 12));
-    createNewGroup.setMargin(new Insets(0, 0, 0, 0));
-    createNewGroup.setText(MessageManager.getString("label.new_feature"));
-    createNewGroup.addActionListener(new java.awt.event.ActionListener()
+    createFeatures.setEnabled(false);
+    createFeatures.setFont(new java.awt.Font("Verdana", 0, 12));
+    createFeatures.setMargin(new Insets(0, 0, 0, 0));
+    createFeatures.setText(MessageManager.getString("label.new_feature"));
+    createFeatures.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        createNewGroup_actionPerformed(e);
+        createFeatures_actionPerformed();
       }
     });
-    textfield.setFont(new java.awt.Font("Verdana", Font.PLAIN, 12));
-    textfield.setText("");
-    textfield.setLineWrap(true);
-    textfield.addCaretListener(new CaretListener()
+    searchBox.setFont(new java.awt.Font("Verdana", Font.PLAIN, 12));
+    ((JTextComponent) searchBox.getEditor().getEditorComponent())
+            .addCaretListener(new CaretListener()
     {
       @Override
       public void caretUpdate(CaretEvent e)
@@ -144,15 +143,15 @@ public class GFinder extends JPanel
         textfield_caretUpdate(e);
       }
     });
-    textfield.addKeyListener(new java.awt.event.KeyAdapter()
-    {
-      @Override
-      public void keyPressed(KeyEvent e)
-      {
-        textfield_keyPressed(e);
-      }
-    });
-
+    searchBox.getEditor().getEditorComponent()
+            .addKeyListener(new java.awt.event.KeyAdapter()
+            {
+              @Override
+              public void keyPressed(KeyEvent e)
+              {
+                textfield_keyPressed(e);
+              }
+            });
     mainBorderLayout.setHgap(5);
     mainBorderLayout.setVgap(5);
     jPanel4.setLayout(borderLayout2);
@@ -166,14 +165,13 @@ public class GFinder extends JPanel
 
     actionsPanel.add(findNext, null);
     actionsPanel.add(findAll, null);
-    actionsPanel.add(createNewGroup, null);
+    actionsPanel.add(createFeatures, null);
     this.add(jLabelFind, java.awt.BorderLayout.WEST);
     this.add(actionsPanel, java.awt.BorderLayout.EAST);
     this.add(jPanel2, java.awt.BorderLayout.SOUTH);
     this.add(jPanel3, java.awt.BorderLayout.NORTH);
     this.add(jPanel4, java.awt.BorderLayout.CENTER);
-    jPanel4.add(jScrollPane1, java.awt.BorderLayout.NORTH);
-    jScrollPane1.getViewport().add(textfield);
+    jPanel4.add(searchBox, java.awt.BorderLayout.NORTH);
 
     JPanel optionsPanel = new JPanel();
 
@@ -187,37 +185,41 @@ public class GFinder extends JPanel
     jPanel4.add(optionsPanel, java.awt.BorderLayout.WEST);
   }
 
-  protected void findNext_actionPerformed(ActionEvent e)
+  protected void textfield_keyPressed(KeyEvent e)
   {
+    if (e.getKeyCode() == KeyEvent.VK_ENTER)
+    {
+      if (!searchBox.isPopupVisible())
+      {
+        e.consume();
+        findNext_actionPerformed(null);
+      }
+    }
   }
 
-  protected void findAll_actionPerformed(ActionEvent e)
+  protected void findNext_actionPerformed(ActionEvent e)
   {
   }
 
-  protected void textfield_keyPressed(KeyEvent e)
+  protected void findAll_actionPerformed(ActionEvent e)
   {
-    if (e.getKeyCode() == KeyEvent.VK_ENTER)
-    {
-      e.consume();
-      findNext_actionPerformed(null);
-    }
   }
 
-  public void createNewGroup_actionPerformed(ActionEvent e)
+
+  public void createFeatures_actionPerformed()
   {
   }
 
   public void textfield_caretUpdate(CaretEvent e)
   {
-    if (textfield.getText().indexOf(">") > -1)
+    if (searchBox.getUserInput().indexOf(">") > -1)
     {
       SwingUtilities.invokeLater(new Runnable()
       {
         @Override
         public void run()
         {
-          String str = textfield.getText();
+          String str = searchBox.getUserInput();
           AlignmentI al = null;
           try
           {
@@ -232,10 +234,27 @@ public class GFinder extends JPanel
                     jalview.util.Comparison.GapChars, al.getSequenceAt(0)
                             .getSequenceAsString());
 
-            textfield.setText(str);
           }
         }
       });
     }
   }
+
+
+
+
+
+  /**
+   * Returns unique key used for storing Finder cache items in the cache data
+   * structure
+   * 
+   * @return
+   */
+  public String getCacheKey()
+  {
+    return FINDER_CACHE_KEY;
+  }
+
+
+
 }
index 8c893a2..5c15e80 100755 (executable)
@@ -63,6 +63,8 @@ public class GFontChooser extends JPanel
 
   protected JCheckBox scaleAsCdna = new JCheckBox();
 
+  protected JCheckBox fontAsCdna = new JCheckBox();
+
   /**
    * Creates a new GFontChooser object.
    */
@@ -98,9 +100,10 @@ public class GFontChooser extends JPanel
     fontSize.setPreferredSize(new Dimension(50, 21));
     fontSize.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        fontSize_actionPerformed(e);
+        fontSize_actionPerformed();
       }
     });
 
@@ -109,9 +112,10 @@ public class GFontChooser extends JPanel
     fontStyle.setPreferredSize(new Dimension(90, 21));
     fontStyle.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        fontStyle_actionPerformed(e);
+        fontStyle_actionPerformed();
       }
     });
 
@@ -132,9 +136,10 @@ public class GFontChooser extends JPanel
     fontName.setPreferredSize(new Dimension(180, 21));
     fontName.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        fontName_actionPerformed(e);
+        fontName_actionPerformed();
       }
     });
 
@@ -142,9 +147,10 @@ public class GFontChooser extends JPanel
     ok.setFont(VERDANA_11PT);
     ok.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        ok_actionPerformed(e);
+        ok_actionPerformed();
       }
     });
 
@@ -152,9 +158,10 @@ public class GFontChooser extends JPanel
     cancel.setFont(VERDANA_11PT);
     cancel.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        cancel_actionPerformed(e);
+        cancel_actionPerformed();
       }
     });
 
@@ -162,37 +169,57 @@ public class GFontChooser extends JPanel
     defaultButton.setText(MessageManager.getString("label.set_as_default"));
     defaultButton.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        defaultButton_actionPerformed(e);
+        defaultButton_actionPerformed();
       }
     });
 
     smoothFont.setFont(JvSwingUtils.getLabelFont());
     smoothFont.setOpaque(false);
     smoothFont.setText(MessageManager.getString("label.anti_alias_fonts"));
-    smoothFont.setBounds(new Rectangle(41, 65, 260, 23));
+    smoothFont.setBounds(new Rectangle(1, 65, 300, 23));
     smoothFont.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        smoothFont_actionPerformed(e);
+        smoothFont_actionPerformed();
       }
     });
 
     /*
-     * Scale protein as cDNA is only visible in SplitFrame protein alignment
+     * Scale protein as cDNA is only visible in SplitFrame
      */
     scaleAsCdna.setVisible(false);
     scaleAsCdna.setFont(JvSwingUtils.getLabelFont());
     scaleAsCdna.setOpaque(false);
     scaleAsCdna.setText(MessageManager.getString("label.scale_as_cdna"));
-    scaleAsCdna.setBounds(new Rectangle(41, 85, 260, 23));
+    scaleAsCdna.setBounds(new Rectangle(1, 85, 300, 23));
     scaleAsCdna.addActionListener(new ActionListener()
     {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        scaleAsCdna_actionPerformed();
+      }
+    });
+
+    /*
+     * Same font for cDNA/peptide is only visible in SplitFrame
+     */
+    fontAsCdna.setVisible(false);
+    fontAsCdna.setFont(JvSwingUtils.getLabelFont());
+    fontAsCdna.setOpaque(false);
+    fontAsCdna.setText(MessageManager.getString("label.font_as_cdna"));
+    fontAsCdna.setBounds(new Rectangle(1, 105, 350, 23));
+    fontAsCdna.addActionListener(new ActionListener()
+    {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        scaleAsCdna_actionPerformed(e);
+        mirrorFonts_actionPerformed();
       }
     });
 
@@ -239,85 +266,53 @@ public class GFontChooser extends JPanel
      */
     JPanel jPanel4 = new JPanel();
     jPanel4.setOpaque(false);
-    jPanel4.setBounds(new Rectangle(24, 112, 300, 35));
+    jPanel4.setBounds(new Rectangle(24, 132, 300, 35));
     jPanel4.add(defaultButton);
     jPanel4.add(ok);
     jPanel4.add(cancel);
 
     this.add(smoothFont);
     this.add(scaleAsCdna);
+    this.add(fontAsCdna);
     this.add(jPanel3, null);
     this.add(jPanel2, null);
     this.add(jPanel4);
     this.add(jPanel1, null);
   }
 
-  protected void scaleAsCdna_actionPerformed(ActionEvent e)
+  protected void mirrorFonts_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void ok_actionPerformed(ActionEvent e)
+  protected void scaleAsCdna_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void cancel_actionPerformed(ActionEvent e)
+  protected void ok_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void fontName_actionPerformed(ActionEvent e)
+  protected void cancel_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void fontSize_actionPerformed(ActionEvent e)
+  protected void fontName_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void fontStyle_actionPerformed(ActionEvent e)
+  protected void fontSize_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  public void defaultButton_actionPerformed(ActionEvent e)
+  protected void fontStyle_actionPerformed()
   {
   }
 
-  public void smoothFont_actionPerformed(ActionEvent e)
+  public void defaultButton_actionPerformed()
   {
+  }
 
+  protected void smoothFont_actionPerformed()
+  {
   }
 }
index 0bc6cac..3715acc 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.util.MessageManager;
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.FlowLayout;
+import java.awt.Font;
 import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -43,54 +44,20 @@ import javax.swing.event.MenuListener;
 
 public class GPCAPanel extends JInternalFrame
 {
-  JPanel jPanel2 = new JPanel();
+  private static final Font VERDANA_12 = new Font("Verdana", 0, 12);
 
-  JLabel jLabel1 = new JLabel();
+  protected JComboBox<String> xCombobox = new JComboBox<String>();
 
-  JLabel jLabel2 = new JLabel();
+  protected JComboBox<String> yCombobox = new JComboBox<String>();
 
-  JLabel jLabel3 = new JLabel();
+  protected JComboBox<String> zCombobox = new JComboBox<String>();
 
-  protected JComboBox xCombobox = new JComboBox();
-
-  protected JComboBox yCombobox = new JComboBox();
-
-  protected JComboBox zCombobox = new JComboBox();
-
-  protected JButton resetButton = new JButton();
-
-  FlowLayout flowLayout1 = new FlowLayout();
-
-  BorderLayout borderLayout1 = new BorderLayout();
-
-  JMenuBar jMenuBar1 = new JMenuBar();
-
-  JMenu fileMenu = new JMenu();
-
-  JMenu saveMenu = new JMenu();
-
-  protected JMenu scoreMatrixMenu = new JMenu();
-
-  JMenuItem eps = new JMenuItem();
-
-  JMenuItem png = new JMenuItem();
-
-  JMenuItem print = new JMenuItem();
-
-  JMenuItem outputValues = new JMenuItem();
-
-  JMenuItem outputPoints = new JMenuItem();
-
-  JMenuItem outputProjPoints = new JMenuItem();
+  protected JMenu scoreModelMenu = new JMenu();
 
   protected JMenu viewMenu = new JMenu();
 
   protected JCheckBoxMenuItem showLabels = new JCheckBoxMenuItem();
 
-  JMenuItem bgcolour = new JMenuItem();
-
-  JMenuItem originalSeqData = new JMenuItem();
-
   protected JMenu associateViewsMenu = new JMenu();
 
   protected JMenu calcSettings = new JMenu();
@@ -99,12 +66,8 @@ public class GPCAPanel extends JInternalFrame
 
   protected JCheckBoxMenuItem protSetting = new JCheckBoxMenuItem();
 
-  protected JCheckBoxMenuItem jvVersionSetting = new JCheckBoxMenuItem();
-
   protected JLabel statusBar = new JLabel();
 
-  protected GridLayout statusPanelLayout = new GridLayout();
-
   protected JPanel statusPanel = new JPanel();
 
   public GPCAPanel()
@@ -123,49 +86,55 @@ public class GPCAPanel extends JInternalFrame
       yCombobox.addItem("dim " + i);
       zCombobox.addItem("dim " + i);
     }
-
-    setJMenuBar(jMenuBar1);
   }
 
   private void jbInit() throws Exception
   {
-    this.getContentPane().setLayout(borderLayout1);
-    jPanel2.setLayout(flowLayout1);
-    jLabel1.setFont(new java.awt.Font("Verdana", 0, 12));
+    this.getContentPane().setLayout(new BorderLayout());
+    JPanel jPanel2 = new JPanel();
+    jPanel2.setLayout(new FlowLayout());
+    JLabel jLabel1 = new JLabel();
+    jLabel1.setFont(VERDANA_12);
     jLabel1.setText("x=");
-    jLabel2.setFont(new java.awt.Font("Verdana", 0, 12));
+    JLabel jLabel2 = new JLabel();
+    jLabel2.setFont(VERDANA_12);
     jLabel2.setText("y=");
-    jLabel3.setFont(new java.awt.Font("Verdana", 0, 12));
+    JLabel jLabel3 = new JLabel();
+    jLabel3.setFont(VERDANA_12);
     jLabel3.setText("z=");
     jPanel2.setBackground(Color.white);
     jPanel2.setBorder(null);
-    zCombobox.setFont(new java.awt.Font("Verdana", 0, 12));
-    zCombobox.addActionListener(new java.awt.event.ActionListener()
+    zCombobox.setFont(VERDANA_12);
+    zCombobox.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         zCombobox_actionPerformed(e);
       }
     });
-    yCombobox.setFont(new java.awt.Font("Verdana", 0, 12));
-    yCombobox.addActionListener(new java.awt.event.ActionListener()
+    yCombobox.setFont(VERDANA_12);
+    yCombobox.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         yCombobox_actionPerformed(e);
       }
     });
-    xCombobox.setFont(new java.awt.Font("Verdana", 0, 12));
-    xCombobox.addActionListener(new java.awt.event.ActionListener()
+    xCombobox.setFont(VERDANA_12);
+    xCombobox.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         xCombobox_actionPerformed(e);
       }
     });
-    resetButton.setFont(new java.awt.Font("Verdana", 0, 12));
+    JButton resetButton = new JButton();
+    resetButton.setFont(VERDANA_12);
     resetButton.setText(MessageManager.getString("action.reset"));
-    resetButton.addActionListener(new java.awt.event.ActionListener()
+    resetButton.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
@@ -173,51 +142,64 @@ public class GPCAPanel extends JInternalFrame
         resetButton_actionPerformed(e);
       }
     });
+    JMenu fileMenu = new JMenu();
     fileMenu.setText(MessageManager.getString("action.file"));
+    JMenu saveMenu = new JMenu();
     saveMenu.setText(MessageManager.getString("action.save_as"));
-    eps.setText("EPS");
+    JMenuItem eps = new JMenuItem("EPS");
     eps.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         eps_actionPerformed(e);
       }
     });
-    png.setText("PNG");
+    JMenuItem png = new JMenuItem("PNG");
     png.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         png_actionPerformed(e);
       }
     });
+    JMenuItem outputValues = new JMenuItem();
     outputValues.setText(MessageManager.getString("label.output_values"));
     outputValues.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         outputValues_actionPerformed(e);
       }
     });
+    JMenuItem outputPoints = new JMenuItem();
     outputPoints.setText(MessageManager.getString("label.output_points"));
     outputPoints.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         outputPoints_actionPerformed(e);
       }
     });
+    JMenuItem outputProjPoints = new JMenuItem();
     outputProjPoints.setText(MessageManager
             .getString("label.output_transformed_points"));
     outputProjPoints.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         outputProjPoints_actionPerformed(e);
       }
     });
+    JMenuItem print = new JMenuItem();
+    print.setText(MessageManager.getString("action.print"));
     print.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         print_actionPerformed(e);
@@ -226,32 +208,38 @@ public class GPCAPanel extends JInternalFrame
     viewMenu.setText(MessageManager.getString("action.view"));
     viewMenu.addMenuListener(new MenuListener()
     {
+      @Override
       public void menuSelected(MenuEvent e)
       {
         viewMenu_menuSelected();
       }
 
+      @Override
       public void menuDeselected(MenuEvent e)
       {
       }
 
+      @Override
       public void menuCanceled(MenuEvent e)
       {
       }
     });
-    scoreMatrixMenu.setText(MessageManager
+    scoreModelMenu.setText(MessageManager
             .getString("label.select_score_model"));
-    scoreMatrixMenu.addMenuListener(new MenuListener()
+    scoreModelMenu.addMenuListener(new MenuListener()
     {
+      @Override
       public void menuSelected(MenuEvent e)
       {
-        scoreMatrix_menuSelected();
+        scoreModel_menuSelected();
       }
 
+      @Override
       public void menuDeselected(MenuEvent e)
       {
       }
 
+      @Override
       public void menuCanceled(MenuEvent e)
       {
       }
@@ -259,23 +247,27 @@ public class GPCAPanel extends JInternalFrame
     showLabels.setText(MessageManager.getString("label.show_labels"));
     showLabels.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         showLabels_actionPerformed(e);
       }
     });
-    print.setText(MessageManager.getString("action.print"));
+    JMenuItem bgcolour = new JMenuItem();
     bgcolour.setText(MessageManager.getString("action.background_colour"));
     bgcolour.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         bgcolour_actionPerformed(e);
       }
     });
+    JMenuItem originalSeqData = new JMenuItem();
     originalSeqData.setText(MessageManager.getString("label.input_data"));
     originalSeqData.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         originalSeqData_actionPerformed(e);
@@ -305,22 +297,12 @@ public class GPCAPanel extends JInternalFrame
         protSetting_actionPerfomed(arg0);
       }
     });
-    jvVersionSetting.setText(MessageManager
-            .getString("label.jalview_pca_calculation"));
-    jvVersionSetting.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent arg0)
-      {
-        jvVersionSetting_actionPerfomed(arg0);
-      }
-    });
-    calcSettings.add(jvVersionSetting);
+
     calcSettings.add(nuclSetting);
     calcSettings.add(protSetting);
-    calcSettings.add(scoreMatrixMenu);
-    statusPanel.setLayout(statusPanelLayout);
-    statusBar.setFont(new java.awt.Font("Verdana", 0, 12));
+    calcSettings.add(scoreModelMenu);
+    statusPanel.setLayout(new GridLayout());
+    statusBar.setFont(VERDANA_12);
     // statusPanel.setBackground(Color.lightGray);
     // statusBar.setBackground(Color.lightGray);
     // statusPanel.add(statusBar, null);
@@ -335,9 +317,12 @@ public class GPCAPanel extends JInternalFrame
     jPanel2.add(jLabel3, null);
     jPanel2.add(zCombobox, null);
     jPanel2.add(resetButton, null);
+
+    JMenuBar jMenuBar1 = new JMenuBar();
     jMenuBar1.add(fileMenu);
     jMenuBar1.add(viewMenu);
     jMenuBar1.add(calcSettings);
+    setJMenuBar(jMenuBar1);
     fileMenu.add(saveMenu);
     fileMenu.add(outputValues);
     fileMenu.add(print);
@@ -351,7 +336,7 @@ public class GPCAPanel extends JInternalFrame
     viewMenu.add(associateViewsMenu);
   }
 
-  protected void scoreMatrix_menuSelected()
+  protected void scoreModel_menuSelected()
   {
     // TODO Auto-generated method stub
 
@@ -438,10 +423,4 @@ public class GPCAPanel extends JInternalFrame
   {
 
   }
-
-  protected void jvVersionSetting_actionPerfomed(ActionEvent arg0)
-  {
-    // TODO Auto-generated method stub
-
-  }
 }
index 90053f5..1ad95dd 100755 (executable)
@@ -45,6 +45,7 @@ import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 
+import javax.swing.AbstractCellEditor;
 import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.DefaultListCellRenderer;
@@ -53,11 +54,11 @@ import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JFileChooser;
 import javax.swing.JLabel;
-import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
 import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
+import javax.swing.JTable;
 import javax.swing.JTextField;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingConstants;
@@ -67,8 +68,8 @@ import javax.swing.border.EtchedBorder;
 import javax.swing.border.TitledBorder;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
 
 /**
  * Base class for the Preferences panel.
@@ -100,6 +101,8 @@ public class GPreferences extends JPanel
 
   protected JComboBox<String> fontNameCB = new JComboBox<String>();
 
+  protected JCheckBox showOccupancy = new JCheckBox();
+
   protected JCheckBox showUnconserved = new JCheckBox();
 
   protected JCheckBox idItalics = new JCheckBox();
@@ -180,7 +183,21 @@ public class GPreferences extends JPanel
   /*
    * Connections tab components
    */
-  protected JList linkURLList = new JList();
+  protected JTable linkUrlTable = new JTable();
+
+  protected JButton editLink = new JButton();
+
+  protected JButton deleteLink = new JButton();
+
+  protected JTextField filterTB = new JTextField();
+
+  protected JButton doReset = new JButton();
+
+  protected JButton userOnly = new JButton();
+
+  protected JLabel portLabel = new JLabel();
+
+  protected JLabel serverLabel = new JLabel();
 
   protected JTextField proxyServerTB = new JTextField();
 
@@ -188,8 +205,6 @@ public class GPreferences extends JPanel
 
   protected JTextField defaultBrowser = new JTextField();
 
-  protected JList linkNameList = new JList();
-
   protected JCheckBox useProxy = new JCheckBox();
 
   protected JCheckBox usagestats = new JCheckBox();
@@ -285,6 +300,9 @@ public class GPreferences extends JPanel
     tabbedPane.add(initConnectionsTab(),
             MessageManager.getString("label.connections"));
 
+    tabbedPane.add(initLinksTab(),
+            MessageManager.getString("label.urllinks"));
+
     tabbedPane.add(initOutputTab(),
             MessageManager.getString("label.output"));
 
@@ -483,40 +501,196 @@ public class GPreferences extends JPanel
   {
     JPanel connectTab = new JPanel();
     connectTab.setLayout(new GridBagLayout());
-    JLabel serverLabel = new JLabel();
-    serverLabel.setText(MessageManager.getString("label.address"));
-    serverLabel.setHorizontalAlignment(SwingConstants.RIGHT);
-    serverLabel.setFont(LABEL_FONT);
-    proxyServerTB.setFont(LABEL_FONT);
-    proxyPortTB.setFont(LABEL_FONT);
-    JLabel portLabel = new JLabel();
-    portLabel.setFont(LABEL_FONT);
-    portLabel.setHorizontalAlignment(SwingConstants.RIGHT);
-    portLabel.setText(MessageManager.getString("label.port"));
+
+    // Label for browser text box
     JLabel browserLabel = new JLabel();
-    browserLabel.setFont(new java.awt.Font("SansSerif", 0, 11));
+    browserLabel.setFont(LABEL_FONT);
     browserLabel.setHorizontalAlignment(SwingConstants.TRAILING);
     browserLabel.setText(MessageManager
             .getString("label.default_browser_unix"));
     defaultBrowser.setFont(LABEL_FONT);
     defaultBrowser.setText("");
-    usagestats.setText(MessageManager
-            .getString("label.send_usage_statistics"));
-    usagestats.setFont(LABEL_FONT);
-    usagestats.setHorizontalAlignment(SwingConstants.RIGHT);
-    usagestats.setHorizontalTextPosition(SwingConstants.LEADING);
-    questionnaire.setText(MessageManager
-            .getString("label.check_for_questionnaires"));
-    questionnaire.setFont(LABEL_FONT);
-    questionnaire.setHorizontalAlignment(SwingConstants.RIGHT);
-    questionnaire.setHorizontalTextPosition(SwingConstants.LEADING);
-    versioncheck.setText(MessageManager
-            .getString("label.check_for_latest_version"));
-    versioncheck.setFont(LABEL_FONT);
-    versioncheck.setHorizontalAlignment(SwingConstants.RIGHT);
-    versioncheck.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    defaultBrowser.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mouseClicked(MouseEvent e)
+      {
+        if (e.getClickCount() > 1)
+        {
+          defaultBrowser_mouseClicked(e);
+        }
+      }
+    });
+
+    JPanel proxyPanel = initConnTabProxyPanel();
+    initConnTabCheckboxes();
+
+    // Add default Browser text box
+    connectTab.add(browserLabel, new GridBagConstraints(0, 0, 1, 1, 0.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
+            new Insets(10, 0, 5, 5), 5, 1));
+    defaultBrowser.setFont(LABEL_FONT);
+    defaultBrowser.setText("");
+
+    connectTab.add(defaultBrowser, new GridBagConstraints(1, 0, 1, 1, 1.0,
+            0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
+            new Insets(10, 0, 5, 10), 30, 1));
+
+    // Add proxy server panel
+    connectTab.add(proxyPanel, new GridBagConstraints(0, 1, 2, 1, 1.0, 0.0,
+            GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL,
+            new Insets(10, 0, 5, 12), 4, 10));
+
+    // Add usage stats, version check and questionnaire checkboxes
+    connectTab.add(usagestats, new GridBagConstraints(0, 2, 1, 1, 1.0, 0.0,
+            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 5, 5), 70, 1));
+    connectTab.add(questionnaire, new GridBagConstraints(1, 2, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 5, 10), 70, 1));
+    connectTab.add(versioncheck, new GridBagConstraints(0, 3, 1, 1, 1.0,
+            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
+            new Insets(0, 2, 5, 5), 70, 1));
+
+    // Add padding so the panel doesn't look ridiculous
+    JPanel spacePanel = new JPanel();
+    connectTab.add(spacePanel, new GridBagConstraints(0, 4, 1, 1, 1.0, 1.0,
+            GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0,
+                    0, 0, 5), 70, 1));
+
+    return connectTab;
+  }
+
+  /**
+   * Initialises the Links tabbed panel.
+   * 
+   * @return
+   */
+  private JPanel initLinksTab()
+  {
+    JPanel linkTab = new JPanel();
+    linkTab.setLayout(new GridBagLayout());
+
+    // Set up table for Url links
+    linkUrlTable.setFillsViewportHeight(true);
+    linkUrlTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
+    linkUrlTable.setAutoCreateRowSorter(true);
+    linkUrlTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+    // adjust row height so radio buttons actually fit
+    // don't do this in the renderer, it causes the awt thread to activate
+    // constantly
+    JRadioButton temp = new JRadioButton();
+    linkUrlTable.setRowHeight(temp.getMinimumSize().height);
+
+    // Table in scrollpane so that the table is given a scrollbar
+    JScrollPane linkScrollPane = new JScrollPane(linkUrlTable);
+    linkScrollPane.setBorder(null);
+
+    // Panel for links functionality
+    JPanel linkPanel = new JPanel(new GridBagLayout());
+    linkPanel.setBorder(new TitledBorder(MessageManager
+            .getString("label.url_linkfrom_sequence_id")));
+
+    // Put the Url links panel together
+
+    // Buttons go at top right, resizing only resizes the blank space vertically
+    JPanel buttonPanel = initLinkTabUrlButtons();
+    GridBagConstraints linkConstraints1 = new GridBagConstraints();
+    linkConstraints1.insets = new Insets(0, 0, 5, 0);
+    linkConstraints1.gridx = 0;
+    linkConstraints1.gridy = 0;
+    linkConstraints1.weightx = 1.0;
+    linkConstraints1.fill = GridBagConstraints.HORIZONTAL;
+    linkTab.add(buttonPanel, linkConstraints1);
+
+    // Links table goes at top left, resizing resizes the table
+    GridBagConstraints linkConstraints2 = new GridBagConstraints();
+    linkConstraints2.insets = new Insets(0, 0, 5, 5);
+    linkConstraints2.gridx = 0;
+    linkConstraints2.gridy = 1;
+    linkConstraints2.weightx = 1.0;
+    linkConstraints2.weighty = 1.0;
+    linkConstraints2.fill = GridBagConstraints.BOTH;
+    linkTab.add(linkScrollPane, linkConstraints2);
+
+    // Filter box and buttons goes at bottom left, resizing resizes the text box
+    JPanel filterPanel = initLinkTabFilterPanel();
+    GridBagConstraints linkConstraints3 = new GridBagConstraints();
+    linkConstraints3.insets = new Insets(0, 0, 0, 5);
+    linkConstraints3.gridx = 0;
+    linkConstraints3.gridy = 2;
+    linkConstraints3.weightx = 1.0;
+    linkConstraints3.fill = GridBagConstraints.HORIZONTAL;
+    linkTab.add(filterPanel, linkConstraints3);
+
+    return linkTab;
+  }
+
+  private JPanel initLinkTabFilterPanel()
+  {
+    // Filter textbox and reset button
+    JLabel filterLabel = new JLabel(
+            MessageManager.getString("label.filter"));
+    filterLabel.setFont(LABEL_FONT);
+    filterLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+    filterLabel.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    filterTB.setFont(LABEL_FONT);
+    filterTB.setText("");
+
+    doReset.setText(MessageManager.getString("action.showall"));
+    userOnly.setText(MessageManager.getString("action.customfilter"));
+
+    // Panel for filter functionality
+    JPanel filterPanel = new JPanel(new GridBagLayout());
+    filterPanel.setBorder(new TitledBorder("Filter"));
+    GridBagConstraints gbc = new GridBagConstraints();
+    gbc.gridx = 0;
+    gbc.gridy = 0;
+    gbc.fill = GridBagConstraints.NONE;
+    gbc.anchor = GridBagConstraints.WEST;
+
+    filterPanel.add(filterLabel, gbc);
+
+    GridBagConstraints gbc1 = new GridBagConstraints();
+    gbc1.gridx = 1;
+    gbc1.gridwidth = 2;
+    gbc1.fill = GridBagConstraints.HORIZONTAL;
+    gbc1.anchor = GridBagConstraints.WEST;
+    gbc1.weightx = 1.0;
+    filterPanel.add(filterTB, gbc1);
+
+    GridBagConstraints gbc2 = new GridBagConstraints();
+    gbc2.gridx = 3;
+    gbc2.fill = GridBagConstraints.NONE;
+    gbc2.anchor = GridBagConstraints.WEST;
+    filterPanel.add(doReset, gbc2);
+
+    GridBagConstraints gbc3 = new GridBagConstraints();
+    gbc3.gridx = 4;
+    gbc3.fill = GridBagConstraints.NONE;
+    gbc3.anchor = GridBagConstraints.WEST;
+    filterPanel.add(userOnly, gbc3);
+
+    return filterPanel;
+  }
+
+  private JPanel initLinkTabUrlButtons()
+  {
+    // Buttons for new / edit / delete Url links
     JButton newLink = new JButton();
     newLink.setText(MessageManager.getString("action.new"));
+
+    editLink.setText(MessageManager.getString("action.edit"));
+
+    deleteLink.setText(MessageManager.getString("action.delete"));
+
+    // no current selection, so initially disable delete/edit buttons
+    editLink.setEnabled(false);
+    deleteLink.setEnabled(false);
+    
     newLink.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
@@ -525,7 +699,7 @@ public class GPreferences extends JPanel
         newLink_actionPerformed(e);
       }
     });
-    JButton editLink = new JButton();
+
     editLink.setText(MessageManager.getString("action.edit"));
     editLink.addActionListener(new java.awt.event.ActionListener()
     {
@@ -535,7 +709,7 @@ public class GPreferences extends JPanel
         editLink_actionPerformed(e);
       }
     });
-    JButton deleteLink = new JButton();
+
     deleteLink.setText(MessageManager.getString("action.delete"));
     deleteLink.addActionListener(new java.awt.event.ActionListener()
     {
@@ -546,55 +720,60 @@ public class GPreferences extends JPanel
       }
     });
 
-    linkURLList.addListSelectionListener(new ListSelectionListener()
-    {
-      @Override
-      public void valueChanged(ListSelectionEvent e)
-      {
-        int index = linkURLList.getSelectedIndex();
-        linkNameList.setSelectedIndex(index);
-      }
-    });
+    JPanel buttonPanel = new JPanel(new GridBagLayout());
+    buttonPanel.setBorder(new TitledBorder("Edit links"));
+    GridBagConstraints gbc = new GridBagConstraints();
+    gbc.gridx = 0;
+    gbc.gridy = 0;
+    gbc.fill = GridBagConstraints.NONE;
+    buttonPanel.add(newLink, gbc);
+
+    GridBagConstraints gbc1 = new GridBagConstraints();
+    gbc1.gridx = 1;
+    gbc1.gridy = 0;
+    gbc1.fill = GridBagConstraints.NONE;
+    buttonPanel.add(editLink, gbc1);
+
+    GridBagConstraints gbc2 = new GridBagConstraints();
+    gbc2.gridx = 2;
+    gbc2.gridy = 0;
+    gbc2.fill = GridBagConstraints.NONE;
+    buttonPanel.add(deleteLink, gbc2);
+
+    GridBagConstraints gbc3 = new GridBagConstraints();
+    gbc3.gridx = 3;
+    gbc3.gridy = 0;
+    gbc3.fill = GridBagConstraints.HORIZONTAL;
+    gbc3.weightx = 1.0;
+    JPanel spacePanel = new JPanel();
+    spacePanel.setBorder(null);
+    buttonPanel.add(spacePanel, gbc3);
+
+    return buttonPanel;
+  }
 
-    linkNameList.addListSelectionListener(new ListSelectionListener()
-    {
-      @Override
-      public void valueChanged(ListSelectionEvent e)
-      {
-        int index = linkNameList.getSelectedIndex();
-        linkURLList.setSelectedIndex(index);
-      }
-    });
+  /**
+   * Initialises the proxy server panel in the Connections tab
+   * 
+   * @return the proxy server panel
+   */
+  private JPanel initConnTabProxyPanel()
+  {
+    // Label for server text box
+    serverLabel.setText(MessageManager.getString("label.address"));
+    serverLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+    serverLabel.setFont(LABEL_FONT);
 
-    JScrollPane linkScrollPane = new JScrollPane();
-    linkScrollPane.setBorder(null);
-    JPanel linkPanel = new JPanel();
-    linkPanel.setBorder(new TitledBorder(MessageManager
-            .getString("label.url_linkfrom_sequence_id")));
-    linkPanel.setLayout(new BorderLayout());
-    GridLayout gridLayout1 = new GridLayout();
-    JPanel editLinkButtons = new JPanel();
-    editLinkButtons.setLayout(gridLayout1);
-    gridLayout1.setRows(3);
-    linkNameList.setFont(LABEL_FONT);
-    linkNameList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-    BorderLayout borderLayout3 = new BorderLayout();
-    JPanel linkPanel2 = new JPanel();
-    linkPanel2.setLayout(borderLayout3);
-    linkURLList.setFont(LABEL_FONT);
-    linkURLList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+    // Proxy server and port text boxes
+    proxyServerTB.setFont(LABEL_FONT);
+    proxyPortTB.setFont(LABEL_FONT);
 
-    defaultBrowser.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mouseClicked(MouseEvent e)
-      {
-        if (e.getClickCount() > 1)
-        {
-          defaultBrowser_mouseClicked(e);
-        }
-      }
-    });
+    // Label for Port text box
+    portLabel.setFont(LABEL_FONT);
+    portLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+    portLabel.setText(MessageManager.getString("label.port"));
+
+    // Use proxy server checkbox
     useProxy.setFont(LABEL_FONT);
     useProxy.setHorizontalAlignment(SwingConstants.RIGHT);
     useProxy.setHorizontalTextPosition(SwingConstants.LEADING);
@@ -607,56 +786,57 @@ public class GPreferences extends JPanel
         useProxy_actionPerformed();
       }
     });
-    linkPanel.add(editLinkButtons, BorderLayout.EAST);
-    editLinkButtons.add(newLink, null);
-    editLinkButtons.add(editLink, null);
-    editLinkButtons.add(deleteLink, null);
-    linkPanel.add(linkScrollPane, BorderLayout.CENTER);
-    linkScrollPane.getViewport().add(linkPanel2, null);
-    linkPanel2.add(linkURLList, BorderLayout.CENTER);
-    linkPanel2.add(linkNameList, BorderLayout.WEST);
-    JPanel jPanel1 = new JPanel();
+
+    // Make proxy server panel
+    JPanel proxyPanel = new JPanel();
     TitledBorder titledBorder1 = new TitledBorder(
             MessageManager.getString("label.proxy_server"));
-    jPanel1.setBorder(titledBorder1);
-    jPanel1.setLayout(new GridBagLayout());
-    jPanel1.add(serverLabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
-                    2, 4, 0), 5, 0));
-    jPanel1.add(portLabel, new GridBagConstraints(2, 1, 1, 1, 0.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
-                    0, 4, 0), 11, 6));
-    connectTab.add(linkPanel, new GridBagConstraints(0, 0, 2, 1, 1.0, 1.0,
-            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
-                    16, 0, 0, 12), 359, -17));
-    connectTab.add(jPanel1, new GridBagConstraints(0, 2, 2, 1, 1.0, 1.0,
-            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
-                    21, 0, 35, 12), 4, 6));
-    connectTab.add(browserLabel, new GridBagConstraints(0, 1, 1, 1, 0.0,
+    proxyPanel.setBorder(titledBorder1);
+    proxyPanel.setLayout(new GridBagLayout());
+    proxyPanel.add(serverLabel, new GridBagConstraints(0, 1, 1, 1, 0.0,
             0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
-            new Insets(16, 0, 0, 0), 5, 1));
-    jPanel1.add(useProxy, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0,
+            new Insets(0, 2, 2, 0), 5, 0));
+    proxyPanel.add(portLabel, new GridBagConstraints(2, 1, 1, 1, 0.0, 0.0,
+            GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
+                    0, 2, 0), 11, 0));
+    proxyPanel.add(useProxy, new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0,
             GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,
                     2, 5, 185), 2, -4));
-    jPanel1.add(proxyPortTB, new GridBagConstraints(3, 1, 1, 1, 1.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 54, 1));
-    jPanel1.add(proxyServerTB, new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 0), 263, 1));
-    connectTab.add(defaultBrowser, new GridBagConstraints(1, 1, 1, 1, 1.0,
+    proxyPanel.add(proxyPortTB, new GridBagConstraints(3, 1, 1, 1, 1.0,
             0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(15, 0, 0, 15), 307, 1));
-    connectTab.add(usagestats, new GridBagConstraints(0, 4, 1, 1, 1.0, 0.0,
-            GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 70, 1));
-    connectTab.add(questionnaire, new GridBagConstraints(1, 4, 1, 1, 1.0,
+            new Insets(0, 2, 2, 2), 54, 1));
+    proxyPanel.add(proxyServerTB, new GridBagConstraints(1, 1, 1, 1, 1.0,
             0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 70, 1));
-    connectTab.add(versioncheck, new GridBagConstraints(0, 5, 1, 1, 1.0,
-            0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
-            new Insets(0, 2, 4, 2), 70, 1));
-    return connectTab;
+            new Insets(0, 2, 2, 0), 263, 1));
+
+    return proxyPanel;
+  }
+
+  /**
+   * Initialises the checkboxes in the Connections tab
+   */
+  private void initConnTabCheckboxes()
+  {
+    // Usage stats checkbox label
+    usagestats.setText(MessageManager
+            .getString("label.send_usage_statistics"));
+    usagestats.setFont(LABEL_FONT);
+    usagestats.setHorizontalAlignment(SwingConstants.RIGHT);
+    usagestats.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    // Questionnaire checkbox label
+    questionnaire.setText(MessageManager
+            .getString("label.check_for_questionnaires"));
+    questionnaire.setFont(LABEL_FONT);
+    questionnaire.setHorizontalAlignment(SwingConstants.RIGHT);
+    questionnaire.setHorizontalTextPosition(SwingConstants.LEADING);
+
+    // Check for latest version checkbox label
+    versioncheck.setText(MessageManager
+            .getString("label.check_for_latest_version"));
+    versioncheck.setFont(LABEL_FONT);
+    versioncheck.setHorizontalAlignment(SwingConstants.RIGHT);
+    versioncheck.setHorizontalTextPosition(SwingConstants.LEADING);
   }
 
   /**
@@ -996,6 +1176,13 @@ public class GPreferences extends JPanel
     identity.setHorizontalTextPosition(SwingConstants.LEFT);
     identity.setSelected(true);
     identity.setText(MessageManager.getString("label.consensus"));
+    showOccupancy.setFont(LABEL_FONT);
+    showOccupancy.setEnabled(false);
+    showOccupancy.setHorizontalAlignment(SwingConstants.RIGHT);
+    showOccupancy.setHorizontalTextPosition(SwingConstants.LEFT);
+    showOccupancy.setSelected(true);
+    showOccupancy.setText(MessageManager.getString("label.occupancy"));
+
     JLabel showGroupbits = new JLabel();
     showGroupbits.setFont(LABEL_FONT);
     showGroupbits.setHorizontalAlignment(SwingConstants.RIGHT);
@@ -1050,10 +1237,10 @@ public class GPreferences extends JPanel
             .getString("label.database_references"));
     annotations.setFont(LABEL_FONT);
     annotations.setHorizontalAlignment(SwingConstants.RIGHT);
-    annotations.setHorizontalTextPosition(SwingConstants.LEADING);
+    annotations.setHorizontalTextPosition(SwingConstants.LEFT);
     annotations.setSelected(true);
     annotations.setText(MessageManager.getString("label.show_annotations"));
-    annotations.setBounds(new Rectangle(169, 12, 200, 23));
+    // annotations.setBounds(new Rectangle(169, 12, 200, 23));
     annotations.addActionListener(new ActionListener()
     {
       @Override
@@ -1180,11 +1367,13 @@ public class GPreferences extends JPanel
     sortAutocalc.setBounds(new Rectangle(290, 285, 165, 21));
 
     JPanel annsettingsPanel = new JPanel();
-    annsettingsPanel.setBounds(new Rectangle(173, 34, 320, 75));
+    annsettingsPanel.setBounds(new Rectangle(173, 13, 320, 96));
     annsettingsPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
     annsettingsPanel.setBorder(new EtchedBorder());
     visualTab.add(annsettingsPanel);
     Border jb = new EmptyBorder(1, 1, 4, 5);
+    annotations.setBorder(jb);
+    showOccupancy.setBorder(jb);
     quality.setBorder(jb);
     conservation.setBorder(jb);
     identity.setBorder(jb);
@@ -1196,17 +1385,26 @@ public class GPreferences extends JPanel
     showConsensLogo.setBorder(jb);
 
     JPanel autoAnnotSettings = new JPanel();
-    autoAnnotSettings.setLayout(new GridLayout(3, 3));
     annsettingsPanel.add(autoAnnotSettings);
+    autoAnnotSettings.setLayout(new GridLayout(0, 2));
+    autoAnnotSettings.add(annotations);
     autoAnnotSettings.add(quality);
+    // second row of autoannotation box
+    autoAnnotSettings = new JPanel();
+    annsettingsPanel.add(autoAnnotSettings);
+
+    autoAnnotSettings.setLayout(new GridLayout(0, 3));
     autoAnnotSettings.add(conservation);
     autoAnnotSettings.add(identity);
+    autoAnnotSettings.add(showOccupancy);
     autoAnnotSettings.add(showGroupbits);
     autoAnnotSettings.add(showGroupConservation);
     autoAnnotSettings.add(showGroupConsensus);
     autoAnnotSettings.add(showConsensbits);
     autoAnnotSettings.add(showConsensHistogram);
     autoAnnotSettings.add(showConsensLogo);
+    
+    
 
     JPanel tooltipSettings = new JPanel();
     tooltipSettings.setBorder(new TitledBorder(MessageManager
@@ -1255,7 +1453,6 @@ public class GPreferences extends JPanel
     jPanel2.add(sortAnnLabel);
     jPanel2.add(startupCheckbox);
     visualTab.add(jPanel2);
-    visualTab.add(annotations);
     visualTab.add(startupFileTextfield);
     visualTab.add(sortby);
     visualTab.add(sortAnnBy);
@@ -1357,8 +1554,82 @@ public class GPreferences extends JPanel
 
   public void useProxy_actionPerformed()
   {
-    proxyServerTB.setEnabled(useProxy.isSelected());
-    proxyPortTB.setEnabled(useProxy.isSelected());
+    boolean enabled = useProxy.isSelected();
+    portLabel.setEnabled(enabled);
+    serverLabel.setEnabled(enabled);
+    proxyServerTB.setEnabled(enabled);
+    proxyPortTB.setEnabled(enabled);
   }
 
+  /**
+   * Customer renderer for JTable: supports column of radio buttons
+   */
+  public class RadioButtonRenderer extends JRadioButton implements
+          TableCellRenderer
+  {
+    public RadioButtonRenderer()
+    {
+      setHorizontalAlignment(CENTER);
+      setToolTipText(MessageManager.getString("label.urltooltip"));
+    }
+
+    @Override
+    public Component getTableCellRendererComponent(JTable table,
+            Object value, boolean isSelected, boolean hasFocus, int row,
+            int column)
+    {
+      setSelected((boolean) value);
+
+      // set colours to match rest of table
+      if (isSelected)
+       {
+         setBackground(table.getSelectionBackground());
+         setForeground(table.getSelectionForeground());
+       }
+       else
+       {
+         setBackground(table.getBackground());
+         setForeground(table.getForeground());
+      }
+      return this;
+    }
+  }
+
+  /**
+   * Customer cell editor for JTable: supports column of radio buttons in
+   * conjunction with renderer
+   */
+  public class RadioButtonEditor extends AbstractCellEditor implements
+            TableCellEditor
+    {
+      private JRadioButton button = new JRadioButton();
+
+      public RadioButtonEditor()
+      {
+      button.setHorizontalAlignment(SwingConstants.CENTER);
+      this.button.addActionListener(new ActionListener()
+        {
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
+            fireEditingStopped();
+          }
+        });
+      }
+
+      @Override
+      public Component getTableCellEditorComponent(JTable table,
+              Object value, boolean isSelected, int row, int column)
+      {
+      button.setSelected((boolean) value);
+        return button;
+      }
+
+      @Override
+      public Object getCellEditorValue()
+      {
+      return button.isSelected();
+      }
+
+  }
 }
index dbce5f3..ab3ea2c 100755 (executable)
@@ -29,19 +29,48 @@ import java.awt.Font;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
-import java.awt.Panel;
 import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.event.KeyAdapter;
 import java.awt.event.KeyEvent;
 
 import javax.swing.BorderFactory;
+import javax.swing.JButton;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
 import javax.swing.SwingConstants;
 
-public class GSequenceLink extends Panel
+public class GSequenceLink extends JPanel
 {
+
+  JTextField nameTB = new JTextField();
+
+  JTextField urlTB = new JTextField();
+
+  JButton insertSeq = new JButton();
+
+  JButton insertDBAcc = new JButton();
+
+  JLabel insert = new JLabel();
+
+  JLabel jLabel1 = new JLabel();
+
+  JLabel jLabel2 = new JLabel();
+
+  JLabel jLabel3 = new JLabel();
+
+  JLabel jLabel4 = new JLabel();
+
+  JLabel jLabel5 = new JLabel();
+
+  JLabel jLabel6 = new JLabel();
+
+  JPanel jPanel1 = new JPanel();
+
+  GridBagLayout gridBagLayout1 = new GridBagLayout();
+
   public GSequenceLink()
   {
     try
@@ -77,23 +106,53 @@ public class GSequenceLink extends Panel
         urlTB_keyTyped(e);
       }
     });
+
+    insertSeq.setLocation(77, 75);
+    insertSeq.setSize(141, 24);
+    insertSeq.setText(MessageManager.getString("action.seq_id"));
+    insertSeq.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        insertSeq_action(e);
+      }
+    });
+
+    insertDBAcc.setLocation(210, 75);
+    insertDBAcc.setSize(141, 24);
+    insertDBAcc.setText(MessageManager.getString("action.db_acc"));
+    insertDBAcc.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        insertDBAcc_action(e);
+      }
+    });
+
+    insert.setText(MessageManager.getString("label.insert"));
+    insert.setFont(JvSwingUtils.getLabelFont());
+    insert.setHorizontalAlignment(SwingConstants.RIGHT);
+    insert.setBounds(17, 78, 58, 16);
+
     jLabel1.setFont(JvSwingUtils.getLabelFont());
     jLabel1.setHorizontalAlignment(SwingConstants.TRAILING);
     jLabel1.setText(MessageManager.getString("label.link_name"));
     jLabel1.setBounds(new Rectangle(4, 10, 71, 24));
     jLabel2.setFont(JvSwingUtils.getLabelFont());
     jLabel2.setHorizontalAlignment(SwingConstants.TRAILING);
-    jLabel2.setText(MessageManager.getString("label.url"));
+    jLabel2.setText(MessageManager.getString("label.url:"));
     jLabel2.setBounds(new Rectangle(17, 37, 54, 27));
     jLabel3.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
     jLabel3.setText(MessageManager.getString("label.use_sequence_id_1"));
-    jLabel3.setBounds(new Rectangle(21, 72, 351, 15));
+    jLabel3.setBounds(new Rectangle(21, 102, 351, 15));
     jLabel4.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
     jLabel4.setText(MessageManager.getString("label.use_sequence_id_2"));
-    jLabel4.setBounds(new Rectangle(21, 88, 351, 15));
+    jLabel4.setBounds(new Rectangle(21, 118, 351, 15));
     jLabel5.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
     jLabel5.setText(MessageManager.getString("label.use_sequence_id_3"));
-    jLabel5.setBounds(new Rectangle(21, 106, 351, 15));
+    jLabel5.setBounds(new Rectangle(21, 136, 351, 15));
 
     String lastLabel = MessageManager.getString("label.use_sequence_id_4");
     if (lastLabel.length() > 0)
@@ -101,7 +160,7 @@ public class GSequenceLink extends Panel
       // e.g. Spanish version has longer text
       jLabel6.setFont(new java.awt.Font("Verdana", Font.ITALIC, 11));
       jLabel6.setText(lastLabel);
-      jLabel6.setBounds(new Rectangle(21, 122, 351, 15));
+      jLabel6.setBounds(new Rectangle(21, 152, 351, 15));
     }
 
     jPanel1.setBorder(BorderFactory.createEtchedBorder());
@@ -109,16 +168,19 @@ public class GSequenceLink extends Panel
     jPanel1.add(jLabel1);
     jPanel1.add(nameTB);
     jPanel1.add(urlTB);
+    jPanel1.add(insertSeq);
+    jPanel1.add(insertDBAcc);
+    jPanel1.add(insert);
     jPanel1.add(jLabel2);
     jPanel1.add(jLabel3);
     jPanel1.add(jLabel4);
     jPanel1.add(jLabel5);
 
-    int height = 130;
+    int height = 160;
     if (lastLabel.length() > 0)
     {
       jPanel1.add(jLabel6);
-      height = 146;
+      height = 176;
     }
 
     this.add(jPanel1, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
@@ -163,25 +225,13 @@ public class GSequenceLink extends Panel
     return false;
   }
 
-  JTextField nameTB = new JTextField();
-
-  JTextField urlTB = new JTextField();
-
-  JLabel jLabel1 = new JLabel();
-
-  JLabel jLabel2 = new JLabel();
-
-  JLabel jLabel3 = new JLabel();
-
-  JLabel jLabel4 = new JLabel();
-
-  JLabel jLabel5 = new JLabel();
-
-  JLabel jLabel6 = new JLabel();
-
-  JPanel jPanel1 = new JPanel();
-
-  GridBagLayout gridBagLayout1 = new GridBagLayout();
+  public void notifyDuplicate()
+  {
+    JvOptionPane.showInternalMessageDialog(jalview.gui.Desktop.desktop,
+            MessageManager.getString("warn.name_cannot_be_duplicate"),
+            MessageManager.getString("label.invalid_name"),
+            JvOptionPane.WARNING_MESSAGE);
+  }
 
   public void nameTB_keyTyped(KeyEvent e)
   {
@@ -200,4 +250,23 @@ public class GSequenceLink extends Panel
     // }
 
   }
+
+  public void insertSeq_action(ActionEvent e)
+  {
+    insertIntoUrl(insertSeq.getText());
+  }
+
+  public void insertDBAcc_action(ActionEvent e)
+  {
+    insertIntoUrl(insertDBAcc.getText());
+  }
+
+  private void insertIntoUrl(String insertion)
+  {
+    int pos = urlTB.getCaretPosition();
+    String text = urlTB.getText();
+    String newText = text.substring(0, pos) + insertion
+            + text.substring(pos);
+    urlTB.setText(newText);
+  }
 }
index 1362007..0c79c6c 100755 (executable)
@@ -28,6 +28,9 @@ 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.MouseAdapter;
 import java.awt.event.MouseEvent;
 
@@ -105,6 +108,7 @@ public class GSliderPanel extends JPanel
     slider.setDoubleBuffered(true);
     slider.addMouseListener(new MouseAdapter()
     {
+      @Override
       public void mouseReleased(MouseEvent e)
       {
         slider_mouseReleased(e);
@@ -115,11 +119,20 @@ public class GSliderPanel extends JPanel
     valueField.setPreferredSize(new Dimension(50, 12));
     valueField.setText("");
     valueField.setHorizontalAlignment(SwingConstants.CENTER);
-    valueField.addActionListener(new java.awt.event.ActionListener()
+    valueField.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        valueField_actionPerformed(e);
+        valueField_actionPerformed();
+      }
+    });
+    valueField.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        valueField_actionPerformed();
       }
     });
     label.setFont(new java.awt.Font("Verdana", 0, 11));
@@ -134,6 +147,7 @@ public class GSliderPanel extends JPanel
     applyButton.setText(MessageManager.getString("action.apply"));
     applyButton.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         applyButton_actionPerformed(e);
@@ -145,6 +159,7 @@ public class GSliderPanel extends JPanel
     undoButton.setText(MessageManager.getString("action.undo"));
     undoButton.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         undoButton_actionPerformed(e);
@@ -157,6 +172,7 @@ public class GSliderPanel extends JPanel
             .getString("action.apply_all_groups"));
     allGroupsCheck.addActionListener(new java.awt.event.ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
         allGroupsCheck_actionPerformed(e);
@@ -180,13 +196,18 @@ public class GSliderPanel extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
+   * Action on changing the slider text field value
    */
-  protected void valueField_actionPerformed(ActionEvent e)
+  protected void valueField_actionPerformed()
   {
+    try
+    {
+      int i = Integer.valueOf(valueField.getText());
+      slider.setValue(i);
+    } catch (NumberFormatException ex)
+    {
+      valueField.setText(String.valueOf(slider.getValue()));
+    }
   }
 
   /**
index 3a064d2..041fefd 100644 (file)
@@ -33,6 +33,7 @@ import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
 import java.awt.CardLayout;
+import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
 import java.awt.GridLayout;
@@ -54,11 +55,14 @@ import javax.swing.JComboBox;
 import javax.swing.JFrame;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
+import javax.swing.JList;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
 import javax.swing.JTabbedPane;
 import javax.swing.JTable;
 import javax.swing.JTextField;
+import javax.swing.ListCellRenderer;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
 import javax.swing.event.DocumentEvent;
@@ -488,6 +492,19 @@ public abstract class GStructureChooser extends JPanel implements
     });
 
     cmb_filterOption.addItemListener(this);
+
+    // add CustomComboSeparatorsRenderer to filter option combo-box
+    cmb_filterOption.setRenderer(new CustomComboSeparatorsRenderer(
+            (ListCellRenderer<Object>) cmb_filterOption.getRenderer())
+    {
+      @Override
+      protected boolean addSeparatorAfter(JList list, FilterOption value,
+              int index)
+      {
+        return value.isAddSeparatorAfter();
+      }
+    });
+
     chk_invertFilter.addItemListener(this);
 
     pnl_actions.add(chk_rememberSettings);
@@ -643,11 +660,28 @@ public abstract class GStructureChooser extends JPanel implements
 
     private String view;
 
-    public FilterOption(String name, String value, String view)
+    private boolean addSeparatorAfter;
+
+    /**
+     * Model for structure filter option
+     * 
+     * @param name
+     *          - the name of the Option
+     * @param value
+     *          - the value of the option
+     * @param view
+     *          - the category of the filter option
+     * @param addSeparatorAfter
+     *          - if true, a horizontal separator is rendered immediately after
+     *          this filter option, otherwise
+     */
+    public FilterOption(String name, String value, String view,
+            boolean addSeparatorAfter)
     {
       this.name = name;
       this.value = value;
       this.view = view;
+      this.addSeparatorAfter = addSeparatorAfter;
     }
 
     public String getName()
@@ -685,6 +719,16 @@ public abstract class GStructureChooser extends JPanel implements
     {
       return this.name;
     }
+
+    public boolean isAddSeparatorAfter()
+    {
+      return addSeparatorAfter;
+    }
+
+    public void setAddSeparatorAfter(boolean addSeparatorAfter)
+    {
+      this.addSeparatorAfter = addSeparatorAfter;
+    }
   }
 
   /**
@@ -800,6 +844,54 @@ public abstract class GStructureChooser extends JPanel implements
     return cmb_filterOption;
   }
 
+  /**
+   * Custom ListCellRenderer for adding a separator between different categories
+   * of structure chooser filter option drop-down.
+   * 
+   * @author tcnofoegbu
+   *
+   */
+  public abstract class CustomComboSeparatorsRenderer implements
+          ListCellRenderer<Object>
+  {
+    private ListCellRenderer<Object> regent;
+
+    private JPanel separatorPanel = new JPanel(new BorderLayout());
+
+    private JSeparator jSeparator = new JSeparator();
+
+    public CustomComboSeparatorsRenderer(ListCellRenderer<Object> listCellRenderer)
+    {
+      this.regent = listCellRenderer;
+    }
+
+    @Override
+    public Component getListCellRendererComponent(JList list,
+            Object value,
+            int index, boolean isSelected, boolean cellHasFocus)
+    {
+
+      Component comp = regent.getListCellRendererComponent(list, value,
+              index, isSelected, cellHasFocus);
+      if (index != -1
+              && addSeparatorAfter(list, (FilterOption) value, index))
+      { 
+        separatorPanel.removeAll();
+        separatorPanel.add(comp, BorderLayout.CENTER);
+        separatorPanel.add(jSeparator, BorderLayout.SOUTH);
+        return separatorPanel;
+      }
+      else
+      {
+        return comp;
+      }
+    }
+
+    protected abstract boolean addSeparatorAfter(JList list,
+            FilterOption value,
+            int index);
+  }
+
   protected abstract void stateChanged(ItemEvent e);
 
   protected abstract void ok_ActionPerformed();
@@ -816,4 +908,4 @@ public abstract class GStructureChooser extends JPanel implements
   public abstract void tabRefresh();
 
   public abstract void validateSelections();
-}
+}
\ No newline at end of file
index bd0f1de..d8f3f61 100644 (file)
 package jalview.jbgui;
 
 import jalview.api.structures.JalviewStructureDisplayI;
+import jalview.gui.ColourMenuHelper.ColourChangeListener;
 import jalview.util.MessageManager;
 
+import java.awt.BorderLayout;
 import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 
-import javax.swing.ButtonGroup;
 import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JMenu;
@@ -37,53 +38,37 @@ import javax.swing.JPanel;
 import javax.swing.JRadioButtonMenuItem;
 
 public abstract class GStructureViewer extends JInternalFrame implements
-        JalviewStructureDisplayI
+        JalviewStructureDisplayI, ColourChangeListener
 {
   // private AAStructureBindingModel bindingModel;
 
-  protected JMenu savemenu = new JMenu();
+  protected JMenu savemenu;
 
-  protected JMenu viewMenu = new JMenu();
+  protected JMenu viewMenu;
 
-  protected JMenu chainMenu = new JMenu();
+  protected JMenu colourMenu;
 
-  protected JMenu viewerActionMenu = new JMenu();
+  protected JMenu chainMenu;
 
-  protected JMenuItem alignStructs = new JMenuItem();
+  protected JMenu viewerActionMenu;
 
-  protected JMenuItem fitToWindow = new JMenuItem();
+  protected JMenuItem alignStructs;
 
-  protected JRadioButtonMenuItem seqColour = new JRadioButtonMenuItem();
+  protected JMenuItem fitToWindow;
 
-  protected JRadioButtonMenuItem chainColour = new JRadioButtonMenuItem();
+  protected JRadioButtonMenuItem seqColour;
 
-  protected JRadioButtonMenuItem chargeColour = new JRadioButtonMenuItem();
+  protected JRadioButtonMenuItem chainColour;
 
-  protected JRadioButtonMenuItem zappoColour = new JRadioButtonMenuItem();
+  protected JRadioButtonMenuItem chargeColour;
 
-  protected JRadioButtonMenuItem taylorColour = new JRadioButtonMenuItem();
+  protected JRadioButtonMenuItem viewerColour;
 
-  protected JRadioButtonMenuItem hydroColour = new JRadioButtonMenuItem();
+  protected JMenuItem helpItem;
 
-  protected JRadioButtonMenuItem strandColour = new JRadioButtonMenuItem();
+  protected JLabel statusBar;
 
-  protected JRadioButtonMenuItem helixColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem turnColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem buriedColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem purinePyrimidineColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem userColour = new JRadioButtonMenuItem();
-
-  protected JRadioButtonMenuItem viewerColour = new JRadioButtonMenuItem();
-
-  protected JMenuItem helpItem = new JMenuItem();
-
-  protected JLabel statusBar = new JLabel();
-
-  protected JPanel statusPanel = new JPanel();
+  protected JPanel statusPanel;
 
   /**
    * Constructor
@@ -107,6 +92,7 @@ public abstract class GStructureViewer extends JInternalFrame implements
     JMenu fileMenu = new JMenu();
     fileMenu.setText(MessageManager.getString("action.file"));
 
+    savemenu = new JMenu();
     savemenu.setActionCommand(MessageManager.getString("action.save_image"));
     savemenu.setText(MessageManager.getString("action.save_as"));
 
@@ -153,10 +139,14 @@ public abstract class GStructureViewer extends JInternalFrame implements
         viewMapping_actionPerformed(actionEvent);
       }
     });
+
+    viewMenu = new JMenu();
     viewMenu.setText(MessageManager.getString("action.view"));
 
+    chainMenu = new JMenu();
     chainMenu.setText(MessageManager.getString("action.show_chain"));
 
+    fitToWindow = new JMenuItem();
     fitToWindow.setText(MessageManager.getString("label.fit_to_window"));
     fitToWindow.addActionListener(new ActionListener()
     {
@@ -167,148 +157,9 @@ public abstract class GStructureViewer extends JInternalFrame implements
       }
     });
 
-    JMenu colourMenu = new JMenu();
-    colourMenu.setText(MessageManager.getString("label.colours"));
-
-    JMenuItem backGround = new JMenuItem();
-    backGround
-            .setText(MessageManager.getString("action.background_colour"));
-    backGround.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        backGround_actionPerformed(actionEvent);
-      }
-    });
-    seqColour.setSelected(false);
-    seqColour.setText(MessageManager.getString("action.by_sequence"));
-    seqColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        seqColour_actionPerformed(actionEvent);
-      }
-    });
-    chainColour.setText(MessageManager.getString("action.by_chain"));
-    chainColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        chainColour_actionPerformed(actionEvent);
-      }
-    });
-    chargeColour.setText(MessageManager.getString("label.charge_cysteine"));
-    chargeColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        chargeColour_actionPerformed(actionEvent);
-      }
-    });
-    zappoColour.setText(MessageManager.getString("label.zappo"));
-    zappoColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        zappoColour_actionPerformed(actionEvent);
-      }
-    });
-    taylorColour.setText(MessageManager.getString("label.taylor"));
-    taylorColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        taylorColour_actionPerformed(actionEvent);
-      }
-    });
-    hydroColour.setText(MessageManager.getString("label.hydrophobicity"));
-    hydroColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        hydroColour_actionPerformed(actionEvent);
-      }
-    });
-    strandColour.setText(MessageManager
-            .getString("label.strand_propensity"));
-    strandColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        strandColour_actionPerformed(actionEvent);
-      }
-    });
-    helixColour.setText(MessageManager.getString("label.helix_propensity"));
-    helixColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        helixColour_actionPerformed(actionEvent);
-      }
-    });
-    turnColour.setText(MessageManager.getString("label.turn_propensity"));
-    turnColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        turnColour_actionPerformed(actionEvent);
-      }
-    });
-    buriedColour.setText(MessageManager.getString("label.buried_index"));
-    buriedColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        buriedColour_actionPerformed(actionEvent);
-      }
-    });
-    purinePyrimidineColour.setText(MessageManager
-            .getString("label.purine_pyrimidine"));
-    purinePyrimidineColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        purinePyrimidineColour_actionPerformed(actionEvent);
-      }
-    });
-
-    userColour.setText(MessageManager.getString("action.user_defined"));
-    userColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        userColour_actionPerformed(actionEvent);
-      }
-    });
-    viewerColour.setSelected(false);
-    viewerColour
-            .setText(MessageManager.getString("label.colour_with_jmol"));
-    viewerColour.setToolTipText(MessageManager
-            .getString("label.let_jmol_manage_structure_colours"));
-    viewerColour.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        viewerColour_actionPerformed(actionEvent);
-      }
-    });
-
     JMenu helpMenu = new JMenu();
     helpMenu.setText(MessageManager.getString("action.help"));
+    helpItem = new JMenuItem();
     helpItem.setText(MessageManager.getString("label.jmol_help"));
     helpItem.addActionListener(new ActionListener()
     {
@@ -318,8 +169,9 @@ public abstract class GStructureViewer extends JInternalFrame implements
         showHelp_actionPerformed(actionEvent);
       }
     });
-    alignStructs
-            .setText(MessageManager.getString("label.align_structures"));
+    alignStructs = new JMenuItem();
+    alignStructs.setText(MessageManager
+            .getString("label.superpose_structures"));
     alignStructs.addActionListener(new ActionListener()
     {
       @Override
@@ -328,56 +180,30 @@ public abstract class GStructureViewer extends JInternalFrame implements
         alignStructs_actionPerformed(actionEvent);
       }
     });
-    viewerActionMenu.setText(MessageManager.getString("label.jmol"));
-    menuBar.add(fileMenu);
-    menuBar.add(viewMenu);
-    menuBar.add(colourMenu);
-    menuBar.add(viewerActionMenu);
+
+    viewerActionMenu = new JMenu(); // text set in sub-classes
     viewerActionMenu.setVisible(false);
-    menuBar.add(helpMenu);
+    viewerActionMenu.add(alignStructs);
+    colourMenu = new JMenu();
+    colourMenu.setText(MessageManager.getString("label.colours"));
     fileMenu.add(savemenu);
     fileMenu.add(viewMapping);
     savemenu.add(pdbFile);
     savemenu.add(png);
     savemenu.add(eps);
     viewMenu.add(chainMenu);
-
-    colourMenu.add(seqColour);
-    colourMenu.add(chainColour);
-    colourMenu.add(chargeColour);
-    colourMenu.add(zappoColour);
-    colourMenu.add(taylorColour);
-    colourMenu.add(hydroColour);
-    colourMenu.add(helixColour);
-    colourMenu.add(strandColour);
-    colourMenu.add(turnColour);
-    colourMenu.add(buriedColour);
-    colourMenu.add(purinePyrimidineColour);
-    colourMenu.add(userColour);
-    colourMenu.add(viewerColour);
-    colourMenu.add(backGround);
-
-    ButtonGroup colourButtons = new ButtonGroup();
-
-    colourButtons.add(seqColour);
-    colourButtons.add(chainColour);
-    colourButtons.add(chargeColour);
-    colourButtons.add(zappoColour);
-    colourButtons.add(taylorColour);
-    colourButtons.add(hydroColour);
-    colourButtons.add(helixColour);
-    colourButtons.add(strandColour);
-    colourButtons.add(turnColour);
-    colourButtons.add(buriedColour);
-    colourButtons.add(purinePyrimidineColour);
-    colourButtons.add(userColour);
-    colourButtons.add(viewerColour);
-
     helpMenu.add(helpItem);
-    viewerActionMenu.add(alignStructs);
 
+    menuBar.add(fileMenu);
+    menuBar.add(viewMenu);
+    menuBar.add(colourMenu);
+    menuBar.add(viewerActionMenu);
+    menuBar.add(helpMenu);
+
+    statusPanel = new JPanel();
     statusPanel.setLayout(new GridLayout());
-    this.getContentPane().add(statusPanel, java.awt.BorderLayout.SOUTH);
+    this.getContentPane().add(statusPanel, BorderLayout.SOUTH);
+    statusBar = new JLabel();
     statusPanel.add(statusBar, null);
   }
 
@@ -393,9 +219,8 @@ public abstract class GStructureViewer extends JInternalFrame implements
   {
   }
 
-  protected void alignStructs_actionPerformed(ActionEvent actionEvent)
-  {
-  }
+  protected abstract String alignStructs_actionPerformed(
+          ActionEvent actionEvent);
 
   public void pdbFile_actionPerformed(ActionEvent actionEvent)
   {
@@ -432,52 +257,7 @@ public abstract class GStructureViewer extends JInternalFrame implements
 
   }
 
-  public void zappoColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void taylorColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void hydroColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void helixColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void strandColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void turnColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void buriedColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void purinePyrimidineColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void userColour_actionPerformed(ActionEvent actionEvent)
-  {
-
-  }
-
-  public void backGround_actionPerformed(ActionEvent actionEvent)
+  public void background_actionPerformed(ActionEvent actionEvent)
   {
 
   }
@@ -486,14 +266,4 @@ public abstract class GStructureViewer extends JInternalFrame implements
   {
 
   }
-
-  // {
-  // return bindingModel;
-  // }
-
-  // public void setBindingModel(AAStructureBindingModel bindingModel)
-  // {
-  // this.bindingModel = bindingModel;
-  // }
-
 }
index b5695b2..5384cc0 100755 (executable)
@@ -32,6 +32,8 @@ import java.awt.GridBagLayout;
 import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
 
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
@@ -102,7 +104,9 @@ public class GUserDefinedColours extends JPanel
 
   protected JCheckBox caseSensitive = new JCheckBox();
 
-  protected JButton lcaseColour = new JButton();
+  protected JCheckBox lcaseColour = new JCheckBox();
+
+  protected List<JButton> selectedButtons;
 
   /**
    * Creates a new GUserDefinedColours object.
@@ -133,47 +137,52 @@ public class GUserDefinedColours extends JPanel
     gridLayout.setRows(5);
     okButton.setFont(new java.awt.Font("Verdana", 0, 11));
     okButton.setText(MessageManager.getString("action.ok"));
-    okButton.addActionListener(new java.awt.event.ActionListener()
+    okButton.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        okButton_actionPerformed(e);
+        okButton_actionPerformed();
       }
     });
     applyButton.setFont(new java.awt.Font("Verdana", 0, 11));
     applyButton.setText(MessageManager.getString("action.apply"));
-    applyButton.addActionListener(new java.awt.event.ActionListener()
+    applyButton.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        applyButton_actionPerformed(e);
+        applyButton_actionPerformed();
       }
     });
     loadbutton.setFont(new java.awt.Font("Verdana", 0, 11));
     loadbutton.setText(MessageManager.getString("action.load_scheme"));
-    loadbutton.addActionListener(new java.awt.event.ActionListener()
+    loadbutton.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        loadbutton_actionPerformed(e);
+        loadbutton_actionPerformed();
       }
     });
     savebutton.setFont(new java.awt.Font("Verdana", 0, 11));
     savebutton.setText(MessageManager.getString("action.save_scheme"));
-    savebutton.addActionListener(new java.awt.event.ActionListener()
+    savebutton.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        savebutton_actionPerformed(e);
+        savebutton_actionPerformed();
       }
     });
     cancelButton.setFont(JvSwingUtils.getLabelFont());
     cancelButton.setText(MessageManager.getString("action.cancel"));
-    cancelButton.addActionListener(new java.awt.event.ActionListener()
+    cancelButton.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        cancelButton_actionPerformed(e);
+        cancelButton_actionPerformed();
       }
     });
     this.setBackground(new Color(212, 208, 223));
@@ -206,20 +215,16 @@ public class GUserDefinedColours extends JPanel
     caseSensitive.setText(MessageManager.getString("label.case_sensitive"));
     caseSensitive.addActionListener(new ActionListener()
     {
+      @Override
       public void actionPerformed(ActionEvent e)
       {
-        caseSensitive_actionPerformed(e);
+        caseSensitive_actionPerformed();
       }
     });
     lcaseColour
             .setText(MessageManager.getString("label.lower_case_colour"));
-    lcaseColour.addActionListener(new ActionListener()
-    {
-      public void actionPerformed(ActionEvent e)
-      {
-        lcaseColour_actionPerformed(e);
-      }
-    });
+    lcaseColour.setToolTipText(MessageManager
+            .getString("label.lower_case_tip"));
 
     saveLoadPanel.add(savebutton);
     saveLoadPanel.add(loadbutton);
@@ -253,25 +258,21 @@ public class GUserDefinedColours extends JPanel
       colorChooser
               .setChooserPanels(new AbstractColorChooserPanel[] { choosers[0] });
     }
+
+    selectedButtons = new ArrayList<JButton>();
   }
 
   /**
    * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
    */
-  protected void okButton_actionPerformed(ActionEvent e)
+  protected void okButton_actionPerformed()
   {
   }
 
   /**
    * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
    */
-  protected void applyButton_actionPerformed(ActionEvent e)
+  protected void applyButton_actionPerformed()
   {
   }
 
@@ -281,18 +282,13 @@ public class GUserDefinedColours extends JPanel
    * @param e
    *          DOCUMENT ME!
    */
-  protected void loadbutton_actionPerformed(ActionEvent e)
+  protected void loadbutton_actionPerformed()
   {
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param e
-   *          DOCUMENT ME!
-   */
-  protected void savebutton_actionPerformed(ActionEvent e)
+  protected boolean savebutton_actionPerformed()
   {
+    return false;
   }
 
   /**
@@ -301,16 +297,16 @@ public class GUserDefinedColours extends JPanel
    * @param e
    *          DOCUMENT ME!
    */
-  protected void cancelButton_actionPerformed(ActionEvent e)
+  protected void cancelButton_actionPerformed()
   {
   }
 
-  public void caseSensitive_actionPerformed(ActionEvent e)
+  public void caseSensitive_actionPerformed()
   {
 
   }
 
-  public void lcaseColour_actionPerformed(ActionEvent e)
+  public void lcaseColour_actionPerformed()
   {
 
   }
index 28b9d67..b39d3c9 100755 (executable)
@@ -26,44 +26,53 @@ import jalview.util.MessageManager;
 import java.io.PrintStream;
 
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * A class to model rectangular matrices of double values and operations on them
  */
-public class Matrix
+public class Matrix implements MatrixI
 {
-  /**
-   * SMJSPUBLIC
+  /*
+   * the cell values in row-major order
    */
-  public double[][] value;
+  private double[][] value;
 
-  /** DOCUMENT ME!! */
-  public int rows;
+  /*
+   * the number of rows
+   */
+  protected int rows;
 
-  /** DOCUMENT ME!! */
-  public int cols;
+  /*
+   * the number of columns
+   */
+  protected int cols;
 
-  /** DOCUMENT ME!! */
-  public double[] d; // Diagonal
+  protected double[] d; // Diagonal
 
-  /** DOCUMENT ME!! */
-  public double[] e; // off diagonal
+  protected double[] e; // off diagonal
 
   /**
    * maximum number of iterations for tqli
    * 
    */
-  int maxIter = 45; // fudge - add 15 iterations, just in case
+  private static final int maxIter = 45; // fudge - add 15 iterations, just in
+                                         // case
+
+  /**
+   * Default constructor
+   */
+  public Matrix()
+  {
 
+  }
+  
   /**
-   * Creates a new Matrix object. For example
+   * Creates a new Matrix object containing a copy of the supplied array values.
+   * For example
    * 
    * <pre>
-   *   new Matrix(new double[][] {{2, 3}, {4, 5}, 2, 2)
+   *   new Matrix(new double[][] {{2, 3, 4}, {5, 6, 7})
    * constructs
-   *   (2 3)
-   *   (4 5)
+   *   (2 3 4)
+   *   (5 6 7)
    * </pre>
    * 
    * Note that ragged arrays (with not all rows, or columns, of the same
@@ -72,22 +81,35 @@ public class Matrix
    * 
    * @param values
    *          the matrix values in row-major order
-   * @param rows
-   * @param cols
    */
-  public Matrix(double[][] values, int rows, int cols)
+  public Matrix(double[][] values)
   {
-    this.rows = rows;
-    this.cols = cols;
-    this.value = values;
+    this.rows = values.length;
+    this.cols = this.rows == 0 ? 0 : values[0].length;
+
+    /*
+     * make a copy of the values array, for immutability
+     */
+    this.value = new double[rows][];
+    int i = 0;
+    for (double[] row : values)
+    {
+      if (row != null)
+      {
+        value[i] = new double[row.length];
+        System.arraycopy(row, 0, value[i], 0, row.length);
+      }
+      i++;
+    }
   }
 
   /**
-   * Returns a new matrix which is the transposes of this one
+   * Returns a new matrix which is the transpose of this one
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
-  public Matrix transpose()
+  @Override
+  public MatrixI transpose()
   {
     double[][] out = new double[cols][rows];
 
@@ -99,7 +121,7 @@ public class Matrix
       }
     }
 
-    return new Matrix(out, cols, rows);
+    return new Matrix(out);
   }
 
   /**
@@ -107,14 +129,16 @@ public class Matrix
    * 
    * @param ps
    *          DOCUMENT ME!
+   * @param format
    */
-  public void print(PrintStream ps)
+  @Override
+  public void print(PrintStream ps, String format)
   {
     for (int i = 0; i < rows; i++)
     {
       for (int j = 0; j < cols; j++)
       {
-        Format.print(ps, "%8.2f", value[i][j]);
+        Format.print(ps, format, getValue(i, j));
       }
 
       ps.println();
@@ -133,29 +157,32 @@ public class Matrix
    *           if the number of columns in the pre-multiplier is not equal to
    *           the number of rows in the multiplicand (this)
    */
-  public Matrix preMultiply(Matrix in)
+  @Override
+  public MatrixI preMultiply(MatrixI in)
   {
-    if (in.cols != this.rows)
+    if (in.width() != rows)
     {
       throw new IllegalArgumentException("Can't pre-multiply " + this.rows
-              + " rows by " + in.cols + " columns");
+              + " rows by " + in.width() + " columns");
     }
-    double[][] tmp = new double[in.rows][this.cols];
+    double[][] tmp = new double[in.height()][this.cols];
 
-    for (int i = 0; i < in.rows; i++)
+    for (int i = 0; i < in.height(); i++)
     {
       for (int j = 0; j < this.cols; j++)
       {
-        tmp[i][j] = 0.0;
-
-        for (int k = 0; k < in.cols; k++)
+        /*
+         * result[i][j] is the vector product of 
+         * in.row[i] and this.column[j]
+         */
+        for (int k = 0; k < in.width(); k++)
         {
-          tmp[i][j] += (in.value[i][k] * this.value[k][j]);
+          tmp[i][j] += (in.getValue(i, k) * this.value[k][j]);
         }
       }
     }
 
-    return new Matrix(tmp, in.rows, this.cols);
+    return new Matrix(tmp);
   }
 
   /**
@@ -196,12 +223,13 @@ public class Matrix
    *           number of columns in the multiplicand (this)
    * @see #preMultiply(Matrix)
    */
-  public Matrix postMultiply(Matrix in)
+  @Override
+  public MatrixI postMultiply(MatrixI in)
   {
-    if (in.rows != this.cols)
+    if (in.height() != this.cols)
     {
       throw new IllegalArgumentException("Can't post-multiply " + this.cols
-              + " columns by " + in.rows + " rows");
+              + " columns by " + in.height() + " rows");
     }
     return in.preMultiply(this);
   }
@@ -211,29 +239,26 @@ public class Matrix
    * 
    * @return
    */
-  public Matrix copy()
+  @Override
+  public MatrixI copy()
   {
     double[][] newmat = new double[rows][cols];
 
     for (int i = 0; i < rows; i++)
     {
       System.arraycopy(value[i], 0, newmat[i], 0, value[i].length);
-      // for (int j = 0; j < cols; j++)
-      // {
-      // newmat[i][j] = value[i][j];
-      // }
     }
 
-    return new Matrix(newmat, rows, cols);
+    return new Matrix(newmat);
   }
 
   /**
    * DOCUMENT ME!
    */
+  @Override
   public void tred()
   {
     int n = rows;
-    int l;
     int k;
     int j;
     int i;
@@ -249,7 +274,7 @@ public class Matrix
 
     for (i = n; i >= 2; i--)
     {
-      l = i - 1;
+      final int l = i - 1;
       h = 0.0;
       scale = 0.0;
 
@@ -257,22 +282,23 @@ public class Matrix
       {
         for (k = 1; k <= l; k++)
         {
-          scale += Math.abs(value[i - 1][k - 1]);
+          double v = Math.abs(getValue(i - 1, k - 1));
+          scale += v;
         }
 
         if (scale == 0.0)
         {
-          e[i - 1] = value[i - 1][l - 1];
+          e[i - 1] = getValue(i - 1, l - 1);
         }
         else
         {
           for (k = 1; k <= l; k++)
           {
-            value[i - 1][k - 1] /= scale;
-            h += (value[i - 1][k - 1] * value[i - 1][k - 1]);
+            double v = divideValue(i - 1, k - 1, scale);
+            h += v * v;
           }
 
-          f = value[i - 1][l - 1];
+          f = getValue(i - 1, l - 1);
 
           if (f > 0)
           {
@@ -285,46 +311,48 @@ public class Matrix
 
           e[i - 1] = scale * g;
           h -= (f * g);
-          value[i - 1][l - 1] = f - g;
+          setValue(i - 1, l - 1, f - g);
           f = 0.0;
 
           for (j = 1; j <= l; j++)
           {
-            value[j - 1][i - 1] = value[i - 1][j - 1] / h;
+            double val = getValue(i - 1, j - 1) / h;
+            setValue(j - 1, i - 1, val);
             g = 0.0;
 
             for (k = 1; k <= j; k++)
             {
-              g += (value[j - 1][k - 1] * value[i - 1][k - 1]);
+              g += (getValue(j - 1, k - 1) * getValue(i - 1, k - 1));
             }
 
             for (k = j + 1; k <= l; k++)
             {
-              g += (value[k - 1][j - 1] * value[i - 1][k - 1]);
+              g += (getValue(k - 1, j - 1) * getValue(i - 1, k - 1));
             }
 
             e[j - 1] = g / h;
-            f += (e[j - 1] * value[i - 1][j - 1]);
+            f += (e[j - 1] * getValue(i - 1, j - 1));
           }
 
           hh = f / (h + h);
 
           for (j = 1; j <= l; j++)
           {
-            f = value[i - 1][j - 1];
+            f = getValue(i - 1, j - 1);
             g = e[j - 1] - (hh * f);
             e[j - 1] = g;
 
             for (k = 1; k <= j; k++)
             {
-              value[j - 1][k - 1] -= ((f * e[k - 1]) + (g * value[i - 1][k - 1]));
+              double val = (f * e[k - 1]) + (g * getValue(i - 1, k - 1));
+              addValue(j - 1, k - 1, -val);
             }
           }
         }
       }
       else
       {
-        e[i - 1] = value[i - 1][l - 1];
+        e[i - 1] = getValue(i - 1, l - 1);
       }
 
       d[i - 1] = h;
@@ -335,7 +363,7 @@ public class Matrix
 
     for (i = 1; i <= n; i++)
     {
-      l = i - 1;
+      final int l = i - 1;
 
       if (d[i - 1] != 0.0)
       {
@@ -345,30 +373,66 @@ public class Matrix
 
           for (k = 1; k <= l; k++)
           {
-            g += (value[i - 1][k - 1] * value[k - 1][j - 1]);
+            g += (getValue(i - 1, k - 1) * getValue(k - 1, j - 1));
           }
 
           for (k = 1; k <= l; k++)
           {
-            value[k - 1][j - 1] -= (g * value[k - 1][i - 1]);
+            addValue(k - 1, j - 1, -(g * getValue(k - 1, i - 1)));
           }
         }
       }
 
-      d[i - 1] = value[i - 1][i - 1];
-      value[i - 1][i - 1] = 1.0;
+      d[i - 1] = getValue(i - 1, i - 1);
+      setValue(i - 1, i - 1, 1.0);
 
       for (j = 1; j <= l; j++)
       {
-        value[j - 1][i - 1] = 0.0;
-        value[i - 1][j - 1] = 0.0;
+        setValue(j - 1, i - 1, 0.0);
+        setValue(i - 1, j - 1, 0.0);
       }
     }
   }
 
   /**
+   * Adds f to the value at [i, j] and returns the new value
+   * 
+   * @param i
+   * @param j
+   * @param f
+   */
+  protected double addValue(int i, int j, double f)
+  {
+    double v = value[i][j] + f;
+    value[i][j] = v;
+    return v;
+  }
+
+  /**
+   * Divides the value at [i, j] by divisor and returns the new value. If d is
+   * zero, returns the unchanged value.
+   * 
+   * @param i
+   * @param j
+   * @param divisor
+   * @return
+   */
+  protected double divideValue(int i, int j, double divisor)
+  {
+    if (divisor == 0d)
+    {
+      return getValue(i, j);
+    }
+    double v = value[i][j];
+    v = v / divisor;
+    value[i][j] = v;
+    return v;
+  }
+
+  /**
    * DOCUMENT ME!
    */
+  @Override
   public void tqli() throws Exception
   {
     int n = rows;
@@ -381,7 +445,6 @@ public class Matrix
     double s;
     double r;
     double p;
-    ;
 
     double g;
     double f;
@@ -464,9 +527,9 @@ public class Matrix
 
             for (k = 1; k <= n; k++)
             {
-              f = value[k - 1][i];
-              value[k - 1][i] = (s * value[k - 1][i - 1]) + (c * f);
-              value[k - 1][i - 1] = (c * value[k - 1][i - 1]) - (s * f);
+              f = getValue(k - 1, i);
+              setValue(k - 1, i, (s * getValue(k - 1, i - 1)) + (c * f));
+              setValue(k - 1, i - 1, (c * getValue(k - 1, i - 1)) - (s * f));
             }
           }
 
@@ -478,6 +541,18 @@ public class Matrix
     }
   }
 
+  @Override
+  public double getValue(int i, int j)
+  {
+    return value[i][j];
+  }
+
+  @Override
+  public void setValue(int i, int j, double val)
+  {
+    value[i][j] = val;
+  }
+
   /**
    * DOCUMENT ME!
    */
@@ -730,16 +805,14 @@ public class Matrix
   }
 
   /**
-   * DOCUMENT ME!
+   * Answers the first argument with the sign of the second argument
    * 
    * @param a
-   *          DOCUMENT ME!
    * @param b
-   *          DOCUMENT ME!
    * 
-   * @return DOCUMENT ME!
+   * @return
    */
-  public double sign(double a, double b)
+  static double sign(double a, double b)
   {
     if (b < 0)
     {
@@ -775,12 +848,14 @@ public class Matrix
    * 
    * @param ps
    *          DOCUMENT ME!
+   * @param format
    */
-  public void printD(PrintStream ps)
+  @Override
+  public void printD(PrintStream ps, String format)
   {
     for (int j = 0; j < rows; j++)
     {
-      Format.print(ps, "%15.4e", d[j]);
+      Format.print(ps, format, d[j]);
     }
   }
 
@@ -789,12 +864,132 @@ public class Matrix
    * 
    * @param ps
    *          DOCUMENT ME!
+   * @param format TODO
    */
-  public void printE(PrintStream ps)
+  @Override
+  public void printE(PrintStream ps, String format)
   {
     for (int j = 0; j < rows; j++)
     {
-      Format.print(ps, "%15.4e", e[j]);
+      Format.print(ps, format, e[j]);
+    }
+  }
+
+  @Override
+  public double[] getD()
+  {
+    return d;
+  }
+
+  @Override
+  public double[] getE()
+  {
+    return e;
+  }
+  
+  @Override
+  public int height() {
+    return rows;
+  }
+
+  @Override
+  public int width()
+  {
+    return cols;
+  }
+
+  @Override
+  public double[] getRow(int i)
+  {
+    double[] row = new double[cols];
+    System.arraycopy(value[i], 0, row, 0, cols);
+    return row;
+  }
+
+  /**
+   * Returns a length 2 array of {minValue, maxValue} of all values in the
+   * matrix. Returns null if the matrix is null or empty.
+   * 
+   * @return
+   */
+  double[] findMinMax()
+  {
+    if (value == null)
+    {
+      return null;
+    }
+    double min = Double.MAX_VALUE;
+    double max = -Double.MAX_VALUE;
+    boolean empty = true;
+    for (double[] row : value)
+    {
+      if (row != null)
+      {
+        for (double x : row)
+        {
+          empty = false;
+          if (x > max)
+          {
+            max = x;
+          }
+          if (x < min)
+          {
+            min = x;
+          }
+        }
+      }
+    }
+    return empty ? null : new double[] { min, max };
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void reverseRange(boolean maxToZero)
+  {
+    if (value == null)
+    {
+      return;
+    }
+    double[] minMax = findMinMax();
+    if (minMax == null)
+    {
+      return; // empty matrix
+    }
+    double subtractFrom = maxToZero ? minMax[1] : minMax[0] + minMax[1];
+
+    for (double[] row : value)
+    {
+      if (row != null)
+      {
+        int j = 0;
+        for (double x : row)
+        {
+          row[j] = subtractFrom - x;
+          j++;
+        }
+      }
+    }
+  }
+
+  /**
+   * Multiplies every entry in the matrix by the given value.
+   * 
+   * @param
+   */
+  @Override
+  public void multiply(double by)
+  {
+    for (double[] row : value)
+    {
+      if (row != null)
+      {
+        for (int i = 0; i < row.length; i++)
+        {
+          row[i] *= by;
+        }
+      }
     }
   }
 }
diff --git a/src/jalview/math/MatrixI.java b/src/jalview/math/MatrixI.java
new file mode 100644 (file)
index 0000000..94b9333
--- /dev/null
@@ -0,0 +1,97 @@
+package jalview.math;
+
+import java.io.PrintStream;
+
+public interface MatrixI
+{
+  /**
+   * Answers the number of columns
+   * 
+   * @return
+   */
+  int width();
+
+  /**
+   * Answers the number of rows
+   * 
+   * @return
+   */
+  int height();
+
+  /**
+   * Answers the value at row i, column j
+   * 
+   * @param i
+   * @param j
+   * @return
+   */
+  double getValue(int i, int j);
+
+  /**
+   * Sets the value at row i, colum j
+   * 
+   * @param i
+   * @param j
+   * @param d
+   */
+  void setValue(int i, int j, double d);
+
+  /**
+   * Answers a copy of the values in the i'th row
+   * 
+   * @return
+   */
+  double[] getRow(int i);
+  
+  MatrixI copy();
+
+  MatrixI transpose();
+
+  MatrixI preMultiply(MatrixI m);
+
+  MatrixI postMultiply(MatrixI m);
+
+  double[] getD();
+
+  double[] getE();
+
+  void print(PrintStream ps, String format);
+
+  void printD(PrintStream ps, String format);
+
+  void printE(PrintStream ps, String format);
+
+  void tqli() throws Exception;
+
+  void tred();
+
+  /**
+   * Reverses the range of the matrix values, so that the smallest values become
+   * the largest, and the largest become the smallest. This operation supports
+   * using a distance measure as a similarity measure, or vice versa.
+   * <p>
+   * If parameter <code>maxToZero</code> is true, then the maximum value becomes
+   * zero, i.e. all values are subtracted from the maximum. This is consistent
+   * with converting an identity similarity score to a distance score - the most
+   * similar (identity) corresponds to zero distance. However note that the
+   * operation is not reversible (unless the original minimum value is zero).
+   * For example a range of 10-40 would become 30-0, which would reverse a
+   * second time to 0-30. Also note that a general similarity measure (such as
+   * BLOSUM) may give different 'identity' scores for different sequences, so
+   * they cannot all convert to zero distance.
+   * <p>
+   * If parameter <code>maxToZero</code> is false, then the values are reflected
+   * about the average of {min, max} (effectively swapping min and max). This
+   * operation <em>is</em> reversible.
+   * 
+   * @param maxToZero
+   */
+  void reverseRange(boolean maxToZero);
+
+  /**
+   * Multiply all entries by the given value
+   * 
+   * @param d
+   */
+  void multiply(double d);
+}
diff --git a/src/jalview/math/SparseMatrix.java b/src/jalview/math/SparseMatrix.java
new file mode 100644 (file)
index 0000000..72f0963
--- /dev/null
@@ -0,0 +1,218 @@
+package jalview.math;
+
+import jalview.ext.android.SparseDoubleArray;
+
+/**
+ * A variant of Matrix intended for use for sparse (mostly zero) matrices. This
+ * class uses a SparseDoubleArray to hold each row of the matrix. The sparse
+ * array only stores non-zero values. This gives a smaller memory footprint, and
+ * fewer matrix calculation operations, for mostly zero matrices.
+ * 
+ * @author gmcarstairs
+ */
+public class SparseMatrix extends Matrix
+{
+  /*
+   * we choose columns for the sparse arrays as this allows
+   * optimisation of the preMultiply() method used in PCA.run()
+   */
+  SparseDoubleArray[] sparseColumns;
+
+  /**
+   * Constructor given data in [row][column] order
+   * 
+   * @param v
+   */
+  public SparseMatrix(double[][] v)
+  {
+    rows = v.length;
+    if (rows > 0) {
+      cols = v[0].length;
+    }
+    sparseColumns = new SparseDoubleArray[cols];
+
+    /*
+     * transpose v[row][col] into [col][row] order
+     */
+    for (int col = 0; col < cols; col++)
+    {
+      SparseDoubleArray sparseColumn = new SparseDoubleArray();
+      sparseColumns[col] = sparseColumn;
+      for (int row = 0; row < rows; row++)
+      {
+        double value = v[row][col];
+        if (value != 0d)
+        {
+          sparseColumn.put(row, value);
+        }
+      }
+    }
+  }
+
+  /**
+   * Answers the value at row i, column j
+   */
+  @Override
+  public double getValue(int i, int j)
+  {
+    return sparseColumns[j].get(i);
+  }
+
+  /**
+   * Sets the value at row i, column j to val
+   */
+  @Override
+  public void setValue(int i, int j, double val)
+  {
+    if (val == 0d)
+    {
+      sparseColumns[j].delete(i);
+    }
+    else
+    {
+      sparseColumns[j].put(i, val);
+    }
+  }
+
+  @Override
+  public double[] getColumn(int i)
+  {
+    double[] col = new double[height()];
+
+    SparseDoubleArray vals = sparseColumns[i];
+    for (int nonZero = 0; nonZero < vals.size(); nonZero++)
+    {
+      col[vals.keyAt(nonZero)] = vals.valueAt(nonZero);
+    }
+    return col;
+  }
+
+  @Override
+  public MatrixI copy()
+  {
+    double[][] vals = new double[height()][width()];
+    for (int i = 0; i < height(); i++)
+    {
+      vals[i] = getRow(i);
+    }
+    return new SparseMatrix(vals);
+  }
+
+  @Override
+  public MatrixI transpose()
+  {
+    double[][] out = new double[cols][rows];
+
+    /*
+     * for each column...
+     */
+    for (int i = 0; i < cols; i++)
+    {
+      /*
+       * put non-zero values into the corresponding row
+       * of the transposed matrix
+       */
+      SparseDoubleArray vals = sparseColumns[i];
+      for (int nonZero = 0; nonZero < vals.size(); nonZero++)
+      {
+        out[i][vals.keyAt(nonZero)] = vals.valueAt(nonZero);
+      }
+    }
+
+    return new SparseMatrix(out);
+  }
+
+  /**
+   * Answers a new matrix which is the product in.this. If the product contains
+   * less than 20% non-zero values, it is returned as a SparseMatrix, else as a
+   * Matrix.
+   * <p>
+   * This method is optimised for the sparse arrays which store column values
+   * for a SparseMatrix. Note that postMultiply is not so optimised. That would
+   * require redundantly also storing sparse arrays for the rows, which has not
+   * been done. Currently only preMultiply is used in Jalview.
+   */
+  @Override
+  public MatrixI preMultiply(MatrixI in)
+  {
+    if (in.width() != rows)
+    {
+      throw new IllegalArgumentException("Can't pre-multiply " + this.rows
+              + " rows by " + in.width() + " columns");
+    }
+    double[][] tmp = new double[in.height()][this.cols];
+
+    long count = 0L;
+    for (int i = 0; i < in.height(); i++)
+    {
+      for (int j = 0; j < this.cols; j++)
+      {
+        /*
+         * result[i][j] is the vector product of 
+         * in.row[i] and this.column[j]
+         * we only need to use non-zero values from the column
+         */
+        SparseDoubleArray vals = sparseColumns[j];
+        boolean added = false;
+        for (int nonZero = 0; nonZero < vals.size(); nonZero++)
+        {
+          int myRow = vals.keyAt(nonZero);
+          double myValue = vals.valueAt(nonZero);
+          tmp[i][j] += (in.getValue(i, myRow) * myValue);
+          added = true;
+        }
+        if (added && tmp[i][j] != 0d)
+        {
+          count++; // non-zero entry in product
+        }
+      }
+    }
+
+    /*
+     * heuristic rule - if product is more than 80% zero
+     * then construct a SparseMatrix, else a Matrix
+     */
+    if (count * 5 < in.height() * cols)
+    {
+      return new SparseMatrix(tmp);
+    }
+    else
+    {
+      return new Matrix(tmp);
+    }
+  }
+
+  @Override
+  protected double divideValue(int i, int j, double divisor)
+  {
+    if (divisor == 0d)
+    {
+      return getValue(i, j);
+    }
+    double v = sparseColumns[j].divide(i, divisor);
+    return v;
+  }
+
+  @Override
+  protected double addValue(int i, int j, double addend)
+  {
+    double v = sparseColumns[j].add(i, addend);
+    return v;
+  }
+
+  /**
+   * Returns the fraction of the whole matrix size that is actually modelled in
+   * sparse arrays (normally, the non-zero values)
+   * 
+   * @return
+   */
+  public float getFillRatio()
+  {
+    long count = 0L;
+    for (SparseDoubleArray col : sparseColumns)
+    {
+      count += col.size();
+    }
+    return count / (float) (height() * width());
+  }
+}
index a0e530c..518c179 100644 (file)
@@ -28,9 +28,12 @@ import jalview.api.AlignViewportI;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.ProfilesI;
 import jalview.schemes.ColourSchemeI;
+import jalview.schemes.NucleotideColourScheme;
 import jalview.schemes.ResidueProperties;
+import jalview.schemes.ZappoColourScheme;
 import jalview.util.Platform;
 
 import java.awt.BasicStroke;
@@ -70,9 +73,11 @@ public class AnnotationRenderer
   boolean av_renderHistogram = true, av_renderProfile = true,
           av_normaliseProfile = false;
 
-  ColourSchemeI profcolour = null;
+  ResidueShaderI profcolour = null;
 
   private ColumnSelection columnSelection;
+  
+  private HiddenColumns hiddenColumns;
 
   private ProfilesI hconsensus;
 
@@ -305,22 +310,27 @@ public class AnnotationRenderer
   public void updateFromAlignViewport(AlignViewportI av)
   {
     charWidth = av.getCharWidth();
-    endRes = av.getEndRes();
+    endRes = av.getRanges().getEndRes();
     charHeight = av.getCharHeight();
     hasHiddenColumns = av.hasHiddenColumns();
     validCharWidth = av.isValidCharWidth();
     av_renderHistogram = av.isShowConsensusHistogram();
     av_renderProfile = av.isShowSequenceLogo();
     av_normaliseProfile = av.isNormaliseSequenceLogo();
-    profcolour = av.getGlobalColourScheme();
-    if (profcolour == null)
+    profcolour = av.getResidueShading();
+    if (profcolour == null || profcolour.getColourScheme() == null)
     {
-      // Set the default colour for sequence logo if the alignnent has no
-      // colourscheme set
-      profcolour = av.getAlignment().isNucleotide() ? new jalview.schemes.NucleotideColourScheme()
-              : new jalview.schemes.ZappoColourScheme();
+      /*
+       * Use default colour for sequence logo if 
+       * the alignment has no colourscheme set
+       * (would like to use user preference but n/a for applet)
+       */
+      ColourSchemeI col = av.getAlignment().isNucleotide() ? new NucleotideColourScheme()
+              : new ZappoColourScheme();
+      profcolour = new ResidueShader(col);
     }
     columnSelection = av.getColumnSelection();
+    hiddenColumns = av.getAlignment().getHiddenColumns();
     hconsensus = av.getSequenceConsensusHash();
     complementConsensus = av.getComplementConsensusHash();
     hStrucConsensus = av.getRnaStructureConsensusHash();
@@ -583,7 +593,7 @@ hconsensus.get(column),
         {
           if (hasHiddenColumns)
           {
-            column = columnSelection.adjustForHiddenColumns(startRes + x);
+            column = hiddenColumns.adjustForHiddenColumns(startRes + x);
             if (column > row_annotations.length - 1)
             {
               break;
@@ -1225,7 +1235,7 @@ hconsensus.get(column),
       column = sRes + x;
       if (hasHiddenColumns)
       {
-        column = columnSelection.adjustForHiddenColumns(column);
+        column = hiddenColumns.adjustForHiddenColumns(column);
       }
 
       if (column > aaMax)
@@ -1304,7 +1314,7 @@ hconsensus.get(column),
       column = sRes + x;
       if (hasHiddenColumns)
       {
-        column = columnSelection.adjustForHiddenColumns(column);
+        column = hiddenColumns.adjustForHiddenColumns(column);
       }
 
       if (column > aaMax)
diff --git a/src/jalview/renderer/OverviewRenderer.java b/src/jalview/renderer/OverviewRenderer.java
new file mode 100644 (file)
index 0000000..9291ca6
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * 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 jalview.api.AlignmentColsCollectionI;
+import jalview.api.AlignmentRowsCollectionI;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.SequenceI;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.viewmodel.OverviewDimensions;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+
+public class OverviewRenderer
+{
+  private FeatureColourFinder finder;
+
+  private jalview.api.SequenceRenderer sr;
+
+  // image to render on
+  private BufferedImage miniMe;
+
+  // raw number of pixels to allocate to each column
+  private float pixelsPerCol;
+
+  // raw number of pixels to allocate to each row
+  private float pixelsPerSeq;
+
+  public OverviewRenderer(jalview.api.SequenceRenderer seqRenderer,
+          FeatureRenderer fr, OverviewDimensions od)
+  // FeatureColourFinder colfinder, OverviewDimensions od)
+  {
+    sr = seqRenderer;
+    finder = new FeatureColourFinder(fr); // colfinder;
+
+    pixelsPerCol = od.getPixelsPerCol();
+    pixelsPerSeq = od.getPixelsPerSeq();
+    miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
+            BufferedImage.TYPE_INT_RGB);
+  }
+
+  /**
+   * Draw alignment rows and columns onto an image
+   * 
+   * @param rit
+   *          Iterator over rows to be drawn
+   * @param cit
+   *          Iterator over columns to be drawn
+   * @return image containing the drawing
+   */
+  public BufferedImage draw(AlignmentRowsCollectionI rows,
+          AlignmentColsCollectionI cols)
+  {
+    int rgbcolor = Color.white.getRGB();
+    int seqIndex = 0;
+    int pixelRow = 0;
+    for (int alignmentRow : rows)
+    {
+      // get details of this alignment row
+      boolean hidden = rows.isHidden(alignmentRow);
+      SequenceI seq = rows.getSequence(alignmentRow);
+
+      // calculate where this row extends to in pixels
+      int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
+              miniMe.getHeight() - 1);
+
+      int colIndex = 0;
+      int pixelCol = 0;
+      for (int alignmentCol : cols)
+      {
+        // calculate where this column extends to in pixels
+        int endCol = Math.min(
+                Math.round((colIndex + 1) * pixelsPerCol) - 1,
+                miniMe.getWidth() - 1);
+
+        // don't do expensive colour determination if we're not going to use it
+        // NB this is important to avoid performance issues in the overview
+        // panel
+        if (pixelCol <= endCol)
+        {
+          // determine the colour based on the sequence and column position
+          rgbcolor = getColumnColourFromSequence(seq,
+                  hidden || cols.isHidden(alignmentCol), alignmentCol,
+                  finder);
+
+          // fill in the appropriate number of pixels
+          for (int row = pixelRow; row <= endRow; ++row)
+          {
+            for (int col = pixelCol; col <= endCol; ++col)
+            {
+              miniMe.setRGB(col, row, rgbcolor);
+            }
+          }
+
+          pixelCol = endCol + 1;
+        }
+        colIndex++;
+      }
+      pixelRow = endRow + 1;
+      seqIndex++;
+    }
+    return miniMe;
+  }
+
+  /*
+   * Find the colour of a sequence at a specified column position
+   */
+  private int getColumnColourFromSequence(jalview.datamodel.SequenceI seq,
+          boolean isHidden, int lastcol, FeatureColourFinder fcfinder)
+  {
+    Color color = Color.white;
+
+    if ((seq != null) && (seq.getLength() > lastcol))
+    {
+      color = sr.getResidueColour(seq, lastcol, fcfinder);
+    }
+
+    if (isHidden)
+    {
+      color = color.darker().darker();
+    }
+
+    return color.getRGB();
+  }
+
+  /**
+   * Draw the alignment annotation in the overview panel
+   * 
+   * @param g
+   *          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)
+  {
+    Annotation[] annotations = anno.annotations;
+    g.setColor(Color.white);
+    g.fillRect(0, 0, miniMe.getWidth(), y);
+
+    int height;
+    int colIndex = 0;
+    int pixelCol = 0;
+    for (int alignmentCol : cols)
+    {
+      if (alignmentCol >= annotations.length)
+      {
+        break; // no more annotations to draw here
+      }
+      else
+      {
+        int endCol = Math.min(
+                Math.round((colIndex + 1) * pixelsPerCol) - 1,
+                miniMe.getWidth() - 1);
+
+        if (annotations[alignmentCol] != null)
+        {
+          if (annotations[alignmentCol].colour == null)
+          {
+            g.setColor(Color.black);
+          }
+          else
+          {
+            g.setColor(annotations[alignmentCol].colour);
+          }
+
+          height = (int) ((annotations[alignmentCol].value / anno.graphMax) * y);
+          if (height > y)
+          {
+            height = y;
+          }
+
+          g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
+        }
+        pixelCol = endCol + 1;
+        colIndex++;
+      }
+    }
+  }
+}
diff --git a/src/jalview/renderer/ResidueShader.java b/src/jalview/renderer/ResidueShader.java
new file mode 100644 (file)
index 0000000..b6f7fe6
--- /dev/null
@@ -0,0 +1,375 @@
+package jalview.renderer;
+
+import jalview.analysis.Conservation;
+import jalview.api.ViewStyleI;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.ProfileI;
+import jalview.datamodel.ProfilesI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.ColourSchemeI;
+import jalview.util.ColorUtils;
+import jalview.util.Comparison;
+
+import java.awt.Color;
+import java.util.Map;
+
+/**
+ * A class that computes the colouring of an alignment (or subgroup). Currently
+ * the factors that may influence residue colouring are
+ * <ul>
+ * <li>the colour scheme that provides a colour for each aligned residue</li>
+ * <li>any threshold for colour, based on percentage identity with consensus</li>
+ * <li>any graduation based on conservation of physico-chemical properties</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class ResidueShader implements ResidueShaderI
+{
+  private static final int INITIAL_CONSERVATION = 30;
+
+  /*
+   * the colour scheme that gives the colour of each residue
+   * before applying any conservation or PID shading
+   */
+  private ColourSchemeI colourScheme;
+
+  /*
+   * the consensus data for each column
+   */
+  private ProfilesI consensus;
+
+  /*
+   * if true, apply shading of colour by conservation
+   */
+  private boolean conservationColouring;
+
+  /*
+   * the physico-chemical property conservation scores for columns, with values
+   * 0-9, '+' (all properties conserved), '*' (residue fully conserved) or '-' (gap)
+   * (may be null if colour by conservation is not selected)
+   */
+  private char[] conservation;
+
+  /*
+   * minimum percentage identity for colour to be applied;
+   * if above zero, residue must match consensus (or joint consensus)
+   * and column have >= pidThreshold identity with the residue
+   */
+  private int pidThreshold;
+
+  /*
+   * if true, ignore gaps in percentage identity calculation
+   */
+  private boolean ignoreGaps;
+
+  /*
+   * setting of the By Conservation slider
+   */
+  private int conservationIncrement = INITIAL_CONSERVATION;
+
+  public ResidueShader(ColourSchemeI cs)
+  {
+    colourScheme = cs;
+  }
+
+  /**
+   * Default constructor
+   */
+  public ResidueShader()
+  {
+  }
+
+  /**
+   * Constructor given view style settings
+   * 
+   * @param viewStyle
+   */
+  public ResidueShader(ViewStyleI viewStyle)
+  {
+    // TODO remove duplicated storing of conservation / pid thresholds?
+    this();
+    setConservationApplied(viewStyle.isConservationColourSelected());
+    // setThreshold(viewStyle.getThreshold());
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#setConsensus(jalview.datamodel.ProfilesI)
+   */
+  @Override
+  public void setConsensus(ProfilesI cons)
+  {
+    consensus = cons;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#conservationApplied()
+   */
+  @Override
+  public boolean conservationApplied()
+  {
+    return conservationColouring;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#setConservationApplied(boolean)
+   */
+  @Override
+  public void setConservationApplied(boolean conservationApplied)
+  {
+    conservationColouring = conservationApplied;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#setConservation(jalview.analysis.Conservation)
+   */
+  @Override
+  public void setConservation(Conservation cons)
+  {
+    if (cons == null)
+    {
+      conservationColouring = false;
+      conservation = null;
+    }
+    else
+    {
+      conservationColouring = true;
+      conservation = cons.getConsSequence().getSequenceAsString()
+              .toCharArray();
+    }
+  
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#alignmentChanged(jalview.datamodel.AnnotatedCollectionI,
+   *      java.util.Map)
+   */
+  @Override
+  public void alignmentChanged(AnnotatedCollectionI alignment,
+          Map<SequenceI, SequenceCollectionI> hiddenReps)
+  {
+    if (colourScheme != null)
+    {
+      colourScheme.alignmentChanged(alignment, hiddenReps);
+    }
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#setThreshold(int, boolean)
+   */
+  @Override
+  public void setThreshold(int consensusThreshold, boolean ignoreGaps)
+  {
+    pidThreshold = consensusThreshold;
+    this.ignoreGaps = ignoreGaps;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#setConservationInc(int)
+   */
+  @Override
+  public void setConservationInc(int i)
+  {
+    conservationIncrement = i;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#getConservationInc()
+   */
+  @Override
+  public int getConservationInc()
+  {
+    return conservationIncrement;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#getThreshold()
+   */
+  @Override
+  public int getThreshold()
+  {
+    return pidThreshold;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#findColour(char, int,
+   *      jalview.datamodel.SequenceI)
+   */
+  @Override
+  public Color findColour(char symbol, int position, SequenceI seq)
+  {
+    /*
+     * get 'base' colour
+     */
+    ProfileI profile = consensus == null ? null : consensus.get(position);
+    String modalResidue = profile == null ? null : profile
+            .getModalResidue();
+    float pid = profile == null ? 0f : profile
+            .getPercentageIdentity(ignoreGaps);
+    Color colour = colourScheme == null ? Color.white : colourScheme
+            .findColour(symbol, position, seq, modalResidue, pid);
+
+    /*
+     * apply PID threshold and consensus fading if in force
+     */
+    colour = adjustColour(symbol, position, colour);
+
+    return colour;
+  }
+
+  /**
+   * Adjusts colour by applying thresholding or conservation shading, if in
+   * force. That is
+   * <ul>
+   * <li>if there is a threshold set for colouring, and the residue doesn't
+   * match the consensus (or a joint consensus) residue, or the consensus score
+   * is not above the threshold, then the colour is set to white</li>
+   * <li>if conservation colouring is selected, the colour is faded by an amount
+   * depending on the conservation score for the column, and the conservation
+   * colour threshold</li>
+   * </ul>
+   * 
+   * @param symbol
+   * @param column
+   * @param colour
+   * @return
+   */
+  protected Color adjustColour(char symbol, int column, Color colour)
+  {
+    if (!aboveThreshold(symbol, column))
+    {
+      colour = Color.white;
+    }
+  
+    if (conservationColouring)
+    {
+      colour = applyConservation(colour, column);
+    }
+    return colour;
+  }
+
+  /**
+   * Answers true if there is a consensus profile for the specified column, and
+   * the given residue matches the consensus (or joint consensus) residue for
+   * the column, and the percentage identity for the profile is equal to or
+   * greater than the current threshold; else answers false. The percentage
+   * calculation depends on whether or not we are ignoring gapped sequences.
+   * 
+   * @param residue
+   * @param column
+   *          (index into consensus profiles)
+   * 
+   * @return
+   * @see #setThreshold(int, boolean)
+   */
+  protected boolean aboveThreshold(char residue, int column)
+  {
+    if (pidThreshold == 0)
+    {
+      return true;
+    }
+    if ('a' <= residue && residue <= 'z')
+    {
+      // TO UPPERCASE !!!
+      // Faster than toUpperCase
+      residue -= ('a' - 'A');
+    }
+  
+    if (consensus == null)
+    {
+      return false;
+    }
+  
+    ProfileI profile = consensus.get(column);
+  
+    /*
+     * test whether this is the consensus (or joint consensus) residue
+     */
+    if (profile != null
+            && profile.getModalResidue().contains(String.valueOf(residue)))
+    {
+      if (profile.getPercentageIdentity(ignoreGaps) >= pidThreshold)
+      {
+        return true;
+      }
+    }
+  
+    return false;
+  }
+
+  /**
+   * Applies a combination of column conservation score, and conservation
+   * percentage slider, to 'bleach' out the residue colours towards white.
+   * <p>
+   * If a column is fully conserved (identical residues, conservation score 11,
+   * shown as *), or all 10 physico-chemical properties are conserved
+   * (conservation score 10, shown as +), then the colour is left unchanged.
+   * <p>
+   * Otherwise a 'bleaching' factor is computed and applied to the colour. This
+   * is designed to fade colours for scores of 0-9 completely to white at slider
+   * positions ranging from 18% - 100% respectively.
+   * 
+   * @param currentColour
+   * @param column
+   * 
+   * @return bleached (or unmodified) colour
+   */
+  protected Color applyConservation(Color currentColour, int column)
+  {
+    if (conservation == null || conservation.length <= column)
+    {
+      return currentColour;
+    }
+    char conservationScore = conservation[column];
+  
+    /*
+     * if residues are fully conserved (* or 11), or all properties
+     * are conserved (+ or 10), leave colour unchanged
+     */
+    if (conservationScore == '*' || conservationScore == '+'
+            || conservationScore == (char) 10
+            || conservationScore == (char) 11)
+    {
+      return currentColour;
+    }
+  
+    if (Comparison.isGap(conservationScore))
+    {
+      return Color.white;
+    }
+  
+    /*
+     * convert score 0-9 to a bleaching factor 1.1 - 0.2
+     */
+    float bleachFactor = (11 - (conservationScore - '0')) / 10f;
+  
+    /*
+     * scale this up by 0-5 (percentage slider / 20)
+     * as a result, scores of:         0  1  2  3  4  5  6  7  8  9
+     * fade to white at slider value: 18 20 22 25 29 33 40 50 67 100%
+     */
+    bleachFactor *= (conservationIncrement / 20f);
+  
+    return ColorUtils.bleachColour(currentColour, bleachFactor);
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#getColourScheme()
+   */
+  @Override
+  public ColourSchemeI getColourScheme()
+  {
+    return this.colourScheme;
+  }
+
+  /**
+   * @see jalview.renderer.ResidueShaderI#setColourScheme(jalview.schemes.ColourSchemeI)
+   */
+  @Override
+  public void setColourScheme(ColourSchemeI cs)
+  {
+    colourScheme = cs;
+  }
+}
diff --git a/src/jalview/renderer/ResidueShaderI.java b/src/jalview/renderer/ResidueShaderI.java
new file mode 100644 (file)
index 0000000..a914a1a
--- /dev/null
@@ -0,0 +1,64 @@
+package jalview.renderer;
+
+import jalview.analysis.Conservation;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.ProfilesI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.ColourSchemeI;
+
+import java.awt.Color;
+import java.util.Map;
+
+public interface ResidueShaderI
+{
+
+  public abstract void setConsensus(ProfilesI cons);
+
+  public abstract boolean conservationApplied();
+
+  public abstract void setConservationApplied(boolean conservationApplied);
+
+  public abstract void setConservation(Conservation cons);
+
+  public abstract void alignmentChanged(AnnotatedCollectionI alignment,
+          Map<SequenceI, SequenceCollectionI> hiddenReps);
+
+  /**
+   * Sets the percentage consensus threshold value, and whether gaps are ignored
+   * in percentage identity calculation
+   * 
+   * @param consensusThreshold
+   * @param ignoreGaps
+   */
+  public abstract void setThreshold(int consensusThreshold,
+          boolean ignoreGaps);
+
+  public abstract void setConservationInc(int i);
+
+  public abstract int getConservationInc();
+
+  /**
+   * Get the percentage threshold for this colour scheme
+   * 
+   * @return Returns the percentage threshold
+   */
+  public abstract int getThreshold();
+
+  /**
+   * Returns the possibly context dependent colour for the given symbol at the
+   * aligned position in the given sequence. For example, the colour may depend
+   * on the symbol's relationship to the consensus residue for the column.
+   * 
+   * @param symbol
+   * @param position
+   * @param seq
+   * @return
+   */
+  public abstract Color findColour(char symbol, int position, SequenceI seq);
+
+  public abstract ColourSchemeI getColourScheme();
+
+  public abstract void setColourScheme(ColourSchemeI cs);
+
+}
\ No newline at end of file
index 82536d4..9fec256 100644 (file)
@@ -73,7 +73,7 @@ public class ScaleRenderer
     {
       // find bounds and set origin appopriately
       // locate first visible position for this sequence
-      int[] refbounds = av.getColumnSelection()
+      int[] refbounds = av.getAlignment().getHiddenColumns()
               .locateVisibleBoundsOfSequence(refSeq);
 
       refSp = refbounds[0];
@@ -96,12 +96,14 @@ public class ScaleRenderer
       {
         if (refSeq == null)
         {
-          iadj = av.getColumnSelection().adjustForHiddenColumns(i - 1) + 1;
+          iadj = av.getAlignment().getHiddenColumns()
+                  .adjustForHiddenColumns(i - 1) + 1;
           string = String.valueOf(iadj);
         }
         else
         {
-          iadj = av.getColumnSelection().adjustForHiddenColumns(i - 1);
+          iadj = av.getAlignment().getHiddenColumns()
+                  .adjustForHiddenColumns(i - 1);
           refN = refSeq.findPosition(iadj);
           // TODO show bounds if position is a gap
           // - ie L--R -> "1L|2R" for
diff --git a/src/jalview/renderer/seqfeatures/FeatureColourFinder.java b/src/jalview/renderer/seqfeatures/FeatureColourFinder.java
new file mode 100644 (file)
index 0000000..ba63834
--- /dev/null
@@ -0,0 +1,124 @@
+package jalview.renderer.seqfeatures;
+
+import jalview.api.FeatureRenderer;
+import jalview.api.FeaturesDisplayedI;
+import jalview.datamodel.SequenceI;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+
+/**
+ * A helper class to find feature colour using an associated FeatureRenderer
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureColourFinder
+{
+  /*
+   * the class we delegate feature finding to
+   */
+  private FeatureRenderer featureRenderer;
+
+  /*
+   * a 1-pixel image on which features can be drawn, for the case where
+   * transparency allows 'see-through' of multiple feature colours
+   */
+  private BufferedImage offscreenImage;
+
+  /**
+   * Constructor
+   * 
+   * @param fr
+   */
+  public FeatureColourFinder(FeatureRenderer fr)
+  {
+    featureRenderer = fr;
+    offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+  }
+
+  /**
+   * Answers the feature colour to show for the given sequence and column
+   * position. This delegates to the FeatureRenderer to find the colour, which
+   * will depend on feature location, visibility, ordering, colour scheme, and
+   * whether or not transparency is applied. For feature rendering with
+   * transparency, this class provides a dummy 'offscreen' graphics context
+   * where multiple feature colours can be overlaid and the combined colour read
+   * back.
+   * <p>
+   * This method is not thread-safe when transparency is applied, since a shared
+   * BufferedImage would be used by all threads to hold the composite colour at
+   * a position. Each thread should use a separate instance of this class.
+   * 
+   * @param defaultColour
+   * @param seq
+   * @param column
+   *          alignment column position (0..)
+   * @return
+   */
+  public Color findFeatureColour(Color defaultColour, SequenceI seq,
+          int column)
+  {
+    if (noFeaturesDisplayed())
+    {
+      return defaultColour;
+    }
+
+    Graphics g = null;
+
+    /*
+     * if transparency applies, provide a notional 1x1 graphics context 
+     * that has been primed with the default colour
+     */
+    if (featureRenderer.getTransparency() != 1f)
+    {
+      g = offscreenImage.getGraphics();
+      if (defaultColour != null)
+      {
+        offscreenImage.setRGB(0, 0, defaultColour.getRGB());
+      }
+    }
+
+    Color c = featureRenderer.findFeatureColour(seq, column + 1, g);
+    if (c == null)
+    {
+      return defaultColour;
+    }
+
+    if (g != null)
+    {
+      c = new Color(offscreenImage.getRGB(0, 0));
+    }
+    return c;
+  }
+
+  /**
+   * Answers true if feature display is turned off, or there are no features
+   * configured to be visible
+   * 
+   * @return
+   */
+  boolean noFeaturesDisplayed()
+  {
+    if (featureRenderer == null
+            || !featureRenderer.getViewport().isShowSequenceFeatures())
+    {
+      return true;
+    }
+
+    if (!((FeatureRendererModel) featureRenderer).hasRenderOrder())
+    {
+      return true;
+    }
+
+    FeaturesDisplayedI displayed = featureRenderer.getFeaturesDisplayed();
+    if (displayed == null || displayed.getVisibleFeatureCount() == 0)
+    {
+      return true;
+    }
+
+    return false;
+  }
+}
index 9e0089f..8f4f139 100644 (file)
 package jalview.renderer.seqfeatures;
 
 import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.awt.AlphaComposite;
@@ -30,28 +32,12 @@ import java.awt.Color;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
-import java.awt.image.BufferedImage;
+import java.util.List;
 
 public class FeatureRenderer extends FeatureRendererModel
 {
-
-  FontMetrics fm;
-
-  int charOffset;
-
-  boolean offscreenRender = false;
-
-  protected SequenceI lastSeq;
-
-  char s;
-
-  int i;
-
-  int av_charHeight, av_charWidth;
-
-  boolean av_validCharWidth, av_isShowSeqFeatureHeight;
-
-  private Integer currentColour;
+  private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
+          .getInstance(AlphaComposite.SRC_OVER, 1.0f);
 
   /**
    * Constructor given a viewport
@@ -63,273 +49,240 @@ public class FeatureRenderer extends FeatureRendererModel
     this.av = viewport;
   }
 
-  protected void updateAvConfig()
+  /**
+   * Renders the sequence using the given feature colour between the given start
+   * and end columns. Returns true if at least one column is drawn, else false
+   * (the feature range does not overlap the start and end positions).
+   * 
+   * @param g
+   * @param seq
+   * @param featureStart
+   * @param featureEnd
+   * @param featureColour
+   * @param start
+   * @param end
+   * @param y1
+   * @param colourOnly
+   * @return
+   */
+  boolean renderFeature(Graphics g, SequenceI seq, int featureStart,
+          int featureEnd, Color featureColour, int start, int end, int y1,
+          boolean colourOnly)
   {
-    av_charHeight = av.getCharHeight();
-    av_charWidth = av.getCharWidth();
-    av_validCharWidth = av.isValidCharWidth();
-    av_isShowSeqFeatureHeight = av.isShowSequenceFeaturesHeight();
-  }
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    boolean validCharWidth = av.isValidCharWidth();
 
-  void renderFeature(Graphics g, SequenceI seq, int fstart, int fend,
-          Color featureColour, int start, int end, int y1)
-  {
-    updateAvConfig();
-    if (((fstart <= end) && (fend >= start)))
+    if (featureStart > end || featureEnd < start)
     {
-      if (fstart < start)
-      { // fix for if the feature we have starts before the sequence start,
-        fstart = start; // but the feature end is still valid!!
-      }
-
-      if (fend >= end)
-      {
-        fend = end;
-      }
-      int pady = (y1 + av_charHeight) - av_charHeight / 5;
-      for (i = fstart; i <= fend; i++)
-      {
-        s = seq.getCharAt(i);
-
-        if (jalview.util.Comparison.isGap(s))
-        {
-          continue;
-        }
-
-        g.setColor(featureColour);
-
-        g.fillRect((i - start) * av_charWidth, y1, av_charWidth,
-                av_charHeight);
-
-        if (offscreenRender || !av_validCharWidth)
-        {
-          continue;
-        }
-
-        g.setColor(Color.white);
-        charOffset = (av_charWidth - fm.charWidth(s)) / 2;
-        g.drawString(String.valueOf(s), charOffset
-                + (av_charWidth * (i - start)), pady);
+      return false;
+    }
 
-      }
+    if (featureStart < start)
+    {
+      featureStart = start;
     }
-  }
+    if (featureEnd >= end)
+    {
+      featureEnd = end;
+    }
+    int pady = (y1 + charHeight) - charHeight / 5;
 
-  void renderScoreFeature(Graphics g, SequenceI seq, int fstart, int fend,
-          Color featureColour, int start, int end, int y1, byte[] bs)
-  {
-    updateAvConfig();
-    if (((fstart <= end) && (fend >= start)))
+    FontMetrics fm = g.getFontMetrics();
+    for (int i = featureStart; i <= featureEnd; i++)
     {
-      if (fstart < start)
-      { // fix for if the feature we have starts before the sequence start,
-        fstart = start; // but the feature end is still valid!!
-      }
+      char s = seq.getCharAt(i);
 
-      if (fend >= end)
+      if (Comparison.isGap(s))
       {
-        fend = end;
-      }
-      int pady = (y1 + av_charHeight) - av_charHeight / 5;
-      int ystrt = 0, yend = av_charHeight;
-      if (bs[0] != 0)
-      {
-        // signed - zero is always middle of residue line.
-        if (bs[1] < 128)
-        {
-          yend = av_charHeight * (128 - bs[1]) / 512;
-          ystrt = av_charHeight - yend / 2;
-        }
-        else
-        {
-          ystrt = av_charHeight / 2;
-          yend = av_charHeight * (bs[1] - 128) / 512;
-        }
-      }
-      else
-      {
-        yend = av_charHeight * bs[1] / 255;
-        ystrt = av_charHeight - yend;
-
+        continue;
       }
-      for (i = fstart; i <= fend; i++)
-      {
-        s = seq.getCharAt(i);
 
-        if (jalview.util.Comparison.isGap(s))
-        {
-          continue;
-        }
+      g.setColor(featureColour);
 
-        g.setColor(featureColour);
-        int x = (i - start) * av_charWidth;
-        g.drawRect(x, y1, av_charWidth, av_charHeight);
-        g.fillRect(x, y1 + ystrt, av_charWidth, yend);
+      g.fillRect((i - start) * charWidth, y1, charWidth,
+              charHeight);
 
-        if (offscreenRender || !av_validCharWidth)
-        {
-          continue;
-        }
-
-        g.setColor(Color.black);
-        charOffset = (av_charWidth - fm.charWidth(s)) / 2;
-        g.drawString(String.valueOf(s), charOffset
-                + (av_charWidth * (i - start)), pady);
+      if (colourOnly || !validCharWidth)
+      {
+        continue;
       }
-    }
-  }
-
-  BufferedImage offscreenImage;
 
-  @Override
-  public Color findFeatureColour(Color initialCol, SequenceI seq, int res)
-  {
-    return new Color(findFeatureColour(initialCol.getRGB(), seq, res));
+      g.setColor(Color.white);
+      int charOffset = (charWidth - fm.charWidth(s)) / 2;
+      g.drawString(String.valueOf(s), charOffset
+              + (charWidth * (i - start)), pady);
+    }
+    return true;
   }
 
   /**
-   * This is used by Structure Viewers and the Overview Window to get the
-   * feature colour of the rendered sequence, returned as an RGB value
+   * Renders the sequence using the given SCORE feature colour between the given
+   * start and end columns. Returns true if at least one column is drawn, else
+   * false (the feature range does not overlap the start and end positions).
    * 
-   * @param defaultColour
+   * @param g
    * @param seq
-   * @param column
+   * @param fstart
+   * @param fend
+   * @param featureColour
+   * @param start
+   * @param end
+   * @param y1
+   * @param bs
+   * @param colourOnly
    * @return
    */
-  public synchronized int findFeatureColour(int defaultColour,
-          final SequenceI seq, int column)
+  boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
+          int fend, Color featureColour, int start, int end, int y1,
+          byte[] bs, boolean colourOnly)
   {
-    if (!av.isShowSequenceFeatures())
+    if (fstart > end || fend < start)
     {
-      return defaultColour;
+      return false;
     }
 
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (seq != lastSeq)
+    if (fstart < start)
+    { // fix for if the feature we have starts before the sequence start,
+      fstart = start; // but the feature end is still valid!!
+    }
+
+    if (fend >= end)
+    {
+      fend = end;
+    }
+    int charHeight = av.getCharHeight();
+    int pady = (y1 + charHeight) - charHeight / 5;
+    int ystrt = 0, yend = charHeight;
+    if (bs[0] != 0)
     {
-      lastSeq = seq;
-      lastSequenceFeatures = sequenceFeatures;
-      if (lastSequenceFeatures != null)
+      // signed - zero is always middle of residue line.
+      if (bs[1] < 128)
       {
-        sfSize = lastSequenceFeatures.length;
+        yend = charHeight * (128 - bs[1]) / 512;
+        ystrt = charHeight - yend / 2;
+      }
+      else
+      {
+        ystrt = charHeight / 2;
+        yend = charHeight * (bs[1] - 128) / 512;
       }
     }
     else
     {
-      if (lastSequenceFeatures != sequenceFeatures)
-      {
-        lastSequenceFeatures = sequenceFeatures;
-        if (lastSequenceFeatures != null)
-        {
-          sfSize = lastSequenceFeatures.length;
-        }
-      }
+      yend = charHeight * bs[1] / 255;
+      ystrt = charHeight - yend;
+
     }
 
-    if (lastSequenceFeatures == null || sfSize == 0)
+    FontMetrics fm = g.getFontMetrics();
+    int charWidth = av.getCharWidth();
+
+    for (int i = fstart; i <= fend; i++)
     {
-      return defaultColour;
+      char s = seq.getCharAt(i);
+
+      if (Comparison.isGap(s))
+      {
+        continue;
+      }
+
+      g.setColor(featureColour);
+      int x = (i - start) * charWidth;
+      g.drawRect(x, y1, charWidth, charHeight);
+      g.fillRect(x, y1 + ystrt, charWidth, yend);
+
+      if (colourOnly || !av.isValidCharWidth())
+      {
+        continue;
+      }
+
+      g.setColor(Color.black);
+      int charOffset = (charWidth - fm.charWidth(s)) / 2;
+      g.drawString(String.valueOf(s), charOffset
+              + (charWidth * (i - start)), pady);
     }
+    return true;
+  }
 
-    if (jalview.util.Comparison.isGap(lastSeq.getCharAt(column)))
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public Color findFeatureColour(SequenceI seq, int column, Graphics g)
+  {
+    if (!av.isShowSequenceFeatures())
     {
-      return Color.white.getRGB();
+      return null;
     }
 
-    // Only bother making an offscreen image if transparency is applied
-    if (transparency != 1.0f && offscreenImage == null)
+    if (Comparison.isGap(seq.getCharAt(column)))
     {
-      offscreenImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+      return Color.white;
     }
 
-    currentColour = null;
-    // TODO: non-threadsafe - each rendering thread needs its own instance of
-    // the feature renderer - or this should be synchronized.
-    offscreenRender = true;
-
-    if (offscreenImage != null)
+    Color renderedColour = null;
+    if (transparency == 1.0f)
     {
-      offscreenImage.setRGB(0, 0, defaultColour);
-      drawSequence(offscreenImage.getGraphics(), lastSeq, column, column, 0);
-
-      return offscreenImage.getRGB(0, 0);
+      /*
+       * simple case - just find the topmost rendered visible feature colour
+       */
+      renderedColour = findFeatureColour(seq, column);
     }
     else
     {
-      drawSequence(null, lastSeq, lastSeq.findPosition(column), -1, -1);
-
-      if (currentColour == null)
-      {
-        return defaultColour;
-      }
-      else
-      {
-        return currentColour.intValue();
-      }
+      /*
+       * transparency case - draw all visible features in render order to
+       * build up a composite colour on the graphics context
+       */
+      renderedColour = drawSequence(g, seq, column, column, 0, true);
     }
-
+    return renderedColour;
   }
 
-  private volatile SequenceFeature[] lastSequenceFeatures;
-
-  int sfSize;
-
-  int sfindex;
-
-  int spos;
-
-  int epos;
-
   /**
-   * Draws the sequence on the graphics context, or just determines the colour
-   * that would be drawn (if flag offscreenrender is true).
+   * Draws the sequence features on the graphics context, or just determines the
+   * colour that would be drawn (if flag colourOnly is true). Returns the last
+   * colour drawn (which may not be the effective colour if transparency
+   * applies), or null if no feature is drawn in the range given.
    * 
    * @param g
+   *          the graphics context to draw on (may be null if colourOnly==true)
    * @param seq
    * @param start
-   *          start column (or sequence position in offscreenrender mode)
+   *          start column
    * @param end
-   *          end column (not used in offscreenrender mode)
+   *          end column
    * @param y1
    *          vertical offset at which to draw on the graphics
+   * @param colourOnly
+   *          if true, only do enough to determine the colour for the position,
+   *          do not draw the character
+   * @return
    */
-  public synchronized void drawSequence(Graphics g, final SequenceI seq,
-          int start, int end, int y1)
+  public synchronized Color drawSequence(final Graphics g,
+          final SequenceI seq, int start, int end, int y1,
+          boolean colourOnly)
   {
-    SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
-    if (sequenceFeatures == null || sequenceFeatures.length == 0)
+    if (!seq.getFeatures().hasFeatures())
     {
-      return;
-    }
-
-    if (g != null)
-    {
-      fm = g.getFontMetrics();
+      return null;
     }
 
     updateFeatures();
 
-    if (lastSeq == null || seq != lastSeq
-            || sequenceFeatures != lastSequenceFeatures)
-    {
-      lastSeq = seq;
-      lastSequenceFeatures = sequenceFeatures;
-    }
-
-    if (transparency != 1 && g != null)
+    if (transparency != 1f && g != null)
     {
       Graphics2D g2 = (Graphics2D) g;
       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
               transparency));
     }
 
-    if (!offscreenRender)
-    {
-      spos = lastSeq.findPosition(start);
-      epos = lastSeq.findPosition(end);
-    }
+    Color drawnColour = null;
 
-    sfSize = lastSequenceFeatures.length;
+    /*
+     * iterate over features in ordering of their rendering (last is on top)
+     */
     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
     {
       String type = renderOrder[renderIndex];
@@ -338,117 +291,87 @@ public class FeatureRenderer extends FeatureRendererModel
         continue;
       }
 
-      // loop through all features in sequence to find
-      // current feature to render
-      for (sfindex = 0; sfindex < sfSize; sfindex++)
-      {
-        final SequenceFeature sequenceFeature = lastSequenceFeatures[sfindex];
-        if (!sequenceFeature.type.equals(type))
-        {
-          continue;
-        }
+      FeatureColourI fc = getFeatureStyle(type);
+      List<SequenceFeature> overlaps = seq.findFeatures(start + 1, end + 1,
+              type);
 
-        if (featureGroupNotShown(sequenceFeature))
-        {
-          continue;
-        }
+      filterFeaturesForDisplay(overlaps, fc);
 
+      for (SequenceFeature sf : overlaps)
+      {
         /*
-         * check feature overlaps the visible part of the alignment, 
-         * unless doing offscreenRender (to the Overview window or a 
-         * structure viewer) which is not limited 
+         * a feature type may be flagged as shown but the group 
+         * an instance of it belongs to may be hidden
          */
-        if (!offscreenRender
-                && (sequenceFeature.getBegin() > epos || sequenceFeature
-                        .getEnd() < spos))
+        if (featureGroupNotShown(sf))
         {
           continue;
         }
 
-        Color featureColour = getColour(sequenceFeature);
-        boolean isContactFeature = sequenceFeature.isContactFeature();
+        Color featureColour = fc.getColor(sf);
+        boolean isContactFeature = sf.isContactFeature();
 
-        if (offscreenRender && offscreenImage == null)
+        int featureStartCol = seq.findIndex(sf.begin);
+        int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
+                .findIndex(sf.end);
+        if (isContactFeature)
         {
-          /*
-           * offscreen mode with no image (image is only needed if transparency 
-           * is applied to feature colours) - just check feature is rendered at 
-           * the requested position (start == sequence position in this mode)
-           */
-          boolean featureIsAtPosition = sequenceFeature.begin <= start
-                  && sequenceFeature.end >= start;
-          if (isContactFeature)
+          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)
           {
-            featureIsAtPosition = sequenceFeature.begin == start
-                    || sequenceFeature.end == start;
+            drawnColour = featureColour;
           }
-          if (featureIsAtPosition)
-          {
-            // this is passed out to the overview and other sequence renderers
-            // (e.g. molecule viewer) to get displayed colour for rendered
-            // sequence
-            currentColour = new Integer(featureColour.getRGB());
-            // used to be retreived from av.featuresDisplayed
-            // currentColour = av.featuresDisplayed
-            // .get(sequenceFeatures[sfindex].type);
-
-          }
-        }
-        else if (isContactFeature)
-        {
-          renderFeature(g, seq, seq.findIndex(sequenceFeature.begin) - 1,
-                  seq.findIndex(sequenceFeature.begin) - 1, featureColour,
-                  start, end, y1);
-          renderFeature(g, seq, seq.findIndex(sequenceFeature.end) - 1,
-                  seq.findIndex(sequenceFeature.end) - 1, featureColour,
-                  start, end, y1);
-
         }
-        else if (showFeature(sequenceFeature))
+        else if (showFeature(sf))
         {
-          if (av_isShowSeqFeatureHeight
+          /*
+           * showing feature score by height of colour
+           * is not implemented as a selectable option 
+           *
+          if (av.isShowSequenceFeaturesHeight()
                   && !Float.isNaN(sequenceFeature.score))
           {
-            renderScoreFeature(g, seq,
+            boolean drawn = renderScoreFeature(g, seq,
                     seq.findIndex(sequenceFeature.begin) - 1,
-                    seq.findIndex(sequenceFeature.end) - 1,
-                    featureColour, start, end, y1,
-                    normaliseScore(sequenceFeature));
+                    seq.findIndex(sequenceFeature.end) - 1, featureColour,
+                    start, end, y1, normaliseScore(sequenceFeature),
+                    colourOnly);
+            if (drawn)
+            {
+              drawnColour = featureColour;
+            }
           }
           else
           {
-            renderFeature(g, seq, seq.findIndex(sequenceFeature.begin) - 1,
-                    seq.findIndex(sequenceFeature.end) - 1,
-                    featureColour, start, end, y1);
-          }
+          */
+            boolean drawn = renderFeature(g, seq,
+                    featureStartCol - 1,
+                    featureEndCol - 1, featureColour,
+                    start, end, y1, colourOnly);
+            if (drawn)
+            {
+              drawnColour = featureColour;
+            }
+          /*}*/
         }
       }
     }
 
     if (transparency != 1.0f && g != null)
     {
+      /*
+       * reset transparency
+       */
       Graphics2D g2 = (Graphics2D) g;
-      g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
-              1.0f));
+      g2.setComposite(NO_TRANSPARENCY);
     }
-  }
 
-  /**
-   * 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();
+    return drawnColour;
   }
 
   /**
@@ -459,7 +382,57 @@ public class FeatureRenderer extends FeatureRendererModel
   @Override
   public void featuresAdded()
   {
-    lastSeq = null;
     findAllFeatures();
   }
+
+  /**
+   * 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.
+   * <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 column
+   *          (1..)
+   * @return
+   */
+  Color findFeatureColour(SequenceI seq, int column)
+  {
+    /*
+     * check for new feature added while processing
+     */
+    updateFeatures();
+
+    /*
+     * inspect features in reverse renderOrder (the last in the array is 
+     * displayed on top) until we find one that is rendered at the position
+     */
+    for (int renderIndex = renderOrder.length - 1; renderIndex >= 0; renderIndex--)
+    {
+      String type = renderOrder[renderIndex];
+      if (!showFeatureOfType(type))
+      {
+        continue;
+      }
+
+      List<SequenceFeature> overlaps = seq.findFeatures(column, column,
+              type);
+      for (SequenceFeature sequenceFeature : overlaps)
+      {
+        if (!featureGroupNotShown(sequenceFeature))
+        {
+          return getColour(sequenceFeature);
+        }
+      }
+    }
+  
+    /*
+     * no displayed feature found at position
+     */
+    return null;
+  }
 }
index 89b8f07..220d3ab 100755 (executable)
@@ -27,6 +27,8 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.GraphLine;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
+import jalview.renderer.AnnotationRenderer;
+import jalview.util.Comparison;
 
 import java.awt.Color;
 import java.util.IdentityHashMap;
@@ -40,15 +42,25 @@ public class AnnotationColourGradient extends FollowerColourScheme
 
   public static final int ABOVE_THRESHOLD = 1;
 
-  public AlignmentAnnotation annotation;
+  private final AlignmentAnnotation annotation;
 
-  int aboveAnnotationThreshold = -1;
+  private final int aboveAnnotationThreshold;
 
   public boolean thresholdIsMinMax = false;
 
-  GraphLine annotationThreshold;
+  private GraphLine annotationThreshold;
 
-  float r1, g1, b1, rr, gg, bb;
+  private int redMin;
+
+  private int greenMin;
+
+  private int blueMin;
+
+  private int redRange;
+
+  private int greenRange;
+
+  private int blueRange;
 
   private boolean predefinedColours = false;
 
@@ -61,23 +73,23 @@ public class AnnotationColourGradient extends FollowerColourScheme
    */
   private boolean noGradient = false;
 
-  IdentityHashMap<SequenceI, AlignmentAnnotation> seqannot = null;
+  private IdentityHashMap<SequenceI, AlignmentAnnotation> seqannot = null;
 
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
+  public ColourSchemeI getInstance(AnnotatedCollectionI sg,
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
     AnnotationColourGradient acg = new AnnotationColourGradient(annotation,
-            colourScheme, aboveAnnotationThreshold);
+            getColourScheme(), aboveAnnotationThreshold);
     acg.thresholdIsMinMax = thresholdIsMinMax;
     acg.annotationThreshold = (annotationThreshold == null) ? null
             : new GraphLine(annotationThreshold);
-    acg.r1 = r1;
-    acg.g1 = g1;
-    acg.b1 = b1;
-    acg.rr = rr;
-    acg.gg = gg;
-    acg.bb = bb;
+    acg.redMin = redMin;
+    acg.greenMin = greenMin;
+    acg.blueMin = blueMin;
+    acg.redRange = redRange;
+    acg.greenRange = greenRange;
+    acg.blueRange = blueRange;
     acg.predefinedColours = predefinedColours;
     acg.seqAssociated = seqAssociated;
     acg.noGradient = noGradient;
@@ -92,11 +104,12 @@ public class AnnotationColourGradient extends FollowerColourScheme
   {
     if (originalColour instanceof AnnotationColourGradient)
     {
-      colourScheme = ((AnnotationColourGradient) originalColour).colourScheme;
+      setColourScheme(((AnnotationColourGradient) originalColour)
+              .getColourScheme());
     }
     else
     {
-      colourScheme = originalColour;
+      setColourScheme(originalColour);
     }
 
     this.annotation = annotation;
@@ -108,12 +121,12 @@ public class AnnotationColourGradient extends FollowerColourScheme
       annotationThreshold = annotation.threshold;
     }
     // clear values so we don't get weird black bands...
-    r1 = 254;
-    g1 = 254;
-    b1 = 254;
-    rr = 0;
-    gg = 0;
-    bb = 0;
+    redMin = 254;
+    greenMin = 254;
+    blueMin = 254;
+    redRange = 0;
+    greenRange = 0;
+    blueRange = 0;
 
     noGradient = true;
     checkLimits();
@@ -134,13 +147,13 @@ public class AnnotationColourGradient extends FollowerColourScheme
       annotationThreshold = annotation.threshold;
     }
 
-    r1 = minColour.getRed();
-    g1 = minColour.getGreen();
-    b1 = minColour.getBlue();
+    redMin = minColour.getRed();
+    greenMin = minColour.getGreen();
+    blueMin = minColour.getBlue();
 
-    rr = maxColour.getRed() - r1;
-    gg = maxColour.getGreen() - g1;
-    bb = maxColour.getBlue() - b1;
+    redRange = maxColour.getRed() - redMin;
+    greenRange = maxColour.getGreen() - greenMin;
+    blueRange = maxColour.getBlue() - blueMin;
 
     noGradient = false;
     checkLimits();
@@ -210,9 +223,9 @@ public class AnnotationColourGradient extends FollowerColourScheme
 
   float aamin = 0f, aamax = 0f;
 
-  public String getAnnotation()
+  public AlignmentAnnotation getAnnotation()
   {
-    return annotation.label;
+    return annotation;
   }
 
   public int getAboveThreshold()
@@ -234,12 +247,13 @@ public class AnnotationColourGradient extends FollowerColourScheme
 
   public Color getMinColour()
   {
-    return new Color((int) r1, (int) g1, (int) b1);
+    return new Color(redMin, greenMin, blueMin);
   }
 
   public Color getMaxColour()
   {
-    return new Color((int) (r1 + rr), (int) (g1 + gg), (int) (b1 + bb));
+    return new Color(redMin + redRange, greenMin + greenRange, blueMin
+            + blueRange);
   }
 
   /**
@@ -250,142 +264,164 @@ public class AnnotationColourGradient extends FollowerColourScheme
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Color findColour(char c)
   {
     return Color.red;
   }
 
   /**
-   * DOCUMENT ME!
+   * Returns the colour for a given character and position in a sequence
    * 
-   * @param n
-   *          DOCUMENT ME!
+   * @param c
+   *          the residue character
    * @param j
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
+   *          the aligned position
+   * @param seq
+   *          the sequence
+   * @return
    */
   @Override
   public Color findColour(char c, int j, SequenceI seq)
   {
-    Color currentColour = Color.white;
-    AlignmentAnnotation annotation = (seqAssociated && seqannot != null ? seqannot
+    /*
+     * locate the annotation we are configured to colour by
+     */
+    AlignmentAnnotation ann = (seqAssociated && seqannot != null ? seqannot
             .get(seq) : this.annotation);
-    if (annotation == null)
+
+    /*
+     * if gap or no annotation at position, no colour (White)
+     */
+    if (ann == null || ann.annotations == null
+            || j >= ann.annotations.length || ann.annotations[j] == null
+            || Comparison.isGap(c))
     {
-      return currentColour;
+      return Color.white;
     }
-    if ((threshold == 0) || aboveThreshold(c, j))
+
+    Annotation aj = ann.annotations[j];
+    // 'use original colours' => colourScheme != null
+    // -> look up colour to be used
+    // predefined colours => preconfigured shading
+    // -> only use original colours reference if thresholding enabled &
+    // minmax exists
+    // annotation.hasIcons => null or black colours replaced with glyph
+    // colours
+    // -> reuse original colours if present
+    // -> if thresholding enabled then return colour on non-whitespace glyph
+
+    /*
+     * if threshold applies, and annotation fails the test - no colour (white)
+     */
+    if (annotationThreshold != null)
     {
-      if (annotation.annotations != null
-              && j < annotation.annotations.length
-              && annotation.annotations[j] != null
-              && !jalview.util.Comparison.isGap(c))
+      if ((aboveAnnotationThreshold == ABOVE_THRESHOLD && aj.value < annotationThreshold.value)
+              || (aboveAnnotationThreshold == BELOW_THRESHOLD && aj.value > annotationThreshold.value))
       {
-        Annotation aj = annotation.annotations[j];
-        // 'use original colours' => colourScheme != null
-        // -> look up colour to be used
-        // predefined colours => preconfigured shading
-        // -> only use original colours reference if thresholding enabled &
-        // minmax exists
-        // annotation.hasIcons => null or black colours replaced with glyph
-        // colours
-        // -> reuse original colours if present
-        // -> if thresholding enabled then return colour on non-whitespace glyph
-
-        if (aboveAnnotationThreshold == NO_THRESHOLD
-                || (annotationThreshold != null && (aboveAnnotationThreshold == ABOVE_THRESHOLD ? aj.value >= annotationThreshold.value
-                        : aj.value <= annotationThreshold.value)))
+        return Color.white;
+      }
+    }
+
+    /*
+     * If 'use original colours' then return the colour of the annotation
+     * at the aligned position - computed using the background colour scheme
+     */
+    if (predefinedColours && aj.colour != null
+            && !aj.colour.equals(Color.black))
+    {
+      return aj.colour;
+    }
+
+    Color result = Color.white;
+    if (ann.hasIcons && ann.graph == AlignmentAnnotation.NO_GRAPH)
+    {
+      /*
+       * secondary structure symbol colouring
+       */
+      if (aj.secondaryStructure > ' ' && aj.secondaryStructure != '.'
+              && aj.secondaryStructure != '-')
+      {
+        if (getColourScheme() != null)
         {
-          if (predefinedColours && aj.colour != null
-                  && !aj.colour.equals(Color.black))
-          {
-            currentColour = aj.colour;
-          }
-          else if (annotation.hasIcons
-                  && annotation.graph == AlignmentAnnotation.NO_GRAPH)
-          {
-            if (aj.secondaryStructure > ' ' && aj.secondaryStructure != '.'
-                    && aj.secondaryStructure != '-')
-            {
-              if (colourScheme != null)
-              {
-                currentColour = colourScheme.findColour(c, j, seq);
-              }
-              else
-              {
-                if (annotation.isRNA())
-                {
-                  currentColour = ColourSchemeProperty.rnaHelices[(int) aj.value];
-                }
-                else
-                {
-                  currentColour = annotation.annotations[j].secondaryStructure == 'H' ? jalview.renderer.AnnotationRenderer.HELIX_COLOUR
-                          : annotation.annotations[j].secondaryStructure == 'E' ? jalview.renderer.AnnotationRenderer.SHEET_COLOUR
-                                  : jalview.renderer.AnnotationRenderer.STEM_COLOUR;
-                }
-              }
-            }
-            else
-            {
-              //
-              return Color.white;
-            }
-          }
-          else if (noGradient)
+          result = getColourScheme().findColour(c, j, seq, null, 0f);
+        }
+        else
+        {
+          if (ann.isRNA())
           {
-            if (colourScheme != null)
-            {
-              currentColour = colourScheme.findColour(c, j, seq);
-            }
-            else
-            {
-              if (aj.colour != null)
-              {
-                currentColour = aj.colour;
-              }
-            }
+            result = ColourSchemeProperty.rnaHelices[(int) aj.value];
           }
           else
           {
-            currentColour = shadeCalculation(annotation, j);
+            result = ann.annotations[j].secondaryStructure == 'H' ? AnnotationRenderer.HELIX_COLOUR
+                    : ann.annotations[j].secondaryStructure == 'E' ? AnnotationRenderer.SHEET_COLOUR
+                            : AnnotationRenderer.STEM_COLOUR;
           }
         }
-        if (conservationColouring)
+      }
+      else
+      {
+        return Color.white;
+      }
+    }
+    else if (noGradient)
+    {
+      if (getColourScheme() != null)
+      {
+        result = getColourScheme().findColour(c, j, seq, null, 0f);
+      }
+      else
+      {
+        if (aj.colour != null)
         {
-          currentColour = applyConservation(currentColour, j);
+          result = aj.colour;
         }
       }
     }
-    return currentColour;
+    else
+    {
+      result = shadeCalculation(ann, j);
+    }
+
+    return result;
   }
 
-  private Color shadeCalculation(AlignmentAnnotation annotation, int j)
+  /**
+   * Returns a graduated colour for the annotation at the given column. If there
+   * is a threshold value, and it is used as the top/bottom of the colour range,
+   * and the value satisfies the threshold condition, then a colour
+   * proportionate to the range from the threshold is calculated. For all other
+   * cases, a colour proportionate to the annotation's min-max range is
+   * calulated. Note that thresholding is _not_ done here (a colour is computed
+   * even if threshold is not passed).
+   * 
+   * @param ann
+   * @param col
+   * @return
+   */
+  Color shadeCalculation(AlignmentAnnotation ann, int col)
   {
-
-    // calculate a shade
     float range = 1f;
-    if (thresholdIsMinMax
-            && annotation.threshold != null
+    float value = ann.annotations[col].value;
+    if (thresholdIsMinMax && ann.threshold != null
             && aboveAnnotationThreshold == ABOVE_THRESHOLD
-            && annotation.annotations[j].value >= annotation.threshold.value)
+            && value >= ann.threshold.value)
     {
-      range = (annotation.annotations[j].value - annotation.threshold.value)
-              / (annotation.graphMax - annotation.threshold.value);
+      range = (value - ann.threshold.value)
+              / (ann.graphMax - ann.threshold.value);
     }
-    else if (thresholdIsMinMax && annotation.threshold != null
+    else if (thresholdIsMinMax && ann.threshold != null
             && aboveAnnotationThreshold == BELOW_THRESHOLD
-            && annotation.annotations[j].value >= annotation.graphMin)
+            && value <= ann.threshold.value)
     {
-      range = (annotation.annotations[j].value - annotation.graphMin)
-              / (annotation.threshold.value - annotation.graphMin);
+      range = (value - ann.graphMin) / (ann.threshold.value - ann.graphMin);
     }
     else
     {
-      if (annotation.graphMax != annotation.graphMin)
+      if (ann.graphMax != ann.graphMin)
       {
-        range = (annotation.annotations[j].value - annotation.graphMin)
-                / (annotation.graphMax - annotation.graphMin);
+        range = (value - ann.graphMin) / (ann.graphMax - ann.graphMin);
       }
       else
       {
@@ -393,11 +429,11 @@ public class AnnotationColourGradient extends FollowerColourScheme
       }
     }
 
-    int dr = (int) (rr * range + r1), dg = (int) (gg * range + g1), db = (int) (bb
-            * range + b1);
+    int dr = (int) (redRange * range + redMin);
+    int dg = (int) (greenRange * range + greenMin);
+    int db = (int) (blueRange * range + blueMin);
 
     return new Color(dr, dg, db);
-
   }
 
   public boolean isPredefinedColours()
@@ -419,4 +455,26 @@ public class AnnotationColourGradient extends FollowerColourScheme
   {
     seqAssociated = sassoc;
   }
+
+  public boolean isThresholdIsMinMax()
+  {
+    return thresholdIsMinMax;
+  }
+
+  public void setThresholdIsMinMax(boolean minMax)
+  {
+    this.thresholdIsMinMax = minMax;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return "Annotation";
+  }
+
+  @Override
+  public boolean isSimple()
+  {
+    return false;
+  }
 }
index c47f171..70f4910 100755 (executable)
@@ -20,6 +20,8 @@
  */
 package jalview.schemes;
 
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.api.analysis.PairwiseScoreModelI;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
@@ -38,73 +40,79 @@ public class Blosum62ColourScheme extends ResidueColourScheme
     super();
   }
 
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
   @Override
-  public Color findColour(char res, int j, SequenceI seq)
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
   {
+    return new Blosum62ColourScheme();
+  }
+
+  @Override
+  public Color findColour(char res, int j, SequenceI seq,
+          String consensusResidue, float pid)
+  {
+    PairwiseScoreModelI sm = ScoreModels.getInstance().getBlosum62();
+
+    /*
+     * compare as upper case; note consensusResidue is 
+     * always computed as uppercase
+     */
     if ('a' <= res && res <= 'z')
     {
-      // TO UPPERCASE !!!
       res -= ('a' - 'A');
     }
 
-    if (consensus == null || consensus.get(j) == null
-            || (threshold != 0 && !aboveThreshold(res, j)))
+    if (Comparison.isGap(res) || consensusResidue == null)
     {
       return Color.white;
     }
 
-    Color currentColour;
+    Color colour;
 
-    if (!Comparison.isGap(res))
+    if (consensusResidue.indexOf(res) > -1)
     {
-      /*
-       * test if this is the consensus (or joint consensus) residue
-       */
-      String max = consensus.get(j).getModalResidue();
+      colour = DARK_BLUE;
+    }
+    else
+    {
+      float score = 0;
 
-      if (max.indexOf(res) > -1)
+      for (char consensus : consensusResidue.toCharArray())
       {
-        currentColour = DARK_BLUE;
+        score += sm.getPairwiseScore(consensus, res);
       }
-      else
-      {
-        int c = 0;
-        int max_aa = 0;
-        int n = max.length();
 
-        do
-        {
-          c += ResidueProperties.getBLOSUM62(max.charAt(max_aa), res);
-        } while (++max_aa < n);
-
-        if (c > 0)
-        {
-          currentColour = LIGHT_BLUE;
-        }
-        else
-        {
-          currentColour = Color.white;
-        }
+      if (score > 0)
+      {
+        colour = LIGHT_BLUE;
       }
-
-      if (conservationColouring)
+      else
       {
-        currentColour = applyConservation(currentColour, j);
+        colour = Color.white;
       }
     }
-    else
-    {
-      return Color.white;
-    }
+    return colour;
+  }
 
-    return currentColour;
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Blosum62.toString();
   }
 
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
-          Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
+  public boolean isSimple()
   {
-    ColourSchemeI newcs = super.applyTo(sg, hiddenRepSequences);
-    return newcs;
+    return false;
   }
 }
index 3c68da0..a3b85b9 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
 import java.awt.Color;
+import java.util.Map;
 
 /**
  * DOCUMENT ME!
@@ -47,8 +52,32 @@ public class BuriedColourScheme extends ScoreColourScheme
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Color makeColour(float c)
   {
     return new Color(0, (float) (1.0 - c), c);
   }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Buried.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new BuriedColourScheme();
+  }
 }
index ca4316f..f13a90c 100755 (executable)
@@ -23,9 +23,9 @@ package jalview.schemes;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
+import jalview.util.Comparison;
 
 import java.awt.Color;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -39,10 +39,32 @@ public class ClustalxColourScheme extends ResidueColourScheme
 
   private static final int SIXTY = 60;
 
-  /*
-   * Map from conventional colour names to Clustal version of the same
-   */
-  private static Map<Color, Color> colhash = new HashMap<Color, Color>();
+  enum ClustalColour
+  {
+    RED(0.9f, 0.2f, 0.1f), BLUE(0.5f, 0.7f, 0.9f), GREEN(0.1f, 0.8f, 0.1f),
+    ORANGE(0.9f, 0.6f, 0.3f), CYAN(0.1f, 0.7f, 0.7f),
+    PINK(0.9f, 0.5f, 0.5f), MAGENTA(0.8f, 0.3f, 0.8f), YELLOW(0.8f, 0.8f,
+            0.0f);
+
+    final Color colour;
+
+    ClustalColour(float r, float g, float b)
+    {
+      colour = new Color(r, g, b);
+    }
+  }
+  private class ConsensusColour
+  {
+    Consensus[] cons;
+
+    Color c;
+
+    public ConsensusColour(ClustalColour col, Consensus[] conses)
+    {
+      this.cons = conses;
+      this.c = col.colour;
+    }
+  }
 
   private int[][] cons2;
 
@@ -56,16 +78,12 @@ public class ClustalxColourScheme extends ResidueColourScheme
 
   private boolean includeGaps = true;
 
-  static
+  /**
+   * Default constructor (required for Class.newInstance())
+   */
+  public ClustalxColourScheme()
   {
-    colhash.put(Color.RED, new Color(0.9f, 0.2f, 0.1f));
-    colhash.put(Color.BLUE, new Color(0.5f, 0.7f, 0.9f));
-    colhash.put(Color.GREEN, new Color(0.1f, 0.8f, 0.1f));
-    colhash.put(Color.ORANGE, new Color(0.9f, 0.6f, 0.3f));
-    colhash.put(Color.CYAN, new Color(0.1f, 0.7f, 0.7f));
-    colhash.put(Color.PINK, new Color(0.9f, 0.5f, 0.5f));
-    colhash.put(Color.MAGENTA, new Color(0.8f, 0.3f, 0.8f));
-    colhash.put(Color.YELLOW, new Color(0.8f, 0.8f, 0.0f));
+
   }
 
   public ClustalxColourScheme(AnnotatedCollectionI alignment,
@@ -74,6 +92,7 @@ public class ClustalxColourScheme extends ResidueColourScheme
     alignmentChanged(alignment, hiddenReps);
   }
 
+  @Override
   public void alignmentChanged(AnnotatedCollectionI alignment,
           Map<SequenceI, SequenceCollectionI> hiddenReps)
   {
@@ -83,29 +102,15 @@ public class ClustalxColourScheme extends ResidueColourScheme
     includeGaps = isIncludeGaps(); // does nothing - TODO replace with call to
     // get the current setting of the
     // includeGaps param.
-    int start = 0;
-
-    // Initialize the array
-    for (int j = 0; j < 24; j++)
-    {
-      for (int i = 0; i < maxWidth; i++)
-      {
-        cons2[i][j] = 0;
-      }
-    }
-
-    int res;
-    int i;
-    int j = 0;
-    char[] seq;
+    int res = 0;
 
     for (SequenceI sq : seqs)
     {
-      seq = sq.getSequence();
+      char[] seq = sq.getSequence();
 
       int end_j = seq.length - 1;
 
-      for (i = start; i <= end_j; i++)
+      for (int i = 0; i <= end_j; i++)
       {
         if ((seq.length - 1) < i)
         {
@@ -115,18 +120,15 @@ public class ClustalxColourScheme extends ResidueColourScheme
         {
           res = ResidueProperties.aaIndex[seq[i]];
         }
-
         cons2[i][res]++;
       }
-
-      j++;
     }
 
     this.size = seqs.size();
     makeColours();
   }
 
-  public void makeColours()
+  void makeColours()
   {
     conses[0] = new Consensus("WLVIMAFCYHP", SIXTY);
     conses[1] = new Consensus("WLVIMAFCYHP", EIGHTY);
@@ -167,15 +169,15 @@ public class ClustalxColourScheme extends ResidueColourScheme
 
     Consensus[] tmp8 = new Consensus[1];
     tmp8[0] = conses[30]; // G
-    colours[7] = new ConsensusColour(colhash.get(Color.ORANGE), tmp8);
+    colours[7] = new ConsensusColour(ClustalColour.ORANGE, tmp8);
 
     Consensus[] tmp9 = new Consensus[1];
     tmp9[0] = conses[31]; // P
-    colours[8] = new ConsensusColour(colhash.get(Color.YELLOW), tmp9);
+    colours[8] = new ConsensusColour(ClustalColour.YELLOW, tmp9);
 
     Consensus[] tmp10 = new Consensus[1];
     tmp10[0] = conses[27]; // C
-    colours[9] = new ConsensusColour(colhash.get(Color.PINK), tmp8);
+    colours[9] = new ConsensusColour(ClustalColour.PINK, tmp8);
 
     Consensus[] tmp1 = new Consensus[14];
     tmp1[0] = conses[0]; // %
@@ -192,9 +194,9 @@ public class ClustalxColourScheme extends ResidueColourScheme
     tmp1[11] = conses[25]; // Y
     tmp1[12] = conses[18]; // P
     tmp1[13] = conses[19]; // p
-    colours[0] = new ConsensusColour(colhash.get(Color.BLUE), tmp1);
+    colours[0] = new ConsensusColour(ClustalColour.BLUE, tmp1);
 
-    colours[10] = new ConsensusColour(colhash.get(Color.CYAN), tmp1);
+    colours[10] = new ConsensusColour(ClustalColour.CYAN, tmp1);
 
     Consensus[] tmp2 = new Consensus[5];
     tmp2[0] = conses[8]; // t
@@ -202,14 +204,14 @@ public class ClustalxColourScheme extends ResidueColourScheme
     tmp2[2] = conses[22]; // T
     tmp2[3] = conses[0]; // %
     tmp2[4] = conses[1]; // #
-    colours[1] = new ConsensusColour(colhash.get(Color.GREEN), tmp2);
+    colours[1] = new ConsensusColour(ClustalColour.GREEN, tmp2);
 
     Consensus[] tmp3 = new Consensus[3];
 
     tmp3[0] = conses[17]; // N
     tmp3[1] = conses[29]; // D
     tmp3[2] = conses[5]; // n
-    colours[2] = new ConsensusColour(colhash.get(Color.GREEN), tmp3);
+    colours[2] = new ConsensusColour(ClustalColour.GREEN, tmp3);
 
     Consensus[] tmp4 = new Consensus[6];
     tmp4[0] = conses[6]; // q = QE
@@ -218,14 +220,14 @@ public class ClustalxColourScheme extends ResidueColourScheme
     tmp4[3] = conses[3]; // +
     tmp4[4] = conses[28]; // K
     tmp4[5] = conses[20]; // R
-    colours[3] = new ConsensusColour(colhash.get(Color.GREEN), tmp4);
+    colours[3] = new ConsensusColour(ClustalColour.GREEN, tmp4);
 
     Consensus[] tmp5 = new Consensus[4];
     tmp5[0] = conses[3]; // +
     tmp5[1] = conses[28]; // K
     tmp5[2] = conses[20]; // R
     tmp5[3] = conses[19]; // Q
-    colours[4] = new ConsensusColour(colhash.get(Color.RED), tmp5);
+    colours[4] = new ConsensusColour(ClustalColour.RED, tmp5);
 
     Consensus[] tmp6 = new Consensus[6];
     tmp6[0] = conses[3]; // -
@@ -234,7 +236,7 @@ public class ClustalxColourScheme extends ResidueColourScheme
     tmp6[3] = conses[6]; // QE
     tmp6[4] = conses[19]; // Q
     tmp6[5] = conses[2]; // DE
-    colours[5] = new ConsensusColour(colhash.get(Color.MAGENTA), tmp6);
+    colours[5] = new ConsensusColour(ClustalColour.MAGENTA, tmp6);
 
     Consensus[] tmp7 = new Consensus[5];
     tmp7[0] = conses[3]; // -
@@ -242,7 +244,7 @@ public class ClustalxColourScheme extends ResidueColourScheme
     tmp7[2] = conses[10]; // E
     tmp7[3] = conses[17]; // N
     tmp7[4] = conses[2]; // DE
-    colours[6] = new ConsensusColour(colhash.get(Color.MAGENTA), tmp7);
+    colours[6] = new ConsensusColour(ClustalColour.MAGENTA, tmp7);
 
     // Now attach the ConsensusColours to the residue letters
     residueColour = new ConsensusColour[20];
@@ -275,48 +277,45 @@ public class ClustalxColourScheme extends ResidueColourScheme
   }
 
   @Override
-  public Color findColour(char c, int j, SequenceI seq)
+  protected Color findColour(char c, int j, SequenceI seq)
   {
-    Color currentColour;
-
-    if (cons2.length <= j
-            || (includeGaps && threshold != 0 && !aboveThreshold(c, j)))
+    // TODO why the test for includeGaps here?
+    if (cons2.length <= j || Comparison.isGap(c)
+    /*|| (includeGaps && threshold != 0 && !aboveThreshold(c, j))*/)
     {
       return Color.white;
     }
 
     int i = ResidueProperties.aaIndex[c];
 
-    currentColour = Color.white;
+    Color colour = Color.white;
 
     if (i > 19)
     {
-      return currentColour;
+      return colour;
     }
 
-    for (int k = 0; k < residueColour[i].conses.length; k++)
+    for (int k = 0; k < residueColour[i].cons.length; k++)
     {
-      if (residueColour[i].conses[k].isConserved(cons2, j, size,
+      if (residueColour[i].cons[k].isConserved(cons2, j, size,
               includeGaps))
       {
-        currentColour = residueColour[i].c;
+        colour = residueColour[i].c;
       }
     }
 
     if (i == 4)
     {
+      /*
+       * override to colour C pink if >85% conserved
+       */
       if (conses[27].isConserved(cons2, j, size, includeGaps))
       {
-        currentColour = colhash.get(Color.PINK);
+        colour = ClustalColour.PINK.colour;
       }
     }
 
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
-    }
-
-    return currentColour;
+    return colour;
   }
 
   /**
@@ -337,7 +336,7 @@ public class ClustalxColourScheme extends ResidueColourScheme
   }
 
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
+  public ColourSchemeI getInstance(AnnotatedCollectionI sg,
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
     ClustalxColourScheme css = new ClustalxColourScheme(sg,
@@ -345,19 +344,22 @@ public class ClustalxColourScheme extends ResidueColourScheme
     css.includeGaps = includeGaps;
     return css;
   }
-}
 
-class ConsensusColour
-{
-  Consensus[] conses;
-
-  Color c;
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
 
-  public ConsensusColour(Color c, Consensus[] conses)
+  @Override
+  public String getSchemeName()
   {
-    this.conses = conses;
+    return JalviewColourScheme.Clustal.toString();
+  }
 
-    // this.list = list;
-    this.c = c;
+  @Override
+  public boolean isSimple()
+  {
+    return false;
   }
 }
index da99a4a..f16ca21 100755 (executable)
@@ -21,7 +21,6 @@
 package jalview.schemes;
 
 import jalview.datamodel.AnnotatedCollectionI;
-import jalview.datamodel.ProfilesI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
 
@@ -31,99 +30,72 @@ import java.util.Map;
 public interface ColourSchemeI
 {
   /**
+   * Returns the possibly context dependent colour for the given symbol at the
+   * aligned position in the given sequence. For example, the colour may depend
+   * on the symbol's relationship to the consensus residue for the column.
    * 
-   * @param c
-   * @return the colour for the given character
-   */
-  public Color findColour(char c);
-
-  /**
-   * 
-   * @param c
-   *          - sequence symbol or gap
-   * @param j
-   *          - position in seq
+   * @param symbol
+   * @param position
    * @param seq
-   *          - sequence being coloured
-   * @return context dependent colour for the given symbol at the position in
-   *         the given sequence
-   */
-  public Color findColour(char c, int j, SequenceI seq);
-
-  /**
-   * assign the given consensus profile for the colourscheme
-   */
-  public void setConsensus(ProfilesI hconsensus);
-
-  /**
-   * assign the given conservation to the colourscheme
-   * 
-   * @param c
-   */
-  public void setConservation(jalview.analysis.Conservation c);
-
-  /**
-   * enable or disable conservation shading for this colourscheme
-   * 
-   * @param conservationApplied
-   */
-  public void setConservationApplied(boolean conservationApplied);
-
-  /**
-   * 
-   * @return true if conservation shading is enabled for this colourscheme
+   * @param consensusResidue
+   *          the modal symbol (e.g. K) or symbols (e.g. KF) for the column
+   * @param pid
+   *          the percentage identity of the column's consensus (if known)
+   * @return
    */
-  public boolean conservationApplied();
-
-  /**
-   * set scale factor for bleaching of colour in unconserved regions
-   * 
-   * @param i
-   */
-  public void setConservationInc(int i);
+  Color findColour(char symbol, int position, SequenceI seq,
+          String consensusResidue, float pid);
 
   /**
+   * Recalculate dependent data using the given sequence collection, taking
+   * account of hidden rows
    * 
-   * @return scale factor for bleaching colour in unconserved regions
+   * @param alignment
+   * @param hiddenReps
    */
-  public int getConservationInc();
+  void alignmentChanged(AnnotatedCollectionI alignment,
+          Map<SequenceI, SequenceCollectionI> hiddenReps);
 
   /**
+   * Creates and returns a new instance of the colourscheme configured to colour
+   * the given collection. Note that even simple colour schemes should return a
+   * new instance for each call to this method, as different instances may have
+   * differing shading by consensus or percentage identity applied.
    * 
-   * @return percentage identity threshold for applying colourscheme
+   * @param sg
+   * @param hiddenRepSequences
+   * @return copy of current scheme with any inherited settings transferred
    */
-  public int getThreshold();
+  ColourSchemeI getInstance(AnnotatedCollectionI sg,
+          Map<SequenceI, SequenceCollectionI> hiddenRepSequences);
 
   /**
-   * set percentage identity threshold and type of %age identity calculation for
-   * shading
+   * Answers true if the colour scheme is suitable for the given data, else
+   * false. For example, some colour schemes are specific to either peptide or
+   * nucleotide, or only apply if certain kinds of annotation are present.
    * 
-   * @param ct
-   *          0..100 percentage identity for applying this colourscheme
-   * @param ignoreGaps
-   *          when true, calculate PID without including gapped positions
+   * @param ac
+   * @return
    */
-  public void setThreshold(int ct, boolean ignoreGaps);
+  // TODO can make this method static in Java 8
+  boolean isApplicableTo(AnnotatedCollectionI ac);
 
   /**
-   * recalculate dependent data using the given sequence collection, taking
-   * account of hidden rows
+   * Answers the 'official' name of the colour scheme (as used, for example, as
+   * a Jalview startup parameter)
    * 
-   * @param alignment
-   * @param hiddenReps
+   * @return
    */
-  public void alignmentChanged(AnnotatedCollectionI alignment,
-          Map<SequenceI, SequenceCollectionI> hiddenReps);
+  String getSchemeName();
 
   /**
-   * create a new instance of the colourscheme configured to colour the given
-   * connection
+   * Answers true if the colour scheme depends only on the sequence symbol, and
+   * not on other information such as alignment consensus or annotation. (Note
+   * that simple colour schemes may have a fading by percentage identity or
+   * conservation overlaid.) Simple colour schemes can be propagated to
+   * structure viewers.
    * 
-   * @param sg
-   * @param hiddenRepSequences
-   * @return copy of current scheme with any inherited settings transfered
+   * @return
    */
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
-          Map<SequenceI, SequenceCollectionI> hiddenRepSequences);
-
+  boolean isSimple();
 }
diff --git a/src/jalview/schemes/ColourSchemeLoader.java b/src/jalview/schemes/ColourSchemeLoader.java
new file mode 100644 (file)
index 0000000..8660f3e
--- /dev/null
@@ -0,0 +1,125 @@
+package jalview.schemes;
+
+import jalview.binding.JalviewUserColours;
+
+import java.awt.Color;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+
+import org.exolab.castor.xml.Unmarshaller;
+
+public class ColourSchemeLoader
+{
+
+  /**
+   * Loads a user defined colour scheme from file. The file should contain a
+   * definition of residue colours in XML format as defined in
+   * JalviewUserColours.xsd.
+   * 
+   * @param filePath
+   * 
+   * @return
+   */
+  public static UserColourScheme loadColourScheme(String filePath)
+  {
+    UserColourScheme ucs = null;
+    Color[] newColours = null;
+    File file = new File(filePath);
+    try
+    {
+      InputStreamReader in = new InputStreamReader(
+              new FileInputStream(file), "UTF-8");
+  
+      jalview.schemabinding.version2.JalviewUserColours jucs = new jalview.schemabinding.version2.JalviewUserColours();
+  
+      org.exolab.castor.xml.Unmarshaller unmar = new org.exolab.castor.xml.Unmarshaller(
+              jucs);
+      jucs = (jalview.schemabinding.version2.JalviewUserColours) unmar
+              .unmarshal(in);
+  
+      /*
+       * non-case-sensitive colours are for 20 amino acid codes,
+       * B, Z, X and Gap
+       * optionally, lower-case alternatives for all except Gap
+       */
+      newColours = new Color[24];
+      Color[] lowerCase = new Color[23];
+      boolean caseSensitive = false;
+  
+      String name;
+      int index;
+      for (int i = 0; i < jucs.getColourCount(); i++)
+      {
+        name = jucs.getColour(i).getName();
+        if (ResidueProperties.aa3Hash.containsKey(name))
+        {
+          index = ResidueProperties.aa3Hash.get(name).intValue();
+        }
+        else
+        {
+          index = ResidueProperties.aaIndex[name.charAt(0)];
+        }
+        if (index == -1)
+        {
+          continue;
+        }
+  
+        Color color = new Color(Integer.parseInt(jucs.getColour(i)
+                .getRGB(), 16));
+        if (name.toLowerCase().equals(name))
+        {
+          caseSensitive = true;
+          lowerCase[index] = color;
+        }
+        else
+        {
+          newColours[index] = color;
+        }
+      }
+  
+      /*
+       * instantiate the colour scheme
+       */
+      ucs = new UserColourScheme(newColours);
+      ucs.setName(jucs.getSchemeName());
+      if (caseSensitive)
+      {
+        ucs.setLowerCaseColours(lowerCase);
+      }
+    } catch (Exception ex)
+    {
+      // Could be old Jalview Archive format
+      try
+      {
+        InputStreamReader in = new InputStreamReader(new FileInputStream(
+                file), "UTF-8");
+  
+        jalview.binding.JalviewUserColours jucs = new jalview.binding.JalviewUserColours();
+  
+        jucs = JalviewUserColours.unmarshal(in);
+  
+        newColours = new Color[jucs.getColourCount()];
+  
+        for (int i = 0; i < 24; i++)
+        {
+          newColours[i] = new Color(Integer.parseInt(jucs.getColour(i)
+                  .getRGB(), 16));
+        }
+        ucs = new UserColourScheme(newColours);
+        ucs.setName(jucs.getSchemeName());
+      } catch (Exception ex2)
+      {
+        ex2.printStackTrace();
+      }
+  
+      if (newColours == null)
+      {
+        System.out.println("Error loading User ColourFile\n" + ex);
+      }
+    }
+  
+    return ucs;
+  }
+
+}
index 0d9c39d..3e8e87a 100755 (executable)
@@ -21,6 +21,7 @@
 package jalview.schemes;
 
 import jalview.datamodel.AnnotatedCollectionI;
+import jalview.util.ColorUtils;
 
 import java.awt.Color;
 
@@ -38,564 +39,68 @@ import java.awt.Color;
  */
 public class ColourSchemeProperty
 {
-  /** Undefined Colourscheme Index */
-  public static final int UNDEFINED = -1;
-
-  /** for schemes defined on the fly */
-  public static final int USER_DEFINED = 0;
-
-  /** No Colourscheme Index */
-  public static final int NONE = 1;
-
-  /** DOCUMENT ME!! */
-  public static final int CLUSTAL = 2;
-
-  /** DOCUMENT ME!! */
-  public static final int BLOSUM = 3;
-
-  /** DOCUMENT ME!! */
-  public static final int PID = 4;
-
-  /** DOCUMENT ME!! */
-  public static final int ZAPPO = 5;
-
-  /** DOCUMENT ME!! */
-  public static final int TAYLOR = 6;
-
-  /** DOCUMENT ME!! */
-  public static final int HYDROPHOBIC = 7;
-
-  /** DOCUMENT ME!! */
-  public static final int HELIX = 8;
-
-  /** DOCUMENT ME!! */
-  public static final int STRAND = 9;
-
-  /** DOCUMENT ME!! */
-  public static final int TURN = 10;
-
-  /** DOCUMENT ME!! */
-  public static final int BURIED = 11;
-
-  /** DOCUMENT ME!! */
-  public static final int NUCLEOTIDE = 12;
 
   /**
-   * purine/pyrimidine
-   */
-  public static final int PURINEPYRIMIDINE = 13;
-
-  public static final int COVARIATION = 14;
-
-  public static final int TCOFFEE = 15;
-
-  public static final int RNAHELIX = 16;
-
-  public static final int RNAINTERACTION = 17;
-
-  /**
-   * index of first colourscheme (includes 'None')
-   */
-  public static final int FIRST_COLOUR = NONE;
-
-  public static final int LAST_COLOUR = RNAINTERACTION;
-
-  /**
-   * DOCUMENT ME!
+   * Returns a colour scheme for the given name, with which the given data may
+   * be coloured. The name is not case-sensitive, and may be one of
+   * <ul>
+   * <li>any currently registered colour scheme; Jalview by default provides</li>
+   * <ul>
+   * <li>Clustal</li>
+   * <li>Blosum62</li>
+   * <li>% Identity</li>
+   * <li>Hydrophobic</li>
+   * <li>Zappo</li>
+   * <li>Taylor</li>
+   * <li>Helix Propensity</li>
+   * <li>Strand Propensity</li>
+   * <li>Turn Propensity</li>
+   * <li>Buried Index</li>
+   * <li>Nucleotide</li>
+   * <li>Purine/Pyrimidine</li>
+   * <li>T-Coffee Scores</li>
+   * <li>RNA Helices</li>
+   * </ul>
+   * <li>the name of a programmatically added colour scheme</li> <li>an AWT
+   * colour name e.g. red</li> <li>an AWT hex rgb colour e.g. ff2288</li> <li>
+   * residue colours list e.g. D,E=red;K,R,H=0022FF;c=yellow</li> </ul>
    * 
-   * @param name
-   *          DOCUMENT ME!
+   * If none of these formats is matched, the string is converted to a colour
+   * using a hashing algorithm. For name "None", returns null.
    * 
-   * @return DOCUMENT ME!
+   * @param forData
+   * @param name
+   * @return
    */
-  public static int getColourIndexFromName(String name)
+  public static ColourSchemeI getColourScheme(AnnotatedCollectionI forData,
+          String name)
   {
-    int ret = UNDEFINED;
-
-    if (name.equalsIgnoreCase("Clustal"))
+    if (ResidueColourScheme.NONE.equalsIgnoreCase(name))
     {
-      ret = CLUSTAL;
-    }
-    else if (name.equalsIgnoreCase("Blosum62"))
-    {
-      ret = BLOSUM;
-    }
-    else if (name.equalsIgnoreCase("% Identity"))
-    {
-      ret = PID;
-    }
-    else if (name.equalsIgnoreCase("Zappo"))
-    {
-      ret = ZAPPO;
-    }
-    else if (name.equalsIgnoreCase("Taylor"))
-    {
-      ret = TAYLOR;
-    }
-    else if (name.equalsIgnoreCase("Hydrophobic"))
-    {
-      ret = HYDROPHOBIC;
-    }
-    else if (name.equalsIgnoreCase("Helix Propensity"))
-    {
-      ret = HELIX;
-    }
-    else if (name.equalsIgnoreCase("Strand Propensity"))
-    {
-      ret = STRAND;
-    }
-    else if (name.equalsIgnoreCase("Turn Propensity"))
-    {
-      ret = TURN;
-    }
-    else if (name.equalsIgnoreCase("Buried Index"))
-    {
-      ret = BURIED;
-    }
-    else if (name.equalsIgnoreCase("Nucleotide"))
-    {
-      ret = NUCLEOTIDE;
-    }
-    else if (name.equalsIgnoreCase("T-Coffee Scores"))
-    {
-      ret = TCOFFEE;
-    }
+      return null;
 
-    else if (name.equalsIgnoreCase("User Defined"))
-    {
-      ret = USER_DEFINED;
-    }
-    else if (name.equalsIgnoreCase("None"))
-    {
-      ret = NONE;
-    }
-    else if (name.equalsIgnoreCase("Purine/Pyrimidine"))
-    {
-      ret = PURINEPYRIMIDINE;
-    }
-    else if (name.equalsIgnoreCase("RNA Interaction type"))
-    {
-      ret = RNAINTERACTION;
-    }
-    else if (name.equalsIgnoreCase("RNA Helices"))
-    {
-      ret = RNAHELIX;
     }
-    // else if (name.equalsIgnoreCase("Covariation"))
-    // {
-    // ret = COVARIATION;
-    // }
-
-    return ret;
-  }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param cs
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public static String getColourName(ColourSchemeI cs)
-  {
-
-    int index = NONE;
-
-    if (cs instanceof ClustalxColourScheme)
-    {
-      index = CLUSTAL;
-    }
-    else if (cs instanceof Blosum62ColourScheme)
-    {
-      index = BLOSUM;
-    }
-    else if (cs instanceof PIDColourScheme)
-    {
-      index = PID;
-    }
-    else if (cs instanceof ZappoColourScheme)
-    {
-      index = ZAPPO;
-    }
-    else if (cs instanceof TaylorColourScheme)
-    {
-      index = TAYLOR;
-    }
-    else if (cs instanceof HydrophobicColourScheme)
-    {
-      index = HYDROPHOBIC;
-    }
-    else if (cs instanceof HelixColourScheme)
-    {
-      index = HELIX;
-    }
-    else if (cs instanceof StrandColourScheme)
-    {
-      index = STRAND;
-    }
-    else if (cs instanceof TurnColourScheme)
-    {
-      index = TURN;
-    }
-    else if (cs instanceof BuriedColourScheme)
-    {
-      index = BURIED;
-    }
-    else if (cs instanceof NucleotideColourScheme)
-    {
-      index = NUCLEOTIDE;
-    }
-    else if (cs instanceof PurinePyrimidineColourScheme)
-    {
-      index = PURINEPYRIMIDINE;
-    }
-    else if (cs instanceof TCoffeeColourScheme)
-    {
-      index = TCOFFEE;
-    }
-    else if (cs instanceof RNAHelicesColour)
-    {
-      index = RNAHELIX;
-    }
     /*
-     * else if (cs instanceof CovariationColourScheme) { index = COVARIATION; }
+     * if this is the name of a registered colour scheme, just
+     * create a new instance of it
      */
-    else if (cs instanceof UserColourScheme)
+    ColourSchemeI scheme = ColourSchemes.getInstance().getColourScheme(
+            name, forData, null);
+    if (scheme != null)
     {
-      if ((((UserColourScheme) cs).getName() != null)
-              && (((UserColourScheme) cs).getName().length() > 0))
-      {
-        return ((UserColourScheme) cs).getName();
-      }
-      // get default colourscheme name
-      index = USER_DEFINED;
+      return scheme;
     }
 
-    return getColourName(index);
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param index
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public static String getColourName(int index)
-  {
-    String ret = null;
-
-    switch (index)
-    {
-    case CLUSTAL:
-      ret = "Clustal";
-
-      break;
-
-    case BLOSUM:
-      ret = "Blosum62";
-
-      break;
-
-    case PID:
-      ret = "% Identity";
-
-      break;
-
-    case ZAPPO:
-      ret = "Zappo";
-
-      break;
-
-    case TAYLOR:
-      ret = "Taylor";
-      break;
-
-    case HYDROPHOBIC:
-      ret = "Hydrophobic";
-
-      break;
-
-    case HELIX:
-      ret = "Helix Propensity";
-
-      break;
-
-    case STRAND:
-      ret = "Strand Propensity";
-
-      break;
-
-    case TURN:
-      ret = "Turn Propensity";
-
-      break;
-
-    case BURIED:
-      ret = "Buried Index";
-
-      break;
-
-    case NUCLEOTIDE:
-      ret = "Nucleotide";
-
-      break;
-
-    case PURINEPYRIMIDINE:
-      ret = "Purine/Pyrimidine";
-
-      break;
-
-    case TCOFFEE:
-      ret = "T-Coffee Scores";
-
-      break;
-
-    case RNAINTERACTION:
-      ret = "RNA Interaction type";
-
-      break;
-    case RNAHELIX:
-      ret = "RNA Helices";
-
-      break;
     /*
-     * case COVARIATION: ret = "Covariation";
-     * 
-     * break;
+     * try to parse the string as a residues colour scheme
+     * e.g. A=red;T,G=blue etc
+     * else parse the name as a colour specification
+     * e.g. "red" or "ff00ed",
+     * or failing that hash the name to a colour
      */
-    case USER_DEFINED:
-      ret = "User Defined";
-
-      break;
-
-    default:
-      ret = "None";
-
-      break;
-    }
-
-    return ret;
-  }
-
-  /**
-   * retrieve or create colourscheme associated with name
-   * 
-   * @param seqs
-   *          sequences to colour
-   * @param width
-   *          range of sequences to colour
-   * @param name
-   *          colourscheme name, applet colour parameter specification, or
-   *          string to parse as colour for new coloursheme
-   * @return Valid Colourscheme
-   */
-  public static ColourSchemeI getColour(AnnotatedCollectionI alignment,
-          String name)
-  {
-    int colindex = getColourIndexFromName(name);
-    if (colindex == UNDEFINED)
-    {
-      if (name.indexOf('=') == -1)
-      {
-        // try to build a colour from the string directly
-        try
-        {
-          return new UserColourScheme(name);
-        } catch (Exception e)
-        {
-          // System.err.println("Ignoring unknown colourscheme name");
-        }
-      }
-      else
-      {
-        // try to parse the string as a residue colourscheme
-        try
-        {
-          // fix the launchApp user defined coloursheme transfer bug
-          jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(
-                  "white");
-          ucs.parseAppletParameter(name);
-
-        } catch (Exception e)
-        {
-          // System.err.println("Ignoring exception when parsing colourscheme as applet-parameter");
-        }
-      }
-    }
-    return getColour(alignment, getColourIndexFromName(name));
-  }
-
-  /**
-   * Construct an instance of ColourSchemeI corresponding to the given
-   * colourscheme index
-   * 
-   * @param seqs
-   *          sequences to be coloured by colourscheme
-   * @param width
-   *          geometry of alignment
-   * @param index
-   *          colourscheme number
-   * 
-   * @return null or an instance of the colourscheme configured to colour given
-   *         sequence set
-   */
-  public static ColourSchemeI getColour(
-          jalview.datamodel.AnnotatedCollectionI coll, int index)
-  {
-    // TODO 3.0 2.8 refactor signature to take an alignmentI like container so
-    // colourschemes based on annotation can be initialised
-    ColourSchemeI cs = null;
-
-    switch (index)
-    {
-    case CLUSTAL:
-      cs = new ClustalxColourScheme(coll, null);
-
-      break;
-
-    case BLOSUM:
-      cs = new Blosum62ColourScheme();
-
-      break;
-
-    case PID:
-      cs = new PIDColourScheme();
-
-      break;
-
-    case ZAPPO:
-      cs = new ZappoColourScheme();
-
-      break;
-
-    case TAYLOR:
-      cs = new TaylorColourScheme();
-      break;
-
-    case HYDROPHOBIC:
-      cs = new HydrophobicColourScheme();
-
-      break;
-
-    case HELIX:
-      cs = new HelixColourScheme();
-
-      break;
-
-    case STRAND:
-      cs = new StrandColourScheme();
-
-      break;
-
-    case TURN:
-      cs = new TurnColourScheme();
-
-      break;
-
-    case BURIED:
-      cs = new BuriedColourScheme();
-
-      break;
-
-    case NUCLEOTIDE:
-      cs = new NucleotideColourScheme();
-
-      break;
-
-    case PURINEPYRIMIDINE:
-      cs = new PurinePyrimidineColourScheme();
-
-      break;
-
-    case TCOFFEE:
-      cs = new TCoffeeColourScheme(coll);
-      break;
-
-    case RNAHELIX:
-      cs = new RNAHelicesColour(coll);
-      break;
-
-    // case COVARIATION:
-    // cs = new CovariationColourScheme(annotation);
-    // break;
-
-    case USER_DEFINED:
-      Color[] col = new Color[24];
-      for (int i = 0; i < 24; i++)
-      {
-        col[i] = Color.white;
-      }
-      cs = new UserColourScheme(col);
-      break;
-
-    default:
-      break;
-    }
-
-    return cs;
-  }
-
-  public static Color getAWTColorFromName(String name)
-  {
-    Color col = null;
-    name = name.toLowerCase();
-    if (name.equals("black"))
-    {
-      col = Color.black;
-    }
-    else if (name.equals("blue"))
-    {
-      col = Color.blue;
-    }
-    else if (name.equals("cyan"))
-    {
-      col = Color.cyan;
-    }
-    else if (name.equals("darkGray"))
-    {
-      col = Color.darkGray;
-    }
-    else if (name.equals("gray"))
-    {
-      col = Color.gray;
-    }
-    else if (name.equals("green"))
-    {
-      col = Color.green;
-    }
-    else if (name.equals("lightGray"))
-    {
-      col = Color.lightGray;
-    }
-    else if (name.equals("magenta"))
-    {
-      col = Color.magenta;
-    }
-    else if (name.equals("orange"))
-    {
-      col = Color.orange;
-    }
-    else if (name.equals("pink"))
-    {
-      col = Color.pink;
-    }
-    else if (name.equals("red"))
-    {
-      col = Color.red;
-    }
-    else if (name.equals("white"))
-    {
-      col = Color.white;
-    }
-    else if (name.equals("yellow"))
-    {
-      col = Color.yellow;
-    }
-
-    return col;
+    UserColourScheme ucs = new UserColourScheme(name);
+    return ucs;
   }
 
   public static Color rnaHelices[] = null;
@@ -621,17 +126,28 @@ public class ColourSchemeProperty
     // Generate random colors and store
     for (; j <= n; j++)
     {
-      rnaHelices[j] = jalview.util.ColorUtils
-              .generateRandomColor(Color.white);
+      rnaHelices[j] = ColorUtils.generateRandomColor(Color.white);
     }
   }
 
   /**
-   * delete the existing cached RNA helces colours
+   * delete the existing cached RNA helices colours
    */
   public static void resetRnaHelicesShading()
   {
     rnaHelices = null;
   }
 
+  /**
+   * Returns the name of the colour scheme (or "None" if it is null)
+   * 
+   * @param cs
+   * @return
+   */
+  public static String getColourName(ColourSchemeI cs)
+  {
+    return cs == null ? ResidueColourScheme.NONE : cs
+            .getSchemeName();
+  }
+
 }
diff --git a/src/jalview/schemes/ColourSchemes.java b/src/jalview/schemes/ColourSchemes.java
new file mode 100644 (file)
index 0000000..269811b
--- /dev/null
@@ -0,0 +1,170 @@
+package jalview.schemes;
+
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ColourSchemes
+{
+  /*
+   * singleton instance of this class
+   */
+  private static ColourSchemes instance = new ColourSchemes();
+
+  /*
+   * a map from scheme name (lower-cased) to an instance of it
+   */
+  private Map<String, ColourSchemeI> schemes;
+
+  /**
+   * Returns the singleton instance of this class
+   * 
+   * @return
+   */
+  public static ColourSchemes getInstance()
+  {
+    return instance;
+  }
+
+  private ColourSchemes()
+  {
+    loadColourSchemes();
+  }
+
+  /**
+   * Loads an instance of each standard or user-defined colour scheme
+   * 
+   * @return
+   */
+  void loadColourSchemes()
+  {
+    /*
+     * store in an order-preserving map, so items can be added to menus 
+     * in the order in which they are 'discovered'
+     */
+    schemes = new LinkedHashMap<String, ColourSchemeI>();
+
+    for (JalviewColourScheme cs : JalviewColourScheme.values())
+    {
+      try
+      {
+        registerColourScheme(cs.getSchemeClass().newInstance());
+      } catch (InstantiationException | IllegalAccessException e)
+      {
+        System.err.println("Error instantiating colour scheme for "
+                + cs.toString() + " " + e.getMessage());
+      }
+    }
+  }
+
+  /**
+   * Registers a colour scheme
+   * 
+   * @param cs
+   */
+  public void registerColourScheme(ColourSchemeI cs)
+  {
+    String name = cs.getSchemeName();
+    if (name == null)
+    {
+      System.err.println("ColourScheme name may not be null");
+      return;
+    }
+
+    /*
+     * name is lower-case for non-case-sensitive lookup
+     * (name in the colour keeps its true case)
+     */
+    String lower = name.toLowerCase();
+    if (schemes.containsKey(lower))
+    {
+      System.err
+              .println("Warning: overwriting colour scheme named " + name);
+    }
+    schemes.put(lower, cs);
+  }
+
+  /**
+   * Removes a colour scheme by name
+   * 
+   * @param name
+   */
+  public void removeColourScheme(String name)
+  {
+    if (name != null)
+    {
+      schemes.remove(name.toLowerCase());
+    }
+  }
+  
+  /**
+   * Returns an instance of the colour scheme with which the given view may be
+   * coloured
+   * 
+   * @param name
+   *          name of the colour scheme
+   * @param forData
+   *          the data to be coloured
+   * @param optional
+   *          map from hidden representative sequences to the sequences they
+   *          represent
+   * @return
+   */
+  public ColourSchemeI getColourScheme(String name,
+          AnnotatedCollectionI forData,
+          Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
+  {
+    if (name == null)
+    {
+      return null;
+    }
+    ColourSchemeI cs = schemes.get(name.toLowerCase());
+    return cs == null ? null : cs.getInstance(forData, hiddenRepSequences);
+  }
+
+  /**
+   * Returns an instance of the colour scheme with which the given view may be
+   * coloured
+   * 
+   * @param name
+   *          name of the colour scheme
+   * @param forData
+   *          the data to be coloured
+   * @return
+   */
+  public ColourSchemeI getColourScheme(String name,
+          AnnotatedCollectionI forData)
+  {
+    return getColourScheme(name, forData, null);
+  }
+
+  /**
+   * Returns an iterable set of the colour schemes, in the order in which they
+   * were added
+   * 
+   * @return
+   */
+  public Iterable<ColourSchemeI> getColourSchemes()
+  {
+    return schemes.values();
+  }
+
+  /**
+   * Answers true if there is a scheme with the given name, else false. The test
+   * is not case-sensitive.
+   * 
+   * @param name
+   * @return
+   */
+  public boolean nameExists(String name)
+  {
+    if (name == null)
+    {
+      return false;
+    }
+    return schemes.containsKey(name.toLowerCase());
+  }
+}
index 49d5dee..6cb095b 100644 (file)
 package jalview.schemes;
 
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+import jalview.util.ColorUtils;
 
 import java.awt.Color;
 import java.util.Hashtable;
+import java.util.Map;
 
 /**
  * Became RNAHelicesColour.java. Placeholder for true covariation color scheme
@@ -33,15 +38,26 @@ import java.util.Hashtable;
  */
 public class CovariationColourScheme extends ResidueColourScheme
 {
-  public Hashtable helixcolorhash = new Hashtable();
+  public Map<String, Color> helixcolorhash = new Hashtable<String, Color>();
 
-  public Hashtable positionsToHelix = new Hashtable();
+  public Map<Integer, String> positionsToHelix = new Hashtable<Integer, String>();
 
   int numHelix = 0;
 
   public AlignmentAnnotation annotation;
 
   /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new CovariationColourScheme(coll.getAlignmentAnnotation()[0]);
+  }
+
+  /**
    * Creates a new CovariationColourScheme object.
    */
   public CovariationColourScheme(AlignmentAnnotation annotation)
@@ -71,8 +87,8 @@ public class CovariationColourScheme extends ResidueColourScheme
 
     for (int j = 0; j <= numHelix; j++)
     {
-      helixcolorhash.put(Integer.toString(j),
-              jalview.util.ColorUtils.generateRandomColor(Color.white));
+      helixcolorhash.put(String.valueOf(j),
+              ColorUtils.generateRandomColor(Color.white));
     }
 
   }
@@ -85,6 +101,7 @@ public class CovariationColourScheme extends ResidueColourScheme
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Color findColour(char c)
   {
     // System.out.println("called"); log.debug
@@ -108,12 +125,12 @@ public class CovariationColourScheme extends ResidueColourScheme
     Color currentColour = Color.white;
     String currentHelix = null;
     // System.out.println(c + " " + j);
-    currentHelix = (String) positionsToHelix.get(j);
+    currentHelix = positionsToHelix.get(j);
     // System.out.println(positionsToHelix.get(j));
 
     if (currentHelix != null)
     {
-      currentColour = (Color) helixcolorhash.get(currentHelix);
+      currentColour = helixcolorhash.get(currentHelix);
     }
 
     // System.out.println(c + " " + j + " helix " + currentHelix + " " +
@@ -121,4 +138,21 @@ public class CovariationColourScheme extends ResidueColourScheme
     return currentColour;
   }
 
+  @Override
+  public boolean isNucleotideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return "Covariation";
+  }
+
+  @Override
+  public boolean isSimple()
+  {
+    return false;
+  }
 }
index 23087a8..dbe4901 100644 (file)
@@ -22,6 +22,7 @@ package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+import jalview.util.ColorUtils;
 import jalview.util.Format;
 
 import java.awt.Color;
@@ -121,7 +122,7 @@ public class FeatureColour implements FeatureColourI
       /*
        * only a simple colour specification - parse it
        */
-      Color colour = UserColourScheme.getColourFromString(descriptor);
+      Color colour = ColorUtils.parseColourString(descriptor);
       if (colour == null)
       {
         throw new IllegalArgumentException("Invalid colour descriptor: "
@@ -212,9 +213,9 @@ public class FeatureColour implements FeatureColourI
     FeatureColour featureColour;
     try
     {
-      featureColour = new FeatureColour(
-              new UserColourScheme(mincol).findColour('A'),
-              new UserColourScheme(maxcol).findColour('A'), min, max);
+      Color minColour = ColorUtils.parseColourString(mincol);
+      Color maxColour = ColorUtils.parseColourString(maxcol);
+      featureColour = new FeatureColour(minColour, maxColour, min, max);
       featureColour.setColourByLabel(labelColour);
       featureColour.setAutoScaled(autoScaled);
       // add in any additional parameters
@@ -306,6 +307,14 @@ 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;
@@ -533,7 +542,7 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      return UserColourScheme
+      return ColorUtils
               .createColourFromName(feature.getDescription());
     }
 
index 35be31b..57c19e5 100644 (file)
  */
 package jalview.schemes;
 
-import jalview.analysis.Conservation;
-import jalview.datamodel.ProfilesI;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
+import java.util.Map;
 
 /**
  * Colourscheme that takes its colours from some other colourscheme
@@ -32,7 +35,7 @@ import jalview.datamodel.ProfilesI;
 public class FollowerColourScheme extends ResidueColourScheme
 {
 
-  protected ColourSchemeI colourScheme;
+  private ColourSchemeI colourScheme;
 
   public ColourSchemeI getBaseColour()
   {
@@ -40,30 +43,30 @@ public class FollowerColourScheme extends ResidueColourScheme
   }
 
   @Override
-  public void setConsensus(ProfilesI consensus)
+  public String getSchemeName()
   {
-    if (colourScheme != null)
-    {
-      colourScheme.setConsensus(consensus);
-    }
+    return "Follower";
   }
 
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
   @Override
-  public void setConservation(Conservation cons)
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
   {
-    if (colourScheme != null)
-    {
-      colourScheme.setConservation(cons);
-    }
+    return new FollowerColourScheme();
   }
 
-  @Override
-  public void setConservationInc(int i)
+  protected ColourSchemeI getColourScheme()
+  {
+    return colourScheme;
+  }
+
+  protected void setColourScheme(ColourSchemeI colourScheme)
   {
-    if (colourScheme != null)
-    {
-      colourScheme.setConservationInc(i);
-    }
+    this.colourScheme = colourScheme;
   }
 
 }
index 1242cd3..7123d93 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
 import java.awt.Color;
+import java.util.Map;
 
 public class HelixColourScheme extends ScoreColourScheme
 {
@@ -30,8 +35,32 @@ public class HelixColourScheme extends ScoreColourScheme
             ResidueProperties.helixmin, ResidueProperties.helixmax);
   }
 
+  @Override
   public Color makeColour(float c)
   {
     return new Color(c, (float) 1.0 - c, c);
   }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Helix.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new HelixColourScheme();
+  }
 }
index 055000f..69af3c9 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
 import java.awt.Color;
+import java.util.Map;
 
 /**
  * DOCUMENT ME!
@@ -47,8 +52,32 @@ public class HydrophobicColourScheme extends ScoreColourScheme
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Color makeColour(float c)
   {
     return new Color(c, (float) 0.0, (float) 1.0 - c);
   }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Hydrophobic.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new HydrophobicColourScheme();
+  }
 }
diff --git a/src/jalview/schemes/JalviewColourScheme.java b/src/jalview/schemes/JalviewColourScheme.java
new file mode 100644 (file)
index 0000000..185d2b4
--- /dev/null
@@ -0,0 +1,66 @@
+package jalview.schemes;
+
+
+/**
+ * An enum with the colour schemes supported by Jalview.
+ */
+public enum JalviewColourScheme
+{
+  /*
+   * the order of declaration is the default order in which 
+   * items are added to Colour menus
+   */
+  Clustal("Clustal", ClustalxColourScheme.class), Blosum62("Blosum62",
+          Blosum62ColourScheme.class), PID("% Identity",
+          PIDColourScheme.class), Zappo("Zappo", ZappoColourScheme.class),
+  Taylor("Taylor", TaylorColourScheme.class), Hydrophobic("Hydrophobic",
+          HydrophobicColourScheme.class), Helix("Helix Propensity",
+          HelixColourScheme.class), Strand("Strand Propensity",
+          StrandColourScheme.class), Turn("Turn Propensity",
+          TurnColourScheme.class), Buried("Buried Index",
+          BuriedColourScheme.class), Nucleotide("Nucleotide",
+          NucleotideColourScheme.class), PurinePyrimidine(
+          "Purine/Pyrimidine", PurinePyrimidineColourScheme.class),
+  RNAHelices("RNA Helices", RNAHelicesColour.class), TCoffee(
+          "T-Coffee Scores", TCoffeeColourScheme.class);
+  // RNAInteraction("RNA Interaction type", RNAInteractionColourScheme.class)
+
+  private String name;
+
+  private Class<? extends ColourSchemeI> myClass;
+
+  /**
+   * Constructor given the name of the colour scheme (as used in Jalview
+   * parameters). Note this is not necessarily the same as the 'display name'
+   * used in menu options (as this may be language-dependent).
+   * 
+   * @param s
+   */
+  JalviewColourScheme(String s, Class<? extends ColourSchemeI> cl)
+  {
+    name = s;
+    myClass = cl;
+  }
+
+  /**
+   * Returns the class of the colour scheme
+   * 
+   * @return
+   */
+  public Class<? extends ColourSchemeI> getSchemeClass()
+  {
+    return myClass;
+  }
+
+  /**
+   * Returns the 'official' name of this colour scheme. This is the name that
+   * identifies the colour scheme as a start-up parameter for the Jalview
+   * application or applet. Note that it may not be the name shown in menu
+   * options, as these may be internationalised.
+   */
+  @Override
+  public String toString()
+  {
+    return name;
+  }
+}
index dff2bf3..abae733 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
 
-import java.awt.Color;
+import java.util.Map;
 
 /**
  * DOCUMENT ME!
@@ -37,59 +39,29 @@ public class NucleotideColourScheme extends ResidueColourScheme
    */
   public NucleotideColourScheme()
   {
-    super(ResidueProperties.nucleotideIndex, ResidueProperties.nucleotide,
-            0);
+    super(ResidueProperties.nucleotideIndex, ResidueProperties.nucleotide);
+  }
+
+  @Override
+  public boolean isNucleotideSpecific()
+  {
+    return true;
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param n
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
   @Override
-  public Color findColour(char c)
+  public String getSchemeName()
   {
-    // System.out.println("called"); log.debug
-    return colors[ResidueProperties.nucleotideIndex[c]];
+    return JalviewColourScheme.Nucleotide.toString();
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param n
-   *          DOCUMENT ME!
-   * @param j
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
    */
   @Override
-  public Color findColour(char c, int j, SequenceI seq)
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
   {
-    Color currentColour;
-    if ((threshold == 0) || aboveThreshold(c, j))
-    {
-      try
-      {
-        currentColour = colors[ResidueProperties.nucleotideIndex[c]];
-      } catch (Exception ex)
-      {
-        return Color.white;
-      }
-    }
-    else
-    {
-      return Color.white;
-    }
-
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
-    }
-
-    return currentColour;
+    return new NucleotideColourScheme();
   }
 }
index 0ad5b5c..657d6b0 100755 (executable)
  */
 package jalview.schemes;
 
-import jalview.datamodel.ProfileI;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.util.Comparison;
 
 import java.awt.Color;
+import java.util.Map;
 
 public class PIDColourScheme extends ResidueColourScheme
 {
-  public Color[] pidColours;
+  private static final Color[] pidColours = { new Color(100, 100, 255),
+      new Color(153, 153, 255), new Color(204, 204, 255), };
 
-  public float[] thresholds;
+  private static final float[] thresholds = { 80, 60, 40, };
 
   SequenceGroup group;
 
   public PIDColourScheme()
   {
-    this.pidColours = ResidueProperties.pidColours;
-    this.thresholds = ResidueProperties.pidThresholds;
   }
 
   @Override
-  public Color findColour(char c, int j, SequenceI seq)
+  public Color findColour(char c, int j, SequenceI seq,
+          String consensusResidue, float pid)
   {
+    /*
+     * compare as upper case; note consensusResidue is 
+     * always computed as uppercase
+     */
     if ('a' <= c && c <= 'z')
     {
       c -= ('a' - 'A');
     }
 
-    if (consensus == null || consensus.get(j) == null)
-    {
-      return Color.white;
-    }
-
-    if ((threshold != 0) && !aboveThreshold(c, j))
+    if (consensusResidue == null || Comparison.isGap(c))
     {
       return Color.white;
     }
 
-    Color currentColour = Color.white;
-
-    double sc = 0;
-
+    Color colour = Color.white;
 
     /*
      * test whether this is the consensus (or joint consensus) residue
      */
-    ProfileI profile = consensus.get(j);
-    boolean matchesConsensus = profile.getModalResidue().contains(
+    boolean matchesConsensus = consensusResidue.contains(
             String.valueOf(c));
     if (matchesConsensus)
     {
-      sc = profile.getPercentageIdentity(ignoreGaps);
-
-      if (!Comparison.isGap(c))
+      for (int i = 0; i < thresholds.length; i++)
       {
-        for (int i = 0; i < thresholds.length; i++)
+        if (pid > thresholds[i])
         {
-          if (sc > thresholds[i])
-          {
-            currentColour = pidColours[i];
-            break;
-          }
+          colour = pidColours[i];
+          break;
         }
       }
     }
 
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
-    }
+    return colour;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.PID.toString();
+  }
 
-    return currentColour;
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new PIDColourScheme();
+  }
+
+  @Override
+  public boolean isSimple()
+  {
+    return false;
   }
 }
index d2b878d..1b36f30 100644 (file)
  */
 package jalview.schemes;
 
-import java.awt.Color;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
+import java.util.Map;
 
 /**
  * Class is based off of NucleotideColourScheme
@@ -35,56 +39,29 @@ public class PurinePyrimidineColourScheme extends ResidueColourScheme
   public PurinePyrimidineColourScheme()
   {
     super(ResidueProperties.purinepyrimidineIndex,
-            ResidueProperties.purinepyrimidine, 0);
+            ResidueProperties.purinepyrimidine);
   }
 
-  /**
-   * Finds the corresponding color for the type of character inputed
-   * 
-   * @param c
-   *          Character in sequence
-   * 
-   * @return Color from purinepyrimidineIndex in
-   *         jalview.schemes.ResidueProperties
-   */
-  public Color findColour(char c)
+  @Override
+  public boolean isNucleotideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
   {
-    return colors[ResidueProperties.purinepyrimidineIndex[c]];
+    return JalviewColourScheme.PurinePyrimidine.toString();
   }
 
   /**
-   * Returns color based on conservation
-   * 
-   * @param c
-   *          Character in sequence
-   * @param j
-   *          Threshold
-   * 
-   * @return Color in RGB
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
    */
-  public Color findColour(char c, int j)
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
   {
-    Color currentColour;
-    if ((threshold == 0) || aboveThreshold(c, j))
-    {
-      try
-      {
-        currentColour = colors[ResidueProperties.purinepyrimidineIndex[c]];
-      } catch (Exception ex)
-      {
-        return Color.white;
-      }
-    }
-    else
-    {
-      return Color.white;
-    }
-
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
-    }
-
-    return currentColour;
+    return new PurinePyrimidineColourScheme();
   }
 }
index 9729a83..056a167 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.schemes;
 
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
@@ -54,6 +55,14 @@ public class RNAHelicesColour extends ResidueColourScheme
   public AlignmentAnnotation annotation;
 
   /**
+   * Default constructor (required for ColourSchemes cache)
+   */
+  public RNAHelicesColour()
+  {
+
+  }
+
+  /**
    * Creates a new RNAHelicesColour object.
    */
   public RNAHelicesColour(AlignmentAnnotation annotation)
@@ -197,9 +206,44 @@ public class RNAHelicesColour extends ResidueColourScheme
   }
 
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
+  public ColourSchemeI getInstance(AnnotatedCollectionI sg,
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
-    return new RNAHelicesColour(this);
+    return new RNAHelicesColour(sg);
+  }
+
+  @Override
+  public boolean isNucleotideSpecific()
+  {
+    return true;
+  }
+
+  /**
+   * Answers true if the data has RNA secondary structure annotation
+   */
+  @Override
+  public boolean isApplicableTo(AnnotatedCollectionI ac)
+  {
+    if (ac instanceof AlignmentI && ((AlignmentI) ac).hasRNAStructure())
+    {
+      return true;
+    }
+
+    /*
+     * not currently supporting this option for group annotation / colouring
+     */
+    return false;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.RNAHelices.toString();
+  }
+
+  @Override
+  public boolean isSimple()
+  {
+    return false;
   }
-}
\ No newline at end of file
+}
index b1b3c5a..15cb157 100644 (file)
@@ -25,8 +25,8 @@ import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.SequenceGroup;
 
-import java.awt.event.ActionEvent;
 import java.util.Hashtable;
+import java.util.Map;
 import java.util.Vector;
 
 /**
@@ -34,8 +34,9 @@ import java.util.Vector;
  * change colors based on covariation.
  * 
  * @author Lauren Michelle Lui
- * 
+ * @deprecated this seems to be unfinished - just use RNAHelicesColour
  */
+@Deprecated
 public class RNAHelicesColourChooser
 {
 
@@ -45,9 +46,9 @@ public class RNAHelicesColourChooser
 
   ColourSchemeI oldcs;
 
-  Hashtable oldgroupColours;
+  Map<SequenceGroup, ColourSchemeI> oldgroupColours;
 
-  jalview.datamodel.AlignmentAnnotation currentAnnotation;
+  AlignmentAnnotation currentAnnotation;
 
   boolean adjusting = false;
 
@@ -57,26 +58,20 @@ public class RNAHelicesColourChooser
     oldcs = av.getGlobalColourScheme();
     if (av.getAlignment().getGroups() != null)
     {
-      oldgroupColours = new Hashtable();
+      oldgroupColours = new Hashtable<SequenceGroup, ColourSchemeI>();
       for (SequenceGroup sg : ap.getAlignment().getGroups())
       {
-        if (sg.cs != null)
+        if (sg.getColourScheme() != null)
         {
-          oldgroupColours.put(sg, sg.cs);
+          oldgroupColours.put(sg, sg.getColourScheme());
         }
       }
     }
     this.av = av;
     this.ap = ap;
 
-    if (oldcs instanceof RNAHelicesColour)
-    {
-      RNAHelicesColour rhc = (RNAHelicesColour) oldcs;
-
-    }
-
     adjusting = true;
-    Vector list = new Vector();
+    Vector<String> list = new Vector<String>();
     int index = 1;
     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
     if (anns != null)
@@ -96,9 +91,7 @@ public class RNAHelicesColourChooser
     }
 
     adjusting = false;
-
     changeColour();
-
   }
 
   void changeColour()
@@ -108,30 +101,10 @@ public class RNAHelicesColourChooser
     {
       return;
     }
-    RNAHelicesColour rhc = null;
-
-    rhc = new RNAHelicesColour(av.getAlignment());
+    RNAHelicesColour rhc = new RNAHelicesColour(av.getAlignment());
 
     av.setGlobalColourScheme(rhc);
 
     ap.paintAlignment(true);
   }
-
-  void reset()
-  {
-    av.setGlobalColourScheme(oldcs);
-    if (av.getAlignment().getGroups() != null)
-    {
-      for (SequenceGroup sg : ap.getAlignment().getGroups())
-      {
-        sg.cs = (ColourSchemeI) oldgroupColours.get(sg);
-      }
-    }
-  }
-
-  public void annotations_actionPerformed(ActionEvent e)
-  {
-    changeColour();
-  }
-
 }
index 794195a..d236803 100644 (file)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
 
 import java.awt.Color;
+import java.util.Map;
 
 public class RNAInteractionColourScheme extends ResidueColourScheme
 {
@@ -31,55 +34,48 @@ public class RNAInteractionColourScheme extends ResidueColourScheme
     super();
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param n
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
   @Override
   public Color findColour(char c)
   {
-    // System.out.println("called"); log.debug
+    // FIXME this is just a copy of NucleotideColourScheme
     return colors[ResidueProperties.nucleotideIndex[c]];
   }
 
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param n
-   *          DOCUMENT ME!
-   * @param j
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
   @Override
   public Color findColour(char c, int j, SequenceI seq)
   {
-    Color currentColour;
-    if ((threshold == 0) || aboveThreshold(c, j))
+    // FIXME this is just a copy of NucleotideColourScheme
+    Color currentColour = Color.white;
+    try
     {
-      try
-      {
-        currentColour = colors[ResidueProperties.nucleotideIndex[c]];
-      } catch (Exception ex)
-      {
-        return Color.white;
-      }
-    }
-    else
+      currentColour = colors[ResidueProperties.nucleotideIndex[c]];
+    } catch (Exception ex)
     {
-      return Color.white;
-    }
-
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
     }
 
     return currentColour;
   }
+
+  @Override
+  public boolean isNucleotideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return "RNA Interaction type";
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new RNAInteractionColourScheme();
+  }
 }
index f6b7c5e..03fc129 100755 (executable)
  */
 package jalview.schemes;
 
-import jalview.analysis.Conservation;
 import jalview.datamodel.AnnotatedCollectionI;
-import jalview.datamodel.ProfileI;
-import jalview.datamodel.ProfilesI;
 import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.util.ColorUtils;
 import jalview.util.Comparison;
-import jalview.util.MessageManager;
 
 import java.awt.Color;
 import java.util.Map;
 
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * Base class for residue-based colour schemes
  */
-public class ResidueColourScheme implements ColourSchemeI
+public abstract class ResidueColourScheme implements ColourSchemeI
 {
-  final int[] symbolIndex;
+  public static final String NONE = "None";
 
-  boolean conservationColouring = false;
-
-  Color[] colors = null;
-
-  int threshold = 0;
-
-  /* Set when threshold colouring to either pid_gaps or pid_nogaps */
-  protected boolean ignoreGaps = false;
+  public static final String USER_DEFINED = "User Defined";
 
   /*
-   * Consensus data indexed by column
+   * lookup up by character value e.g. 'G' to the colors array index
+   * e.g. if symbolIndex['K'] = 11 then colors[11] is the colour for K
    */
-  ProfilesI consensus;
+  final int[] symbolIndex;
 
   /*
-   * Conservation string as a char array 
+   * colour for residue characters as indexed by symbolIndex
    */
-  char[] conservation;
+  Color[] colors = null;
 
-  /*
-   * The conservation slider percentage setting 
-   */
-  int inc = 30;
+  /* Set when threshold colouring to either pid_gaps or pid_nogaps */
+  protected boolean ignoreGaps = false;
 
   /**
    * Creates a new ResidueColourScheme object.
@@ -74,15 +59,11 @@ public class ResidueColourScheme implements ColourSchemeI
    *        ResidueProperties.aaIndex)
    * @param colors
    *          colours for symbols in sequences
-   * @param threshold
-   *          threshold for conservation shading
    */
-  public ResidueColourScheme(int[] aaOrnaIndex, Color[] colours,
-          int threshold)
+  public ResidueColourScheme(int[] aaOrnaIndex, Color[] colours)
   {
     symbolIndex = aaOrnaIndex;
     this.colors = colours;
-    this.threshold = threshold;
   }
 
   /**
@@ -106,241 +87,118 @@ public class ResidueColourScheme implements ColourSchemeI
   /**
    * Find a colour without an index in a sequence
    */
-  @Override
   public Color findColour(char c)
   {
-    return colors == null ? Color.white : colors[symbolIndex[c]];
-  }
+    Color colour = Color.white;
 
-  @Override
-  public Color findColour(char c, int j, SequenceI seq)
-  {
-    Color currentColour;
-
-    if (colors != null && symbolIndex != null && (threshold == 0)
-            || aboveThreshold(c, j))
+    if (!Comparison.isGap(c) && colors != null && symbolIndex != null
+            && c < symbolIndex.length
+            && symbolIndex[c] < colors.length)
     {
-      currentColour = colors[symbolIndex[c]];
-    }
-    else
-    {
-      currentColour = Color.white;
-    }
-
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
+      colour = colors[symbolIndex[c]];
     }
 
-    return currentColour;
+    return colour;
   }
 
   /**
-   * Get the percentage threshold for this colour scheme
-   * 
-   * @return Returns the percentage threshold
+   * Default is to call the overloaded method that ignores consensus. A colour
+   * scheme that depends on consensus (for example, Blosum62), should override
+   * this method instead.
    */
   @Override
-  public int getThreshold()
+  public Color findColour(char c, int j, SequenceI seq,
+          String consensusResidue, float pid)
   {
-    return threshold;
+    return findColour(c, j, seq);
   }
 
   /**
-   * Sets the percentage consensus threshold value, and whether gaps are ignored
-   * in percentage identity calculation
-   * 
-   * @param consensusThreshold
-   * @param ignoreGaps
-   */
-  @Override
-  public void setThreshold(int consensusThreshold, boolean ignoreGaps)
-  {
-    threshold = consensusThreshold;
-    this.ignoreGaps = ignoreGaps;
-  }
-
-  /**
-   * Answers true if there is a consensus profile for the specified column, and
-   * the given residue matches the consensus (or joint consensus) residue for
-   * the column, and the percentage identity for the profile is equal to or
-   * greater than the current threshold; else answers false. The percentage
-   * calculation depends on whether or not we are ignoring gapped sequences.
-   * 
-   * @param residue
-   * @param column
-   *          (index into consensus profiles)
+   * Default implementation looks up the residue colour in a fixed scheme, or
+   * returns White if not found. Override this method for a colour scheme that
+   * depends on the column position or sequence.
    * 
+   * @param c
+   * @param j
+   * @param seq
    * @return
-   * @see #setThreshold(int, boolean)
    */
-  public boolean aboveThreshold(char residue, int column)
-  {
-    if ('a' <= residue && residue <= 'z')
-    {
-      // TO UPPERCASE !!!
-      // Faster than toUpperCase
-      residue -= ('a' - 'A');
-    }
-
-    if (consensus == null)
-    {
-      return false;
-    }
-
-    ProfileI profile = consensus.get(column);
-
-    /*
-     * test whether this is the consensus (or joint consensus) residue
-     */
-    if (profile != null
-            && profile.getModalResidue().contains(String.valueOf(residue)))
-    {
-      if (profile.getPercentageIdentity(ignoreGaps) >= threshold)
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  @Override
-  public boolean conservationApplied()
-  {
-    return conservationColouring;
-  }
-
-  @Override
-  public void setConservationApplied(boolean conservationApplied)
-  {
-    conservationColouring = conservationApplied;
-  }
-
-  @Override
-  public void setConservationInc(int i)
+  protected Color findColour(char c, int j, SequenceI seq)
   {
-    inc = i;
+    return findColour(c);
   }
 
   @Override
-  public int getConservationInc()
+  public void alignmentChanged(AnnotatedCollectionI alignment,
+          Map<SequenceI, SequenceCollectionI> hiddenReps)
   {
-    return inc;
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param consensus
-   *          DOCUMENT ME!
+   * Answers false if the colour scheme is nucleotide or peptide specific, and
+   * the data does not match, else true. Override to modify or extend this test
+   * as required.
    */
   @Override
-  public void setConsensus(ProfilesI consensus)
-  {
-    if (consensus == null)
-    {
-      return;
-    }
-
-    this.consensus = consensus;
-  }
-
-  @Override
-  public void setConservation(Conservation cons)
+  public boolean isApplicableTo(AnnotatedCollectionI ac)
   {
-    if (cons == null)
+    if (!isPeptideSpecific() && !isNucleotideSpecific())
     {
-      conservationColouring = false;
-      conservation = null;
+      return true;
     }
-    else
+    if (ac == null)
     {
-      conservationColouring = true;
-      int iSize = cons.getConsSequence().getLength();
-      conservation = new char[iSize];
-      for (int i = 0; i < iSize; i++)
-      {
-        conservation[i] = cons.getConsSequence().getCharAt(i);
-      }
+      return true;
     }
-
-  }
-
-  /**
-   * Applies a combination of column conservation score, and conservation
-   * percentage slider, to 'bleach' out the residue colours towards white.
-   * <p>
-   * If a column is fully conserved (identical residues, conservation score 11,
-   * shown as *), or all 10 physico-chemical properties are conserved
-   * (conservation score 10, shown as +), then the colour is left unchanged.
-   * <p>
-   * Otherwise a 'bleaching' factor is computed and applied to the colour. This
-   * is designed to fade colours for scores of 0-9 completely to white at slider
-   * positions ranging from 18% - 100% respectively.
-   * 
-   * @param currentColour
-   * @param column
-   * 
-   * @return bleached (or unmodified) colour
-   */
-  Color applyConservation(Color currentColour, int column)
-  {
-    if (conservation == null || conservation.length <= column)
-    {
-      return currentColour;
-    }
-    char conservationScore = conservation[column];
-
     /*
-     * if residues are fully conserved (* or 11), or all properties
-     * are conserved (+ or 10), leave colour unchanged
+     * pop-up menu on selection group before group created
+     * (no alignment context)
      */
-    if (conservationScore == '*' || conservationScore == '+'
-            || conservationScore == (char) 10
-            || conservationScore == (char) 11)
+    // TODO: add nucleotide flag to SequenceGroup?
+    if (ac instanceof SequenceGroup && ac.getContext() == null)
     {
-      return currentColour;
-    }
-
-    if (Comparison.isGap(conservationScore))
-    {
-      return Color.white;
+      return true;
     }
 
     /*
-     * convert score 0-9 to a bleaching factor 1.1 - 0.2
+     * inspect the data context (alignment) for residue type
      */
-    float bleachFactor = (11 - (conservationScore - '0')) / 10f;
+    boolean nucleotide = ac.isNucleotide();
 
     /*
-     * scale this up by 0-5 (percentage slider / 20)
-     * as a result, scores of:         0  1  2  3  4  5  6  7  8  9
-     * fade to white at slider value: 18 20 22 25 29 33 40 50 67 100%
+     * does data type match colour scheme type?
      */
-    bleachFactor *= (inc / 20f);
+    return (nucleotide && isNucleotideSpecific())
+            || (!nucleotide && isPeptideSpecific());
+  }
 
-    return ColorUtils.bleachColour(currentColour, bleachFactor);
+  /**
+   * Answers true if the colour scheme is normally only for peptide data
+   * 
+   * @return
+   */
+  public boolean isPeptideSpecific()
+  {
+    return false;
   }
 
-  @Override
-  public void alignmentChanged(AnnotatedCollectionI alignment,
-          Map<SequenceI, SequenceCollectionI> hiddenReps)
+  /**
+   * Answers true if the colour scheme is normally only for nucleotide data
+   * 
+   * @return
+   */
+  public boolean isNucleotideSpecific()
   {
+    return false;
   }
 
+  /**
+   * Default method returns true. Override this to return false in colour
+   * schemes that are not determined solely by the sequence symbol.
+   */
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
-          Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
+  public boolean isSimple()
   {
-    try
-    {
-      return getClass().newInstance();
-    } catch (Exception q)
-    {
-      throw new Error(MessageManager.formatMessage(
-              "error.implementation_error_cannot_duplicate_colour_scheme",
-              new String[] { getClass().getName() }), q);
-    }
+    return true;
   }
 }
index 4d46279..751175d 100755 (executable)
  */
 package jalview.schemes;
 
-import jalview.analysis.scoremodels.FeatureScoreModel;
-import jalview.analysis.scoremodels.PIDScoreModel;
-import jalview.api.analysis.ScoreModelI;
-
 import java.awt.Color;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -36,8 +32,6 @@ import java.util.Vector;
 
 public class ResidueProperties
 {
-  public static Hashtable<String, ScoreModelI> scoreMatrices = new Hashtable<String, ScoreModelI>();
-
   // Stores residue codes/names and colours and other things
   public static final int[] aaIndex; // aaHash version 2.1.1 and below
 
@@ -477,105 +471,6 @@ public class ResidueProperties
 
   // public static final double hydmax = 1.38;
   // public static final double hydmin = -2.53;
-  private static final int[][] BLOSUM62 = {
-      { 4, -1, -2, -2, 0, -1, -1, 0, -2, -1, -1, -1, -1, -2, -1, 1, 0, -3,
-          -2, 0, -2, -1, 0, -4 },
-      { -1, 5, 0, -2, -3, 1, 0, -2, 0, -3, -2, 2, -1, -3, -2, -1, -1, -3,
-          -2, -3, -1, 0, -1, -4 },
-      { -2, 0, 6, 1, -3, 0, 0, 0, 1, -3, -3, 0, -2, -3, -2, 1, 0, -4, -2,
-          -3, 3, 0, -1, -4 },
-      { -2, -2, 1, 6, -3, 0, 2, -1, -1, -3, -4, -1, -3, -3, -1, 0, -1, -4,
-          -3, -3, 4, 1, -1, -4 },
-      { 0, 3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1,
-          -2, -2, -1, -3, -3, -2, -4 },
-      { -1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, 1, 0, -3, -1, 0, -1, -2, -1,
-          -2, 0, 3, -1, -4 },
-      { -1, 0, 0, 2, -4, 2, 5, -2, 0, -3, -3, 1, -2, -3, -1, 0, -1, -3, -2,
-          -2, 1, 4, -1, -4 },
-      { 0, -2, 0, -1, -3, -2, -2, 6, -2, -4, -4, -2, -3, -3, -2, 0, -2, -2,
-          -3, -3, -1, -2, -1, -4 },
-      { -2, 0, 1, -1, -3, 0, 0, -2, 8, -3, -3, -1, -2, -1, -2, -1, -2, -2,
-          2, -3, 0, 0, -1, -4 },
-      { -1, -3, -3, -3, -1, -3, -3, -4, -3, 4, 2, -3, 1, 0, -3, -2, -1, -3,
-          -1, 3, -3, -3, -1, -4 },
-      { -1, -2, -3, -4, -1, -2, -3, -4, -3, 2, 4, -2, 2, 0, -3, -2, -1, -2,
-          -1, 1, -4, -3, -1, -4 },
-      { -1, 2, 0, -1, -3, 1, 1, -2, -1, -3, -2, 5, -1, -3, -1, 0, -1, -3,
-          -2, -2, 0, 1, -1, -4 },
-      { -1, -1, -2, -3, -1, 0, -2, -3, -2, 1, 2, -1, 5, 0, -2, -1, -1, -1,
-          -1, 1, -3, -1, -1, -4 },
-      { -2, -3, -3, -3, -2, -3, -3, -3, -1, 0, 0, -3, 0, 6, -4, -2, -2, 1,
-          3, -1, -3, -3, -1, -4 },
-      { -1, -2, -2, -1, -3, -1, -1, -2, -2, -3, -3, -1, -2, -4, 7, -1, -1,
-          -4, -3, -2, -2, -1, -2, -4 },
-      { 1, -1, 1, 0, -1, 0, 0, 0, -1, -2, -2, 0, -1, -2, -1, 4, 1, -3, -2,
-          -2, 0, 0, 0, -4 },
-      { 0, -1, 0, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -2, -1, 1, 5, -2,
-          -2, 0, -1, -1, 0, -4 },
-      { -3, -3, -4, -4, -2, -2, -3, -2, -2, -3, -2, -3, -1, 1, -4, -3, -2,
-          11, 2, -3, -4, -3, -2, -4 },
-      { -2, -2, -2, -3, -2, -1, -2, -3, 2, -1, -1, -2, -1, 3, -3, -2, -2,
-          2, 7, -1, -3, -2, -1, -4 },
-      { 0, -3, -3, -3, -1, -2, -2, -3, -3, 3, 1, -2, 1, -1, -2, -2, 0, -3,
-          -1, 4, -3, -2, -1, -4 },
-      { -2, -1, 3, 4, -3, 0, 1, -1, 0, -3, -4, 0, -3, -3, -2, 0, -1, -4,
-          -3, -3, 4, 1, -1, -4 },
-      { -1, 0, 0, 1, -3, 3, 4, -2, 0, -3, -3, 1, -1, -3, -1, 0, -1, -3, -2,
-          -2, 1, 4, -1, -4 },
-      { 0, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, 0, 0,
-          -2, -1, -1, -1, -1, -1, -4 },
-      { -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4,
-          -4, -4, -4, -4, -4, -4, 1 }, };
-
-  static final int[][] PAM250 = {
-      { 2, -2, 0, 0, -2, 0, 0, 1, -1, -1, -2, -1, -1, -3, 1, 1, 1, -6, -3,
-          0, 0, 0, 0, -8 },
-      { -2, 6, 0, -1, -4, 1, -1, -3, 2, -2, -3, 3, 0, -4, 0, 0, -1, 2, -4,
-          -2, -1, 0, -1, -8 },
-      { 0, 0, 2, 2, -4, 1, 1, 0, 2, -2, -3, 1, -2, -3, 0, 1, 0, -4, -2, -2,
-          2, 1, 0, -8 },
-      { 0, -1, 2, 4, -5, 2, 3, 1, 1, -2, -4, 0, -3, -6, -1, 0, 0, -7, -4,
-          -2, 3, 3, -1, -8 },
-      { -2, -4, -4, -5, 12, -5, -5, -3, -3, -2, -6, -5, -5, -4, -3, 0, -2,
-          -8, 0, -2, -4, -5, -3, -8 },
-      { 0, 1, 1, 2, -5, 4, 2, -1, 3, -2, -2, 1, -1, -5, 0, -1, -1, -5, -4,
-          -2, 1, 3, -1, -8 },
-      { 0, -1, 1, 3, -5, 2, 4, 0, 1, -2, -3, 0, -2, -5, -1, 0, 0, -7, -4,
-          -2, 3, 3, -1, -8 },
-      { 1, -3, 0, 1, -3, -1, 0, 5, -2, -3, -4, -2, -3, -5, 0, 1, 0, -7, -5,
-          -1, 0, 0, -1, -8 },
-      { -1, 2, 2, 1, -3, 3, 1, -2, 6, -2, -2, 0, -2, -2, 0, -1, -1, -3, 0,
-          -2, 1, 2, -1, -8 },
-      { -1, -2, -2, -2, -2, -2, -2, -3, -2, 5, 2, -2, 2, 1, -2, -1, 0, -5,
-          -1, 4, -2, -2, -1, -8 },
-      { -2, -3, -3, -4, -6, -2, -3, -4, -2, 2, 6, -3, 4, 2, -3, -3, -2, -2,
-          -1, 2, -3, -3, -1, -8 },
-      { -1, 3, 1, 0, -5, 1, 0, -2, 0, -2, -3, 5, 0, -5, -1, 0, 0, -3, -4,
-          -2, 1, 0, -1, -8 },
-      { -1, 0, -2, -3, -5, -1, -2, -3, -2, 2, 4, 0, 6, 0, -2, -2, -1, -4,
-          -2, 2, -2, -2, -1, -8 },
-      { -3, -4, -3, -6, -4, -5, -5, -5, -2, 1, 2, -5, 0, 9, -5, -3, -3, 0,
-          7, -1, -4, -5, -2, -8 },
-      { 1, 0, 0, -1, -3, 0, -1, 0, 0, -2, -3, -1, -2, -5, 6, 1, 0, -6, -5,
-          -1, -1, 0, -1, -8 },
-      { 1, 0, 1, 0, 0, -1, 0, 1, -1, -1, -3, 0, -2, -3, 1, 2, 1, -2, -3,
-          -1, 0, 0, 0, -8 },
-      { 1, -1, 0, 0, -2, -1, 0, 0, -1, 0, -2, 0, -1, -3, 0, 1, 3, -5, -3,
-          0, 0, -1, 0, -8 },
-      { -6, 2, -4, -7, -8, -5, -7, -7, -3, -5, -2, -3, -4, 0, -6, -2, -5,
-          17, 0, -6, -5, -6, -4, -8 },
-      { -3, -4, -2, -4, 0, -4, -4, -5, 0, -1, -1, -4, -2, 7, -5, -3, -3, 0,
-          10, -2, -3, -4, -2, -8 },
-      { 0, -2, -2, -2, -2, -2, -2, -1, -2, 4, 2, -2, 2, -1, -1, -1, 0, -6,
-          -2, 4, -2, -2, -1, -8 },
-      { 0, -1, 2, 3, -4, 1, 3, 0, 1, -2, -3, 1, -2, -4, -1, 0, 0, -5, -3,
-          -2, 3, 2, -1, -8 },
-      { 0, 0, 1, 3, -5, 3, 3, 0, 2, -2, -3, 0, -2, -5, 0, 0, -1, -6, -4,
-          -2, 2, 3, -1, -8 },
-      { 0, -1, 0, -1, -3, -1, -1, -1, -1, -1, -1, -1, -1, -2, -1, 0, 0, -4,
-          -2, -1, -1, -1, -1, -8 },
-      { -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8, -8,
-          -8, -8, -8, -8, -8, -8, 1 }, };
 
   // not currently used
   // public static final Map<String, Color> ssHash = new Hashtable<String,
@@ -598,40 +493,6 @@ public class ResidueProperties
    * Color.white, // R Color.white, // Y Color.white, // N Color.white, // Gap
    */
 
-  // JBPNote: patch matrix for T/U equivalence when working with DNA or RNA.
-  // Will equate sequences if working with mixed nucleotide sets.
-  // treats T and U identically. R and Y weak equivalence with AG and CTU.
-  // N matches any other base weakly
-  //
-  static final int[][] DNA = { { 10, -8, -8, -8, -8, 1, 1, 1, -8, 1, 1 }, // A
-      { -8, 10, -8, -8, -8, 1, 1, -8, 1, 1, 1 }, // C
-      { -8, -8, 10, -8, -8, 1, 1, 1, -8, 1, 1 }, // G
-      { -8, -8, -8, 10, 10, 1, 1, -8, 1, 1, 1 }, // T
-      { -8, -8, -8, 10, 10, 1, 1, -8, 1, 1, 1 }, // U
-      { 1, 1, 1, 1, 1, 10, 0, 0, 0, 1, 1 }, // I
-      { 1, 1, 1, 1, 1, 0, 10, 0, 0, 1, 1 }, // X
-      { 1, -8, 1, -8, -8, 0, 0, 10, -8, 1, 1 }, // R
-      { -8, 1, -8, 1, 1, 0, 0, -8, 10, 1, 1 }, // Y
-      { 1, 1, 1, 1, 1, 1, 1, 1, 1, 10, 1 }, // N
-      { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, // -
-  };
-  /**
-   * register matrices in list
-   */
-  static
-  {
-    scoreMatrices.put("BLOSUM62", new ScoreMatrix("BLOSUM62", BLOSUM62, 0));
-    scoreMatrices.put("PAM250", new ScoreMatrix("PAM250", PAM250, 0));
-    scoreMatrices.put("DNA", new ScoreMatrix("DNA", DNA, 1));
-  }
-
-  public static final Color[] pidColours = { midBlue,
-      new Color(153, 153, 255),
-      // Color.lightGray,
-      new Color(204, 204, 255), };
-
-  public static final float[] pidThresholds = { 80, 60, 40, };
-
   public static List<String> STOP = Arrays.asList("TGA", "TAA", "TAG");
 
   public static String START = "ATG";
@@ -1251,15 +1112,6 @@ public class ResidueProperties
       propMatrixPos[i][i] = maxP;
       propMatrixEpos[i][i] = maxEP;
     }
-    // JAL-1512 comment out physicochemical score matrices for 2.8.1 release
-    // scoreMatrices.put("Conservation Pos", new
-    // ScoreMatrix("Conservation Pos",propMatrixPos,0));
-    // scoreMatrices.put("Conservation Both", new
-    // ScoreMatrix("Conservation Both",propMatrixF,0));
-    // scoreMatrices.put("Conservation EnhPos", new
-    // ScoreMatrix("Conservation EnhPos",propMatrixEpos,0));
-    scoreMatrices.put("PID", new PIDScoreModel());
-    scoreMatrices.put("Displayed Features", new FeatureScoreModel());
   }
 
   private ResidueProperties()
@@ -1286,39 +1138,6 @@ public class ResidueProperties
     return aa3Hash;
   }
 
-  public static int[][] getDNA()
-  {
-    return ResidueProperties.DNA;
-  }
-
-  public static int[][] getBLOSUM62()
-  {
-    return ResidueProperties.BLOSUM62;
-  }
-
-  public static int getPAM250(String A1, String A2)
-  {
-    return getPAM250(A1.charAt(0), A2.charAt(0));
-  }
-
-  public static int getBLOSUM62(char c1, char c2)
-  {
-    int pog = 0;
-
-    try
-    {
-      int a = aaIndex[c1];
-      int b = aaIndex[c2];
-
-      pog = ResidueProperties.BLOSUM62[a][b];
-    } catch (Exception e)
-    {
-      // System.out.println("Unknown residue in " + A1 + " " + A2);
-    }
-
-    return pog;
-  }
-
   public static String codonTranslate(String lccodon)
   {
     String cdn = codonHash2.get(lccodon.toUpperCase());
@@ -1329,53 +1148,6 @@ public class ResidueProperties
     return cdn;
   }
 
-  public static int[][] getDefaultPeptideMatrix()
-  {
-    return ResidueProperties.getBLOSUM62();
-  }
-
-  public static int[][] getDefaultDnaMatrix()
-  {
-    return ResidueProperties.getDNA();
-  }
-
-  /**
-   * get a ScoreMatrix based on its string name
-   * 
-   * @param pwtype
-   * @return matrix in scoreMatrices with key pwtype or null
-   */
-  public static ScoreMatrix getScoreMatrix(String pwtype)
-  {
-    Object val = scoreMatrices.get(pwtype);
-    if (val != null && val instanceof ScoreMatrix)
-    {
-      return (ScoreMatrix) val;
-    }
-    return null;
-  }
-
-  /**
-   * get a ScoreModel based on its string name
-   * 
-   * @param pwtype
-   * @return scoremodel of type pwtype or null
-   */
-  public static ScoreModelI getScoreModel(String pwtype)
-  {
-    return scoreMatrices.get(pwtype);
-  }
-
-  public static int getPAM250(char c, char d)
-  {
-    int a = aaIndex[c];
-    int b = aaIndex[d];
-
-    int pog = ResidueProperties.PAM250[a][b];
-
-    return pog;
-  }
-
   public static Hashtable<String, String> toDssp3State;
   static
   {
index 7eb920d..aa20121 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
 
 import java.awt.Color;
+import java.util.Map;
 
 /**
  * DOCUMENT ME!
@@ -84,49 +87,30 @@ public class ScoreColourScheme extends ResidueColourScheme
   /**
    * DOCUMENT ME!
    * 
-   * @param s
-   *          DOCUMENT ME!
-   * @param j
+   * @param c
    *          DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
-  @Override
-  public Color findColour(char c, int j, SequenceI seq)
+  public Color makeColour(float c)
   {
-    if (threshold > 0)
-    {
-      if (!aboveThreshold(c, j))
-      {
-        return Color.white;
-      }
-    }
-
-    if (jalview.util.Comparison.isGap(c))
-    {
-      return Color.white;
-    }
-
-    Color currentColour = colors[ResidueProperties.aaIndex[c]];
-
-    if (conservationColouring)
-    {
-      currentColour = applyConservation(currentColour, j);
-    }
+    return new Color(c, (float) 0.0, (float) 1.0 - c);
+  }
 
-    return currentColour;
+  @Override
+  public String getSchemeName()
+  {
+    return "Score";
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param c
-   *          DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
    */
-  public Color makeColour(float c)
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
   {
-    return new Color(c, (float) 0.0, (float) 1.0 - c);
+    return new ScoreColourScheme(symbolIndex, scores, min, max);
   }
 }
diff --git a/src/jalview/schemes/ScoreMatrix.java b/src/jalview/schemes/ScoreMatrix.java
deleted file mode 100644 (file)
index 5e5fa8d..0000000
+++ /dev/null
@@ -1,173 +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.schemes;
-
-import jalview.analysis.scoremodels.PairwiseSeqScoreModel;
-import jalview.api.analysis.ScoreModelI;
-
-public class ScoreMatrix extends PairwiseSeqScoreModel implements
-        ScoreModelI
-{
-  String name;
-
-  @Override
-  public String getName()
-  {
-    return name;
-  }
-
-  /**
-   * reference to integer score matrix
-   */
-  int[][] matrix;
-
-  /**
-   * 0 for Protein Score matrix. 1 for dna score matrix
-   */
-  int type;
-
-  /**
-   * 
-   * @param name
-   *          Unique, human readable name for the matrix
-   * @param matrix
-   *          Pairwise scores indexed according to appropriate symbol alphabet
-   * @param type
-   *          0 for Protein, 1 for NA
-   */
-  ScoreMatrix(String name, int[][] matrix, int type)
-  {
-    this.matrix = matrix;
-    this.type = type;
-    this.name = name;
-  }
-
-  @Override
-  public boolean isDNA()
-  {
-    return type == 1;
-  }
-
-  @Override
-  public boolean isProtein()
-  {
-    return type == 0;
-  }
-
-  @Override
-  public int[][] getMatrix()
-  {
-    return matrix;
-  }
-
-  /**
-   * 
-   * @param A1
-   * @param A2
-   * @return score for substituting first char in A1 with first char in A2
-   */
-  public int getPairwiseScore(String A1, String A2)
-  {
-    return getPairwiseScore(A1.charAt(0), A2.charAt(0));
-  }
-
-  public int getPairwiseScore(char c, char d)
-  {
-    int pog = 0;
-
-    try
-    {
-      int a = (type == 0) ? ResidueProperties.aaIndex[c]
-              : ResidueProperties.nucleotideIndex[c];
-      int b = (type == 0) ? ResidueProperties.aaIndex[d]
-              : ResidueProperties.nucleotideIndex[d];
-
-      pog = matrix[a][b];
-    } catch (Exception e)
-    {
-      // System.out.println("Unknown residue in " + A1 + " " + A2);
-    }
-
-    return pog;
-  }
-
-  /**
-   * pretty print the matrix
-   */
-  public String toString()
-  {
-    return outputMatrix(false);
-  }
-
-  public String outputMatrix(boolean html)
-  {
-    StringBuffer sb = new StringBuffer();
-    int[] symbols = (type == 0) ? ResidueProperties.aaIndex
-            : ResidueProperties.nucleotideIndex;
-    int symMax = (type == 0) ? ResidueProperties.maxProteinIndex
-            : ResidueProperties.maxNucleotideIndex;
-    boolean header = true;
-    if (html)
-    {
-      sb.append("<table border=\"1\">");
-    }
-    for (char sym = 'A'; sym <= 'Z'; sym++)
-    {
-      if (symbols[sym] >= 0 && symbols[sym] < symMax)
-      {
-        if (header)
-        {
-          sb.append(html ? "<tr><td></td>" : "");
-          for (char sym2 = 'A'; sym2 <= 'Z'; sym2++)
-          {
-            if (symbols[sym2] >= 0 && symbols[sym2] < symMax)
-            {
-              sb.append((html ? "<td>&nbsp;" : "\t") + sym2
-                      + (html ? "&nbsp;</td>" : ""));
-            }
-          }
-          header = false;
-          sb.append(html ? "</tr>\n" : "\n");
-        }
-        if (html)
-        {
-          sb.append("<tr>");
-        }
-        sb.append((html ? "<td>" : "") + sym + (html ? "</td>" : ""));
-        for (char sym2 = 'A'; sym2 <= 'Z'; sym2++)
-        {
-          if (symbols[sym2] >= 0 && symbols[sym2] < symMax)
-          {
-            sb.append((html ? "<td>" : "\t")
-                    + matrix[symbols[sym]][symbols[sym2]]
-                    + (html ? "</td>" : ""));
-          }
-        }
-        sb.append(html ? "</tr>\n" : "\n");
-      }
-    }
-    if (html)
-    {
-      sb.append("</table>");
-    }
-    return sb.toString();
-  }
-}
index 1340de3..5f11c29 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
 import java.awt.Color;
+import java.util.Map;
 
 /**
  * DOCUMENT ME!
@@ -47,8 +52,32 @@ public class StrandColourScheme extends ScoreColourScheme
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Color makeColour(float c)
   {
     return new Color(c, c, (float) 1.0 - c);
   }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Strand.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new StrandColourScheme();
+  }
 }
index 2be51c2..13d1d55 100644 (file)
@@ -31,6 +31,7 @@ import jalview.io.TCoffeeScoreFile;
 import java.awt.Color;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -44,20 +45,15 @@ import java.util.Map;
  */
 public class TCoffeeColourScheme extends ResidueColourScheme
 {
+  IdentityHashMap<SequenceI, Color[]> seqMap;
 
-  static final Color[] colors = { new Color(102, 102, 255), // #6666FF
-      new Color(0, 255, 0), // #00FF00
-      new Color(102, 255, 0), // #66FF00
-      new Color(204, 255, 0), // #CCFF00
-      new Color(255, 255, 0), // #FFFF00
-      new Color(255, 204, 0), // #FFCC00
-      new Color(255, 153, 0), // #FF9900
-      new Color(255, 102, 0), // #FF6600
-      new Color(255, 51, 0), // #FF3300
-      new Color(255, 34, 0) // #FF2000
-  };
+  /**
+   * Default constructor (required for Class.newInstance())
+   */
+  public TCoffeeColourScheme()
+  {
 
-  IdentityHashMap<SequenceI, Color[]> seqMap;
+  }
 
   /**
    * the color scheme needs to look at the alignment to get and cache T-COFFEE
@@ -71,6 +67,13 @@ public class TCoffeeColourScheme extends ResidueColourScheme
     alignmentChanged(alignment, null);
   }
 
+  /**
+   * Finds the TCoffeeScore annotation (if any) for each sequence and notes the
+   * annotation colour for each column position. The colours are fixed for
+   * scores 0-9 and are set when annotation is parsed.
+   * 
+   * @see TCoffeeScoreFile#annotateAlignment(AlignmentI, boolean)
+   */
   @Override
   public void alignmentChanged(AnnotatedCollectionI alignment,
           Map<SequenceI, SequenceCollectionI> hiddenReps)
@@ -80,7 +83,7 @@ public class TCoffeeColourScheme extends ResidueColourScheme
 
     // assume only one set of TCOFFEE scores - but could have more than one
     // potentially.
-    ArrayList<AlignmentAnnotation> annots = new ArrayList<AlignmentAnnotation>();
+    List<AlignmentAnnotation> annots = new ArrayList<AlignmentAnnotation>();
     // Search alignment to get all tcoffee annotation and pick one set of
     // annotation to use to colour seqs.
     seqMap = new IdentityHashMap<SequenceI, Color[]>();
@@ -119,9 +122,12 @@ public class TCoffeeColourScheme extends ResidueColourScheme
   @Override
   public Color findColour(char c, int j, SequenceI seq)
   {
-    Color[] cols;
-
-    if (seqMap == null || (cols = seqMap.get(seq)) == null)
+    if (seqMap == null)
+    {
+      return Color.WHITE;
+    }
+    Color[] cols = seqMap.get(seq);
+    if (cols == null)
     {
       // see above TODO about computing a colour for each residue in each
       // column: cc = _rcols[i][indexFor[c]];
@@ -136,9 +142,38 @@ public class TCoffeeColourScheme extends ResidueColourScheme
   }
 
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
+  public ColourSchemeI getInstance(AnnotatedCollectionI sg,
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
     return new TCoffeeColourScheme(sg);
   }
+
+  /**
+   * Answers true if the data has TCoffee score annotation
+   */
+  @Override
+  public boolean isApplicableTo(AnnotatedCollectionI ac)
+  {
+    AnnotatedCollectionI alcontext = ac instanceof AlignmentI ? ac : ac
+            .getContext();
+    if (alcontext == null)
+    {
+      return false;
+    }
+    Iterable<AlignmentAnnotation> anns = alcontext
+            .findAnnotation(TCoffeeScoreFile.TCOFFEE_SCORE);
+    return anns.iterator().hasNext();
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.TCoffee.toString();
+  }
+
+  @Override
+  public boolean isSimple()
+  {
+    return false;
+  }
 }
index 8c46bba..ac8abbc 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
+import java.util.Map;
+
 public class TaylorColourScheme extends ResidueColourScheme
 {
   public TaylorColourScheme()
   {
-    super(ResidueProperties.aaIndex, ResidueProperties.taylor, 0);
+    super(ResidueProperties.aaIndex, ResidueProperties.taylor);
+  }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Taylor.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new TaylorColourScheme();
   }
 }
index 22422ff..67116b8 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
 import java.awt.Color;
+import java.util.Map;
 
 /**
  * DOCUMENT ME!
@@ -47,8 +52,32 @@ public class TurnColourScheme extends ScoreColourScheme
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Color makeColour(float c)
   {
     return new Color(c, 1 - c, 1 - c);
   }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Turn.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new TurnColourScheme();
+  }
 }
index 9ae14ca..8e58c20 100755 (executable)
@@ -23,13 +23,23 @@ package jalview.schemes;
 import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceI;
+import jalview.util.ColorUtils;
+import jalview.util.StringUtils;
 
 import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.StringTokenizer;
 
 public class UserColourScheme extends ResidueColourScheme
 {
+  /*
+   * lookup (by symbol index) of lower case colours (if configured)
+   */
   Color[] lowerCaseColours;
 
   protected String schemeName;
@@ -46,37 +56,86 @@ public class UserColourScheme extends ResidueColourScheme
   }
 
   @Override
-  public ColourSchemeI applyTo(AnnotatedCollectionI sg,
+  public ColourSchemeI getInstance(AnnotatedCollectionI sg,
           Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
   {
-    UserColourScheme usc = new UserColourScheme(colors);
-    if (lowerCaseColours != null)
+    return new UserColourScheme(this);
+  }
+
+  /**
+   * Copy constructor
+   * 
+   * @return
+   */
+  protected UserColourScheme(UserColourScheme from)
+  {
+    this(from.colors);
+    schemeName = from.schemeName;
+    if (from.lowerCaseColours != null)
     {
-      usc.schemeName = new String(schemeName);
-      usc.lowerCaseColours = new Color[lowerCaseColours.length];
-      System.arraycopy(lowerCaseColours, 0, usc.lowerCaseColours, 0,
-              lowerCaseColours.length);
+      lowerCaseColours = new Color[from.lowerCaseColours.length];
+      System.arraycopy(from.lowerCaseColours, 0, lowerCaseColours, 0,
+              from.lowerCaseColours.length);
     }
-    return usc;
   }
 
+  /**
+   * Constructor for an animino acid colour scheme. The colour specification may
+   * be one of
+   * <ul>
+   * <li>an AWT colour name e.g. red</li>
+   * <li>an AWT hex rgb colour e.g. ff2288</li>
+   * <li>residue colours list e.g. D,E=red;K,R,H=0022FF;c=yellow</li>
+   * </ul>
+   * 
+   * @param colour
+   */
   public UserColourScheme(String colour)
   {
     super(ResidueProperties.aaIndex);
-    Color col = getColourFromString(colour);
+
+    if (colour.contains("="))
+    {
+      /*
+       * a list of colours per residue(s)
+       */
+      parseAppletParameter(colour);
+      return;
+    }
+
+    Color col = ColorUtils.parseColourString(colour);
 
     if (col == null)
     {
       System.out.println("Making colour from name: " + colour);
-      col = createColourFromName(colour);
+      col = ColorUtils.createColourFromName(colour);
     }
 
-    colors = new Color[24];
-    for (int i = 0; i < 24; i++)
+    setAll(col);
+    schemeName = colour;
+  }
+
+  /**
+   * Sets all symbols to the specified colour
+   * 
+   * @param col
+   */
+  protected void setAll(Color col)
+  {
+    if (symbolIndex == null)
+    {
+      return;
+    }
+    int max = 0;
+    for (int index : symbolIndex)
+    {
+      max = Math.max(max, index);
+    }
+    colors = new Color[max + 1];
+    for (int i = 0; i <= max; i++)
     {
       colors[i] = col;
     }
-    schemeName = colour;
   }
 
   public Color[] getColours()
@@ -99,77 +158,25 @@ public class UserColourScheme extends ResidueColourScheme
     return schemeName;
   }
 
-  public static Color getColourFromString(String colour)
-  {
-    if (colour == null)
-    {
-      return null;
-    }
-    colour = colour.trim();
-
-    Color col = null;
-    try
-    {
-      int value = Integer.parseInt(colour, 16);
-      col = new Color(value);
-    } catch (NumberFormatException ex)
-    {
-    }
-
-    if (col == null)
-    {
-      col = ColourSchemeProperty.getAWTColorFromName(colour);
-    }
-
-    if (col == null)
-    {
-      try
-      {
-        java.util.StringTokenizer st = new java.util.StringTokenizer(
-                colour, ",");
-        int r = Integer.parseInt(st.nextToken());
-        int g = Integer.parseInt(st.nextToken());
-        int b = Integer.parseInt(st.nextToken());
-        col = new Color(r, g, b);
-      } catch (Exception ex)
-      {
-      }
-    }
-
-    return col;
-
-  }
-
-  public static Color createColourFromName(String name)
+  /**
+   * Parse and save residue colours specified as (for example)
+   * 
+   * <pre>
+   *     D,E=red; K,R,H=0022FF; c=100,50,75
+   * </pre>
+   * 
+   * This should be a semi-colon separated list of colours, which may be defined
+   * by colour name, hex value or comma-separated RGB triple. Each colour is
+   * defined for a comma-separated list of amino acid single letter codes. (Note
+   * that this also allows a colour scheme to be defined for ACGT, but not for
+   * U.)
+   * 
+   * @param paramValue
+   */
+  void parseAppletParameter(String paramValue)
   {
-    int r, g, b;
-
-    int lsize = name.length();
-    int start = 0, end = lsize / 3;
-
-    int rgbOffset = Math.abs(name.hashCode() % 10) * 15;
-
-    r = Math.abs(name.substring(start, end).hashCode() + rgbOffset) % 210 + 20;
-    start = end;
-    end += lsize / 3;
-    if (end > lsize)
-    {
-      end = lsize;
-    }
+    setAll(Color.white);
 
-    g = Math.abs(name.substring(start, end).hashCode() + rgbOffset) % 210 + 20;
-
-    b = Math.abs(name.substring(end).hashCode() + rgbOffset) % 210 + 20;
-
-    Color color = new Color(r, g, b);
-
-    return color;
-  }
-
-  public void parseAppletParameter(String paramValue)
-  {
-    // TODO: need a function to generate appletParameter colour string from a
-    // UCS
     StringTokenizer st = new StringTokenizer(paramValue, ";");
     StringTokenizer st2;
     String token = null, colour, residues;
@@ -184,43 +191,42 @@ public class UserColourScheme extends ResidueColourScheme
         st2 = new StringTokenizer(residues, " ,");
         while (st2.hasMoreTokens())
         {
-          token = st2.nextToken();
+          String residue = st2.nextToken();
 
-          if (ResidueProperties.aaIndex[token.charAt(0)] == -1)
+          int colIndex = ResidueProperties.aaIndex[residue.charAt(0)];
+          if (colIndex == -1)
           {
             continue;
           }
 
-          int colIndex = ResidueProperties.aaIndex[token.charAt(0)];
-
-          if (token.equalsIgnoreCase("lowerCase"))
+          if (residue.equalsIgnoreCase("lowerCase"))
           {
             if (lowerCaseColours == null)
             {
-              lowerCaseColours = new Color[23];
+              lowerCaseColours = new Color[colors.length];
             }
-            for (int i = 0; i < 23; i++)
+            for (int i = 0; i < lowerCaseColours.length; i++)
             {
               if (lowerCaseColours[i] == null)
               {
-                lowerCaseColours[i] = getColourFromString(colour);
+                lowerCaseColours[i] = ColorUtils.parseColourString(colour);
               }
             }
 
             continue;
           }
 
-          if (token.equals(token.toLowerCase()))
+          if (residue.equals(residue.toLowerCase()))
           {
             if (lowerCaseColours == null)
             {
-              lowerCaseColours = new Color[23];
+              lowerCaseColours = new Color[colors.length];
             }
-            lowerCaseColours[colIndex] = getColourFromString(colour);
+            lowerCaseColours[colIndex] = ColorUtils.parseColourString(colour);
           }
           else
           {
-            colors[colIndex] = getColourFromString(colour);
+            colors[colIndex] = ColorUtils.parseColourString(colour);
           }
         }
       }
@@ -232,39 +238,115 @@ public class UserColourScheme extends ResidueColourScheme
 
   }
 
-  @Override
-  public Color findColour(char c, int j, SequenceI seq)
+  public void setLowerCaseColours(Color[] lcolours)
   {
-    Color currentColour;
-    int index = ResidueProperties.aaIndex[c];
+    lowerCaseColours = lcolours;
+  }
 
-    if ((threshold == 0) || aboveThreshold(c, j))
+  /**
+   * Returns the colour for the given residue character. If the residue is
+   * lower-case, and there is a specific colour defined for lower case, that
+   * colour is returned, else the colour for the upper case residue.
+   */
+  @Override
+  public Color findColour(char c)
+  {
+    if ('a' <= c && c <= 'z' && lowerCaseColours != null)
     {
-      if (lowerCaseColours != null && 'a' <= c && c <= 'z')
-      {
-        currentColour = lowerCaseColours[index];
-      }
-      else
+      Color colour = lowerCaseColours[symbolIndex[c]];
+      if (colour != null)
       {
-        currentColour = colors[index];
+        return colour;
       }
     }
-    else
+    return super.findColour(c);
+  }
+
+  /**
+   * Answers the customised name of the colour scheme, if it has one, else
+   * "User Defined"
+   */
+  @Override
+  public String getSchemeName()
+  {
+    if (schemeName != null && schemeName.length() > 0)
     {
-      currentColour = Color.white;
+      return schemeName;
     }
+    return "User Defined";
+  }
+
+  /**
+   * Generate an applet colour parameter like A,C,D=12ffe9;Q,W=2393fd;w=9178dd
+   * 
+   * @return
+   */
+  public String toAppletParameter()
+  {
+    /*
+     * step 1: build a map from colours to the symbol(s) that have the colour
+     */
+    Map<Color, List<String>> colours = new HashMap<Color, List<String>>();
 
-    if (conservationColouring)
+    for (char symbol = 'A'; symbol <= 'Z'; symbol++)
     {
-      currentColour = applyConservation(currentColour, j);
+      String residue = String.valueOf(symbol);
+      int index = symbolIndex[symbol];
+      Color c = colors[index];
+      if (c != null && !c.equals(Color.white))
+      {
+        if (colours.get(c) == null)
+        {
+          colours.put(c, new ArrayList<String>());
+        }
+        colours.get(c).add(residue);
+      }
+      if (lowerCaseColours != null)
+      {
+        c = lowerCaseColours[index];
+        if (c != null && !c.equals(Color.white))
+        {
+          residue = residue.toLowerCase();
+          if (colours.get(c) == null)
+          {
+            colours.put(c, new ArrayList<String>());
+          }
+          colours.get(c).add(residue);
+        }
+      }
     }
 
-    return currentColour;
-  }
+    /*
+     * step 2: make a list of { A,G,R=12f9d6 } residues/colour specs
+     */
+    List<String> residueColours = new ArrayList<String>();
+    for (Entry<Color, List<String>> cols : colours.entrySet())
+    {
+      boolean first = true;
+      StringBuilder sb = new StringBuilder();
+      for (String residue : cols.getValue())
+      {
+        if (!first)
+        {
+          sb.append(",");
+        }
+        sb.append(residue);
+        first = false;
+      }
+      sb.append("=");
+      /*
+       * get color as hex value, dropping the alpha (ff) part
+       */
+      String hexString = Integer.toHexString(cols.getKey().getRGB())
+              .substring(2);
+      sb.append(hexString);
+      residueColours.add(sb.toString());
+    }
 
-  public void setLowerCaseColours(Color[] lcolours)
-  {
-    lowerCaseColours = lcolours;
+    /*
+     * sort and output
+     */
+    Collections.sort(residueColours);
+    return StringUtils.listToDelimitedString(residueColours, ";");
   }
-
 }
index 8fa1656..c32a39c 100755 (executable)
  */
 package jalview.schemes;
 
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+
+import java.util.Map;
+
 /**
  * DOCUMENT ME!
  * 
@@ -33,6 +39,29 @@ public class ZappoColourScheme extends ResidueColourScheme
    */
   public ZappoColourScheme()
   {
-    super(ResidueProperties.aaIndex, ResidueProperties.zappo, 0);
+    super(ResidueProperties.aaIndex, ResidueProperties.zappo);
+  }
+
+  @Override
+  public boolean isPeptideSpecific()
+  {
+    return true;
+  }
+
+  @Override
+  public String getSchemeName()
+  {
+    return JalviewColourScheme.Zappo.toString();
+  }
+
+  /**
+   * Returns a new instance of this colour scheme with which the given data may
+   * be coloured
+   */
+  @Override
+  public ColourSchemeI getInstance(AnnotatedCollectionI coll,
+          Map<SequenceI, SequenceCollectionI> hrs)
+  {
+    return new ZappoColourScheme();
   }
 }
index 2877d46..fe878ce 100644 (file)
@@ -38,5 +38,7 @@ public interface SelectionListener
    *          - source of the selection event
    */
   public void selection(jalview.datamodel.SequenceGroup seqsel,
-          jalview.datamodel.ColumnSelection colsel, SelectionSource source);
+          jalview.datamodel.ColumnSelection colsel,
+          jalview.datamodel.HiddenColumns hidden,
+          SelectionSource source);
 }
index e5c5d04..9fde3f1 100644 (file)
@@ -31,7 +31,7 @@ public interface StructureListener
    * handles messages for, or null if generic listener (only used by
    * removeListener method)
    */
-  public String[] getPdbFile();
+  public String[] getStructureFiles();
 
   /**
    * Called by StructureSelectionManager to inform viewer to highlight given
index fb8e3f8..db0b47e 100644 (file)
@@ -29,12 +29,14 @@ import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceI;
 import jalview.ext.jmol.JmolParser;
 import jalview.gui.IProgressIndicator;
+import jalview.io.AppletFormatAdapter;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
 import jalview.util.MappingUtils;
@@ -299,7 +301,7 @@ public class StructureSelectionManager
   {
     for (StructureMapping sm : mappings)
     {
-      if (sm.getPdbId().equalsIgnoreCase(pdbid))
+      if (sm.getPdbId().equals(pdbid))
       {
         return sm.pdbfile;
       }
@@ -384,6 +386,7 @@ public class StructureSelectionManager
     boolean isMapUsingSIFTs = SiftsSettings.isMapWithSifts();
     try
     {
+      sourceType = AppletFormatAdapter.checkProtocol(pdbFile);
       pdb = new JmolParser(pdbFile, sourceType);
 
       if (pdb.getId() != null && pdb.getId().trim().length() > 0
@@ -454,7 +457,7 @@ public class StructureSelectionManager
        * Attempt pairwise alignment of the sequence with each chain in the PDB,
        * and remember the highest scoring chain
        */
-      int max = -10;
+      float max = -10;
       AlignSeq maxAlignseq = null;
       String maxChainId = " ";
       PDBChain maxChain = null;
@@ -589,11 +592,9 @@ public class StructureSelectionManager
     return pdb;
   }
 
-  private boolean isCIFFile(String filename)
+  public void addStructureMapping(StructureMapping sm)
   {
-    String fileExt = filename.substring(filename.lastIndexOf(".") + 1,
-            filename.length());
-    return "cif".equalsIgnoreCase(fileExt);
+    mappings.add(sm);
   }
 
   /**
@@ -741,7 +742,7 @@ public class StructureSelectionManager
       if (listeners.elementAt(i) instanceof StructureListener)
       {
         sl = (StructureListener) listeners.elementAt(i);
-        for (String pdbfile : sl.getPdbFile())
+        for (String pdbfile : sl.getStructureFiles())
         {
           pdbs.remove(pdbfile);
         }
@@ -824,10 +825,10 @@ public class StructureSelectionManager
    * @param atoms
    * @return
    */
-  public SearchResults findAlignmentPositionsForStructurePositions(
+  public SearchResultsI findAlignmentPositionsForStructurePositions(
           List<AtomSpec> atoms)
   {
-    SearchResults results = new SearchResults();
+    SearchResultsI results = new SearchResults();
     for (AtomSpec atom : atoms)
     {
       SequenceI lastseq = null;
@@ -1203,13 +1204,14 @@ public class StructureSelectionManager
 
   public synchronized void sendSelection(
           jalview.datamodel.SequenceGroup selection,
-          jalview.datamodel.ColumnSelection colsel, SelectionSource source)
+          jalview.datamodel.ColumnSelection colsel, HiddenColumns hidden,
+          SelectionSource source)
   {
     for (SelectionListener slis : sel_listeners)
     {
       if (slis != source)
       {
-        slis.selection(selection, colsel, source);
+        slis.selection(selection, colsel, hidden, source);
       }
     }
   }
index 063eacf..1637631 100644 (file)
  */
 package jalview.structures.models;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.api.SequenceRenderer;
 import jalview.api.StructureSelectionManagerProvider;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
+import jalview.schemes.ColourSchemeI;
 import jalview.structure.AtomSpec;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
+import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.Comparison;
 import jalview.util.MessageManager;
 
+import java.awt.Color;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 
 /**
@@ -85,6 +92,8 @@ public abstract class AAStructureBindingModel extends
    */
   protected String[] modelFileNames = null;
 
+  public String fileLoadingError;
+
   /**
    * Data bean class to simplify parameterisation in superposeStructures
    */
@@ -512,18 +521,18 @@ public abstract class AAStructureBindingModel extends
    *          the sequence alignment which is the basis of structure
    *          superposition
    * @param matched
-   *          an array of booleans, indexed by alignment column, where true
-   *          indicates that every structure has a mapped residue present in the
-   *          column (so the column can participate in structure alignment)
+   *          a BitSet, where bit j is set to indicate that every structure has
+   *          a mapped residue present in column j (so the column can
+   *          participate in structure alignment)
    * @param structures
    *          an array of data beans corresponding to pdb file index
    * @return
    */
   protected int findSuperposableResidues(AlignmentI alignment,
-          boolean[] matched, SuperposeData[] structures)
+          BitSet matched, SuperposeData[] structures)
   {
     int refStructure = -1;
-    String[] files = getPdbFile();
+    String[] files = getStructureFiles();
     if (files == null)
     {
       return -1;
@@ -550,16 +559,16 @@ public abstract class AAStructureBindingModel extends
             {
               refStructure = pdbfnum;
             }
-            for (int r = 0; r < matched.length; r++)
+            for (int r = 0; r < alignment.getWidth(); r++)
             {
-              if (!matched[r])
+              if (!matched.get(r))
               {
                 continue;
               }
               int pos = getMappedPosition(theSequence, r, mapping);
               if (pos < 1 || pos == lastPos)
               {
-                matched[r] = false;
+                matched.clear(r);
                 continue;
               }
               lastPos = pos;
@@ -688,4 +697,79 @@ public abstract class AAStructureBindingModel extends
   {
     return null;
   }
+
+  public abstract void setJalviewColourScheme(ColourSchemeI cs);
+
+  /**
+   * Constructs and sends a command to align structures against a reference
+   * structure, based on one or more sequence alignments. May optionally return
+   * an error or warning message for the alignment command.
+   * 
+   * @param alignments
+   *          an array of alignments to process
+   * @param structureIndices
+   *          an array of corresponding reference structures (index into pdb
+   *          file array); if a negative value is passed, the first PDB file
+   *          mapped to an alignment sequence is used as the reference for
+   *          superposition
+   * @param hiddenCols
+   *          an array of corresponding hidden columns for each alignment
+   * @return
+   */
+  public abstract String superposeStructures(AlignmentI[] alignments,
+          int[] structureIndices, HiddenColumns[] hiddenCols);
+
+  public abstract void setBackgroundColour(Color col);
+
+  protected abstract StructureMappingcommandSet[] getColourBySequenceCommands(
+          String[] files, SequenceRenderer sr, AlignmentViewPanel avp);
+
+  /**
+   * returns the current sequenceRenderer that should be used to colour the
+   * structures
+   * 
+   * @param alignment
+   * 
+   * @return
+   */
+  public abstract SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment);
+
+  protected abstract void colourBySequence(
+          StructureMappingcommandSet[] colourBySequenceCommands);
+
+  public abstract void colourByChain();
+
+  public abstract void colourByCharge();
+
+  /**
+   * colour any structures associated with sequences in the given alignment
+   * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
+   * if colourBySequence is enabled.
+   */
+  public void colourBySequence(AlignmentViewPanel alignmentv)
+  {
+    if (!colourBySequence || !isLoadingFinished())
+    {
+      return;
+    }
+    if (getSsm() == null)
+    {
+      return;
+    }
+    String[] files = getStructureFiles();
+  
+    SequenceRenderer sr = getSequenceRenderer(alignmentv);
+  
+    StructureMappingcommandSet[] colourBySequenceCommands = getColourBySequenceCommands(
+            files, sr, alignmentv);
+    colourBySequence(colourBySequenceCommands);
+  }
+
+  public boolean hasFileLoadingError()
+  {
+    return fileLoadingError != null && fileLoadingError.length() > 0;
+  }
+
+  public abstract jalview.api.FeatureRenderer getFeatureRenderer(
+          AlignmentViewPanel alignment);
 }
diff --git a/src/jalview/urls/CustomUrlProvider.java b/src/jalview/urls/CustomUrlProvider.java
new file mode 100644 (file)
index 0000000..07f4068
--- /dev/null
@@ -0,0 +1,342 @@
+/*
+ * 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.urls;
+
+import static jalview.util.UrlConstants.DB_ACCESSION;
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
+import static jalview.util.UrlConstants.SEQUENCE_ID;
+
+import jalview.util.MessageManager;
+import jalview.util.UrlConstants;
+import jalview.util.UrlLink;
+
+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;
+import java.util.StringTokenizer;
+
+/**
+ * 
+ * Implements the UrlProviderI interface for a UrlProvider object which serves
+ * custom URLs defined by the user
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class CustomUrlProvider extends UrlProviderImpl
+{
+  // Default sequence URL link label for SRS
+  private static final String SRS_LABEL = "SRS";
+
+  // map of string ids to urlLinks (selected)
+  private HashMap<String, UrlLink> selectedUrls;
+
+  // map of string ids to urlLinks (not selected)
+  private HashMap<String, UrlLink> nonselectedUrls;
+
+  /**
+   * Construct UrlProvider for custom (user-entered) URLs
+   * 
+   * @param inMenuUrlList
+   *          list of URLs set to be displayed in menu, in form stored in Cache.
+   *          i.e. SEP delimited string
+   * @param storedUrlList
+   *          list of custom URLs entered by user but not currently displayed in
+   *          menu, in form stored in Cache
+   */
+  public CustomUrlProvider(String inMenuUrlList, String storedUrlList)
+  {
+    try
+    {
+      selectedUrls = parseUrlStrings(inMenuUrlList);
+      nonselectedUrls = parseUrlStrings(storedUrlList);
+    } catch (Exception ex)
+    {
+      System.out
+              .println(ex.getMessage() + "\nError parsing sequence links");
+    }
+  }
+
+  /**
+   * Construct UrlProvider for custom (user-entered) URLs
+   * 
+   * @param urlList
+   *          list of URLs to be displayed in menu, as (label,url) pairs
+   * @param storedUrlList
+   *          list of custom URLs entered by user but not currently displayed in
+   *          menu, as (label,url) pairs
+   */
+  public CustomUrlProvider(Map<String, String> inMenuUrlList,
+          Map<String, String> storedUrlList)
+  {
+    try
+    {
+      selectedUrls = parseUrlList(inMenuUrlList);
+      nonselectedUrls = parseUrlList(storedUrlList);
+    } catch (Exception ex)
+    {
+      System.out
+              .println(ex.getMessage() + "\nError parsing sequence links");
+    }
+  }
+
+  private HashMap<String, UrlLink> parseUrlStrings(String urlStrings)
+  {
+    // cachedUrlList is in form <label>|<url>|<label>|<url>...
+    // parse cachedUrlList into labels (used as id) and url links
+    HashMap<String, UrlLink> urls = new HashMap<String, UrlLink>();
+
+    StringTokenizer st = new StringTokenizer(urlStrings, SEP);
+    while (st.hasMoreElements())
+    {
+      String name = st.nextToken();
+
+      if (!isMiriamId(name))
+      {
+        // this one of our custom urls
+        String url = st.nextToken();
+        // check for '|' within a regex
+        int rxstart = url.indexOf(DELIM + DB_ACCESSION + DELIM);
+        if (rxstart == -1)
+        {
+          rxstart = url.indexOf(DELIM + SEQUENCE_ID + DELIM);
+        }
+        while (rxstart == -1 && url.indexOf("/=" + DELIM) == -1
+                && st.hasMoreTokens())
+        {
+          url = url + SEP + st.nextToken();
+        }
+        urls.put(name, new UrlLink(name, url, name));
+      }
+    }
+    upgradeOldLinks(urls);
+    return urls;
+  }
+
+  private HashMap<String, UrlLink> parseUrlList(Map<String, String> urlList)
+  {
+    HashMap<String, UrlLink> urls = new HashMap<String, UrlLink>();
+    if (urlList == null)
+    {
+      return urls;
+    }
+
+    Iterator<Map.Entry<String, String>> it = urlList.entrySet().iterator();
+    while (it.hasNext())
+    {
+      Map.Entry<String, String> pair = it.next();
+      urls.put(pair.getKey(),
+ new UrlLink(pair.getKey(), pair.getValue(),
+              pair.getKey()));
+    }
+    upgradeOldLinks(urls);
+    return urls;
+  }
+
+  /*
+   * Upgrade any legacy links which may have been left lying around
+   */
+  private void upgradeOldLinks(HashMap<String, UrlLink> urls)
+  {
+    // upgrade old SRS link
+    if (urls.containsKey(SRS_LABEL))
+    {
+      urls.remove(SRS_LABEL);
+      UrlLink link = new UrlLink(UrlConstants.DEFAULT_STRING);
+      link.setLabel(UrlConstants.DEFAULT_LABEL);
+      urls.put(UrlConstants.DEFAULT_LABEL, link);
+    }
+  }
+
+  @Override
+  public List<String> getLinksForMenu()
+  {
+    List<String> links = new ArrayList<String>();
+    Iterator<Map.Entry<String, UrlLink>> it = selectedUrls.entrySet()
+            .iterator();
+    while (it.hasNext())
+    {
+      Map.Entry<String, UrlLink> pair = it.next();
+      links.add(pair.getValue().toString());
+    }
+    return links;
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    ArrayList<UrlLinkDisplay> displayLinks = new ArrayList<UrlLinkDisplay>();
+    displayLinks = getLinksForTable(selectedUrls, true);
+    displayLinks.addAll(getLinksForTable(nonselectedUrls, false));
+    return displayLinks;
+  }
+
+  private ArrayList<UrlLinkDisplay> getLinksForTable(
+          HashMap<String, UrlLink> urlList, boolean selected)
+  {
+    return super.getLinksForTable(urlList, null, selected);
+  }
+
+  @Override
+  public boolean setPrimaryUrl(String id)
+  {
+    if (selectedUrls.containsKey(id))
+    {
+      primaryUrl = id;
+    }
+    else if (nonselectedUrls.containsKey(id))
+    {
+      primaryUrl = id;
+    }
+    else
+    {
+      primaryUrl = null;
+    }
+
+    return (primaryUrl != null);
+  }
+
+  @Override
+  public String writeUrlsAsString(boolean selected)
+  {
+    StringBuffer links = new StringBuffer();
+    HashMap<String, UrlLink> urls;
+    if (selected)
+    {
+      urls = selectedUrls;
+    }
+    else
+    {
+      urls = nonselectedUrls;
+    }
+    if (urls.size() > 0)
+    {
+      for (Entry<String, UrlLink> entry : urls.entrySet())
+      {
+        links.append(entry.getValue().toString());
+        links.append(SEP);
+      }
+
+      // remove last SEP
+      links.setLength(links.length() - 1);
+    }
+    else
+    {
+      urls.clear();
+    }
+    return links.toString();
+  }
+
+  @Override
+  public String getPrimaryUrl(String seqid)
+  {
+    String result = super.getPrimaryUrl(seqid, selectedUrls);
+    if (result == null)
+    {
+      result = super.getPrimaryUrl(seqid, nonselectedUrls);
+    }
+    return result;
+  }
+
+  @Override
+  public String getPrimaryUrlId()
+  {
+    return primaryUrl;
+  }
+
+  @Override
+  public String getPrimaryTarget(String seqid)
+  {
+    return selectedUrls.get(primaryUrl).getTarget();
+  }
+
+  @Override
+  public void setUrlData(List<UrlLinkDisplay> links)
+  {
+    HashMap<String, UrlLink> unselurls = new HashMap<String, UrlLink>();
+    HashMap<String, UrlLink> selurls = new HashMap<String, UrlLink>();
+
+    Iterator<UrlLinkDisplay> it = links.iterator();
+    while (it.hasNext())
+    {
+      UrlLinkDisplay link = it.next();
+
+      // MIRIAM ids will be handled by a different UrlProvider class
+      if (!isMiriamId(link.getId()))
+      {
+        // don't allow duplicate key names as entries will be overwritten
+        if (unselurls.containsKey(link.getId())
+                || selurls.containsKey(link.getId()))
+        {
+          throw new IllegalArgumentException(MessageManager.formatMessage(
+                  "exception.url_cannot_have_duplicate_id", link.getId()));
+        }
+        if (link.getIsSelected())
+        {
+          selurls.put(link.getId(),
+                  new UrlLink(link.getDescription(), link.getUrl(), link.getDescription()));
+        }
+        else
+        {
+          unselurls
+                  .put(link.getId(),
+                          new UrlLink(link.getDescription(), link.getUrl(), link
+                                  .getDescription()));
+        }
+        // sort out primary and selected ids
+        if (link.getIsPrimary())
+        {
+          setPrimaryUrl(link.getId());
+        }
+      }
+
+    }
+    nonselectedUrls = unselurls;
+    selectedUrls = selurls;
+  }
+
+  @Override
+  public String choosePrimaryUrl()
+  {
+    // unilaterally set the primary id to the EMBL_EBI link
+    if ((!nonselectedUrls.containsKey(UrlConstants.DEFAULT_LABEL))
+            && (!selectedUrls.containsKey(UrlConstants.DEFAULT_LABEL)))
+    {
+      UrlLink link = new UrlLink(UrlConstants.DEFAULT_STRING);
+      link.setLabel(UrlConstants.DEFAULT_LABEL);
+      selectedUrls.put(UrlConstants.DEFAULT_LABEL, link);
+    }
+    primaryUrl = UrlConstants.DEFAULT_LABEL;
+    return UrlConstants.DEFAULT_LABEL;
+  }
+
+  @Override
+  public boolean contains(String id)
+  {
+    return (selectedUrls.containsKey(id) || nonselectedUrls.containsKey(id));
+  }
+
+}
diff --git a/src/jalview/urls/IdOrgSettings.java b/src/jalview/urls/IdOrgSettings.java
new file mode 100644 (file)
index 0000000..7dd1a19
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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.urls;
+
+/**
+ * Holds settings for identifiers.org e.g. url, download location
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class IdOrgSettings
+{
+  private static String url;
+
+  private static String location;
+
+  public static void setUrl(String seturl)
+  {
+    url = seturl;
+  }
+
+  public static void setDownloadLocation(String setloc)
+  {
+    location = setloc;
+  }
+
+  public static String getUrl()
+  {
+    return url;
+  }
+
+  public static String getDownloadLocation()
+  {
+    return location;
+  }
+}
diff --git a/src/jalview/urls/IdentifiersUrlProvider.java b/src/jalview/urls/IdentifiersUrlProvider.java
new file mode 100644 (file)
index 0000000..c938666
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+ * 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.urls;
+
+import static jalview.util.UrlConstants.DB_ACCESSION;
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
+
+import jalview.util.UrlLink;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+/**
+ * 
+ * Implements the UrlProviderI interface for a UrlProvider object which serves
+ * URLs from identifiers.org
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class IdentifiersUrlProvider extends UrlProviderImpl
+{
+
+  private static final String LOCAL_KEY = "Local";
+
+  private static final String ID_ORG_KEY = "identifiers.org";
+
+  // map of string ids to urls
+  private HashMap<String, UrlLink> urls;
+
+  // list of selected urls
+  private ArrayList<String> selectedUrls;
+
+  public IdentifiersUrlProvider(String cachedUrlList)
+  {
+    urls = readIdentifiers(IdOrgSettings.getDownloadLocation());
+    selectedUrls = new ArrayList<String>();
+    checkSelectionMatchesUrls(cachedUrlList);
+  }
+
+  /**
+   * Read data from an identifiers.org download file
+   * 
+   * @param idFileName
+   *          name of identifiers.org download file
+   * @return hashmap of identifiers.org data, keyed by MIRIAM id
+   */
+  private HashMap<String, UrlLink> readIdentifiers(
+          String idFileName)
+  {
+    JSONParser parser = new JSONParser();
+
+    // identifiers.org data
+    HashMap<String, UrlLink> idData = new HashMap<String, UrlLink>();
+
+    try
+    {
+      FileReader reader = new FileReader(idFileName);
+      String key = "";
+      JSONObject obj = (JSONObject) parser.parse(reader);
+      if (obj.containsKey(ID_ORG_KEY))
+      {
+        key = ID_ORG_KEY;
+      }
+      else if (obj.containsKey(LOCAL_KEY))
+      {
+        key = LOCAL_KEY;
+      }
+      else
+      {
+        System.out
+                .println("Unexpected key returned from identifiers jalview service");
+        return idData;
+      }
+
+      JSONArray jsonarray = (JSONArray) obj.get(key);
+
+      // loop over each entry in JSON array and build HashMap entry
+      for (int i = 0; i < jsonarray.size(); i++)
+      {
+        JSONObject item = (JSONObject) jsonarray.get(i);
+
+        String url = (String) item.get("url") + "/" + DELIM + DB_ACCESSION
+                + DELIM;
+        UrlLink link = new UrlLink((String) item.get("name"), url,
+                (String) item.get("prefix"));
+        idData.put((String) item.get("id"), link);
+      }
+    } catch (FileNotFoundException e)
+    {
+      e.printStackTrace();
+      idData.clear();
+    } catch (IOException e)
+    {
+      e.printStackTrace();
+      idData.clear();
+    } catch (ParseException e)
+    {
+      e.printStackTrace();
+      idData.clear();
+    }
+    return idData;
+  }
+
+  private void checkSelectionMatchesUrls(String cachedUrlList)
+  {
+    StringTokenizer st = new StringTokenizer(cachedUrlList, SEP);
+    while (st.hasMoreElements())
+    {
+      String id = st.nextToken();
+
+      if (isMiriamId(id))
+      {
+        // this is an identifiers.org MIRIAM id
+        if (urls.containsKey(id))
+        {
+          selectedUrls.add(id);
+        }
+      }
+    }
+
+    // reset defaultUrl in case it is no longer selected
+    setPrimaryUrl(primaryUrl);
+  }
+
+  @Override
+  public boolean setPrimaryUrl(String id)
+  {
+    if (urls.containsKey(id))
+    {
+      primaryUrl = id;
+    }
+    else
+    {
+      primaryUrl = null;
+    }
+
+    return urls.containsKey(id);
+  }
+
+  @Override
+  public String writeUrlsAsString(boolean selected)
+  {
+    if (!selected)
+    {
+      return ""; // we don't cache unselected identifiers.org urls
+    }
+
+    StringBuffer links = new StringBuffer();
+    if (!selectedUrls.isEmpty())
+    {
+      for (String k : selectedUrls)
+      {
+        links.append(k);
+        links.append(SEP);
+      }
+      // remove last SEP
+      links.setLength(links.length() - 1);
+    }
+    return links.toString();
+  }
+
+  @Override
+  public List<String> getLinksForMenu()
+  {
+    List<String> links = new ArrayList<String>();
+    for (String key : selectedUrls)
+    {
+      links.add(urls.get(key).toStringWithTarget());
+    }
+    return links;
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    return super.getLinksForTable(urls, selectedUrls, false);
+  }
+
+  @Override
+  public void setUrlData(List<UrlLinkDisplay> links)
+  {
+    selectedUrls = new ArrayList<String>();
+
+    Iterator<UrlLinkDisplay> it = links.iterator();
+    while (it.hasNext())
+    {
+      UrlLinkDisplay link = it.next();
+
+      // Handle links with MIRIAM ids only
+      if (isMiriamId(link.getId()))
+      {
+        // select/deselect links accordingly and set default url
+        if (urls.containsKey(link.getId()))
+        {
+          if (link.getIsSelected())
+          {
+            selectedUrls.add(link.getId());
+          }
+          if (link.getIsPrimary())
+          {
+            setPrimaryUrl(link.getId());
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public String getPrimaryUrl(String seqid)
+  {
+    return super.getPrimaryUrl(seqid, urls);
+  }
+
+  @Override
+  public String getPrimaryUrlId()
+  {
+    return primaryUrl;
+  }
+
+  @Override
+  public String getPrimaryTarget(String seqid)
+  {
+    return null;
+  }
+
+  @Override
+  public String choosePrimaryUrl()
+  {
+    return null;
+  }
+
+  @Override
+  public boolean contains(String id)
+  {
+    return (urls.containsKey(id));
+  }
+}
diff --git a/src/jalview/urls/UrlLinkDisplay.java b/src/jalview/urls/UrlLinkDisplay.java
new file mode 100644 (file)
index 0000000..5b376a2
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * 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.urls;
+
+import jalview.util.MessageManager;
+import jalview.util.UrlLink;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UrlLink table row definition
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class UrlLinkDisplay
+{
+  // column positions
+  public static final int DATABASE = 0;
+
+  public static final int NAME = 1;
+
+  public static final int URL = 2;
+
+  public static final int SELECTED = 3;
+
+  public static final int PRIMARY = 4;
+
+  public static final int ID = 5;
+
+  // Headers for columns in table
+  @SuppressWarnings("serial")
+  private static final List<String> COLNAMES = new ArrayList<String>()
+  {
+    {
+      add(MessageManager.formatMessage("label.database"));
+      add(MessageManager.formatMessage("label.name"));
+      add(MessageManager.formatMessage("label.url"));
+      add(MessageManager.formatMessage("label.inmenu"));
+      add(MessageManager.formatMessage("label.primary"));
+      add(MessageManager.formatMessage("label.id"));
+    }
+  };
+
+  private String id; // id is not supplied to display, but used to identify
+  // entries when saved
+
+  private boolean isPrimary;
+
+  private boolean isSelected;
+
+  private UrlLink link;
+
+  public UrlLinkDisplay(String rowId, UrlLink rowLink,
+          boolean rowSelected, boolean rowDefault)
+  {
+    id = rowId;
+    isPrimary = rowDefault;
+    isSelected = rowSelected;
+
+    link = rowLink;
+  }
+
+  // getters/setters
+  public String getId()
+  {
+    return id;
+  }
+
+  public String getDescription()
+  {
+    return link.getLabel();
+  }
+
+  public String getDBName()
+  {
+    return link.getTarget();
+  }
+
+  public String getUrl()
+  {
+    return link.getUrlWithToken();
+  }
+
+  public boolean getIsPrimary()
+  {
+    return isPrimary;
+  }
+
+  public boolean getIsSelected()
+  {
+    return isSelected;
+  }
+
+  public void setDBName(String name)
+  {
+    link.setTarget(name);
+  }
+
+  public void setUrl(String rowUrl)
+  {
+    link = new UrlLink(getDescription(), rowUrl, getDBName());
+  }
+
+  public void setDescription(String desc)
+  {
+    link.setLabel(desc);
+  }
+
+  public void setIsDefault(boolean rowDefault)
+  {
+    isPrimary = rowDefault;
+  }
+
+  public void setIsSelected(boolean rowSelected)
+  {
+    isSelected = rowSelected;
+  }
+
+  public Object getValue(int index)
+  {
+    switch (index)
+    {
+    case ID:
+      return id;
+    case URL:
+      return getUrl();
+    case PRIMARY:
+      return isPrimary;
+    case SELECTED:
+      return isSelected;
+    case NAME:
+      return getDescription();
+    case DATABASE:
+      return getDBName();
+    default:
+      return null;
+    }
+  }
+
+  public void setValue(int index, Object value)
+  {
+    switch (index)
+    {
+    case ID:
+      id = (String) value;
+      break;
+    case URL:
+      setUrl((String) value);
+      break;
+    case PRIMARY:
+      isPrimary = (boolean) value;
+      break;
+    case SELECTED:
+      isSelected = (boolean) value;
+      break;
+    case NAME:
+      setDescription((String) value);
+      break;
+    case DATABASE:
+      setDBName((String) value);
+      break;
+    default:
+      // do nothing
+    }
+  }
+
+  /**
+   * Identify editable columns
+   * 
+   * @param index
+   *          index of column
+   * @return whether column can be edited in table
+   */
+  public boolean isEditable(int index)
+  {
+    if (index == PRIMARY)
+    {
+      // primary link must not be a $DB_ACCESSION$ link
+      // so only allow editing if it is not
+      return (!link.usesDBAccession());
+    }
+    else
+    {
+      return index == SELECTED;
+    }
+  }
+
+  /**
+   * Get list of column names to display in UI
+   * 
+   * @return column names
+   */
+  public static List<String> getDisplayColumnNames()
+  {
+    // Display names between DESCRIPTION and ID (excludes ID)
+    return COLNAMES.subList(DATABASE, ID);
+  }
+}
diff --git a/src/jalview/urls/UrlLinkTableModel.java b/src/jalview/urls/UrlLinkTableModel.java
new file mode 100644 (file)
index 0000000..d6d26b5
--- /dev/null
@@ -0,0 +1,277 @@
+/*
+ * 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.urls;
+
+import jalview.bin.Cache;
+import jalview.urls.api.UrlProviderI;
+import jalview.util.UrlLink;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.RowFilter.Entry;
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableModel;
+
+/**
+ * TableModel for UrlLinks table
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class UrlLinkTableModel extends AbstractTableModel
+{
+  // local storage of data
+  private List<UrlLinkDisplay> data;
+
+  // supplier of url data
+  private UrlProviderI dataProvider;
+
+  // list of columns to display in table in correct order
+  private List<String> displayColumns;
+
+  // row in table which is currently the primary
+  private int primaryRow;
+
+  /**
+   * UrlLinkTableModel constructor
+   * 
+   * @param baseData
+   *          base data set to be presented in table
+   * @param entryNames
+   *          keys of entries in baseData's nested hashmap. Should match order
+   *          in displayColNames
+   * @param displayColNames
+   *          names of columns to display in order.
+   * @param keyColName
+   *          name of column corresponding to keys in baseData
+   */
+  public UrlLinkTableModel(UrlProviderI baseData)
+  {
+    dataProvider = baseData;
+    data = baseData.getLinksForTable();
+    displayColumns = UrlLinkDisplay.getDisplayColumnNames();
+
+    // find the primary row
+    primaryRow = 0;
+    Iterator<UrlLinkDisplay> it = data.iterator();
+    while (it.hasNext())
+    {
+      if (it.next().getIsPrimary())
+      {
+        break;
+      }
+      else
+      {
+        primaryRow++;
+      }
+    }
+
+    // set up listener which updates data source when table changes
+    this.addTableModelListener(new TableModelListener()
+    {
+      @Override
+      public void tableChanged(TableModelEvent e)
+      {
+        try
+        {
+          // update the UrlProvider from data list
+          dataProvider.setUrlData(data);
+        } catch (IllegalArgumentException ex)
+        {
+          Cache.log.error(ex.getMessage());
+        }
+      }
+    });
+
+  }
+
+  @Override
+  public int getRowCount()
+  {
+    if (data == null)
+    {
+      return 0;
+    }
+    else
+    {
+      return data.size();
+    }
+  }
+
+  @Override
+  public int getColumnCount()
+  {
+    return displayColumns.size();
+  }
+
+  @Override
+  public Object getValueAt(int rowIndex, int columnIndex)
+  {
+    return data.get(rowIndex).getValue(columnIndex);
+  }
+
+  @Override
+  public boolean isCellEditable(int rowIndex, int columnIndex)
+  {
+    return data.get(rowIndex).isEditable(columnIndex);
+  }
+
+  /**
+   * Determine if a row is editable indirectly (rather than directly in table as
+   * in isCellEditable)
+   * 
+   * @param rowIndex
+   * @return true if row can be edited indirectly
+   */
+  public boolean isRowEditable(int rowIndex)
+  {
+    // to edit, row must be a user entered row
+    return (dataProvider.isUserEntry(data.get(rowIndex).getId()));
+  }
+
+  /**
+   * Determine if a row is deletable
+   * 
+   * @param rowIndex
+   *          the row to be tested
+   * @return true if row can be deleted
+   */
+  public boolean isRowDeletable(int rowIndex)
+  {
+    // to delete, row must be a user entered row, and not the default row
+    return (dataProvider.isUserEntry(data.get(rowIndex).getId()) && !data
+            .get(rowIndex).getIsPrimary());
+  }
+
+  @Override
+  public void setValueAt(Object aValue, int rowIndex, int columnIndex)
+  {
+    if (columnIndex == UrlLinkDisplay.PRIMARY)
+    {
+      // Default url column: exactly one row must always be true
+      if (rowIndex != primaryRow)
+      {
+        // selected row is not currently the default
+        // set the current default to false
+        data.get(primaryRow).setValue(columnIndex, false);
+        fireTableRowsUpdated(primaryRow, primaryRow);
+
+        // set the default to be the selected row
+        primaryRow = rowIndex;
+        data.get(rowIndex).setValue(columnIndex, aValue);
+
+        fireTableRowsUpdated(rowIndex, rowIndex);
+      }
+    }
+    else
+    {
+      data.get(rowIndex).setValue(columnIndex, aValue);
+      fireTableRowsUpdated(rowIndex, rowIndex);
+    }
+  }
+
+  @Override
+  public Class<?> getColumnClass(int columnIndex)
+  {
+    return getValueAt(0, columnIndex).getClass();
+  }
+
+  @Override
+  public String getColumnName(int columnIndex)
+  {
+    return displayColumns.get(columnIndex);
+  }
+
+  public void removeRow(int rowIndex)
+  {
+    // remove the row from data
+    data.remove(rowIndex);
+
+    // update default row
+    if (primaryRow > rowIndex)
+    {
+      primaryRow--;
+    }
+
+    // fire update which will update data source
+    fireTableRowsDeleted(rowIndex, rowIndex);
+  }
+
+  public int insertRow(String name, String url)
+  {
+    // add a row to the data
+    UrlLink link = new UrlLink(name, url, name);
+    UrlLinkDisplay u = new UrlLinkDisplay(name, link, true, false);
+    int index = data.size();
+    data.add(u);
+
+    // fire update which will update data source
+    fireTableRowsInserted(index, index);
+    return index;
+  }
+
+  public int getPrimaryColumn()
+  {
+    return UrlLinkDisplay.PRIMARY;
+  }
+
+  public int getNameColumn()
+  {
+    return UrlLinkDisplay.NAME;
+  }
+
+  public int getDatabaseColumn()
+  {
+    return UrlLinkDisplay.DATABASE;
+  }
+
+  public int getIdColumn()
+  {
+    return UrlLinkDisplay.ID;
+  }
+
+  public int getUrlColumn()
+  {
+    return UrlLinkDisplay.URL;
+  }
+
+  public int getSelectedColumn()
+  {
+    return UrlLinkDisplay.SELECTED;
+  }
+
+  public boolean isUserEntry(
+          Entry<? extends TableModel, ? extends Object> entry)
+  {
+    return dataProvider
+            .isUserEntry(entry.getStringValue(UrlLinkDisplay.ID));
+  }
+
+  public boolean isUniqueName(String name)
+  {
+    return !dataProvider.contains(name);
+  }
+}
diff --git a/src/jalview/urls/UrlProvider.java b/src/jalview/urls/UrlProvider.java
new file mode 100644 (file)
index 0000000..bd01e89
--- /dev/null
@@ -0,0 +1,251 @@
+/*
+ * 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.urls;
+
+import static jalview.util.UrlConstants.SEP;
+
+import jalview.urls.api.UrlProviderI;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * 
+ * Implements the UrlProviderI interface for a composite UrlProvider object
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public class UrlProvider implements UrlProviderI
+{
+  // List of actual URL link providers
+  private List<UrlProviderI> providers;
+
+  // Specific reference to custom URL link provider
+  private UrlProviderI customProvider;
+
+  /**
+   * Constructor for UrlProvider composite
+   * 
+   * @param defaultUrlString
+   *          id of default url
+   * @param allProviders
+   *          list of UrlProviders this provider gives access to
+   */
+  public UrlProvider(String defaultUrlString,
+          List<UrlProviderI> allProviders)
+  {
+    providers = allProviders;
+
+    customProvider = findCustomProvider();
+
+    // check that the defaultUrl still exists
+    if (!contains(defaultUrlString))
+    {
+      // if the defaultUrl can't be found in any of the providers
+      // set up a custom default url
+      choosePrimaryUrl();
+    }
+    else
+    {
+      setPrimaryUrl(defaultUrlString);
+    }
+  }
+
+  /*
+   * Store ref to custom url provider
+   */
+  private UrlProviderI findCustomProvider()
+  {
+    for (UrlProviderI p : providers)
+    {
+      if (p instanceof CustomUrlProvider)
+      {
+        return p;
+      }
+    }
+
+    System.out
+            .println("Error initialising UrlProvider - no custom url provider");
+    return null;
+  }
+  
+  @Override
+  public boolean setPrimaryUrl(String id)
+  {
+    boolean outcome = false;
+    for (UrlProviderI p : providers)
+    {
+      if (p.setPrimaryUrl(id))
+      {
+        outcome = true;
+      }
+    }
+    if (!outcome)
+    {
+      throw new IllegalArgumentException();
+    }
+    return outcome;
+  }
+
+  @Override
+  public boolean contains(String id)
+  {
+    boolean outcome = false;
+    for (UrlProviderI p : providers)
+    {
+      if (p.contains(id))
+      {
+        outcome = true;
+      }
+    }
+    return outcome;
+  }
+  
+  @Override
+  public String writeUrlsAsString(boolean selected)
+  {
+    String result = "";
+    for (UrlProviderI p : providers)
+    {
+      String next = p.writeUrlsAsString(selected);
+      if (!next.isEmpty())
+      {
+        result += next;
+        result += SEP;
+      }
+    }
+    // remove last sep
+    if (!result.isEmpty())
+    {
+      result = result.substring(0, result.length() - 1);
+    }
+    return result;
+  }
+
+  @Override
+  public Vector<String> getLinksForMenu()
+  {
+    Vector<String> fullLinks = new Vector<String>();
+    for (UrlProviderI p : providers)
+    {
+      List<String> links = p.getLinksForMenu();
+      if (links != null)
+      {
+        // will obliterate links with same keys from different providers
+        // must have checks in place to prevent user from duplicating ids
+        fullLinks.addAll(links);
+      }
+    }
+    return fullLinks;
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    ArrayList<UrlLinkDisplay> displayLinks = new ArrayList<UrlLinkDisplay>();
+    for (UrlProviderI p : providers)
+    {
+      displayLinks.addAll(p.getLinksForTable());
+    }
+    return displayLinks;
+  }
+
+  @Override
+  public void setUrlData(List<UrlLinkDisplay> links)
+  {
+    for (UrlProviderI p : providers)
+    {
+      p.setUrlData(links);
+    }
+  }
+
+  @Override
+  public String getPrimaryUrl(String seqid)
+  {
+    String link = null;
+    for (UrlProviderI p : providers)
+    {
+      if (p.getPrimaryUrl(seqid) == null)
+      {
+        continue;
+      }
+      else
+      {
+        link = p.getPrimaryUrl(seqid);
+        break;
+      }
+    }
+    return link;
+  }
+
+  @Override
+  public String getPrimaryUrlId()
+  {
+    String id = null;
+    for (UrlProviderI p : providers)
+    {
+      if (p.getPrimaryUrlId() == null)
+      {
+        continue;
+      }
+      else
+      {
+        id = p.getPrimaryUrlId();
+        break;
+      }
+    }
+    return id;
+  }
+
+  @Override
+  public String getPrimaryTarget(String seqid)
+  {
+    String target = null;
+    for (UrlProviderI p : providers)
+    {
+      if (p.getPrimaryTarget(seqid) == null)
+      {
+        continue;
+      }
+      else
+      {
+        target = p.getPrimaryTarget(seqid);
+        break;
+      }
+    }
+    return target;
+  }
+  
+  @Override
+  public String choosePrimaryUrl()
+  {
+    // choose a custom url default
+    return customProvider.choosePrimaryUrl();
+  }
+
+  @Override
+  public boolean isUserEntry(String id)
+  {
+    return customProvider.isUserEntry(id);
+  }
+}
diff --git a/src/jalview/urls/UrlProviderImpl.java b/src/jalview/urls/UrlProviderImpl.java
new file mode 100644 (file)
index 0000000..c1a57ca
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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.urls;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.util.UrlLink;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+/**
+ * Leaf node of UrlProvider composite
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public abstract class UrlProviderImpl implements UrlProviderI
+{
+  // minimum length of substitution in url link string
+  protected static final int MIN_SUBST_LENGTH = 4;
+
+  private static final Pattern MIRIAM_PATTERN = Pattern
+          .compile("^MIR:\\d{8}$");
+
+  protected String primaryUrl;
+
+  protected String getPrimaryUrl(String seqid, HashMap<String, UrlLink> urls)
+  {
+    if (seqid.length() < MIN_SUBST_LENGTH)
+    {
+      return null;
+    }
+    else if (primaryUrl == null)
+    {
+      return null;
+    }
+    else if (!urls.containsKey(primaryUrl))
+    {
+      return null;
+    }
+    else
+    {
+      String url = null;
+      UrlLink urlLink = urls.get(primaryUrl);
+      String[] primaryUrls = urlLink.makeUrls(seqid, true);
+      if (primaryUrls == null || primaryUrls[0] == null
+              || primaryUrls[0].length() < MIN_SUBST_LENGTH)
+      {
+        url = null;
+      }
+      else
+      {
+        // just take first URL made from regex
+        url = primaryUrls[1];
+      }
+      return url;
+    }
+  }
+
+  @Override
+  public List<UrlLinkDisplay> getLinksForTable()
+  {
+    return null;
+  }
+
+  protected ArrayList<UrlLinkDisplay> getLinksForTable(
+          HashMap<String, UrlLink> urls, ArrayList<String> selectedUrls,
+          boolean selected)
+  {
+    ArrayList<UrlLinkDisplay> displayLinks = new ArrayList<UrlLinkDisplay>();
+    for (Entry<String, UrlLink> entry : urls.entrySet())
+    {
+      String key = entry.getKey();
+      boolean isPrimary = (key.equals(primaryUrl));
+      boolean isSelected;
+      if (selectedUrls != null)
+      {
+        isSelected = selectedUrls.contains(key);
+      }
+      else
+      {
+        isSelected = selected;
+      }
+      displayLinks.add(new UrlLinkDisplay(key, entry.getValue(),
+              isSelected, isPrimary));
+    }
+    return displayLinks;
+  }
+
+  protected boolean isMiriamId(String id)
+  {
+    return MIRIAM_PATTERN.matcher(id).matches();
+  }
+
+  @Override
+  public boolean isUserEntry(String id)
+  {
+    return !isMiriamId(id);
+  }
+}
+
diff --git a/src/jalview/urls/api/UrlProviderFactoryI.java b/src/jalview/urls/api/UrlProviderFactoryI.java
new file mode 100644 (file)
index 0000000..b96113e
--- /dev/null
@@ -0,0 +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.urls.api;
+
+
+/**
+ * Interface to UrlProvider factories
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public interface UrlProviderFactoryI
+{
+  public UrlProviderI createUrlProvider();
+
+}
diff --git a/src/jalview/urls/api/UrlProviderI.java b/src/jalview/urls/api/UrlProviderI.java
new file mode 100644 (file)
index 0000000..728d9be
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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.urls.api;
+
+import jalview.urls.UrlLinkDisplay;
+
+import java.util.List;
+
+/**
+ * Methods for providing consistent access to up-to-date URLs
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+public interface UrlProviderI
+{
+
+  /**
+   * Get names and urls in the UrlProvider as strings for display
+   * 
+   */
+  List<String> getLinksForMenu();
+
+  /**
+   * Get names and urls as strings for display
+   * 
+   */
+  List<UrlLinkDisplay> getLinksForTable();
+
+  /**
+   * Set names and urls from display settings
+   */
+  void setUrlData(List<UrlLinkDisplay> links);
+
+  /**
+   * Get the link for the primary URL
+   * 
+   * @seqid sequence id for which to build link
+   * @return link for the primary URL
+   */
+  String getPrimaryUrl(String seqid);
+
+  /**
+   * Get the primary URL id
+   * 
+   * @return id for primary URL
+   */
+  String getPrimaryUrlId();
+
+  /**
+   * Get the target of the link for the primary URL
+   * 
+   * @seqid sequence id for which to build link
+   * @return target of link for the primary URL
+   */
+  String getPrimaryTarget(String seqid);
+
+  /**
+   * Set the primary URL: if only one URL can be used, this URL is the one which
+   * should be chosen, e.g. provides the URL to be used on double-click of a
+   * sequence id
+   * 
+   * @param id
+   *          the id of the URL to set as primary
+   * @return true if setting is successful
+   * @throws IllegalArgumentException
+   *           if id does not exist as a url in the UrlProvider
+   */
+  boolean setPrimaryUrl(String id) throws IllegalArgumentException;
+
+  /**
+   * Test if UrlProvider contains a url
+   * 
+   * @param id
+   *          to test for
+   * @return true of UrlProvider contains this id, false otherwise
+   */
+  boolean contains(String id);
+
+  /**
+   * Write out all URLs as a string suitable for serialising
+   * 
+   * @return string representation of available URLs
+   */
+  String writeUrlsAsString(boolean selected);
+
+  /**
+   * Choose the primary URL in the event of the selected primary being
+   * unavailable
+   * 
+   * @return id of chosen primary url
+   */
+  String choosePrimaryUrl();
+
+  /**
+   * Determine if id is for a user-defined URL
+   */
+  boolean isUserEntry(String id);
+}
diff --git a/src/jalview/urls/applet/AppletUrlProviderFactory.java b/src/jalview/urls/applet/AppletUrlProviderFactory.java
new file mode 100644 (file)
index 0000000..fcedaa4
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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.urls.applet;
+
+import jalview.urls.CustomUrlProvider;
+import jalview.urls.UrlProvider;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * UrlProvider factory for applet code
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class AppletUrlProviderFactory implements UrlProviderFactoryI
+{
+  private String provDefaultUrl;
+
+  private Map<String, String> provUrlList;
+
+  public AppletUrlProviderFactory(String defaultUrlString,
+          Map<String, String> urlList)
+  {
+    provDefaultUrl = defaultUrlString;
+    provUrlList = urlList;
+  }
+
+  @Override
+  public UrlProviderI createUrlProvider()
+  {
+    // create all the UrlProviders we need
+    List<UrlProviderI> providers = new ArrayList<UrlProviderI>();
+    UrlProviderI customProvider = new CustomUrlProvider(provUrlList, null);
+    providers.add(customProvider);
+
+    UrlProviderI prov = new UrlProvider(provDefaultUrl, providers);
+    return prov;
+  }
+
+}
diff --git a/src/jalview/urls/desktop/DesktopUrlProviderFactory.java b/src/jalview/urls/desktop/DesktopUrlProviderFactory.java
new file mode 100644 (file)
index 0000000..ce933c2
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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.urls.desktop;
+
+import jalview.urls.CustomUrlProvider;
+import jalview.urls.IdentifiersUrlProvider;
+import jalview.urls.UrlProvider;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UrlProvider factory for desktop code
+ * 
+ * @author $author$
+ * @version $Revision$
+ */
+
+public class DesktopUrlProviderFactory implements UrlProviderFactoryI
+{
+
+  private String provDefaultUrl;
+
+  private String menuUrlList;
+
+  private String nonMenuUrlList;
+
+  public DesktopUrlProviderFactory(String defaultUrlString,
+          String cachedUrlList, String userUrlList)
+  {
+    provDefaultUrl = defaultUrlString;
+    menuUrlList = cachedUrlList;
+    nonMenuUrlList = userUrlList;
+  }
+
+  @Override
+  public UrlProviderI createUrlProvider()
+  {
+    // create all the UrlProviders we need
+    List<UrlProviderI> providers = new ArrayList<UrlProviderI>();
+
+    UrlProviderI idProvider = new IdentifiersUrlProvider(menuUrlList);
+    UrlProviderI customProvider = new CustomUrlProvider(menuUrlList,
+            nonMenuUrlList);
+    providers.add(idProvider);
+    providers.add(customProvider);
+
+    return new UrlProvider(provDefaultUrl, providers);
+  }
+
+}
index 525bfdb..d7829df 100644 (file)
@@ -191,4 +191,167 @@ public class ColorUtils
       return new Color(red, green, blue);
     }
   }
+
+  /**
+   * Parses a string into a Color, where the accepted formats are
+   * <ul>
+   * <li>an AWT colour name e.g. white</li>
+   * <li>a hex colour value (without prefix) e.g. ff0000</li>
+   * <li>an rgb triple e.g. 100,50,150</li>
+   * </ul>
+   * 
+   * @param colour
+   * @return the parsed colour, or null if parsing fails
+   */
+  public static Color parseColourString(String colour)
+  {
+    if (colour == null)
+    {
+      return null;
+    }
+    colour = colour.trim();
+  
+    Color col = null;
+    try
+    {
+      int value = Integer.parseInt(colour, 16);
+      col = new Color(value);
+    } catch (NumberFormatException ex)
+    {
+    }
+  
+    if (col == null)
+    {
+      col = ColorUtils.getAWTColorFromName(colour);
+    }
+  
+    if (col == null)
+    {
+      try
+      {
+        String[] tokens = colour.split(",");
+        if (tokens.length == 3)
+        {
+          int r = Integer.parseInt(tokens[0].trim());
+          int g = Integer.parseInt(tokens[1].trim());
+          int b = Integer.parseInt(tokens[2].trim());
+          col = new Color(r, g, b);
+        }
+      } catch (Exception ex)
+      {
+        // non-numeric token or out of 0-255 range
+      }
+    }
+  
+    return col;
+  }
+
+  /**
+   * Constructs a colour from a text string. The hashcode of the whole string is
+   * scaled to the range 0-135. This is added to RGB values made from the
+   * hashcode of each third of the string, and scaled to the range 20-229.
+   * 
+   * @param name
+   * @return
+   */
+  public static Color createColourFromName(String name)
+  {
+    if (name == null)
+    {
+      return Color.white;
+    }
+    int lsize = name.length();
+    int start = 0;
+    int end = lsize / 3;
+  
+    int rgbOffset = Math.abs(name.hashCode() % 10) * 15; // 0-135
+  
+    /*
+     * red: first third
+     */
+    int r = Math.abs(name.substring(start, end).hashCode() + rgbOffset) % 210 + 20;
+    start = end;
+    end += lsize / 3;
+    if (end > lsize)
+    {
+      end = lsize;
+    }
+  
+    /*
+     * green: second third
+     */
+    int g = Math.abs(name.substring(start, end).hashCode() + rgbOffset) % 210 + 20;
+  
+    /*
+     * blue: third third
+     */
+    int b = Math.abs(name.substring(end).hashCode() + rgbOffset) % 210 + 20;
+  
+    Color color = new Color(r, g, b);
+  
+    return color;
+  }
+
+  /**
+   * Returns the Color constant for a given colour name e.g. "pink", or null if
+   * the name is not recognised
+   * 
+   * @param name
+   * @return
+   */
+  public static Color getAWTColorFromName(String name)
+  {
+    if (name == null)
+    {
+      return null;
+    }
+    Color col = null;
+    name = name.toLowerCase();
+  
+    // or make a static map; or use reflection on the field name
+    switch (name)
+    {
+    case "black":
+      col = Color.black;
+      break;
+    case "blue":
+      col = Color.blue;
+      break;
+    case "cyan":
+      col = Color.cyan;
+      break;
+    case "darkgray":
+      col = Color.darkGray;
+      break;
+    case "gray":
+      col = Color.gray;
+      break;
+    case "green":
+      col = Color.green;
+      break;
+    case "lightgray":
+      col = Color.lightGray;
+      break;
+    case "magenta":
+      col = Color.magenta;
+      break;
+    case "orange":
+      col = Color.orange;
+      break;
+    case "pink":
+      col = Color.pink;
+      break;
+    case "red":
+      col = Color.red;
+      break;
+    case "white":
+      col = Color.white;
+      break;
+    case "yellow":
+      col = Color.yellow;
+      break;
+    }
+  
+    return col;
+  }
 }
index 1326647..22e1ab7 100644 (file)
@@ -34,11 +34,11 @@ public class Comparison
 
   private static final int TO_UPPER_CASE = 'a' - 'A';
 
-  private static final char GAP_SPACE = ' ';
+  public static final char GAP_SPACE = ' ';
 
-  private static final char GAP_DOT = '.';
+  public static final char GAP_DOT = '.';
 
-  private static final char GAP_DASH = '-';
+  public static final char GAP_DASH = '-';
 
   public static final String GapChars = new String(new char[] { GAP_SPACE,
       GAP_DOT, GAP_DASH });
@@ -135,7 +135,9 @@ public class Comparison
    * @param s2
    *          SequenceI
    * @return float
+   * @deprecated use PIDModel.computePID()
    */
+  @Deprecated
   public final static float PID(String seq1, String seq2)
   {
     return PID(seq1, seq2, 0, seq1.length());
@@ -144,6 +146,10 @@ public class Comparison
   static final int caseShift = 'a' - 'A';
 
   // Another pid with region specification
+  /**
+   * @deprecated use PIDModel.computePID()
+   */
+  @Deprecated
   public final static float PID(String seq1, String seq2, int start, int end)
   {
     return PID(seq1, seq2, start, end, true, false);
@@ -165,7 +171,9 @@ public class Comparison
    * @param ungappedOnly
    *          - if true - only count PID over ungapped columns
    * @return
+   * @deprecated use PIDModel.computePID()
    */
+  @Deprecated
   public final static float PID(String seq1, String seq2, int start,
           int end, boolean wcGaps, boolean ungappedOnly)
   {
index 8f8af54..fa2533f 100755 (executable)
@@ -20,7 +20,6 @@
  */
 package jalview.util;
 
-import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.gui.EPSOptions;
 import jalview.gui.IProgressIndicator;
@@ -310,8 +309,7 @@ public class ImageMaker
     {
       return null;
     }
-    return new JalviewFileChooser(Cache.getProperty("LAST_DIRECTORY"),
-            PNG_EXTENSION, PNG_DESCRIPTION);
+    return new JalviewFileChooser(PNG_EXTENSION, PNG_DESCRIPTION);
   }
 
   static JalviewFileChooser getEPSChooser()
@@ -320,8 +318,7 @@ public class ImageMaker
     {
       return null;
     }
-    return new JalviewFileChooser(Cache.getProperty("LAST_DIRECTORY"),
-            EPS_EXTENSION, EPS_DESCRIPTION);
+    return new JalviewFileChooser(EPS_EXTENSION, EPS_DESCRIPTION);
   }
 
   private void setProgressMessage(String message)
@@ -338,7 +335,6 @@ public class ImageMaker
     {
       return null;
     }
-    return new JalviewFileChooser(Cache.getProperty("LAST_DIRECTORY"),
-            SVG_EXTENSION, SVG_DESCRIPTION);
+    return new JalviewFileChooser(SVG_EXTENSION, SVG_DESCRIPTION);
   }
 }
diff --git a/src/jalview/util/IntRangeComparator.java b/src/jalview/util/IntRangeComparator.java
new file mode 100644 (file)
index 0000000..cb32a0e
--- /dev/null
@@ -0,0 +1,31 @@
+package jalview.util;
+
+import java.util.Comparator;
+
+/**
+ * A comparator to order [from, to] ranges into ascending or descending order of
+ * their start position
+ */
+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;
+
+  IntRangeComparator(boolean forward)
+  {
+    forwards = forward;
+  }
+
+  @Override
+  public int compare(int[] o1, int[] o2)
+  {
+    int compared = Integer.compare(o1[0], o2[0]);
+    return forwards ? compared : -compared;
+  }
+
+}
\ No newline at end of file
index bce540f..7f109ec 100644 (file)
@@ -34,7 +34,7 @@ import java.util.LinkedHashMap;
  */
 public class LinkedIdentityHashSet<E> extends AbstractSet<E>
 {
-  LinkedHashMap<IdentityWrapper, IdentityWrapper> set = new LinkedHashMap<IdentityWrapper, IdentityWrapper>();
+  LinkedHashMap<IdentityWrapper, IdentityWrapper> set = new LinkedHashMap<>();
 
   static class IdentityWrapper
   {
@@ -51,7 +51,7 @@ public class LinkedIdentityHashSet<E> extends AbstractSet<E>
     @Override
     public boolean equals(Object obj)
     {
-      return this.obj == obj;
+      return this.obj == ((IdentityWrapper) obj).obj;
     }
 
     @Override
index f35339c..926ccc7 100644 (file)
@@ -31,6 +31,7 @@ import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -320,7 +321,7 @@ public final class MappingUtils
      * Copy group name, colours etc, but not sequences or sequence colour scheme
      */
     SequenceGroup mappedGroup = new SequenceGroup(sg);
-    mappedGroup.cs = mapTo.getGlobalColourScheme();
+    mappedGroup.setColourScheme(mapTo.getGlobalColourScheme());
     mappedGroup.clear();
 
     int minStartCol = -1;
@@ -508,18 +509,20 @@ public final class MappingUtils
    * @param mapTo
    * @return
    */
-  public static ColumnSelection mapColumnSelection(ColumnSelection colsel,
-          AlignViewportI mapFrom, AlignViewportI mapTo)
+  public static void mapColumnSelection(ColumnSelection colsel,
+          HiddenColumns hiddencols, AlignViewportI mapFrom,
+          AlignViewportI mapTo, ColumnSelection newColSel,
+          HiddenColumns newHidden)
   {
     boolean targetIsNucleotide = mapTo.isNucleotide();
     AlignViewportI protein = targetIsNucleotide ? mapFrom : mapTo;
     List<AlignedCodonFrame> codonFrames = protein.getAlignment()
             .getCodonFrames();
-    ColumnSelection mappedColumns = new ColumnSelection();
+    // ColumnSelection mappedColumns = new ColumnSelection();
 
     if (colsel == null)
     {
-      return mappedColumns;
+      return; // mappedColumns;
     }
 
     char fromGapChar = mapFrom.getAlignment().getGapCharacter();
@@ -533,16 +536,16 @@ public final class MappingUtils
 
     for (Integer sel : colsel.getSelected())
     {
-      mapColumn(sel.intValue(), codonFrames, mappedColumns, fromSequences,
+      mapColumn(sel.intValue(), codonFrames, newColSel, fromSequences,
               toSequences, fromGapChar);
     }
 
-    for (int[] hidden : colsel.getHiddenColumns())
+    for (int[] hidden : hiddencols.getHiddenRegions())
     {
-      mapHiddenColumns(hidden, codonFrames, mappedColumns, fromSequences,
+      mapHiddenColumns(hidden, codonFrames, newHidden, fromSequences,
               toSequences, fromGapChar);
     }
-    return mappedColumns;
+    return; // mappedColumns;
   }
 
   /**
@@ -557,7 +560,7 @@ public final class MappingUtils
    * @param fromGapChar
    */
   protected static void mapHiddenColumns(int[] hidden,
-          List<AlignedCodonFrame> mappings, ColumnSelection mappedColumns,
+          List<AlignedCodonFrame> mappings, HiddenColumns mappedColumns,
           List<SequenceI> fromSequences, List<SequenceI> toSequences,
           char fromGapChar)
   {
index f1b83b8..139ee19 100644 (file)
@@ -97,12 +97,12 @@ public class ParseHtmlBodyAndLinks
   public ParseHtmlBodyAndLinks(String description, boolean removeHTML,
           String newline)
   {
-    StringBuilder sb = new StringBuilder(description.length());
     if (description == null || description.length() == 0)
     {
       htmlContent = false;
       return;
     }
+    StringBuilder sb = new StringBuilder(description.length());
     if (description.toUpperCase().indexOf("<HTML>") == -1)
     {
       htmlContent = false;
diff --git a/src/jalview/util/SetUtils.java b/src/jalview/util/SetUtils.java
new file mode 100644 (file)
index 0000000..381d9f6
--- /dev/null
@@ -0,0 +1,43 @@
+package jalview.util;
+
+import java.util.Set;
+
+public class SetUtils
+{
+  /**
+   * Returns the count of things that are in one or other of two sets but not in
+   * both. The sets are not modified.
+   * 
+   * @param set1
+   * @param set2
+   * @return
+   */
+  public static int countDisjunction(Set<? extends Object> set1,
+          Set<? extends Object> set2)
+  {
+    if (set1 == null)
+    {
+      return set2 == null ? 0 : set2.size();
+    }
+    if (set2 == null)
+    {
+      return set1.size();
+    }
+
+    int size1 = set1.size();
+    int size2 = set2.size();
+    Set<? extends Object> smallerSet = size1 < size2 ? set1 : set2;
+    Set<? extends Object> largerSet = (smallerSet == set1 ? set2 : set1);
+    int inCommon = 0;
+    for (Object k : smallerSet)
+    {
+      if (largerSet.contains(k))
+      {
+        inCommon++;
+      }
+    }
+
+    int notInCommon = (size1 - inCommon) + (size2 - inCommon);
+    return notInCommon;
+  }
+}
index 1910bff..3347cc7 100644 (file)
@@ -37,14 +37,25 @@ public class UrlConstants
   public static final String SEQUENCE_ID = "SEQUENCE_ID";
 
   /*
-   * Default sequence URL link string for EMBL-EBI search
+   * Separator character used in Url links
+   */
+  public static final String SEP = "|";
+
+  /*
+   * Delimiter character used in Url links
    */
-  public static final String EMBLEBI_STRING = "EMBL-EBI Search|http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
+  public static final String DELIM = "$";
 
   /*
-   * Default sequence URL link string for SRS 
+   * Default sequence URL link label for EMBL-EBI search
+   */
+  public static final String DEFAULT_LABEL = "EMBL-EBI Search";
+
+  /*
+   * Default sequence URL link string for EMBL-EBI search
    */
-  public static final String SRS_STRING = "SRS|http://srs.ebi.ac.uk/srsbin/cgi-bin/wgetz?-newId+(([uniprot-all:$SEQUENCE_ID$]))+-view+SwissEntry";
+  public static final String DEFAULT_STRING = DEFAULT_LABEL
+          + "|http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
 
   /*
    * not instantiable
index 3ee6432..0529c73 100644 (file)
@@ -21,6 +21,8 @@
 package jalview.util;
 
 import static jalview.util.UrlConstants.DB_ACCESSION;
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
 import static jalview.util.UrlConstants.SEQUENCE_ID;
 
 import jalview.datamodel.DBRefEntry;
@@ -43,10 +45,9 @@ public class UrlLink
    * documentation todo.
    */
 
-  // Internal constants
-  private static final String SEP = "|";
+  private static final String EQUALS = "=";
 
-  private static final String DELIM = "$";
+  private static final String SPACE = " ";
 
   private String urlSuffix;
 
@@ -56,6 +57,8 @@ public class UrlLink
 
   private String label;
 
+  private String dbname;
+
   private String regexReplace;
 
   private boolean dynamic = false;
@@ -81,23 +84,38 @@ public class UrlLink
       dynamic = true;
       usesDBaccession = true;
 
-      sep = parseTargetAndLabel(sep, psqid, link);
+      sep = parseLabel(sep, psqid, link);
 
-      parseUrl(link, DB_ACCESSION, psqid, sep);
+      int endOfRegex = parseUrl(link, DB_ACCESSION, psqid, sep);
+      parseTarget(link, sep, endOfRegex);
     }
     else if (nsqid > -1)
     {
       dynamic = true;
-      sep = parseTargetAndLabel(sep, nsqid, link);
+      sep = parseLabel(sep, nsqid, link);
+
+      int endOfRegex = parseUrl(link, SEQUENCE_ID, nsqid, sep);
 
-      parseUrl(link, SEQUENCE_ID, nsqid, sep);
+      parseTarget(link, sep, endOfRegex);
     }
     else
     {
-      target = link.substring(0, sep);
-      sep = link.lastIndexOf(SEP);
-      label = link.substring(0, sep);
-      urlPrefix = link.substring(sep + 1).trim();
+      label = link.substring(0, sep).trim();
+
+      // if there's a third element in the url link string
+      // it is the target name, otherwise target=label
+      int lastsep = link.lastIndexOf(SEP);
+      if (lastsep != sep)
+      {
+        urlPrefix = link.substring(sep + 1, lastsep).trim();
+        target = link.substring(lastsep + 1).trim();
+      }
+      else
+      {
+        urlPrefix = link.substring(sep + 1).trim();
+        target = label;
+      }
+
       regexReplace = null; // implies we trim any prefix if necessary //
       urlSuffix = null;
     }
@@ -107,9 +125,24 @@ public class UrlLink
   }
 
   /**
+   * Alternative constructor for separate name, link and description
+   * 
+   * @param name
+   *          The string used to match the link to a DB reference id
+   * @param url
+   *          The url to link to
+   * @param desc
+   *          The description of the associated target DB
+   */
+  public UrlLink(String name, String url, String desc)
+  {
+    this(name + SEP + url + SEP + desc);
+  }
+
+  /**
    * @return the url_suffix
    */
-  public String getUrl_suffix()
+  public String getUrlSuffix()
   {
     return urlSuffix;
   }
@@ -117,7 +150,7 @@ public class UrlLink
   /**
    * @return the url_prefix
    */
-  public String getUrl_prefix()
+  public String getUrlPrefix()
   {
     return urlPrefix;
   }
@@ -138,6 +171,16 @@ public class UrlLink
     return label;
   }
 
+  public String getUrlWithToken()
+  {
+    String var = (usesDBaccession ? DB_ACCESSION : SEQUENCE_ID);
+
+    return urlPrefix
+            + (dynamic ? (DELIM + var + ((regexReplace != null) ? EQUALS
+                    + regexReplace + EQUALS + DELIM : DELIM)) : "")
+            + ((urlSuffix == null) ? "" : urlSuffix);
+  }
+
   /**
    * @return the regexReplace
    */
@@ -194,6 +237,16 @@ public class UrlLink
   }
 
   /**
+   * Set the target
+   * 
+   * @param desc
+   */
+  public void setTarget(String desc)
+  {
+    target = desc;
+  }
+
+  /**
    * return one or more URL strings by applying regex to the given idstring
    * 
    * @param idstring
@@ -234,7 +287,7 @@ public class UrlLink
                       + rg.stringMatched(s) + "'");
             }
             // try to collate subgroup matches
-            Vector subs = new Vector();
+            Vector<String> subs = new Vector<String>();
             // have to loop through submatches, collating them at top level
             // match
             int s = 0; // 1;
@@ -278,7 +331,7 @@ public class UrlLink
             String[] res = new String[subs.size()];
             for (int r = 0, rs = subs.size(); r < rs; r++)
             {
-              res[r] = (String) subs.elementAt(r);
+              res[r] = subs.elementAt(r);
             }
             subs.removeAllElements();
             return res;
@@ -307,17 +360,19 @@ public class UrlLink
   @Override
   public String toString()
   {
-    String var = (usesDBaccession ? DB_ACCESSION : SEQUENCE_ID);
+    return label + SEP + getUrlWithToken();
+  }
 
-    return label
-            + SEP
-            + urlPrefix
-            + (dynamic ? (DELIM + var + ((regexReplace != null) ? "="
-                    + regexReplace + "=" + DELIM : DELIM)) : "")
-            + ((urlSuffix == null) ? "" : urlSuffix);
+  /**
+   * @return delimited string containing label, url and target
+   */
+  public String toStringWithTarget()
+  {
+    return label + SEP + getUrlWithToken() + SEP + target;
   }
 
   /**
+   * Parse the label from the link string
    * 
    * @param firstSep
    *          Location of first occurrence of separator in link string
@@ -327,7 +382,7 @@ public class UrlLink
    *          Link string containing database name and url
    * @return Position of last separator symbol prior to any regex symbols
    */
-  protected int parseTargetAndLabel(int firstSep, int psqid, String link)
+  protected int parseLabel(int firstSep, int psqid, String link)
   {
     int p = firstSep;
     int sep = firstSep;
@@ -339,21 +394,44 @@ public class UrlLink
     // Assuming that the URL itself does not contain any SEP symbols
     // sep now contains last pipe symbol position prior to any regex symbols
     label = link.substring(0, sep);
-    if (label.indexOf(SEP) > -1)
-    {
-      // SEP terminated database name / www target at start of Label
-      target = label.substring(0, label.indexOf(SEP));
-    }
-    else if (label.indexOf(" ") > 2)
+
+    return sep;
+  }
+
+  /**
+   * Parse the target from the link string
+   * 
+   * @param link
+   *          Link string containing database name and url
+   * @param sep
+   *          Location of first separator symbol
+   * @param endOfRegex
+   *          Location of end of any regular expression in link string
+   */
+  protected void parseTarget(String link, int sep, int endOfRegex)
+  {
+    int lastsep = link.lastIndexOf(SEP);
+
+    if ((lastsep != sep) && (lastsep > endOfRegex))
     {
-      // space separated Label - matches database name
-      target = label.substring(0, label.indexOf(" "));
+      // final element in link string is the target
+      target = link.substring(lastsep + 1).trim();
     }
     else
     {
       target = label;
     }
-    return sep;
+
+    if (target.indexOf(SEP) > -1)
+    {
+      // SEP terminated database name / www target at start of Label
+      target = target.substring(0, target.indexOf(SEP));
+    }
+    else if (target.indexOf(SPACE) > 2)
+    {
+      // space separated label - first word matches database name
+      target = target.substring(0, target.indexOf(SPACE));
+    }
   }
 
   /**
@@ -367,8 +445,9 @@ public class UrlLink
    *          Position of id or name in link string
    * @param sep
    *          Position of separator in link string
+   * @return Location of end of any regex in link string
    */
-  protected void parseUrl(String link, String varName, int sqidPos, int sep)
+  protected int parseUrl(String link, String varName, int sqidPos, int sep)
   {
     urlPrefix = link.substring(sep + 1, sqidPos).trim();
 
@@ -411,7 +490,14 @@ public class UrlLink
       // verify format is really correct.
       if (link.indexOf(DELIM + varName + DELIM) == sqidPos)
       {
-        urlSuffix = link.substring(sqidPos + startLength - 1);
+        int lastsep = link.lastIndexOf(SEP);
+        if (lastsep < sqidPos + startLength - 1)
+        {
+          // the last SEP character was before the regex, ignore
+          lastsep = link.length();
+        }
+        urlSuffix = link.substring(sqidPos + startLength - 1, lastsep)
+                .trim();
         regexReplace = null;
       }
       else
@@ -420,6 +506,8 @@ public class UrlLink
                 + link;
       }
     }
+
+    return p;
   }
 
   /**
@@ -453,11 +541,11 @@ public class UrlLink
    */
   protected void createStaticLink(Map<String, List<String>> linkset)
   {
-    if (!linkset.containsKey(label + SEP + getUrl_prefix()))
+    if (!linkset.containsKey(label + SEP + getUrlPrefix()))
     {
       // Add a non-dynamic link
-      linkset.put(label + SEP + getUrl_prefix(),
-              Arrays.asList(target, label, null, getUrl_prefix()));
+      linkset.put(label + SEP + getUrlPrefix(),
+              Arrays.asList(target, label, null, getUrlPrefix()));
     }
   }
 
@@ -539,82 +627,4 @@ public class UrlLink
       }
     }
   }
-
-  private static void testUrls(UrlLink ul, String idstring, String[] urls)
-  {
-
-    if (urls == null)
-    {
-      System.out.println("Created NO urls.");
-    }
-    else
-    {
-      System.out.println("Created " + (urls.length / 2) + " Urls.");
-      for (int uls = 0; uls < urls.length; uls += 2)
-      {
-        System.out.println("URL Replacement text : " + urls[uls]
-                + " : URL : " + urls[uls + 1]);
-      }
-    }
-  }
-
-  public static void main(String argv[])
-  {
-    String[] links = new String[] {
-    /*
-     * "AlinkT|Target|http://foo.foo.soo/",
-     * "myUrl1|http://$SEQUENCE_ID=/[0-9]+/=$.someserver.org/foo",
-     * "myUrl2|http://$SEQUENCE_ID=/(([0-9]+).+([A-Za-z]+))/=$.someserver.org/foo"
-     * ,
-     * "myUrl3|http://$SEQUENCE_ID=/([0-9]+).+([A-Za-z]+)/=$.someserver.org/foo"
-     * , "myUrl4|target|http://$SEQUENCE_ID$.someserver.org/foo|too",
-     * "PF1|http://us.expasy.org/cgi-bin/niceprot.pl?$SEQUENCE_ID=/(?:PFAM:)?(.+)/=$"
-     * ,
-     * "PF2|http://us.expasy.org/cgi-bin/niceprot.pl?$SEQUENCE_ID=/(PFAM:)?(.+)/=$"
-     * ,
-     * "PF3|http://us.expasy.org/cgi-bin/niceprot.pl?$SEQUENCE_ID=/PFAM:(.+)/=$"
-     * , "NOTFER|http://notfer.org/$SEQUENCE_ID=/(?<!\\s)(.+)/=$",
-     */
-    "NESTED|http://nested/$" + DB_ACCESSION
-            + "=/^(?:Label:)?(?:(?:gi\\|(\\d+))|([^:]+))/=$/nested" };
-    String[] idstrings = new String[] {
-    /*
-     * //"LGUL_human", //"QWIQW_123123", "uniprot|why_do+_12313_foo",
-     * //"123123312", "123123 ABCDE foo", "PFAM:PF23943",
-     */
-    "Label:gi|9234|pdb|102L|A" };
-    // TODO: test the setLabel method.
-    for (int i = 0; i < links.length; i++)
-    {
-      UrlLink ul = new UrlLink(links[i]);
-      if (ul.isValid())
-      {
-        System.out.println("\n\n\n");
-        System.out.println("Link " + i + " " + links[i] + " : "
-                + ul.toString());
-        System.out.println(" pref : "
-                + ul.getUrl_prefix()
-                + "\n suf : "
-                + ul.getUrl_suffix()
-                + "\n : "
-                + ((ul.getRegexReplace() != null) ? ul.getRegexReplace()
-                        : ""));
-        for (int ids = 0; ids < idstrings.length; ids++)
-        {
-          System.out.println("ID String : " + idstrings[ids]
-                  + "\nWithout onlyIfMatches:");
-          String[] urls = ul.makeUrls(idstrings[ids], false);
-          testUrls(ul, idstrings[ids], urls);
-          System.out.println("With onlyIfMatches set.");
-          urls = ul.makeUrls(idstrings[ids], true);
-          testUrls(ul, idstrings[ids], urls);
-        }
-      }
-      else
-      {
-        System.err.println("Invalid URLLink : " + links[i] + " : "
-                + ul.getInvalidMessage());
-      }
-    }
-  }
 }
index 0cca5b4..60cee46 100644 (file)
  */
 package jalview.viewmodel;
 
-import java.awt.Color;
-import java.beans.PropertyChangeSupport;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.Conservation;
 import jalview.api.AlignCalcManagerI;
@@ -46,6 +35,7 @@ import jalview.datamodel.AlignmentView;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.CigarArray;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.ProfilesI;
 import jalview.datamodel.SearchResultsI;
@@ -53,21 +43,33 @@ import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.Blosum62ColourScheme;
+import jalview.renderer.ResidueShader;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemes.ColourSchemeI;
-import jalview.schemes.PIDColourScheme;
 import jalview.structure.CommandListener;
 import jalview.structure.StructureSelectionManager;
 import jalview.structure.VamsasSource;
 import jalview.util.Comparison;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
+import jalview.util.MessageManager;
 import jalview.viewmodel.styles.ViewStyle;
 import jalview.workers.AlignCalcManager;
 import jalview.workers.ComplementConsensusThread;
 import jalview.workers.ConsensusThread;
 import jalview.workers.StrucConsensusThread;
 
+import java.awt.Color;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
 /**
  * base class holding visualization and analysis attributes and common logic for
  * an active alignment view displayed in the GUI
@@ -78,6 +80,8 @@ import jalview.workers.StrucConsensusThread;
 public abstract class AlignmentViewport implements AlignViewportI,
         CommandListener, VamsasSource
 {
+  final protected ViewportRanges ranges;
+
   protected ViewStyleI viewStyle = new ViewStyle();
 
   /**
@@ -88,9 +92,20 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
   FeaturesDisplayedI featuresDisplayed = null;
 
-  protected Deque<CommandI> historyList = new ArrayDeque<CommandI>();
+  protected Deque<CommandI> historyList = new ArrayDeque<>();
+
+  protected Deque<CommandI> redoList = new ArrayDeque<>();
 
-  protected Deque<CommandI> redoList = new ArrayDeque<CommandI>();
+  /**
+   * alignment displayed in the viewport. Please use get/setter
+   */
+  protected AlignmentI alignment;
+
+  public AlignmentViewport(AlignmentI al)
+  {
+    setAlignment(al);
+    ranges = new ViewportRanges(al);
+  }
 
   /**
    * @param name
@@ -552,10 +567,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
     viewStyle.setSeqNameItalics(default1);
   }
 
-  /**
-   * alignment displayed in the viewport. Please use get/setter
-   */
-  protected AlignmentI alignment;
+
 
   @Override
   public AlignmentI getAlignment()
@@ -597,7 +609,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
   protected boolean ignoreGapsInConsensusCalculation = false;
 
-  protected ColourSchemeI globalColourScheme = null;
+  protected ResidueShaderI residueShading = new ResidueShader();
 
   @Override
   public void setGlobalColourScheme(ColourSchemeI cs)
@@ -605,74 +617,51 @@ public abstract class AlignmentViewport implements AlignViewportI,
     // TODO: logic refactored from AlignFrame changeColour -
     // TODO: autorecalc stuff should be changed to rely on the worker system
     // check to see if we should implement a changeColour(cs) method rather than
-    // put th logic in here
+    // put the logic in here
     // - means that caller decides if they want to just modify state and defer
     // calculation till later or to do all calculations in thread.
     // via changecolour
-    globalColourScheme = cs;
-    boolean recalc = false;
+
+    /*
+     * only instantiate alignment colouring once, thereafter update it;
+     * this means that any conservation or PID threshold settings
+     * persist when the alignment colour scheme is changed
+     */
+    if (residueShading == null)
+    {
+      residueShading = new ResidueShader(viewStyle);
+    }
+    residueShading.setColourScheme(cs);
+
+    // TODO: do threshold and increment belong in ViewStyle or ResidueShader?
+    // ...problem: groups need these, but do not currently have a ViewStyle
+
     if (cs != null)
     {
-      recalc = getConservationSelected();
-      if (getAbovePIDThreshold() || cs instanceof PIDColourScheme
-              || cs instanceof Blosum62ColourScheme)
-      {
-        recalc = true;
-        cs.setThreshold(viewStyle.getThreshold(),
-                ignoreGapsInConsensusCalculation);
-      }
-      else
-      {
-        cs.setThreshold(0, ignoreGapsInConsensusCalculation);
-      }
-      if (recalc)
+      if (getConservationSelected())
       {
-        cs.setConsensus(hconsensus);
-        cs.setConservation(hconservation);
+        residueShading.setConservation(hconservation);
       }
-      cs.setConservationApplied(getConservationSelected());
-      cs.alignmentChanged(alignment, hiddenRepSequences);
+      residueShading.alignmentChanged(alignment, hiddenRepSequences);
     }
+
+    /*
+     * if 'apply colour to all groups' is selected... do so
+     * (but don't transfer any colour threshold settings to groups)
+     */
     if (getColourAppliesToAllGroups())
     {
       for (SequenceGroup sg : getAlignment().getGroups())
       {
-        if (cs == null)
-        {
-          sg.cs = null;
-          continue;
-        }
-        sg.cs = cs.applyTo(sg, getHiddenRepSequences());
-        sg.setConsPercGaps(ConsPercGaps);
-        if (getAbovePIDThreshold() || cs instanceof PIDColourScheme
-                || cs instanceof Blosum62ColourScheme)
-        {
-          sg.cs.setThreshold(viewStyle.getThreshold(),
-                  isIgnoreGapsConsensus());
-          recalc = true;
-        }
-        else
-        {
-          sg.cs.setThreshold(0, isIgnoreGapsConsensus());
-        }
-
-        if (getConservationSelected())
-        {
-          sg.cs.setConservationApplied(true);
-          recalc = true;
-        }
-        else
+        /*
+         * retain any colour thresholds per group while
+         * changing choice of colour scheme (JAL-2386)
+         */
+        sg.setColourScheme(cs);
+        if (cs != null)
         {
-          sg.cs.setConservation(null);
-          // sg.cs.setThreshold(0, getIgnoreGapsConsensus());
-        }
-        if (recalc)
-        {
-          sg.recalcConservation();
-        }
-        else
-        {
-          sg.cs.alignmentChanged(sg, hiddenRepSequences);
+          sg.getGroupColourScheme()
+                  .alignmentChanged(sg, hiddenRepSequences);
         }
       }
     }
@@ -681,7 +670,14 @@ public abstract class AlignmentViewport implements AlignViewportI,
   @Override
   public ColourSchemeI getGlobalColourScheme()
   {
-    return globalColourScheme;
+    return residueShading == null ? null : residueShading
+            .getColourScheme();
+  }
+
+  @Override
+  public ResidueShaderI getResidueShading()
+  {
+    return residueShading;
   }
 
   protected AlignmentAnnotation consensus;
@@ -837,7 +833,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public void updateConsensus(final AlignmentViewPanel ap)
   {
     // see note in mantis : issue number 8585
-    if ((consensus == null || gapcounts == null) || !autoCalculateConsensus)
+    if (consensus == null || !autoCalculateConsensus)
     {
       return;
     }
@@ -949,7 +945,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
     hconsensus = null;
     hcomplementConsensus = null;
     // colour scheme may hold reference to consensus
-    globalColourScheme = null;
+    residueShading = null;
     // TODO remove listeners from changeSupport?
     changeSupport = null;
     setAlignment(null);
@@ -1097,7 +1093,8 @@ public abstract class AlignmentViewport implements AlignViewportI,
   }
 
   /**
-   * Set the selection group for this window.
+   * Set the selection group for this window. Also sets the current alignment as
+   * the context for the group, if it does not already have one.
    * 
    * @param sg
    *          - group holding references to sequences in this alignment view
@@ -1107,11 +1104,15 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public void setSelectionGroup(SequenceGroup sg)
   {
     selectionGroup = sg;
+    if (sg != null && sg.getContext() == null)
+    {
+      sg.setContext(alignment);
+    }
   }
 
-  public void setHiddenColumns(ColumnSelection colsel)
+  public void setHiddenColumns(HiddenColumns hidden)
   {
-    this.colSel = colsel;
+    this.alignment.setHiddenColumns(hidden);
   }
 
   @Override
@@ -1158,7 +1159,8 @@ public abstract class AlignmentViewport implements AlignViewportI,
   @Override
   public boolean hasHiddenColumns()
   {
-    return colSel != null && colSel.hasHiddenColumns();
+    return colSel != null
+            && alignment.getHiddenColumns().hasHiddenColumns();
   }
 
   public void updateHiddenColumns()
@@ -1219,9 +1221,9 @@ public abstract class AlignmentViewport implements AlignViewportI,
     if (ap != null)
     {
       updateConsensus(ap);
-      if (globalColourScheme != null)
+      if (residueShading != null)
       {
-        globalColourScheme.setThreshold(globalColourScheme.getThreshold(),
+        residueShading.setThreshold(residueShading.getThreshold(),
                 ignoreGapsInConsensusCalculation);
       }
     }
@@ -1293,7 +1295,9 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
   protected boolean showConsensus = true;
 
-  private Map<SequenceI, Color> sequenceColours = new HashMap<SequenceI, Color>();
+  protected boolean showOccupancy = true;
+
+  private Map<SequenceI, Color> sequenceColours = new HashMap<>();
 
   protected SequenceAnnotationOrder sortAnnotationsBy = null;
 
@@ -1304,15 +1308,6 @@ public abstract class AlignmentViewport implements AlignViewportI,
    */
   private boolean followHighlight = true;
 
-  // TODO private with getters and setters?
-  public int startRes;
-
-  public int endRes;
-
-  public int startSeq;
-
-  public int endSeq;
-
   /**
    * Property change listener for changes in alignment
    * 
@@ -1362,7 +1357,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
       return;
     }
 
-    colSel.hideSelectedColumns();
+    colSel.hideSelectedColumns(alignment);
     setSelectionGroup(null);
     isColSelChanged(true);
   }
@@ -1371,24 +1366,24 @@ public abstract class AlignmentViewport implements AlignViewportI,
   {
     if (start == end)
     {
-      colSel.hideColumns(start);
+      colSel.hideSelectedColumns(start, alignment.getHiddenColumns());
     }
     else
     {
-      colSel.hideColumns(start, end);
+      alignment.getHiddenColumns().hideColumns(start, end);
     }
     isColSelChanged(true);
   }
 
   public void showColumn(int col)
   {
-    colSel.revealHiddenColumns(col);
+    alignment.getHiddenColumns().revealHiddenColumns(col, colSel);
     isColSelChanged(true);
   }
 
   public void showAllHiddenColumns()
   {
-    colSel.revealAllHiddenColumns();
+    alignment.getHiddenColumns().revealAllHiddenColumns(colSel);
     isColSelChanged(true);
   }
 
@@ -1532,7 +1527,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
     if (hiddenRepSequences == null)
     {
-      hiddenRepSequences = new Hashtable<SequenceI, SequenceCollectionI>();
+      hiddenRepSequences = new Hashtable<>();
     }
 
     hiddenRepSequences.put(repSequence, sg);
@@ -1610,7 +1605,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   @Override
   public void invertColumnSelection()
   {
-    colSel.invertColumnSelection(0, alignment.getWidth());
+    colSel.invertColumnSelection(0, alignment.getWidth(), alignment);
   }
 
   @Override
@@ -1658,7 +1653,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   @Override
   public CigarArray getViewAsCigars(boolean selectedRegionOnly)
   {
-    return new CigarArray(alignment, colSel,
+    return new CigarArray(alignment, alignment.getHiddenColumns(),
             (selectedRegionOnly ? selectionGroup : null));
   }
 
@@ -1673,8 +1668,10 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public jalview.datamodel.AlignmentView getAlignmentView(
           boolean selectedOnly, boolean markGroups)
   {
-    return new AlignmentView(alignment, colSel, selectionGroup,
-            colSel != null && colSel.hasHiddenColumns(), selectedOnly,
+    return new AlignmentView(alignment, alignment.getHiddenColumns(),
+            selectionGroup, alignment.getHiddenColumns() != null
+                    && alignment.getHiddenColumns().hasHiddenColumns(),
+            selectedOnly,
             markGroups);
   }
 
@@ -1718,9 +1715,11 @@ public abstract class AlignmentViewport implements AlignViewportI,
     }
 
     selection = new String[iSize];
-    if (colSel != null && colSel.hasHiddenColumns())
+    if (alignment.getHiddenColumns() != null
+            && alignment.getHiddenColumns().hasHiddenColumns())
     {
-      selection = colSel.getVisibleSequenceStrings(start, end, seqs);
+      selection = alignment.getHiddenColumns().getVisibleSequenceStrings(
+              start, end, seqs);
     }
     else
     {
@@ -1736,20 +1735,21 @@ public abstract class AlignmentViewport implements AlignViewportI,
   @Override
   public List<int[]> getVisibleRegionBoundaries(int min, int max)
   {
-    ArrayList<int[]> regions = new ArrayList<int[]>();
+    ArrayList<int[]> regions = new ArrayList<>();
     int start = min;
     int end = max;
 
     do
     {
-      if (colSel != null && colSel.hasHiddenColumns())
+      HiddenColumns hidden = alignment.getHiddenColumns();
+      if (hidden != null && hidden.hasHiddenColumns())
       {
         if (start == 0)
         {
-          start = colSel.adjustForHiddenColumns(start);
+          start = hidden.adjustForHiddenColumns(start);
         }
 
-        end = colSel.getHiddenBoundaryRight(start);
+        end = hidden.getHiddenBoundaryRight(start);
         if (start == end)
         {
           end = max;
@@ -1762,10 +1762,10 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
       regions.add(new int[] { start, end });
 
-      if (colSel != null && colSel.hasHiddenColumns())
+      if (hidden != null && hidden.hasHiddenColumns())
       {
-        start = colSel.adjustForHiddenColumns(end);
-        start = colSel.getHiddenBoundaryLeft(start) + 1;
+        start = hidden.adjustForHiddenColumns(end);
+        start = hidden.getHiddenBoundaryLeft(start) + 1;
       }
     } while (end < max);
 
@@ -1778,7 +1778,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public List<AlignmentAnnotation> getVisibleAlignmentAnnotation(
           boolean selectedOnly)
   {
-    ArrayList<AlignmentAnnotation> ala = new ArrayList<AlignmentAnnotation>();
+    ArrayList<AlignmentAnnotation> ala = new ArrayList<>();
     AlignmentAnnotation[] aa;
     if ((aa = alignment.getAlignmentAnnotation()) != null)
     {
@@ -1787,12 +1787,13 @@ public abstract class AlignmentViewport implements AlignViewportI,
         AlignmentAnnotation clone = new AlignmentAnnotation(annot);
         if (selectedOnly && selectionGroup != null)
         {
-          colSel.makeVisibleAnnotation(selectionGroup.getStartRes(),
+          alignment.getHiddenColumns().makeVisibleAnnotation(
+                  selectionGroup.getStartRes(),
                   selectionGroup.getEndRes(), clone);
         }
         else
         {
-          colSel.makeVisibleAnnotation(clone);
+          alignment.getHiddenColumns().makeVisibleAnnotation(clone);
         }
         ala.add(clone);
       }
@@ -1857,7 +1858,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
       selectionGroup.setEndRes(alWidth - 1);
     }
 
-    resetAllColourSchemes();
+    updateAllColourSchemes();
     calculator.restartWorkers();
     // alignment.adjustSequenceAnnotations();
   }
@@ -1865,17 +1866,17 @@ public abstract class AlignmentViewport implements AlignViewportI,
   /**
    * reset scope and do calculations for all applied colourschemes on alignment
    */
-  void resetAllColourSchemes()
+  void updateAllColourSchemes()
   {
-    ColourSchemeI cs = globalColourScheme;
-    if (cs != null)
+    ResidueShaderI rs = residueShading;
+    if (rs != null)
     {
-      cs.alignmentChanged(alignment, hiddenRepSequences);
+      rs.alignmentChanged(alignment, hiddenRepSequences);
 
-      cs.setConsensus(hconsensus);
-      if (cs.conservationApplied())
+      rs.setConsensus(hconsensus);
+      if (rs.conservationApplied())
       {
-        cs.setConservation(Conservation.calculateConservation("All",
+        rs.setConservation(Conservation.calculateConservation("All",
                 alignment.getSequences(), 0, alignment.getWidth(), false,
                 getConsPercGaps(), false));
       }
@@ -1908,24 +1909,21 @@ public abstract class AlignmentViewport implements AlignViewportI,
       {
         initRNAStructure();
       }
-      consensus = new AlignmentAnnotation("Consensus", "PID",
+      consensus = new AlignmentAnnotation("Consensus",
+              MessageManager.getString("label.consensus_descr"),
               new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
       initConsensus(consensus);
-      gapcounts = new AlignmentAnnotation("Occupancy",
-              "Number of aligned positions",
-              new Annotation[1], 0f, alignment.getHeight(),
-              AlignmentAnnotation.BAR_GRAPH);
-      initGapCounts(gapcounts);
+      initGapCounts();
 
       initComplementConsensus();
     }
   }
 
   /**
-   * If this is a protein alignment and there are mappings to cDNA, add the cDNA
-   * consensus annotation.
+   * If this is a protein alignment and there are mappings to cDNA, adds the
+   * cDNA consensus annotation and returns true, else returns false.
    */
-  public void initComplementConsensus()
+  public boolean initComplementConsensus()
   {
     if (!alignment.isNucleotide())
     {
@@ -1949,12 +1947,16 @@ public abstract class AlignmentViewport implements AlignViewportI,
         if (doConsensus)
         {
           complementConsensus = new AlignmentAnnotation("cDNA Consensus",
-                  "PID for cDNA", new Annotation[1], 0f, 100f,
+                  MessageManager
+                          .getString("label.complement_consensus_descr"),
+                  new Annotation[1], 0f, 100f,
                   AlignmentAnnotation.BAR_GRAPH);
           initConsensus(complementConsensus);
+          return true;
         }
       }
     }
+    return false;
   }
 
   private void initConsensus(AlignmentAnnotation aa)
@@ -1970,15 +1972,20 @@ public abstract class AlignmentViewport implements AlignViewportI,
 
   // these should be extracted from the view model - style and settings for
   // derived annotation
-  private void initGapCounts(AlignmentAnnotation counts)
+  private void initGapCounts()
   {
-    counts.hasText = false;
-    counts.autoCalculated = true;
-    counts.graph = AlignmentAnnotation.BAR_GRAPH;
-
-    if (showConsensus)
+    if (showOccupancy)
     {
-      alignment.addAnnotation(counts);
+      gapcounts = new AlignmentAnnotation("Occupancy",
+              MessageManager.getString("label.occupancy_descr"),
+              new Annotation[1], 0f,
+              alignment.getHeight(), AlignmentAnnotation.BAR_GRAPH);
+      gapcounts.hasText = true;
+      gapcounts.autoCalculated = true;
+      gapcounts.scaleColLabel = true;
+      gapcounts.graph = AlignmentAnnotation.BAR_GRAPH;
+
+      alignment.addAnnotation(gapcounts);
     }
   }
 
@@ -1989,8 +1996,8 @@ public abstract class AlignmentViewport implements AlignViewportI,
       if (conservation == null)
       {
         conservation = new AlignmentAnnotation("Conservation",
-                "Conservation of total alignment less than "
-                        + getConsPercGaps() + "% gaps", new Annotation[1],
+                MessageManager.formatMessage("label.conservation_descr",
+                        getConsPercGaps()), new Annotation[1],
                 0f, 11f, AlignmentAnnotation.BAR_GRAPH);
         conservation.hasText = true;
         conservation.autoCalculated = true;
@@ -2006,7 +2013,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
       if (quality == null)
       {
         quality = new AlignmentAnnotation("Quality",
-                "Alignment Quality based on Blosum62 scores",
+                MessageManager.getString("label.quality_descr"),
                 new Annotation[1], 0f, 11f, AlignmentAnnotation.BAR_GRAPH);
         quality.hasText = true;
         quality.autoCalculated = true;
@@ -2019,7 +2026,8 @@ public abstract class AlignmentViewport implements AlignViewportI,
   {
     if (alignment.hasRNAStructure() && strucConsensus == null)
     {
-      strucConsensus = new AlignmentAnnotation("StrucConsensus", "PID",
+      strucConsensus = new AlignmentAnnotation("StrucConsensus",
+              MessageManager.getString("label.strucconsensus_descr"),
               new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
       strucConsensus.hasText = true;
       strucConsensus.autoCalculated = true;
@@ -2124,7 +2132,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
     // intersect alignment annotation with alignment groups
 
     AlignmentAnnotation[] aan = alignment.getAlignmentAnnotation();
-    List<SequenceGroup> oldrfs = new ArrayList<SequenceGroup>();
+    List<SequenceGroup> oldrfs = new ArrayList<>();
     if (aan != null)
     {
       for (int an = 0; an < aan.length; an++)
@@ -2468,6 +2476,11 @@ public abstract class AlignmentViewport implements AlignViewportI,
   public void setViewStyle(ViewStyleI settingsForView)
   {
     viewStyle = new ViewStyle(settingsForView);
+    if (residueShading != null)
+    {
+      residueShading.setConservationApplied(settingsForView
+              .isConservationColourSelected());
+    }
   }
 
   @Override
@@ -2661,6 +2674,18 @@ public abstract class AlignmentViewport implements AlignViewportI,
     viewStyle.setScaleProteinAsCdna(b);
   }
 
+  @Override
+  public boolean isProteinFontAsCdna()
+  {
+    return viewStyle.isProteinFontAsCdna();
+  }
+
+  @Override
+  public void setProteinFontAsCdna(boolean b)
+  {
+    viewStyle.setProteinFontAsCdna(b);
+  }
+
   /**
    * @return true if view should scroll to show the highlighted region of a
    *         sequence
@@ -2678,63 +2703,10 @@ public abstract class AlignmentViewport implements AlignViewportI,
     this.followHighlight = b;
   }
 
-  public int getStartRes()
-  {
-    return startRes;
-  }
-
   @Override
-  public int getEndRes()
-  {
-    return endRes;
-  }
-
-  public int getStartSeq()
-  {
-    return startSeq;
-  }
-
-  public void setStartRes(int res)
-  {
-    this.startRes = res;
-  }
-
-  public void setStartSeq(int seq)
-  {
-    this.startSeq = seq;
-  }
-
-  public void setEndRes(int res)
-  {
-    if (res > alignment.getWidth() - 1)
-    {
-      // log.System.out.println(" Corrected res from " + res + " to maximum " +
-      // (alignment.getWidth()-1));
-      res = alignment.getWidth() - 1;
-    }
-    if (res < 0)
-    {
-      res = 0;
-    }
-    this.endRes = res;
-  }
-
-  public void setEndSeq(int seq)
-  {
-    if (seq > alignment.getHeight())
-    {
-      seq = alignment.getHeight();
-    }
-    if (seq < 0)
-    {
-      seq = 0;
-    }
-    this.endSeq = seq;
-  }
-
-  public int getEndSeq()
+  public ViewportRanges getRanges()
   {
-    return endSeq;
+    return ranges;
   }
 
   /**
@@ -2774,7 +2746,8 @@ public abstract class AlignmentViewport implements AlignViewportI,
      * locate 'middle' column (true middle if an odd number visible, left of
      * middle if an even number visible)
      */
-    int middleColumn = getStartRes() + (getEndRes() - getStartRes()) / 2;
+    int middleColumn = ranges.getStartRes()
+            + (ranges.getEndRes() - ranges.getStartRes()) / 2;
     final HiddenSequences hiddenSequences = getAlignment()
             .getHiddenSequences();
 
@@ -2784,7 +2757,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
      */
     int lastSeq = alignment.getHeight() - 1;
     List<AlignedCodonFrame> seqMappings = null;
-    for (int seqNo = getStartSeq(); seqNo < lastSeq; seqNo++, seqOffset++)
+    for (int seqNo = ranges.getStartSeq(); seqNo < lastSeq; seqNo++, seqOffset++)
     {
       sequence = getAlignment().getSequenceAt(seqNo);
       if (hiddenSequences != null && hiddenSequences.isHidden(sequence))
@@ -2872,8 +2845,7 @@ public abstract class AlignmentViewport implements AlignViewportI,
         selectionIsDefinedGroup = gps.contains(selectionGroup);
       }
     }
-    return selectionGroup.getContext() == alignment
-            || selectionIsDefinedGroup;
+    return selectionGroup.isDefined() || selectionIsDefinedGroup;
   }
 
   /**
diff --git a/src/jalview/viewmodel/OverviewDimensions.java b/src/jalview/viewmodel/OverviewDimensions.java
new file mode 100644 (file)
index 0000000..d2912d8
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+ * 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.viewmodel;
+
+import jalview.api.AlignmentColsCollectionI;
+import jalview.api.AlignmentRowsCollectionI;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.HiddenSequences;
+
+import java.awt.Graphics;
+
+public abstract class OverviewDimensions
+{
+  protected static final int MAX_WIDTH = 400;
+
+  protected static final int MIN_WIDTH = 120;
+
+  protected static final int MIN_SEQ_HEIGHT = 40;
+
+  protected static final int MAX_SEQ_HEIGHT = 300;
+
+  private static final int DEFAULT_GRAPH_HEIGHT = 20;
+
+  protected int width;
+
+  protected int sequencesHeight;
+
+  protected int graphHeight = DEFAULT_GRAPH_HEIGHT;
+
+  protected int boxX = -1;
+
+  protected int boxY = -1;
+
+  protected int boxWidth = -1;
+
+  protected int boxHeight = -1;
+
+  protected int alwidth;
+
+  protected int alheight;
+
+  /**
+   * Create an OverviewDimensions object
+   * 
+   * @param ranges
+   *          positional properties of the viewport
+   * @param showAnnotationPanel
+   *          true if the annotation panel is to be shown, false otherwise
+   */
+  public OverviewDimensions(ViewportRanges ranges,
+          boolean showAnnotationPanel)
+  {
+    // scale the initial size of overviewpanel to shape of alignment
+    float initialScale = (float) ranges.getAbsoluteAlignmentWidth()
+            / (float) ranges.getAbsoluteAlignmentHeight();
+
+    if (!showAnnotationPanel)
+    {
+      graphHeight = 0;
+    }
+
+    if (ranges.getAbsoluteAlignmentWidth() > ranges
+            .getAbsoluteAlignmentHeight())
+    {
+      // wider
+      width = MAX_WIDTH;
+      sequencesHeight = Math.round(MAX_WIDTH / initialScale);
+      if (sequencesHeight < MIN_SEQ_HEIGHT)
+      {
+        sequencesHeight = MIN_SEQ_HEIGHT;
+      }
+    }
+    else
+    {
+      // taller
+      width = Math.round(MAX_WIDTH * initialScale);
+      sequencesHeight = MAX_SEQ_HEIGHT;
+
+      if (width < MIN_WIDTH)
+      {
+        width = MIN_WIDTH;
+      }
+    }
+  }
+
+  /**
+   * Draw the overview panel's viewport box on a graphics object
+   * 
+   * @param g
+   *          the graphics object to draw on
+   */
+  public void drawBox(Graphics g)
+  {
+    g.drawRect(boxX, boxY, boxWidth, boxHeight);
+    g.drawRect(boxX + 1, boxY + 1, boxWidth - 2, boxHeight - 2);
+  }
+
+  public int getBoxX()
+  {
+    return boxX;
+  }
+
+  public int getBoxY()
+  {
+    return boxY;
+  }
+
+  public int getBoxWidth()
+  {
+    return boxWidth;
+  }
+
+  public int getBoxHeight()
+  {
+    return boxHeight;
+  }
+
+  public int getWidth()
+  {
+    return width;
+  }
+
+  public int getHeight()
+  {
+    return sequencesHeight + graphHeight;
+  }
+
+  public int getSequencesHeight()
+  {
+    return sequencesHeight;
+  }
+
+  public int getGraphHeight()
+  {
+    return graphHeight;
+  }
+
+  public float getPixelsPerCol()
+  {
+    resetAlignmentDims();
+    return (float) width / alwidth;
+  }
+
+  public float getPixelsPerSeq()
+  {
+    resetAlignmentDims();
+    return (float) sequencesHeight / alheight;
+  }
+
+  public void setWidth(int w)
+  {
+    width = w;
+  }
+
+  public void setHeight(int h)
+  {
+    sequencesHeight = h - graphHeight;
+  }
+
+  /**
+   * Update the viewport location from a mouse click in the overview panel
+   * 
+   * @param mousex
+   *          x location of mouse
+   * @param mousey
+   *          y location of mouse
+   * @param hiddenSeqs
+   *          the alignment's hidden sequences
+   * @param hiddenCols
+   *          the alignment's hidden columns
+   */
+  public abstract void updateViewportFromMouse(int mousex, int mousey,
+          HiddenSequences hiddenSeqs, HiddenColumns hiddenCols);
+
+  /**
+   * Set the overview panel's box position to match the viewport
+   * 
+   * @param hiddenSeqs
+   *          the alignment's hidden sequences
+   * @param hiddenCols
+   *          the alignment's hidden columns
+   */
+  public abstract void setBoxPosition(HiddenSequences hiddenSeqs,
+          HiddenColumns hiddenCols);
+
+  /**
+   * Get the collection of columns used by this overview dimensions object
+   * 
+   * @param hiddenCols
+   *          the alignment's hidden columns
+   * @return a column collection
+   */
+  public abstract AlignmentColsCollectionI getColumns(AlignmentI al);
+
+  /**
+   * Get the collection of rows used by this overview dimensions object
+   * 
+   * @param al
+   *          the alignment
+   * @return a row collection
+   */
+  public abstract AlignmentRowsCollectionI getRows(AlignmentI al);
+
+  /**
+   * Updates overview dimensions to account for current alignment dimensions
+   */
+  protected abstract void resetAlignmentDims();
+
+  protected void setBoxPosition(int startRes, int startSeq, int vpwidth,
+          int vpheight)
+  {
+    resetAlignmentDims();
+
+    // boxX, boxY is the x,y location equivalent to startRes, startSeq
+    boxX = Math.round((float) startRes * width / alwidth);
+    boxY = Math.round((float) startSeq * sequencesHeight / alheight);
+
+    // boxWidth is the width in residues translated to pixels
+    boxWidth = Math.round((float) vpwidth * width / alwidth);
+
+    // boxHeight is the height in sequences translated to pixels
+    boxHeight = Math.round((float) vpheight * sequencesHeight / alheight);
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/viewmodel/OverviewDimensionsHideHidden.java b/src/jalview/viewmodel/OverviewDimensionsHideHidden.java
new file mode 100644 (file)
index 0000000..4bf7694
--- /dev/null
@@ -0,0 +1,130 @@
+package jalview.viewmodel;
+
+import jalview.api.AlignmentColsCollectionI;
+import jalview.api.AlignmentRowsCollectionI;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.HiddenSequences;
+import jalview.datamodel.VisibleColsCollection;
+import jalview.datamodel.VisibleRowsCollection;
+
+public class OverviewDimensionsHideHidden extends OverviewDimensions
+{
+  private ViewportRanges ranges;
+
+  public OverviewDimensionsHideHidden(ViewportRanges vpranges,
+          boolean showAnnotationPanel)
+  {
+    super(vpranges, showAnnotationPanel);
+    ranges = vpranges;
+    resetAlignmentDims();
+  }
+
+  @Override
+  public void updateViewportFromMouse(int mousex, int mousey,
+          HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
+  {
+    resetAlignmentDims();
+
+    int x = mousex;
+    int y = mousey;
+
+    if (x < 0)
+    {
+      x = 0;
+    }
+
+    if (y < 0)
+    {
+      y = 0;
+    }
+
+    //
+    // Convert x value to residue position
+    //
+
+    // need to determine where scrollCol should be, given x
+    // to do this also need to know width of viewport, and some hidden column
+    // correction
+
+    // convert x to residues - this is an absolute position
+    int xAsRes = Math.round((float) x * alwidth / width);
+
+    // get viewport width in residues
+    int vpwidth = ranges.getViewportWidth();
+
+    if (xAsRes + vpwidth > alwidth)
+    {
+      // went past the end of the alignment, adjust backwards
+
+      // if last position was before the end of the alignment, need to update
+      if (ranges.getStartRes() < alwidth)
+      {
+        xAsRes = alwidth - vpwidth;
+      }
+      else
+      {
+        xAsRes = ranges.getStartRes();
+      }
+    }
+
+
+    //
+    // Convert y value to sequence position
+    //
+
+    // convert y to residues
+    int yAsSeq = Math.round((float) y * alheight / sequencesHeight);
+
+    // get viewport height in sequences
+    // add 1 because height includes both endSeq and startSeq
+    int vpheight = ranges.getViewportHeight();
+
+    if (yAsSeq + vpheight > alheight)
+    {
+      // went past the end of the alignment, adjust backwards
+      if (ranges.getEndSeq() < alheight)
+      {
+        yAsSeq = alheight - vpheight;
+      }
+      else
+      {
+        yAsSeq = ranges.getStartSeq();
+      }
+    }
+
+    // update viewport
+    ranges.setStartRes(xAsRes);
+    ranges.setStartSeq(yAsSeq);
+
+  }
+
+  @Override
+  public void setBoxPosition(HiddenSequences hiddenSeqs,
+          HiddenColumns hiddenCols)
+  {
+    setBoxPosition(ranges.getStartRes(), ranges.getStartSeq(),
+            ranges.getViewportWidth(), ranges.getViewportHeight());
+  }
+
+  @Override
+  public AlignmentColsCollectionI getColumns(AlignmentI al)
+  {
+    return new VisibleColsCollection(0,
+            ranges.getAbsoluteAlignmentWidth() - 1, al);
+  }
+
+  @Override
+  public AlignmentRowsCollectionI getRows(AlignmentI al)
+  {
+    return new VisibleRowsCollection(0,
+            ranges.getAbsoluteAlignmentHeight() - 1, al);
+  }
+
+  @Override
+  protected void resetAlignmentDims()
+  {
+    alwidth = ranges.getVisibleAlignmentWidth();
+    alheight = ranges.getVisibleAlignmentHeight();
+  }
+}
diff --git a/src/jalview/viewmodel/OverviewDimensionsShowHidden.java b/src/jalview/viewmodel/OverviewDimensionsShowHidden.java
new file mode 100644 (file)
index 0000000..4b396a6
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * 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.viewmodel;
+
+import jalview.api.AlignmentColsCollectionI;
+import jalview.api.AlignmentRowsCollectionI;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AllColsCollection;
+import jalview.datamodel.AllRowsCollection;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.HiddenSequences;
+
+public class OverviewDimensionsShowHidden extends OverviewDimensions
+{
+  private ViewportRanges ranges;
+
+  /**
+   * Create an OverviewDimensions object
+   * 
+   * @param ranges
+   *          positional properties of the viewport
+   * @param showAnnotationPanel
+   *          true if the annotation panel is to be shown, false otherwise
+   */
+  public OverviewDimensionsShowHidden(ViewportRanges vpranges,
+          boolean showAnnotationPanel)
+  {
+    super(vpranges, showAnnotationPanel);
+    ranges = vpranges;
+    resetAlignmentDims();
+  }
+
+  /**
+   * Check box dimensions and scroll positions and correct if necessary
+   * 
+   * @param mousex
+   *          x position in overview panel
+   * @param mousey
+   *          y position in overview panel
+   * @param hiddenSeqs
+   *          hidden sequences
+   * @param hiddenCols
+   *          hidden columns
+   * @param ranges
+   *          viewport position properties
+   */
+  @Override
+  public void updateViewportFromMouse(int mousex, int mousey,
+          HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
+  {
+    int x = mousex;
+    int y = mousey;
+
+    resetAlignmentDims();
+
+    if (x < 0)
+    {
+      x = 0;
+    }
+
+    if (y < 0)
+    {
+      y = 0;
+    }
+
+    //
+    // Convert x value to residue position
+    //
+
+    // need to determine where scrollCol should be, given x
+    // to do this also need to know width of viewport, and some hidden column
+    // correction
+
+    // convert x to residues - this is an absolute position
+    int xAsRes = Math.round((float) x * alwidth / width);
+
+    // get viewport width in residues
+    int vpwidth = ranges.getViewportWidth();
+
+    // get where x should be when accounting for hidden cols
+    // if x is in a hidden col region, shift to left - but we still need
+    // absolute position
+    // so convert back after getting visible region position
+    int visXAsRes = hiddenCols.findColumnPosition(xAsRes);
+
+    // check in case we went off the edge of the alignment
+    int visAlignWidth = hiddenCols.findColumnPosition(alwidth - 1);
+    if (visXAsRes + vpwidth - 1 > visAlignWidth)
+    {
+      // went past the end of the alignment, adjust backwards
+
+      // 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));
+      }
+      else
+      {
+        visXAsRes = ranges.getStartRes();
+      }
+    }
+
+    //
+    // Convert y value to sequence position
+    //
+
+    // convert y to residues
+    int yAsSeq = Math.round((float) y * alheight / sequencesHeight);
+
+    // get viewport height in sequences
+    int vpheight = ranges.getViewportHeight();
+
+    // get where y should be when accounting for hidden rows
+    // if y is in a hidden row region, shift up - but we still need absolute
+    // position,
+    // so convert back after getting visible region position
+    yAsSeq = hiddenSeqs.adjustForHiddenSeqs(hiddenSeqs
+            .findIndexWithoutHiddenSeqs(yAsSeq));
+
+    // check in case we went off the edge of the alignment
+    int visAlignHeight = hiddenSeqs.findIndexWithoutHiddenSeqs(alheight);
+    int visYAsSeq = hiddenSeqs.findIndexWithoutHiddenSeqs(yAsSeq);
+    if (visYAsSeq + vpheight - 1 > visAlignHeight)
+    {
+      // went past the end of the alignment, adjust backwards
+      if (ranges.getEndSeq() < visAlignHeight)
+      {
+        visYAsSeq = hiddenSeqs.findIndexWithoutHiddenSeqs(hiddenSeqs
+                .subtractVisibleRows(vpheight - 1, alheight - 1));
+      }
+      else
+      {
+        visYAsSeq = ranges.getStartSeq();
+      }
+    }
+
+    // update viewport
+    ranges.setStartRes(visXAsRes);
+    ranges.setStartSeq(visYAsSeq);
+
+  }
+
+  /**
+   * Update the overview panel box when the associated alignment panel is
+   * changed
+   * 
+   * @param hiddenSeqs
+   *          hidden sequences
+   * @param hiddenCols
+   *          hidden columns
+   * @param ranges
+   *          viewport position properties
+   */
+  @Override
+  public void setBoxPosition(HiddenSequences hiddenSeqs,
+          HiddenColumns hiddenCols)
+  {
+    // work with absolute values of startRes and endRes
+    int startRes = hiddenCols.adjustForHiddenColumns(ranges.getStartRes());
+    int endRes = hiddenCols.adjustForHiddenColumns(ranges.getEndRes());
+
+    // work with absolute values of startSeq and endSeq
+    int startSeq = hiddenSeqs.adjustForHiddenSeqs(ranges.getStartSeq());
+    int endSeq = hiddenSeqs.adjustForHiddenSeqs(ranges.getEndSeq());
+
+    setBoxPosition(startRes, startSeq, endRes - startRes + 1, endSeq
+            - startSeq + 1);
+  }
+
+  @Override
+  public AlignmentColsCollectionI getColumns(AlignmentI al)
+  {
+    return new AllColsCollection(0,
+            ranges.getAbsoluteAlignmentWidth() - 1, al);
+  }
+
+  @Override
+  public AlignmentRowsCollectionI getRows(AlignmentI al)
+  {
+    return new AllRowsCollection(0,
+            ranges.getAbsoluteAlignmentHeight() - 1,
+            al);
+  }
+
+  @Override
+  protected void resetAlignmentDims()
+  {
+    alwidth = ranges.getAbsoluteAlignmentWidth();
+    alheight = ranges.getAbsoluteAlignmentHeight();
+  }
+}
index b0af302..928d35e 100644 (file)
@@ -22,6 +22,8 @@ package jalview.viewmodel;
 
 import jalview.analysis.PCA;
 import jalview.api.RotatableCanvasI;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
 import jalview.datamodel.AlignmentView;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.SequencePoint;
@@ -30,16 +32,6 @@ import java.util.Vector;
 
 public class PCAModel
 {
-
-  public PCAModel(AlignmentView seqstrings2, SequenceI[] seqs2,
-          boolean nucleotide2)
-  {
-    seqstrings = seqstrings2;
-    seqs = seqs2;
-    nucleotide = nucleotide2;
-    score_matrix = nucleotide2 ? "PID" : "BLOSUM62";
-  }
-
   private volatile PCA pca;
 
   int top;
@@ -48,31 +40,41 @@ public class PCAModel
 
   SequenceI[] seqs;
 
-  /**
-   * Score matrix used to calculate PC
+  /*
+   * Name of score model used to calculate PCA
    */
-  String score_matrix;
+  ScoreModelI scoreModel;
 
-  /**
-   * use the identity matrix for calculating similarity between sequences.
-   */
   private boolean nucleotide = false;
 
   private Vector<SequencePoint> points;
 
-  private boolean jvCalcMode = true;
+  private SimilarityParamsI similarityParams;
 
-  public boolean isJvCalcMode()
+  /**
+   * Constructor given sequence data, score model and score calculation
+   * parameter options.
+   * 
+   * @param seqData
+   * @param sqs
+   * @param nuc
+   * @param modelName
+   * @param params
+   */
+  public PCAModel(AlignmentView seqData, SequenceI[] sqs, boolean nuc,
+          ScoreModelI modelName,
+          SimilarityParamsI params)
   {
-    return jvCalcMode;
+    seqstrings = seqData;
+    seqs = sqs;
+    nucleotide = nuc;
+    scoreModel = modelName;
+    similarityParams = params;
   }
 
   public void run()
   {
-
-    pca = new PCA(seqstrings.getSequenceStrings(' '), nucleotide,
-            score_matrix);
-    pca.setJvCalcMode(jvCalcMode);
+    pca = new PCA(seqstrings, scoreModel, similarityParams);
     pca.run();
 
     // Now find the component coordinates
@@ -83,32 +85,23 @@ public class PCAModel
       ii++;
     }
 
-    double[][] comps = new double[ii][ii];
-
-    for (int i = 0; i < ii; i++)
-    {
-      if (pca.getEigenvalue(i) > 1e-4)
-      {
-        comps[i] = pca.component(i);
-      }
-    }
-
-    top = pca.getM().rows - 1;
+    int height = pca.getHeight();
+    // top = pca.getM().height() - 1;
+    top = height - 1;
 
     points = new Vector<SequencePoint>();
     float[][] scores = pca.getComponents(top - 1, top - 2, top - 3, 100);
 
-    for (int i = 0; i < pca.getM().rows; i++)
+    for (int i = 0; i < height; i++)
     {
       SequencePoint sp = new SequencePoint(seqs[i], scores[i]);
       points.addElement(sp);
     }
-
   }
 
   public void updateRc(RotatableCanvasI rc)
   {
-    rc.setPoints(points, pca.getM().rows);
+    rc.setPoints(points, pca.getHeight());
   }
 
   public boolean isNucleotide()
@@ -146,9 +139,9 @@ public class PCAModel
     // note: actual indices for components are dim1-1, etc (patch for JAL-1123)
     float[][] scores = pca.getComponents(dim1 - 1, dim2 - 1, dim3 - 1, 100);
 
-    for (int i = 0; i < pca.getM().rows; i++)
+    for (int i = 0; i < pca.getHeight(); i++)
     {
-      ((SequencePoint) points.elementAt(i)).coord = scores[i];
+      points.elementAt(i).coord = scores[i];
     }
   }
 
@@ -228,19 +221,14 @@ public class PCAModel
     return pts;
   }
 
-  public void setJvCalcMode(boolean state)
-  {
-    jvCalcMode = state;
-  }
-
-  public String getScore_matrix()
+  public String getScoreModelName()
   {
-    return score_matrix;
+    return scoreModel == null ? "" : scoreModel.getName();
   }
 
-  public void setScore_matrix(String score_matrix)
+  public void setScoreModel(ScoreModelI sm)
   {
-    this.score_matrix = score_matrix;
+    this.scoreModel = sm;
   }
 
 }
diff --git a/src/jalview/viewmodel/ViewportListenerI.java b/src/jalview/viewmodel/ViewportListenerI.java
new file mode 100644 (file)
index 0000000..555089e
--- /dev/null
@@ -0,0 +1,8 @@
+package jalview.viewmodel;
+
+import java.beans.PropertyChangeListener;
+
+public interface ViewportListenerI extends PropertyChangeListener
+{
+
+}
diff --git a/src/jalview/viewmodel/ViewportProperties.java b/src/jalview/viewmodel/ViewportProperties.java
new file mode 100644 (file)
index 0000000..a78a1c0
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.viewmodel;
+
+import java.beans.PropertyChangeSupport;
+
+public abstract class ViewportProperties
+{
+  protected PropertyChangeSupport changeSupport = new PropertyChangeSupport(
+          this);
+
+  public void addPropertyChangeListener(ViewportListenerI listener)
+  {
+    changeSupport.addPropertyChangeListener(listener);
+  }
+
+  public void removePropertyChangeListener(ViewportListenerI listener)
+  {
+    changeSupport.removePropertyChangeListener(listener);
+  }
+
+}
diff --git a/src/jalview/viewmodel/ViewportRanges.java b/src/jalview/viewmodel/ViewportRanges.java
new file mode 100644 (file)
index 0000000..4eb8c95
--- /dev/null
@@ -0,0 +1,519 @@
+/*
+ * 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.viewmodel;
+
+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
+ */
+public class ViewportRanges extends ViewportProperties
+{
+  // start residue of viewport
+  private int startRes;
+
+  // end residue of viewport
+  private int endRes;
+
+  // start sequence of viewport
+  private int startSeq;
+
+  // end sequence of viewport
+  private int endSeq;
+
+  // alignment
+  private AlignmentI al;
+
+  /**
+   * Constructor
+   * 
+   * @param alignment
+   *          the viewport's alignment
+   */
+  public ViewportRanges(AlignmentI alignment)
+  {
+    // initial values of viewport settings
+    this.startRes = 0;
+    this.endRes = alignment.getWidth() - 1;
+    this.startSeq = 0;
+    this.endSeq = alignment.getHeight() - 1;
+    this.al = alignment;
+  }
+
+  /**
+   * Get alignment width in cols, including hidden cols
+   */
+  public int getAbsoluteAlignmentWidth()
+  {
+    return al.getWidth();
+  }
+
+  /**
+   * Get alignment height in rows, including hidden rows
+   */
+  public int getAbsoluteAlignmentHeight()
+  {
+    return al.getHeight() + al.getHiddenSequences().getSize();
+  }
+
+  /**
+   * Get alignment width in cols, excluding hidden cols
+   */
+  public int getVisibleAlignmentWidth()
+  {
+    return al.getWidth() - al.getHiddenColumns().getSize();
+  }
+
+  /**
+   * Get alignment height in rows, excluding hidden rows
+   */
+  public int getVisibleAlignmentHeight()
+  {
+    return al.getHeight();
+  }
+
+  /**
+   * Set first residue visible in the viewport, and retain the current width.
+   * Fires a property change event.
+   * 
+   * @param res
+   *          residue position
+   */
+  public void setStartRes(int res)
+  {
+    int width = getViewportWidth();
+    setStartEndRes(res, res + width - 1);
+  }
+
+  /**
+   * Set start and end residues at the same time. This method only fires one
+   * event for the two changes, and should be used in preference to separate
+   * calls to setStartRes and setEndRes.
+   * 
+   * @param start
+   *          the start residue
+   * @param end
+   *          the end residue
+   */
+  public void setStartEndRes(int start, int end)
+  {
+    int oldstartres = this.startRes;
+    if (start > getVisibleAlignmentWidth() - 1)
+    {
+      startRes = Math.max(getVisibleAlignmentWidth() - 1, 0);
+    }
+    else if (start < 0)
+    {
+      startRes = 0;
+    }
+    else
+    {
+      startRes = start;
+    }
+
+    int oldendres = this.endRes;
+    if (end < 0)
+    {
+      endRes = 0;
+    }
+    else if (end > getVisibleAlignmentWidth() - 1)
+    {
+      endRes = Math.max(getVisibleAlignmentWidth() - 1, 0);
+    }
+    else
+    {
+      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);
+    }
+  }
+
+  /**
+   * Set last residue visible in the viewport. Fires a property change event.
+   * 
+   * @param res
+   *          residue position
+   */
+  public void setEndRes(int res)
+  {
+    int startres = res;
+    int width = getViewportWidth();
+    if (startres + width - 1 > getVisibleAlignmentWidth() - 1)
+    {
+      startres = getVisibleAlignmentWidth() - width;
+    }
+    setStartEndRes(startres - width + 1, startres);
+  }
+
+  /**
+   * Set the first sequence visible in the viewport, maintaining the height. If
+   * the viewport would extend past the last sequence, sets the viewport so it
+   * sits at the bottom of the alignment. Fires a property change event.
+   * 
+   * @param seq
+   *          sequence position
+   */
+  public void setStartSeq(int seq)
+  {
+    int startseq = seq;
+    int height = getViewportHeight();
+    if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
+    {
+      startseq = getVisibleAlignmentHeight() - height;
+    }
+    setStartEndSeq(startseq, startseq + height - 1);
+  }
+
+  /**
+   * Set start and end sequences at the same time. The viewport height may
+   * change. This method only fires one event for the two changes, and should be
+   * used in preference to separate calls to setStartSeq and setEndSeq.
+   * 
+   * @param start
+   *          the start sequence
+   * @param end
+   *          the end sequence
+   */
+  public void setStartEndSeq(int start, int end)
+  {
+    int oldstartseq = this.startSeq;
+    if (start > getVisibleAlignmentHeight() - 1)
+    {
+      startSeq = Math.max(getVisibleAlignmentHeight() - 1, 0);
+    }
+    else if (start < 0)
+    {
+      startSeq = 0;
+    }
+    else
+    {
+      startSeq = start;
+    }
+
+    int oldendseq = this.endSeq;
+    if (end >= getVisibleAlignmentHeight())
+    {
+      endSeq = Math.max(getVisibleAlignmentHeight() - 1, 0);
+    }
+    else if (end < 0)
+    {
+      endSeq = 0;
+    }
+    else
+    {
+      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);
+    }
+  }
+
+  /**
+   * Set the last sequence visible in the viewport. Fires a property change
+   * event.
+   * 
+   * @param seq
+   *          sequence position
+   */
+  public void setEndSeq(int seq)
+  {
+    int height = getViewportHeight();
+    setStartEndSeq(seq - height + 1, seq);
+  }
+
+  /**
+   * Get start residue of viewport
+   */
+  public int getStartRes()
+  {
+    return startRes;
+  }
+
+  /**
+   * Get end residue of viewport
+   */
+  public int getEndRes()
+  {
+    return endRes;
+  }
+
+  /**
+   * Get start sequence of viewport
+   */
+  public int getStartSeq()
+  {
+    return startSeq;
+  }
+
+  /**
+   * Get end sequence of viewport
+   */
+  public int getEndSeq()
+  {
+    return endSeq;
+  }
+
+  /**
+   * Set viewport width in residues, without changing startRes. Use in
+   * preference to calculating endRes from the width, to avoid out by one
+   * errors! Fires a property change event.
+   * 
+   * @param w
+   *          width in residues
+   */
+  public void setViewportWidth(int w)
+  {
+    setStartEndRes(startRes, startRes + w - 1);
+  }
+
+  /**
+   * Set viewport height in residues, without changing startSeq. Use in
+   * preference to calculating endSeq from the height, to avoid out by one
+   * errors! Fires a property change event.
+   * 
+   * @param h
+   *          height in sequences
+   */
+  public void setViewportHeight(int h)
+  {
+    setStartEndSeq(startSeq, startSeq + h - 1);
+  }
+
+  /**
+   * Set viewport horizontal start position and width. Use in preference to
+   * calculating endRes from the width, to avoid out by one errors! Fires a
+   * property change event.
+   * 
+   * @param start
+   *          start residue
+   * @param w
+   *          width in residues
+   */
+  public void setViewportStartAndWidth(int start, int w)
+  {
+    int vpstart = start;
+    if (vpstart < 0)
+    {
+      vpstart = 0;
+    }
+    else if ((w <= getVisibleAlignmentWidth())
+            && (vpstart + w - 1 > getVisibleAlignmentWidth() - 1))
+    // viewport width is less than the full alignment and we are running off the
+    // RHS edge
+    {
+      vpstart = getVisibleAlignmentWidth() - w;
+    }
+    setStartEndRes(vpstart, vpstart + w - 1);
+  }
+
+  /**
+   * Set viewport vertical start position and height. Use in preference to
+   * calculating endSeq from the height, to avoid out by one errors! Fires a
+   * property change event.
+   * 
+   * @param start
+   *          start sequence
+   * @param h
+   *          height in sequences
+   */
+  public void setViewportStartAndHeight(int start, int h)
+  {
+    int vpstart = start;
+    if (vpstart < 0)
+    {
+      vpstart = 0;
+    }
+    else if ((h <= getVisibleAlignmentHeight())
+            && (vpstart + h - 1 > getVisibleAlignmentHeight() - 1))
+    // viewport height is less than the full alignment and we are running off
+    // the bottom
+    {
+      vpstart = getVisibleAlignmentHeight() - h;
+    }
+    setStartEndSeq(vpstart, vpstart + h - 1);
+  }
+
+  /**
+   * Get width of viewport in residues
+   * 
+   * @return width of viewport
+   */
+  public int getViewportWidth()
+  {
+    return (endRes - startRes + 1);
+  }
+
+  /**
+   * Get height of viewport in residues
+   * 
+   * @return height of viewport
+   */
+  public int getViewportHeight()
+  {
+    return (endSeq - startSeq + 1);
+  }
+
+  /**
+   * Scroll the viewport range vertically. Fires a property change event.
+   * 
+   * @param up
+   *          true if scrolling up, false if down
+   * 
+   * @return true if the scroll is valid
+   */
+  public boolean scrollUp(boolean up)
+  {
+    if (up)
+    {
+      if (startSeq < 1)
+      {
+        return false;
+      }
+
+      setStartSeq(startSeq - 1);
+    }
+    else
+    {
+      if (endSeq >= getVisibleAlignmentHeight() - 1)
+      {
+        return false;
+      }
+
+      setStartSeq(startSeq + 1);
+    }
+    return true;
+  }
+
+  /**
+   * Scroll the viewport range horizontally. Fires a property change event.
+   * 
+   * @param right
+   *          true if scrolling right, false if left
+   * 
+   * @return true if the scroll is valid
+   */
+  public boolean scrollRight(boolean right)
+  {
+    if (!right)
+    {
+      if (startRes < 1)
+      {
+        return false;
+      }
+
+      setStartRes(startRes - 1);
+    }
+    else
+    {
+      if (endRes >= getVisibleAlignmentWidth() - 1)
+      {
+        return false;
+      }
+
+      setStartRes(startRes + 1);
+    }
+
+    return true;
+  }
+
+  /**
+   * Scroll a wrapped alignment so that the specified residue is visible. Fires
+   * a property change event.
+   * 
+   * @param res
+   *          residue position to scroll to
+   */
+  public void scrollToWrappedVisible(int res)
+  {
+    // get the start residue of the wrapped row which res is in
+    // and set that as our start residue
+    int width = getViewportWidth();
+    setStartRes((res / width) * width);
+  }
+
+  /**
+   * Scroll so that (x,y) is visible. Fires a property change event.
+   * 
+   * @param x
+   *          x position in alignment
+   * @param y
+   *          y position in alignment
+   */
+  public void scrollToVisible(int x, int y)
+  {
+    while (y < startSeq)
+    {
+      scrollUp(true);
+    }
+    while (y > endSeq)
+    {
+      scrollUp(false);
+    }
+
+    HiddenColumns hidden = al.getHiddenColumns();
+    while (x < hidden.adjustForHiddenColumns(startRes))
+    {
+      if (!scrollRight(false))
+      {
+        break;
+      }
+    }
+    while (x > hidden.adjustForHiddenColumns(endRes))
+    {
+      if (!scrollRight(true))
+      {
+        break;
+      }
+    }
+  }
+  
+  /**
+   * Adjust sequence position for page up. Fires a property change event.
+   */
+  public void pageUp()
+  {
+    setViewportStartAndHeight(2 * startSeq - endSeq, getViewportHeight());
+  }
+  
+  /**
+   * Adjust sequence position for page down. Fires a property change event.
+   */
+  public void pageDown()
+  {
+    setViewportStartAndHeight(endSeq, getViewportHeight());
+  }
+}
index c1ad465..de1ee5e 100644 (file)
@@ -26,9 +26,10 @@ import jalview.api.FeaturesDisplayedI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.renderer.seqfeatures.FeatureRenderer;
 import jalview.schemes.FeatureColour;
-import jalview.schemes.UserColourScheme;
+import jalview.util.ColorUtils;
 
 import java.awt.Color;
 import java.beans.PropertyChangeListener;
@@ -36,6 +37,7 @@ import java.beans.PropertyChangeSupport;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
@@ -115,11 +117,10 @@ public abstract class FeatureRendererModel implements
           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);
             }
           }
         }
@@ -263,50 +264,40 @@ public abstract class FeatureRendererModel implements
   }
 
   @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<SequenceFeature>();
+    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]);
-        }
+    for (SequenceFeature sf : features)
+    {
+      if (!featureGroupNotShown(sf))
+      {
+        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
    */
@@ -328,8 +319,7 @@ public abstract class FeatureRendererModel implements
     }
     FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
 
-    ArrayList<String> allfeatures = new ArrayList<String>();
-    ArrayList<String> oldfeatures = new ArrayList<String>();
+    Set<String> oldfeatures = new HashSet<String>();
     if (renderOrder != null)
     {
       for (int i = 0; i < renderOrder.length; i++)
@@ -340,94 +330,110 @@ public abstract class FeatureRendererModel implements
         }
       }
     }
-    if (minmax == null)
-    {
-      minmax = new Hashtable<String, float[][]>();
-    }
+
     AlignmentI alignment = av.getAlignment();
+    List<String> allfeatures = new ArrayList<String>();
+
     for (int i = 0; i < alignment.getHeight(); i++)
     {
       SequenceI asq = alignment.getSequenceAt(i);
-      SequenceFeature[] features = asq.getSequenceFeatures();
-
-      if (features == null)
-      {
-        continue;
-      }
-
-      int index = 0;
-      while (index < features.length)
+      for (String group : asq.getFeatures().getFeatureGroups(true))
       {
-        if (!featuresDisplayed.isRegistered(features[index].getType()))
+        boolean groupDisplayed = true;
+        if (group != null)
         {
-          String fgrp = features[index].getFeatureGroup();
-          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()))
+        if (groupDisplayed)
         {
-          allfeatures.add(features[index].getType());
-        }
-        if (!Float.isNaN(features[index].score))
-        {
-          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)
+            if (!allfeatures.contains(type)) // or use HashSet and no test?
             {
-              mm[nonpos][0] = features[index].score;
-            }
-            if (mm[nonpos][1] < features[index].score)
-            {
-              mm[nonpos][1] = features[index].score;
+              allfeatures.add(type);
             }
+            updateMinMax(asq, type, true); // todo: for all features?
           }
         }
-        index++;
       }
     }
+
+    // uncomment to add new features in alphebetical order (but JAL-2575)
+    // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER);
+    if (newMadeVisible)
+    {
+      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<String, float[][]>();
+    }
+    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;
 
   /**
@@ -542,7 +548,7 @@ public abstract class FeatureRendererModel implements
     FeatureColourI fc = featureColours.get(featureType);
     if (fc == null)
     {
-      Color col = UserColourScheme.createColourFromName(featureType);
+      Color col = ColorUtils.createColourFromName(featureType);
       fc = new FeatureColour(col);
       featureColours.put(featureType, fc);
     }
@@ -550,11 +556,13 @@ public abstract class FeatureRendererModel implements
   }
 
   /**
-   * calculate the render colour for a specific feature using current feature
-   * settings.
+   * 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 render colour for the given feature
+   * @return
    */
   public Color getColour(SequenceFeature feature)
   {
@@ -562,6 +570,13 @@ public abstract class FeatureRendererModel implements
     return fc.getColor(feature);
   }
 
+  /**
+   * Answers true unless the feature has a graduated colour scheme and the
+   * feature value lies outside the current threshold for display
+   * 
+   * @param sequenceFeature
+   * @return
+   */
   protected boolean showFeature(SequenceFeature sequenceFeature)
   {
     FeatureColourI fc = getFeatureStyle(sequenceFeature.type);
@@ -586,11 +601,13 @@ public abstract class FeatureRendererModel implements
     featureColours.put(featureType, col);
   }
 
+  @Override
   public void setTransparency(float value)
   {
     transparency = value;
   }
 
+  @Override
   public float getTransparency()
   {
     return transparency;
@@ -653,7 +670,8 @@ public abstract class FeatureRendererModel implements
   }
 
   /**
-   * 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) }
@@ -822,7 +840,7 @@ public abstract class FeatureRendererModel implements
    * @return list of groups
    */
   @Override
-  public List getGroups(boolean visible)
+  public List<String> getGroups(boolean visible)
   {
     if (featureGroups != null)
     {
@@ -877,11 +895,10 @@ public abstract class FeatureRendererModel implements
     {
       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;
@@ -923,25 +940,115 @@ public abstract class FeatureRendererModel implements
   public List<String> getDisplayedFeatureGroups()
   {
     List<String> _gps = new ArrayList<String>();
-    boolean valid = false;
     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<SequenceFeature>();
+    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))
       {
-        return null;
+        result.add(sf);
       }
-      else
+    }
+    return result;
+  }
+
+  /**
+   * Removes from the list of features any that have a feature group that is not
+   * displayed, or duplicate the location of a feature of the same type (unless
+   * a graduated colour scheme is applied)
+   * 
+   * @param features
+   * @param fc
+   */
+  public void filterFeaturesForDisplay(List<SequenceFeature> features,
+          FeatureColourI fc)
+  {
+    if (features.isEmpty())
+    {
+      return;
+    }
+    SequenceFeatures.sortFeatures(features, true);
+    boolean graduated = fc != null && fc.isGraduatedColour();
+    SequenceFeature lastFeature = null;
+
+    Iterator<SequenceFeature> it = features.iterator();
+    while (it.hasNext())
+    {
+      SequenceFeature sf = it.next();
+      if (featureGroupNotShown(sf))
+      {
+        it.remove();
+        continue;
+      }
+
+      /*
+       * a feature is redundant for rendering purposes if it has the
+       * 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 (!graduated)
       {
-        // gps = new String[_gps.size()];
-        // _gps.toArray(gps);
+        if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+                && sf.getEnd() == lastFeature.getEnd()
+                && sf.isContactFeature() == lastFeature.isContactFeature()
+                && sf.getType().equals(lastFeature.getType()))
+        {
+          it.remove();
+        }
       }
+      lastFeature = sf;
     }
-    return _gps;
   }
 
 }
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 14a1bde..f01047d 100644 (file)
@@ -168,6 +168,12 @@ public class ViewStyle implements ViewStyleI
    */
   private boolean scaleProteinAsCdna = true;
 
+  /*
+   * if true, font changes to protein or cDNA are applied to both
+   * sides of a split screen
+   */
+  private boolean proteinFontAsCdna = true;
+
   /**
    * Copy constructor
    * 
@@ -195,6 +201,7 @@ public class ViewStyle implements ViewStyleI
     setScaleAboveWrapped(vs.getScaleAboveWrapped());
     setScaleLeftWrapped(vs.getScaleLeftWrapped());
     setScaleProteinAsCdna(vs.isScaleProteinAsCdna());
+    setProteinFontAsCdna(vs.isProteinFontAsCdna());
     setScaleRightWrapped(vs.getScaleRightWrapped());
     setSeqNameItalics(vs.isSeqNameItalics());
     setShowAnnotation(vs.isShowAnnotation());
@@ -255,6 +262,7 @@ public class ViewStyle implements ViewStyleI
             && getScaleAboveWrapped() == vs.getScaleAboveWrapped()
             && getScaleLeftWrapped() == vs.getScaleLeftWrapped()
             && isScaleProteinAsCdna() == vs.isScaleProteinAsCdna()
+            && isProteinFontAsCdna() == vs.isProteinFontAsCdna()
             && getScaleRightWrapped() == vs.getScaleRightWrapped()
             && isSeqNameItalics() == vs.isSeqNameItalics()
             && isShowAnnotation() == vs.isShowAnnotation()
@@ -1094,4 +1102,16 @@ public class ViewStyle implements ViewStyleI
   {
     this.scaleProteinAsCdna = b;
   }
+
+  @Override
+  public boolean isProteinFontAsCdna()
+  {
+    return proteinFontAsCdna;
+  }
+
+  @Override
+  public void setProteinFontAsCdna(boolean b)
+  {
+    proteinFontAsCdna = b;
+  }
 }
index beee1eb..b0392d4 100644 (file)
@@ -48,35 +48,17 @@ public class AlignmentAnnotationFactory
    * @param counter
    *          provider of feature counts per alignment position
    */
-  public static void newCalculator(FeatureCounterI counter)
+  public static void newCalculator(FeatureSetCounterI counter)
   {
-    // TODO need an interface for AlignFrame by which to access
-    // its AlignViewportI and AlignmentViewPanel
     AlignmentViewPanel currentAlignFrame = Jalview.getCurrentAlignFrame().alignPanel;
-    if (currentAlignFrame != null)
-    {
-      newCalculator(currentAlignFrame.getAlignViewport(),
-              currentAlignFrame, counter);
-    }
-    else
+    if (currentAlignFrame == null)
     {
       System.err
               .println("Can't register calculator as no alignment window has focus");
+      return;
     }
-  }
-
-  /**
-   * Constructs and registers a new alignment annotation worker
-   * 
-   * @param viewport
-   * @param panel
-   * @param counter
-   *          provider of feature counts per alignment position
-   */
-  public static void newCalculator(AlignViewportI viewport,
-          AlignmentViewPanel panel, FeatureCounterI counter)
-  {
-    new ColumnCounterWorker(viewport, panel, counter);
+    new ColumnCounterSetWorker(currentAlignFrame.getAlignViewport(),
+            currentAlignFrame, counter);
   }
 
   /**
@@ -92,8 +74,8 @@ public class AlignmentAnnotationFactory
     AlignFrame currentAlignFrame = Jalview.getCurrentAlignFrame();
     if (currentAlignFrame != null)
     {
-      newCalculator(currentAlignFrame.getViewport(), currentAlignFrame
-              .getAlignPanels().get(0), calculator);
+      new AnnotationWorker(currentAlignFrame.getViewport(),
+              currentAlignFrame.getAlignPanels().get(0), calculator);
     }
     else
     {
@@ -36,16 +36,16 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * A class to compute an alignment annotation with column counts of any
- * properties of interest of positions in an alignment. <br>
+ * A class to compute alignment annotations with column counts for a set of
+ * properties of interest on positions in an alignment. <br>
  * This is designed to be extensible, by supplying to the constructor an object
- * that computes a count for each residue position, based on the residue value
- * and any sequence features at that position.
+ * that computes a vector of counts for each residue position, based on the
+ * residue and and sequence features at that position.
  * 
  */
-class ColumnCounterWorker extends AlignCalcWorker
+class ColumnCounterSetWorker extends AlignCalcWorker
 {
-  FeatureCounterI counter;
+  FeatureSetCounterI counter;
 
   /**
    * Constructor registers the annotation for the given alignment frame
@@ -53,8 +53,8 @@ class ColumnCounterWorker extends AlignCalcWorker
    * @param af
    * @param counter
    */
-  public ColumnCounterWorker(AlignViewportI viewport,
-          AlignmentViewPanel panel, FeatureCounterI counter)
+  public ColumnCounterSetWorker(AlignViewportI viewport,
+          AlignmentViewPanel panel, FeatureSetCounterI counter)
   {
     super(viewport, panel);
     ourAnnots = new ArrayList<AlignmentAnnotation>();
@@ -69,6 +69,7 @@ class ColumnCounterWorker extends AlignCalcWorker
   @Override
   public void run()
   {
+    boolean annotationAdded = false;
     try
     {
       calcMan.notifyStart(this);
@@ -93,7 +94,7 @@ class ColumnCounterWorker extends AlignCalcWorker
       {
         try
         {
-          computeAnnotations();
+          annotationAdded = computeAnnotations();
         } catch (IndexOutOfBoundsException x)
         {
           // probable race condition. just finish and return without any fuss.
@@ -111,7 +112,10 @@ class ColumnCounterWorker extends AlignCalcWorker
 
     if (ap != null)
     {
-      ap.adjustAnnotationHeight();
+      if (annotationAdded)
+      {
+        ap.adjustAnnotationHeight();
+      }
       ap.paintAlignment(true);
     }
 
@@ -120,8 +124,10 @@ class ColumnCounterWorker extends AlignCalcWorker
   /**
    * Scan each column of the alignment to calculate a count by feature type. Set
    * the count as the value of the alignment annotation for that feature type.
+   * 
+   * @return
    */
-  void computeAnnotations()
+  boolean computeAnnotations()
   {
     FeatureRenderer fr = new FeatureRenderer(alignViewport);
     // TODO use the commented out code once JAL-2075 is fixed
@@ -130,56 +136,91 @@ class ColumnCounterWorker extends AlignCalcWorker
     // AlignmentView alignmentView = alignViewport.getAlignmentView(false);
     // AlignmentI alignment = alignmentView.getVisibleAlignment(' ');
 
-    // int width = alignmentView.getWidth();
+    int rows = counter.getNames().length;
+
     int width = alignment.getWidth();
     int height = alignment.getHeight();
-    int[] counts = new int[width];
-    int max = 0;
+    int[][] counts = new int[width][rows];
+    int max[] = new int[rows];
+    for (int crow = 0; crow < rows; crow++)
+    {
+      max[crow] = 0;
+    }
+
+    int[] minC = counter.getMinColour();
+    int[] maxC = counter.getMaxColour();
+    Color minColour = new Color(minC[0], minC[1], minC[2]);
+    Color maxColour = new Color(maxC[0], maxC[1], maxC[2]);
 
     for (int col = 0; col < width; col++)
     {
-      int count = 0;
+      int[] count = counts[col];
+      for (int crow = 0; crow < rows; crow++)
+      {
+        count[crow] = 0;
+      }
       for (int row = 0; row < height; row++)
       {
-        count += countFeaturesAt(alignment, col, row, fr);
+        int[] colcount = countFeaturesAt(alignment, col, row, fr);
+        if (colcount != null)
+        {
+          for (int crow = 0; crow < rows; crow++)
+          {
+            count[crow] += colcount[crow];
+          }
+        }
       }
       counts[col] = count;
-      max = Math.max(count, max);
+      for (int crow = 0; crow < rows; crow++)
+      {
+        max[crow] = Math.max(count[crow], max[crow]);
+      }
     }
 
-    Annotation[] anns = new Annotation[width];
-    /*
-     * add non-zero counts as annotations
-     */
-    for (int i = 0; i < counts.length; i++)
+    boolean annotationAdded = false;
+
+    for (int anrow = 0; anrow < rows; anrow++)
     {
-      int count = counts[i];
-      if (count > 0)
+      Annotation[] anns = new Annotation[width];
+      long rmax = 0;
+      /*
+       * add counts as annotations. zeros are needed since select-by-annotation ignores empty annotation positions
+       */
+      for (int i = 0; i < counts.length; i++)
       {
-        Color color = ColorUtils.getGraduatedColour(count, 0, Color.cyan,
-                max, Color.blue);
+        int count = counts[i][anrow];
+
+        Color color = ColorUtils.getGraduatedColour(count, 0, minColour,
+                max[anrow], maxColour);
         String str = String.valueOf(count);
         anns[i] = new Annotation(str, str, '0', count, color);
+        rmax = Math.max(count, rmax);
       }
-    }
 
-    /*
-     * construct or update the annotation
-     */
-    AlignmentAnnotation ann = alignViewport.getAlignment()
-            .findOrCreateAnnotation(counter.getName(),
-                    counter.getDescription(), false, null, null);
-    ann.description = counter.getDescription();
-    ann.showAllColLabels = true;
-    ann.scaleColLabel = true;
-    ann.graph = AlignmentAnnotation.BAR_GRAPH;
-    ann.annotations = anns;
-    setGraphMinMax(ann, anns);
-    ann.validateRangeAndDisplay();
-    if (!ourAnnots.contains(ann))
-    {
-      ourAnnots.add(ann);
+      /*
+       * construct or update the annotation
+       */
+      String description = counter.getDescriptions()[anrow];
+      if (!alignment.findAnnotation(description).iterator().hasNext())
+      {
+        annotationAdded = true;
+      }
+      AlignmentAnnotation ann = alignment.findOrCreateAnnotation(
+              counter.getNames()[anrow], description, false, null, null);
+      ann.description = description;
+      ann.showAllColLabels = true;
+      ann.scaleColLabel = true;
+      ann.graph = AlignmentAnnotation.BAR_GRAPH;
+      ann.annotations = anns;
+      ann.graphMin = 0f; // minimum always zero count
+      ann.graphMax = rmax; // maximum count from loop over feature columns
+      ann.validateRangeAndDisplay();
+      if (!ourAnnots.contains(ann))
+      {
+        ourAnnots.add(ann);
+      }
     }
+    return annotationAdded;
   }
 
   /**
@@ -188,35 +229,34 @@ class ColumnCounterWorker extends AlignCalcWorker
    * 
    * @param alignment
    * @param col
+   *          (0..)
    * @param row
    * @param fr
    */
-  int countFeaturesAt(AlignmentI alignment, int col, int row,
+  int[] countFeaturesAt(AlignmentI alignment, int col, int row,
           FeatureRenderer fr)
   {
     SequenceI seq = alignment.getSequenceAt(row);
     if (seq == null)
     {
-      return 0;
+      return null;
     }
     if (col >= seq.getLength())
     {
-      return 0;// sequence doesn't extend this far
+      return null;// sequence doesn't extend this far
     }
     char res = seq.getCharAt(col);
     if (Comparison.isGap(res))
     {
-      return 0;
+      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);
-    int count = this.counter.count(String.valueOf(res), features);
+    List<SequenceFeature> features = fr.findFeaturesAtColumn(seq, col + 1);
+    int[] count = this.counter.count(String.valueOf(res), features);
     return count;
   }
 
index 1cce3ff..284f1a1 100644 (file)
@@ -28,7 +28,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ProfilesI;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.ColourSchemeI;
+import jalview.renderer.ResidueShaderI;
 
 public class ConsensusThread extends AlignCalcWorker
 {
@@ -153,11 +153,10 @@ public class ConsensusThread extends AlignCalcWorker
    */
   protected void setColourSchemeConsensus(ProfilesI hconsensus)
   {
-    ColourSchemeI globalColourScheme = alignViewport
-            .getGlobalColourScheme();
-    if (globalColourScheme != null)
+    ResidueShaderI cs = alignViewport.getResidueShading();
+    if (cs != null)
     {
-      globalColourScheme.setConsensus(hconsensus);
+      cs.setConsensus(hconsensus);
     }
   }
 
index e71c4f5..571234c 100644 (file)
@@ -55,7 +55,7 @@ public class ConservationThread extends AlignCalcWorker
     {
       calcMan.notifyStart(this); // updatingConservation = true;
 
-      while (!calcMan.notifyWorking(this))
+      while ((calcMan != null) && (!calcMan.notifyWorking(this)))
       {
         try
         {
@@ -69,7 +69,8 @@ public class ConservationThread extends AlignCalcWorker
           ex.printStackTrace();
         }
       }
-      if (alignViewport.isClosed())
+      if ((alignViewport == null) || (calcMan == null)
+              || (alignViewport.isClosed()))
       {
         abortAndDestroy();
         return;
@@ -114,6 +115,12 @@ public class ConservationThread extends AlignCalcWorker
     }
     calcMan.workerComplete(this);
 
+    if ((alignViewport == null) || (calcMan == null)
+            || (alignViewport.isClosed()))
+    {
+      abortAndDestroy();
+      return;
+    }
     if (ap != null)
     {
       ap.paintAlignment(true);
similarity index 76%
rename from src/jalview/workers/FeatureCounterI.java
rename to src/jalview/workers/FeatureSetCounterI.java
index 3a080ec..e14952f 100644 (file)
@@ -18,6 +18,7 @@
  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
  * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
+
 package jalview.workers;
 
 import jalview.datamodel.SequenceFeature;
@@ -25,15 +26,16 @@ import jalview.datamodel.SequenceFeature;
 import java.util.List;
 
 /**
- * An interface for a type that returns counts of any value of interest at a
- * sequence position that can be determined from the sequence character and any
- * features present at that position
+ * An interface for a type that returns counts (per computed annotation type) of
+ * any value of interest at a sequence position that can be determined from the
+ * sequence character and any features present at that position
  * 
  */
-public interface FeatureCounterI
+public interface FeatureSetCounterI
 {
   /**
-   * Returns a count of some property of interest, for example
+   * Returns counts (per annotation type) of some properties of interest, for
+   * example
    * <ul>
    * <li>the number of variant features at the position</li>
    * <li>the number of Cath features of status 'True Positive'</li>
@@ -46,22 +48,22 @@ public interface FeatureCounterI
    * @param a
    *          list of any sequence features which include the position
    */
-  int count(String residue, List<SequenceFeature> features);
+  int[] count(String residue, List<SequenceFeature> features);
 
   /**
-   * Returns a name for the annotation that this is counting, for use as the
-   * displayed label
+   * Returns names for the annotations that this is counting, for use as the
+   * displayed labels
    * 
    * @return
    */
-  String getName();
+  String[] getNames();
 
   /**
-   * Returns a description for the annotation, for display as a tooltip
+   * Returns descriptions for the annotations, for display as tooltips
    * 
    * @return
    */
-  String getDescription();
+  String[] getDescriptions();
 
   /**
    * Returns the colour (as [red, green, blue] values in the range 0-255) to use
index fd511dc..8c8a717 100644 (file)
@@ -26,7 +26,6 @@ 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;
@@ -697,28 +696,13 @@ 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(startShift);
             }
           }
         }
index 676a4b6..4d3dd2f 100644 (file)
@@ -617,8 +617,8 @@ public class DasSequenceFeatureFetcher
     }
     af.getFeatureRenderer().featuresAdded();
 
-    int start = af.getViewport().getStartSeq();
-    int end = af.getViewport().getEndSeq();
+    int start = af.getViewport().getRanges().getStartSeq();
+    int end = af.getViewport().getRanges().getEndSeq();
     int index;
     for (index = start; index < end; index++)
     {
index 3afe8ec..7261cba 100644 (file)
@@ -28,8 +28,9 @@ 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.ws.ebi.EBIFetchClient;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
 
@@ -254,16 +255,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(),
+                uf.getDescription(), uf.getBegin(), uf.getEnd(), "Uniprot");
+        copy.setStatus(uf.getStatus());
+        sequence.addSequenceFeature(copy);
       }
     }
     for (DBRefEntry dbr : dbRefs)
index b184ff2..e0f7f70 100644 (file)
@@ -69,6 +69,7 @@ public class DasSourceRegistry implements DasSourceRegistryI,
     return loadingDasSources;
   }
 
+  @Override
   public String getDasRegistryURL()
   {
     String registry = jalview.bin.Cache.getDefault("DAS_REGISTRY_URL",
@@ -150,9 +151,8 @@ public class DasSourceRegistry implements DasSourceRegistryI,
       return dsrc;
     } catch (Exception ex)
     {
-      System.err.println("Failed to contact DAS1 registry at "
-              + registryURL);
-      ex.printStackTrace();
+      System.out.println("DAS1 registry at " + registryURL
+              + " no longer exists");
       return new ArrayList<jalviewSourceI>();
     }
   }
index e685d00..b0210d8 100644 (file)
@@ -27,7 +27,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
@@ -114,7 +114,7 @@ class JPredThread extends JWS1Thread implements WSClientI
         return null;
       }
       AlignmentI al = null;
-      ColumnSelection alcsel = null;
+      HiddenColumns alhidden = null;
       int FirstSeq = -1; // the position of the query sequence in Alignment al
 
       JpredResult result = (JpredResult) this.result;
@@ -141,10 +141,10 @@ class JPredThread extends JWS1Thread implements WSClientI
           if (predMap != null)
           {
             Object[] alandcolsel = input
-                    .getAlignmentAndColumnSelection(getGapChar());
+                    .getAlignmentAndHiddenColumns(getGapChar());
             sqs = (SequenceI[]) alandcolsel[0];
             al = new Alignment(sqs);
-            alcsel = (ColumnSelection) alandcolsel[1];
+            alhidden = (HiddenColumns) alandcolsel[1];
           }
           else
           {
@@ -192,7 +192,7 @@ class JPredThread extends JWS1Thread implements WSClientI
         {
           char gc = getGapChar();
           SequenceI[] sqs = (SequenceI[]) input
-                  .getAlignmentAndColumnSelection(gc)[0];
+                  .getAlignmentAndHiddenColumns(gc)[0];
           if (this.msaIndex >= sqs.length)
           {
             throw new Error(
@@ -237,7 +237,7 @@ class JPredThread extends JWS1Thread implements WSClientI
           {
             // Adjust input view for gaps
             // propagate insertions into profile
-            alcsel = ColumnSelection.propagateInsertions(profileseq, al,
+            alhidden = HiddenColumns.propagateInsertions(profileseq, al,
                     input);
           }
         }
@@ -252,7 +252,7 @@ class JPredThread extends JWS1Thread implements WSClientI
                   alant.sequenceRef);
         }
       }
-      return new Object[] { al, alcsel }; // , FirstSeq, noMsa};
+      return new Object[] { al, alhidden }; // , FirstSeq, noMsa};
     }
 
     /**
@@ -625,7 +625,7 @@ class JPredThread extends JWS1Thread implements WSClientI
             if (res[1] != null)
             {
               af = new AlignFrame((Alignment) res[0],
-                      (ColumnSelection) res[1], AlignFrame.DEFAULT_WIDTH,
+                      (HiddenColumns) res[1], AlignFrame.DEFAULT_WIDTH,
                       AlignFrame.DEFAULT_HEIGHT);
             }
             else
@@ -651,7 +651,8 @@ class JPredThread extends JWS1Thread implements WSClientI
              */
 
             af = new AlignFrame((Alignment) res[0],
-                    (ColumnSelection) res[1], AlignFrame.DEFAULT_WIDTH,
+ (HiddenColumns) res[1],
+                    AlignFrame.DEFAULT_WIDTH,
                     AlignFrame.DEFAULT_HEIGHT);
           }
           Desktop.addInternalFrame(af, altitle, AlignFrame.DEFAULT_WIDTH,
index e4247f7..72d41c9 100644 (file)
@@ -26,7 +26,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
 import jalview.datamodel.AlignmentView;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
@@ -632,7 +632,7 @@ class MsaWSThread extends JWS1Thread implements WSClientI
       orders[j] = null;
     }
     SequenceI[] alignment = (SequenceI[]) newview[0];
-    ColumnSelection columnselection = (ColumnSelection) newview[1];
+    HiddenColumns hidden = (HiddenColumns) newview[1];
     Alignment al = new Alignment(alignment);
     // TODO: add 'provenance' property to alignment from the method notes
     // accompanying each subjob
@@ -646,7 +646,7 @@ class MsaWSThread extends JWS1Thread implements WSClientI
 
     if (newFrame)
     {
-      AlignFrame af = new AlignFrame(al, columnselection,
+      AlignFrame af = new AlignFrame(al, hidden,
               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
 
       // initialise with same renderer settings as in parent alignframe.
index b14917e..edc9ae8 100644 (file)
@@ -666,7 +666,7 @@ class SeqSearchWSThread extends JWS1Thread implements WSClientI
               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
       if (nf != null)
       {
-        af.ShowNewickTree(nf, MessageManager.formatMessage(
+        af.showNewickTree(nf, MessageManager.formatMessage(
                 "label.tree_from", new String[] { this.alTitle }));
       }
       // initialise with same renderer settings as in parent alignframe.
index 0cf76e0..b877d20 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;
@@ -29,7 +28,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.schemes.FeatureColour;
-import jalview.schemes.UserColourScheme;
+import jalview.util.ColorUtils;
 import jalview.ws.jws2.jabaws2.Jws2Instance;
 import jalview.ws.params.WsParamSetI;
 
@@ -238,14 +237,14 @@ public class AADisorderClient extends JabawsCalcWorker
                 }
                 if (vals.hasNext())
                 {
+                  val = vals.next().floatValue();
                   sf = new SequenceFeature(type[0], type[1],
-                          base + rn.from, base + rn.to, val = vals.next()
-                                  .floatValue(), methodName);
+                          base + rn.from, base + rn.to, val, methodName);
                 }
                 else
                 {
-                  sf = new SequenceFeature(type[0], type[1], null, base
-                          + rn.from, base + rn.to, methodName);
+                  sf = new SequenceFeature(type[0], type[1],
+                          base + rn.from, base + rn.to, methodName);
                 }
                 dseq.addSequenceFeature(sf);
                 if (last != val && !Float.isNaN(last))
@@ -308,7 +307,7 @@ public class AADisorderClient extends JabawsCalcWorker
                 annot.description += "<br/>" + threshNote;
               }
               annot.description += "</html>";
-              Color col = UserColourScheme.createColourFromName(typeName
+              Color col = ColorUtils.createColourFromName(typeName
                       + scr.getMethod());
               for (int p = 0, ps = annot.annotations.length; p < ps; p++)
               {
diff --git a/src/jalview/ws/jws2/JPred301Client.java b/src/jalview/ws/jws2/JPred301Client.java
deleted file mode 100644 (file)
index c15f256..0000000
+++ /dev/null
@@ -1,299 +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.jws2;
-
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.Annotation;
-import jalview.gui.AlignFrame;
-import jalview.ws.jws2.jabaws2.Jws2Instance;
-import jalview.ws.params.ArgumentI;
-import jalview.ws.params.OptionI;
-import jalview.ws.params.WsParamSetI;
-import jalview.ws.uimodel.AlignAnalysisUIText;
-
-import java.awt.Color;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import compbio.data.sequence.FastaSequence;
-import compbio.data.sequence.JpredAlignment;
-import compbio.metadata.Argument;
-
-public class JPred301Client extends JabawsMsaInterfaceAlignCalcWorker
-{
-  /**
-   * 
-   * @return default args for this service when run as dynamic web service
-   */
-  public List<Argument> selectDefaultArgs()
-  {
-    List<ArgumentI> rgs = new ArrayList<ArgumentI>();
-    for (ArgumentI argi : service.getParamStore().getServiceParameters())
-    {
-      if (argi instanceof OptionI)
-      {
-        List<String> o = ((OptionI) argi).getPossibleValues();
-        if (o.contains("-pred-nohits"))
-        {
-          OptionI cpy = ((OptionI) argi).copy();
-          cpy.setValue("-pred-nohits");
-          rgs.add(cpy);
-        }
-      }
-    }
-    return JabaParamStore.getJabafromJwsArgs(rgs);
-  }
-
-  public JPred301Client(Jws2Instance service, AlignFrame alignFrame,
-          WsParamSetI preset, List<Argument> paramset)
-  {
-    super(service, alignFrame, preset, paramset);
-    submitGaps = true;
-    alignedSeqs = true;
-    nucleotidesAllowed = false;
-    proteinAllowed = true;
-    gapMap = new boolean[0];
-    updateParameters(null, selectDefaultArgs());
-  }
-
-  @Override
-  boolean checkValidInputSeqs(boolean dynamic, List<FastaSequence> seqs)
-  {
-    return (seqs.size() > 1);
-  }
-
-  @Override
-  public String getServiceActionText()
-  {
-    return "calculating consensus secondary structure prediction using JPred service";
-  }
-
-  private static Map<String, String[]> jpredRowLabels = new HashMap<String, String[]>();
-
-  private static final Set<String> jpredRes_graph;
-
-  private static final Set<String> jpredRes_ssonly;
-  static
-  {
-    jpredRes_ssonly = new HashSet<String>();
-    jpredRes_ssonly.add("jnetpred".toLowerCase());
-    jpredRes_ssonly.add("jnetpssm".toLowerCase());
-    jpredRes_ssonly.add("jnethmm".toLowerCase());
-    jpredRes_graph = new HashSet<String>();
-    jpredRes_graph.add("jnetconf".toLowerCase());
-    jpredRes_graph.add("jnet burial".toLowerCase());
-  }
-
-  /**
-   * update the consensus annotation from the sequence profile data using
-   * current visualization settings.
-   */
-  @Override
-  public void updateResultAnnotation(boolean immediate)
-  {
-    if (immediate || !calcMan.isWorking(this) && msascoreset != null)
-    {
-      if (msascoreset instanceof compbio.data.sequence.JpredAlignment)
-      {
-        JpredAlignment jpres = (JpredAlignment) msascoreset;
-        int alWidth = alignViewport.getAlignment().getWidth();
-        ArrayList<AlignmentAnnotation> ourAnnot = new ArrayList<AlignmentAnnotation>();
-        char[] sol = new char[jpres.getJpredSequences().get(0).getLength()];
-        boolean firstsol = true;
-        for (FastaSequence fsq : jpres.getJpredSequences())
-        {
-          String[] k = jpredRowLabels.get(fsq.getId());
-          if (k == null)
-          {
-            k = new String[] { fsq.getId(), "JNet Output" };
-          }
-          if (fsq.getId().startsWith("JNETSOL"))
-          {
-            char amnt = (fsq.getId().endsWith("25") ? "3" : fsq.getId()
-                    .endsWith("5") ? "6" : "9").charAt(0);
-            char[] vseq = fsq.getSequence().toCharArray();
-            for (int spos = 0, sposL = fsq.getLength(); spos < sposL; spos++)
-            {
-              if (firstsol)
-              {
-                sol[spos] = '0';
-              }
-              if (vseq[spos] == 'B'
-                      && (sol[spos] == '0' || sol[spos] < amnt))
-              {
-                sol[spos] = amnt;
-              }
-            }
-            firstsol = false;
-          }
-          else
-          {
-            createAnnotationRowFromString(
-                    ourAnnot,
-                    getCalcId(),
-                    alWidth,
-                    k[0],
-                    k[1],
-                    jpredRes_graph.contains(fsq.getId()) ? AlignmentAnnotation.BAR_GRAPH
-                            : AlignmentAnnotation.NO_GRAPH, 0f, 9f,
-                    fsq.getSequence());
-          }
-
-        }
-        createAnnotationRowFromString(
-                ourAnnot,
-                getCalcId(),
-                alWidth,
-                "Jnet Burial",
-                "<html>Prediction of Solvent Accessibility<br/>levels are<ul><li>0 - Exposed</li><li>3 - 25% or more S.A. accessible</li><li>6 - 5% or more S.A. accessible</li><li>9 - Buried (<5% exposed)</li></ul>",
-                AlignmentAnnotation.BAR_GRAPH, 0f, 9f, new String(sol));
-        for (FastaSequence fsq : jpres.getSequences())
-        {
-          if (fsq.getId().equalsIgnoreCase("QUERY"))
-          {
-            createAnnotationRowFromString(ourAnnot, getCalcId(), alWidth,
-                    "Query", "JPred Reference Sequence",
-                    AlignmentAnnotation.NO_GRAPH, 0f, 0f, fsq.getSequence());
-          }
-        }
-        if (ourAnnot.size() > 0)
-        {
-          updateOurAnnots(ourAnnot);
-        }
-      }
-    }
-  }
-
-  private void createAnnotationRowFromString(
-          ArrayList<AlignmentAnnotation> ourAnnot, String calcId,
-          int alWidth, String label, String descr, int rowType, float min,
-          float max, String jpredPrediction)
-  {
-    // simple annotation row
-    AlignmentAnnotation annotation = alignViewport.getAlignment()
-            .findOrCreateAnnotation(label, calcId, true, null, null);
-    if (alWidth == gapMap.length) // scr.getScores().size())
-    {
-      annotation.label = new String(label);
-      annotation.description = new String(descr);
-      annotation.graph = rowType;
-      annotation.graphMin = min;
-      annotation.graphMax = max;
-      if (constructAnnotationFromString(annotation, jpredPrediction,
-              alWidth, rowType))
-      {
-        // created a valid annotation from the data
-        ourAnnot.add(annotation);
-        // annotation.validateRangeAndDisplay();
-      }
-    }
-  }
-
-  private boolean constructAnnotationFromString(
-          AlignmentAnnotation annotation, String sourceData, int alWidth,
-          int rowType)
-  {
-    if (sourceData.length() == 0 && alWidth > 0)
-    {
-      return false;
-    }
-    Annotation[] elm = new Annotation[alWidth];
-    boolean ssOnly = jpredRes_ssonly.contains(annotation.label
-            .toLowerCase());
-    boolean graphOnly = rowType != AlignmentAnnotation.NO_GRAPH;
-    if (!ssOnly && !graphOnly)
-    {
-      // for burial 'B'
-      annotation.showAllColLabels = true;
-    }
-
-    for (int i = 0, iSize = sourceData.length(); i < iSize; i++)
-    {
-      char annot = sourceData.charAt(i);
-      // if we're at a gapped column then skip to next ungapped position
-      if (gapMap != null && gapMap.length > 0)
-      {
-        while (!gapMap[i])
-        {
-          elm[i++] = new Annotation("", "", ' ', Float.NaN);
-        }
-      }
-      switch (rowType)
-      {
-      case AlignmentAnnotation.NO_GRAPH:
-        elm[i] = ssOnly ? new Annotation("", "", annot, Float.NaN,
-                colourSS(annot)) : new Annotation("" + annot, "" + annot,
-                '\0', Float.NaN);
-        break;
-      default:
-        try
-        {
-          elm[i] = new Annotation("" + annot, "" + annot, annot,
-                  Integer.valueOf("" + annot));
-        } catch (Exception x)
-        {
-          System.err.println("Expected numeric value in character '"
-                  + annot + "'");
-        }
-      }
-    }
-
-    annotation.annotations = elm;
-    annotation.belowAlignment = true;
-    annotation.validateRangeAndDisplay();
-    return true;
-  }
-
-  private Color colourSS(char annot)
-  {
-    switch (annot)
-    {
-    case 'H':
-      return jalview.renderer.AnnotationRenderer.HELIX_COLOUR;
-    case 'E':
-      return jalview.renderer.AnnotationRenderer.SHEET_COLOUR;
-    }
-    return jalview.renderer.AnnotationRenderer.GLYPHLINE_COLOR;
-  }
-
-  @Override
-  public String getCalcId()
-  {
-    return CALC_ID;
-  }
-
-  private static String CALC_ID = "jabaws21.JPred3Cons";
-
-  public static AlignAnalysisUIText getAlignAnalysisUITest()
-  {
-    return new AlignAnalysisUIText(
-            compbio.ws.client.Services.JpredWS.toString(),
-            jalview.ws.jws2.JPred301Client.class, CALC_ID, false, true,
-            true, "JPred Consensus",
-            "When checked, JPred consensus is updated automatically.",
-            "Change JPred Settings...",
-            "Modify settings for JPred calculations.");
-  }
-}
index 1b2c708..12a08a0 100644 (file)
@@ -67,7 +67,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
   /*
    * the .jalview_properties entry for JWS2 URLS
    */
-  final static String JWS2HOSTURLS = "JWS2HOSTURLS";
+  private final static String JWS2HOSTURLS = "JWS2HOSTURLS";
 
   /*
    * Singleton instance
@@ -85,12 +85,17 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
   private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
           this);
 
-  Vector<String> invalidServiceUrls = null, urlsWithoutServices = null,
-          validServiceUrls = null;
+  private Vector<String> invalidServiceUrls = null;
 
-  boolean running = false, aborted = false;
+  private Vector<String> urlsWithoutServices = null;
 
-  Thread oldthread = null;
+  private Vector<String> validServiceUrls = null;
+
+  private volatile boolean running = false;
+
+  private volatile boolean aborted = false;
+
+  private Thread oldthread = null;
 
   /**
    * holds list of services.
@@ -143,9 +148,9 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
   public void setAborted(boolean aborted)
   {
     this.aborted = aborted;
-
   }
 
+  @Override
   public void run()
   {
 
@@ -167,6 +172,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
         {
         }
       }
+      aborted = false;
       Cache.log.debug("Old discovery thread has finished.");
     }
     running = true;
@@ -179,7 +185,8 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
       ignoredServices.add(ignored);
     }
 
-    changeSupport.firePropertyChange("services", services, new Vector());
+    changeSupport.firePropertyChange("services", services,
+            new Vector<Jws2Instance>());
     oldthread = Thread.currentThread();
     try
     {
@@ -263,7 +270,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
     if (!aborted)
     {
       // resort services according to order found in jabaws service list
-      // also ensure servics for each host are ordered in same way.
+      // also ensure services for each host are ordered in same way.
 
       if (services != null && services.size() > 0)
       {
@@ -290,7 +297,8 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
     }
     oldthread = null;
     running = false;
-    changeSupport.firePropertyChange("services", new Vector(), services);
+    changeSupport.firePropertyChange("services",
+            new Vector<Jws2Instance>(), services);
   }
 
   /**
@@ -321,7 +329,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
     service.hasParameters();
     if (validServiceUrls == null)
     {
-      validServiceUrls = new Vector();
+      validServiceUrls = new Vector<String>();
     }
     validServiceUrls.add(jwsservers);
   }
@@ -330,6 +338,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
    * attach all available web services to the appropriate submenu in the given
    * JMenu
    */
+  @Override
   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
   {
     // dynamically regenerate service list.
@@ -348,8 +357,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
     {
       return;
     }
-    boolean byhost = Cache.getDefault("WSMENU_BYHOST", false), bytype = Cache
-            .getDefault("WSMENU_BYTYPE", false);
+
     /**
      * eventually, JWS2 services will appear under the same align/etc submenus.
      * for moment we keep them separate.
@@ -442,27 +450,19 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
             {
               new Thread(new Runnable()
               {
+                @Override
                 public void run()
                 {
                   setPreferredServiceFor(alignFrame, sv.serviceType,
                           sv.action, sv);
                   changeSupport.firePropertyChange("services",
-                          new Vector(), services);
+                          new Vector<Jws2Instance>(), services);
                 };
               }).start();
 
             }
           });
         }
-        /*
-         * hitm.addActionListener(new ActionListener() {
-         * 
-         * @Override public void actionPerformed(ActionEvent arg0) { new
-         * Thread(new Runnable() {
-         * 
-         * @Override public void run() { new SetPreferredServer(alignFrame,
-         * service.serviceType, service.action); } }).start(); } });
-         */
       }
     }
   }
@@ -481,7 +481,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
      * for moment we keep them separate.
      */
     JMenu atpoint;
-    MsaWSClient msacl = new MsaWSClient();
+
     List<String> hostLabels = new ArrayList<String>();
     Hashtable<String, String> lasthostFor = new Hashtable<String, String>();
     Hashtable<String, ArrayList<Jws2Instance>> hosts = new Hashtable<String, ArrayList<Jws2Instance>>();
@@ -590,6 +590,7 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
             new PropertyChangeListener()
             {
 
+              @Override
               public void propertyChange(PropertyChangeEvent evt)
               {
                 if (getDiscoverer().services != null)
@@ -765,6 +766,22 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
     return true;
   }
 
+  public boolean restart()
+  {
+    synchronized (this)
+    {
+      if (running)
+      {
+        aborted = true;
+      }
+      else
+      {
+        running = true;
+      }
+      return aborted;
+    }
+  }
+
   /**
    * Start a fresh discovery thread and notify the given object when we're
    * finished. Any known existing threads will be killed before this one is
@@ -775,6 +792,16 @@ public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
    */
   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
   {
+    /*    if (restart())
+        {
+          return;
+        }
+        else
+        {
+          Thread thr = new Thread(this);
+          thr.start();
+        }
+       */
     if (isRunning())
     {
       setAborted(true);
index e425624..2187f46 100644 (file)
@@ -26,7 +26,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
 import jalview.datamodel.AlignmentView;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
@@ -961,7 +961,7 @@ class MsaWSThread extends AWS2Thread implements WSClientI
       orders[j] = null;
     }
     SequenceI[] alignment = (SequenceI[]) newview[0];
-    ColumnSelection columnselection = (ColumnSelection) newview[1];
+    HiddenColumns hidden = (HiddenColumns) newview[1];
     Alignment al = new Alignment(alignment);
     // TODO: add 'provenance' property to alignment from the method notes
     if (lastProgram != null)
@@ -979,7 +979,7 @@ class MsaWSThread extends AWS2Thread implements WSClientI
 
     if (newFrame)
     {
-      displayInNewFrame(al, alorders, columnselection);
+      displayInNewFrame(al, alorders, hidden);
 
     }
     else
@@ -1000,9 +1000,9 @@ class MsaWSThread extends AWS2Thread implements WSClientI
    * @param columnselection
    */
   protected void displayInNewFrame(AlignmentI al,
-          List<AlignmentOrder> alorders, ColumnSelection columnselection)
+          List<AlignmentOrder> alorders, HiddenColumns hidden)
   {
-    AlignFrame af = new AlignFrame(al, columnselection,
+    AlignFrame af = new AlignFrame(al, hidden,
             AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
 
     // initialise with same renderer settings as in parent alignframe.
index 7c8395f..671b5f1 100644 (file)
@@ -21,7 +21,6 @@
 package jalview.ws.jws2.jabaws2;
 
 import jalview.ws.jws2.AAConClient;
-import jalview.ws.jws2.JPred301Client;
 import jalview.ws.jws2.RNAalifoldClient;
 import jalview.ws.uimodel.AlignAnalysisUIText;
 
@@ -51,11 +50,8 @@ public class Jws2InstanceFactory
               AAConClient.getAlignAnalysisUITest());
       aaConGUI.put(compbio.ws.client.Services.RNAalifoldWS.toString(),
               RNAalifoldClient.getAlignAnalysisUITest());
-      // disable the JPred301 client in jalview ...
+      // ignore list for JABAWS services not supported in jalview ...
       ignoreGUI = new HashSet<String>();
-      ignoreGUI.add(compbio.ws.client.Services.JpredWS.toString());
-      aaConGUI.put(compbio.ws.client.Services.JpredWS.toString(),
-              JPred301Client.getAlignAnalysisUITest());
     }
   }
 
index 7fbae89..27f5271 100644 (file)
@@ -259,7 +259,8 @@ public class RestClient extends WSClient implements WSClientI,
         {
           // intersect groups with selected region
           _input = new AlignmentView(av.getAlignment(),
-                  av.getColumnSelection(), av.getSelectionGroup(),
+ av.getAlignment()
+                  .getHiddenColumns(), av.getSelectionGroup(),
                   av.hasHiddenColumns(), true, true);
           viewTitle = MessageManager.formatMessage(
                   "label.select_visible_region_of",
@@ -272,7 +273,8 @@ public class RestClient extends WSClient implements WSClientI,
         {
           // use selected region to partition alignment
           _input = new AlignmentView(av.getAlignment(),
-                  av.getColumnSelection(), av.getSelectionGroup(),
+ av.getAlignment()
+                  .getHiddenColumns(), av.getSelectionGroup(),
                   av.hasHiddenColumns(), false, true);
         }
         viewTitle = MessageManager.formatMessage(
@@ -286,7 +288,8 @@ public class RestClient extends WSClient implements WSClientI,
       {
         // just take selected region intersection
         _input = new AlignmentView(av.getAlignment(),
-                av.getColumnSelection(), av.getSelectionGroup(),
+ av.getAlignment()
+                .getHiddenColumns(), av.getSelectionGroup(),
                 av.hasHiddenColumns(), true, true);
         viewTitle = MessageManager.formatMessage(
                 "label.select_visible_region_of",
@@ -300,7 +303,8 @@ public class RestClient extends WSClient implements WSClientI,
     {
       // standard alignment view without selection present
       _input = new AlignmentView(av.getAlignment(),
-              av.getColumnSelection(), null, av.hasHiddenColumns(), false,
+ av.getAlignment()
+              .getHiddenColumns(), null, av.hasHiddenColumns(), false,
               true);
       viewTitle = ""
               + (av.hasHiddenColumns() ? (new StringBuffer(" ")
index 75d2cd4..0592426 100644 (file)
@@ -26,7 +26,7 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
 import jalview.datamodel.Annotation;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
@@ -173,9 +173,13 @@ public class RestJobThread extends AWSThread
   private String getStage(Stage stg)
   {
     if (stg == Stage.SUBMIT)
+    {
       return "submitting ";
+    }
     if (stg == Stage.POLL)
+    {
       return "checking status of ";
+    }
 
     return (" being confused about ");
   }
@@ -609,7 +613,7 @@ public class RestJobThread extends AWSThread
     // total number of distinct alignment sets generated by job set.
     int numAlSets = 0, als = 0;
     List<AlignmentI> destAls = new ArrayList<AlignmentI>();
-    List<jalview.datamodel.ColumnSelection> destColsel = new ArrayList<jalview.datamodel.ColumnSelection>();
+    List<jalview.datamodel.HiddenColumns> destColsel = new ArrayList<jalview.datamodel.HiddenColumns>();
     List<List<NewickFile>> trees = new ArrayList<List<NewickFile>>();
 
     do
@@ -715,7 +719,7 @@ public class RestJobThread extends AWSThread
           RestJob rj = (RestJob) jobs[nrj];
           int contigs[] = input.getVisibleContigs();
           AlignmentI destAl = null;
-          jalview.datamodel.ColumnSelection destCs = null;
+          jalview.datamodel.HiddenColumns destHCs = null;
           // Resolve destAl for this data.
           if (als == 0 && rj.isInputContextModified())
           {
@@ -731,7 +735,7 @@ public class RestJobThread extends AWSThread
               if (!restClient.isAlignmentModified() && merge)
               {
                 destAl = restClient.av.getAlignment();
-                destCs = restClient.av.getColumnSelection();
+                destHCs = restClient.av.getAlignment().getHiddenColumns();
                 resultDest
                         .add(restClient.isShowResultsInNewView() ? AddDataTo.newView
                                 : AddDataTo.currentView);
@@ -742,15 +746,15 @@ public class RestJobThread extends AWSThread
                 newAlignment = true;
                 // recreate the input alignment data
                 Object[] idat = input
-                        .getAlignmentAndColumnSelection(gapCharacter);
+                        .getAlignmentAndHiddenColumns(gapCharacter);
                 destAl = new Alignment((SequenceI[]) idat[0]);
-                destCs = (ColumnSelection) idat[1];
+                destHCs = (HiddenColumns) idat[1];
                 resultDest.add(AddDataTo.newAlignment);
                 // but do not add to the alignment panel list - since we need to
                 // create a whole new alignFrame set.
               }
               destAls.add(destAl);
-              destColsel.add(destCs);
+              destColsel.add(destHCs);
             }
           }
           else
@@ -769,7 +773,7 @@ public class RestJobThread extends AWSThread
               // recover reference to last alignment created for this rest frame
               // ready for extension
               destAl = destAls.get(als);
-              destCs = destColsel.get(als);
+              destHCs = destColsel.get(als);
             }
             else
             {
@@ -798,12 +802,12 @@ public class RestJobThread extends AWSThread
                 newview = input.getUpdatedView(rseqs, orders, gapCharacter);
               }
               destAl = new Alignment((SequenceI[]) newview[0]);
-              destCs = (ColumnSelection) newview[1];
+              destHCs = (HiddenColumns) newview[1];
               newAlignment = true;
               // TODO create alignment from result data with propagated
               // references.
               destAls.add(destAl);
-              destColsel.add(destCs);
+              destColsel.add(destHCs);
               resultDest.add(AddDataTo.newAlignment);
               throw new Error(
                       MessageManager
@@ -1043,7 +1047,7 @@ public class RestJobThread extends AWSThread
     for (AddDataTo action : resultDest)
     {
       AlignmentI destal;
-      ColumnSelection destcs;
+      HiddenColumns destcs;
       String alTitle = MessageManager.formatMessage(
               "label.webservice_job_title_on", new String[] {
                   restClient.service.details.Action,
index a8258e2..b85073f 100644 (file)
@@ -108,8 +108,8 @@ public class Alignment extends InputType
   @Override
   public List<String> getURLEncodedParameter()
   {
-    ArrayList<String> prms = new ArrayList<String>();
-    prms.add("format='" + format + "'");
+    List<String> prms = new ArrayList<String>();
+    prms.add("format='" + format.getName() + "'");
     if (type != null)
     {
       prms.add("type='" + type.toString() + "'");
index ea65125..2ff4a8b 100644 (file)
@@ -21,6 +21,8 @@
 package jalview.ws.sifts;
 
 import jalview.analysis.AlignSeq;
+import jalview.analysis.scoremodels.ScoreMatrix;
+import jalview.analysis.scoremodels.ScoreModels;
 import jalview.api.DBRefEntryI;
 import jalview.api.SiftsClientI;
 import jalview.datamodel.DBRefEntry;
@@ -575,18 +577,8 @@ public class SiftsClient implements SiftsClientI
                   .equalsIgnoreCase(seqCoordSys.getName())
                   && isAccessionMatched(cRefDb.getDbAccessionId()))
           {
-            String resNumIndexString = cRefDb.getDbResNum()
-                    .equalsIgnoreCase("None") ? String.valueOf(UNASSIGNED)
-                    : cRefDb.getDbResNum();
-            try
-            {
-              currSeqIndex = Integer.valueOf(resNumIndexString);
-            } catch (NumberFormatException nfe)
-            {
-              currSeqIndex = Integer.valueOf(resNumIndexString
-                      .split("[a-zA-Z]")[0]);
-              continue;
-            }
+            currSeqIndex = getLeadingIntegerValue(
+                    cRefDb.getDbResNum(), UNASSIGNED);
             if (pdbRefDb != null)
             {
               break;// exit loop if pdb and uniprot are already found
@@ -599,19 +591,11 @@ public class SiftsClient implements SiftsClientI
         }
         if (currSeqIndex >= seq.getStart() && currSeqIndex <= seq.getEnd())
         {
-          int resNum;
-          try
-          {
-            resNum = (pdbRefDb == null) ? Integer.valueOf(residue
-                    .getDbResNum()) : Integer.valueOf(pdbRefDb
-                    .getDbResNum());
-          } catch (NumberFormatException nfe)
-          {
-            resNum = (pdbRefDb == null) ? Integer.valueOf(residue
-                    .getDbResNum()) : Integer.valueOf(pdbRefDb
-                    .getDbResNum().split("[a-zA-Z]")[0]);
-            continue;
-          }
+
+          int resNum = (pdbRefDb == null) ? getLeadingIntegerValue(
+                  residue.getDbResNum(), UNASSIGNED)
+                  : getLeadingIntegerValue(pdbRefDb.getDbResNum(),
+                          UNASSIGNED);
 
           if (isResidueObserved(residue)
                   || seqCoordSys == CoordinateSys.UNIPROT)
@@ -634,6 +618,30 @@ public class SiftsClient implements SiftsClientI
   }
 
   /**
+   * Get the leading integer part of a string that begins with an integer.
+   * 
+   * @param input
+   *          - the string input to process
+   * @param failValue
+   *          - value returned if unsuccessful
+   * @return
+   */
+  static int getLeadingIntegerValue(String input, int failValue)
+  {
+    if (input == null)
+    {
+      return failValue;
+    }
+    String[] parts = input.split("(?=\\D)(?<=\\d)");
+    if (parts != null && parts.length > 0 && parts[0].matches("[0-9]+"))
+    {
+      return Integer.valueOf(parts[0]);
+    }
+    return failValue;
+  }
+
+
+  /**
    * 
    * @param chainId
    *          Target chain to populate mapping of its atom positions.
@@ -957,7 +965,7 @@ public class SiftsClient implements SiftsClientI
   }
 
   @Override
-  public StringBuffer getMappingOutput(MappingOutputPojo mp)
+  public StringBuilder getMappingOutput(MappingOutputPojo mp)
           throws SiftsException
   {
     String seqRes = mp.getSeqResidue();
@@ -979,7 +987,7 @@ public class SiftsClient implements SiftsClientI
     int nochunks = ((seqRes.length()) / len)
             + ((seqRes.length()) % len > 0 ? 1 : 0);
     // output mappings
-    StringBuffer output = new StringBuffer();
+    StringBuilder output = new StringBuilder(512);
     output.append(NEWLINE);
     output.append("Sequence \u27f7 Structure mapping details").append(
             NEWLINE);
@@ -1000,6 +1008,7 @@ public class SiftsClient implements SiftsClientI
     output.append(String.valueOf(pdbEnd));
     output.append(NEWLINE).append(NEWLINE);
 
+    ScoreMatrix pam250 = ScoreModels.getInstance().getPam250();
     int matchedSeqCount = 0;
     for (int j = 0; j < nochunks; j++)
     {
@@ -1018,27 +1027,29 @@ public class SiftsClient implements SiftsClientI
       output.append(NEWLINE);
       output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
 
-      // Print out the matching chars
+      /*
+       * Print out the match symbols:
+       * | for exact match (ignoring case)
+       * . if PAM250 score is positive
+       * else a space
+       */
       for (int i = 0; i < len; i++)
       {
         try
         {
           if ((i + (j * len)) < seqRes.length())
           {
-            boolean sameChar = Comparison.isSameResidue(
-                    seqRes.charAt(i + (j * len)),
-                    strRes.charAt(i + (j * len)), false);
-            if (sameChar
-                    && !jalview.util.Comparison.isGap(seqRes.charAt(i
-                            + (j * len))))
+            char c1 = seqRes.charAt(i + (j * len));
+            char c2 = strRes.charAt(i + (j * len));
+            boolean sameChar = Comparison.isSameResidue(c1, c2, false);
+            if (sameChar && !Comparison.isGap(c1))
             {
               matchedSeqCount++;
               output.append("|");
             }
             else if (type.equals("pep"))
             {
-              if (ResidueProperties.getPAM250(seqRes.charAt(i + (j * len)),
-                      strRes.charAt(i + (j * len))) > 0)
+              if (pam250.getPairwiseScore(c1, c2) > 0)
               {
                 output.append(".");
               }
diff --git a/src/jalview/ws/utils/UrlDownloadClient.java b/src/jalview/ws/utils/UrlDownloadClient.java
new file mode 100644 (file)
index 0000000..dcd861a
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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.utils;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+public class UrlDownloadClient
+{
+  public UrlDownloadClient()
+  {
+
+  }
+
+  /**
+   * Download and save a file from a URL
+   * 
+   * @param urlstring
+   *          url to download from, as string
+   * @param outfile
+   *          the name of file to save the URLs to
+   * @throws IOException
+   */
+  public static void download(String urlstring, String outfile)
+          throws IOException
+  {
+    FileOutputStream fos = null;
+    ReadableByteChannel rbc = null;
+    Path temp = null;
+    try
+    {
+      temp = Files.createTempFile(".jalview_", ".tmp");
+
+      URL url = new URL(urlstring);
+      rbc = Channels.newChannel(url.openStream());
+      fos = new FileOutputStream(temp.toString());
+      fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+
+      // copy tempfile to outfile once our download completes
+      // incase something goes wrong
+      Files.copy(temp, Paths.get(outfile),
+              StandardCopyOption.REPLACE_EXISTING);
+    } catch (IOException e)
+    {
+      throw e;
+    } finally
+    {
+      try
+      {
+        if (fos != null)
+        {
+          fos.close();
+        }
+      } catch (IOException e)
+      {
+        System.out
+                .println("Exception while closing download file output stream: "
+                        + e.getMessage());
+      }
+      try
+      {
+        if (rbc != null)
+        {
+          rbc.close();
+        }
+      } catch (IOException e)
+      {
+        System.out.println("Exception while closing download channel: "
+                        + e.getMessage());
+      }
+      try
+      {
+        if (temp != null)
+        {
+          Files.deleteIfExists(temp);
+        }
+      } catch (IOException e)
+      {
+        System.out.println("Exception while deleting download temp file: "
+                        + e.getMessage());
+      }
+    }
+  }
+}
index 7132939..defcdbc 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;
@@ -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)
index 4cb5329..837e970 100644 (file)
  */
 package jalview.analysis;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import jalview.datamodel.Sequence;
 import jalview.gui.JvOptionPane;
 
 import org.testng.annotations.BeforeClass;
@@ -45,10 +47,32 @@ public class AlignSeqTest
     assertNull(AlignSeq.extractGaps(null, "ACG"));
     assertNull(AlignSeq.extractGaps("-. ", null));
 
-    assertEquals(" AC-G.T", AlignSeq.extractGaps("", " AC-G.T"));
-    assertEquals("AC-G.T", AlignSeq.extractGaps(" ", " AC-G.T"));
-    assertEquals("ACG.T", AlignSeq.extractGaps(" -", " AC-G.T"));
-    assertEquals("ACGT", AlignSeq.extractGaps(" -.", " AC-G.T ."));
-    assertEquals(" ACG.T", AlignSeq.extractGaps("-", " AC-G.T"));
+    assertEquals(AlignSeq.extractGaps("", " AC-G.T"), " AC-G.T");
+    assertEquals(AlignSeq.extractGaps(" ", " AC-G.T"), "AC-G.T");
+    assertEquals(AlignSeq.extractGaps(" -", " AC-G.T"), "ACG.T");
+    assertEquals(AlignSeq.extractGaps(" -.", " AC-G.T ."), "ACGT");
+    assertEquals(AlignSeq.extractGaps("-", " AC-G.T"), " ACG.T");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testIndexEncode_nucleotide()
+  {
+    AlignSeq as = new AlignSeq(new Sequence("s1", "TTAG"), new Sequence(
+            "s2", "ACGT"), AlignSeq.DNA);
+    int[] expected = new int[] { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
+        7, 7, 8, 8, 9, 9, -1, -1, 10, -1 };
+    String s = "aAcCgGtTuUiIxXrRyYnN .-?";
+    assertArrayEquals(expected, as.indexEncode(s));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testIndexEncode_peptide()
+  {
+    AlignSeq as = new AlignSeq(new Sequence("s1", "PFY"), new Sequence(
+            "s2", "RQW"), AlignSeq.PEP);
+    int[] expected = new int[] { 0, 0, 1, 1, 2, 2, 21, 21, 22, 22, -1, 23,
+        -1, -1, -1 };
+    String s = "aArRnNzZxX *.-?";
+    assertArrayEquals(expected, as.indexEncode(s));
   }
 }
diff --git a/test/jalview/analysis/AlignmentSorterTest.java b/test/jalview/analysis/AlignmentSorterTest.java
new file mode 100644 (file)
index 0000000..3b9be23
--- /dev/null
@@ -0,0 +1,131 @@
+package jalview.analysis;
+
+import static org.testng.Assert.assertSame;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+
+import java.util.Arrays;
+import java.util.List;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class AlignmentSorterTest
+{
+  @Test(groups = "Functional")
+  public void testSortByFeature_score()
+  {
+    SequenceI seq1 = new Sequence("Seq1", "ABC--D-EFGHIJ");
+    SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ");
+    SequenceI seq3 = new Sequence("Seq3", "ABCDE-FGHIJ");
+    SequenceI seq4 = new Sequence("Seq4", "ABCDEFGHIJ");
+    SequenceI[] seqs = new SequenceI[] { seq1, seq2, seq3, seq4 };
+    AlignmentI al = new Alignment(seqs);
+    al.setDataset(null);
+
+    /*
+     * sort with no score features does nothing
+     */
+    PA.setValue(AlignmentSorter.class, "sortByFeatureCriteria", null);
+
+    AlignmentSorter.sortByFeature(null, null, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
+    assertSame(al.getSequenceAt(0), seq1);
+    assertSame(al.getSequenceAt(1), seq2);
+    assertSame(al.getSequenceAt(2), seq3);
+    assertSame(al.getSequenceAt(3), seq4);
+
+    /*
+     * add score and non-score features
+     * seq1 Cath(2.0) Pfam(4.0) average 3.0
+     * seq2 Cath(2.5) Metal(NaN) average 2.5
+     * seq3 KD(-4), KD(3.0) average -0.5
+     * seq4 Helix(NaN) - should sort as if largest score
+     */
+    seq1.addSequenceFeature(new SequenceFeature("Cath", "", 2, 3, 2.0f,
+            "g1"));
+    seq1.addSequenceFeature(new SequenceFeature("Pfam", "", 4, 5, 4.0f,
+            "g2"));
+    seq2.addSequenceFeature(new SequenceFeature("Cath", "", 2, 3, 2.5f,
+            "g3"));
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 2, 3,
+            Float.NaN, "g4"));
+    seq3.addSequenceFeature(new SequenceFeature("kD", "", 2, 3, -4f, "g5"));
+    seq3.addSequenceFeature(new SequenceFeature("kD", "", 5, 6, 3.0f, "g6"));
+    seq4.addSequenceFeature(new SequenceFeature("Helix", "", 2, 3,
+            Float.NaN, "g7"));
+
+    /*
+     * 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, "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
+    assertSame(al.getSequenceAt(0), seq4); // maximum 'score'
+
+    /*
+     * repeat sort toggles order - now ascending
+     */
+    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
+    assertSame(al.getSequenceAt(3), seq4);
+
+    /*
+     * specify features, excluding Pfam
+     * seq1 average is now 2.0
+     * next sort is ascending (not toggled) as for a different feature set
+     */
+    List<String> types = Arrays.asList(new String[] { "Cath", "kD" });
+    AlignmentSorter.sortByFeature(types, null, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
+    assertSame(al.getSequenceAt(0), seq3); // -0.5
+    assertSame(al.getSequenceAt(1), seq1); // 2.0
+    assertSame(al.getSequenceAt(2), seq2); // 2.5
+    assertSame(al.getSequenceAt(3), seq4);
+
+    /*
+     * specify groups, excluding g5 (kD -4 score)
+     * seq3 average is now 3.0
+     * next sort is ascending (not toggled) as for a different group spec
+     */
+    List<String> groups = Arrays.asList(new String[] { "g1", "g2", "g3",
+        "g6" });
+    AlignmentSorter.sortByFeature(types, groups, 0, al.getWidth(), al,
+            AlignmentSorter.FEATURE_SCORE);
+    assertSame(al.getSequenceAt(0), seq1); // 2.0
+    assertSame(al.getSequenceAt(1), seq2); // 2.5
+    assertSame(al.getSequenceAt(2), seq3); // 3.0
+    assertSame(al.getSequenceAt(3), seq4);
+
+    /*
+     * limit to columns 0-4, excluding 2nd feature of seq1 and seq3
+     * seq1 is now 2.0, seq3 is now -4
+     */
+    // fails because seq1.findPosition(4) returns 4
+    // although residue 4 is in column 5! - JAL-2544
+    AlignmentSorter.sortByFeature(null, null, 0, 4, al,
+            AlignmentSorter.FEATURE_SCORE);
+    assertSame(al.getSequenceAt(0), seq3); // -4
+    assertSame(al.getSequenceAt(1), seq1); // 2.0
+    assertSame(al.getSequenceAt(2), seq2); // 2.5
+    assertSame(al.getSequenceAt(3), seq4);
+  }
+
+  @Test(groups = "Functional")
+  public void testSortByFeature_density()
+  {
+    // TODO
+  }
+}
index bada3ca..4439bb9 100644 (file)
@@ -40,6 +40,7 @@ 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;
@@ -1179,12 +1180,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 +1490,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 +1552,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 +1585,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());
@@ -2078,24 +2079,29 @@ public class AlignmentUtilsTests
      * var6 P -> H COSMIC
      * var6 P -> R COSMIC
      */
-    SequenceFeature[] sfs = peptide.getSequenceFeatures();
-    assertEquals(5, sfs.length);
+    List<SequenceFeature> sfs = peptide.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(sfs, true);
+    assertEquals(5, 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)
+     */
+    SequenceFeature sf = sfs.get(0);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
-    assertEquals("p.Lys1Glu", sf.getDescription());
-    assertEquals("var1.125A>G", sf.getValue("ID"));
-    assertNull(sf.getValue("clinical_significance"));
-    assertEquals("ID=var1.125A>G", sf.getAttributes());
+    assertEquals("p.Lys1Asn", sf.getDescription());
+    assertEquals("var4", sf.getValue("ID"));
+    assertEquals("Benign", sf.getValue("clinical_significance"));
+    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
     assertEquals(1, sf.links.size());
-    // link to variation is urlencoded
     assertEquals(
-            "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
+            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
-    sf = sfs[1];
+    sf = sfs.get(1);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
     assertEquals("p.Lys1Gln", sf.getDescription());
@@ -2108,43 +2114,44 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(dbSnp, sf.getFeatureGroup());
 
-    sf = sfs[2];
+    sf = sfs.get(2);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
-    assertEquals("p.Lys1Asn", sf.getDescription());
-    assertEquals("var4", sf.getValue("ID"));
-    assertEquals("Benign", sf.getValue("clinical_significance"));
-    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
+    assertEquals("p.Lys1Glu", sf.getDescription());
+    assertEquals("var1.125A>G", sf.getValue("ID"));
+    assertNull(sf.getValue("clinical_significance"));
+    assertEquals("ID=var1.125A>G", sf.getAttributes());
     assertEquals(1, sf.links.size());
+    // link to variation is urlencoded
     assertEquals(
-            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            "p.Lys1Glu var1.125A>G|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var1.125A%3EG",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
-    // var5 generates two distinct protein variant features
-    sf = sfs[3];
+    sf = sfs.get(3);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
-    assertEquals("p.Pro3His", sf.getDescription());
+    assertEquals("p.Pro3Arg", sf.getDescription());
     assertEquals("var6", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
     assertEquals("ID=var6;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 var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
 
-    sf = sfs[4];
+    // var5 generates two distinct protein variant features
+    sf = sfs.get(4);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
-    assertEquals("p.Pro3Arg", sf.getDescription());
+    assertEquals("p.Pro3His", sf.getDescription());
     assertEquals("var6", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
     assertEquals("ID=var6;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 var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
   }
index 2e21d9c..d2fa99a 100644 (file)
@@ -28,7 +28,7 @@ import jalview.api.AlignViewportI;
 import jalview.datamodel.AlignedCodon;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignViewport;
@@ -133,7 +133,7 @@ public class DnaTest
     AlignmentI alf = new FormatAdapter().readFile(
             JAL_1312_example_align_fasta, DataSourceType.PASTE,
             FileFormat.Fasta);
-    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(alf, cs);
     Dna dna = new Dna(av, new int[] { 0, alf.getWidth() - 1 });
     AlignmentI translated = dna.translateCdna();
@@ -157,7 +157,7 @@ public class DnaTest
     int vwidth = 15;
     for (int ipos = 0; ipos + vwidth < alf.getWidth(); ipos += vwidth)
     {
-      ColumnSelection cs = new ColumnSelection();
+      HiddenColumns cs = new HiddenColumns();
       if (ipos > 0)
       {
         cs.hideColumns(0, ipos - 1);
@@ -188,7 +188,7 @@ public class DnaTest
   {
     AlignmentI alf = new FormatAdapter().readFile(fasta,
             DataSourceType.PASTE, FileFormat.Fasta);
-    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(alf, cs);
     Dna dna = new Dna(av, new int[] { 0, alf.getWidth() - 1 });
     AlignmentI translated = dna.translateCdna();
@@ -208,7 +208,7 @@ public class DnaTest
   {
     AlignmentI alf = new FormatAdapter().readFile(fasta,
             DataSourceType.PASTE, FileFormat.Fasta);
-    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns cs = new HiddenColumns();
     cs.hideColumns(6, 14); // hide codons 3/4/5
     cs.hideColumns(24, 35); // hide codons 9-12
     cs.hideColumns(177, 191); // hide codons 60-64
@@ -296,7 +296,7 @@ public class DnaTest
      */
     AlignmentI cdna = new AlignmentGenerator(true)
             .generate(12, 8, 97, 5, 5);
-    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(cdna, cs);
     Dna dna = new Dna(av, new int[] { 0, cdna.getWidth() - 1 });
     AlignmentI translated = dna.translateCdna();
@@ -542,7 +542,7 @@ public class DnaTest
     assertEquals(seqDs, al.getSequenceAt(0).getDatasetSequence()
             .getSequenceAsString());
 
-    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(al, cs);
     Dna testee = new Dna(av, new int[] { 0, al.getWidth() - 1 });
     AlignmentI reversed = testee.reverseCdna(false);
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 9fc88ea..70e59c5 100644 (file)
@@ -125,7 +125,7 @@ public class TestAlignSeq
     };
 
     as.printAlignment(ps);
-    String expected = "Score = 320\nLength of alignment = 10\nSequence Seq1 :  3 - 18 (Sequence length = 14)\nSequence Seq1 :  1 - 10 (Sequence length = 10)\n\n"
+    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";
diff --git a/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java b/test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java
new file mode 100644 (file)
index 0000000..16ca70d
--- /dev/null
@@ -0,0 +1,355 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.analysis.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;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.gui.JvOptionPane;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.math.MatrixI;
+
+import java.util.Arrays;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class FeatureDistanceModelTest
+{
+
+  @BeforeClass(alwaysRun = true)
+  public void setUpJvOptionPane()
+  {
+    JvOptionPane.setInteractiveMode(false);
+    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+  }
+
+  public static String alntestFile = "FER1_MESCR/72-76 DVYIL\nFER1_SPIOL/71-75 DVYIL\nFER3_RAPSA/21-25 DVYVL\nFER1_MAIZE/73-77 DVYIL\n";
+
+  int[] sf1 = new int[] { 74, 74, 73, 73, 23, 23, -1, -1 };
+
+  int[] sf2 = new int[] { -1, -1, 74, 75, -1, -1, 76, 77 };
+
+  int[] sf3 = new int[] { -1, -1, -1, -1, -1, -1, 76, 77 };
+
+  /**
+   * <pre>
+   * Load test alignment and add features to sequences: 
+   *      FER1_MESCR FER1_SPIOL FER3_RAPSA FER1_MAIZE 
+   *  sf1     X          X          X  
+   *  sf2                X                     X 
+   *  sf3                                      X
+   * </pre>
+   * 
+   * @return
+   */
+  public AlignFrame getTestAlignmentFrame()
+  {
+    AlignFrame alf = new FileLoader(false).LoadFileWaitTillLoaded(
+            alntestFile, DataSourceType.PASTE);
+    AlignmentI al = alf.getViewport().getAlignment();
+    Assert.assertEquals(al.getHeight(), 4);
+    Assert.assertEquals(al.getWidth(), 5);
+    for (int i = 0; i < 4; i++)
+    {
+      SequenceI ds = al.getSequenceAt(i).getDatasetSequence();
+      if (sf1[i * 2] > 0)
+      {
+        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[i * 2],
+                sf2[i * 2 + 1], "sf2"));
+      }
+      if (sf3[i * 2] > 0)
+      {
+        ds.addSequenceFeature(new SequenceFeature("sf3", "sf3", sf3[i * 2],
+                sf3[i * 2 + 1], "sf3"));
+      }
+    }
+    alf.setShowSeqFeatures(true);
+    alf.getFeatureRenderer().setVisible("sf1");
+    alf.getFeatureRenderer().setVisible("sf2");
+    alf.getFeatureRenderer().setVisible("sf3");
+    alf.getFeatureRenderer().findAllFeatures(true);
+    Assert.assertEquals(alf.getFeatureRenderer().getDisplayedFeatureTypes()
+            .size(), 3, "Number of feature types");
+    assertTrue(alf.getCurrentView().areFeaturesDisplayed());
+    return alf;
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFeatureScoreModel() throws Exception
+  {
+    AlignFrame alf = getTestAlignmentFrame();
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
+    alf.selectAllSequenceMenuItem_actionPerformed(null);
+
+    MatrixI dm = sm.findDistances(
+            alf.getViewport().getAlignmentView(true),
+            SimilarityParams.Jalview);
+    assertEquals(dm.getValue(0, 2), 0d,
+            "FER1_MESCR (0) should be identical with RAPSA (2)");
+    assertTrue(dm.getValue(0, 1) > dm.getValue(0, 2),
+            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFeatureScoreModel_hiddenFirstColumn() throws Exception
+  {
+    AlignFrame alf = getTestAlignmentFrame();
+    // hiding first two columns shouldn't affect the tree
+    alf.getViewport().hideColumns(0, 1);
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
+    alf.selectAllSequenceMenuItem_actionPerformed(null);
+    MatrixI dm = sm.findDistances(
+            alf.getViewport().getAlignmentView(true),
+            SimilarityParams.Jalview);
+    assertEquals(dm.getValue(0, 2), 0d,
+            "FER1_MESCR (0) should be identical with RAPSA (2)");
+    assertTrue(dm.getValue(0, 1) > dm.getValue(0, 2),
+            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFeatureScoreModel_HiddenColumns() throws Exception
+  {
+    AlignFrame alf = getTestAlignmentFrame();
+    // hide columns and check tree changes
+    alf.getViewport().hideColumns(3, 4);
+    alf.getViewport().hideColumns(0, 1);
+    // 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 = sm.findDistances(
+            alf.getViewport().getAlignmentView(true),
+            SimilarityParams.Jalview);
+    assertEquals(
+            dm.getValue(0, 2),
+            0d,
+            "After hiding last two columns FER1_MESCR (0) should still be identical with RAPSA (2)");
+    assertEquals(
+            dm.getValue(0, 1),
+            0d,
+            "After hiding last two columns FER1_MESCR (0) should now also be identical with SPIOL (1)");
+    for (int s = 0; s < 3; s++)
+    {
+      assertTrue(dm.getValue(s, 3) > 0d, "After hiding last two columns "
+              + alf.getViewport().getAlignment().getSequenceAt(s).getName()
+              + "(" + s + ") should still be distinct from FER1_MAIZE (3)");
+    }
+  }
+
+  /**
+   * Check findFeatureAt doesn't return contact features except at contact
+   * points TODO:move to under the FeatureRendererModel test suite
+   */
+  @Test(groups = { "Functional" })
+  public void testFindFeatureAt_PointFeature() throws Exception
+  {
+    String alignment = "a CCCCCCGGGGGGCCCCCC\n" + "b CCCCCCGGGGGGCCCCCC\n"
+            + "c CCCCCCGGGGGGCCCCCC\n";
+    AlignFrame af = new jalview.io.FileLoader(false)
+            .LoadFileWaitTillLoaded(alignment, DataSourceType.PASTE);
+    SequenceI aseq = af.getViewport().getAlignment().getSequenceAt(0);
+    SequenceFeature sf = null;
+    sf = new SequenceFeature("disulphide bond", "", 2, 5, Float.NaN, "");
+    aseq.addSequenceFeature(sf);
+    assertTrue(sf.isContactFeature());
+    af.refreshFeatureUI(true);
+    af.getFeatureRenderer().setAllVisible(Arrays.asList("disulphide bond"));
+    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().findFeaturesAtColumn(aseq, 1)
+            .size(), 0);
+    // step through and check for pointwise feature presence/absence
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 2)
+            .size(), 1);
+    // step through and check for pointwise feature presence/absence
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 3)
+            .size(), 0);
+    // step through and check for pointwise feature presence/absence
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 4)
+            .size(), 0);
+    // step through and check for pointwise feature presence/absence
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 5)
+            .size(), 1);
+    // step through and check for pointwise feature presence/absence
+    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtColumn(aseq, 6)
+            .size(), 0);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindDistances() throws Exception
+  {
+    String seqs = ">s1\nABCDE\n>seq2\nABCDE\n";
+    AlignFrame alf = new FileLoader().LoadFileWaitTillLoaded(seqs,
+            DataSourceType.PASTE);
+    SequenceI s1 = alf.getViewport().getAlignment().getSequenceAt(0);
+    SequenceI s2 = alf.getViewport().getAlignment().getSequenceAt(1);
+
+    /*
+     * set domain and variant features thus:
+     *     ----5
+     *  s1 ddd..
+     *  s1 .vvv.
+     *  s1 ..vvv    
+     *  s2 .ddd. 
+     *  s2 vv..v
+     *  The number of unshared feature types per column is
+     *     20120 (two features of the same type doesn't affect score)
+     *  giving an average (pairwise distance) of 5/5 or 1.0 
+     */
+    s1.addSequenceFeature(new SequenceFeature("domain", null, 1, 3, 0f,
+            null));
+    s1.addSequenceFeature(new SequenceFeature("variant", null, 2, 4, 0f,
+            null));
+    s1.addSequenceFeature(new SequenceFeature("variant", null, 3, 5, 0f,
+            null));
+    s2.addSequenceFeature(new SequenceFeature("domain", null, 2, 4, 0f,
+            null));
+    s2.addSequenceFeature(new SequenceFeature("variant", null, 1, 2, 0f,
+            null));
+    s2.addSequenceFeature(new SequenceFeature("variant", null, 5, 5, 0f,
+            null));
+    alf.setShowSeqFeatures(true);
+    alf.getFeatureRenderer().findAllFeatures(true);
+
+    ScoreModelI sm = new FeatureDistanceModel();
+    sm = ScoreModels.getInstance().getScoreModel(sm.getName(),
+            alf.getCurrentView().getAlignPanel());
+    alf.selectAllSequenceMenuItem_actionPerformed(null);
+
+    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);
+    assertEquals(distances.getValue(1, 1), 0d);
+
+    assertEquals(distances.getValue(0, 1), 1d,
+            "expected identical pairs. (check normalisation for similarity score)");
+    assertEquals(distances.getValue(1, 0), 1d);
+  }
+
+  /**
+   * Verify computed distances with varying parameter options
+   */
+  @Test(groups = "Functional")
+  public void testFindDistances_withParams()
+  {
+    AlignFrame af = setupAlignmentView();
+    AlignViewport viewport = af.getViewport();
+    AlignmentView view = viewport.getAlignmentView(false);
+
+    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)
+     * the only variable parameter is 'includeGaps'
+     */
+
+    /*
+     * include gaps
+     * score = 3 + 3 + 0 + 2 + 3 + 2 = 13/6
+     */
+    SimilarityParamsI params = new SimilarityParams(true, true, true, true);
+    MatrixI distances = sm.findDistances(view, params);
+    assertEquals(distances.getValue(0, 0), 0d);
+    assertEquals(distances.getValue(1, 1), 0d);
+    assertEquals(distances.getValue(0, 1), 13d / 6); // should be 13d/6
+    assertEquals(distances.getValue(1, 0), 13d / 6);
+  
+    /*
+     * exclude gaps
+     * score = 3 + 3 + 0 + 0 + 0 + 0 = 6/6
+     */
+    params = new SimilarityParams(true, true, false, true);
+    distances = sm.findDistances(view, params);
+    assertEquals(distances.getValue(0, 1), 6d / 6);// should be 6d/6
+  }
+
+  /**
+   * <pre>
+   * Set up
+   *   column      1 2 3 4 5 6
+   *        seq s1 F R - K - S
+   *        seq s2 F S - - L
+   *   s1 chain    c c   c   c
+   *   s1 domain   d d   d   d
+   *   s2 chain    c c     c
+   *   s2 metal    m m     m
+   *   s2 Pfam     P P     P
+   *      scores:  3 3 0 2 3 2
+   * </pre>
+   * 
+   * @return
+   */
+  protected AlignFrame setupAlignmentView()
+  {
+    /*
+     * for now, using space for gap to match callers of
+     * AlignmentView.getSequenceStrings()
+     * may change this to '-' (with corresponding change to matrices)
+     */
+    SequenceI s1 = new Sequence("s1", "FR K S");
+    SequenceI s2 = new Sequence("s2", "FS  L");
+
+    s1.addSequenceFeature(new SequenceFeature("chain", null, 1, 4, 0f, null));
+    s1.addSequenceFeature(new SequenceFeature("domain", null, 1, 4, 0f,
+            null));
+    s2.addSequenceFeature(new SequenceFeature("chain", null, 1, 3, 0f, null));
+    s2.addSequenceFeature(new SequenceFeature("metal", null, 1, 3, 0f, null));
+    s2.addSequenceFeature(new SequenceFeature("Pfam", null, 1, 3, 0f, null));
+    AlignmentI al = new Alignment(new SequenceI[] { s1, s2 });
+    AlignFrame af = new AlignFrame(al, 300, 300);
+    af.setShowSeqFeatures(true);
+    af.getFeatureRenderer().findAllFeatures(true);
+    return af;
+  }
+
+}
diff --git a/test/jalview/analysis/scoremodels/FeatureScoreModelTest.java b/test/jalview/analysis/scoremodels/FeatureScoreModelTest.java
deleted file mode 100644 (file)
index 66eb5a5..0000000
+++ /dev/null
@@ -1,193 +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.analysis.scoremodels;
-
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.gui.AlignFrame;
-import jalview.gui.JvOptionPane;
-import jalview.io.DataSourceType;
-import jalview.io.FileLoader;
-
-import java.util.Arrays;
-
-import org.testng.Assert;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-public class FeatureScoreModelTest
-{
-
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
-  public static String alntestFile = "FER1_MESCR/72-76 DVYIL\nFER1_SPIOL/71-75 DVYIL\nFER3_RAPSA/21-25 DVYVL\nFER1_MAIZE/73-77 DVYIL\n";
-
-  int[] sf1 = new int[] { 74, 74, 73, 73, 23, 23, -1, -1 };
-
-  int[] sf2 = new int[] { -1, -1, 74, 75, -1, -1, 76, 77 };
-
-  int[] sf3 = new int[] { -1, -1, -1, -1, -1, -1, 76, 77 };
-
-  public AlignFrame getTestAlignmentFrame()
-  {
-    AlignFrame alf = new FileLoader(false).LoadFileWaitTillLoaded(
-            alntestFile, DataSourceType.PASTE);
-    AlignmentI al = alf.getViewport().getAlignment();
-    Assert.assertEquals(al.getHeight(), 4);
-    Assert.assertEquals(al.getWidth(), 5);
-    for (int i = 0; i < 4; i++)
-    {
-      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"));
-      }
-      if (sf2[i * 2] > 0)
-      {
-        ds.addSequenceFeature(new SequenceFeature("sf2", "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"));
-      }
-    }
-    alf.setShowSeqFeatures(true);
-    alf.getFeatureRenderer().setVisible("sf1");
-    alf.getFeatureRenderer().setVisible("sf2");
-    alf.getFeatureRenderer().setVisible("sf3");
-    alf.getFeatureRenderer().findAllFeatures(true);
-    Assert.assertEquals(alf.getFeatureRenderer().getDisplayedFeatureTypes()
-            .size(), 3, "Number of feature types");
-    Assert.assertTrue(alf.getCurrentView().areFeaturesDisplayed());
-    return alf;
-  }
-
-  @Test(groups = { "Functional" })
-  public void testFeatureScoreModel() throws Exception
-  {
-    AlignFrame alf = getTestAlignmentFrame();
-    FeatureScoreModel fsm = new FeatureScoreModel();
-    Assert.assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
-    alf.selectAllSequenceMenuItem_actionPerformed(null);
-    float[][] dm = fsm.findDistances(alf.getViewport().getAlignmentView(
-            true));
-    Assert.assertTrue(dm[0][2] == 0f,
-            "FER1_MESCR (0) should be identical with RAPSA (2)");
-    Assert.assertTrue(dm[0][1] > dm[0][2],
-            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
-  }
-
-  @Test(groups = { "Functional" })
-  public void testFeatureScoreModel_hiddenFirstColumn() throws Exception
-  {
-    AlignFrame alf = getTestAlignmentFrame();
-    // hiding first two columns shouldn't affect the tree
-    alf.getViewport().hideColumns(0, 1);
-    FeatureScoreModel fsm = new FeatureScoreModel();
-    Assert.assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
-    alf.selectAllSequenceMenuItem_actionPerformed(null);
-    float[][] dm = fsm.findDistances(alf.getViewport().getAlignmentView(
-            true));
-    Assert.assertTrue(dm[0][2] == 0f,
-            "FER1_MESCR (0) should be identical with RAPSA (2)");
-    Assert.assertTrue(dm[0][1] > dm[0][2],
-            "FER1_MESCR (0) should be further from SPIOL (1) than it is from RAPSA (2)");
-  }
-
-  @Test(groups = { "Functional" })
-  public void testFeatureScoreModel_HiddenColumns() throws Exception
-  {
-    AlignFrame alf = getTestAlignmentFrame();
-    // hide columns and check tree changes
-    alf.getViewport().hideColumns(3, 4);
-    alf.getViewport().hideColumns(0, 1);
-    FeatureScoreModel fsm = new FeatureScoreModel();
-    Assert.assertTrue(fsm.configureFromAlignmentView(alf.getCurrentView()
-            .getAlignPanel()));
-    alf.selectAllSequenceMenuItem_actionPerformed(null);
-    float[][] dm = fsm.findDistances(alf.getViewport().getAlignmentView(
-            true));
-    Assert.assertTrue(
-            dm[0][2] == 0f,
-            "After hiding last two columns FER1_MESCR (0) should still be identical with RAPSA (2)");
-    Assert.assertTrue(
-            dm[0][1] == 0f,
-            "After hiding last two columns FER1_MESCR (0) should now also be identical with SPIOL (1)");
-    for (int s = 0; s < 3; s++)
-    {
-      Assert.assertTrue(dm[s][3] > 0f, "After hiding last two columns "
-              + alf.getViewport().getAlignment().getSequenceAt(s).getName()
-              + "(" + s + ") should still be distinct from FER1_MAIZE (3)");
-    }
-  }
-
-  /**
-   * Check findFeatureAt doesn't return contact features except at contact
-   * points TODO:move to under the FeatureRendererModel test suite
-   */
-  @Test(groups = { "Functional" })
-  public void testFindFeatureAt_PointFeature() throws Exception
-  {
-    String alignment = "a CCCCCCGGGGGGCCCCCC\n" + "b CCCCCCGGGGGGCCCCCC\n"
-            + "c CCCCCCGGGGGGCCCCCC\n";
-    AlignFrame af = new jalview.io.FileLoader(false)
-            .LoadFileWaitTillLoaded(alignment, DataSourceType.PASTE);
-    SequenceI aseq = af.getViewport().getAlignment().getSequenceAt(0);
-    SequenceFeature sf = null;
-    sf = new SequenceFeature("disulphide bond", "", 2, 5, Float.NaN, "");
-    aseq.addSequenceFeature(sf);
-    Assert.assertTrue(sf.isContactFeature());
-    af.refreshFeatureUI(true);
-    af.getFeatureRenderer().setAllVisible(Arrays.asList("disulphide bond"));
-    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)
-            .size(), 0);
-    // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 2)
-            .size(), 1);
-    // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 3)
-            .size(), 0);
-    // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 4)
-            .size(), 0);
-    // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 5)
-            .size(), 1);
-    // step through and check for pointwise feature presence/absence
-    Assert.assertEquals(af.getFeatureRenderer().findFeaturesAtRes(aseq, 6)
-            .size(), 0);
-  }
-
-}
diff --git a/test/jalview/analysis/scoremodels/PIDModelTest.java b/test/jalview/analysis/scoremodels/PIDModelTest.java
new file mode 100644 (file)
index 0000000..212f825
--- /dev/null
@@ -0,0 +1,176 @@
+package jalview.analysis.scoremodels;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.util.Comparison;
+
+import org.testng.annotations.Test;
+
+public class PIDModelTest
+{
+  private static final double DELTA = 0.00001D;
+
+  @Test(groups = "Functional")
+  public void testGetPairwiseScore()
+  {
+    PIDModel sm = new PIDModel();
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1f);
+    assertEquals(sm.getPairwiseScore('A', 'a'), 1f);
+    assertEquals(sm.getPairwiseScore('a', 'A'), 1f);
+    assertEquals(sm.getPairwiseScore('A', 'B'), 0f);
+    assertEquals(sm.getPairwiseScore('A', ' '), 0f);
+    assertEquals(sm.getPairwiseScore(' ', ' '), 0f);
+    assertEquals(sm.getPairwiseScore('.', '.'), 0f);
+    assertEquals(sm.getPairwiseScore('-', '-'), 0f);
+  }
+
+  /**
+   * Regression test to verify that a (suitably configured) PIDModel computes
+   * the same percentage identities as the Comparison.PID method
+   */
+  @Test(groups = "Functional")
+  public void testComputePID_matchesComparisonPID()
+  {
+    SimilarityParamsI params = new SimilarityParams(true, true, true, true);
+
+    /*
+     * same length, no gaps
+     */
+    String s1 = "ARFNQDWSGI";
+    String s2 = "ARKNQDQSGI";
+
+    new PIDModel();
+    double newScore = PIDModel.computePID(s1, s2, params);
+    double oldScore = Comparison.PID(s1, s2);
+    assertEquals(newScore, oldScore, DELTA);
+
+    /*
+     * same length, with gaps
+     */
+    s1 = "-RFNQDWSGI";
+    s2 = "ARKNQ-QSGI";
+    new PIDModel();
+    newScore = PIDModel.computePID(s1, s2, params);
+    oldScore = Comparison.PID(s1, s2);
+    assertEquals(newScore, oldScore, DELTA);
+
+    /*
+     * s2 longer than s1, with gaps
+     */
+    s1 = "ARK-";
+    s2 = "-RFNQ";
+    new PIDModel();
+    newScore = PIDModel.computePID(s1, s2, params);
+    oldScore = Comparison.PID(s1, s2);
+    assertEquals(newScore, oldScore, DELTA);
+
+    /*
+     * s1 longer than s2, with gaps
+     */
+    s1 = "-RFNQ";
+    s2 = "ARK-";
+    new PIDModel();
+    newScore = PIDModel.computePID(s1, s2, params);
+    oldScore = Comparison.PID(s1, s2);
+    assertEquals(newScore, oldScore, DELTA);
+
+    /*
+     * same but now also with gapped columns
+     */
+    s1 = "-R-F-NQ";
+    s2 = "AR-K--";
+    new PIDModel();
+    newScore = PIDModel.computePID(s1, s2, params);
+    oldScore = Comparison.PID(s1, s2);
+    assertEquals(newScore, oldScore, DELTA);
+  }
+
+  /**
+   * Tests for percentage identity variants where only the shorter length of two
+   * sequences is used
+   */
+  @Test(groups = "Functional")
+  public void testComputePID_matchShortestSequence()
+  {
+    String s1 = "FR-K-S";
+    String s2 = "FS--L";
+
+    /*
+     * match gap-gap and gap-char
+     * PID = 4/5 = 80%
+     */
+    SimilarityParamsI params = new SimilarityParams(true, true, true, true);
+    assertEquals(PIDModel.computePID(s1, s2, params), 80d);
+
+    /*
+     * match gap-char but not gap-gap
+     * PID = 3/4 = 75%
+     */
+    params = new SimilarityParams(false, true, true, true);
+    assertEquals(PIDModel.computePID(s1, s2, params), 75d);
+
+    /*
+     * include gaps but don't match them
+     * include gap-gap, counted as identity
+     * PID = 2/5 = 40%
+     */
+    params = new SimilarityParams(true, false, true, true);
+    assertEquals(PIDModel.computePID(s1, s2, params), 40d);
+
+    /*
+     * include gaps but don't match them
+     * exclude gap-gap
+     * PID = 1/4 = 25%
+     */
+    params = new SimilarityParams(false, false, true, true);
+    assertEquals(PIDModel.computePID(s1, s2, params), 25d);
+  }
+
+  /**
+   * Tests for percentage identity variants where the longer length of two
+   * sequences is used
+   */
+  @Test(groups = "Functional")
+  public void testComputePID_matchLongestSequence()
+  {
+    String s1 = "FR-K-S";
+    String s2 = "FS--L";
+  
+    /*
+     * match gap-gap and gap-char
+     * shorter sequence treated as if with trailing gaps
+     * PID = 5/6 = 83.333...%
+     */
+    SimilarityParamsI params = new SimilarityParams(true, true, true, false);
+    assertEquals(PIDModel.computePID(s1, s2, params), 500d / 6);
+  
+    /*
+     * match gap-char but not gap-gap
+     * PID = 4/5 = 80%
+     */
+    params = new SimilarityParams(false, true, true, false);
+    assertEquals(PIDModel.computePID(s1, s2, params), 80d);
+  
+    /*
+     * include gaps but don't match them
+     * include gap-gap, counted as identity
+     * PID = 2/6 = 33.333...%
+     */
+    params = new SimilarityParams(true, false, true, false);
+    assertEquals(PIDModel.computePID(s1, s2, params), 100d / 3);
+  
+    /*
+     * include gaps but don't match them
+     * exclude gap-gap
+     * PID = 1/5 = 25%
+     */
+    params = new SimilarityParams(false, false, true, false);
+    assertEquals(PIDModel.computePID(s1, s2, params), 20d);
+
+    /*
+     * no tests for matchGaps=true, includeGaps=false
+     * as it don't make sense
+     */
+  }
+}
diff --git a/test/jalview/analysis/scoremodels/ScoreMatrixTest.java b/test/jalview/analysis/scoremodels/ScoreMatrixTest.java
new file mode 100644 (file)
index 0000000..1a5d43c
--- /dev/null
@@ -0,0 +1,588 @@
+package jalview.analysis.scoremodels;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
+
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.io.DataSourceType;
+import jalview.io.FileParse;
+import jalview.io.ScoreMatrixFile;
+import jalview.math.MatrixI;
+import jalview.schemes.ResidueProperties;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Arrays;
+
+import org.testng.annotations.Test;
+
+public class ScoreMatrixTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    // note score matrix does not have to be symmetric (though it should be!)
+    float[][] scores = new float[3][];
+    scores[0] = new float[] { 1f, 2f, 3f };
+    scores[1] = new float[] { -4f, 5f, 6f };
+    scores[2] = new float[] { 7f, 8f, 9f };
+    ScoreMatrix sm = new ScoreMatrix("Test", "ABC".toCharArray(), scores);
+    assertEquals(sm.getSize(), 3);
+    assertArrayEquals(scores, sm.getMatrix());
+    assertEquals(sm.getPairwiseScore('A', 'a'), 1f);
+    assertEquals(sm.getPairwiseScore('b', 'c'), 6f);
+    assertEquals(sm.getPairwiseScore('c', 'b'), 8f);
+    assertEquals(sm.getMatrixIndex('c'), 2);
+    assertEquals(sm.getMatrixIndex(' '), -1);
+
+    // substitution to or from unknown symbol gets minimum score
+    assertEquals(sm.getPairwiseScore('A', 'D'), -4f);
+    assertEquals(sm.getPairwiseScore('D', 'A'), -4f);
+    // unknown-to-self gets a score of 1
+    assertEquals(sm.getPairwiseScore('D', 'D'), 1f);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testConstructor_matrixTooSmall()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 3f, 4f };
+    new ScoreMatrix("Test", "ABC".toCharArray(), scores);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testConstructor_matrixTooBig()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 3f, 4f };
+    new ScoreMatrix("Test", "A".toCharArray(), scores);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testConstructor_matrixNotSquare()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 3f };
+    new ScoreMatrix("Test", "AB".toCharArray(), scores);
+  }
+
+  @Test(groups = "Functional")
+  public void testBuildSymbolIndex()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 3f, 4f };
+    ScoreMatrix sm = new ScoreMatrix("Test", new char[] { 'A', '.' },
+            scores);
+    short[] index = sm.buildSymbolIndex("AX-yxYp".toCharArray());
+
+    assertEquals(index.length, 128); // ASCII character set size
+
+    assertEquals(index['A'], 0);
+    assertEquals(index['a'], 0); // lower-case mapping added
+    assertEquals(index['X'], 1);
+    assertEquals(index['-'], 2);
+    assertEquals(index['y'], 3); // lower-case override
+    assertEquals(index['x'], 4); // lower-case override
+    assertEquals(index['Y'], 5);
+    assertEquals(index['p'], 6);
+    assertEquals(index['P'], -1); // lower-case doesn't map upper-case
+
+    /*
+     * check all unmapped symbols have index for unmapped
+     */
+    for (int c = 0; c < index.length; c++)
+    {
+      if (!"AaXx-. Yyp".contains(String.valueOf((char) c)))
+      {
+        assertEquals(index[c], -1);
+      }
+    }
+  }
+
+  /**
+   * check that characters not in the basic ASCII set are simply ignored
+   */
+  @Test(groups = "Functional")
+  public void testBuildSymbolIndex_nonAscii()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 3f, 4f };
+    ScoreMatrix sm = new ScoreMatrix("Test", new char[] { 'A', '.' },
+            scores);
+    char[] weird = new char[] { 128, 245, 'P' };
+    short[] index = sm.buildSymbolIndex(weird);
+    assertEquals(index.length, 128);
+    assertEquals(index['P'], 2);
+    assertEquals(index['p'], 2);
+    for (int c = 0; c < index.length; c++)
+    {
+      if (c != 'P' && c != 'p')
+      {
+        assertEquals(index[c], -1);
+      }
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatrix()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    float[][] m = sm.getMatrix();
+    assertEquals(m.length, sm.getSize());
+    assertEquals(m[2][4], -3f);
+    // verify a defensive copy is returned
+    float[][] m2 = sm.getMatrix();
+    assertNotSame(m, m2);
+    assertTrue(Arrays.deepEquals(m, m2));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatrixIndex()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    assertEquals(sm.getMatrixIndex('A'), 0);
+    assertEquals(sm.getMatrixIndex('R'), 1);
+    assertEquals(sm.getMatrixIndex('r'), 1);
+    assertEquals(sm.getMatrixIndex('N'), 2);
+    assertEquals(sm.getMatrixIndex('D'), 3);
+    assertEquals(sm.getMatrixIndex('X'), 22);
+    assertEquals(sm.getMatrixIndex('x'), 22);
+    assertEquals(sm.getMatrixIndex('-'), -1);
+    assertEquals(sm.getMatrixIndex('*'), 23);
+    assertEquals(sm.getMatrixIndex('.'), -1);
+    assertEquals(sm.getMatrixIndex(' '), -1);
+    assertEquals(sm.getMatrixIndex('?'), -1);
+    assertEquals(sm.getMatrixIndex((char) 128), -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSize()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    assertEquals(sm.getMatrix().length, sm.getSize());
+  }
+
+  @Test(groups = "Functional")
+  public void testComputePairwiseScores()
+  {
+    /*
+     * NB score matrix expects '-' for gap
+     */
+    String[] seqs = new String[] { "FKL", "R-D", "QIA", "GWC" };
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+
+    MatrixI pairwise = sm.findSimilarities(seqs, SimilarityParams.Jalview);
+
+    /*
+     * should be NxN where N = number of sequences
+     */
+    assertEquals(pairwise.height(), 4);
+    assertEquals(pairwise.width(), 4);
+
+    /*
+     * should be symmetrical (because BLOSUM62 is)
+     */
+    for (int i = 0; i < pairwise.height(); i++)
+    {
+      for (int j = i + 1; j < pairwise.width(); j++)
+      {
+        assertEquals(pairwise.getValue(i, j), pairwise.getValue(j, i),
+                String.format("Not symmetric at [%d, %d]", i, j));
+      }
+    }
+    /*
+     * verify expected BLOSUM dot product scores
+     */
+    // F.F + K.K + L.L = 6 + 5 + 4 = 15
+    assertEquals(pairwise.getValue(0, 0), 15d);
+    // R.R + -.- + D.D = 5 + 1 + 6 = 12
+    assertEquals(pairwise.getValue(1, 1), 12d);
+    // Q.Q + I.I + A.A = 5 + 4 + 4 = 13
+    assertEquals(pairwise.getValue(2, 2), 13d);
+    // G.G + W.W + C.C = 6 + 11 + 9 = 26
+    assertEquals(pairwise.getValue(3, 3), 26d);
+    // F.R + K.- + L.D = -3 + -4 + -4 = -11
+    assertEquals(pairwise.getValue(0, 1), -11d);
+    // F.Q + K.I + L.A = -3 + -3 + -1 = -7
+    assertEquals(pairwise.getValue(0, 2), -7d);
+    // F.G + K.W + L.C = -3 + -3 + -1 = -7
+    assertEquals(pairwise.getValue(0, 3), -7d);
+    // R.Q + -.I + D.A = 1 + -4 + -2 = -5
+    assertEquals(pairwise.getValue(1, 2), -5d);
+    // R.G + -.W + D.C = -2 + -4 + -3 = -9
+    assertEquals(pairwise.getValue(1, 3), -9d);
+    // Q.G + I.W + A.C = -2 + -3 + 0 = -5
+    assertEquals(pairwise.getValue(2, 3), -5d);
+  }
+
+  /**
+   * Test that the result of outputMatrix can be reparsed to give an identical
+   * ScoreMatrix
+   * 
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  @Test(groups = "Functional")
+  public void testOutputMatrix_roundTrip() throws MalformedURLException,
+          IOException
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    String output = sm.outputMatrix(false);
+    FileParse fp = new FileParse(output, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    ScoreMatrix sm2 = parser.parseMatrix();
+    assertNotNull(sm2);
+    assertTrue(sm2.equals(sm));
+  }
+
+  @Test(groups = "Functional")
+  public void testEqualsAndHashCode()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    ScoreMatrix sm2 = new ScoreMatrix(sm.getName(), sm.getSymbols()
+            .toCharArray(), sm.getMatrix());
+    assertTrue(sm.equals(sm2));
+    assertEquals(sm.hashCode(), sm2.hashCode());
+
+    sm2 = ScoreModels.getInstance().getPam250();
+    assertFalse(sm.equals(sm2));
+    assertNotEquals(sm.hashCode(), sm2.hashCode());
+
+    assertFalse(sm.equals("hello"));
+  }
+
+  /**
+   * Tests for scoring options where the longer length of two sequences is used
+   */
+  @Test(groups = "Functional")
+  public void testcomputeSimilarity_matchLongestSequence()
+  {
+    /*
+     * ScoreMatrix expects '-' for gaps
+     */
+    String s1 = "FR-K-S";
+    String s2 = "FS--L";
+    ScoreMatrix blosum = ScoreModels.getInstance().getBlosum62();
+  
+    /*
+     * score gap-gap and gap-char
+     * shorter sequence treated as if with trailing gaps
+     * score = F^F + R^S + -^- + K^- + -^L + S^-
+     * = 6 + -1 + 1 + -4 + -4 + -4 = -6
+     */
+    SimilarityParamsI params = new SimilarityParams(true, true, true, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -6d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(true, false, true, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -6d);
+  
+    /*
+     * score gap-char but not gap-gap
+     * score = F^F + R^S + 0 + K^- + -^L + S^-
+     * = 6 + -1 + 0 + -4 + -4 + -4 = -7
+     */
+    params = new SimilarityParams(false, true, true, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -7d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(false, false, true, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -7d);
+  
+    /*
+     * score gap-gap but not gap-char
+     * score = F^F + R^S + -^- + 0 + 0 + 0
+     * = 6 + -1 + 1 = 6
+     */
+    params = new SimilarityParams(true, false, false, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(true, true, false, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
+  
+    /*
+     * score neither gap-gap nor gap-char
+     * score = F^F + R^S + 0 + 0 + 0 + 0
+     * = 6 + -1  = 5
+     */
+    params = new SimilarityParams(false, false, false, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(false, true, false, false);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
+  }
+
+  /**
+   * Tests for scoring options where only the shorter length of two sequences is
+   * used
+   */
+  @Test(groups = "Functional")
+  public void testcomputeSimilarity_matchShortestSequence()
+  {
+    /*
+     * ScoreMatrix expects '-' for gaps
+     */
+    String s1 = "FR-K-S";
+    String s2 = "FS--L";
+    ScoreMatrix blosum = ScoreModels.getInstance().getBlosum62();
+
+    /*
+     * score gap-gap and gap-char
+     * match shorter sequence only
+     * score = F^F + R^S + -^- + K^- + -^L
+     * = 6 + -1 + 1 + -4 + -4 = -2
+     */
+    SimilarityParamsI params = new SimilarityParams(true, true, true, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -2d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(true, false, true, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -2d);
+  
+    /*
+     * score gap-char but not gap-gap
+     * score = F^F + R^S + 0 + K^- + -^L
+     * = 6 + -1 + 0 + -4 + -4 = -3
+     */
+    params = new SimilarityParams(false, true, true, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -3d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(false, false, true, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), -3d);
+  
+    /*
+     * score gap-gap but not gap-char
+     * score = F^F + R^S + -^- + 0 + 0
+     * = 6 + -1 + 1 = 6
+     */
+    params = new SimilarityParams(true, false, false, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(true, true, false, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 6d);
+  
+    /*
+     * score neither gap-gap nor gap-char
+     * score = F^F + R^S + 0 + 0 + 0
+     * = 6 + -1  = 5
+     */
+    params = new SimilarityParams(false, false, false, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
+    // matchGap (arg2) is ignored:
+    params = new SimilarityParams(false, true, false, true);
+    assertEquals(blosum.computeSimilarity(s1, s2, params), 5d);
+  }
+
+  @Test(groups = "Functional")
+  public void testSymmetric()
+  {
+    verifySymmetric(ScoreModels.getInstance().getBlosum62());
+    verifySymmetric(ScoreModels.getInstance().getPam250());
+    verifySymmetric(ScoreModels.getInstance().getDefaultModel(false)); // dna
+  }
+
+  private void verifySymmetric(ScoreMatrix sm)
+  {
+    float[][] m = sm.getMatrix();
+    int rows = m.length;
+    for (int row = 0; row < rows; row++)
+    {
+      assertEquals(m[row].length, rows);
+      for (int col = 0; col < rows; col++)
+      {
+        assertEquals(m[row][col], m[col][row], String.format("%s [%s, %s]",
+                sm.getName(), ResidueProperties.aa[row],
+                ResidueProperties.aa[col]));
+      }
+    }
+  }
+
+  /**
+   * A test that just asserts the expected values in the Blosum62 score matrix
+   */
+  @Test(groups = "Functional")
+  public void testBlosum62_values()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+
+    assertTrue(sm.isProtein());
+    assertFalse(sm.isDNA());
+    assertNull(sm.getDescription());
+
+    /*
+     * verify expected scores against ARNDCQEGHILKMFPSTWYVBZX
+     * scraped from https://www.ncbi.nlm.nih.gov/Class/FieldGuide/BLOSUM62.txt
+     */
+    verifyValues(sm, 'A', new float[] { 4, -1, -2, -2, 0, -1, -1, 0, -2,
+        -1,
+        -1, -1, -1, -2, -1, 1, 0, -3, -2, 0, -2, -1, 0 });
+    verifyValues(sm, 'R', new float[] { -1, 5, 0, -2, -3, 1, 0, -2, 0, -3,
+        -2, 2, -1, -3, -2, -1, -1, -3, -2, -3, -1, 0, -1 });
+    verifyValues(sm, 'N', new float[] { -2, 0, 6, 1, -3, 0, 0, 0, 1, -3,
+        -3,
+        0, -2, -3, -2, 1, 0, -4, -2, -3, 3, 0, -1 });
+    verifyValues(sm, 'D', new float[] { -2, -2, 1, 6, -3, 0, 2, -1, -1, -3,
+        -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1 });
+    verifyValues(sm, 'C', new float[] { 0, -3, -3, -3, 9, -3, -4, -3, -3,
+        -1,
+        -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2 });
+    verifyValues(sm, 'Q', new float[] { -1, 1, 0, 0, -3, 5, 2, -2, 0, -3,
+        -2,
+        1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1 });
+    verifyValues(sm, 'E', new float[] { -1, 0, 0, 2, -4, 2, 5, -2, 0, -3,
+        -3,
+        1, -2, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1 });
+    verifyValues(sm, 'G', new float[] { 0, -2, 0, -1, -3, -2, -2, 6, -2,
+        -4,
+        -4, -2, -3, -3, -2, 0, -2, -2, -3, -3, -1, -2, -1 });
+    verifyValues(sm, 'H', new float[] { -2, 0, 1, -1, -3, 0, 0, -2, 8, -3,
+        -3, -1, -2, -1, -2, -1, -2, -2, 2, -3, 0, 0, -1 });
+    verifyValues(sm, 'I', new float[] { -1, -3, -3, -3, -1, -3, -3, -4, -3,
+        4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3, -3, -3, -1 });
+    verifyValues(sm, 'L', new float[] { -1, -2, -3, -4, -1, -2, -3, -4, -3,
+        2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1, -4, -3, -1 });
+    verifyValues(sm, 'K', new float[] { -1, 2, 0, -1, -3, 1, 1, -2, -1, -3,
+        -2, 5, -1, -3, -1, 0, -1, -3, -2, -2, 0, 1, -1 });
+    verifyValues(sm, 'M', new float[] { -1, -1, -2, -3, -1, 0, -2, -3, -2,
+        1,
+        2, -1, 5, 0, -2, -1, -1, -1, -1, 1, -3, -1, -1 });
+    verifyValues(sm, 'F', new float[] { -2, -3, -3, -3, -2, -3, -3, -3, -1,
+        0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1, -3, -3, -1 });
+    verifyValues(sm, 'P', new float[] { -1, -2, -2, -1, -3, -1, -1, -2, -2,
+        -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2, -2, -1, -2 });
+    verifyValues(sm, 'S', new float[] { 1, -1, 1, 0, -1, 0, 0, 0, -1, -2,
+        -2,
+        0, -1, -2, -1, 4, 1, -3, -2, -2, 0, 0, 0 });
+    verifyValues(sm, 'T', new float[] { 0, -1, 0, -1, -1, -1, -1, -2, -2,
+        -1,
+        -1, -1, -1, -2, -1, 1, 5, -2, -2, 0, -1, -1, 0 });
+    verifyValues(sm, 'W', new float[] { -3, -3, -4, -4, -2, -2, -3, -2, -2,
+        -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3, -4, -3, -2 });
+    verifyValues(sm, 'Y', new float[] { -2, -2, -2, -3, -2, -1, -2, -3, 2,
+        -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1, -3, -2, -1 });
+    verifyValues(sm, 'V', new float[] { 0, -3, -3, -3, -1, -2, -2, -3, -3,
+        3,
+        1, -2, 1, -1, -2, -2, 0, -3, -1, 4, -3, -2, -1 });
+    verifyValues(sm, 'B', new float[] { -2, -1, 3, 4, -3, 0, 1, -1, 0, -3,
+        -4, 0, -3, -3, -2, 0, -1, -4, -3, -3, 4, 1, -1 });
+    verifyValues(sm, 'Z', new float[] { -1, 0, 0, 1, -3, 3, 4, -2, 0, -3,
+        -3,
+        1, -1, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1 });
+    verifyValues(sm, 'X', new float[] { 0, -1, -1, -1, -2, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -2, 0, 0, -2, -1, -1, -1, -1, -1 });
+  }
+
+  /**
+   * Helper method to check pairwise scores for one residue
+   * 
+   * @param sm
+   * @param res
+   * @param expected
+   *          score values against 'res', in ResidueProperties.aaIndex order
+   */
+  private void verifyValues(ScoreMatrix sm, char res, float[] expected)
+  {
+    for (int j = 0; j < expected.length; j++)
+    {
+      char c2 = ResidueProperties.aa[j].charAt(0);
+      assertEquals(sm.getPairwiseScore(res, c2), expected[j],
+              String.format("%s->%s", res, c2));
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testConstructor_gapDash()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 4f, 5f };
+    ScoreMatrix sm = new ScoreMatrix("Test", new char[] { 'A', '-' },
+            scores);
+    assertEquals(sm.getSize(), 2);
+    assertArrayEquals(scores, sm.getMatrix());
+    assertEquals(sm.getPairwiseScore('A', 'a'), 1f);
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1f);
+    assertEquals(sm.getPairwiseScore('a', '-'), 2f);
+    assertEquals(sm.getPairwiseScore('-', 'A'), 4f);
+    assertEquals(sm.getMatrixIndex('a'), 0);
+    assertEquals(sm.getMatrixIndex('A'), 0);
+    assertEquals(sm.getMatrixIndex('-'), 1);
+    assertEquals(sm.getMatrixIndex(' '), -1);
+    assertEquals(sm.getMatrixIndex('.'), -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPairwiseScore()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { -4f, 5f };
+    ScoreMatrix sm = new ScoreMatrix("Test", new char[] { 'A', 'B' },
+            scores);
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1f);
+    assertEquals(sm.getPairwiseScore('A', 'a'), 1f);
+    assertEquals(sm.getPairwiseScore('A', 'B'), 2f);
+    assertEquals(sm.getPairwiseScore('b', 'a'), -4f);
+    assertEquals(sm.getPairwiseScore('B', 'b'), 5f);
+
+    /*
+     * unknown symbols currently score minimum score
+     * or 1 for identity with self
+     */
+    assertEquals(sm.getPairwiseScore('A', '-'), -4f);
+    assertEquals(sm.getPairwiseScore('-', 'A'), -4f);
+    assertEquals(sm.getPairwiseScore('-', '-'), 1f);
+    assertEquals(sm.getPairwiseScore('Q', 'W'), -4f);
+    assertEquals(sm.getPairwiseScore('Q', 'Q'), 1f);
+
+    /*
+     * symbols not in basic ASCII set score zero
+     */
+    char c = (char) 200;
+    assertEquals(sm.getPairwiseScore('Q', c), 0f);
+    assertEquals(sm.getPairwiseScore(c, 'Q'), 0f);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinimumScore()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    assertEquals(sm.getMinimumScore(), -4f);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMaximumScore()
+  {
+    ScoreMatrix sm = ScoreModels.getInstance().getBlosum62();
+    assertEquals(sm.getMaximumScore(), 11f);
+  }
+
+  @Test(groups = "Functional")
+  public void testOutputMatrix_html()
+  {
+    float[][] scores = new float[2][];
+    scores[0] = new float[] { 1f, 2f };
+    scores[1] = new float[] { 4f, -5.3E-10f };
+    ScoreMatrix sm = new ScoreMatrix("Test", "AB".toCharArray(), scores);
+    String html = sm.outputMatrix(true);
+    String expected = "<table border=\"1\"><tr><th></th><th>&nbsp;A&nbsp;</th><th>&nbsp;B&nbsp;</th></tr>\n"
+            + "<tr><td>A</td><td>1.0</td><td>2.0</td></tr>\n"
+            + "<tr><td>B</td><td>4.0</td><td>-5.3E-10</td></tr>\n"
+            + "</table>";
+    assertEquals(html, expected);
+  }
+}
diff --git a/test/jalview/analysis/scoremodels/ScoreModelsTest.java b/test/jalview/analysis/scoremodels/ScoreModelsTest.java
new file mode 100644 (file)
index 0000000..ffcd1a8
--- /dev/null
@@ -0,0 +1,105 @@
+package jalview.analysis.scoremodels;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.api.analysis.PairwiseScoreModelI;
+import jalview.api.analysis.ScoreModelI;
+
+import java.util.Iterator;
+
+import org.testng.annotations.Test;
+
+public class ScoreModelsTest
+{
+  /**
+   * Verify that the singleton constructor successfully loads Jalview's built-in
+   * score models
+   */
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    Iterator<ScoreModelI> models = ScoreModels.getInstance().getModels()
+            .iterator();
+    assertTrue(models.hasNext());
+
+    /*
+     * models are served in order of addition
+     */
+    ScoreModelI sm = models.next();
+    assertTrue(sm instanceof SimilarityScoreModel);
+    assertTrue(sm instanceof PairwiseScoreModelI);
+    assertFalse(sm instanceof DistanceScoreModel);
+    assertEquals(sm.getName(), "BLOSUM62");
+    assertEquals(((PairwiseScoreModelI) sm).getPairwiseScore('I', 'R'), -3f);
+
+    sm = models.next();
+    assertTrue(sm instanceof SimilarityScoreModel);
+    assertTrue(sm instanceof PairwiseScoreModelI);
+    assertFalse(sm instanceof DistanceScoreModel);
+    assertEquals(sm.getName(), "PAM250");
+    assertEquals(((PairwiseScoreModelI) sm).getPairwiseScore('R', 'C'), -4f);
+
+    sm = models.next();
+    assertTrue(sm instanceof SimilarityScoreModel);
+    assertTrue(sm instanceof PairwiseScoreModelI);
+    assertFalse(sm instanceof DistanceScoreModel);
+    assertEquals(sm.getName(), "PID");
+    assertEquals(((PairwiseScoreModelI) sm).getPairwiseScore('R', 'C'), 0f);
+    assertEquals(((PairwiseScoreModelI) sm).getPairwiseScore('R', 'r'), 1f);
+
+    sm = models.next();
+    assertTrue(sm instanceof SimilarityScoreModel);
+    assertTrue(sm instanceof PairwiseScoreModelI);
+    assertFalse(sm instanceof DistanceScoreModel);
+    assertEquals(sm.getName(), "DNA");
+    assertEquals(((PairwiseScoreModelI) sm).getPairwiseScore('c', 'x'), 1f);
+
+    sm = models.next();
+    assertFalse(sm instanceof SimilarityScoreModel);
+    assertFalse(sm instanceof PairwiseScoreModelI);
+    assertTrue(sm instanceof DistanceScoreModel);
+    assertEquals(sm.getName(), "Sequence Feature Similarity");
+  }
+
+  /**
+   * 'Test' that prints out score matrices in tab-delimited format. This test is
+   * intentionally not assigned to any group so would not be run as part of a
+   * suite. It makes no assertions and is just provided as a utility method for
+   * printing out matrices. Relocated here from ScoreMatrixPrinter.
+   */
+  @Test(groups = "none")
+  public void printAllMatrices_tabDelimited()
+  {
+    printAllMatrices(false);
+  }
+
+  /**
+   * 'Test' that prints out score matrices in html format. This test is
+   * intentionally not assigned to any group so would not be run as part of a
+   * suite. It makes no assertions and is just provided as a utility method for
+   * printing out matrices. Relocated here from ScoreMatrixPrinter.
+   */
+  @Test(groups = "none")
+  public void printAllMatrices_asHtml()
+  {
+    printAllMatrices(true);
+  }
+
+  /**
+   * Print all registered ScoreMatrix as plain or html tables
+   * 
+   * @param asHtml
+   */
+  protected void printAllMatrices(boolean asHtml)
+  {
+    for (ScoreModelI sm : ScoreModels.getInstance().getModels())
+    {
+      if (sm instanceof ScoreMatrix)
+      {
+        System.out.println(((ScoreMatrix) sm).outputMatrix(asHtml));
+      }
+    }
+  }
+}
index 3223042..155f00e 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.commands;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 
 import jalview.commands.EditCommand.Action;
@@ -28,11 +29,15 @@ 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.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,6 +50,14 @@ import org.testng.annotations.Test;
  */
 public class EditCommandTest
 {
+  /*
+   * 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()
@@ -639,4 +652,222 @@ 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];
+    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));
+
+    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(4, sfs.size()); // feature internal to cut has been deleted
+    SequenceFeature sf = sfs.get(0);
+    assertEquals("before", sf.getType());
+    assertEquals(1, sf.getBegin());
+    assertEquals(3, sf.getEnd());
+    sf = sfs.get(1);
+    assertEquals("overlap left", sf.getType());
+    assertEquals(2, sf.getBegin());
+    assertEquals(3, sf.getEnd()); // truncated by cut
+    sf = sfs.get(2);
+    assertEquals("overlap right", sf.getType());
+    assertEquals(4, sf.getBegin()); // shifted left by cut
+    assertEquals(5, 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
+  }
+
+  /**
+   * 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");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    alignment.setDataset(null);
+    for (int from = 1; from <= seq0.getLength(); from++)
+    {
+      for (int to = from; to <= seq0.getLength(); 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());
+
+    /*
+     * now perform all possible cuts of subranges of 1-5 (followed by Undo)
+     * and validate the resulting remaining sequence features!
+     */
+    SequenceI[] sqs = new SequenceI[] { seq0 };
+
+    // goal is to have this passing for all from/to values!!
+    // for (int from = 0; from < seq0.getLength(); from++)
+    // {
+    // for (int to = from; to < seq0.getLength(); to++)
+    for (int from = 1; from < 3; from++)
+    {
+      for (int to = 2; to < 3; to++)
+      {
+        testee.appendEdit(Action.CUT, sqs, from, (to - from + 1),
+                alignment, true);
+
+        sfs = seq0.getSequenceFeatures();
+
+        /*
+         * confirm the number of features has reduced by the
+         * number of features within the cut region i.e. by
+         * func(length of cut)
+         */
+        String msg = String.format("Cut %d-%d ", from, to);
+        if (to - from == 4)
+        {
+          // all columns cut
+          assertNull(sfs);
+        }
+        else
+        {
+          assertEquals(msg + "wrong number of features left", func(5)
+                  - func(to - from + 1), sfs.size());
+        }
+
+        /*
+         * inspect individual features
+         */
+        if (sfs != null)
+        {
+          for (SequenceFeature sf : sfs)
+          {
+            checkFeatureRelocation(sf, from + 1, to + 1);
+          }
+        }
+        /*
+         * undo ready for next cut
+         */
+        testee.undoCommand(new AlignmentI[] { alignment });
+        assertEquals(func(5), seq0.getSequenceFeatures().size());
+      }
+    }
+  }
+
+  /**
+   * Helper method to check a feature has been correctly relocated after a cut
+   * 
+   * @param sf
+   * @param from
+   *          start of cut (first residue cut)
+   * @param to
+   *          end of cut (last residue cut)
+   */
+  private void checkFeatureRelocation(SequenceFeature sf, int from, int to)
+  {
+    // TODO handle the gapped sequence case as well
+    int cutSize = to - from + 1;
+    int oldFrom = ((Integer) sf.getValue("from")).intValue();
+    int oldTo = ((Integer) sf.getValue("to")).intValue();
+
+    String msg = String.format(
+            "Feature %s relocated to %d-%d after cut of %d-%d",
+            sf.getDescription(), sf.getBegin(), sf.getEnd(), from, to);
+    if (oldTo < from)
+    {
+      // before cut region so unchanged
+      assertEquals("1: " + msg, oldFrom, sf.getBegin());
+      assertEquals("2: " + msg, oldTo, sf.getEnd());
+    }
+    else if (oldFrom > to)
+    {
+      // follows cut region - shift by size of cut
+      assertEquals("3: " + msg, oldFrom - cutSize, sf.getBegin());
+      assertEquals("4: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFrom < from && oldTo > to)
+    {
+      // feature encloses cut region - shrink it right
+      assertEquals("5: " + msg, oldFrom, sf.getBegin());
+      assertEquals("6: " + msg, oldTo - cutSize, sf.getEnd());
+    }
+    else if (oldFrom < from)
+    {
+      // feature overlaps left side of cut region - truncated right
+      assertEquals("7: " + msg, from - 1, sf.getEnd());
+    }
+    else if (oldTo > to)
+    {
+      // feature overlaps right side of cut region - truncated left
+      assertEquals("8: " + msg, from, sf.getBegin());
+      assertEquals("9: " + msg, from + oldTo - to - 1, 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_gappedWithFeatures()
+  {
+    /*
+     * create sequence features before, after and overlapping
+     * a cut of columns/residues 4-7
+     */
+    SequenceI seq0 = new Sequence("seq", "A-BCC");
+    seq0.addSequenceFeature(new SequenceFeature("", "", 3, 4, 0f,
+            null));
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq0 });
+    // cut columns of A-B
+    Edit ec = testee.new Edit(Action.CUT, seqs, 0, 3, alignment); // cols 0-3
+                                                                  // base 0
+    EditCommand.cut(ec, new AlignmentI[] { alignment });
+  
+    /*
+     * feature on CC(3-4) should now be on CC(1-2)
+     */
+    List<SequenceFeature> sfs = seq0.getSequenceFeatures();
+    assertEquals(1, sfs.size());
+    SequenceFeature sf = sfs.get(0);
+    assertEquals(1, sf.getBegin());
+    assertEquals(2, sf.getEnd());
+
+    // TODO add further cases including Undo - see JAL-2541
+  }
 }
index a2c6256..e47e9d6 100644 (file)
@@ -28,6 +28,7 @@ import jalview.gui.JvOptionPane;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.FileFormat;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -293,4 +294,45 @@ public class AlignmentAnnotationTests
               ann.getDefaultRnaHelixSymbol(i));
     }
   }
+
+  public static Annotation newAnnotation(String ann)
+  {
+    float val = 0f;
+    try
+    {
+      val = Float.parseFloat(ann);
+    } catch (NumberFormatException q)
+    {
+    }
+    ;
+    return new Annotation(ann, ann, '\0', val);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testIsQuantitative()
+  {
+    AlignmentAnnotation ann = null;
+
+    ann = new AlignmentAnnotation("an", "some an", null);
+    Assert.assertFalse(ann.isQuantitative(),
+            "Empty annotation set should not be quantitative.");
+
+    ann = new AlignmentAnnotation("an", "some an", new Annotation[] {
+        newAnnotation("4"), newAnnotation("1"), newAnnotation("1"),
+        newAnnotation("0.1"), newAnnotation("0.3") });
+    Assert.assertTrue(ann.isQuantitative(),
+            "All numbers annotation set should be quantitative.");
+
+    ann = new AlignmentAnnotation("an", "some an", new Annotation[] {
+        newAnnotation("E"), newAnnotation("E"), newAnnotation("E"),
+        newAnnotation("E"), newAnnotation("E") });
+    Assert.assertFalse(ann.isQuantitative(),
+            "All 'E' annotation set should not be quantitative.");
+
+    ann = new AlignmentAnnotation("an", "some an", new Annotation[] {
+        newAnnotation("E"), newAnnotation("1"), newAnnotation("2"),
+        newAnnotation("3"), newAnnotation("E") });
+    Assert.assertTrue(ann.isQuantitative(),
+            "Mixed 'E' annotation set should be quantitative.");
+  }
 }
index 618bac5..d6e09fd 100644 (file)
@@ -27,6 +27,7 @@ import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.analysis.AlignmentGenerator;
 import jalview.datamodel.AlignedCodonFrame.SequenceToSequenceMapping;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
@@ -611,6 +612,73 @@ public class AlignmentTest
     AlignmentAnnotation ann = iter.next();
     assertEquals("D.melanogaster.2", ann.sequenceRef.getName());
     assertFalse(iter.hasNext());
+
+    // invalid id
+    anns = al.findAnnotation("CalcIdForD.melanogaster.?");
+    assertFalse(iter.hasNext());
+    anns = al.findAnnotation(null);
+    assertFalse(iter.hasNext());
+  }
+
+  /**
+   * Test method that returns annotations that match on reference sequence,
+   * label, or calcId.
+   */
+  @Test(groups = { "Functional" })
+  public void testFindAnnotations_bySeqLabelandorCalcId()
+  {
+    // TODO: finish testFindAnnotations_bySeqLabelandorCalcId test
+    /* Note - this is an incomplete test - need to check null or
+     * non-null [ matches, not matches ] behaviour for each of the three
+     * parameters..*/
+
+    // search for a single, unique calcId with wildcards on other params
+    Iterable<AlignmentAnnotation> anns = al.findAnnotations(null,
+            "CalcIdForD.melanogaster.2", null);
+    Iterator<AlignmentAnnotation> iter = anns.iterator();
+    assertTrue(iter.hasNext());
+    AlignmentAnnotation ann = iter.next();
+    assertEquals("D.melanogaster.2", ann.sequenceRef.getName());
+    assertFalse(iter.hasNext());
+
+    // save reference to test sequence reference parameter
+    SequenceI rseq = ann.sequenceRef;
+
+    // search for annotation associated with a single sequence
+    anns = al.findAnnotations(rseq, null, null);
+    iter = anns.iterator();
+    assertTrue(iter.hasNext());
+    ann = iter.next();
+    assertEquals("D.melanogaster.2", ann.sequenceRef.getName());
+    assertFalse(iter.hasNext());
+
+    // search for annotation with a non-existant calcId
+    anns = al.findAnnotations(null, "CalcIdForD.melanogaster.?", null);
+    iter = anns.iterator();
+    assertFalse(iter.hasNext());
+
+    // search for annotation with a particular label - expect three
+    anns = al.findAnnotations(null, null, "Secondary Structure");
+    iter = anns.iterator();
+    assertTrue(iter.hasNext());
+    iter.next();
+    assertTrue(iter.hasNext());
+    iter.next();
+    assertTrue(iter.hasNext());
+    iter.next();
+    // third found.. so
+    assertFalse(iter.hasNext());
+
+    // null on all parameters == find all annotations
+    anns = al.findAnnotations(null, null, null);
+    iter = anns.iterator();
+    int n = al.getAlignmentAnnotation().length;
+    while (iter.hasNext())
+    {
+      n--;
+      iter.next();
+    }
+    assertTrue("Found " + n + " fewer annotations from search.", n == 0);
   }
 
   @Test(groups = { "Functional" })
@@ -1190,4 +1258,76 @@ public class AlignmentTest
     assertNull(a.findGroup(seq2, 8));
   }
 
+  @Test(groups = { "Functional" })
+  public void testDeleteSequenceByIndex()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI a = gen.generate(20, 15, 123, 5, 5);
+
+    // delete sequence 10, alignment reduced by 1
+    int height = a.getAbsoluteHeight();
+    a.deleteSequence(10);
+    assertEquals(a.getAbsoluteHeight(), height - 1);
+
+    // try to delete -ve index, nothing happens
+    a.deleteSequence(-1);
+    assertEquals(a.getAbsoluteHeight(), height - 1);
+
+    // try to delete beyond end of alignment, nothing happens
+    a.deleteSequence(14);
+    assertEquals(a.getAbsoluteHeight(), height - 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDeleteSequenceBySeq()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI a = gen.generate(20, 15, 123, 5, 5);
+
+    // delete sequence 10, alignment reduced by 1
+    int height = a.getAbsoluteHeight();
+    SequenceI seq = a.getSequenceAt(10);
+    a.deleteSequence(seq);
+    assertEquals(a.getAbsoluteHeight(), height - 1);
+
+    // try to delete non-existent sequence, nothing happens
+    seq = new Sequence("cds", "GCCTCGGAT");
+    assertEquals(a.getAbsoluteHeight(), height - 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDeleteHiddenSequence()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI a = gen.generate(20, 15, 123, 5, 5);
+
+    // delete a sequence which is hidden, check it is NOT removed from hidden
+    // sequences
+    int height = a.getAbsoluteHeight();
+    SequenceI seq = a.getSequenceAt(2);
+    a.getHiddenSequences().hideSequence(seq);
+    assertEquals(a.getHiddenSequences().getSize(), 1);
+    a.deleteSequence(2);
+    assertEquals(a.getAbsoluteHeight(), height - 1);
+    assertEquals(a.getHiddenSequences().getSize(), 1);
+
+    // delete a sequence which is not hidden, check hiddenSequences are not
+    // affected
+    a.deleteSequence(10);
+    assertEquals(a.getAbsoluteHeight(), height - 2);
+    assertEquals(a.getHiddenSequences().getSize(), 1);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testSetDataset_selfReference()
+  {
+    SequenceI seq = new Sequence("a", "a");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq });
+    alignment.setDataset(alignment);
+  }
 }
index 594d6e6..b201c7e 100644 (file)
@@ -22,7 +22,11 @@ package jalview.datamodel;
 
 import static org.testng.Assert.assertEquals;
 
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
 import jalview.gui.JvOptionPane;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -50,4 +54,69 @@ public class AlignmentViewTest
     assertEquals(av.getVisibleAlignment('$').getSequenceAt(0)
             .getSequenceAsString(), "A$$CDE");
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetVisibleContigs()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            ">s1\n0123456789\n", DataSourceType.PASTE);
+    AlignViewport av = af.getViewport();
+    AlignmentView view = av.getAlignmentView(true);
+
+    /*
+     * verify getVisibleContigs returns inclusive [start, end] ranges
+     * 
+     * no columns hidden
+     */
+    int[] contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 0, 9 });
+
+    /*
+     * hide 3 internal columns
+     */
+    av.hideColumns(5, 7);
+    // the old AlignmentView is now stale!
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 0, 9 });
+    // get a fresh AlignmentView
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 0, 4, 8, 9 });
+
+    // hide first 2 columns
+    av.hideColumns(0, 1);
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 2, 4, 8, 9 });
+
+    // hide last column
+    av.hideColumns(9, 9);
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 2, 4, 8, 8 });
+
+    // unhide columns 5-7
+    av.showColumn(5);
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 2, 8 });
+
+    // hide columns 2-7
+    av.hideColumns(2, 7);
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 8, 8 });
+
+    // hide column 8
+    av.hideColumns(8, 8);
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] {});
+
+    // unhide all
+    av.showAllHiddenColumns();
+    view = av.getAlignmentView(true);
+    contigs = view.getVisibleContigs();
+    assertEquals(contigs, new int[] { 0, 9 });
+  }
 }
diff --git a/test/jalview/datamodel/AllColsIteratorTest.java b/test/jalview/datamodel/AllColsIteratorTest.java
new file mode 100644 (file)
index 0000000..3942f0b
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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.assertTrue;
+
+import java.util.NoSuchElementException;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class AllColsIteratorTest
+{
+  HiddenColumns hiddenCols;
+  
+  @BeforeClass
+  public void setup()
+  {
+    hiddenCols = new HiddenColumns();
+   hiddenCols.hideColumns(2,4);
+  }
+  
+
+  /*
+   * Test iterator iterates through collection correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNext()
+  {
+    AllColsIterator it = new AllColsIterator(0, 3, hiddenCols);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator throws NoSuchElementException at end of iteration
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNext() throws NoSuchElementException
+  {
+    AllColsIterator it = new AllColsIterator(0, 3, hiddenCols);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test iterator throws UnsupportedOperationException on call to remove
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { UnsupportedOperationException.class })
+  public void testRemove() throws UnsupportedOperationException
+  {
+    AllColsIterator it = new AllColsIterator(0, 3, hiddenCols);
+    it.remove();
+  }
+
+  /*
+   * Test iterator behaves correctly when there is only one element in the collection
+   */
+  @Test(groups = { "Functional" })
+  public void testOneElement()
+  {
+    HiddenColumns hidden = new HiddenColumns();
+    AllColsIterator it = new AllColsIterator(0, 0, hidden);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 1, "hasNext() is false after 1 iteration");
+  }
+}
diff --git a/test/jalview/datamodel/AllRowsIteratorTest.java b/test/jalview/datamodel/AllRowsIteratorTest.java
new file mode 100644 (file)
index 0000000..aeff71d
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * 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.assertTrue;
+
+import jalview.analysis.AlignmentGenerator;
+
+import java.util.Hashtable;
+import java.util.NoSuchElementException;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class AllRowsIteratorTest
+{
+  AlignmentI al;
+
+  Hashtable<SequenceI, SequenceCollectionI> hiddenRepSequences = new Hashtable<>();
+
+  @BeforeClass
+  public void setup()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    al = gen.generate(20, 15, 123, 5, 5);
+    if (!hiddenRepSequences.isEmpty())
+    {
+      al.getHiddenSequences().showAll(hiddenRepSequences);
+    }
+    hideSequences(2, 4);
+  }
+
+  /*
+   * Test iterator iterates through collection correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNext()
+  {
+    AllRowsIterator it = new AllRowsIterator(0, 3, al);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator throws NoSuchElementException at end of iteration
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNext() throws NoSuchElementException
+  {
+    AllRowsIterator it = new AllRowsIterator(0, 3, al);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test iterator throws UnsupportedOperationException on call to remove
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { UnsupportedOperationException.class })
+  public void testRemove() throws UnsupportedOperationException
+  {
+    AllRowsIterator it = new AllRowsIterator(0, 3, al);
+    it.remove();
+  }
+
+
+  /*
+   * Hide sequences between start and end
+   */
+  private void hideSequences(int start, int end)
+  {
+    SequenceI[] allseqs = al.getSequencesArray();
+    SequenceGroup theseSeqs = new SequenceGroup();
+
+    for (int i = start; i <= end; i++)
+    {
+      theseSeqs.addSequence(allseqs[i], false);
+      al.getHiddenSequences().hideSequence(allseqs[i]);
+    }
+
+    hiddenRepSequences.put(allseqs[start], theseSeqs);
+  }
+
+  /*
+   * Test iterator behaves correctly when there is only one element in the collection
+   */
+  @Test(groups = { "Functional" })
+  public void testOneElement()
+  {
+    AllRowsIterator it = new AllRowsIterator(0, 0, al);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 1, "hasNext() is false after 1 iteration");
+  }
+
+}
index 4faa482..7237e63 100644 (file)
@@ -22,10 +22,10 @@ package jalview.datamodel;
 
 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 static org.testng.AssertJUnit.fail;
 
+import jalview.analysis.AlignmentGenerator;
 import jalview.gui.JvOptionPane;
 
 import java.util.Arrays;
@@ -59,6 +59,30 @@ public class ColumnSelectionTest
     assertEquals("[2, 5, 3]", sel.toString());
   }
 
+  @Test(groups = { "Functional" })
+  public void testSetElementsFrom()
+  {
+    ColumnSelection fromcs = new ColumnSelection();
+    ColumnSelection tocs = new ColumnSelection();
+    HiddenColumns hidden = new HiddenColumns();
+
+    fromcs.addElement(2);
+    fromcs.addElement(3);
+    fromcs.addElement(5);
+
+    tocs.setElementsFrom(fromcs, hidden);
+    assertTrue(tocs.equals(fromcs));
+
+    hidden.hideColumns(4, 6);
+    tocs.setElementsFrom(fromcs, hidden);
+
+    // expect cols 2 and 3 to be selected but not 5
+    ColumnSelection expectcs = new ColumnSelection();
+    expectcs.addElement(2);
+    expectcs.addElement(3);
+    assertTrue(tocs.equals(expectcs));
+  }
+
   /**
    * Test the remove method - in particular to verify that remove(int i) removes
    * the element whose value is i, _NOT_ the i'th element.
@@ -88,176 +112,6 @@ public class ColumnSelectionTest
   }
 
   /**
-   * Test the method that finds the visible column position of an alignment
-   * column, allowing for hidden columns.
-   */
-  @Test(groups = { "Functional" })
-  public void testFindColumnPosition()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    assertEquals(5, cs.findColumnPosition(5));
-
-    // hiding column 6 makes no difference
-    cs.hideColumns(6, 6);
-    assertEquals(5, cs.findColumnPosition(5));
-
-    // hiding column 4 moves column 5 to column 4
-    cs.hideColumns(4, 4);
-    assertEquals(4, cs.findColumnPosition(5));
-
-    // hiding columns 1 and 2 moves column 5 to column 2
-    cs.hideColumns(1, 2);
-    assertEquals(2, cs.findColumnPosition(5));
-  }
-
-  /**
-   * Test the code used to locate the reference sequence ruler origin
-   */
-  @Test(groups = { "Functional" })
-  public void testLocateVisibleBoundsofSequence()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    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
-    cs.hideColumns(13);
-    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)));
-
-    cs.revealAllHiddenColumns();
-    // hidden column on gap before beginning of sequence - should vis bounds by
-    // one
-    cs.hideColumns(0);
-    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();
-    // 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();
-
-    // 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)));
-
-  }
-
-  @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") });
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideInsertionsFor(al.getSequenceAt(0));
-    assertEquals(
-            "G",
-            ""
-                    + al.getSequenceAt(0).getCharAt(
-                            cs.adjustForHiddenColumns(9)));
-
-  }
-
-  @Test(groups = { "Functional" })
-  public void testHideColumns()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(5);
-    List<int[]> hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(0)));
-
-    cs.hideColumns(3);
-    assertEquals(2, hidden.size());
-    // two hidden ranges, in order:
-    assertSame(hidden, cs.getHiddenColumns());
-    assertEquals("[3, 3]", Arrays.toString(hidden.get(0)));
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(1)));
-
-    // hiding column 4 expands [3, 3] to [3, 4]
-    // and merges to [5, 5] to make [3, 5]
-    cs.hideColumns(4);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[3, 5]", Arrays.toString(hidden.get(0)));
-
-    // clear hidden columns (note they are added to selected)
-    cs.revealAllHiddenColumns();
-    // it is now actually null but getter returns an empty list
-    assertTrue(cs.getHiddenColumns().isEmpty());
-
-    cs.hideColumns(3, 6);
-    hidden = cs.getHiddenColumns();
-    int[] firstHiddenRange = hidden.get(0);
-    assertEquals("[3, 6]", Arrays.toString(firstHiddenRange));
-
-    // adding a subrange of already hidden should do nothing
-    cs.hideColumns(4, 5);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-    cs.hideColumns(3, 5);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-    cs.hideColumns(4, 6);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-    cs.hideColumns(3, 6);
-    assertEquals(1, hidden.size());
-    assertSame(firstHiddenRange, cs.getHiddenColumns().get(0));
-
-    cs.revealAllHiddenColumns();
-    cs.hideColumns(2, 4);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
-
-    // extend contiguous with 2 positions overlap
-    cs.hideColumns(3, 5);
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 5]", Arrays.toString(hidden.get(0)));
-
-    // extend contiguous with 1 position overlap
-    cs.hideColumns(5, 6);
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 6]", Arrays.toString(hidden.get(0)));
-
-    // extend contiguous with overlap both ends:
-    cs.hideColumns(1, 7);
-    assertEquals(1, hidden.size());
-    assertEquals("[1, 7]", Arrays.toString(hidden.get(0)));
-  }
-
-  /**
    * Test the method that hides a specified column including any adjacent
    * selected columns. This is a convenience method for the case where multiple
    * column regions are selected and then hidden using menu option View | Hide |
@@ -266,48 +120,55 @@ public class ColumnSelectionTest
   @Test(groups = { "Functional" })
   public void testHideColumns_withSelection()
   {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
     ColumnSelection cs = new ColumnSelection();
     // select columns 4-6
     cs.addElement(4);
     cs.addElement(5);
     cs.addElement(6);
     // hide column 5 (and adjacent):
-    cs.hideColumns(5);
+    cs.hideSelectedColumns(5, al.getHiddenColumns());
     // 4,5,6 now hidden:
-    List<int[]> hidden = cs.getHiddenColumns();
+    List<int[]> hidden = al.getHiddenColumns().getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
     // none now selected:
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, hiding column 4 (5 and 6)
+    al = gen.generate(50, 20, 123, 5, 5);
     cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(5);
     cs.addElement(6);
-    cs.hideColumns(4);
-    hidden = cs.getHiddenColumns();
+    cs.hideSelectedColumns(4, al.getHiddenColumns());
+    hidden = al.getHiddenColumns().getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, hiding column (4, 5 and) 6
+    al = gen.generate(50, 20, 123, 5, 5);
     cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(5);
     cs.addElement(6);
-    cs.hideColumns(6);
-    hidden = cs.getHiddenColumns();
+    cs.hideSelectedColumns(6, al.getHiddenColumns());
+    hidden = al.getHiddenColumns().getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, with _only_ adjacent columns selected
+    al = gen.generate(50, 20, 123, 5, 5);
     cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(6);
-    cs.hideColumns(5);
-    hidden = cs.getHiddenColumns();
+    cs.hideSelectedColumns(5, al.getHiddenColumns());
+    hidden = al.getHiddenColumns().getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
     assertTrue(cs.getSelected().isEmpty());
@@ -319,17 +180,23 @@ public class ColumnSelectionTest
   @Test(groups = { "Functional" })
   public void testHideSelectedColumns()
   {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
     ColumnSelection cs = new ColumnSelection();
     int[] sel = { 2, 3, 4, 7, 8, 9, 20, 21, 22 };
     for (int col : sel)
     {
       cs.addElement(col);
     }
-    cs.hideColumns(15, 18);
 
-    cs.hideSelectedColumns();
+    HiddenColumns cols = al.getHiddenColumns();
+    cols.hideColumns(15, 18);
+
+    cs.hideSelectedColumns(al);
     assertTrue(cs.getSelected().isEmpty());
-    List<int[]> hidden = cs.getHiddenColumns();
+    List<int[]> hidden = cols.getHiddenRegions();
     assertEquals(4, hidden.size());
     assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
     assertEquals("[7, 9]", Arrays.toString(hidden.get(1)));
@@ -367,104 +234,27 @@ public class ColumnSelectionTest
     assertEquals("[0, 4]", Arrays.toString(range.get(0)));
   }
 
-  /**
-   * Test the method that reveals a range of hidden columns given the start
-   * column of the range
-   */
-  @Test(groups = { "Functional" })
-  public void testRevealHiddenColumns()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    cs.addElement(10);
-    cs.revealHiddenColumns(5);
-    // hidden columns list now null but getter returns empty list:
-    assertTrue(cs.getHiddenColumns().isEmpty());
-    // revealed columns are marked as selected (added to selection):
-    assertEquals("[10, 5, 6, 7, 8]", cs.getSelected().toString());
-
-    // calling with a column other than the range start does nothing:
-    cs = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    List<int[]> hidden = cs.getHiddenColumns();
-    cs.revealHiddenColumns(6);
-    assertSame(hidden, cs.getHiddenColumns());
-    assertTrue(cs.getSelected().isEmpty());
-  }
-
-  @Test(groups = { "Functional" })
-  public void testRevealAllHiddenColumns()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    cs.hideColumns(2, 3);
-    cs.addElement(11);
-    cs.addElement(1);
-    cs.revealAllHiddenColumns();
-
-    /*
-     * revealing hidden columns adds them (in order) to the (unordered)
-     * selection list
-     */
-    assertTrue(cs.getHiddenColumns().isEmpty());
-    assertEquals("[11, 1, 2, 3, 5, 6, 7, 8]", cs.getSelected().toString());
-  }
-
-  @Test(groups = { "Functional" })
-  public void testIsVisible()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(2, 4);
-    cs.hideColumns(6, 7);
-    assertTrue(cs.isVisible(0));
-    assertTrue(cs.isVisible(-99));
-    assertTrue(cs.isVisible(1));
-    assertFalse(cs.isVisible(2));
-    assertFalse(cs.isVisible(3));
-    assertFalse(cs.isVisible(4));
-    assertTrue(cs.isVisible(5));
-    assertFalse(cs.isVisible(6));
-    assertFalse(cs.isVisible(7));
-  }
-
-  @Test(groups = { "Functional" })
-  public void testGetVisibleContigs()
-  {
-    ColumnSelection cs = new ColumnSelection();
-    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(groups = { "Functional" })
   public void testInvertColumnSelection()
   {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
     ColumnSelection cs = new ColumnSelection();
     cs.addElement(4);
     cs.addElement(6);
     cs.addElement(8);
-    cs.hideColumns(3, 3);
-    cs.hideColumns(6, 6);
+
+    HiddenColumns cols = al.getHiddenColumns();
+    cols.hideColumns(3, 3);
+    cols.hideColumns(6, 6);
 
     // invert selection from start (inclusive) to end (exclusive)
-    // hidden columns are _not_ changed
-    cs.invertColumnSelection(2, 9);
+    cs.invertColumnSelection(2, 9, al);
     assertEquals("[2, 5, 7]", cs.getSelected().toString());
 
-    cs.invertColumnSelection(1, 9);
+    cs.invertColumnSelection(1, 9, al);
     assertEquals("[1, 4, 8]", cs.getSelected().toString());
   }
 
@@ -507,9 +297,6 @@ public class ColumnSelectionTest
     cs.addElement(0);
     cs.addElement(513);
     cs.addElement(1);
-    cs.hideColumns(3);
-    cs.hideColumns(7);
-    cs.hideColumns(5, 9);
 
     // same selections added in a different order
     ColumnSelection cs2 = new ColumnSelection();
@@ -517,15 +304,6 @@ public class ColumnSelectionTest
     cs2.addElement(513);
     cs2.addElement(0);
 
-    // with no hidden columns
-    assertFalse(cs.equals(cs2));
-    assertFalse(cs2.equals(cs));
-
-    // with hidden columns added in a different order
-    cs2.hideColumns(6, 9);
-    cs2.hideColumns(5, 8);
-    cs2.hideColumns(3);
-
     assertTrue(cs.equals(cs2));
     assertTrue(cs.equals(cs));
     assertTrue(cs2.equals(cs));
@@ -537,18 +315,20 @@ public class ColumnSelectionTest
 
     cs2.removeElement(12);
     assertTrue(cs.equals(cs2));
-
-    cs2.hideColumns(88);
-    assertFalse(cs.equals(cs2));
-    /*
-     * unhiding a column adds it to selection!
-     */
-    cs2.revealHiddenColumns(88);
-    assertFalse(cs.equals(cs2));
-    cs.addElement(88);
-    assertTrue(cs.equals(cs2));
   }
 
+  /*
+      cs2.hideSelectedColumns(88);
+      assertFalse(cs.equals(cs2));
+      /*
+       * unhiding a column adds it to selection!
+       */
+  /*    cs2.revealHiddenColumns(88);
+      assertFalse(cs.equals(cs2));
+      cs.addElement(88);
+      assertTrue(cs.equals(cs2));
+    */
+
   /**
    * Test the method that returns selected columns, in the order in which they
    * were added
@@ -746,73 +526,75 @@ public class ColumnSelectionTest
     ColumnSelection cs = new ColumnSelection();
     cs.addElement(3);
     cs.addElement(1);
-    cs.hideColumns(10, 11);
-    cs.hideColumns(5, 7);
-    assertEquals("[5, 7]", Arrays.toString(cs.getHiddenColumns().get(0)));
 
     ColumnSelection cs2 = new ColumnSelection(cs);
     assertTrue(cs2.hasSelectedColumns());
-    assertTrue(cs2.hasHiddenColumns());
+
     // order of column selection is preserved
     assertEquals("[3, 1]", cs2.getSelected().toString());
-    assertEquals(2, cs2.getHiddenColumns().size());
-    // hidden columns are held in column order
-    assertEquals("[5, 7]", Arrays.toString(cs2.getHiddenColumns().get(0)));
-    assertEquals("[10, 11]", Arrays.toString(cs2.getHiddenColumns().get(1)));
   }
 
-  /**
-   * Test for the case when a hidden range encloses more one already hidden
-   * range
-   */
+
   @Test(groups = { "Functional" })
-  public void testHideColumns_subsumingHidden()
+  public void testStretchGroup_expand()
   {
     /*
-     * JAL-2370 bug scenario:
-     * two hidden ranges subsumed by a third
+     * test that emulates clicking column 4 (selected)
+     * and dragging right to column 5 (all base 0)
      */
     ColumnSelection cs = new ColumnSelection();
-    cs.hideColumns(49, 59);
-    cs.hideColumns(69, 79);
-    List<int[]> hidden = cs.getHiddenColumns();
-    assertEquals(2, hidden.size());
-    assertEquals("[49, 59]", Arrays.toString(hidden.get(0)));
-    assertEquals("[69, 79]", Arrays.toString(hidden.get(1)));
-  
-    cs.hideColumns(48, 80);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[48, 80]", Arrays.toString(hidden.get(0)));
+    cs.addElement(4);
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(4);
+    sg.setEndRes(4);
+    cs.stretchGroup(5, sg, 4, 4);
+    assertEquals(cs.getSelected().size(), 2);
+    assertTrue(cs.contains(4));
+    assertTrue(cs.contains(5));
+    assertEquals(sg.getStartRes(), 4);
+    assertEquals(sg.getEndRes(), 5);
 
     /*
-     * another...joining hidden ranges
+     * emulate drag right with columns 10-20 already selected
      */
-    cs = new ColumnSelection();
-    cs.hideColumns(10, 20);
-    cs.hideColumns(30, 40);
-    cs.hideColumns(50, 60);
-    // hiding 21-49 should merge to one range
-    cs.hideColumns(21, 49);
-    hidden = cs.getHiddenColumns();
-    assertEquals(1, hidden.size());
-    assertEquals("[10, 60]", Arrays.toString(hidden.get(0)));
+    cs.clear();
+    for (int i = 10; i <= 20; i++)
+    {
+      cs.addElement(i);
+    }
+    assertEquals(cs.getSelected().size(), 11);
+    sg = new SequenceGroup();
+    sg.setStartRes(10);
+    sg.setEndRes(20);
+    cs.stretchGroup(21, sg, 10, 20);
+    assertEquals(cs.getSelected().size(), 12);
+    assertTrue(cs.contains(10));
+    assertTrue(cs.contains(21));
+    assertEquals(sg.getStartRes(), 10);
+    assertEquals(sg.getEndRes(), 21);
+  }
 
+  @Test(groups = { "Functional" })
+  public void testStretchGroup_shrink()
+  {
     /*
-     * another...lef overlap, subsumption, right overlap,
-     * no overlap of existing hidden ranges
+     * emulate drag left to 19 with columns 10-20 already selected
      */
-    cs = new ColumnSelection();
-    cs.hideColumns(10, 20);
-    cs.hideColumns(10, 20);
-    cs.hideColumns(30, 35);
-    cs.hideColumns(40, 50);
-    cs.hideColumns(60, 70);
-
-    cs.hideColumns(15, 45);
-    hidden = cs.getHiddenColumns();
-    assertEquals(2, hidden.size());
-    assertEquals("[10, 50]", Arrays.toString(hidden.get(0)));
-    assertEquals("[60, 70]", Arrays.toString(hidden.get(1)));
+    ColumnSelection cs = new ColumnSelection();
+    for (int i = 10; i <= 20; i++)
+    {
+      cs.addElement(i);
+    }
+    assertEquals(cs.getSelected().size(), 11);
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(10);
+    sg.setEndRes(20);
+    cs.stretchGroup(19, sg, 10, 20);
+    assertEquals(cs.getSelected().size(), 10);
+    assertTrue(cs.contains(10));
+    assertTrue(cs.contains(19));
+    assertFalse(cs.contains(20));
+    assertEquals(sg.getStartRes(), 10);
+    assertEquals(sg.getEndRes(), 19);
   }
 }
diff --git a/test/jalview/datamodel/HiddenColumnsTest.java b/test/jalview/datamodel/HiddenColumnsTest.java
new file mode 100644 (file)
index 0000000..10808d6
--- /dev/null
@@ -0,0 +1,588 @@
+/*
+ * 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.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertSame;
+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 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
+   */
+  @Test(groups = { "Functional" })
+  public void testGetSize()
+  {
+    HiddenColumns hidden = new HiddenColumns();
+    assertEquals(0, hidden.getSize());
+
+    hidden.hideColumns(3, 5);
+    assertEquals(3, hidden.getSize());
+
+    hidden.hideColumns(8, 8);
+    assertEquals(4, hidden.getSize());
+
+    hidden.hideColumns(9, 14);
+    assertEquals(10, hidden.getSize());
+
+    ColumnSelection cs = new ColumnSelection();
+    hidden.revealAllHiddenColumns(cs);
+    assertEquals(0, hidden.getSize());
+  }
+
+  /**
+   * Test the method that finds the visible column position of an alignment
+   * column, allowing for hidden columns.
+   */
+  @Test(groups = { "Functional" })
+  public void testFindColumnPosition()
+  {
+    HiddenColumns cs = new HiddenColumns();
+    assertEquals(5, cs.findColumnPosition(5));
+
+    // hiding column 6 makes no difference
+    cs.hideColumns(6, 6);
+    assertEquals(5, cs.findColumnPosition(5));
+
+    // hiding column 4 moves column 5 to column 4
+    cs.hideColumns(4, 4);
+    assertEquals(4, cs.findColumnPosition(5));
+
+    // hiding column 4 moves column 4 to position 3
+    assertEquals(3, cs.findColumnPosition(4));
+
+    // hiding columns 1 and 2 moves column 5 to column 2
+    cs.hideColumns(1, 2);
+    assertEquals(2, cs.findColumnPosition(5));
+
+    // check with > 1 hidden column regions
+    // where some columns are in the hidden regions
+    HiddenColumns cs2 = new HiddenColumns();
+    cs2.hideColumns(5, 10);
+    cs2.hideColumns(20, 27);
+    cs2.hideColumns(40, 44);
+
+    // hiding columns 5-10 and 20-27 moves column 8 to column 4
+    assertEquals(4, cs2.findColumnPosition(8));
+
+    // and moves column 24 to 13
+    assertEquals(13, cs2.findColumnPosition(24));
+
+    // and moves column 28 to 14
+    assertEquals(14, cs2.findColumnPosition(28));
+
+    // and moves column 40 to 25
+    assertEquals(25, cs2.findColumnPosition(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));
+
+  }
+
+  /**
+   * Test the method that finds the visible column position a given distance
+   * before another column
+   */
+  @Test(groups = { "Functional" })
+  public void testFindColumnNToLeft()
+  {
+    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);
+
+  }
+
+  @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(groups = { "Functional" })
+  public void testEquals()
+  {
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideColumns(5, 9);
+
+    // a different set of hidden columns
+    HiddenColumns cs2 = new HiddenColumns();
+
+    // with no hidden columns
+    assertFalse(cs.equals(cs2));
+    assertFalse(cs2.equals(cs));
+
+    // with hidden columns added in a different order
+    cs2.hideColumns(6, 9);
+    cs2.hideColumns(5, 8);
+
+    assertTrue(cs.equals(cs2));
+    assertTrue(cs.equals(cs));
+    assertTrue(cs2.equals(cs));
+    assertTrue(cs2.equals(cs2));
+  }
+
+  @Test(groups = "Functional")
+  public void testCopyConstructor()
+  {
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideColumns(10, 11);
+    cs.hideColumns(5, 7);
+    assertEquals("[5, 7]", Arrays.toString(cs.getHiddenRegions().get(0)));
+
+    HiddenColumns cs2 = new HiddenColumns(cs);
+    assertTrue(cs2.hasHiddenColumns());
+    assertEquals(2, cs2.getHiddenRegions().size());
+    // hidden columns are held in column order
+    assertEquals("[5, 7]", Arrays.toString(cs2.getHiddenRegions().get(0)));
+    assertEquals("[10, 11]", Arrays.toString(cs2.getHiddenRegions().get(1)));
+  }
+
+  /**
+   * Test the code used to locate the reference sequence ruler origin
+   */
+  @Test(groups = { "Functional" })
+  public void testLocateVisibleBoundsofSequence()
+  {
+    // 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(
+            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)));
+
+    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);
+
+    // 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)));
+
+  }
+
+  @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()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
+    ColumnSelection colsel = new ColumnSelection();
+    HiddenColumns cs = al.getHiddenColumns();
+    colsel.hideSelectedColumns(5, al.getHiddenColumns());
+    List<int[]> hidden = cs.getHiddenRegions();
+    assertEquals(1, hidden.size());
+    assertEquals("[5, 5]", Arrays.toString(hidden.get(0)));
+
+    colsel.hideSelectedColumns(3, al.getHiddenColumns());
+    assertEquals(2, hidden.size());
+    // two hidden ranges, in order:
+    assertSame(hidden, cs.getHiddenRegions());
+    assertEquals("[3, 3]", Arrays.toString(hidden.get(0)));
+    assertEquals("[5, 5]", Arrays.toString(hidden.get(1)));
+
+    // 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.getHiddenRegions();
+    assertEquals(1, hidden.size());
+    assertEquals("[3, 5]", Arrays.toString(hidden.get(0)));
+
+    // 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.getHiddenRegions().isEmpty());
+
+    cs.hideColumns(3, 6);
+    hidden = cs.getHiddenRegions();
+    int[] firstHiddenRange = hidden.get(0);
+    assertEquals("[3, 6]", Arrays.toString(firstHiddenRange));
+
+    // adding a subrange of already hidden should do nothing
+    cs.hideColumns(4, 5);
+    assertEquals(1, hidden.size());
+    assertSame(firstHiddenRange, cs.getHiddenRegions().get(0));
+    cs.hideColumns(3, 5);
+    assertEquals(1, hidden.size());
+    assertSame(firstHiddenRange, cs.getHiddenRegions().get(0));
+    cs.hideColumns(4, 6);
+    assertEquals(1, hidden.size());
+    assertSame(firstHiddenRange, cs.getHiddenRegions().get(0));
+    cs.hideColumns(3, 6);
+    assertEquals(1, hidden.size());
+    assertSame(firstHiddenRange, cs.getHiddenRegions().get(0));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(2, 4);
+    hidden = cs.getHiddenRegions();
+    assertEquals(1, hidden.size());
+    assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
+
+    // extend contiguous with 2 positions overlap
+    cs.hideColumns(3, 5);
+    assertEquals(1, hidden.size());
+    assertEquals("[2, 5]", Arrays.toString(hidden.get(0)));
+
+    // extend contiguous with 1 position overlap
+    cs.hideColumns(5, 6);
+    assertEquals(1, hidden.size());
+    assertEquals("[2, 6]", Arrays.toString(hidden.get(0)));
+
+    // extend contiguous with overlap both ends:
+    cs.hideColumns(1, 7);
+    assertEquals(1, hidden.size());
+    assertEquals("[1, 7]", Arrays.toString(hidden.get(0)));
+  }
+
+  /**
+   * Test the method that reveals a range of hidden columns given the start
+   * column of the range
+   */
+  @Test(groups = { "Functional" })
+  public void testRevealHiddenColumns()
+  {
+    ColumnSelection colsel = new ColumnSelection();
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideColumns(5, 8);
+    colsel.addElement(10);
+    cs.revealHiddenColumns(5, colsel);
+    // hidden columns list now null but getter returns empty list:
+    assertTrue(cs.getHiddenRegions().isEmpty());
+    // revealed columns are marked as selected (added to selection):
+    assertEquals("[10, 5, 6, 7, 8]", colsel.getSelected().toString());
+
+    // calling with a column other than the range start does nothing:
+    colsel = new ColumnSelection();
+    cs = new HiddenColumns();
+    cs.hideColumns(5, 8);
+    List<int[]> hidden = cs.getHiddenRegions();
+    cs.revealHiddenColumns(6, colsel);
+    assertSame(hidden, cs.getHiddenRegions());
+    assertTrue(colsel.getSelected().isEmpty());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testRevealAllHiddenColumns()
+  {
+    HiddenColumns cs = new HiddenColumns();
+    ColumnSelection colsel = new ColumnSelection();
+    cs.hideColumns(5, 8);
+    cs.hideColumns(2, 3);
+    colsel.addElement(11);
+    colsel.addElement(1);
+    cs.revealAllHiddenColumns(colsel);
+
+    /*
+     * revealing hidden columns adds them (in order) to the (unordered)
+     * selection list
+     */
+    assertTrue(cs.getHiddenRegions().isEmpty());
+    assertEquals("[11, 1, 2, 3, 5, 6, 7, 8]", colsel.getSelected()
+            .toString());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testIsVisible()
+  {
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideColumns(2, 4);
+    cs.hideColumns(6, 7);
+    assertTrue(cs.isVisible(0));
+    assertTrue(cs.isVisible(-99));
+    assertTrue(cs.isVisible(1));
+    assertFalse(cs.isVisible(2));
+    assertFalse(cs.isVisible(3));
+    assertFalse(cs.isVisible(4));
+    assertTrue(cs.isVisible(5));
+    assertFalse(cs.isVisible(6));
+    assertFalse(cs.isVisible(7));
+  }
+
+  /**
+   * Test for the case when a hidden range encloses more one already hidden
+   * range
+   */
+  @Test(groups = { "Functional" })
+  public void testHideColumns_subsumingHidden()
+  {
+    /*
+     * JAL-2370 bug scenario:
+     * two hidden ranges subsumed by a third
+     */
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideColumns(49, 59);
+    cs.hideColumns(69, 79);
+    List<int[]> hidden = cs.getHiddenRegions();
+    assertEquals(2, hidden.size());
+    assertEquals("[49, 59]", Arrays.toString(hidden.get(0)));
+    assertEquals("[69, 79]", Arrays.toString(hidden.get(1)));
+
+    cs.hideColumns(48, 80);
+    hidden = cs.getHiddenRegions();
+    assertEquals(1, hidden.size());
+    assertEquals("[48, 80]", Arrays.toString(hidden.get(0)));
+
+    /*
+     * another...joining hidden ranges
+     */
+    cs = new HiddenColumns();
+    cs.hideColumns(10, 20);
+    cs.hideColumns(30, 40);
+    cs.hideColumns(50, 60);
+    // hiding 21-49 should merge to one range
+    cs.hideColumns(21, 49);
+    hidden = cs.getHiddenRegions();
+    assertEquals(1, hidden.size());
+    assertEquals("[10, 60]", Arrays.toString(hidden.get(0)));
+
+    /*
+     * another...left overlap, subsumption, right overlap,
+     * no overlap of existing hidden ranges
+     */
+    cs = new HiddenColumns();
+    cs.hideColumns(10, 20);
+    cs.hideColumns(10, 20);
+    cs.hideColumns(30, 35);
+    cs.hideColumns(40, 50);
+    cs.hideColumns(60, 70);
+
+    cs.hideColumns(15, 45);
+    hidden = cs.getHiddenRegions();
+    assertEquals(2, hidden.size());
+    assertEquals("[10, 50]", Arrays.toString(hidden.get(0)));
+    assertEquals("[60, 70]", Arrays.toString(hidden.get(1)));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testHideBitset()
+  {
+    HiddenColumns cs;
+
+    BitSet one = new BitSet();
+
+    // one hidden range
+    one.set(1);
+    cs = new HiddenColumns();
+    cs.hideMarkedBits(one);
+    assertEquals(1, cs.getHiddenRegions().size());
+
+    one.set(2);
+    cs = new HiddenColumns();
+    cs.hideMarkedBits(one);
+    assertEquals(1, cs.getHiddenRegions().size());
+
+    one.set(3);
+    cs = new HiddenColumns();
+    cs.hideMarkedBits(one);
+    assertEquals(1, cs.getHiddenRegions().size());
+
+    // split
+    one.clear(2);
+    cs = new HiddenColumns();
+    cs.hideMarkedBits(one);
+    assertEquals(2, cs.getHiddenRegions().size());
+
+    assertEquals(0, cs.adjustForHiddenColumns(0));
+    assertEquals(2, cs.adjustForHiddenColumns(1));
+    assertEquals(4, cs.adjustForHiddenColumns(2));
+
+    // one again
+    one.clear(1);
+    cs = new HiddenColumns();
+    cs.hideMarkedBits(one);
+
+    assertEquals(1, cs.getHiddenRegions().size());
+
+    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);
+    }
+  }
+}
index cae3536..7795988 100644 (file)
@@ -49,7 +49,7 @@ public class HiddenSequencesTest
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  static int SEQ_COUNT = 10;
+  static int SEQ_COUNT = 25;
 
   SequenceI[] seqs;
 
@@ -62,8 +62,9 @@ public class HiddenSequencesTest
     seqs = new SequenceI[SEQ_COUNT];
     for (int i = 0; i < SEQ_COUNT; i++)
     {
-      // sequence lengths are 1, 2, ... 10
-      seqs[i] = new Sequence("Seq" + i, "abcdefghijk".substring(0, i + 1));
+      // sequence lengths are 1, 2, ... 25
+      seqs[i] = new Sequence("Seq" + i,
+              "abcdefghijklmnopqrstuvwxy".substring(0, i + 1));
     }
   }
 
@@ -89,7 +90,7 @@ public class HiddenSequencesTest
     /*
      * alignment is now seq0/2/3/4/7/8/9
      */
-    assertEquals(7, al.getHeight());
+    assertEquals(SEQ_COUNT - 3, al.getHeight());
     assertEquals(0, hs.adjustForHiddenSeqs(0));
     assertEquals(2, hs.adjustForHiddenSeqs(1));
     assertEquals(3, hs.adjustForHiddenSeqs(2));
@@ -193,7 +194,7 @@ public class HiddenSequencesTest
     /*
      * alignment is now seq0/2/3/4/7/8/9
      */
-    assertEquals(7, al.getHeight());
+    assertEquals(SEQ_COUNT - 3, al.getHeight());
     assertEquals(0, hs.findIndexWithoutHiddenSeqs(0));
     assertEquals(0, hs.findIndexWithoutHiddenSeqs(1));
     assertEquals(1, hs.findIndexWithoutHiddenSeqs(2));
@@ -207,6 +208,76 @@ public class HiddenSequencesTest
   }
 
   /**
+   * Test the method that finds the visible row position a given distance before
+   * another row
+   */
+  @Test(groups = { "Functional" })
+  public void testFindIndexNFromRow()
+  {
+    AlignmentI al = new Alignment(seqs);
+    HiddenSequences hs = new HiddenSequences(al);
+
+    // test that without hidden rows, findIndexNFromRow returns
+    // position n above provided position
+    int pos = hs.subtractVisibleRows(3, 10);
+    assertEquals(7, pos);
+
+    // 0 returns same position
+    pos = hs.subtractVisibleRows(0, 10);
+    assertEquals(10, pos);
+
+    // overflow to top returns negative number
+    pos = hs.subtractVisibleRows(3, 0);
+    assertEquals(-3, pos);
+
+    // test that with hidden rows above result row
+    // behaviour is the same as above
+    hs.hideSequence(seqs[1]);
+    hs.hideSequence(seqs[2]);
+    hs.hideSequence(seqs[3]);
+
+    // position n above provided position
+    pos = hs.subtractVisibleRows(3, 10);
+    assertEquals(7, pos);
+
+    // 0 returns same position
+    pos = hs.subtractVisibleRows(0, 10);
+    assertEquals(10, pos);
+
+    // test with one set of hidden rows between start and required position
+    hs.hideSequence(seqs[12]);
+    hs.hideSequence(seqs[13]);
+    hs.hideSequence(seqs[14]);
+    hs.hideSequence(seqs[15]);
+    pos = hs.subtractVisibleRows(8, 17);
+    assertEquals(5, pos);
+
+    // test with two sets of hidden rows between start and required position
+    hs.hideSequence(seqs[20]);
+    hs.hideSequence(seqs[21]);
+    pos = hs.subtractVisibleRows(8, 23);
+    assertEquals(9, pos);
+
+    // repeat last 2 tests with no hidden columns to left of required position
+    hs.showAll(null);
+
+    // test with one set of hidden rows between start and required position
+    hs.hideSequence(seqs[12]);
+    hs.hideSequence(seqs[13]);
+    hs.hideSequence(seqs[14]);
+    hs.hideSequence(seqs[15]);
+    pos = hs.subtractVisibleRows(8, 17);
+    assertEquals(5, pos);
+
+    // test with two sets of hidden rows between start and required position
+    hs.hideSequence(seqs[20]);
+    hs.hideSequence(seqs[21]);
+    pos = hs.subtractVisibleRows(8, 23);
+    assertEquals(9, pos);
+
+  }
+
+  /**
    * Test the method that reconstructs (sort of) the full alignment including
    * hidden sequences
    */
@@ -289,7 +360,7 @@ public class HiddenSequencesTest
     assertTrue(al.getSequences().contains(seqs[1]));
     HiddenSequences hs = al.getHiddenSequences();
     assertEquals(0, hs.getSize());
-    assertEquals(10, al.getHeight());
+    assertEquals(SEQ_COUNT, al.getHeight());
 
     /*
      * hide the second sequence in the alignment
@@ -299,7 +370,7 @@ public class HiddenSequencesTest
     assertTrue(hs.isHidden(seqs[1]));
     assertFalse(al.getSequences().contains(seqs[1]));
     assertEquals(1, hs.getSize());
-    assertEquals(9, al.getHeight());
+    assertEquals(SEQ_COUNT - 1, al.getHeight());
     assertSame(seqs[2], al.getSequenceAt(1));
 
     /*
@@ -312,7 +383,7 @@ public class HiddenSequencesTest
     assertFalse(al.getSequences().contains(seqs[1]));
     assertFalse(al.getSequences().contains(seqs[2]));
     assertEquals(2, hs.getSize());
-    assertEquals(8, al.getHeight());
+    assertEquals(SEQ_COUNT - 2, al.getHeight());
 
     /*
      * perform 'reveal' on what is now the second sequence in the alignment
@@ -323,7 +394,54 @@ public class HiddenSequencesTest
     assertTrue(revealed.contains(seqs[1]));
     assertTrue(revealed.contains(seqs[2]));
     assertEquals(0, hs.getSize());
-    assertEquals(10, al.getHeight());
+    assertEquals(SEQ_COUNT, al.getHeight());
+  }
+
+  /**
+   * Test the method that adds a sequence to the hidden sequences and deletes it
+   * from the alignment, and its converse, where the first hidden sequences are
+   * at the bottom of the alignment (JAL-2437)
+   */
+  @Test(groups = "Functional")
+  public void testHideShowLastSequences()
+  {
+    AlignmentI al = new Alignment(seqs);
+    assertTrue(al.getSequences().contains(seqs[1]));
+    HiddenSequences hs = al.getHiddenSequences();
+    assertEquals(0, hs.getSize());
+    assertEquals(SEQ_COUNT, al.getHeight());
+
+    /*
+     * hide the last sequence in the alignment
+     */
+    hs.hideSequence(seqs[SEQ_COUNT - 1]);
+    assertFalse(hs.isHidden(seqs[SEQ_COUNT - 2]));
+    assertTrue(hs.isHidden(seqs[SEQ_COUNT - 1]));
+    assertFalse(al.getSequences().contains(seqs[SEQ_COUNT - 1]));
+    assertEquals(1, hs.getSize());
+    assertEquals(SEQ_COUNT - 1, al.getHeight());
+
+    /*
+     * hide the third last sequence in the alignment
+     */
+    hs.hideSequence(seqs[SEQ_COUNT - 3]);
+    assertFalse(hs.isHidden(seqs[SEQ_COUNT - 2]));
+    assertTrue(hs.isHidden(seqs[SEQ_COUNT - 3]));
+    assertFalse(al.getSequences().contains(seqs[SEQ_COUNT - 3]));
+    assertEquals(2, hs.getSize());
+    assertEquals(SEQ_COUNT - 2, al.getHeight());
+
+    /*
+     * reveal all the sequences, which should be reinstated in the same order as they started in
+     */
+    hs.showAll(null);
+    assertFalse(hs.isHidden(seqs[SEQ_COUNT - 3]));
+    assertFalse(hs.isHidden(seqs[SEQ_COUNT - 1]));
+    assertEquals(seqs[SEQ_COUNT - 3], al.getSequences().get(SEQ_COUNT - 3));
+    assertEquals(seqs[SEQ_COUNT - 2], al.getSequences().get(SEQ_COUNT - 2));
+    assertEquals(seqs[SEQ_COUNT - 1], al.getSequences().get(SEQ_COUNT - 1));
+    assertEquals(0, hs.getSize());
+    assertEquals(SEQ_COUNT, al.getHeight());
   }
 
   @Test(groups = "Functional")
index 2da8918..d105cc5 100644 (file)
@@ -123,51 +123,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 +225,21 @@ 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());
   }
 }
diff --git a/test/jalview/datamodel/SequenceGroupTest.java b/test/jalview/datamodel/SequenceGroupTest.java
new file mode 100644 (file)
index 0000000..f6d4028
--- /dev/null
@@ -0,0 +1,293 @@
+package jalview.datamodel;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
+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.schemes.NucleotideColourScheme;
+import jalview.schemes.PIDColourScheme;
+
+import java.awt.Color;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class SequenceGroupTest
+{
+  @Test(groups={"Functional"})
+  public void testAddSequence()
+  {
+    SequenceGroup sg = new SequenceGroup();
+    assertTrue(sg.getSequences().isEmpty());
+
+    SequenceI seq1 = new Sequence("seq1", "abc");
+    SequenceI seq2 = new Sequence("seq2", "abc");
+    SequenceI seq3 = new Sequence(seq1);
+
+    sg.addSequence(null, false);
+    assertTrue(sg.getSequences().isEmpty());
+    sg.addSequence(seq1, false);
+    assertEquals(sg.getSequences().size(), 1);
+    assertTrue(sg.getSequences().contains(seq1));
+    // adding the same sequence again does nothing
+    sg.addSequence(seq1, false);
+    assertEquals(sg.getSequences().size(), 1);
+    assertTrue(sg.getSequences().contains(seq1));
+    sg.addSequence(seq2, false);
+    sg.addSequence(seq2, false);
+    sg.addSequence(seq3, false);
+    assertEquals(sg.getSequences().size(), 3);
+    assertTrue(sg.getSequences().contains(seq1));
+    assertTrue(sg.getSequences().contains(seq2));
+    assertTrue(sg.getSequences().contains(seq3));
+  }
+
+  @Test(groups={"Functional"})
+  public void testAddOrRemove()
+  {
+    SequenceGroup sg = new SequenceGroup();
+    assertTrue(sg.getSequences().isEmpty());
+  
+    SequenceI seq1 = new Sequence("seq1", "abc");
+    SequenceI seq2 = new Sequence("seq2", "abc");
+    SequenceI seq3 = new Sequence(seq1);
+  
+    sg.addOrRemove(seq1, false);
+    assertEquals(sg.getSequences().size(), 1);
+    sg.addOrRemove(seq2, false);
+    assertEquals(sg.getSequences().size(), 2);
+    sg.addOrRemove(seq3, false);
+    assertEquals(sg.getSequences().size(), 3);
+    assertTrue(sg.getSequences().contains(seq1));
+    assertTrue(sg.getSequences().contains(seq2));
+    assertTrue(sg.getSequences().contains(seq3));
+    sg.addOrRemove(seq1, false);
+    assertEquals(sg.getSequences().size(), 2);
+    assertFalse(sg.getSequences().contains(seq1));
+  }
+
+  @Test(groups={"Functional"})
+  public void testGetColourScheme()
+  {
+    SequenceGroup sg = new SequenceGroup();
+    assertNotNull(sg.getGroupColourScheme());
+    assertNull(sg.getColourScheme());
+
+    sg.setGroupColourScheme(null);
+    assertNull(sg.getColourScheme());
+
+    NucleotideColourScheme scheme = new NucleotideColourScheme();
+    sg.setColourScheme(scheme);
+    assertSame(scheme, sg.getColourScheme());
+  }
+
+  @Test(groups={"Functional"})
+  public void testSetContext()
+  {
+    SequenceGroup sg1 = new SequenceGroup();
+    SequenceGroup sg2 = new SequenceGroup();
+    SequenceGroup sg3 = new SequenceGroup();
+    assertNull(sg1.getContext());
+    sg1.setContext(null);
+    assertNull(sg1.getContext());
+    try
+    {
+      sg1.setContext(sg1); // self-reference :-O
+      fail("Expected exception");
+    } catch (IllegalArgumentException e)
+    {
+      // expected
+      assertNull(sg1.getContext());
+    }
+    sg1.setContext(sg2);
+    assertSame(sg2, sg1.getContext());
+    sg2.setContext(sg3);
+    try
+    {
+      sg3.setContext(sg1); // circular reference :-O
+      fail("Expected exception");
+    } catch (IllegalArgumentException e)
+    {
+      // expected
+      assertNull(sg3.getContext());
+    }
+
+    /*
+     * use PrivilegedAccessor to 'force' a SequenceGroup with
+     * a circular context reference
+     */
+    PA.setValue(sg2, "context", sg2);
+    try
+    {
+      sg3.setContext(sg2, false); // circular reference in sg2
+      fail("Expected exception");
+    } catch (IllegalArgumentException e)
+    {
+      // expected
+      assertNull(sg3.getContext());
+    }
+
+    // test isDefined setting behaviour
+    sg2 = new SequenceGroup();
+    sg1.setContext(null, false);
+    assertFalse(sg1.isDefined());
+
+    sg1.setContext(sg2, false);
+    assertFalse(sg1.isDefined());
+
+    sg1.setContext(sg2, true);
+    assertTrue(sg1.isDefined());
+
+    // setContext without defined parameter does not change isDefined
+    sg1.setContext(null);
+    assertTrue(sg1.isDefined());
+
+    sg1.setContext(null, false);
+    sg1.setContext(sg2);
+    assertFalse(sg1.isDefined());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testContains()
+  {
+    /* 
+     * essentially the same tests as AlignmentI.findGroup 
+     * but from a particular group's perspective
+     */
+
+    SequenceI seq1 = new Sequence("seq1", "ABCDEF---GHI");
+    SequenceI seq2 = new Sequence("seq2", "---JKLMNO---");
+    AlignmentI a = new Alignment(new SequenceI[] { seq1, seq2 });
+    /*
+     * add a group consisting of just "DEF"
+     */
+    SequenceGroup sg1 = new SequenceGroup();
+    sg1.addSequence(seq1, false);
+    sg1.setStartRes(3);
+    sg1.setEndRes(5);
+
+    /*
+     * test sequence membership
+     */
+    assertTrue(sg1.contains(seq1));
+    assertFalse(sg1.contains(seq2));
+
+    /*
+     * test sequence+position
+     */
+
+    assertFalse(sg1.contains(seq1, 2)); // position not in group
+    assertFalse(sg1.contains(seq1, 6)); // position not in group
+    assertFalse(sg1.contains(seq2, 5)); // sequence not in group
+    assertTrue(sg1.contains(seq1, 3)); // yes
+    assertTrue(sg1.contains(seq1, 4));
+    assertTrue(sg1.contains(seq1, 5));
+
+    /*
+     * add a group consisting of 
+     * EF--
+     * KLMN
+     */
+    SequenceGroup sg2 = new SequenceGroup();
+    sg2.addSequence(seq1, false);
+    sg2.addSequence(seq2, false);
+    sg2.setStartRes(4);
+    sg2.setEndRes(7);
+    a.addGroup(sg2);
+
+    /*
+     * if a residue is in more than one group, method returns
+     * the first found (in order groups were added)
+     */
+    assertTrue(sg2.contains(seq1, 4));
+    assertTrue(sg2.contains(seq1, 5));
+
+    /*
+     * seq2 only belongs to the second group
+     */
+    assertTrue(sg2.contains(seq2, 4));
+    assertTrue(sg2.contains(seq2, 5));
+    assertTrue(sg2.contains(seq2, 6));
+    assertTrue(sg2.contains(seq2, 7));
+    assertFalse(sg2.contains(seq2, 3));
+    assertFalse(sg2.contains(seq2, 8));
+    sg2.setEndRes(8);
+    assertTrue(sg2.contains(seq2, 8));
+    sg2.deleteSequence(seq2, false);
+    assertFalse(sg2.contains(seq2));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCopyConstructor()
+  {
+    SequenceI seq = new Sequence("seq", "ABC");
+    SequenceGroup sg = new SequenceGroup();
+    sg.addSequence(seq, false);
+    sg.setName("g1");
+    sg.setDescription("desc");
+    sg.setColourScheme(new PIDColourScheme());
+    sg.setDisplayBoxes(false);
+    sg.setDisplayText(false);
+    sg.setColourText(true);
+    sg.isDefined = true;
+    sg.setShowNonconserved(true);
+    sg.setOutlineColour(Color.red);
+    sg.setIdColour(Color.blue);
+    sg.thresholdTextColour = 1;
+    sg.textColour = Color.orange;
+    sg.textColour2 = Color.yellow;
+    sg.setIgnoreGapsConsensus(false);
+    sg.setshowSequenceLogo(true);
+    sg.setNormaliseSequenceLogo(true);
+    sg.setHidereps(true);
+    sg.setHideCols(true);
+    sg.setShowConsensusHistogram(true);
+    sg.setContext(new SequenceGroup());
+
+    SequenceGroup sg2 = new SequenceGroup(sg);
+    assertEquals(sg2.getName(), sg.getName());
+    assertEquals(sg2.getDescription(), sg.getDescription());
+    assertNotSame(sg2.getGroupColourScheme(), sg.getGroupColourScheme());
+    assertSame(sg2.getColourScheme(), sg.getColourScheme());
+    assertEquals(sg2.getDisplayBoxes(), sg.getDisplayBoxes());
+    assertEquals(sg2.getDisplayText(), sg.getDisplayText());
+    assertEquals(sg2.getColourText(), sg.getColourText());
+    assertEquals(sg2.getShowNonconserved(), sg.getShowNonconserved());
+    assertEquals(sg2.getOutlineColour(), sg.getOutlineColour());
+    assertEquals(sg2.getIdColour(), sg.getIdColour());
+    assertEquals(sg2.thresholdTextColour, sg.thresholdTextColour);
+    assertEquals(sg2.textColour, sg.textColour);
+    assertEquals(sg2.textColour2, sg.textColour2);
+    assertEquals(sg2.getIgnoreGapsConsensus(), sg.getIgnoreGapsConsensus());
+    assertEquals(sg2.isShowSequenceLogo(), sg.isShowSequenceLogo());
+    assertEquals(sg2.isNormaliseSequenceLogo(),
+            sg.isNormaliseSequenceLogo());
+    assertEquals(sg2.isHidereps(), sg.isHidereps());
+    assertEquals(sg2.isHideCols(), sg.isHideCols());
+    assertEquals(sg2.isShowConsensusHistogram(),
+            sg.isShowConsensusHistogram());
+
+    /*
+     * copy of sequences
+     */
+    assertNotSame(sg2.getSequences(), sg.getSequences());
+    assertEquals(sg2.getSequences(), sg.getSequences());
+
+    /*
+     * isDefined should only be set true when a new group is added to
+     * an alignment, not in the copy constructor
+     */
+    assertFalse(sg2.isDefined());
+
+    /*
+     * context should be set explicitly, not by copy
+     */
+    assertNull(sg2.getContext());
+  }
+}
index 08e6f7d..90856e8 100644 (file)
@@ -23,11 +23,13 @@ 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.commands.EditCommand;
+import jalview.commands.EditCommand.Action;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.gui.JvOptionPane;
 import jalview.util.MapList;
@@ -35,9 +37,12 @@ import jalview.util.MapList;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 import java.util.Vector;
 
+import junit.extensions.PA;
+
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
@@ -74,6 +79,18 @@ public class SequenceTest
     assertEquals("Gap interval 1 end wrong", 4, gapInt.get(0)[1]);
     assertEquals("Gap interval 2 start wrong", 6, gapInt.get(1)[0]);
     assertEquals("Gap interval 2 end wrong", 8, gapInt.get(1)[1]);
+
+    BitSet gapfield = aseq.getInsertionsAsBits();
+    BitSet expectedgaps = new BitSet();
+    expectedgaps.set(2, 5);
+    expectedgaps.set(6, 9);
+
+    assertEquals(6, expectedgaps.cardinality());
+
+    assertEquals("getInsertionsAsBits didn't mark expected number of gaps",
+            6, gapfield.cardinality());
+
+    assertEquals("getInsertionsAsBits not correct.", expectedgaps, gapfield);
   }
 
   @Test(groups = ("Functional"))
@@ -222,82 +239,350 @@ 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));
+    sq = new Sequence("test/8-13", "-A--B-C-D-E-F--");
+    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));
-
   }
 
   /**
-   * 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));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals("test:Pos12:Col9:startCol3:endCol10:tok1",
+            cursor.toString());
+    // findPosition with cursor accepts base 1 column values
+    assertEquals(13, ((Sequence) sq).findPosition(10, cursor));
+    assertEquals(13, sq.findPosition(9));
+    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"));
+
+    /*
+     * 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"));
+  }
+
+  @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" })
@@ -335,16 +620,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
@@ -353,18 +638,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
@@ -385,7 +671,7 @@ public class SequenceTest
       assertTrue(e.getMessage().toLowerCase()
               .contains("implementation error"));
     }
-    assertNull(sq.getSequenceFeatures());
+    assertTrue(sq.getSequenceFeatures().isEmpty());
   }
 
   /**
@@ -435,11 +721,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"));
   }
 
   /**
@@ -546,12 +844,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
@@ -681,18 +976,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();
@@ -711,6 +1006,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
    * 
@@ -1010,4 +1335,240 @@ 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 sf1 = new SequenceFeature("Cath", "desc", 9, 11, 2f,
+            null);
+    sq.addSequenceFeature(sf1);
+    // add feature on DE
+    SequenceFeature sf2 = new SequenceFeature("Cath", "desc", 11, 12, 2f,
+            null);
+    sq.addSequenceFeature(sf2);
+    // add contact feature at [B, H]
+    SequenceFeature sf3 = new SequenceFeature("Disulphide bond", "desc", 9,
+            15, 2f,
+            null);
+    sq.addSequenceFeature(sf3);
+    // add contact feature at [F, G]
+    SequenceFeature sf4 = new SequenceFeature("Disulfide Bond", "desc", 13,
+            14, 2f,
+            null);
+    sq.addSequenceFeature(sf4);
+
+    // 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(sf1));
+    assertTrue(found.contains(sf3));
+
+    // columns 5-6 (--) includes (enclosing) BCD but not (contact) B/H feature
+    found = sq.findFeatures(5, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sf1));
+
+    // 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(sf1));
+    assertTrue(found.contains(sf2));
+    assertTrue(found.contains(sf4));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFindIndex_withCursor()
+  {
+    Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
+
+    // find F given A
+    assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0)));
+
+    // find A given F
+    assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0)));
+
+    // find C given C
+    assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0)));
+  }
+
+  @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);
+
+    /*
+     * 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);
+  }
 }
diff --git a/test/jalview/datamodel/VisibleColsIteratorTest.java b/test/jalview/datamodel/VisibleColsIteratorTest.java
new file mode 100644 (file)
index 0000000..b2d747b
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * 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.assertTrue;
+
+import java.util.NoSuchElementException;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class VisibleColsIteratorTest
+{
+  HiddenColumns hiddenCols;
+
+  HiddenColumns hiddenColsAtStart;
+
+  @BeforeClass(groups = { "Functional" })
+  public void setup()
+  {
+    hiddenCols = new HiddenColumns();
+    hiddenCols.hideColumns(2, 4);
+
+    hiddenColsAtStart = new HiddenColumns();
+    hiddenColsAtStart.hideColumns(0, 2);
+  }
+
+  /*
+   * Test iterator iterates correctly through the columns
+   * when alignment has hidden cols
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextWithHidden()
+  {
+    VisibleColsIterator it = new VisibleColsIterator(0, 6, hiddenCols);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator iterates correctly through the columns
+   * when alignment has no hidden cols
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextNoHidden()
+  {
+    VisibleColsIterator it2 = new VisibleColsIterator(0, 3,
+            new HiddenColumns());
+    int count = 0;
+    while (it2.hasNext())
+    {
+      it2.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator iterates correctly through the columns
+   * when alignment has hidden cols at start
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextStartHidden()
+  {
+    VisibleColsIterator it3 = new VisibleColsIterator(0, 6,
+            hiddenColsAtStart);
+    int count = 0;
+    while (it3.hasNext())
+    {
+      it3.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator iterates correctly through the columns
+   * when alignment has hidden cols at end
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextEndHidden()
+  {
+    VisibleColsIterator it4 = new VisibleColsIterator(0, 4, hiddenCols);
+    int count = 0;
+    while (it4.hasNext())
+    {
+      it4.next();
+      count++;
+    }
+    assertTrue(count == 2, "hasNext() is false after 2 iterations");
+
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has hidden cols
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextWithHidden() throws NoSuchElementException
+  {
+    VisibleColsIterator it = new VisibleColsIterator(0, 3, hiddenCols);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has no hidden cols
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextNoHidden() throws NoSuchElementException
+  {
+    VisibleColsIterator it2 = new VisibleColsIterator(0, 3,
+            new HiddenColumns());
+    while (it2.hasNext())
+    {
+      it2.next();
+    }
+    it2.next();
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has hidden cols at start
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextStartHidden() throws NoSuchElementException
+  {
+    VisibleColsIterator it3 = new VisibleColsIterator(0, 6,
+            hiddenColsAtStart);
+    while (it3.hasNext())
+    {
+      it3.next();
+    }
+    it3.next();
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has hidden cols at end
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextEndHidden() throws NoSuchElementException
+  {
+    VisibleColsIterator it4 = new VisibleColsIterator(0, 4, hiddenCols);
+    while (it4.hasNext())
+    {
+      it4.next();
+    }
+    it4.next();
+  }
+
+  /*
+   * Test calls to remove throw UnsupportedOperationException
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { UnsupportedOperationException.class })
+  public void testRemove() throws UnsupportedOperationException
+  {
+    VisibleColsIterator it = new VisibleColsIterator(0, 3, hiddenCols);
+    it.remove();
+  }
+}
diff --git a/test/jalview/datamodel/VisibleRowsIteratorTest.java b/test/jalview/datamodel/VisibleRowsIteratorTest.java
new file mode 100644 (file)
index 0000000..4a021c5
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * 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.assertTrue;
+
+import jalview.analysis.AlignmentGenerator;
+
+import java.util.Hashtable;
+import java.util.NoSuchElementException;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class VisibleRowsIteratorTest
+{
+  AlignmentI al;
+
+  AlignmentI al2;
+
+  AlignmentI al3;
+
+  Hashtable<SequenceI, SequenceCollectionI> hiddenRepSequences = new Hashtable<SequenceI, SequenceCollectionI>();
+
+  Hashtable<SequenceI, SequenceCollectionI> hiddenRepSequences2 = new Hashtable<SequenceI, SequenceCollectionI>();
+
+  @BeforeClass(groups = { "Functional" })
+  public void setup()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    al = gen.generate(20, 15, 123, 5, 5);
+    if (!hiddenRepSequences.isEmpty())
+    {
+      al.getHiddenSequences().showAll(hiddenRepSequences);
+    }
+    hideSequences(al, hiddenRepSequences, 2, 4);
+
+    al2 = gen.generate(20, 15, 123, 5, 5);
+    if (!hiddenRepSequences2.isEmpty())
+    {
+      al2.getHiddenSequences().showAll(hiddenRepSequences2);
+    }
+    hideSequences(al2, hiddenRepSequences2, 0, 2);
+
+    al3 = gen.generate(20, 15, 123, 5, 5);
+  }
+
+  /*
+   * Test iterator iterates correctly through the rows
+   * when alignment has hidden rows
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextWithHidden()
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 6, al);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator iterates correctly through the rows
+   * when alignment has no hidden rows
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextNoHidden()
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 3, al3);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator iterates correctly through the rows
+   * when alignment has hidden rows at start
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextStartHidden()
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 6, al2);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 4, "hasNext() is false after 4 iterations");
+  }
+
+  /*
+   * Test iterator iterates correctly through the rows
+   * when alignment has hidden rows at end
+   */
+  @Test(groups = { "Functional" })
+  public void testHasNextAndNextEndHidden()
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 4, al);
+    int count = 0;
+    while (it.hasNext())
+    {
+      it.next();
+      count++;
+    }
+    assertTrue(count == 2, "hasNext() is false after 2 iterations");
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has hidden rows
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextWithHidden() throws NoSuchElementException
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 3, al);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has no hidden rows
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextNoHidden() throws NoSuchElementException
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 3, al3);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has hidden rows at start
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextStartHidden() throws NoSuchElementException
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 3, al2);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test iterator always throws NoSuchElementException at end of iteration
+   * when alignment has hidden rows at end
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { NoSuchElementException.class })
+  public void testLastNextEndHidden() throws NoSuchElementException
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 4, al);
+    while (it.hasNext())
+    {
+      it.next();
+    }
+    it.next();
+  }
+
+  /*
+   * Test calls to remove throw UnsupportedOperationException
+   */
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { UnsupportedOperationException.class })
+  public void testRemove() throws UnsupportedOperationException
+  {
+    VisibleRowsIterator it = new VisibleRowsIterator(0, 3, al);
+    it.remove();
+  }
+
+  /*
+   * Hide sequences between start and end
+   */
+  private void hideSequences(AlignmentI alignment,
+          Hashtable<SequenceI, SequenceCollectionI> hiddenRepSequences,
+          int start, int end)
+  {
+    SequenceI[] allseqs = alignment.getSequencesArray();
+    SequenceGroup theseSeqs = new SequenceGroup();
+
+    for (int i = start; i <= end; i++)
+    {
+      theseSeqs.addSequence(allseqs[i], false);
+      alignment.getHiddenSequences().hideSequence(allseqs[i]);
+    }
+
+    hiddenRepSequences.put(allseqs[start], theseSeqs);
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureStoreTest.java b/test/jalview/datamodel/features/FeatureStoreTest.java
new file mode 100644 (file)
index 0000000..f5be818
--- /dev/null
@@ -0,0 +1,830 @@
+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.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", "", 0, 0,
+            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", "", 0, 0,
+            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 testContains()
+  {
+    assertFalse(FeatureStore.contains(null, null));
+    List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+    assertFalse(FeatureStore.contains(features, null));
+
+    SequenceFeature sf1 = new SequenceFeature("type1", "desc1", 20, 30, 3f,
+            "group1");
+    assertFalse(FeatureStore.contains(null, sf1));
+    assertFalse(FeatureStore.contains(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.contains(features, sf2));
+    assertFalse(FeatureStore.contains(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(1));
+
+    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 features right by 5
+     */
+    assertTrue(fs.shiftFeatures(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(-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);
+  }
+}
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..b7b52a7
--- /dev/null
@@ -0,0 +1,1217 @@
+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.Set;
+
+import org.testng.annotations.Test;
+
+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
+     */
+    Iterable<String> types = sf.varargToTypes();
+    Iterator<String> iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "Cath");
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "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());
+    assertEquals(iterator.next(), "Cath");
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "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());
+    assertEquals(iterator.next(), "Cath");
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "Metal");
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one type specified
+     */
+    types = sf.varargToTypes("Metal");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "Metal");
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two types specified - get sorted alphabetically
+     */
+    types = sf.varargToTypes("Metal", "Helix");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "Helix");
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "Metal");
+    assertFalse(iterator.hasNext());
+
+    /*
+     * null type included - should get removed
+     */
+    types = sf.varargToTypes("Metal", null, "Helix");
+    iterator = types.iterator();
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "Helix");
+    assertTrue(iterator.hasNext());
+    assertEquals(iterator.next(), "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 sf1 = new SequenceFeature("transcript", "desc", 10, 20,
+            Float.NaN, null);
+    store.add(sf1);
+
+    // mRNA isA transcript; added here 'as if' non-positional
+    // just to show that non-positional features are included in results
+    SequenceFeature sf2 = new SequenceFeature("mRNA", "desc", 0, 0,
+            Float.NaN, null);
+    store.add(sf2);
+
+    SequenceFeature sf3 = new SequenceFeature("Pfam", "desc", 30, 40,
+            Float.NaN, null);
+    store.add(sf3);
+
+    features = store.getFeaturesByOntology("transcript");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf2));
+
+    features = store.getFeaturesByOntology("mRNA");
+    assertEquals(features.size(), 1);
+    assertTrue(features.contains(sf2));
+
+    features = store.getFeaturesByOntology("mRNA", "Pfam");
+    assertEquals(features.size(), 2);
+    assertTrue(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+  }
+
+  @Test(groups = "Functional")
+  public void testSortFeatures()
+  {
+    List<SequenceFeature> sfs = new ArrayList<SequenceFeature>();
+    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(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(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(-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");
+  }
+
+  @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));
+  }
+}
diff --git a/test/jalview/ext/android/SparseDoubleArrayTest.java b/test/jalview/ext/android/SparseDoubleArrayTest.java
new file mode 100644 (file)
index 0000000..7d64a28
--- /dev/null
@@ -0,0 +1,53 @@
+package jalview.ext.android;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class SparseDoubleArrayTest
+{
+
+  @Test
+  public void testConstructor()
+  {
+    double[] d = new double[] { 0d, 0d, 1.2d, 0d, 0d, 3.4d };
+    SparseDoubleArray s = new SparseDoubleArray(d);
+    for (int i = 0; i < d.length; i++)
+    {
+      assertEquals(s.get(i), d[i], "At [" + i + "]");
+    }
+  }
+
+  @Test
+  public void testAdd()
+  {
+    double[] d = new double[] { 0d, 0d, 1.2d, 0d, 0d, 3.4d };
+    SparseDoubleArray s = new SparseDoubleArray(d);
+    // add to zero (absent)
+    s.add(0, 3.2d);
+    assertEquals(s.get(0), 3.2d);
+    // add to non-zero
+    s.add(0, 2.5d);
+    assertEquals(s.get(0), 5.7d);
+    // add negative value
+    s.add(2, -5.3d);
+    assertEquals(s.get(2), -4.1d);
+    // add to unset value
+    s.add(12, 9.8d);
+    assertEquals(s.get(12), 9.8d);
+  }
+
+  @Test
+  public void testDivide()
+  {
+    double delta = 1.0e-10;
+    double[] d = new double[] { 0d, 2.4d, 1.2d, 0d, -4.8d, -3.6d };
+    SparseDoubleArray s = new SparseDoubleArray(d);
+    assertEquals(s.divide(0, 1d), 0d); // no such entry
+    assertEquals(s.divide(2, 0d), 0d); // zero divisor
+    assertEquals(s.divide(1, 2d), 1.2d, delta); // + / +
+    assertEquals(s.divide(2, -2d), -0.6d, delta); // + / -
+    assertEquals(s.divide(4, 3d), -1.6d, delta); // - / +
+    assertEquals(s.divide(5, -3d), 1.2d, delta); // - / -
+  }
+}
index fb0204b..6611e05 100644 (file)
@@ -212,14 +212,16 @@ 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
@@ -254,15 +256,18 @@ public class EnsemblCdnaTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // exon sub-type with right parent is valid
-    sf.setType("coding_exon");
+    sf = new SequenceFeature("coding_exon", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript not valid:
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // CDS not valid:
-    sf.setType("CDS");
+    sf = new SequenceFeature("CDS", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index b7f9f8d..8482c90 100644 (file)
@@ -130,11 +130,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
@@ -169,15 +170,18 @@ public class EnsemblCdsTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // cds sub-type with right parent is valid
-    sf.setType("CDS_predicted");
+    sf = new SequenceFeature("CDS_predicted", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript not valid:
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // exon not valid:
-    sf.setType("exon");
+    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf.setValue("Parent", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index 6cfd85b..a8c491c 100644 (file)
@@ -22,7 +22,6 @@ 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;
@@ -76,7 +75,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // gene at (start+10000) length 501
+    // gene at (start+20000) length 501
+    // should be ignored - the first 'gene' found defines the whole range
+    // (note features are found in position order, not addition order)
     SequenceFeature sf = new SequenceFeature("gene", "", 20000, 20500, 0f,
             null);
     sf.setValue("ID", "gene:" + geneId);
@@ -84,7 +85,6 @@ public class EnsemblGeneTest
     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);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
@@ -94,13 +94,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,7 +115,9 @@ public class EnsemblGeneTest
     genomic.setEnd(50000);
     String geneId = "ABC123";
 
-    // gene at (start+10000) length 501
+    // gene at (start+20000) length 501
+    // should be ignored - the first 'gene' found defines the whole range
+    // (real data would only have one such feature)
     SequenceFeature sf = new SequenceFeature("ncRNA_gene", "", 20000,
             20500, 0f, null);
     sf.setValue("ID", "gene:" + geneId);
@@ -123,8 +125,6 @@ public class EnsemblGeneTest
     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);
     sf.setValue("ID", "gene:" + geneId);
     sf.setStrand("+");
@@ -135,13 +135,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 +164,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 +172,13 @@ public class EnsemblGeneTest
 
     // NMD_transcript_variant treated like transcript in Ensembl
     SequenceFeature sf3 = new SequenceFeature("NMD_transcript_variant", "",
-            20000, 20500, 0f, null);
+            22000, 22500, 0f, null);
     sf3.setValue("Parent", "gene:" + geneId);
     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 +192,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 +211,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));
   }
 
@@ -253,15 +255,18 @@ public class EnsemblGeneTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // gene sub-type with right ID is valid
-    sf.setType("snRNA_gene");
+    sf = new SequenceFeature("snRNA_gene", "", 1, 2, 0f, null);
+    sf.setValue("ID", "gene:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript not valid:
-    sf.setType("transcript");
+    sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
+    sf.setValue("ID", "gene:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // exon not valid:
-    sf.setType("exon");
+    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf.setValue("ID", "gene:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index 654797c..8687da9 100644 (file)
@@ -136,14 +136,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
@@ -179,19 +181,23 @@ public class EnsemblGenomeTest
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // transcript sub-type with right ID is valid
-    sf.setType("ncRNA");
+    sf = new SequenceFeature("ncRNA", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // Ensembl treats NMD_transcript_variant as if a transcript
-    sf.setType("NMD_transcript_variant");
+    sf = new SequenceFeature("NMD_transcript_variant", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
     // gene not valid:
-    sf.setType("gene");
+    sf = new SequenceFeature("gene", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
 
     // exon not valid:
-    sf.setType("exon");
+    sf = new SequenceFeature("exon", "", 1, 2, 0f, null);
+    sf.setValue("ID", "transcript:" + accId);
     assertFalse(testee.identifiesSequence(sf, accId));
   }
 
index e977233..aa2c315 100644 (file)
@@ -22,12 +22,13 @@ 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 static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
 import jalview.datamodel.Alignment;
 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;
@@ -37,6 +38,7 @@ 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;
@@ -269,15 +271,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);
   }
 }
index 439e188..3309adf 100644 (file)
  */
 package jalview.ext.jmol;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.gui.SequenceRenderer;
+import jalview.schemes.JalviewColourScheme;
+import jalview.structure.StructureMapping;
 import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 
+import java.util.HashMap;
+
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -58,6 +66,77 @@ public class JmolCommandsTest
     // need some mappings!
 
     StructureMappingcommandSet[] commands = JmolCommands
-            .getColourBySequenceCommand(ssm, files, seqs, sr, null, al);
+            .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetColourBySequenceCommands_hiddenColumns()
+  {
+    /*
+     * load these sequences, coloured by Strand propensity,
+     * with columns 2-4 hidden
+     */
+    SequenceI seq1 = new Sequence("seq1", "MHRSQSSSGG");
+    SequenceI seq2 = new Sequence("seq2", "MVRSNGGSSS");
+    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
+    AlignFrame af = new AlignFrame(al, 800, 500);
+    af.changeColour_actionPerformed(JalviewColourScheme.Strand.toString());
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(2);
+    cs.addElement(3);
+    cs.addElement(4);
+    af.getViewport().setColumnSelection(cs);
+    af.hideSelColumns_actionPerformed(null);
+    SequenceRenderer sr = new SequenceRenderer(af.getViewport());
+    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
+    String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
+    StructureSelectionManager ssm = new StructureSelectionManager();
+  
+    /*
+     * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
+     */
+    HashMap<Integer, int[]> map = new HashMap<Integer, int[]>();
+    for (int pos = 1; pos <= seq1.getLength(); pos++)
+    {
+      map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) });
+    }
+    StructureMapping sm1 = new StructureMapping(seq1, "seq1.pdb", "pdb1",
+            "A", map, null);
+    ssm.addStructureMapping(sm1);
+    StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
+            "B", map, null);
+    ssm.addStructureMapping(sm2);
+  
+    StructureMappingcommandSet[] commands = JmolCommands
+            .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+    assertEquals(commands.length, 2);
+    assertEquals(commands[0].commands.length, 1);
+
+    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]"));
+    // H colour is #60609f == (96, 96, 159)
+    assertTrue(chainACommand.contains(";select 22:A/1.1;color[96,96,159]"));
+    // hidden columns are Gray (128, 128, 128)
+    assertTrue(chainACommand
+            .contains(";select 23-25:A/1.1;color[128,128,128]"));
+    // S and G are both coloured #4949b6 == (73, 73, 182)
+    assertTrue(chainACommand
+            .contains(";select 26-30:A/1.1;color[73,73,182]"));
+
+    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]"));
+    // V colour is #ffff00 == (255, 255, 0)
+    assertTrue(chainBCommand
+.contains(";select 22:B/2.1;color[255,255,0]"));
+    // hidden columns are Gray (128, 128, 128)
+    assertTrue(chainBCommand
+            .contains(";select 23-25:B/2.1;color[128,128,128]"));
+    // S and G are both coloured #4949b6 == (73, 73, 182)
+    assertTrue(chainBCommand
+            .contains(";select 26-30:B/2.1;color[73,73,182]"));
   }
 }
index 131ef41..f5e637c 100644 (file)
@@ -272,15 +272,14 @@ public class JmolParserTest
      * local structure files should yield a false ID based on the filename
      */
     assertNotNull(structureData.getId());
-    assertEquals(structureData.getId(), "localstruct.pdb");
+    assertEquals(structureData.getId(), "localstruct");
     assertNotNull(structureData.getSeqs());
     /*
      * 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.pdb");
-
+    String featureGroup = structureData.getSeqs().get(0)
+            .getSequenceFeatures().get(0).featureGroup;
+    assertNotNull(featureGroup);
+    assertEquals(featureGroup, "localstruct");
   }
 }
index 995b099..792f7ad 100644 (file)
@@ -115,4 +115,6 @@ public class JmolViewerTest
       }
     }
   }
+
+
 }
index 70c6922..2c973ca 100644 (file)
@@ -23,7 +23,18 @@ package jalview.ext.rbvi.chimera;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
+import jalview.gui.SequenceRenderer;
+import jalview.schemes.JalviewColourScheme;
+import jalview.structure.StructureMapping;
+import jalview.structure.StructureMappingcommandSet;
+import jalview.structure.StructureSelectionManager;
 
 import java.awt.Color;
 import java.util.HashMap;
@@ -49,15 +60,15 @@ public class ChimeraCommandsTest
   {
 
     Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
-    ChimeraCommands.addRange(map, Color.blue, 0, 2, 5, "A");
-    ChimeraCommands.addRange(map, Color.blue, 0, 7, 7, "B");
-    ChimeraCommands.addRange(map, Color.blue, 0, 9, 23, "A");
-    ChimeraCommands.addRange(map, Color.blue, 1, 1, 1, "A");
-    ChimeraCommands.addRange(map, Color.blue, 1, 4, 7, "B");
-    ChimeraCommands.addRange(map, Color.yellow, 1, 8, 8, "A");
-    ChimeraCommands.addRange(map, Color.yellow, 1, 3, 5, "A");
-    ChimeraCommands.addRange(map, Color.red, 0, 3, 5, "A");
-    ChimeraCommands.addRange(map, Color.red, 0, 6, 9, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 0, 2, 5, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 0, 7, 7, "B");
+    ChimeraCommands.addColourRange(map, Color.blue, 0, 9, 23, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 1, 1, 1, "A");
+    ChimeraCommands.addColourRange(map, Color.blue, 1, 4, 7, "B");
+    ChimeraCommands.addColourRange(map, Color.yellow, 1, 8, 8, "A");
+    ChimeraCommands.addColourRange(map, Color.yellow, 1, 3, 5, "A");
+    ChimeraCommands.addColourRange(map, Color.red, 0, 3, 5, "A");
+    ChimeraCommands.addColourRange(map, Color.red, 0, 6, 9, "A");
 
     // Colours should appear in the Chimera command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
@@ -80,7 +91,7 @@ public class ChimeraCommandsTest
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addRange(featureValues, "X", 0, 8, 20, "A");
+    ChimeraCommands.addColourRange(featureValues, "X", 0, 8, 20, "A");
   
     List<String> commands = ChimeraCommands
             .buildSetAttributeCommands(featuresMap);
@@ -90,43 +101,46 @@ public class ChimeraCommandsTest
      * feature name gets a jv_ namespace prefix
      * feature value is quoted in case it contains spaces
      */
-    assertEquals(commands.get(0), "setattr r jv_chain \"X\" #0:8-20.A");
+    assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addColourRange(featureValues, "X", 0, 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addRange(featureValues, "X", 0, 21, 25, "A");
+    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
-    assertEquals(commands.get(0), "setattr r jv_chain \"X\" #0:3-25.A");
+    assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A");
 
     // same feature value and model, different chain
-    ChimeraCommands.addRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addColourRange(featureValues, "X", 0, 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addRange(featureValues, "X", 1, 26, 30, "A");
+    ChimeraCommands.addColourRange(featureValues, "X", 1, 26, 30, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(1, commands.size());
     assertEquals(commands.get(0),
-            "setattr r jv_chain \"X\" #0:3-25.A,21-25.B|#1:26-30.A");
+            "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A");
 
     // same feature, different value
-    ChimeraCommands.addRange(featureValues, "Y", 0, 40, 50, "A");
+    ChimeraCommands.addColourRange(featureValues, "Y", 0, 40, 50, "A");
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
     // so use contains to test for the expected command:
     assertTrue(commands
-            .contains("setattr r jv_chain \"X\" #0:3-25.A,21-25.B|#1:26-30.A"));
-    assertTrue(commands.contains("setattr r jv_chain \"Y\" #0:40-50.A"));
+            .contains("setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A"));
+    assertTrue(commands.contains("setattr r jv_chain 'Y' #0:40-50.A"));
 
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
-    ChimeraCommands.addRange(featureValues, "metal ion!", 0, 7, 15, "A");
-    // feature names are sanitised to change space or hyphen to underscore
+    ChimeraCommands.addColourRange(featureValues,
+            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
+            "A");
+    // feature names are sanitised to change non-alphanumeric to underscore
+    // feature values are sanitised to encode single quote characters
     commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
     assertTrue(commands
-            .contains("setattr r jv_side_chain_binding_ \"metal ion!\" #0:7-15.A"));
+            .contains("setattr r jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' #0:7-15.A"));
   }
 
   /**
@@ -148,4 +162,59 @@ public class ChimeraCommandsTest
     assertEquals(ChimeraCommands.makeAttributeName("helixColor"),
             "jv_helixColor_");
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetColourBySequenceCommands_hiddenColumns()
+  {
+    /*
+     * load these sequences, coloured by Strand propensity,
+     * with columns 2-4 hidden
+     */
+    SequenceI seq1 = new Sequence("seq1", "MHRSQSSSGG");
+    SequenceI seq2 = new Sequence("seq2", "MVRSNGGSSS");
+    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
+    AlignFrame af = new AlignFrame(al, 800, 500);
+    af.changeColour_actionPerformed(JalviewColourScheme.Strand.toString());
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(2);
+    cs.addElement(3);
+    cs.addElement(4);
+    af.getViewport().setColumnSelection(cs);
+    af.hideSelColumns_actionPerformed(null);
+    SequenceRenderer sr = new SequenceRenderer(af.getViewport());
+    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
+    String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
+    StructureSelectionManager ssm = new StructureSelectionManager();
+
+    /*
+     * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
+     */
+    HashMap<Integer, int[]> map = new HashMap<Integer, int[]>();
+    for (int pos = 1; pos <= seq1.getLength(); pos++)
+    {
+      map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) });
+    }
+    StructureMapping sm1 = new StructureMapping(seq1, "seq1.pdb", "pdb1",
+            "A", map, null);
+    ssm.addStructureMapping(sm1);
+    StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
+            "B", map, null);
+    ssm.addStructureMapping(sm2);
+
+    StructureMappingcommandSet[] commands = ChimeraCommands
+            .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+    assertEquals(1, commands.length);
+    assertEquals(1, commands[0].commands.length);
+    String theCommand = commands[0].commands[0];
+    // M colour is #82827d (see strand.html help page)
+    assertTrue(theCommand.contains("color #82827d #0:21.A|#1:21.B"));
+    // H colour is #60609f
+    assertTrue(theCommand.contains("color #60609f #0:22.A"));
+    // V colour is #ffff00
+    assertTrue(theCommand.contains("color #ffff00 #1:22.B"));
+    // hidden columns are Gray (128, 128, 128)
+    assertTrue(theCommand.contains("color #808080 #0:23-25.A|#1:23-25.B"));
+    // S and G are both coloured #4949b6
+    assertTrue(theCommand.contains("color #4949b6 #0:26-30.A|#1:26-30.B"));
+  }
 }
index d85bb10..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;
@@ -217,7 +217,7 @@ public class JalviewChimeraView
      * (or possibly 52-145 to 1-94 - see JAL-2319)
      */
     StructureSelectionManager ssm = binding.getSsm();
-    String pdbFile = binding.getPdbFile()[0];
+    String pdbFile = binding.getStructureFiles()[0];
     StructureMapping[] mappings = ssm.getMapping(pdbFile);
     assertTrue(mappings[0].getMappingDetailsOutput().contains("SIFTS"),
             "Failed to perform SIFTS mapping");
@@ -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 901bffc..9912e44 100644 (file)
@@ -25,8 +25,8 @@ import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.gui.JvOptionPane;
 
+import javax.swing.JComboBox;
 import javax.swing.JInternalFrame;
-import javax.swing.JTextField;
 
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
@@ -98,17 +98,17 @@ public class PDBFTSPanelTest
     assertEquals(expectedString, outcome);
   }
 
-  @Test(groups = { "External" }, timeOut = 7000)
+  @Test(groups = { "External" }, timeOut = 8000)
   public void txt_search_ActionPerformedTest()
   {
     PDBFTSPanel searchPanel = new PDBFTSPanel(null);
     JInternalFrame mainFrame = searchPanel.getMainFrame();
-    JTextField txt_search = searchPanel.getTxtSearch();
+    JComboBox<String> txt_search = searchPanel.getTxtSearch();
 
     assertTrue(mainFrame.getTitle().length() == 20);
     assertTrue(mainFrame.getTitle()
             .equalsIgnoreCase("PDB Sequence Fetcher"));
-    txt_search.setText("ABC");
+    txt_search.setSelectedItem("ABC");
     try
     {
       // wait for web-service to handle response
index 60db9dd..fed5992 100644 (file)
  */
 package jalview.gui;
 
-import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
 
+import jalview.bin.Cache;
+import jalview.bin.Jalview;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.io.Jalview2xmlTests;
+import jalview.renderer.ResidueShaderI;
+import jalview.schemes.BuriedColourScheme;
+import jalview.schemes.HelixColourScheme;
+import jalview.schemes.JalviewColourScheme;
+import jalview.schemes.StrandColourScheme;
+import jalview.schemes.TurnColourScheme;
+import jalview.util.MessageManager;
 
+import java.awt.Color;
 import java.util.List;
 
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 public class AlignFrameTest
 {
+  AlignFrame af;
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -59,27 +78,30 @@ public class AlignFrameTest
     seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9,
             Float.NaN, null));
     AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-    AlignFrame af = new AlignFrame(al, al.getWidth(), al.getHeight());
+    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight());
 
     /*
      * hiding a feature not present does nothing
      */
-    assertFalse(af.hideFeatureColumns("exon", true));
-    assertTrue(af.getViewport().getColumnSelection().isEmpty());
-    assertTrue(af.getViewport().getColumnSelection().getHiddenColumns()
+    assertFalse(alignFrame.hideFeatureColumns("exon", true));
+    assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
+    assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getHiddenRegions()
             .isEmpty());
-    assertFalse(af.hideFeatureColumns("exon", false));
-    assertTrue(af.getViewport().getColumnSelection().isEmpty());
-    assertTrue(af.getViewport().getColumnSelection().getHiddenColumns()
+    assertFalse(alignFrame.hideFeatureColumns("exon", false));
+    assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
+    assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getHiddenRegions()
             .isEmpty());
 
     /*
      * hiding a feature in all columns does nothing
      */
-    assertFalse(af.hideFeatureColumns("Metal", true));
-    assertTrue(af.getViewport().getColumnSelection().isEmpty());
-    List<int[]> hidden = af.getViewport().getColumnSelection()
-            .getHiddenColumns();
+    assertFalse(alignFrame.hideFeatureColumns("Metal", true));
+    assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
+    List<int[]> hidden = alignFrame.getViewport().getAlignment()
+            .getHiddenColumns()
+            .getHiddenRegions();
     assertTrue(hidden.isEmpty());
 
     /*
@@ -87,12 +109,463 @@ public class AlignFrameTest
      * sequence positions [2-4], [7-9] are column positions
      * [1-3], [6-8] base zero
      */
-    assertTrue(af.hideFeatureColumns("Turn", true));
-    hidden = af.getViewport().getColumnSelection().getHiddenColumns();
-    assertEquals(2, hidden.size());
-    assertEquals(1, hidden.get(0)[0]);
-    assertEquals(3, hidden.get(0)[1]);
-    assertEquals(6, hidden.get(1)[0]);
-    assertEquals(8, hidden.get(1)[1]);
+    assertTrue(alignFrame.hideFeatureColumns("Turn", true));
+    hidden = alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getHiddenRegions();
+    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);
+  }
+
+  @BeforeClass(alwaysRun = true)
+  public static void setUpBeforeClass() throws Exception
+  {
+    /*
+     * use read-only test properties file
+     */
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Jalview.main(new String[] { "-nonews" });
+  }
+
+  @AfterMethod(alwaysRun = true)
+  public void tearDown()
+  {
+    Desktop.instance.closeAll_actionPerformed(null);
+  }
+
+  /**
+   * configure (read-only) properties for test to ensure Consensus is computed
+   * for colour Above PID testing
+   */
+  @BeforeMethod(alwaysRun = true)
+  public void setUp()
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Cache.applicationProperties.setProperty("SHOW_IDENTITY",
+            Boolean.TRUE.toString());
+    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
+            DataSourceType.FILE);
+
+    /*
+     * wait for Consensus thread to complete
+     */
+    synchronized (this)
+    {
+      while (af.getViewport().getConsensusSeq() == null)
+      {
+        try
+        {
+          wait(50);
+        } catch (InterruptedException e)
+        {
+        }
+      }
+    }
+  }
+
+  /**
+   * Test that changing background (alignment) colour scheme
+   * <ul>
+   * <li>with Apply Colour to All Groups not selected, does not change group
+   * colours</li>
+   * <li>with Apply Colour to All Groups selected, does change group colours</li>
+   * <li>in neither case, changes alignment or group colour thresholds (PID or
+   * Conservation)</li>
+   * </ul>
+   */
+  @Test(groups = "Functional")
+  public void testChangeColour_background_groupsAndThresholds()
+  {
+    AlignViewport av = af.getViewport();
+    AlignmentI al = av.getAlignment();
+
+    /*
+     * Colour alignment by Buried Index
+     */
+    af.applyToAllGroups_actionPerformed(false);
+    af.changeColour_actionPerformed(JalviewColourScheme.Buried.toString());
+    assertTrue(av.getGlobalColourScheme() instanceof BuriedColourScheme);
+    assertFalse(av.getResidueShading().conservationApplied());
+    assertEquals(av.getResidueShading().getThreshold(), 0);
+
+    /*
+     * Apply Conservation 20%
+     */
+    af.conservationMenuItem_actionPerformed(true);
+    SliderPanel sp = SliderPanel.getSliderPanel();
+    assertEquals(sp.getTitle(), MessageManager.formatMessage(
+            "label.conservation_colour_increment",
+            new String[] { "Background" }));
+    assertTrue(sp.isForConservation());
+    sp.valueChanged(20);
+    assertTrue(av.getResidueShading().conservationApplied());
+    assertEquals(av.getResidueShading().getConservationInc(), 20);
+
+    /*
+     * Apply PID threshold 10% (conservation still applies as well)
+     */
+    af.abovePIDThreshold_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    assertEquals(sp.getTitle(), MessageManager.formatMessage(
+            "label.percentage_identity_threshold",
+            new String[] { "Background" }));
+    sp.valueChanged(10);
+    assertEquals(av.getResidueShading().getThreshold(), 10);
+    assertTrue(av.getResidueShading().conservationApplied());
+    assertEquals(av.getResidueShading().getConservationInc(), 20);
+
+    /*
+     * create a group with Strand colouring, 30% Conservation
+     * and 40% PID threshold
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.addSequence(al.getSequenceAt(0), false);
+    sg.setStartRes(15);
+    sg.setEndRes(25);
+    av.setSelectionGroup(sg);
+
+    /*
+     * apply 30% Conservation to group
+     */
+    PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
+    popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand
+            .toString());
+    assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
+    assertEquals(al.getGroups().size(), 1);
+    assertSame(al.getGroups().get(0), sg);
+    popupMenu.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    assertEquals(sp.getTitle(), MessageManager.formatMessage(
+            "label.conservation_colour_increment",
+            new String[] { sg.getName() }));
+    sp.valueChanged(30);
+    assertTrue(sg.getGroupColourScheme().conservationApplied());
+    assertEquals(sg.getGroupColourScheme().getConservationInc(), 30);
+
+    /*
+     * apply 40% PID threshold to group
+     */
+    popupMenu.abovePIDColour_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    assertEquals(sp.getTitle(), MessageManager.formatMessage(
+            "label.percentage_identity_threshold",
+            new String[] { sg.getName() }));
+    sp.valueChanged(40);
+    assertEquals(sg.getGroupColourScheme().getThreshold(), 40);
+    // conservation threshold is unchanged:
+    assertTrue(sg.getGroupColourScheme().conservationApplied());
+    assertEquals(sg.getGroupColourScheme().getConservationInc(), 30);
+
+    /*
+     * change alignment colour - group colour, and all thresholds,
+     * should be unaffected
+     */
+    af.changeColour_actionPerformed(JalviewColourScheme.Turn.toString());
+    assertTrue(av.getGlobalColourScheme() instanceof TurnColourScheme);
+    assertTrue(av.getResidueShading().conservationApplied());
+    assertEquals(av.getResidueShading().getConservationInc(), 20);
+    assertEquals(av.getResidueShading().getThreshold(), 10);
+    assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
+    assertTrue(sg.getGroupColourScheme().conservationApplied());
+    assertEquals(sg.getGroupColourScheme().getConservationInc(), 30);
+    assertEquals(sg.getGroupColourScheme().getThreshold(), 40);
+
+    /*
+     * Now change alignment colour with Apply Colour To All Groups
+     * - group colour should change, but not colour thresholds
+     */
+    af.applyToAllGroups_actionPerformed(true);
+    af.changeColour_actionPerformed(JalviewColourScheme.Helix.toString());
+    assertTrue(av.getGlobalColourScheme() instanceof HelixColourScheme);
+    assertTrue(av.getResidueShading().conservationApplied());
+    assertEquals(av.getResidueShading().getConservationInc(), 20);
+    assertEquals(av.getResidueShading().getThreshold(), 10);
+    assertTrue(sg.getColourScheme() instanceof HelixColourScheme);
+    assertTrue(sg.getGroupColourScheme().conservationApplied());
+    assertEquals(sg.getGroupColourScheme().getConservationInc(), 30);
+    assertEquals(sg.getGroupColourScheme().getThreshold(), 40);
+  }
+
+  /**
+   * Test residue colouring with various options
+   * <ol>
+   * <li>no PID or Conservation threshold</li>
+   * <li>colour by Conservation applied</li>
+   * <li>colour by Conservation removed</li>
+   * <li>colour above PID - various values</li>
+   * <li>colour above PID removed</li>
+   * <li>Above PID plus By Conservation combined</li>
+   * <li>remove Above PID to leave just By Conservation</li>
+   * <li>re-add Above PID</li>
+   * <li>remove By Conservation to leave just Above PID</li>
+   * <li>remove Above PID to leave original colours</li>
+   * </ol>
+   */
+  @Test(groups = "Functional")
+  public void testColourThresholdActions()
+  {
+    AlignViewport av = af.getViewport();
+    AlignmentI al = av.getAlignment();
+
+    /*
+     * Colour alignment by Helix Propensity, no thresholds
+     */
+    af.applyToAllGroups_actionPerformed(false);
+    af.changeColour_actionPerformed(JalviewColourScheme.Helix.toString());
+    assertTrue(av.getGlobalColourScheme() instanceof HelixColourScheme);
+    assertFalse(av.getResidueShading().conservationApplied());
+    assertEquals(av.getResidueShading().getThreshold(), 0);
+
+    /*
+     * inspect the colour of 
+     * FER_CAPAN.9(I), column 14 (14 base 0)
+     * FER_CAPAN.10(SER), column 16 (15 base 0)
+     */
+    SequenceI ferCapan = al.findName("FER_CAPAN");
+    ResidueShaderI rs = av.getResidueShading();
+    Color c = rs.findColour('I', 14, ferCapan);
+    Color i_original = new Color(138, 117, 138);
+    assertEquals(c, i_original);
+    c = rs.findColour('S', 15, ferCapan);
+    Color s_original = new Color(54, 201, 54);
+    assertEquals(c, s_original);
+
+    /*
+     * colour by conservation with increment 10
+     */
+    af.conservationMenuItem_actionPerformed(true);
+    SliderPanel sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    assertEquals(sp.getValue(), 30); // initial slider setting
+    sp.valueChanged(10);
+    assertSame(rs, av.getResidueShading());
+    c = rs.findColour('I', 14, ferCapan);
+    Color i_faded = new Color(196, 186, 196);
+    assertEquals(c, i_faded);
+    c = rs.findColour('S', 15, ferCapan);
+    Color s_faded = new Color(144, 225, 144);
+    assertEquals(c, s_faded);
+
+    /*
+     * deselect By Conservation - colour should revert
+     */
+    af.conservationMenuItem_actionPerformed(false);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+
+    /*
+     * now Above PID, threshold = 0%
+     * should be no change
+     */
+    af.abovePIDThreshold_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    assertEquals(sp.getValue(), 0); // initial slider setting
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, i_original);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+
+    /*
+     * Above PID, threshold = 1%
+     * 15.I becomes White because no match to consensus (V)
+     * 16.S remains coloured as matches 66.66% consensus
+     */
+    sp.valueChanged(1);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, Color.white);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+
+    /*
+     * threshold 66% - no further change yet...
+     */
+    sp.valueChanged(66);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, Color.white);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+
+    /*
+     * threshold 67% - now both residues are white
+     */
+    sp.valueChanged(67);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, Color.white);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, Color.white);
+
+    /*
+     * deselect Above PID - colours should revert
+     */
+    af.abovePIDThreshold_actionPerformed(false);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, i_original);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+
+    /*
+     * Now combine Above 50% PID and By Conservation 10%
+     * 15.I is White because no match to consensus (V)
+     * 16.S is coloured but faded
+     */
+    af.abovePIDThreshold_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    sp.valueChanged(50);
+    af.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    sp.valueChanged(10);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, Color.white);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_faded);
+
+    /*
+     * turn off Above PID - should just leave Conservation fading as before 
+     */
+    af.abovePIDThreshold_actionPerformed(false);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, i_faded);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_faded);
+
+    /*
+     * Now add Above 50% PID to conservation colouring
+     * - should give the same as PID followed by conservation (above)
+     */
+    af.abovePIDThreshold_actionPerformed(true);
+    SliderPanel.getSliderPanel().valueChanged(50);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, Color.white);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_faded);
+
+    /*
+     * turn off By Conservation
+     * should leave I white, S original (unfaded) colour
+     */
+    af.conservationMenuItem_actionPerformed(false);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, Color.white);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+
+    /*
+     * finally turn off Above PID to leave original colours
+     */
+    af.abovePIDThreshold_actionPerformed(false);
+    c = rs.findColour('I', 14, ferCapan);
+    assertEquals(c, i_original);
+    c = rs.findColour('S', 15, ferCapan);
+    assertEquals(c, s_original);
+  }
+
+  /**
+   * Verify that making a New View transfers alignment and group colour schemes,
+   * including any thresholds, to the new view. Because New View is performed by
+   * saving and reloading a 'project' file, this is similar to verifying a
+   * project save and reload.
+   * 
+   * @see Jalview2xmlTests#testStoreAndRecoverColourThresholds()
+   */
+  @Test(groups = "Functional")
+  public void testNewView_colourThresholds()
+  {
+    AlignViewport av = af.getViewport();
+    AlignmentI al = av.getAlignment();
+
+    /*
+     * Colour alignment by Buried Index, Above 10% PID, By Conservation 20%
+     */
+    af.changeColour_actionPerformed(JalviewColourScheme.Buried.toString());
+    assertTrue(av.getGlobalColourScheme() instanceof BuriedColourScheme);
+    af.abovePIDThreshold_actionPerformed(true);
+    SliderPanel sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    sp.valueChanged(10);
+    af.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    sp.valueChanged(20);
+    ResidueShaderI rs = av.getResidueShading();
+    assertEquals(rs.getThreshold(), 10);
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 20);
+
+    /*
+     * create a group with Strand colouring, 30% Conservation
+     * and 40% PID threshold
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.addSequence(al.getSequenceAt(0), false);
+    sg.setStartRes(15);
+    sg.setEndRes(25);
+    av.setSelectionGroup(sg);
+    PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
+    popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand
+            .toString());
+    assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
+    assertEquals(al.getGroups().size(), 1);
+    assertSame(al.getGroups().get(0), sg);
+    popupMenu.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    sp.valueChanged(30);
+    popupMenu.abovePIDColour_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    sp.valueChanged(40);
+    rs = sg.getGroupColourScheme();
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 30);
+    assertEquals(rs.getThreshold(), 40);
+
+    /*
+     * set slider panel focus to the background alignment
+     */
+    af.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    assertEquals(sp.getTitle(), MessageManager.formatMessage(
+            "label.conservation_colour_increment",
+            new String[] { "Background" }));
+
+    /*
+     * make a new View, verify alignment and group colour schemes
+     */
+    af.newView_actionPerformed(null);
+    assertEquals(af.alignPanel.getViewName(), "View 1");
+    AlignViewport av2 = af.getViewport();
+    assertNotSame(av, av2);
+    rs = av2.getResidueShading();
+    assertNotSame(av.getResidueShading(), rs);
+    assertEquals(rs.getThreshold(), 10);
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 20);
+    assertEquals(av2.getAlignment().getGroups().size(), 1);
+    sg = av2.getAlignment().getGroups().get(0);
+    rs = sg.getGroupColourScheme();
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 30);
+    assertEquals(rs.getThreshold(), 40);
+
+    /*
+     * check the Conservation SliderPanel (still open) is linked to 
+     * and updates the new view (JAL-2385)
+     */
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    assertEquals(sp.getTitle(), MessageManager.formatMessage(
+            "label.conservation_colour_increment",
+            new String[] { "View 1" }));
+    sp.valueChanged(22);
+    assertEquals(av2.getResidueShading().getConservationInc(), 22);
   }
 }
index 5695b15..4660842 100644 (file)
@@ -38,6 +38,7 @@ import jalview.datamodel.PDBEntry.Type;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
@@ -49,6 +50,7 @@ import jalview.util.MapList;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -70,7 +72,8 @@ public class AlignViewportTest
   @BeforeClass(alwaysRun = true)
   public static void setUpBeforeClass() throws Exception
   {
-    Jalview.main(new String[] { "-props", "test/jalview/testProps.jvprops" });
+    Jalview.main(new String[] { "-nonews", "-props",
+        "test/jalview/testProps.jvprops" });
   }
 
   @BeforeMethod(alwaysRun = true)
@@ -157,7 +160,7 @@ public class AlignViewportTest
     acf2.addMap(s1, s1, new MapList(new int[] { 1, 4 }, new int[] { 4, 1 },
             1, 1));
 
-    List<AlignedCodonFrame> mappings = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> mappings = new ArrayList<>();
     mappings.add(acf1);
     mappings.add(acf2);
     af1.getViewport().getAlignment().setCodonFrames(mappings);
@@ -214,11 +217,11 @@ public class AlignViewportTest
     acf3.addMap(cs2, cs2, new MapList(new int[] { 1, 12 }, new int[] { 1,
         12 }, 1, 1));
 
-    List<AlignedCodonFrame> mappings1 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> mappings1 = new ArrayList<>();
     mappings1.add(acf1);
     af1.getViewport().getAlignment().setCodonFrames(mappings1);
 
-    List<AlignedCodonFrame> mappings2 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> mappings2 = new ArrayList<>();
     mappings2.add(acf2);
     mappings2.add(acf3);
     af2.getViewport().getAlignment().setCodonFrames(mappings2);
@@ -277,12 +280,12 @@ public class AlignViewportTest
     acf3.addMap(cs2, cs2, new MapList(new int[] { 1, 12 }, new int[] { 1,
         12 }, 1, 1));
 
-    List<AlignedCodonFrame> mappings1 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> mappings1 = new ArrayList<>();
     mappings1.add(acf1);
     mappings1.add(acf2);
     af1.getViewport().getAlignment().setCodonFrames(mappings1);
 
-    List<AlignedCodonFrame> mappings2 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> mappings2 = new ArrayList<>();
     mappings2.add(acf2);
     mappings2.add(acf3);
     af2.getViewport().getAlignment().setCodonFrames(mappings2);
@@ -325,6 +328,8 @@ public class AlignViewportTest
             Boolean.TRUE.toString());
     Cache.applicationProperties.setProperty("SHOW_CONSERVATION",
             Boolean.FALSE.toString());
+    Cache.applicationProperties.setProperty("SHOW_OCCUPANCY",
+            Boolean.FALSE.toString());
     Cache.applicationProperties.setProperty("SHOW_IDENTITY",
             Boolean.FALSE.toString());
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
@@ -345,16 +350,17 @@ public class AlignViewportTest
   public void testSetGlobalColourScheme()
   {
     /*
-     * test for JAL-2283 don't inadvertently turn on colour by conservation
+     * test for JAL-2283: don't inadvertently turn on colour by conservation
      */
-    Cache.applicationProperties.setProperty("DEFAULT_COLOUR_PROT", "NONE");
+    Cache.applicationProperties.setProperty("DEFAULT_COLOUR_PROT", "None");
     Cache.applicationProperties.setProperty("SHOW_CONSERVATION",
             Boolean.TRUE.toString());
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/uniref50.fa", DataSourceType.FILE);
     ColourSchemeI cs = new PIDColourScheme();
     af.getViewport().setGlobalColourScheme(cs);
-    assertFalse(cs.conservationApplied());
+    assertFalse(af.getViewport().getResidueShading()
+            .conservationApplied());
   }
 
   @Test(groups = { "Functional" })
@@ -380,4 +386,70 @@ public class AlignViewportTest
     af.getViewport().setSearchResults(null);
     assertFalse(af.getViewport().hasSearchResults());
   }
+
+  /**
+   * Verify that setting the selection group has the side-effect of setting the
+   * context on the group, unless it already has one, but does not change
+   * whether the group is defined or not.
+   */
+  @Test(groups = { "Functional" })
+  public void testSetSelectionGroup()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewport av = af.getViewport();
+    SequenceGroup sg1 = new SequenceGroup();
+    SequenceGroup sg2 = new SequenceGroup();
+    SequenceGroup sg3 = new SequenceGroup();
+
+    av.setSelectionGroup(sg1);
+    assertSame(sg1.getContext(), av.getAlignment()); // context set
+    assertFalse(sg1.isDefined()); // group not defined
+
+    sg2.setContext(sg1, false);
+    av.setSelectionGroup(sg2);
+    assertFalse(sg2.isDefined()); // unchanged
+    assertSame(sg2.getContext(), sg1); // unchanged
+
+    // create a defined group
+    sg3.setContext(av.getAlignment(), true);
+    av.setSelectionGroup(sg3);
+    assertTrue(sg3.isDefined()); // unchanged
+  }
+  /**
+   * Verify that setting/clearing SHOW_OCCUPANCY preference adds or omits occupancy row from viewport
+   */
+  @Test(groups = { "Functional" })
+  public void testShowOrDontShowOccupancy()
+  {
+    // disable occupancy
+    jalview.bin.Cache.setProperty("SHOW_OCCUPANCY", Boolean.FALSE.toString());
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewport av = af.getViewport();
+    Assert.assertNull(av.getAlignmentGapAnnotation(), "Preference did not disable occupancy row.");
+    int c = 0;
+    for (AlignmentAnnotation aa : av.getAlignment().findAnnotations(null,
+            null, "Occupancy"))
+    {
+      c++;
+    }
+    Assert.assertEquals(c, 0, "Expected zero occupancy rows.");
+    
+    // enable occupancy
+    jalview.bin.Cache.setProperty("SHOW_OCCUPANCY", Boolean.TRUE.toString());
+    af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    av = af.getViewport();
+    Assert.assertNotNull(av.getAlignmentGapAnnotation(), "Preference did not enable occupancy row.");
+    c = 0;
+    for (AlignmentAnnotation aa : av.getAlignment().findAnnotations(null,
+            null, av.getAlignmentGapAnnotation().label))
+    {
+      c++;
+    }
+    ;
+    Assert.assertEquals(c, 1, "Expected to find one occupancy row.");
+
+  }
 }
diff --git a/test/jalview/gui/AlignmentPanelTest.java b/test/jalview/gui/AlignmentPanelTest.java
new file mode 100644 (file)
index 0000000..b228ba1
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * 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.bin.Cache;
+import jalview.bin.Jalview;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.viewmodel.ViewportRanges;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class AlignmentPanelTest
+{
+  SequenceI seq1 = new Sequence(
+          "Seq1",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq2 = new Sequence(
+          "Seq2",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq3 = new Sequence(
+          "Seq3",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq4 = new Sequence(
+          "Seq4",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq5 = new Sequence(
+          "Seq5",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq6 = new Sequence(
+          "Seq6",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq7 = new Sequence(
+          "Seq7",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq8 = new Sequence(
+          "Seq8",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq9 = new Sequence(
+          "Seq9",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq10 = new Sequence(
+          "Seq10",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq11 = new Sequence(
+          "Seq11",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq12 = new Sequence(
+          "Seq12",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq13 = new Sequence(
+          "Seq13",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq14 = new Sequence(
+          "Seq14",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq15 = new Sequence(
+          "Seq15",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq16 = new Sequence(
+          "Seq16",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq17 = new Sequence(
+          "Seq17",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq18 = new Sequence(
+          "Seq18",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq19 = new Sequence(
+          "Seq19",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq20 = new Sequence(
+          "Seq20",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq21 = new Sequence(
+          "Seq21",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq22 = new Sequence(
+          "Seq22",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  SequenceI seq23 = new Sequence(
+          "Seq23",
+          "ABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBACABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+  AlignFrame af;
+
+  @BeforeMethod(alwaysRun = true)
+  public void setUp()
+  {
+    Jalview.main(new String[] { "-nonews", "-props",
+        "test/jalview/testProps.jvprops" });
+
+    Cache.applicationProperties.setProperty("SHOW_IDENTITY",
+            Boolean.TRUE.toString());
+    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
+            DataSourceType.FILE);
+
+    /*
+     * wait for Consensus thread to complete
+     */
+    synchronized (this)
+    {
+      while (af.getViewport().getConsensusSeq() == null)
+      {
+        try
+        {
+          wait(50);
+        } catch (InterruptedException e)
+        {
+        }
+      }
+    }
+  }
+
+
+  /**
+   * Test side effect that end residue is set correctly by setScrollValues, with
+   * or without hidden columns
+   */
+  @Test(groups = "Functional")
+  public void TestSetScrollValues()
+  {
+    ViewportRanges ranges = af.getViewport().getRanges();
+    af.alignPanel.setScrollValues(0, 0);
+
+    int oldres = ranges.getEndRes();
+    af.alignPanel.setScrollValues(-1, 5);
+
+    // setting -ve x value does not change residue
+    assertEquals(ranges.getEndRes(), oldres);
+
+    af.alignPanel.setScrollValues(0, 5);
+
+    // setting 0 as x value does not change residue
+    assertEquals(ranges.getEndRes(), oldres);
+
+    af.alignPanel.setScrollValues(5, 5);
+    // setting x value to 5 extends endRes by 5 residues
+    assertEquals(ranges.getEndRes(), oldres + 5);
+
+    // scroll to position after hidden columns sets endres to oldres (width) +
+    // position
+    int scrollpos = 60;
+    af.getViewport().hideColumns(30, 50);
+    af.alignPanel.setScrollValues(scrollpos, 5);
+    assertEquals(ranges.getEndRes(), oldres + scrollpos);
+
+    // scroll to position within hidden columns, still sets endres to oldres +
+    // position
+    // not sure if this is actually correct behaviour but this is what Jalview
+    // currently does
+    scrollpos = 40;
+    af.getViewport().showAllHiddenColumns();
+    af.getViewport().hideColumns(30, 50);
+    af.alignPanel.setScrollValues(scrollpos, 5);
+    assertEquals(ranges.getEndRes(), oldres + scrollpos);
+
+    // scroll to position within <width> distance of the end of the alignment
+    // endRes should be set to width of alignment - 1
+    scrollpos = 130;
+    af.getViewport().showAllHiddenColumns();
+    af.alignPanel.setScrollValues(scrollpos, 5);
+    assertEquals(ranges.getEndRes(), af.getViewport()
+            .getAlignment().getWidth() - 1);
+
+    // now hide some columns, and scroll to position within <width>
+    // distance of the end of the alignment
+    // endRes should be set to width of alignment - 1 - the number of hidden
+    // columns
+    af.getViewport().hideColumns(30, 50);
+    af.alignPanel.setScrollValues(scrollpos, 5);
+    assertEquals(ranges.getEndRes(), af.getViewport()
+            .getAlignment().getWidth() - 1 - 21); // 21 is the number of hidden
+                                                  // columns
+  }
+}
index 38c1855..fbdda09 100644 (file)
@@ -326,7 +326,7 @@ public class AnnotationChooserTest
 
     types = AnnotationChooser.getAnnotationTypes(
             parentPanel.getAlignment(), false);
-    assertEquals("Not six annotation types", 6, types.size());
+    assertEquals("Not six annotation types", 7, types.size());
     assertTrue("IUPRED missing", types.contains("IUPRED"));
     assertTrue("JMol missing", types.contains("JMol"));
     assertTrue("Beauty missing", types.contains("Beauty"));
@@ -334,6 +334,7 @@ public class AnnotationChooserTest
     assertTrue("Consensus missing", types.contains("Consensus"));
     assertTrue("Quality missing", types.contains("Quality"));
     assertTrue("Conservation missing", types.contains("Conservation"));
+    assertTrue("Occupancy missing", types.contains("Occupancy"));
   }
 
   /**
@@ -357,18 +358,20 @@ public class AnnotationChooserTest
     AlignmentAnnotation[] anns = parentPanel.getAlignment()
             .getAlignmentAnnotation();
 
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertTrue(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertTrue(anns[autocalc + 2].visible); // JMol for seq3
+    assertTrue(anns[autocalc + 4].visible); // JMol for seq1
 
     setSelected(getTypeCheckbox("JMol"), true);
     assertTrue(anns[0].visible); // Conservation
     assertTrue(anns[1].visible); // Quality
     assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertFalse(anns[5].visible); // JMol for seq3 - not selected but hidden
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertFalse(anns[7].visible); // JMol for seq1 - selected and hidden
+    assertTrue(anns[3].visible); // Occupancy
+    assertTrue(anns[4].visible); // IUPred for seq0
+    assertTrue(anns[5].visible); // Beauty
+    assertFalse(anns[6].visible); // JMol for seq3 - not selected but hidden
+    assertTrue(anns[7].visible); // IUPRED for seq2
+    assertFalse(anns[8].visible); // JMol for seq1 - selected and hidden
   }
 
   /**
@@ -395,17 +398,19 @@ public class AnnotationChooserTest
     AlignmentAnnotation[] anns = parentPanel.getAlignment()
             .getAlignmentAnnotation();
 
-    assertTrue(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertTrue(anns[autocalc + 4].visible); // JMol for seq1
 
     setSelected(getTypeCheckbox("JMol"), true);
     assertTrue(anns[0].visible); // Conservation
     assertTrue(anns[1].visible); // Quality
     assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertTrue(anns[5].visible); // JMol for seq3 not in selection group
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertFalse(anns[7].visible); // JMol for seq1 in selection group
+    assertTrue(anns[3].visible); // Occupancy
+    assertTrue(anns[4].visible); // IUPred for seq0
+    assertTrue(anns[5].visible); // Beauty
+    assertTrue(anns[6].visible); // JMol for seq3 not in selection group
+    assertTrue(anns[7].visible); // IUPRED for seq2
+    assertFalse(anns[8].visible); // JMol for seq1 in selection group
   }
 
   /**
@@ -435,19 +440,35 @@ public class AnnotationChooserTest
 
     // select JMol - all hidden
     setSelected(typeCheckbox, true);
-    assertFalse(anns[5].visible); // JMol for seq3
-    assertFalse(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertFalse(anns[autocalc + 2].visible); // JMol for seq3
+    assertFalse(anns[autocalc + 4].visible); // JMol for seq1
 
     // deselect JMol - all unhidden
     setSelected(typeCheckbox, false);
-    assertTrue(anns[0].visible); // Conservation
-    assertTrue(anns[1].visible); // Quality
-    assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertTrue(anns[7].visible); // JMol for seq1
+    for (AlignmentAnnotation ann : anns)
+    {
+      assertTrue(ann.visible);
+    }
+  }
+
+  /**
+   * Returns a count of autocalculated annotations in the set provided
+   * 
+   * @param anns
+   * @return
+   */
+  private int countAutocalc(AlignmentAnnotation[] anns)
+  {
+    int count = 0;
+    for (AlignmentAnnotation ann : anns)
+    {
+      if (ann.autoCalculated)
+      {
+        count++;
+      }
+    }
+    return count;
   }
 
   /**
@@ -510,18 +531,15 @@ public class AnnotationChooserTest
     setSelected(allSequencesCheckbox, true);
     setSelected(hideCheckbox, true);
     setSelected(getTypeCheckbox("JMol"), true);
-    assertFalse(anns[5].visible); // JMol for seq3
-    assertFalse(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertFalse(anns[autocalc + 2].visible); // JMol for seq3
+    assertFalse(anns[autocalc + 4].visible); // JMol for seq1
     // ...now show them...
     setSelected(showCheckbox, true);
-    assertTrue(anns[0].visible); // Conservation
-    assertTrue(anns[1].visible); // Quality
-    assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertTrue(anns[7].visible); // JMol for seq1
+    for (AlignmentAnnotation ann : anns)
+    {
+      assertTrue(ann.visible);
+    }
   }
 
   /**
@@ -551,19 +569,16 @@ public class AnnotationChooserTest
     setSelected(hideCheckbox, true);
     setSelected(getTypeCheckbox("JMol"), true);
 
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertFalse(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertTrue(anns[autocalc + 2].visible); // JMol for seq3
+    assertFalse(anns[autocalc + 4].visible); // JMol for seq1
     // ...now show them...
     setSelected(showCheckbox, true);
 
-    assertTrue(anns[0].visible); // Conservation
-    assertTrue(anns[1].visible); // Quality
-    assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertTrue(anns[7].visible); // JMol for seq1
+    for (AlignmentAnnotation ann : anns)
+    {
+      assertTrue(ann.visible);
+    }
   }
 
   /**
@@ -592,19 +607,21 @@ public class AnnotationChooserTest
     final Checkbox typeCheckbox = getTypeCheckbox("JMol");
     // select JMol - all shown
     setSelected(typeCheckbox, true);
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertTrue(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertTrue(anns[autocalc + 2].visible); // JMol for seq3
+    assertTrue(anns[autocalc + 4].visible); // JMol for seq1
 
     // deselect JMol - all hidden
     setSelected(typeCheckbox, false);
     assertTrue(anns[0].visible); // Conservation
     assertTrue(anns[1].visible); // Quality
     assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertFalse(anns[5].visible); // JMol for seq3
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertFalse(anns[7].visible); // JMol for seq1
+    assertTrue(anns[3].visible); // Occupancy
+    assertTrue(anns[4].visible); // IUPred for seq0
+    assertTrue(anns[5].visible); // Beauty
+    assertFalse(anns[6].visible); // JMol for seq3
+    assertTrue(anns[7].visible); // IUPRED for seq2
+    assertFalse(anns[8].visible); // JMol for seq1
   }
 
   /**
@@ -633,19 +650,21 @@ public class AnnotationChooserTest
 
     // select JMol - should remain visible
     setSelected(getTypeCheckbox("JMol"), true);
-    assertTrue(anns[5].visible); // JMol for seq3
-    assertTrue(anns[7].visible); // JMol for seq1
+    int autocalc = countAutocalc(anns);
+    assertTrue(anns[autocalc + 2].visible); // JMol for seq3
+    assertTrue(anns[autocalc + 4].visible); // JMol for seq1
 
     // deselect JMol - should be hidden for selected sequences only
     setSelected(getTypeCheckbox("JMol"), false);
     assertTrue(anns[0].visible); // Conservation
     assertTrue(anns[1].visible); // Quality
     assertTrue(anns[2].visible); // Consensus
-    assertTrue(anns[3].visible); // IUPred for seq0
-    assertTrue(anns[4].visible); // Beauty
-    assertTrue(anns[5].visible); // JMol for seq3 not in selection group
-    assertTrue(anns[6].visible); // IUPRED for seq2
-    assertFalse(anns[7].visible); // JMol for seq1 in selection group
+    assertTrue(anns[3].visible); // Occupancy
+    assertTrue(anns[4].visible); // IUPred for seq0
+    assertTrue(anns[5].visible); // Beauty
+    assertTrue(anns[6].visible); // JMol for seq3 not in selection group
+    assertTrue(anns[7].visible); // IUPRED for seq2
+    assertFalse(anns[8].visible); // JMol for seq1 in selection group
   }
 
   /**
@@ -721,12 +740,12 @@ public class AnnotationChooserTest
 
     AlignmentAnnotation[] anns = parentPanel.getAlignment()
             .getAlignmentAnnotation();
-    // remember 3 annotations to skip (Conservation/Quality/Consensus)
-    assertFalse(testee.isInActionScope(anns[3]));
-    assertFalse(testee.isInActionScope(anns[4]));
-    assertFalse(testee.isInActionScope(anns[5]));
-    assertTrue(testee.isInActionScope(anns[6]));
-    assertTrue(testee.isInActionScope(anns[7]));
+    int autocalc = countAutocalc(anns);
+    assertFalse(testee.isInActionScope(anns[autocalc]));
+    assertFalse(testee.isInActionScope(anns[autocalc + 1]));
+    assertFalse(testee.isInActionScope(anns[autocalc + 2]));
+    assertTrue(testee.isInActionScope(anns[autocalc + 3]));
+    assertTrue(testee.isInActionScope(anns[autocalc + 4]));
   }
 
   /**
@@ -747,12 +766,12 @@ public class AnnotationChooserTest
 
     AlignmentAnnotation[] anns = parentPanel.getAlignment()
             .getAlignmentAnnotation();
-    // remember 3 annotations to skip (Conservation/Quality/Consensus)
-    assertTrue(testee.isInActionScope(anns[3]));
-    assertTrue(testee.isInActionScope(anns[4]));
-    assertTrue(testee.isInActionScope(anns[5]));
-    assertFalse(testee.isInActionScope(anns[6]));
-    assertFalse(testee.isInActionScope(anns[7]));
+    int autocalc = countAutocalc(anns);
+    assertTrue(testee.isInActionScope(anns[autocalc]));
+    assertTrue(testee.isInActionScope(anns[autocalc + 1]));
+    assertTrue(testee.isInActionScope(anns[autocalc + 2]));
+    assertFalse(testee.isInActionScope(anns[autocalc + 3]));
+    assertFalse(testee.isInActionScope(anns[autocalc + 4]));
   }
 
   /**
@@ -787,11 +806,12 @@ public class AnnotationChooserTest
     assertTrue(anns[0].visible); // Conservation
     assertTrue(anns[1].visible); // Quality
     assertTrue(anns[2].visible); // Consensus
-    assertFalse(anns[3].visible); // IUPRED
-    assertTrue(anns[4].visible); // Beauty (not seq-related)
-    assertFalse(anns[5].visible); // JMol
-    assertFalse(anns[6].visible); // IUPRED
-    assertFalse(anns[7].visible); // JMol
+    assertTrue(anns[3].visible); // Occupancy
+    assertFalse(anns[4].visible); // IUPRED
+    assertTrue(anns[5].visible); // Beauty (not seq-related)
+    assertFalse(anns[6].visible); // JMol
+    assertFalse(anns[7].visible); // IUPRED
+    assertFalse(anns[8].visible); // JMol
 
     // reset - should all be visible
     testee.resetOriginalState();
diff --git a/test/jalview/gui/AnnotationRowFilterTest.java b/test/jalview/gui/AnnotationRowFilterTest.java
new file mode 100644 (file)
index 0000000..69a41c5
--- /dev/null
@@ -0,0 +1,121 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+
+import java.util.Vector;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for methods of base class of annotation column or colour chooser
+ */
+public class AnnotationRowFilterTest
+{
+  AlignFrame af;
+
+  private AnnotationRowFilter testee;
+
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS",
+            Boolean.TRUE.toString());
+    Cache.applicationProperties.setProperty(
+            Preferences.SHOW_AUTOCALC_ABOVE, Boolean.TRUE.toString());
+    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
+            DataSourceType.FILE);
+    testee = new AnnotationRowFilter(af.viewport, af.alignPanel)
+    {
+      @Override
+      public void valueChanged(boolean updateAllAnnotation)
+      {
+      }
+
+      @Override
+      public void updateView()
+      {
+      }
+
+      @Override
+      public void reset()
+      {
+      }
+    };
+  }
+
+  /**
+   * Test the method that builds the drop-down list of annotations to choose
+   * from for colour by annotation or select columns by annotation
+   */
+  @Test(groups = "Functional")
+  public void testGetAnnotationItems()
+  {
+    AlignmentI al = af.getViewport().getAlignment();
+    SequenceI seq1 = al.findSequenceMatch("FER_CAPAA")[0];
+    SequenceI seq2 = al.findSequenceMatch("FER_BRANA")[0];
+
+    AlignmentAnnotation ann1 = new AlignmentAnnotation("ann1Label", "ann1",
+            null);
+    al.addAnnotation(ann1);
+    AlignmentAnnotation ann2 = new AlignmentAnnotation("Significance",
+            "ann2", null);
+    al.addAnnotation(ann2);
+    /*
+     * a second Significance alignment annotation
+     */
+    AlignmentAnnotation ann2a = new AlignmentAnnotation("Significance",
+            "ann2", null);
+    al.addAnnotation(ann2a);
+
+    AlignmentAnnotation ann3 = new AlignmentAnnotation("Jronn", "Jronn",
+            null);
+    ann3.setSequenceRef(seq1);
+    al.addAnnotation(ann3);
+    AlignmentAnnotation ann4 = new AlignmentAnnotation("Jronn", "Jronn",
+            null);
+    ann4.setSequenceRef(seq2);
+    al.addAnnotation(ann4);
+    AlignmentAnnotation ann5 = new AlignmentAnnotation("Jnet", "Jnet", null);
+    ann5.setSequenceRef(seq2);
+    al.addAnnotation(ann5);
+    /*
+     * a second Jnet annotation for FER_BRANA
+     */
+    AlignmentAnnotation ann6 = new AlignmentAnnotation("Jnet", "Jnet", null);
+    ann6.setSequenceRef(seq2);
+    al.addAnnotation(ann6);
+
+    /*
+     * drop-down items with 'Per-sequence only' not checked
+     */
+    Vector<String> items = testee.getAnnotationItems(false);
+    assertEquals(
+            items.toString(),
+            "[Conservation, Quality, Consensus, Occupancy, ann1Label, Significance, Significance_1, Jronn_FER_CAPAA, Jronn_FER_BRANA, Jnet_FER_BRANA, Jnet_FER_BRANA_2]");
+    assertEquals(testee.getAnnotationMenuLabel(ann1), "ann1Label");
+    assertEquals(testee.getAnnotationMenuLabel(ann2), "Significance");
+    assertEquals(testee.getAnnotationMenuLabel(ann2a), "Significance_1");
+    assertEquals(testee.getAnnotationMenuLabel(ann3), "Jronn_FER_CAPAA");
+    assertEquals(testee.getAnnotationMenuLabel(ann4), "Jronn_FER_BRANA");
+    assertEquals(testee.getAnnotationMenuLabel(ann5), "Jnet_FER_BRANA");
+    assertEquals(testee.getAnnotationMenuLabel(ann6), "Jnet_FER_BRANA_2");
+
+    /*
+     * drop-down items with 'Per-sequence only' checked
+     */
+    items = testee.getAnnotationItems(true);
+    assertEquals(items.toString(), "[Jronn, Jnet]");
+    // the first annotation of the type is associated with the menu item
+    assertEquals(testee.getAnnotationMenuLabel(ann3), "Jronn");
+    assertEquals(testee.getAnnotationMenuLabel(ann5), "Jnet");
+  }
+}
index 922d457..335240b 100644 (file)
@@ -321,7 +321,8 @@ public class PopupMenuTest
 
     assertEquals(4, showOptions.length); // includes 'All' and separator
     assertEquals(4, hideOptions.length);
-    assertEquals("All", ((JMenuItem) showOptions[0]).getText());
+    String all = MessageManager.getString("label.all");
+    assertEquals(all, ((JMenuItem) showOptions[0]).getText());
     assertTrue(showOptions[1] instanceof JPopupMenu.Separator);
     assertEquals(JSeparator.HORIZONTAL,
             ((JSeparator) showOptions[1]).getOrientation());
@@ -331,7 +332,7 @@ public class PopupMenuTest
     assertEquals("SSP", ((JMenuItem) showOptions[3]).getText());
     assertEquals("JPred", ((JMenuItem) showOptions[3]).getToolTipText());
 
-    assertEquals("All", ((JMenuItem) hideOptions[0]).getText());
+    assertEquals(all, ((JMenuItem) hideOptions[0]).getText());
     assertTrue(hideOptions[1] instanceof JPopupMenu.Separator);
     assertEquals(JSeparator.HORIZONTAL,
             ((JSeparator) hideOptions[1]).getOrientation());
@@ -383,12 +384,13 @@ public class PopupMenuTest
 
     assertEquals(2, showOptions.length); // includes 'All' and separator
     assertEquals(4, hideOptions.length);
-    assertEquals("All", ((JMenuItem) showOptions[0]).getText());
+    String all = MessageManager.getString("label.all");
+    assertEquals(all, ((JMenuItem) showOptions[0]).getText());
     assertTrue(showOptions[1] instanceof JPopupMenu.Separator);
     assertEquals(JSeparator.HORIZONTAL,
             ((JSeparator) showOptions[1]).getOrientation());
 
-    assertEquals("All", ((JMenuItem) hideOptions[0]).getText());
+    assertEquals(all, ((JMenuItem) hideOptions[0]).getText());
     assertTrue(hideOptions[1] instanceof JPopupMenu.Separator);
     assertEquals(JSeparator.HORIZONTAL,
             ((JSeparator) hideOptions[1]).getOrientation());
@@ -440,7 +442,8 @@ public class PopupMenuTest
 
     assertEquals(4, showOptions.length); // includes 'All' and separator
     assertEquals(2, hideOptions.length);
-    assertEquals("All", ((JMenuItem) showOptions[0]).getText());
+    String all = MessageManager.getString("label.all");
+    assertEquals(all, ((JMenuItem) showOptions[0]).getText());
     assertTrue(showOptions[1] instanceof JPopupMenu.Separator);
     assertEquals(JSeparator.HORIZONTAL,
             ((JSeparator) showOptions[1]).getOrientation());
@@ -450,7 +453,7 @@ public class PopupMenuTest
     assertEquals("Temp", ((JMenuItem) showOptions[3]).getText());
     assertEquals("PDB2", ((JMenuItem) showOptions[3]).getToolTipText());
 
-    assertEquals("All", ((JMenuItem) hideOptions[0]).getText());
+    assertEquals(all, ((JMenuItem) hideOptions[0]).getText());
     assertTrue(hideOptions[1] instanceof JPopupMenu.Separator);
     assertEquals(JSeparator.HORIZONTAL,
             ((JSeparator) hideOptions[1]).getOrientation());
diff --git a/test/jalview/gui/SeqPanelTest.java b/test/jalview/gui/SeqPanelTest.java
new file mode 100644 (file)
index 0000000..a5d244d
--- /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.gui;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class SeqPanelTest
+{
+  AlignFrame af;
+
+  @BeforeClass(alwaysRun = true)
+  public void setUpJvOptionPane()
+  {
+    JvOptionPane.setInteractiveMode(false);
+    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+  }
+  @Test(groups = "Functional")
+  public void testSetStatusReturnsNearestResiduePosition()
+  {
+    SequenceI seq1 = new Sequence("Seq1", "AACDE");
+    SequenceI seq2 = new Sequence("Seq2", "AA--E");
+    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
+    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
+            al.getHeight());
+    AlignmentI visAl = alignFrame.getViewport().getAlignment();
+
+    // Test either side of gap
+    assertEquals(
+            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
+                    visAl.getSequenceAt(1), 1, 1), 2);
+    assertEquals(alignFrame.statusBar.getText(),
+            "Sequence 2 ID: Seq2 Residue: ALA (2)");
+    assertEquals(
+            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
+                    visAl.getSequenceAt(1), 4, 1), 3);
+    assertEquals(alignFrame.statusBar.getText(),
+            "Sequence 2 ID: Seq2 Residue: GLU (3)");
+    // no status message at a gap, returns next residue position to the right
+    assertEquals(
+            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
+                    visAl.getSequenceAt(1), 2, 1), 3);
+    assertEquals(alignFrame.statusBar.getText(), "Sequence 2 ID: Seq2");
+    assertEquals(
+            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
+                    visAl.getSequenceAt(1), 3, 1), 3);
+    assertEquals(alignFrame.statusBar.getText(), "Sequence 2 ID: Seq2");
+  }
+
+  @Test(groups = "Functional")
+  public void testAmbiguousAminoAcidGetsStatusMessage()
+  {
+    SequenceI seq1 = new Sequence("Seq1", "ABCDE");
+    SequenceI seq2 = new Sequence("Seq2", "AB--E");
+    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
+    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
+            al.getHeight());
+    AlignmentI visAl = alignFrame.getViewport().getAlignment();
+
+    assertEquals(
+            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
+                    visAl.getSequenceAt(1), 1, 1), 2);
+    assertEquals(alignFrame.statusBar.getText(),
+            "Sequence 2 ID: Seq2 Residue: B (2)");
+  }
+}
index 81289b0..c80b830 100644 (file)
@@ -53,11 +53,29 @@ public class SequenceRendererTest
     av.setGlobalColourScheme(new ZappoColourScheme());
 
     // @see ResidueProperties.zappo
-    assertEquals(Color.pink, sr.getResidueColour(seq, 0, null)); // M
-    assertEquals(Color.green, sr.getResidueColour(seq, 2, null)); // T
-    assertEquals(Color.magenta, sr.getResidueColour(seq, 5, null)); // G
-    assertEquals(Color.orange, sr.getResidueColour(seq, 12, null)); // F
+    assertEquals(Color.pink, sr.getResidueBoxColour(seq, 0)); // M
+    assertEquals(Color.green, sr.getResidueBoxColour(seq, 2)); // T
+    assertEquals(Color.magenta, sr.getResidueBoxColour(seq, 5)); // G
+    assertEquals(Color.orange, sr.getResidueBoxColour(seq, 12)); // F
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetResidueBoxColour_none()
+  {
+    SequenceI seq = new Sequence("name", "MA--TVLGSPRAPAFF");
+    AlignmentI al = new Alignment(new SequenceI[] { seq });
+    final AlignViewport av = new AlignViewport(al);
+    SequenceRenderer sr = new SequenceRenderer(av);
+
+    assertEquals(Color.white, sr.getResidueBoxColour(seq, 0));
+    assertEquals(Color.white, sr.getResidueBoxColour(seq, 2));
+
+    // set for overview
+    sr.forOverview = true;
+    assertEquals(Color.lightGray, sr.getResidueBoxColour(seq, 0));
+    assertEquals(Color.white, sr.getResidueBoxColour(seq, 2));
+  }
+
   // TODO more tests for getResidueBoxColour covering groups, feature rendering,
   // gaps, overview...
 
index b7eef0f..4535c93 100644 (file)
@@ -121,7 +121,7 @@ public class StructureChooserTest
     StructureChooser sc = new StructureChooser(selectedSeqs, seq, null);
     sc.populateFilterComboBox(false, false);
     int optionsSize = sc.getCmbFilterOption().getItemCount();
-    assertEquals(3, optionsSize); // if structures are not discovered then don't
+    assertEquals(2, optionsSize); // if structures are not discovered then don't
                                   // populate filter options
 
     sc.populateFilterComboBox(true, false);
@@ -136,7 +136,7 @@ public class StructureChooserTest
     assertEquals("Cached PDB Entries", filterOpt.getName());
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void fetchStructuresInfoTest()
   {
     SequenceI[] selectedSeqs = new SequenceI[] { seq };
index d8ae999..e14a478 100644 (file)
@@ -30,12 +30,14 @@ 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.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
@@ -127,32 +129,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" })
index 5b90e19..6a00cde 100644 (file)
@@ -24,7 +24,7 @@ import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.gui.JvOptionPane;
 import jalview.io.AnnotationFile.ViewDef;
 
@@ -70,7 +70,7 @@ public class AnnotationFileIOTest
     }
   }
 
-  public static AlignmentI readAlignmentFile(File f)
+  AlignmentI readAlignmentFile(File f)
   {
     System.out.println("Reading file: " + f);
     String ff = f.getPath();
@@ -106,9 +106,7 @@ public class AnnotationFileIOTest
    *          - label for IO class used to write and read back in the data from
    *          f
    */
-
-  // @Test(groups ={ "Functional" })
-  public static void testAnnotationFileIO(String testname, File f,
+  void testAnnotationFileIO(String testname, File f,
           File annotFile)
   {
     System.out.println("Test: " + testname + "\nReading annotation file '"
@@ -117,7 +115,7 @@ public class AnnotationFileIOTest
     try
     {
       AlignmentI al = readAlignmentFile(f);
-      ColumnSelection cs = new ColumnSelection();
+      HiddenColumns cs = new HiddenColumns();
       assertTrue(
               "Test "
                       + testname
@@ -126,6 +124,7 @@ public class AnnotationFileIOTest
                       DataSourceType.FILE));
 
       AnnotationFile aff = new AnnotationFile();
+      // ViewDef is not used by Jalview
       ViewDef v = aff.new ViewDef(null, al.getHiddenSequences(), cs,
               new Hashtable());
       String anfileout = new AnnotationFile().printAnnotations(
@@ -154,7 +153,8 @@ public class AnnotationFileIOTest
                       DataSourceType.PASTE));
 
       // test for consistency in io
-      StockholmFileTest.testAlignmentEquivalence(al, al_new, false);
+      StockholmFileTest.testAlignmentEquivalence(al, al_new, false, false,
+              false);
       return;
     } catch (Exception e)
     {
index d6f1e8b..45340d9 100644 (file)
@@ -23,7 +23,6 @@ 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.assertTrue;
 
 import jalview.api.FeatureColourI;
@@ -33,12 +32,17 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 
 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.List;
 import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
@@ -82,10 +86,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);
@@ -95,46 +104,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);
@@ -177,10 +192,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);
@@ -190,8 +205,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);
@@ -238,10 +253,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);
@@ -254,8 +269,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);
@@ -291,10 +306,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);
@@ -302,8 +317,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);
@@ -333,14 +348,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
@@ -406,6 +421,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"
@@ -415,28 +431,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<String>(
+            Arrays.asList(new String[] {}));
     String exported = featuresFile.printJalviewFormat(
-            al.getSequencesArray(), visible);
+            al.getSequencesArray(), visible, visibleGroups, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
     /*
+     * include non-positional features
+     */
+    visibleGroups.add("uniprot");
+    exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
+            visible, 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, visibleGroups, false);
     expected = "METAL\tcc9900\n"
             + "GAMMA-TURN\tff0000|00ffff|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);
 
@@ -446,19 +491,119 @@ public class FeaturesFileTest
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible);
+            visible, 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"
             + "\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<String, FeatureColourI>();
+    List<String> visibleGroups = new ArrayList<String>(
+            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);
   }
 }
index 8df228f..7810504 100644 (file)
@@ -10,28 +10,39 @@ import static org.testng.Assert.assertTrue;
 import java.util.Iterator;
 
 import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 public class FileFormatsTest
 {
-  @AfterMethod()
+  @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
     FileFormats.getInstance().reset();
   }
 
+  @BeforeMethod(alwaysRun = true)
+  public void setUp()
+  {
+    FileFormats.getInstance().reset();
+  }
+
   @Test(groups = "Functional")
-  public void testIsDynamic()
+  public void testIsIdentifiable()
   {
     FileFormats formats = FileFormats.getInstance();
-    for (FileFormatI ff : FileFormat.values())
-    {
-      assertFalse(formats.isIdentifiable(ff));
-    }
-    assertTrue(formats.isIdentifiable(null));
+    assertTrue(formats.isIdentifiable(formats.forName(FileFormat.Fasta
+            .getName())));
+    assertTrue(formats.isIdentifiable(formats.forName(FileFormat.MMCif
+            .getName())));
+    assertTrue(formats.isIdentifiable(formats.forName(FileFormat.Jnet
+            .getName())));
+    assertFalse(formats.isIdentifiable(formats.forName(FileFormat.Jalview
+            .getName())));
+    assertFalse(formats.isIdentifiable(null));
 
     /*
-     * remove and re-add a format: it is now considered 'dynamically added'
+     * remove and re-add a format: it is still 'identifiable'
      */
     formats.deregisterFileFormat(FileFormat.Fasta.getName());
     assertNull(formats.forName(FileFormat.Fasta.getName()));
@@ -104,7 +115,7 @@ public class FileFormatsTest
     FileFormats formats = FileFormats.getInstance();
     assertSame(FileFormat.MMCif,
             formats.forName(FileFormat.MMCif.getName()));
-    assertFalse(formats.isIdentifiable(FileFormat.MMCif));
+    assertTrue(formats.isIdentifiable(FileFormat.MMCif));
 
     /*
      * deregister mmCIF format
@@ -114,7 +125,7 @@ public class FileFormatsTest
 
     /*
      * re-register mmCIF format
-     * it is reinstated (but now classed as 'dynamic')
+     * it is reinstated (still 'identifiable')
      */
     formats.registerFileFormat(FileFormat.MMCif);
     assertSame(FileFormat.MMCif,
diff --git a/test/jalview/io/FileLoaderTest.java b/test/jalview/io/FileLoaderTest.java
new file mode 100644 (file)
index 0000000..968901f
--- /dev/null
@@ -0,0 +1,22 @@
+package jalview.io;
+
+import org.junit.Assert;
+import org.testng.annotations.Test;
+
+public class FileLoaderTest
+{
+
+  @Test(groups = { "Network" })
+  public void testDownloadStructuresIfInputFromURL()
+  {
+    String urlFile = "http://www.jalview.org/builds/develop/examples/3W5V.pdb";
+    FileLoader fileLoader = new FileLoader();
+    fileLoader.LoadFileWaitTillLoaded(urlFile, DataSourceType.URL,
+            FileFormat.PDB);
+    Assert.assertNotNull(fileLoader.file);
+    // The FileLoader's file is expected to be same as the original URL.
+    Assert.assertEquals(urlFile, fileLoader.file);
+    // Data source type expected to be DataSourceType.URL
+    Assert.assertEquals(DataSourceType.URL, fileLoader.protocol);
+  }
+}
index 3d800d8..dd4f6ba 100644 (file)
@@ -110,7 +110,8 @@ public class IdentifyFileTest
         {
             "examples/testdata/cullpdb_pc25_res3.0_R0.3_d150729_chains9361.fasta.15316",
             FileFormat.Fasta },
-
+        { "resources/scoreModel/pam250.scm", FileFormat.ScoreMatrix },
+        { "resources/scoreModel/blosum80.scm", FileFormat.ScoreMatrix }
     // { "examples/testdata/test.amsa", "AMSA" },
     // { "examples/test.jnet", "JnetFile" },
     };
index a705a78..17982be 100644 (file)
@@ -27,20 +27,23 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
-import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 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;
 import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ResidueColourScheme;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.testng.Assert;
 import org.testng.AssertJUnit;
@@ -78,7 +81,7 @@ public class JSONFileTest
 
   private HashMap<String, SequenceGroup> expectedGrps = new HashMap<String, SequenceGroup>();
 
-  private ColumnSelection expectedColSel = new ColumnSelection();
+  private HiddenColumns expectedColSel = new HiddenColumns();
 
   private SequenceI[] expectedHiddenSeqs = new SequenceI[1];
 
@@ -90,9 +93,15 @@ public class JSONFileTest
 
   private JSONFile jf;
 
+  private AlignExportSettingI exportSettings;
+
   @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",
@@ -112,14 +121,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)
     {
@@ -127,17 +140,18 @@ public class JSONFileTest
       expectedSeqs.put(seq.getName(), seq);
     }
 
-    // create and add sequence groups
-    ArrayList<SequenceI> grpSeqs = new ArrayList<SequenceI>();
+    // create and add a sequence group
+    List<SequenceI> grpSeqs = new ArrayList<SequenceI>();
     grpSeqs.add(seqs[1]);
     grpSeqs.add(seqs[2]);
     grpSeqs.add(seqs[3]);
     grpSeqs.add(seqs[4]);
-    SequenceGroup seqGrp = new SequenceGroup(grpSeqs, "JGroup:1883305585",
+    SequenceGroup seqGrp = new SequenceGroup(grpSeqs,
+            "JGroup:1883305585",
             null, true, true, false, 21, 29);
     ColourSchemeI scheme = ColourSchemeMapper.getJalviewColourScheme(
             "zappo", seqGrp);
-    seqGrp.cs = scheme;
+    seqGrp.cs.setColourScheme(scheme);
     seqGrp.setShowNonconserved(false);
     seqGrp.setDescription(null);
 
@@ -191,9 +205,9 @@ public class JSONFileTest
     TEST_SEQ_HEIGHT = expectedSeqs.size();
     TEST_GRP_HEIGHT = expectedGrps.size();
     TEST_ANOT_HEIGHT = expectedAnnots.size();
-    TEST_CS_HEIGHT = expectedColSel.getHiddenColumns().size();
+    TEST_CS_HEIGHT = expectedColSel.getHiddenRegions().size();
 
-    AlignExportSettingI exportSettings = new AlignExportSettingI()
+    exportSettings = new AlignExportSettingI()
     {
       @Override
       public boolean isExportHiddenSequences()
@@ -240,7 +254,7 @@ public class JSONFileTest
       jf = (JSONFile) formatAdapter.getAlignFile();
 
       AlignFrame af = new AlignFrame(alignment, jf.getHiddenSequences(),
-              jf.getColumnSelection(), AlignFrame.DEFAULT_WIDTH,
+              jf.getHiddenColumns(), AlignFrame.DEFAULT_WIDTH,
               AlignFrame.DEFAULT_HEIGHT);
       af.getViewport().setShowSequenceFeatures(jf.isShowSeqFeatures());
       String colourSchemeName = jf.getGlobalColourScheme();
@@ -309,13 +323,13 @@ public class JSONFileTest
   @Test(groups = { "Functional" })
   public void hiddenColsTest()
   {
-    ColumnSelection cs = testJsonFile.getColumnSelection();
+    HiddenColumns cs = testJsonFile.getHiddenColumns();
     Assert.assertNotNull(cs);
-    Assert.assertNotNull(cs.getHiddenColumns());
-    List<int[]> hiddenCols = cs.getHiddenColumns();
+    Assert.assertNotNull(cs.getHiddenRegions());
+    List<int[]> hiddenCols = cs.getHiddenRegions();
     Assert.assertEquals(hiddenCols.size(), TEST_CS_HEIGHT);
     Assert.assertEquals(hiddenCols.get(0), expectedColSel
-            .getHiddenColumns().get(0),
+            .getHiddenRegions().get(0),
             "Mismatched hidden columns!");
   }
 
@@ -337,6 +351,58 @@ public class JSONFileTest
             "Zappo colour scheme expected!");
   }
 
+  /**
+   * Test for bug JAL-2489, NPE when exporting BioJSON with global colour
+   * scheme, and a group colour scheme, set as 'None'
+   */
+  @Test(groups = { "Functional" })
+  public void testBioJSONRoundTripWithColourSchemeNone()
+  {
+    AppletFormatAdapter formatAdapter = new AppletFormatAdapter();
+
+    Alignment _alignment;
+    try
+    {
+      // load example BioJSON file
+      _alignment = (Alignment) formatAdapter.readFile(TEST_JSON_FILE,
+              DataSourceType.FILE, FileFormat.Json);
+      JSONFile bioJsonFile = (JSONFile) formatAdapter.getAlignFile();
+      AlignFrame alignFrame = new AlignFrame(_alignment,
+              bioJsonFile.getHiddenSequences(),
+              bioJsonFile.getHiddenColumns(), AlignFrame.DEFAULT_WIDTH,
+              AlignFrame.DEFAULT_HEIGHT);
+
+      /*
+       * Create a group on the alignment;
+       * Change global and group colour scheme to 'None' and perform round trip
+       */
+      SequenceGroup sg = new SequenceGroup();
+      sg.addSequence(_alignment.getSequenceAt(0), false);
+      sg.setColourScheme(null);
+      ColourSchemeI cs = ColourSchemeMapper.getJalviewColourScheme(
+              ResidueColourScheme.NONE, _alignment);
+      alignFrame.changeColour(cs);
+      alignFrame.getViewport().setFeaturesDisplayed(
+              bioJsonFile.getDisplayedFeatures());
+      formatAdapter = new AppletFormatAdapter(alignFrame.alignPanel,
+              exportSettings);
+      // export BioJSON string
+      String jsonOutput = formatAdapter.formatSequences(FileFormat.Json,
+              alignFrame.alignPanel.getAlignment(), false);
+      // read back Alignment from BioJSON string
+      formatAdapter = new AppletFormatAdapter();
+      formatAdapter.readFile(jsonOutput, DataSourceType.PASTE,
+              FileFormat.Json);
+      // assert 'None' colour scheme is retained after round trip
+      JSONFile _bioJsonFile = (JSONFile) formatAdapter.getAlignFile();
+      Assert.assertEquals(_bioJsonFile.getGlobalColourScheme(),
+              ResidueColourScheme.NONE);
+    } catch (IOException e)
+    {
+      e.printStackTrace();
+    }
+  }
+
   @Test(groups = { "Functional" })
   public void isShowSeqFeaturesSet()
   {
@@ -400,7 +466,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());
 
@@ -434,14 +500,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()
@@ -454,7 +525,6 @@ public class JSONFileTest
 
   private boolean featuresMatched(SequenceI seq1, SequenceI seq2)
   {
-    boolean matched = false;
     try
     {
       if (seq1 == null && seq2 == null)
@@ -462,51 +532,137 @@ 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;
+  }
+
+  /**
+   * Test group roundtrip with null (None) group colour scheme
+   * 
+   * @throws IOException
+   */
+  @Test(groups = { "Functional" })
+  public void testGrpParsed_colourNone() throws IOException
+  {
+    AlignmentI copy = new Alignment(testAlignment);
+    SequenceGroup sg = testAlignment.getGroups().get(0);
+    SequenceGroup copySg = new SequenceGroup(new ArrayList<SequenceI>(),
+            sg.getName(),
+            null, sg.getDisplayBoxes(), sg.getDisplayText(),
+            sg.getColourText(), sg.getStartRes(), sg.getEndRes());
+    for (SequenceI seq : sg.getSequences())
+    {
+      int seqIndex = testAlignment.findIndex(seq);
+      copySg.addSequence(copy.getSequenceAt(seqIndex), false);
+    }
+    copy.addGroup(copySg);
+
+    AlignFrame af = new AlignFrame(copy, copy.getWidth(), copy.getHeight());
+    AppletFormatAdapter formatAdapter = new AppletFormatAdapter(
+            af.alignPanel);
+    String jsonOutput = formatAdapter.formatSequences(FileFormat.Json,
+            copy, false);
+    formatAdapter = new AppletFormatAdapter();
+    AlignmentI newAlignment = formatAdapter.readFile(jsonOutput,
+            DataSourceType.PASTE, FileFormat.Json);
+
+    Assert.assertNotNull(newAlignment.getGroups());
+    for (SequenceGroup seqGrp : newAlignment.getGroups())
+    {
+      SequenceGroup expectedGrp = copySg;
+      AssertJUnit.assertTrue(
+              "Failed SequenceGroup Test for >>> " + seqGrp.getName(),
+              isGroupMatched(expectedGrp, seqGrp));
+      passedCount++;
+    }
+    AssertJUnit.assertEquals("Some SequenceGroups did not pass the test",
+            TEST_GRP_HEIGHT, passedCount);
   }
 }
index de12086..6abb7e5 100644 (file)
  */
 package jalview.io;
 
-import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertNotNull;
-import static org.testng.AssertJUnit.assertSame;
-import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
 
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
@@ -38,18 +38,27 @@ import jalview.datamodel.SequenceCollectionI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.Desktop;
 import jalview.gui.Jalview2XML;
 import jalview.gui.JvOptionPane;
+import jalview.gui.PopupMenu;
+import jalview.gui.SliderPanel;
+import jalview.renderer.ResidueShaderI;
 import jalview.schemes.AnnotationColourGradient;
+import jalview.schemes.BuriedColourScheme;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
+import jalview.schemes.JalviewColourScheme;
+import jalview.schemes.RNAHelicesColour;
+import jalview.schemes.StrandColourScheme;
 import jalview.schemes.TCoffeeColourScheme;
 import jalview.structure.StructureImportSettings;
 import jalview.viewmodel.AlignmentViewport;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -64,6 +73,7 @@ import org.testng.annotations.Test;
 public class Jalview2xmlTests extends Jalview2xmlBase
 {
 
+  @Override
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
   {
@@ -77,32 +87,34 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     String inFile = "examples/RF00031_folded.stk";
     String tfile = File.createTempFile("JalviewTest", ".jvp")
             .getAbsolutePath();
-    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
-            inFile, DataSourceType.FILE);
-    assertTrue("Didn't read input file " + inFile, af != null);
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
+            DataSourceType.FILE);
+    assertNotNull(af, "Didn't read input file " + inFile);
     int olddsann = countDsAnn(af.getViewport());
-    assertTrue("Didn't find any dataset annotations", olddsann > 0);
-    af.rnahelicesColour_actionPerformed(null);
+    assertTrue(olddsann > 0, "Didn't find any dataset annotations");
+    af.changeColour_actionPerformed(JalviewColourScheme.RNAHelices
+            .toString());
     assertTrue(
-            "Couldn't apply RNA helices colourscheme",
-            af.getViewport().getGlobalColourScheme() instanceof jalview.schemes.RNAHelicesColour);
-    assertTrue("Failed to store as a project.",
-            af.saveAlignment(tfile, FileFormat.Jalview));
+            af.getViewport().getGlobalColourScheme() instanceof RNAHelicesColour,
+            "Couldn't apply RNA helices colourscheme");
+    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
+            "Failed to store as a project.");
     af.closeMenuItem_actionPerformed(true);
     af = null;
-    af = new FileLoader().LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
-    assertTrue("Failed to import new project", af != null);
+    af = new FileLoader()
+            .LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
+    assertNotNull(af, "Failed to import new project");
     int newdsann = countDsAnn(af.getViewport());
-    assertTrue(
+    assertEquals(olddsann, newdsann,
             "Differing numbers of dataset sequence annotation\nOriginally "
-                    + olddsann + " and now " + newdsann,
-            olddsann == newdsann);
+                    + olddsann + " and now " + newdsann);
     System.out
             .println("Read in same number of annotations as originally present ("
                     + olddsann + ")");
     assertTrue(
-            "RNA helices colourscheme was not applied on import.",
-            af.getViewport().getGlobalColourScheme() instanceof jalview.schemes.RNAHelicesColour);
+
+    af.getViewport().getGlobalColourScheme() instanceof RNAHelicesColour,
+            "RNA helices colourscheme was not applied on import.");
   }
 
   @Test(groups = { "Functional" })
@@ -111,27 +123,26 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     String inFile = "examples/uniref50.fa", inAnnot = "examples/uniref50.score_ascii";
     String tfile = File.createTempFile("JalviewTest", ".jvp")
             .getAbsolutePath();
-    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
-            inFile, DataSourceType.FILE);
-    assertNotNull("Didn't read input file " + inFile, af);
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
+            DataSourceType.FILE);
+    assertNotNull(af, "Didn't read input file " + inFile);
     af.loadJalviewDataFile(inAnnot, DataSourceType.FILE, null, null);
-    assertSame("Didn't set T-coffee colourscheme", af.getViewport()
-            .getGlobalColourScheme().getClass(), TCoffeeColourScheme.class);
-    assertNotNull("Recognise T-Coffee score from string",
-            ColourSchemeProperty.getColour(af.getViewport()
-                    .getAlignment(), ColourSchemeProperty.getColourName(af
-                    .getViewport().getGlobalColourScheme())));
-
-    assertTrue("Failed to store as a project.",
-            af.saveAlignment(tfile, FileFormat.Jalview));
+    assertSame(af.getViewport().getGlobalColourScheme().getClass(),
+            TCoffeeColourScheme.class, "Didn't set T-coffee colourscheme");
+    assertNotNull(ColourSchemeProperty.getColourScheme(af.getViewport()
+            .getAlignment(), af.getViewport().getGlobalColourScheme()
+            .getSchemeName()), "Recognise T-Coffee score from string");
+
+    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
+            "Failed to store as a project.");
     af.closeMenuItem_actionPerformed(true);
     af = null;
-    af = new FileLoader().LoadFileWaitTillLoaded(tfile,
-            DataSourceType.FILE);
-    assertNotNull("Failed to import new project", af);
-    assertSame("Didn't set T-coffee colourscheme for imported project.", af
-            .getViewport().getGlobalColourScheme().getClass(),
-            TCoffeeColourScheme.class);
+    af = new FileLoader()
+            .LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
+    assertNotNull(af, "Failed to import new project");
+    assertSame(af.getViewport().getGlobalColourScheme().getClass(),
+            TCoffeeColourScheme.class,
+            "Didn't set T-coffee colourscheme for imported project.");
     System.out
             .println("T-Coffee score shading successfully recovered from project.");
   }
@@ -142,14 +153,16 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     String inFile = "examples/uniref50.fa", inAnnot = "examples/testdata/uniref50_iupred.jva";
     String tfile = File.createTempFile("JalviewTest", ".jvp")
             .getAbsolutePath();
-    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile, DataSourceType.FILE);
-    assertNotNull("Didn't read input file " + inFile, af);
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(inFile,
+            DataSourceType.FILE);
+    assertNotNull(af, "Didn't read input file " + inFile);
     af.loadJalviewDataFile(inAnnot, DataSourceType.FILE, null, null);
     AlignmentAnnotation[] aa = af.getViewport().getAlignment()
             .getSequenceAt(0).getAnnotation("IUPredWS (Short)");
     assertTrue(
-            "Didn't find any IUPred annotation to use to shade alignment.",
-            aa != null && aa.length > 0);
+
+    aa != null && aa.length > 0,
+            "Didn't find any IUPred annotation to use to shade alignment.");
     AnnotationColourGradient cs = new AnnotationColourGradient(aa[0], null,
             AnnotationColourGradient.ABOVE_THRESHOLD);
     AnnotationColourGradient gcs = new AnnotationColourGradient(aa[0],
@@ -160,62 +173,63 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     SequenceGroup sg = new SequenceGroup();
     sg.setStartRes(57);
     sg.setEndRes(92);
-    sg.cs = gcs;
+    sg.cs.setColourScheme(gcs);
     af.getViewport().getAlignment().addGroup(sg);
     sg.addSequence(af.getViewport().getAlignment().getSequenceAt(1), false);
     sg.addSequence(af.getViewport().getAlignment().getSequenceAt(2), true);
     af.alignPanel.alignmentChanged();
-    assertTrue("Failed to store as a project.",
-            af.saveAlignment(tfile, FileFormat.Jalview));
+    assertTrue(af.saveAlignment(tfile, FileFormat.Jalview),
+            "Failed to store as a project.");
     af.closeMenuItem_actionPerformed(true);
     af = null;
-    af = new FileLoader().LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
-    assertTrue("Failed to import new project", af != null);
+    af = new FileLoader()
+            .LoadFileWaitTillLoaded(tfile, DataSourceType.FILE);
+    assertNotNull(af, "Failed to import new project");
 
     // check for group and alignment colourschemes
 
     ColourSchemeI _rcs = af.getViewport().getGlobalColourScheme();
     ColourSchemeI _rgcs = af.getViewport().getAlignment().getGroups()
-            .get(0).cs;
-    assertNotNull("Didn't recover global colourscheme", _rcs);
-    assertTrue("Didn't recover annotation colour global scheme",
-            _rcs instanceof AnnotationColourGradient);
+            .get(0).getColourScheme();
+    assertNotNull(_rcs, "Didn't recover global colourscheme");
+    assertTrue(_rcs instanceof AnnotationColourGradient,
+            "Didn't recover annotation colour global scheme");
     AnnotationColourGradient __rcs = (AnnotationColourGradient) _rcs;
-    assertTrue("Annotation colourscheme wasn't sequence associated",
-            __rcs.isSeqAssociated());
+    assertTrue(__rcs.isSeqAssociated(),
+            "Annotation colourscheme wasn't sequence associated");
 
     boolean diffseqcols = false, diffgseqcols = false;
     SequenceI[] sqs = af.getViewport().getAlignment().getSequencesArray();
     for (int p = 0, pSize = af.getViewport().getAlignment().getWidth(); p < pSize
             && (!diffseqcols || !diffgseqcols); p++)
     {
-      if (_rcs.findColour(sqs[0].getCharAt(p), p, sqs[0]) != _rcs
-              .findColour(sqs[5].getCharAt(p), p, sqs[5]))
+      if (_rcs.findColour(sqs[0].getCharAt(p), p, sqs[0], null, 0f) != _rcs
+              .findColour(sqs[5].getCharAt(p), p, sqs[5], null, 0f))
       {
         diffseqcols = true;
       }
     }
-    assertTrue("Got Different sequence colours", diffseqcols);
+    assertTrue(diffseqcols, "Got Different sequence colours");
     System.out
             .println("Per sequence colourscheme (Background) successfully applied and recovered.");
 
-    assertNotNull("Didn't recover group colourscheme", _rgcs);
-    assertTrue("Didn't recover annotation colour group colourscheme",
-            _rgcs instanceof AnnotationColourGradient);
+    assertNotNull(_rgcs, "Didn't recover group colourscheme");
+    assertTrue(_rgcs instanceof AnnotationColourGradient,
+            "Didn't recover annotation colour group colourscheme");
     __rcs = (AnnotationColourGradient) _rgcs;
-    assertTrue("Group Annotation colourscheme wasn't sequence associated",
-            __rcs.isSeqAssociated());
+    assertTrue(__rcs.isSeqAssociated(),
+            "Group Annotation colourscheme wasn't sequence associated");
 
     for (int p = 0, pSize = af.getViewport().getAlignment().getWidth(); p < pSize
             && (!diffseqcols || !diffgseqcols); p++)
     {
-      if (_rgcs.findColour(sqs[1].getCharAt(p), p, sqs[1]) != _rgcs
-              .findColour(sqs[2].getCharAt(p), p, sqs[2]))
+      if (_rgcs.findColour(sqs[1].getCharAt(p), p, sqs[1], null, 0f) != _rgcs
+              .findColour(sqs[2].getCharAt(p), p, sqs[2], null, 0f))
       {
         diffgseqcols = true;
       }
     }
-    assertTrue("Got Different group sequence colours", diffgseqcols);
+    assertTrue(diffgseqcols, "Got Different group sequence colours");
     System.out
             .println("Per sequence (Group) colourscheme successfully applied and recovered.");
   }
@@ -227,9 +241,9 @@ public class Jalview2xmlTests extends Jalview2xmlBase
             .getAlignFrames().length;
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/exampleFile_2_7.jar", DataSourceType.FILE);
-    assertNotNull("Didn't read in the example file correctly.", af);
-    assertTrue("Didn't gather the views in the example file.",
-            Desktop.getAlignFrames().length == 1 + origCount);
+    assertNotNull(af, "Didn't read in the example file correctly.");
+    assertTrue(Desktop.getAlignFrames().length == 1 + origCount,
+            "Didn't gather the views in the example file.");
 
   }
 
@@ -240,7 +254,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     StructureImportSettings.setVisibleChainAnnotation(true);
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/exampleFile_2_7.jar", DataSourceType.FILE);
-    assertNotNull("Didn't read in the example file correctly.", af);
+    assertNotNull(af, "Didn't read in the example file correctly.");
     AlignmentViewPanel sps = null;
     for (AlignmentViewPanel ap : af.alignPanel.alignFrame.getAlignPanels())
     {
@@ -250,7 +264,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
         break;
       }
     }
-    assertNotNull("Couldn't find the structure view", sps);
+    assertNotNull(sps, "Couldn't find the structure view");
     AlignmentAnnotation refan = null;
     for (AlignmentAnnotation ra : sps.getAlignment()
             .getAlignmentAnnotation())
@@ -261,13 +275,13 @@ public class Jalview2xmlTests extends Jalview2xmlBase
         break;
       }
     }
-    assertNotNull("Annotation secondary structure not found.", refan);
+    assertNotNull(refan, "Annotation secondary structure not found.");
     SequenceI sq = sps.getAlignment().findName("1A70|");
-    assertNotNull("Couldn't find 1a70 null chain", sq);
+    assertNotNull(sq, "Couldn't find 1a70 null chain");
     // compare the manually added temperature factor annotation
     // to the track automatically transferred from the pdb structure on load
-    assertNotNull("1a70 has no annotation", sq.getDatasetSequence()
-            .getAnnotation());
+    assertNotNull(sq.getDatasetSequence().getAnnotation(),
+            "1a70 has no annotation");
     for (AlignmentAnnotation ala : sq.getDatasetSequence().getAnnotation())
     {
       AlignmentAnnotation alaa;
@@ -281,9 +295,9 @@ public class Jalview2xmlTests extends Jalview2xmlBase
           try
           {
             assertTrue(
-                    "Mismatch at alignment position " + p,
                     (alaa.annotations[p] == null && refan.annotations[p] == null)
-                            || alaa.annotations[p].value == refan.annotations[p].value);
+                            || alaa.annotations[p].value == refan.annotations[p].value,
+                    "Mismatch at alignment position " + p);
           } catch (NullPointerException q)
           {
             Assert.fail("Mismatch of alignment annotations at position "
@@ -301,7 +315,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
   {
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/exampleFile_2_7.jar", DataSourceType.FILE);
-    assertNotNull("Didn't read in the example file correctly.", af);
+    assertNotNull(af, "Didn't read in the example file correctly.");
     AlignmentViewPanel sps = null, groups = null;
     for (AlignmentViewPanel ap : af.alignPanel.alignFrame.getAlignPanels())
     {
@@ -314,8 +328,8 @@ public class Jalview2xmlTests extends Jalview2xmlBase
         groups = ap;
       }
     }
-    assertNotNull("Couldn't find the structure view", sps);
-    assertNotNull("Couldn't find the MAFFT view", groups);
+    assertNotNull(sps, "Couldn't find the structure view");
+    assertNotNull(groups, "Couldn't find the MAFFT view");
 
     ViewStyleI structureStyle = sps.getAlignViewport().getViewStyle();
     ViewStyleI groupStyle = groups.getAlignViewport().getViewStyle();
@@ -346,9 +360,10 @@ public class Jalview2xmlTests extends Jalview2xmlBase
 
     // check FileLoader returned a reference to the one alignFrame that is
     // actually on the Desktop
-    assertTrue(
-            "Jalview2XML.loadAlignFrame() didn't return correct AlignFrame reference for multiple view window",
-            af == Desktop.getAlignFrameFor(af.getViewport()));
+    assertSame(
+            af,
+            Desktop.getAlignFrameFor(af.getViewport()),
+            "Jalview2XML.loadAlignFrame() didn't return correct AlignFrame reference for multiple view window");
 
     Desktop.explodeViews(af);
 
@@ -371,15 +386,15 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     {
       Assert.assertEquals(Desktop.getAlignFrames().length, 0);
     }
-    af = new FileLoader().LoadFileWaitTillLoaded(
-            tfile.getAbsolutePath(), DataSourceType.FILE);
+    af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
+            DataSourceType.FILE);
     Assert.assertNotNull(af);
     Assert.assertEquals(
             Desktop.getAlignFrames().length,
             Desktop.getAlignmentPanels(af.getViewport().getSequenceSetId()).length);
     Assert.assertEquals(
-            oldviews,
-            Desktop.getAlignmentPanels(af.getViewport().getSequenceSetId()).length);
+            Desktop.getAlignmentPanels(af.getViewport().getSequenceSetId()).length,
+            oldviews);
   }
 
   /**
@@ -394,7 +409,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     Desktop.instance.closeAll_actionPerformed(null);
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/exampleFile_2_7.jar", DataSourceType.FILE);
-    assertNotNull("Didn't read in the example file correctly.", af);
+    assertNotNull(af, "Didn't read in the example file correctly.");
     String afid = af.getViewport().getSequenceSetId();
 
     // remember reference sequence for each panel
@@ -436,8 +451,8 @@ public class Jalview2xmlTests extends Jalview2xmlBase
       Assert.assertEquals(Desktop.getAlignFrames().length, 0);
     }
 
-    af = new FileLoader().LoadFileWaitTillLoaded(
-            tfile.getAbsolutePath(), DataSourceType.FILE);
+    af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
+            DataSourceType.FILE);
     afid = af.getViewport().getSequenceSetId();
 
     for (AlignmentViewPanel ap : Desktop.getAlignmentPanels(afid))
@@ -527,7 +542,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     Desktop.instance.closeAll_actionPerformed(null);
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
             "examples/uniref50.fa", DataSourceType.FILE);
-    assertNotNull("Didn't read in the example file correctly.", af);
+    assertNotNull(af, "Didn't read in the example file correctly.");
     String afid = af.getViewport().getSequenceSetId();
     // make a second view of the alignment
     af.newView_actionPerformed(null);
@@ -578,7 +593,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
       assertSame(repSeq, sg.getSeqrep());
       assertTrue(sg.getSequences().contains(repSeq));
       assertTrue(sg.getSequences().contains(precedingSeq));
-      assertTrue("alignment has groups", alignment.getGroups().isEmpty());
+      assertTrue(alignment.getGroups().isEmpty(), "alignment has groups");
       Map<SequenceI, SequenceCollectionI> hiddenRepSeqsMap = av
               .getHiddenRepSequences();
       assertNotNull(hiddenRepSeqsMap);
@@ -604,9 +619,9 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     {
       Assert.assertEquals(Desktop.getAlignFrames().length, 0);
     }
-  
-    af = new FileLoader().LoadFileWaitTillLoaded(
-            tfile.getAbsolutePath(), DataSourceType.FILE);
+
+    af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
+            DataSourceType.FILE);
     afid = af.getViewport().getSequenceSetId();
 
     for (AlignmentViewPanel ap : Desktop.getAlignmentPanels(afid))
@@ -616,10 +631,10 @@ public class Jalview2xmlTests extends Jalview2xmlBase
       AlignmentI alignment = ap.getAlignment();
       List<SequenceGroup> groups = alignment.getGroups();
       assertNotNull(groups);
-      assertTrue("Alignment has groups", groups.isEmpty());
+      assertTrue(groups.isEmpty(), "Alignment has groups");
       Map<SequenceI, SequenceCollectionI> hiddenRepSeqsMap = av
               .getHiddenRepSequences();
-      assertNotNull("No hidden represented sequences", hiddenRepSeqsMap);
+      assertNotNull(hiddenRepSeqsMap, "No hidden represented sequences");
       assertEquals(1, hiddenRepSeqsMap.size());
       assertEquals(repSeqs.get(viewName).getDisplayId(true),
               hiddenRepSeqsMap.keySet().iterator().next()
@@ -631,8 +646,10 @@ public class Jalview2xmlTests extends Jalview2xmlBase
       List<String> hidden = hiddenSeqNames.get(ap.getViewName());
       HiddenSequences hs = alignment.getHiddenSequences();
       assertEquals(
+              hidden.size(),
+              hs.getSize(),
               "wrong number of restored hidden sequences in "
-                      + ap.getViewName(), hidden.size(), hs.getSize());
+                      + ap.getViewName());
     }
   }
 
@@ -648,7 +665,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     String exampleFile = "examples/3W5V.pdb";
     AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(exampleFile,
             DataSourceType.FILE);
-    assertNotNull("Didn't read in the example file correctly.", af);
+    assertNotNull(af, "Didn't read in the example file correctly.");
     String afid = af.getViewport().getSequenceSetId();
 
     AlignmentPanel[] alignPanels = Desktop.getAlignmentPanels(afid);
@@ -729,4 +746,99 @@ public class Jalview2xmlTests extends Jalview2xmlBase
               "Recovered PDBEntry should have a non-null file entry");
     }
   }
+
+  /**
+   * Configure an alignment and a sub-group each with distinct colour schemes,
+   * Conservation and PID thresholds, and confirm these are restored from the
+   * saved project.
+   * 
+   * @throws IOException
+   */
+  @Test(groups = { "Functional" })
+  public void testStoreAndRecoverColourThresholds() throws IOException
+  {
+    Desktop.instance.closeAll_actionPerformed(null);
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+
+    AlignViewport av = af.getViewport();
+    AlignmentI al = av.getAlignment();
+
+    /*
+     * Colour alignment by Buried Index, Above 10% PID, By Conservation 20%
+     */
+    af.changeColour_actionPerformed(JalviewColourScheme.Buried.toString());
+    assertTrue(av.getGlobalColourScheme() instanceof BuriedColourScheme);
+    af.abovePIDThreshold_actionPerformed(true);
+    SliderPanel sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    sp.valueChanged(10);
+    af.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    sp.valueChanged(20);
+    ResidueShaderI rs = av.getResidueShading();
+    assertEquals(rs.getThreshold(), 10);
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 20);
+
+    /*
+     * create a group with Strand colouring, 30% Conservation
+     * and 40% PID threshold
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.addSequence(al.getSequenceAt(0), false);
+    sg.setStartRes(15);
+    sg.setEndRes(25);
+    av.setSelectionGroup(sg);
+    PopupMenu popupMenu = new PopupMenu(af.alignPanel, null, null);
+    popupMenu.changeColour_actionPerformed(JalviewColourScheme.Strand
+            .toString());
+    assertTrue(sg.getColourScheme() instanceof StrandColourScheme);
+    assertEquals(al.getGroups().size(), 1);
+    assertSame(al.getGroups().get(0), sg);
+    popupMenu.conservationMenuItem_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertTrue(sp.isForConservation());
+    sp.valueChanged(30);
+    popupMenu.abovePIDColour_actionPerformed(true);
+    sp = SliderPanel.getSliderPanel();
+    assertFalse(sp.isForConservation());
+    sp.valueChanged(40);
+    assertTrue(sg.getGroupColourScheme().conservationApplied());
+    assertEquals(sg.getGroupColourScheme().getConservationInc(), 30);
+    assertEquals(sg.getGroupColourScheme().getThreshold(), 40);
+
+    /*
+     * save project, close windows, reload project, verify
+     */
+    File tfile = File.createTempFile("testStoreAndRecoverColourThresholds",
+            ".jvp");
+    tfile.deleteOnExit();
+    new Jalview2XML(false).saveState(tfile);
+    Desktop.instance.closeAll_actionPerformed(null);
+    af = new FileLoader().LoadFileWaitTillLoaded(tfile.getAbsolutePath(),
+            DataSourceType.FILE);
+    Assert.assertNotNull(af, "Failed to reload project");
+
+    /*
+     * verify alignment (background) colouring
+     */
+    rs = af.getViewport().getResidueShading();
+    assertTrue(rs.getColourScheme() instanceof BuriedColourScheme);
+    assertEquals(rs.getThreshold(), 10);
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 20);
+
+    /*
+     * verify group colouring
+     */
+    assertEquals(1, af.getViewport().getAlignment().getGroups().size(), 1);
+    rs = af.getViewport().getAlignment().getGroups().get(0)
+            .getGroupColourScheme();
+    assertTrue(rs.getColourScheme() instanceof StrandColourScheme);
+    assertEquals(rs.getThreshold(), 40);
+    assertTrue(rs.conservationApplied());
+    assertEquals(rs.getConservationInc(), 30);
+  }
 }
index d867e5e..aa124d9 100644 (file)
@@ -14,7 +14,7 @@ import org.testng.annotations.Test;
 
 public class JalviewFileViewTest
 {
-  @Test
+  @Test(groups = "Functional")
   public void testGetImageIcon()
   {
     JalviewFileView jfv = new JalviewFileView();
@@ -36,7 +36,7 @@ public class JalviewFileViewTest
     assertNull(jfv.getImageIcon("images/file.png"));
   }
 
-  @Test
+  @Test(groups = "Functional")
   public void testGetExtension()
   {
     assertEquals(JalviewFileView.getExtension(new File("text.txt")), "txt");
@@ -48,7 +48,7 @@ public class JalviewFileViewTest
             "/a/longer/file/path/text")));
   }
 
-  @Test
+  @Test(groups = "Functional")
   public void testGetTypeDescription()
   {
     JalviewFileView jfw = new JalviewFileView();
index d198f0f..a92f5fb 100644 (file)
@@ -22,8 +22,8 @@ package jalview.io;
 
 import static org.testng.ConversionUtils.wrapDataProvider;
 
-import jalview.analysis.NJTree;
 import jalview.analysis.SequenceIdMatcher;
+import jalview.analysis.TreeModel;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.SequenceNode;
 import jalview.gui.JvOptionPane;
@@ -125,7 +125,8 @@ public class NewickFileTests
       stage = "Compare original and generated tree" + treename;
 
       Vector<SequenceNode> oseqs, nseqs;
-      oseqs = new NJTree(new SequenceI[0], nf).findLeaves(nf.getTree());
+      oseqs = new TreeModel(new SequenceI[0], null, nf).findLeaves(nf
+              .getTree());
       AssertJUnit.assertTrue(stage + "No nodes in original tree.",
               oseqs.size() > 0);
       SequenceI[] olsqs = new SequenceI[oseqs.size()];
@@ -133,7 +134,8 @@ public class NewickFileTests
       {
         olsqs[i] = (SequenceI) oseqs.get(i).element();
       }
-      nseqs = new NJTree(new SequenceI[0], nf_regen).findLeaves(nf_regen
+      nseqs = new TreeModel(new SequenceI[0], null, nf_regen)
+              .findLeaves(nf_regen
               .getTree());
       AssertJUnit.assertTrue(stage + "No nodes in regerated tree.",
               nseqs.size() > 0);
index f99c67a..d853350 100644 (file)
@@ -190,7 +190,8 @@ public class PhylipFileTests
             DataSourceType.PASTE, FileFormat.Phylip);
     assertNotNull("Couldn't parse reimported alignment data.", al_input);
 
-    StockholmFileTest.testAlignmentEquivalence(al, al_input, false);
+    StockholmFileTest.testAlignmentEquivalence(al, al_input, false, false,
+            false);
 
   }
 }
index d16fb5f..ae72de4 100644 (file)
@@ -53,7 +53,7 @@ public class RNAMLfileTest
   {
     StockholmFileTest.testFileIOwithFormat(new File(
             "examples/testdata/rna-alignment.xml"), FileFormat.Stockholm,
-            -1, -1);
+            -1, -1, true, true, true);
 
   }
 
diff --git a/test/jalview/io/ScoreMatrixFileTest.java b/test/jalview/io/ScoreMatrixFileTest.java
new file mode 100644 (file)
index 0000000..97349b5
--- /dev/null
@@ -0,0 +1,506 @@
+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.assertTrue;
+import static org.testng.Assert.fail;
+
+import jalview.analysis.scoremodels.ScoreMatrix;
+import jalview.analysis.scoremodels.ScoreModels;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+
+import org.testng.annotations.Test;
+
+public class ScoreMatrixFileTest
+{
+
+  /**
+   * Test a successful parse of a (small) score matrix file
+   * 
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiMixedDelimiters()
+          throws MalformedURLException,
+          IOException
+  {
+    /*
+     * some messy but valid input data, with comma, space
+     * or tab (or combinations) as score value delimiters
+     * this example includes 'guide' symbols on score rows
+     */
+    String data = "ScoreMatrix MyTest (example)\n" + "A\tT\tU\tt\tx\t-\n"
+            + "A,1.1,1.2,1.3,1.4, 1.5, 1.6\n"
+            + "T,2.1 2.2 2.3 2.4 2.5 2.6\n"
+            + "U\t3.1\t3.2\t3.3\t3.4\t3.5\t3.6\t\n"
+            + "t, 5.1,5.3,5.3,5.4,5.5, 5.6\n"
+            + "x\t6.1, 6.2 6.3 6.4 6.5 6.6\n"
+            + "-, \t7.1\t7.2 7.3, 7.4, 7.5\t,7.6\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    ScoreMatrix sm = parser.parseMatrix();
+
+    assertNotNull(sm);
+    assertEquals(sm.getName(), "MyTest (example)");
+    assertEquals(sm.getSize(), 6);
+    assertNull(sm.getDescription());
+    assertTrue(sm.isDNA());
+    assertFalse(sm.isProtein());
+    assertEquals(sm.getMinimumScore(), 1.1f);
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1.1f);
+    assertEquals(sm.getPairwiseScore('A', 'T'), 1.2f);
+    assertEquals(sm.getPairwiseScore('a', 'T'), 1.2f); // A/a equivalent
+    assertEquals(sm.getPairwiseScore('A', 't'), 1.4f); // T/t not equivalent
+    assertEquals(sm.getPairwiseScore('a', 't'), 1.4f);
+    assertEquals(sm.getPairwiseScore('U', 'x'), 3.5f);
+    assertEquals(sm.getPairwiseScore('u', 'x'), 3.5f);
+    // X (upper) and '.' unmapped - get minimum score
+    assertEquals(sm.getPairwiseScore('U', 'X'), 1.1f);
+    assertEquals(sm.getPairwiseScore('A', '.'), 1.1f);
+    assertEquals(sm.getPairwiseScore('-', '-'), 7.6f);
+    assertEquals(sm.getPairwiseScore('A', (char) 128), 0f); // out of range
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_headerMissing()
+  {
+    String data;
+
+    data = "X Y\n1 2\n3 4\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Format error: 'ScoreMatrix <name>' should be the first non-comment line");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiNotEnoughRows()
+  {
+    String data = "ScoreMatrix MyTest\nX Y Z\n1 2 3\n4 5 6\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Expected 3 rows of score data in score matrix but only found 2");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiNotEnoughColumns()
+  {
+    String data = "ScoreMatrix MyTest\nX Y Z\n1 2 3\n4 5\n7 8 9\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Expected 3 scores at line 4: '4 5' but found 2");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiTooManyColumns()
+  {
+    /*
+     * with two too many columns:
+     */
+    String data = "ScoreMatrix MyTest\nX\tY\tZ\n1 2 3\n4 5 6 7\n8 9 10\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Expected 3 scores at line 4: '4 5 6 7' but found 4");
+    }
+
+    /*
+     * with guide character and one too many columns:
+     */
+    data = "ScoreMatrix MyTest\nX Y\nX 1 2\nY 3 4 5\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Expected 2 scores at line 4: 'Y 3 4 5' but found 3");
+    }
+
+    /*
+     * with no guide character and one too many columns
+     */
+    data = "ScoreMatrix MyTest\nX Y\n1 2\n3 4 5\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Expected 2 scores at line 4: '3 4 5' but found 3");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiTooManyRows()
+  {
+    String data = "ScoreMatrix MyTest\n\tX\tY\tZ\n1 2 3\n4 5 6\n7 8 9\n10 11 12\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Unexpected extra input line in score model file: '10 11 12'");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiBadDelimiter()
+  {
+    String data = "ScoreMatrix MyTest\n X Y Z\n1|2|3\n4|5|6\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Invalid score value '1|2|3' at line 3 column 0");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiBadFloat()
+  {
+    String data = "ScoreMatrix MyTest\n\tX\tY\tZ\n1 2 3\n4 five 6\n7 8 9\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Invalid score value 'five' at line 4 column 1");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiBadGuideCharacter()
+  {
+    String data = "ScoreMatrix MyTest\n\tX Y\nX 1 2\ny 3 4\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Error parsing score matrix at line 4, expected 'Y' but found 'y'");
+    }
+
+    data = "ScoreMatrix MyTest\n\tX Y\nXX 1 2\nY 3 4\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Error parsing score matrix at line 3, expected 'X' but found 'XX'");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiNameMissing()
+  {
+    /*
+     * Name missing on ScoreMatrix header line
+     */
+    String data = "ScoreMatrix\nX Y\n1 2\n3 4\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(
+              e.getMessage(),
+              "Format error: expected 'ScoreMatrix <name>', found 'ScoreMatrix' at line 1");
+    }
+  }
+
+  /**
+   * Test a successful parse of a (small) score matrix file
+   * 
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiFormat() throws MalformedURLException,
+          IOException
+  {
+    // input including comment and blank lines
+    String data = "ScoreMatrix MyTest\n#comment\n\n" + "\tA\tB\tC\n"
+            + "A\t1.0\t2.0\t3.0\n" + "B\t4.0\t5.0\t6.0\n"
+            + "C\t7.0\t8.0\t9.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    ScoreMatrix sm = parser.parseMatrix();
+  
+    assertNotNull(sm);
+    assertEquals(sm.getName(), "MyTest");
+    assertEquals(parser.getMatrixName(), "MyTest");
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1.0f);
+    assertEquals(sm.getPairwiseScore('B', 'c'), 6.0f);
+    assertEquals(sm.getSize(), 3);
+  }
+
+  /**
+   * Test a successful parse of a (small) score matrix file
+   * 
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaIndexBlosum80()
+          throws MalformedURLException,
+          IOException
+  {
+    FileParse fp = new FileParse("resources/scoreModel/blosum80.scm",
+            DataSourceType.FILE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    ScoreMatrix sm = parser.parseMatrix();
+  
+    assertNotNull(sm);
+    assertEquals(sm.getName(), "HENS920103");
+    assertEquals(sm.getDescription(),
+            "BLOSUM80 substitution matrix (Henikoff-Henikoff, 1992)");
+    assertFalse(sm.isDNA());
+    assertTrue(sm.isProtein());
+    assertEquals(20, sm.getSize());
+
+    assertEquals(sm.getPairwiseScore('A', 'A'), 7f);
+    assertEquals(sm.getPairwiseScore('A', 'R'), -3f);
+    assertEquals(sm.getPairwiseScore('r', 'a'), -3f); // A/a equivalent
+  }
+
+  /**
+   * Test a successful parse of a (small) score matrix file
+   * 
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaindexFormat() throws MalformedURLException,
+          IOException
+  {
+    /*
+     * aaindex format has scores for diagonal and below only
+     */
+    String data = "H MyTest\n" + "D My description\n" + "R PMID:1438297\n"
+            + "A Authors, names\n" + "T Journal title\n"
+            + "J Journal reference\n" + "* matrix in 1/3 Bit Units\n"
+            + "M rows = ABC, cols = ABC\n" + "A\t1.0\n"
+            + "B\t4.0\t5.0\n"
+            + "C\t7.0\t8.0\t9.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    ScoreMatrix sm = parser.parseMatrix();
+  
+    assertNotNull(sm);
+    assertEquals(sm.getSize(), 3);
+    assertEquals(sm.getName(), "MyTest");
+    assertEquals(sm.getDescription(), "My description");
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1.0f);
+    assertEquals(sm.getPairwiseScore('A', 'B'), 4.0f);
+    assertEquals(sm.getPairwiseScore('A', 'C'), 7.0f);
+    assertEquals(sm.getPairwiseScore('B', 'A'), 4.0f);
+    assertEquals(sm.getPairwiseScore('B', 'B'), 5.0f);
+    assertEquals(sm.getPairwiseScore('B', 'C'), 8.0f);
+    assertEquals(sm.getPairwiseScore('C', 'C'), 9.0f);
+    assertEquals(sm.getPairwiseScore('C', 'B'), 8.0f);
+    assertEquals(sm.getPairwiseScore('C', 'A'), 7.0f);
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaindex_mMissing()
+          throws MalformedURLException,
+          IOException
+  {
+    /*
+     * aaindex format but M cols=, rows= is missing
+     */
+    String data = "H MyTest\n" + "A\t1.0\n"
+            + "B\t4.0\t5.0\n"
+            + "C\t7.0\t8.0\t9.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    try
+    {
+      parser.parseMatrix();
+      fail("Expected exception");
+    } catch (FileFormatException e)
+    {
+      assertEquals(e.getMessage(), "No alphabet specified in matrix file");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaindex_rowColMismatch()
+          throws MalformedURLException,
+          IOException
+  {
+    String data = "H MyTest\n" + "M rows=ABC, cols=ABD\n" + "A\t1.0\n"
+            + "B\t4.0\t5.0\n"
+            + "C\t7.0\t8.0\t9.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    try
+    {
+      parser.parseMatrix();
+      fail("Expected exception");
+    } catch (FileFormatException e)
+    {
+      assertEquals(
+              e.getMessage(),
+              "Unexpected aaIndex score matrix data at line 2: M rows=ABC, cols=ABD rows != cols");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_ncbiHeaderRepeated()
+  {
+    String data = "ScoreMatrix BLOSUM\nScoreMatrix PAM250\nX Y\n1 2\n3 4\n";
+    try
+    {
+      new ScoreMatrixFile(new FileParse(data, DataSourceType.PASTE))
+              .parseMatrix();
+      fail("expected exception");
+    } catch (IOException e)
+    {
+      assertEquals(e.getMessage(),
+              "Error: 'ScoreMatrix' repeated in file at line 2");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaindex_tooManyRows()
+          throws MalformedURLException,
+          IOException
+  {
+    String data = "H MyTest\n" + "M rows=ABC, cols=ABC\n" + "A\t1.0\n"
+            + "B\t4.0\t5.0\n" + "C\t7.0\t8.0\t9.0\n" + "C\t7.0\t8.0\t9.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    try
+    {
+      parser.parseMatrix();
+      fail("Expected exception");
+    } catch (FileFormatException e)
+    {
+      assertEquals(e.getMessage(), "Too many data rows in matrix file");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaindex_extraDataLines()
+          throws MalformedURLException,
+          IOException
+  {
+    String data = "H MyTest\n" + "M rows=ABC, cols=ABC\n" + "A\t1.0\n"
+            + "B\t4.0\t5.0\n" + "C\t7.0\t8.0\t9.0\n" + "something extra\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    try
+    {
+      parser.parseMatrix();
+      fail("Expected exception");
+    } catch (FileFormatException e)
+    {
+      assertEquals(e.getMessage(), "Too many data rows in matrix file");
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testParseMatrix_aaindex_tooFewColumns()
+          throws MalformedURLException,
+          IOException
+  {
+    String data = "H MyTest\n" + "M rows=ABC, cols=ABC\n" + "A\t1.0\n"
+            + "B\t4.0\t5.0\n" + "C\t7.0\t8.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+    try
+    {
+      parser.parseMatrix();
+      fail("Expected exception");
+    } catch (FileFormatException e)
+    {
+      assertEquals(
+              e.getMessage(),
+              "Expected 3 scores at line 5: 'C\t7.0\t8.0' but found 2");
+    }
+  }
+
+  /**
+   * Test a successful parse and register of a score matrix file
+   * 
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  @Test(groups = "Functional")
+  public void testParse_ncbiFormat() throws MalformedURLException,
+          IOException
+  {
+    assertNull(ScoreModels.getInstance().getScoreModel("MyNewTest", null));
+
+    String data = "ScoreMatrix MyNewTest\n" + "\tA\tB\tC\n"
+            + "A\t1.0\t2.0\t3.0\n" + "B\t4.0\t5.0\t6.0\n"
+            + "C\t7.0\t8.0\t9.0\n";
+    FileParse fp = new FileParse(data, DataSourceType.PASTE);
+    ScoreMatrixFile parser = new ScoreMatrixFile(fp);
+
+    parser.parse();
+  
+    ScoreMatrix sm = (ScoreMatrix) ScoreModels.getInstance().getScoreModel(
+            "MyNewTest", null);
+    assertNotNull(sm);
+    assertEquals(sm.getName(), "MyNewTest");
+    assertEquals(parser.getMatrixName(), "MyNewTest");
+    assertEquals(sm.getPairwiseScore('A', 'A'), 1.0f);
+    assertEquals(sm.getPairwiseScore('B', 'c'), 6.0f);
+    assertEquals(sm.getSize(), 3);
+  }
+}
index 2895874..9e61bec 100644 (file)
 package jalview.io;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
 
+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 java.util.HashMap;
 import java.util.Hashtable;
 import java.util.Map;
 
+import junit.extensions.PA;
+
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -192,4 +200,134 @@ 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</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));
+    Map<String, float[][]> minmax = new HashMap<String, float[][]>();
+    minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    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, minmax);
+    assertEquals(expected, sb.toString()); // unchanged!
+
+    /*
+     * 'clinical_significance' currently being specially included
+     */
+    SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0,
+            5f, null);
+    sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign");
+    seq.addSequenceFeature(sf2);
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    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, minmax);
+    assertEquals(expected, sb.toString()); // unchanged
+    // with showDbRefs = true
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    assertEquals(expected, sb.toString());
+    // with showNonPositionalFeatures = false
+    sb.setLength(0);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+    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 4028913..4273e6c 100644 (file)
@@ -33,10 +33,13 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
 import java.io.File;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -56,7 +59,8 @@ public class StockholmFileTest
   @Test(groups = { "Functional" })
   public void pfamFileIO() throws Exception
   {
-    testFileIOwithFormat(new File(PfamFile), FileFormat.Stockholm, -1, 0);
+    testFileIOwithFormat(new File(PfamFile), FileFormat.Stockholm, -1, 0,
+            false, false, false);
   }
 
   @Test(groups = { "Functional" })
@@ -81,7 +85,8 @@ public class StockholmFileTest
   @Test(groups = { "Functional" })
   public void rfamFileIO() throws Exception
   {
-    testFileIOwithFormat(new File(RfamFile), FileFormat.Stockholm, 2, 1);
+    testFileIOwithFormat(new File(RfamFile), FileFormat.Stockholm, 2, 1,
+            false, false, false);
   }
 
   /**
@@ -93,10 +98,14 @@ public class StockholmFileTest
    * @param ioformat
    *          - label for IO class used to write and read back in the data from
    *          f
+   * @param ignoreFeatures
+   * @param ignoreRowVisibility
+   * @param allowNullAnnotations
    */
 
   public static void testFileIOwithFormat(File f, FileFormatI ioformat,
-          int naliannot, int nminseqann)
+          int naliannot, int nminseqann, boolean ignoreFeatures,
+          boolean ignoreRowVisibility, boolean allowNullAnnotations)
   {
     System.out.println("Reading file: " + f);
     String ff = f.getPath();
@@ -130,7 +139,8 @@ public class StockholmFileTest
               "Identify routine could not recognise output generated by '"
                       + ioformat + "' writer",
               ioformat.equals(identifyoutput));
-      testAlignmentEquivalence(al, al_input, false);
+      testAlignmentEquivalence(al, al_input, ignoreFeatures,
+              ignoreRowVisibility, allowNullAnnotations);
       int numaliannot = 0, numsqswithali = 0;
       for (AlignmentAnnotation ala : al_input.getAlignmentAnnotation())
       {
@@ -174,6 +184,34 @@ public class StockholmFileTest
   public static void testAlignmentEquivalence(AlignmentI al,
           AlignmentI al_input, boolean ignoreFeatures)
   {
+    testAlignmentEquivalence(al, al_input, ignoreFeatures, false, false);
+  }
+
+  /**
+   * assert alignment equivalence - uses special comparators for RNA structure
+   * annotation rows.
+   * 
+   * @param al
+   *          'original'
+   * @param al_input
+   *          'secondary' or generated alignment from some datapreserving
+   *          transformation
+   * @param ignoreFeatures
+   *          when true, differences in sequence feature annotation are ignored
+   * 
+   * @param ignoreRowVisibility
+   *          when true, do not fail if there are differences in the visibility
+   *          of annotation rows
+   * @param allowNullAnnotation
+   *          when true, positions in alignment annotation that are null will be
+   *          considered equal to positions containing annotation where
+   *          Annotation.isWhitespace() returns true.
+   * 
+   */
+  public static void testAlignmentEquivalence(AlignmentI al,
+          AlignmentI al_input, boolean ignoreFeatures,
+          boolean ignoreRowVisibility, boolean allowNullAnnotation)
+  {
     assertNotNull("Original alignment was null", al);
     assertNotNull("Generated alignment was null", al_input);
 
@@ -201,13 +239,18 @@ public class StockholmFileTest
       {
         if (aa_new.length > i)
         {
-          assertTrue("Different alignment annotation at position " + i,
-                  equalss(aa_original[i], aa_new[i]));
+          assertEqualSecondaryStructure(
+                  "Different alignment annotation at position " + i,
+                  aa_original[i], aa_new[i], allowNullAnnotation);
           // compare graphGroup or graph properties - needed to verify JAL-1299
           assertEquals("Graph type not identical.", aa_original[i].graph,
                   aa_new[i].graph);
-          assertEquals("Visibility not identical.", aa_original[i].visible,
+          if (!ignoreRowVisibility)
+          {
+            assertEquals("Visibility not identical.",
+                    aa_original[i].visible,
                   aa_new[i].visible);
+          }
           assertEquals("Threshold line not identical.",
                   aa_original[i].threshold, aa_new[i].threshold);
           // graphGroup may differ, but pattern should be the same
@@ -244,7 +287,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++)
@@ -280,23 +324,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
@@ -310,8 +351,9 @@ public class StockholmFileTest
               {
                 annot_original = al.getSequenceAt(i).getAnnotation()[j];
                 annot_new = al_input.getSequenceAt(in).getAnnotation()[j];
-                assertTrue("Different annotation elements",
-                        equalss(annot_original, annot_new));
+                assertEqualSecondaryStructure(
+                        "Different annotation elements", annot_original,
+                        annot_new, allowNullAnnotation);
               }
             }
           }
@@ -333,39 +375,78 @@ public class StockholmFileTest
     }
   }
 
-  /*
-   * compare annotations
+  /**
+   * compare two annotation rows, with special support for secondary structure
+   * comparison. With RNA, only the value and the secondaryStructure symbols are
+   * compared, displayCharacter and description are ignored. Annotations where
+   * Annotation.isWhitespace() is true are always considered equal.
+   * 
+   * @param message
+   *          - not actually used yet..
+   * @param annot_or
+   *          - the original annotation
+   * @param annot_new
+   *          - the one compared to the original annotation
+   * @param allowNullEquivalence
+   *          when true, positions in alignment annotation that are null will be
+   *          considered equal to non-null positions for which
+   *          Annotation.isWhitespace() is true.
    */
-  private static boolean equalss(AlignmentAnnotation annot_or,
-          AlignmentAnnotation annot_new)
+  private static void assertEqualSecondaryStructure(String message,
+          AlignmentAnnotation annot_or, AlignmentAnnotation annot_new,
+          boolean allowNullEqivalence)
   {
+    // TODO: test to cover this assert behaves correctly for all allowed
+    // variations of secondary structure annotation row equivalence
     if (annot_or.annotations.length != annot_new.annotations.length)
     {
-      System.err.println("Different lengths for annotation row elements: "
+      fail("Different lengths for annotation row elements: "
               + annot_or.annotations.length + "!="
               + annot_new.annotations.length);
-      return false;
     }
+    boolean isRna = annot_or.isRNA();
+    assertTrue("Expected " + (isRna ? " valid RNA " : " no RNA ")
+            + " secondary structure in the row.",
+            isRna == annot_new.isRNA());
     for (int i = 0; i < annot_or.annotations.length; i++)
     {
       Annotation an_or = annot_or.annotations[i], an_new = annot_new.annotations[i];
       if (an_or != null && an_new != null)
       {
-        if (!an_or.displayCharacter.trim().equals(
-                an_new.displayCharacter.trim())
-                || !("" + an_or.secondaryStructure).trim().equals(
-                        ("" + an_new.secondaryStructure).trim())
-                || (an_or.description != an_new.description && !((an_or.description == null && an_new.description
-                        .trim().length() == 0)
-                        || (an_new.description == null && an_or.description
-                                .trim().length() == 0) || an_or.description
-                        .trim().equals(an_new.description.trim()))))
+
+        if (isRna)
         {
-          System.err.println("Annotation Element Mismatch\nElement " + i
-                  + " in original: " + annot_or.annotations[i].toString()
-                  + "\nElement " + i + " in new: "
-                  + annot_new.annotations[i].toString());
-          return false;
+          if (an_or.secondaryStructure != an_new.secondaryStructure
+                  || ((Float.isNaN(an_or.value) != Float
+                          .isNaN(an_new.value)) || an_or.value != an_new.value))
+          {
+            fail("Different RNA secondary structure at column " + i
+                    + " expected: [" + annot_or.annotations[i].toString()
+                    + "] but got: [" + annot_new.annotations[i].toString()
+                    + "]");
+          }
+        }
+        else
+        {
+          // not RNA secondary structure, so expect all elements to match...
+          if ((an_or.isWhitespace() != an_new.isWhitespace())
+                  || !an_or.displayCharacter.trim().equals(
+                  an_new.displayCharacter.trim())
+                  || !("" + an_or.secondaryStructure).trim().equals(
+                          ("" + an_new.secondaryStructure).trim())
+                  || (an_or.description != an_new.description && !((an_or.description == null && an_new.description
+                          .trim().length() == 0)
+                          || (an_new.description == null && an_or.description
+                                  .trim().length() == 0) || an_or.description
+                          .trim().equals(an_new.description.trim())))
+                  || !((Float.isNaN(an_or.value) && Float
+                          .isNaN(an_new.value)) || an_or.value == an_new.value))
+          {
+            fail("Annotation Element Mismatch\nElement " + i
+                    + " in original: " + annot_or.annotations[i].toString()
+                    + "\nElement " + i + " in new: "
+                    + annot_new.annotations[i].toString());
+          }
         }
       }
       else if (annot_or.annotations[i] == null
@@ -375,19 +456,202 @@ public class StockholmFileTest
       }
       else
       {
-        System.err.println("Annotation Element Mismatch\nElement "
-                + i
-                + " in original: "
-                + (annot_or.annotations[i] == null ? "is null"
-                        : annot_or.annotations[i].toString())
-                + "\nElement "
-                + i
-                + " in new: "
-                + (annot_new.annotations[i] == null ? "is null"
-                        : annot_new.annotations[i].toString()));
-        return false;
+        if (allowNullEqivalence)
+        {
+          if (an_or != null && an_or.isWhitespace())
+
+          {
+            continue;
+          }
+          if (an_new != null && an_new.isWhitespace())
+          {
+            continue;
+          }
+        }
+        // need also to test for null in one, non-SS annotation in other...
+        fail("Annotation Element Mismatch\nElement " + i + " in original: "
+                + (an_or == null ? "is null" : an_or.toString())
+                + "\nElement " + i + " in new: "
+                + (an_new == null ? "is null" : an_new.toString()));
+      }
+    }
+  }
+
+  /**
+   * @see assertEqualSecondaryStructure - test if two secondary structure
+   *      annotations are not equal
+   * @param message
+   * @param an_orig
+   * @param an_new
+   * @param allowNullEquivalence
+   */
+  public static void assertNotEqualSecondaryStructure(String message,
+          AlignmentAnnotation an_orig, AlignmentAnnotation an_new,
+          boolean allowNullEquivalence)
+  {
+    boolean thrown = false;
+    try
+    {
+      assertEqualSecondaryStructure("", an_orig, an_new,
+              allowNullEquivalence);
+    } catch (AssertionError af)
+    {
+      thrown = true;
+    }
+    if (!thrown)
+    {
+      fail("Expected difference for [" + an_orig + "] and [" + an_new + "]");
+    }
+  }
+  private AlignmentAnnotation makeAnnot(Annotation ae)
+  {
+    return new AlignmentAnnotation("label", "description", new Annotation[]
+    { ae });
+  }
+
+  @Test(groups={"Functional"})
+  public void testAnnotationEquivalence()
+  {
+    AlignmentAnnotation one = makeAnnot(new Annotation("", "", ' ', 1));
+    AlignmentAnnotation anotherOne = makeAnnot(new Annotation("", "", ' ',
+            1));
+    AlignmentAnnotation sheet = makeAnnot(new Annotation("","",'E',0f));
+    AlignmentAnnotation anotherSheet = makeAnnot(new Annotation("","",'E',0f)); 
+    AlignmentAnnotation sheetWithLabel = makeAnnot(new Annotation("1", "",
+            'E', 0f));
+    AlignmentAnnotation anotherSheetWithLabel = makeAnnot(new Annotation(
+            "1", "", 'E', 0f));
+    AlignmentAnnotation rnaNoDC = makeAnnot(new Annotation("","",'<',0f));
+    AlignmentAnnotation anotherRnaNoDC = makeAnnot(new Annotation("","",'<',0f));
+    AlignmentAnnotation rnaWithDC = makeAnnot(new Annotation("B", "", '<',
+            0f));
+    AlignmentAnnotation anotherRnaWithDC = makeAnnot(new Annotation("B",
+            "", '<', 0f));
+    
+    // check self equivalence
+    for (boolean allowNull : new boolean[] { true, false })
+    {
+      assertEqualSecondaryStructure("Should be equal", one, anotherOne,
+              allowNull);
+      assertEqualSecondaryStructure("Should be equal", sheet, anotherSheet,
+              allowNull);
+      assertEqualSecondaryStructure("Should be equal", sheetWithLabel,
+              anotherSheetWithLabel, allowNull);
+      assertEqualSecondaryStructure("Should be equal", rnaNoDC,
+              anotherRnaNoDC, allowNull);
+      assertEqualSecondaryStructure("Should be equal", rnaWithDC,
+              anotherRnaWithDC, allowNull);
+      // display character doesn't matter for RNA structure (for 2.10.2)
+      assertEqualSecondaryStructure("Should be equal", rnaWithDC, rnaNoDC,
+              allowNull);
+      assertEqualSecondaryStructure("Should be equal", rnaNoDC, rnaWithDC,
+              allowNull);
+    }
+
+    // verify others are different
+    List<AlignmentAnnotation> aaSet = Arrays.asList(one, sheet,
+            sheetWithLabel, rnaWithDC);
+    for (int p = 0; p < aaSet.size(); p++)
+    {
+      for (int q = 0; q < aaSet.size(); q++)
+      {
+        if (p != q)
+        {
+          assertNotEqualSecondaryStructure("Should be different",
+                    aaSet.get(p), aaSet.get(q), false);
+        }
+        else
+        {
+          assertEqualSecondaryStructure("Should be same", aaSet.get(p),
+                  aaSet.get(q), false);
+          assertEqualSecondaryStructure("Should be same", aaSet.get(p),
+                  aaSet.get(q), true);
+          assertNotEqualSecondaryStructure(
+                  "Should be different to empty anot", aaSet.get(p),
+                  makeAnnot(Annotation.EMPTY_ANNOTATION), false);
+          assertNotEqualSecondaryStructure(
+                  "Should be different to empty annot",
+                  makeAnnot(Annotation.EMPTY_ANNOTATION), aaSet.get(q),
+                  true);
+          assertNotEqualSecondaryStructure("Should be different to null",
+                  aaSet.get(p), makeAnnot(null), false);
+          assertNotEqualSecondaryStructure("Should be different to null",
+                  makeAnnot(null), aaSet.get(q), true);
+        }
       }
     }
-    return true;
+
+    // test null
+
+  }
+
+  String aliFile = ">Dm\nAAACCCUUUUACACACGGGAAAGGG";
+  String annFile = "JALVIEW_ANNOTATION\n# Created: Thu May 04 11:16:52 BST 2017\n\n"
+          + "SEQUENCE_REF\tDm\nNO_GRAPH\tsecondary structure\tsecondary structure\t"
+          + "(|(|(|(|, .|, .|, .|, .|)|)|)|)|\t0.0\nROWPROPERTIES\t"
+          + "secondary structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false";
+
+  String annFileCurlyWuss = "JALVIEW_ANNOTATION\n# Created: Thu May 04 11:16:52 BST 2017\n\n"
+          + "SEQUENCE_REF\tDm\nNO_GRAPH\tsecondary structure\tsecondary structure\t"
+          + "(|(|(|(||{|{||{|{||)|)|)|)||}|}|}|}|\t0.0\nROWPROPERTIES\t"
+          + "secondary structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false";
+  String annFileFullWuss = "JALVIEW_ANNOTATION\n# Created: Thu May 04 11:16:52 BST 2017\n\n"
+          + "SEQUENCE_REF\tDm\nNO_GRAPH\tsecondary structure\tsecondary structure\t"
+          + "(|(|(|(||{|{||[|[||)|)|)|)||}|}|]|]|\t0.0\nROWPROPERTIES\t"
+          + "secondary structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false";
+
+  @Test(groups = { "Functional" })
+  public void secondaryStructureForRNASequence() throws Exception
+  {
+    roundTripSSForRNA(aliFile, annFile);
+  }
+
+  @Test(groups = { "Functional" })
+  public void curlyWUSSsecondaryStructureForRNASequence() throws Exception
+  {
+    roundTripSSForRNA(aliFile, annFileCurlyWuss);
+  }
+
+  @Test(groups = { "Functional" })
+  public void fullWUSSsecondaryStructureForRNASequence() throws Exception
+  {
+    roundTripSSForRNA(aliFile, annFileFullWuss);
+  }
+
+  @Test(groups = { "Functional" })
+  public void detectWussBrackets()
+  {
+    for (char ch : new char[] { '{', '}', '[', ']', '(', ')', '<', '>' })
+    {
+      Assert.assertTrue(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
+              "Didn't recognise " + ch + " as a WUSS bracket");
+    }
+    for (char ch : new char[] { '@', '!', 'V', 'Q', '*', ' ', '-', '.' })
+    {
+      Assert.assertFalse(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
+              "Shouldn't recognise " + ch + " as a WUSS bracket");
+    }
+  }
+  private static void roundTripSSForRNA(String aliFile, String annFile)
+          throws Exception
+  {
+    AlignmentI al = new AppletFormatAdapter().readFile(aliFile,
+            DataSourceType.PASTE, jalview.io.FileFormat.Fasta);
+    AnnotationFile aaf = new AnnotationFile();
+    aaf.readAnnotationFile(al, annFile, DataSourceType.PASTE);
+    al.getAlignmentAnnotation()[0].visible = true;
+
+    // TODO: create a better 'save as <format>' pattern
+    StockholmFile sf = new StockholmFile(al);
+
+    String stockholmFile = sf.print(al.getSequencesArray(), true);
+
+    AlignmentI newAl = new AppletFormatAdapter().readFile(stockholmFile,
+            DataSourceType.PASTE, jalview.io.FileFormat.Stockholm);
+    // AlignmentUtils.showOrHideSequenceAnnotations(newAl.getViewport()
+    // .getAlignment(), Arrays.asList("Secondary Structure"), newAl
+    // .getViewport().getAlignment().getSequences(), true, true);
+    testAlignmentEquivalence(al, newAl, true, true, true);
+
   }
 }
diff --git a/test/jalview/io/cache/AppCacheTest.java b/test/jalview/io/cache/AppCacheTest.java
new file mode 100644 (file)
index 0000000..5638028
--- /dev/null
@@ -0,0 +1,61 @@
+package jalview.io.cache;
+
+import java.util.LinkedHashSet;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class AppCacheTest
+{
+  private AppCache appCache;
+
+  private static final String TEST_CACHE_KEY = "CACHE.UNIT_TEST";
+
+  private static final String TEST_FAKE_CACHE_KEY = "CACHE.UNIT_TEST_FAKE";
+
+  @BeforeClass(alwaysRun = true)
+  public void setUpCache()
+  {
+    appCache = AppCache.getInstance();
+  }
+
+  public void generateTestCacheItems()
+  {
+    LinkedHashSet<String> testCacheItems = new LinkedHashSet<String>();
+    for (int x = 0; x < 10; x++)
+    {
+      testCacheItems.add("TestCache" + x);
+    }
+    appCache.putCache(TEST_CACHE_KEY, testCacheItems);
+    appCache.persistCache(TEST_CACHE_KEY);
+  }
+
+  @Test(groups = { "Functional" })
+  public void appCacheTest()
+  {
+    LinkedHashSet<String> cacheItems = appCache
+            .getAllCachedItemsFor(TEST_FAKE_CACHE_KEY);
+    Assert.assertEquals(cacheItems.size(), 0);
+    generateTestCacheItems();
+    cacheItems = appCache.getAllCachedItemsFor(TEST_CACHE_KEY);
+    Assert.assertEquals(cacheItems.size(), 10);
+    appCache.deleteCacheItems(TEST_CACHE_KEY);
+    cacheItems = appCache.getAllCachedItemsFor(TEST_CACHE_KEY);
+    Assert.assertEquals(cacheItems.size(), 0);
+  }
+
+  @Test(groups = { "Functional" })
+  public void appCacheLimitTest()
+  {
+    String limit = appCache.getCacheLimit(TEST_CACHE_KEY);
+    Assert.assertEquals(limit, "99");
+    limit = String.valueOf(appCache.updateCacheLimit(TEST_CACHE_KEY, 20));
+    Assert.assertEquals(limit, "20");
+    limit = appCache.getCacheLimit(TEST_CACHE_KEY);
+    Assert.assertEquals(limit, "20");
+    appCache.updateCacheLimit(TEST_CACHE_KEY, 99);
+  }
+
+
+}
diff --git a/test/jalview/io/cache/JvCacheableInputBoxTest.java b/test/jalview/io/cache/JvCacheableInputBoxTest.java
new file mode 100644 (file)
index 0000000..dfd7973
--- /dev/null
@@ -0,0 +1,70 @@
+package jalview.io.cache;
+
+import java.util.LinkedHashSet;
+
+import org.junit.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class JvCacheableInputBoxTest
+{
+
+  private AppCache appCache;
+
+  private static final String TEST_CACHE_KEY = "CACHE.UNIT_TEST";
+
+  private JvCacheableInputBox<String> cacheBox = new JvCacheableInputBox<String>(
+          TEST_CACHE_KEY);
+
+  @BeforeClass(alwaysRun = true)
+  private void setUpCache()
+  {
+    appCache = AppCache.getInstance();
+  }
+
+  @Test(groups = { "Functional" })
+  public void getUserInputTest()
+  {
+    String userInput = cacheBox.getUserInput();
+    Assert.assertEquals("", userInput);
+
+    String testInput = "TestInput";
+    cacheBox.addItem(testInput);
+    cacheBox.setSelectedItem(testInput);
+
+    try
+    {
+      // This 1ms delay is essential to prevent the
+      // assertion below from executing before
+      // swing thread finishes updating the combo-box
+      Thread.sleep(100);
+    } catch (InterruptedException e)
+    {
+      e.printStackTrace();
+    }
+    userInput = cacheBox.getUserInput();
+    Assert.assertEquals(testInput, userInput);
+  }
+
+  @Test(groups = { "Functional" })
+  public void updateCacheTest()
+  {
+    String testInput = "TestInput";
+    cacheBox.addItem(testInput);
+    cacheBox.setSelectedItem(testInput);
+    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);
+    } catch (InterruptedException e)
+    {
+      e.printStackTrace();
+    }
+    LinkedHashSet<String> foundCache = appCache
+            .getAllCachedItemsFor(TEST_CACHE_KEY);
+    Assert.assertTrue(foundCache.contains(testInput));
+  }
+}
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();
 
index 95da22e..8ede59c 100644 (file)
@@ -31,6 +31,7 @@ JAVA_CONSOLE_SCREEN_HEIGHT=162
 PIR_MODELLER=false
 GAP_SYMBOL=-
 SHOW_QUALITY=true
+SHOW_OCCUPANCY=true
 SHOW_GROUP_CONSERVATION=false
 SHOW_JWS2_SERVICES=true
 SHOW_NPFEATS_TOOLTIP=true
index 795b2fa..97ded5a 100644 (file)
@@ -1,23 +1,30 @@
 package jalview.math;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.fail;
 
 import java.util.Arrays;
+import java.util.Random;
 
 import org.testng.annotations.Test;
+import org.testng.internal.junit.ArrayAsserts;
 
 public class MatrixTest
 {
+  final static double DELTA = 0.000001d;
+
   @Test(groups = "Timing")
   public void testPreMultiply_timing()
   {
-    int rows = 500;
-    int cols = 1000;
+    int rows = 50; // increase to stress test timing
+    int cols = 100;
     double[][] d1 = new double[rows][cols];
     double[][] d2 = new double[cols][rows];
-    Matrix m1 = new Matrix(d1, rows, cols);
-    Matrix m2 = new Matrix(d2, cols, rows);
+    Matrix m1 = new Matrix(d1);
+    Matrix m2 = new Matrix(d2);
     long start = System.currentTimeMillis();
     m1.preMultiply(m2);
     long elapsed = System.currentTimeMillis() - start;
@@ -28,27 +35,27 @@ public class MatrixTest
   @Test(groups = "Functional")
   public void testPreMultiply()
   {
-    Matrix m1 = new Matrix(new double[][] { { 2, 3, 4 } }, 1, 3); // 1x3
-    Matrix m2 = new Matrix(new double[][] { { 5 }, { 6 }, { 7 } }, 3, 1); // 3x1
+    Matrix m1 = new Matrix(new double[][] { { 2, 3, 4 } }); // 1x3
+    Matrix m2 = new Matrix(new double[][] { { 5 }, { 6 }, { 7 } }); // 3x1
 
     /*
      * 1x3 times 3x1 is 1x1
      * 2x5 + 3x6 + 4*7 =  56
      */
-    Matrix m3 = m2.preMultiply(m1);
-    assertEquals(m3.rows, 1);
-    assertEquals(m3.cols, 1);
-    assertEquals(m3.value[0][0], 56d);
+    MatrixI m3 = m2.preMultiply(m1);
+    assertEquals(m3.height(), 1);
+    assertEquals(m3.width(), 1);
+    assertEquals(m3.getValue(0, 0), 56d);
 
     /*
      * 3x1 times 1x3 is 3x3
      */
     m3 = m1.preMultiply(m2);
-    assertEquals(m3.rows, 3);
-    assertEquals(m3.cols, 3);
-    assertEquals(Arrays.toString(m3.value[0]), "[10.0, 15.0, 20.0]");
-    assertEquals(Arrays.toString(m3.value[1]), "[12.0, 18.0, 24.0]");
-    assertEquals(Arrays.toString(m3.value[2]), "[14.0, 21.0, 28.0]");
+    assertEquals(m3.height(), 3);
+    assertEquals(m3.width(), 3);
+    assertEquals(Arrays.toString(m3.getRow(0)), "[10.0, 15.0, 20.0]");
+    assertEquals(Arrays.toString(m3.getRow(1)), "[12.0, 18.0, 24.0]");
+    assertEquals(Arrays.toString(m3.getRow(2)), "[14.0, 21.0, 28.0]");
   }
 
   @Test(
@@ -56,8 +63,7 @@ public class MatrixTest
     expectedExceptions = { IllegalArgumentException.class })
   public void testPreMultiply_tooManyColumns()
   {
-    Matrix m1 = new Matrix(new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }, 2,
-            3); // 2x3
+    Matrix m1 = new Matrix(new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }); // 2x3
 
     /*
      * 2x3 times 2x3 invalid operation - 
@@ -72,8 +78,7 @@ public class MatrixTest
     expectedExceptions = { IllegalArgumentException.class })
   public void testPreMultiply_tooFewColumns()
   {
-    Matrix m1 = new Matrix(new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }, 2,
-            3); // 2x3
+    Matrix m1 = new Matrix(new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }); // 2x3
 
     /*
      * 3x2 times 3x2 invalid operation - 
@@ -85,7 +90,18 @@ public class MatrixTest
   
   
   private boolean matrixEquals(Matrix m1, Matrix m2) {
-    return Arrays.deepEquals(m1.value, m2.value);
+    if (m1.width() != m2.width() || m1.height() != m2.height())
+    {
+      return false;
+    }
+    for (int i = 0; i < m1.height(); i++)
+    {
+      if (!Arrays.equals(m1.getRow(i), m2.getRow(i)))
+      {
+        return false;
+      }
+    }
+    return true;
   }
 
   @Test(groups = "Functional")
@@ -99,37 +115,36 @@ public class MatrixTest
      * (3020 30200)
      * (5040 50400)
      */
-    Matrix m1 = new Matrix(new double[][] { { 2, 3 }, { 4, 5 } }, 2, 2);
-    Matrix m2 = new Matrix(new double[][] { { 10, 100 }, { 1000, 10000 } },
-            2, 2);
-    Matrix m3 = m1.postMultiply(m2);
-    assertEquals(Arrays.toString(m3.value[0]), "[3020.0, 30200.0]");
-    assertEquals(Arrays.toString(m3.value[1]), "[5040.0, 50400.0]");
+    MatrixI m1 = new Matrix(new double[][] { { 2, 3 }, { 4, 5 } });
+    MatrixI m2 = new Matrix(new double[][] { { 10, 100 }, { 1000, 10000 } });
+    MatrixI m3 = m1.postMultiply(m2);
+    assertEquals(Arrays.toString(m3.getRow(0)), "[3020.0, 30200.0]");
+    assertEquals(Arrays.toString(m3.getRow(1)), "[5040.0, 50400.0]");
 
     /*
      * also check m2.preMultiply(m1) - should be same as m1.postMultiply(m2) 
      */
     m3 = m2.preMultiply(m1);
-    assertEquals(Arrays.toString(m3.value[0]), "[3020.0, 30200.0]");
-    assertEquals(Arrays.toString(m3.value[1]), "[5040.0, 50400.0]");
+    assertEquals(Arrays.toString(m3.getRow(0)), "[3020.0, 30200.0]");
+    assertEquals(Arrays.toString(m3.getRow(1)), "[5040.0, 50400.0]");
 
     /*
      * m1 has more rows than columns
      * (2).(10 100 1000) = (20 200 2000)
      * (3)                 (30 300 3000)
      */
-    m1 = new Matrix(new double[][] { { 2 }, { 3 } }, 2, 1);
-    m2 = new Matrix(new double[][] { { 10, 100, 1000 } }, 1, 3);
+    m1 = new Matrix(new double[][] { { 2 }, { 3 } });
+    m2 = new Matrix(new double[][] { { 10, 100, 1000 } });
     m3 = m1.postMultiply(m2);
-    assertEquals(m3.rows, 2);
-    assertEquals(m3.cols, 3);
-    assertEquals(Arrays.toString(m3.value[0]), "[20.0, 200.0, 2000.0]");
-    assertEquals(Arrays.toString(m3.value[1]), "[30.0, 300.0, 3000.0]");
+    assertEquals(m3.height(), 2);
+    assertEquals(m3.width(), 3);
+    assertEquals(Arrays.toString(m3.getRow(0)), "[20.0, 200.0, 2000.0]");
+    assertEquals(Arrays.toString(m3.getRow(1)), "[30.0, 300.0, 3000.0]");
     m3 = m2.preMultiply(m1);
-    assertEquals(m3.rows, 2);
-    assertEquals(m3.cols, 3);
-    assertEquals(Arrays.toString(m3.value[0]), "[20.0, 200.0, 2000.0]");
-    assertEquals(Arrays.toString(m3.value[1]), "[30.0, 300.0, 3000.0]");
+    assertEquals(m3.height(), 2);
+    assertEquals(m3.width(), 3);
+    assertEquals(Arrays.toString(m3.getRow(0)), "[20.0, 200.0, 2000.0]");
+    assertEquals(Arrays.toString(m3.getRow(1)), "[30.0, 300.0, 3000.0]");
 
     /*
      * m1 has more columns than rows
@@ -139,22 +154,43 @@ public class MatrixTest
      * [0, 0] = 2*5 + 3*6 + 4*7 = 56
      * [0, 1] = 2*4 + 3*3 + 4*2 = 25  
      */
-    m1 = new Matrix(new double[][] { { 2, 3, 4 } }, 1, 3);
-    m2 = new Matrix(new double[][] { { 5, 4 }, { 6, 3 }, { 7, 2 } }, 3, 2);
+    m1 = new Matrix(new double[][] { { 2, 3, 4 } });
+    m2 = new Matrix(new double[][] { { 5, 4 }, { 6, 3 }, { 7, 2 } });
     m3 = m1.postMultiply(m2);
-    assertEquals(m3.rows, 1);
-    assertEquals(m3.cols, 2);
-    assertEquals(m3.value[0][0], 56d);
-    assertEquals(m3.value[0][1], 25d);
+    assertEquals(m3.height(), 1);
+    assertEquals(m3.width(), 2);
+    assertEquals(m3.getRow(0)[0], 56d);
+    assertEquals(m3.getRow(0)[1], 25d);
 
     /*
      * and check premultiply equivalent
      */
     m3 = m2.preMultiply(m1);
-    assertEquals(m3.rows, 1);
-    assertEquals(m3.cols, 2);
-    assertEquals(m3.value[0][0], 56d);
-    assertEquals(m3.value[0][1], 25d);
+    assertEquals(m3.height(), 1);
+    assertEquals(m3.width(), 2);
+    assertEquals(m3.getRow(0)[0], 56d);
+    assertEquals(m3.getRow(0)[1], 25d);
+  }
+
+  @Test(groups = "Functional")
+  public void testCopy()
+  {
+    Random r = new Random();
+    int rows = 5;
+    int cols = 11;
+    double[][] in = new double[rows][cols];
+
+    for (int i = 0; i < rows; i++)
+    {
+      for (int j = 0; j < cols; j++)
+      {
+        in[i][j] = r.nextDouble();
+      }
+    }
+    Matrix m1 = new Matrix(in);
+    Matrix m2 = (Matrix) m1.copy();
+    assertNotSame(m1, m2);
+    assertTrue(matrixEquals(m1, m2));
   }
 
   /**
@@ -175,18 +211,18 @@ public class MatrixTest
       }
     }
   
-    Matrix origmat = new Matrix(in, n, n);
+    Matrix origmat = new Matrix(in);
   
     // System.out.println(" --- Original matrix ---- ");
     // / origmat.print(System.out);
     // System.out.println();
     // System.out.println(" --- transpose matrix ---- ");
-    Matrix trans = origmat.transpose();
+    MatrixI trans = origmat.transpose();
   
     // trans.print(System.out);
     // System.out.println();
     // System.out.println(" --- OrigT * Orig ---- ");
-    Matrix symm = trans.postMultiply(origmat);
+    MatrixI symm = trans.postMultiply(origmat);
   
     // symm.print(System.out);
     // System.out.println();
@@ -236,4 +272,263 @@ public class MatrixTest
     // }
     // System.out.println();
   }
+
+  @Test(groups = "Timing")
+  public void testSign()
+  {
+    assertEquals(Matrix.sign(-1, -2), -1d);
+    assertEquals(Matrix.sign(-1, 2), 1d);
+    assertEquals(Matrix.sign(-1, 0), 1d);
+    assertEquals(Matrix.sign(1, -2), -1d);
+    assertEquals(Matrix.sign(1, 2), 1d);
+    assertEquals(Matrix.sign(1, 0), 1d);
+  }
+
+  /**
+   * Helper method to make values for a sparse, pseudo-random symmetric matrix
+   * 
+   * @param rows
+   * @param cols
+   * @param occupancy
+   *          one in 'occupancy' entries will be non-zero
+   * @return
+   */
+  public double[][] getSparseValues(int rows, int cols, int occupancy)
+  {
+    Random r = new Random(1729);
+
+    /*
+     * generate whole number values between -12 and +12
+     * (to mimic score matrices used in Jalview)
+     */
+    double[][] d = new double[rows][cols];
+    int m = 0;
+    for (int i = 0; i < rows; i++)
+    {
+      if (++m % occupancy == 0)
+      {
+        d[i][i] = r.nextInt() % 13; // diagonal
+      }
+      for (int j = 0; j < i; j++)
+      {
+        if (++m % occupancy == 0)
+        {
+          d[i][j] = r.nextInt() % 13;
+          d[j][i] = d[i][j];
+        }
+      }
+    }
+    return d;
+  
+  }
+
+  /**
+   * Verify that the results of method tred() are the same if the calculation is
+   * redone
+   */
+  @Test(groups = "Functional")
+  public void testTred_reproducible()
+  {
+    /*
+     * make a pseudo-random symmetric matrix as required for tred/tqli
+     */
+    int rows = 10;
+    int cols = rows;
+    double[][] d = getSparseValues(rows, cols, 3);
+  
+    /*
+     * make a copy of the values so m1, m2 are not
+     * sharing arrays!
+     */
+    double[][] d1 = new double[rows][cols];
+    for (int row = 0; row < rows; row++)
+    {
+      for (int col = 0; col < cols; col++)
+      {
+        d1[row][col] = d[row][col];
+      }
+    }
+    Matrix m1 = new Matrix(d);
+    Matrix m2 = new Matrix(d1);
+    assertMatricesMatch(m1, m2); // sanity check
+    m1.tred();
+    m2.tred();
+    assertMatricesMatch(m1, m2);
+  }
+
+  private void assertMatricesMatch(MatrixI m1, MatrixI m2)
+  {
+    if (m1.height() != m2.height())
+    {
+      fail("height mismatch");
+    }
+    if (m1.width() != m2.width())
+    {
+      fail("width mismatch");
+    }
+    for (int row = 0; row < m1.height(); row++)
+    {
+      for (int col = 0; col < m1.width(); col++)
+      {
+        double v2 = m2.getValue(row, col);
+        double v1 = m1.getValue(row, col);
+        if (Math.abs(v1 - v2) > DELTA)
+        {
+          fail(String.format("At [%d, %d] %f != %f", row, col, v1, v2));
+        }
+      }
+    }
+    ArrayAsserts.assertArrayEquals(m1.getD(), m2.getD(), 0.00001d);
+    ArrayAsserts.assertArrayEquals(m1.getE(), m2.getE(), 0.00001d);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindMinMax()
+  {
+    /*
+     * empty matrix case
+     */
+    Matrix m = new Matrix(new double[][] { {} });
+    assertNull(m.findMinMax());
+
+    /*
+     * normal case
+     */
+    double[][] vals = new double[2][];
+    vals[0] = new double[] {7d, 1d, -2.3d};
+    vals[1] = new double[] {-12d, 94.3d, -102.34d};
+    m = new Matrix(vals);
+    double[] minMax = m.findMinMax();
+    assertEquals(minMax[0], -102.34d);
+    assertEquals(minMax[1], 94.3d);
+  }
+
+  @Test(groups = { "Functional", "Timing" })
+  public void testFindMinMax_timing()
+  {
+    Random r = new Random();
+    int size = 1000; // increase to stress test timing
+    double[][] vals = new double[size][size];
+    double max = -Double.MAX_VALUE;
+    double min = Double.MAX_VALUE;
+    for (int i = 0; i < size; i++)
+    {
+      vals[i] = new double[size];
+      for (int j = 0; j < size; j++)
+      {
+        // use nextLong rather than nextDouble to include negative values
+        double d = r.nextLong();
+        if (d > max)
+        {
+          max = d;
+        }
+        if (d < min)
+        {
+          min = d;
+        }
+        vals[i][j] = d;
+      }
+    }
+    Matrix m = new Matrix(vals);
+    long now = System.currentTimeMillis();
+    double[] minMax = m.findMinMax();
+    System.out.println(String.format("findMinMax for %d x %d took %dms",
+            size, size, (System.currentTimeMillis() - now)));
+    assertEquals(minMax[0], min);
+    assertEquals(minMax[1], max);
+  }
+
+  /**
+   * Test range reversal with maximum value becoming zero
+   */
+  @Test(groups = "Functional")
+  public void testReverseRange_maxToZero()
+  {
+    Matrix m1 = new Matrix(
+            new double[][] { { 2, 3.5, 4 }, { -3.4, 4, 15 } });
+
+    /*
+     * subtract all from max: range -3.4 to 15 becomes 18.4 to 0
+     */
+    m1.reverseRange(true);
+    assertEquals(m1.getValue(0, 0), 13d, DELTA);
+    assertEquals(m1.getValue(0, 1), 11.5d, DELTA);
+    assertEquals(m1.getValue(0, 2), 11d, DELTA);
+    assertEquals(m1.getValue(1, 0), 18.4d, DELTA);
+    assertEquals(m1.getValue(1, 1), 11d, DELTA);
+    assertEquals(m1.getValue(1, 2), 0d, DELTA);
+
+    /*
+     * repeat operation - range is now 0 to 18.4
+     */
+    m1.reverseRange(true);
+    assertEquals(m1.getValue(0, 0), 5.4d, DELTA);
+    assertEquals(m1.getValue(0, 1), 6.9d, DELTA);
+    assertEquals(m1.getValue(0, 2), 7.4d, DELTA);
+    assertEquals(m1.getValue(1, 0), 0d, DELTA);
+    assertEquals(m1.getValue(1, 1), 7.4d, DELTA);
+    assertEquals(m1.getValue(1, 2), 18.4d, DELTA);
+  }
+
+  /**
+   * Test range reversal with minimum and maximum values swapped
+   */
+  @Test(groups = "Functional")
+  public void testReverseRange_swapMinMax()
+  {
+    Matrix m1 = new Matrix(
+            new double[][] { { 2, 3.5, 4 }, { -3.4, 4, 15 } });
+  
+    /*
+     * swap all values in min-max range
+     * = subtract from (min + max = 11.6) 
+     * range -3.4 to 15 becomes 18.4 to -3.4
+     */
+    m1.reverseRange(false);
+    assertEquals(m1.getValue(0, 0), 9.6d, DELTA);
+    assertEquals(m1.getValue(0, 1), 8.1d, DELTA);
+    assertEquals(m1.getValue(0, 2), 7.6d, DELTA);
+    assertEquals(m1.getValue(1, 0), 15d, DELTA);
+    assertEquals(m1.getValue(1, 1), 7.6d, DELTA);
+    assertEquals(m1.getValue(1, 2), -3.4d, DELTA);
+  
+    /*
+     * repeat operation - original values restored
+     */
+    m1.reverseRange(false);
+    assertEquals(m1.getValue(0, 0), 2d, DELTA);
+    assertEquals(m1.getValue(0, 1), 3.5d, DELTA);
+    assertEquals(m1.getValue(0, 2), 4d, DELTA);
+    assertEquals(m1.getValue(1, 0), -3.4d, DELTA);
+    assertEquals(m1.getValue(1, 1), 4d, DELTA);
+    assertEquals(m1.getValue(1, 2), 15d, DELTA);
+  }
+
+  @Test(groups = "Functional")
+  public void testMultiply()
+  {
+    Matrix m = new Matrix(new double[][] { { 2, 3.5, 4 }, { -3.4, 4, 15 } });
+    m.multiply(2d);
+    assertEquals(m.getValue(0, 0), 4d, DELTA);
+    assertEquals(m.getValue(0, 1), 7d, DELTA);
+    assertEquals(m.getValue(0, 2), 8d, DELTA);
+    assertEquals(m.getValue(1, 0), -6.8d, DELTA);
+    assertEquals(m.getValue(1, 1), 8d, DELTA);
+    assertEquals(m.getValue(1, 2), 30d, DELTA);
+  }
+
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    double[][] values = new double[][] { { 1, 2, 3 }, { 4, 5, 6 } };
+    Matrix m = new Matrix(values);
+    assertEquals(m.getValue(0, 0), 1d, DELTA);
+
+    /*
+     * verify the matrix has a copy of the original array
+     */
+    assertNotSame(values[0], m.getRow(0));
+    values[0][0] = -1d;
+    assertEquals(m.getValue(0, 0), 1d, DELTA); // unchanged
+  }
 }
diff --git a/test/jalview/math/SparseMatrixTest.java b/test/jalview/math/SparseMatrixTest.java
new file mode 100644 (file)
index 0000000..3c2ccaa
--- /dev/null
@@ -0,0 +1,416 @@
+package jalview.math;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Random;
+
+import org.testng.annotations.Test;
+import org.testng.internal.junit.ArrayAsserts;
+
+public class SparseMatrixTest
+{
+  final static double DELTA = 0.0001d;
+
+  Random r = new Random(1729);
+
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    MatrixI m1 = new SparseMatrix(
+            new double[][] { { 2, 0, 4 }, { 0, 6, 0 } });
+    assertEquals(m1.getValue(0, 0), 2d);
+    assertEquals(m1.getValue(0, 1), 0d);
+    assertEquals(m1.getValue(0, 2), 4d);
+    assertEquals(m1.getValue(1, 0), 0d);
+    assertEquals(m1.getValue(1, 1), 6d);
+    assertEquals(m1.getValue(1, 2), 0d);
+  }
+
+  @Test(groups = "Functional")
+  public void testTranspose()
+  {
+    MatrixI m1 = new SparseMatrix(
+            new double[][] { { 2, 0, 4 }, { 5, 6, 0 } });
+    MatrixI m2 = m1.transpose();
+    assertTrue(m2 instanceof SparseMatrix);
+    assertEquals(m2.height(), 3);
+    assertEquals(m2.width(), 2);
+    assertEquals(m2.getValue(0, 0), 2d);
+    assertEquals(m2.getValue(0, 1), 5d);
+    assertEquals(m2.getValue(1, 0), 0d);
+    assertEquals(m2.getValue(1, 1), 6d);
+    assertEquals(m2.getValue(2, 0), 4d);
+    assertEquals(m2.getValue(2, 1), 0d);
+  }
+  @Test(groups = "Functional")
+  public void testPreMultiply()
+  {
+    MatrixI m1 = new SparseMatrix(new double[][] { { 2, 3, 4 } }); // 1x3
+    MatrixI m2 = new SparseMatrix(new double[][] { { 5 }, { 6 }, { 7 } }); // 3x1
+
+    /*
+     * 1x3 times 3x1 is 1x1
+     * 2x5 + 3x6 + 4*7 =  56
+     */
+    MatrixI m3 = m2.preMultiply(m1);
+    assertFalse(m3 instanceof SparseMatrix);
+    assertEquals(m3.height(), 1);
+    assertEquals(m3.width(), 1);
+    assertEquals(m3.getValue(0, 0), 56d);
+
+    /*
+     * 3x1 times 1x3 is 3x3
+     */
+    m3 = m1.preMultiply(m2);
+    assertEquals(m3.height(), 3);
+    assertEquals(m3.width(), 3);
+    assertEquals(m3.getValue(0, 0), 10d);
+    assertEquals(m3.getValue(0, 1), 15d);
+    assertEquals(m3.getValue(0, 2), 20d);
+    assertEquals(m3.getValue(1, 0), 12d);
+    assertEquals(m3.getValue(1, 1), 18d);
+    assertEquals(m3.getValue(1, 2), 24d);
+    assertEquals(m3.getValue(2, 0), 14d);
+    assertEquals(m3.getValue(2, 1), 21d);
+    assertEquals(m3.getValue(2, 2), 28d);
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testPreMultiply_tooManyColumns()
+  {
+    Matrix m1 = new SparseMatrix(
+            new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }); // 2x3
+
+    /*
+     * 2x3 times 2x3 invalid operation - 
+     * multiplier has more columns than multiplicand has rows
+     */
+    m1.preMultiply(m1);
+    fail("Expected exception");
+  }
+
+  @Test(
+    groups = "Functional",
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testPreMultiply_tooFewColumns()
+  {
+    Matrix m1 = new SparseMatrix(
+            new double[][] { { 2, 3, 4 }, { 3, 4, 5 } }); // 2x3
+
+    /*
+     * 3x2 times 3x2 invalid operation - 
+     * multiplier has more columns than multiplicand has row
+     */
+    m1.preMultiply(m1);
+    fail("Expected exception");
+  }
+  
+  @Test(groups = "Functional")
+  public void testPostMultiply()
+  {
+    /*
+     * Square matrices
+     * (2 3) . (10   100)
+     * (4 5)   (1000 10000)
+     * =
+     * (3020 30200)
+     * (5040 50400)
+     */
+    MatrixI m1 = new SparseMatrix(new double[][] { { 2, 3 }, { 4, 5 } });
+    MatrixI m2 = new SparseMatrix(new double[][] { { 10, 100 },
+        { 1000, 10000 } });
+    MatrixI m3 = m1.postMultiply(m2);
+    assertEquals(m3.getValue(0, 0), 3020d);
+    assertEquals(m3.getValue(0, 1), 30200d);
+    assertEquals(m3.getValue(1, 0), 5040d);
+    assertEquals(m3.getValue(1, 1), 50400d);
+
+    /*
+     * also check m2.preMultiply(m1) - should be same as m1.postMultiply(m2) 
+     */
+    MatrixI m4 = m2.preMultiply(m1);
+    assertMatricesMatch(m3, m4, 0.00001d);
+
+    /*
+     * m1 has more rows than columns
+     * (2).(10 100 1000) = (20 200 2000)
+     * (3)                 (30 300 3000)
+     */
+    m1 = new SparseMatrix(new double[][] { { 2 }, { 3 } });
+    m2 = new SparseMatrix(new double[][] { { 10, 100, 1000 } });
+    m3 = m1.postMultiply(m2);
+    assertEquals(m3.height(), 2);
+    assertEquals(m3.width(), 3);
+    assertEquals(m3.getValue(0, 0), 20d);
+    assertEquals(m3.getValue(0, 1), 200d);
+    assertEquals(m3.getValue(0, 2), 2000d);
+    assertEquals(m3.getValue(1, 0), 30d);
+    assertEquals(m3.getValue(1, 1), 300d);
+    assertEquals(m3.getValue(1, 2), 3000d);
+
+    m4 = m2.preMultiply(m1);
+    assertMatricesMatch(m3, m4, 0.00001d);
+
+    /*
+     * m1 has more columns than rows
+     * (2 3 4) . (5 4) = (56 25)
+     *           (6 3) 
+     *           (7 2)
+     * [0, 0] = 2*5 + 3*6 + 4*7 = 56
+     * [0, 1] = 2*4 + 3*3 + 4*2 = 25  
+     */
+    m1 = new SparseMatrix(new double[][] { { 2, 3, 4 } });
+    m2 = new SparseMatrix(new double[][] { { 5, 4 }, { 6, 3 }, { 7, 2 } });
+    m3 = m1.postMultiply(m2);
+    assertEquals(m3.height(), 1);
+    assertEquals(m3.width(), 2);
+    assertEquals(m3.getValue(0, 0), 56d);
+    assertEquals(m3.getValue(0, 1), 25d);
+
+    /*
+     * and check premultiply equivalent
+     */
+    m4 = m2.preMultiply(m1);
+    assertMatricesMatch(m3, m4, 0.00001d);
+  }
+
+  @Test(groups = "Timing")
+  public void testSign()
+  {
+    assertEquals(Matrix.sign(-1, -2), -1d);
+    assertEquals(Matrix.sign(-1, 2), 1d);
+    assertEquals(Matrix.sign(-1, 0), 1d);
+    assertEquals(Matrix.sign(1, -2), -1d);
+    assertEquals(Matrix.sign(1, 2), 1d);
+    assertEquals(Matrix.sign(1, 0), 1d);
+  }
+
+  /**
+   * Verify that the results of method tred() are the same for SparseMatrix as
+   * they are for Matrix (i.e. a regression test rather than an absolute test of
+   * correctness of results)
+   */
+  @Test(groups = "Functional")
+  public void testTred_matchesMatrix()
+  {
+    /*
+     * make a pseudo-random symmetric matrix as required for tred/tqli
+     */
+    int rows = 10;
+    int cols = rows;
+    double[][] d = getSparseValues(rows, cols, 3);
+
+    /*
+     * make a copy of the values so m1, m2 are not
+     * sharing arrays!
+     */
+    double[][] d1 = new double[rows][cols];
+    for (int row = 0; row < rows; row++)
+    {
+      for (int col = 0; col < cols; col++)
+      {
+        d1[row][col] = d[row][col];
+      }
+    }
+    Matrix m1 = new Matrix(d);
+    Matrix m2 = new SparseMatrix(d1);
+    assertMatricesMatch(m1, m2, 0.00001d); // sanity check
+    m1.tred();
+    m2.tred();
+    assertMatricesMatch(m1, m2, 0.00001d);
+  }
+
+  private void assertMatricesMatch(MatrixI m1, MatrixI m2, double delta)
+  {
+    if (m1.height() != m2.height())
+    {
+      fail("height mismatch");
+    }
+    if (m1.width() != m2.width())
+    {
+      fail("width mismatch");
+    }
+    for (int row = 0; row < m1.height(); row++)
+    {
+      for (int col = 0; col < m1.width(); col++)
+      {
+        double v2 = m2.getValue(row, col);
+        double v1 = m1.getValue(row, col);
+        if (Math.abs(v1 - v2) > DELTA)
+        {
+          fail(String.format("At [%d, %d] %f != %f", row, col, v1, v2));
+        }
+      }
+    }
+    ArrayAsserts.assertArrayEquals(m1.getD(), m2.getD(), delta);
+    ArrayAsserts.assertArrayEquals(m1.getE(), m2.getE(), 0.00001d);
+  }
+
+  @Test
+  public void testGetValue()
+  {
+    double[][] d = new double[][] { { 0, 0, 1, 0, 0 }, { 2, 3, 0, 0, 0 },
+        { 4, 0, 0, 0, 5 } };
+    MatrixI m = new SparseMatrix(d);
+    for (int row = 0; row < 3; row++)
+    {
+      for (int col = 0; col < 5; col++)
+      {
+        assertEquals(m.getValue(row, col), d[row][col],
+                String.format("At [%d, %d]", row, col));
+      }
+    }
+  }
+
+  /**
+   * Verify that the results of method tqli() are the same for SparseMatrix as
+   * they are for Matrix (i.e. a regression test rather than an absolute test of
+   * correctness of results)
+   * 
+   * @throws Exception
+   */
+  @Test(groups = "Functional")
+  public void testTqli_matchesMatrix() throws Exception
+  {
+    /*
+     * make a pseudo-random symmetric matrix as required for tred
+     */
+    int rows = 6;
+    int cols = rows;
+    double[][] d = getSparseValues(rows, cols, 3);
+  
+    /*
+     * make a copy of the values so m1, m2 are not
+     * sharing arrays!
+     */
+    double[][] d1 = new double[rows][cols];
+    for (int row = 0; row < rows; row++)
+    {
+      for (int col = 0; col < cols; col++)
+      {
+        d1[row][col] = d[row][col];
+      }
+    }
+    Matrix m1 = new Matrix(d);
+    Matrix m2 = new SparseMatrix(d1);
+
+    // have to do tred() before doing tqli()
+    m1.tred();
+    m2.tred();
+    assertMatricesMatch(m1, m2, 0.00001d);
+
+    m1.tqli();
+    m2.tqli();
+    assertMatricesMatch(m1, m2, 0.00001d);
+  }
+
+  /**
+   * Helper method to make values for a sparse, pseudo-random symmetric matrix
+   * 
+   * @param rows
+   * @param cols
+   * @param occupancy
+   *          one in 'occupancy' entries will be non-zero
+   * @return
+   */
+  public double[][] getSparseValues(int rows, int cols, int occupancy)
+  {
+    /*
+     * generate whole number values between -12 and +12
+     * (to mimic score matrices used in Jalview)
+     */
+    double[][] d = new double[rows][cols];
+    int m = 0;
+    for (int i = 0; i < rows; i++)
+    {
+      if (++m % occupancy == 0)
+      {
+        d[i][i] = r.nextInt() % 13; // diagonal
+      }
+      for (int j = 0; j < i; j++)
+      {
+        if (++m % occupancy == 0)
+        {
+          d[i][j] = r.nextInt() % 13;
+          d[j][i] = d[i][j];
+        }
+      }
+    }
+    return d;
+
+  }
+
+  /**
+   * Test that verifies that the result of preMultiply is a SparseMatrix if more
+   * than 80% zeroes, else a Matrix
+   */
+  @Test(groups = "Functional")
+  public void testPreMultiply_sparseProduct()
+  {
+    MatrixI m1 = new SparseMatrix(new double[][] { { 1 }, { 0 }, { 0 },
+        { 0 }, { 0 } }); // 5x1
+    MatrixI m2 = new SparseMatrix(new double[][] { { 1, 1, 1, 1 } }); // 1x4
+  
+    /*
+     * m1.m2 makes a row of 4 1's, and 4 rows of zeros
+     * 20% non-zero so not 'sparse'
+     */
+    MatrixI m3 = m2.preMultiply(m1);
+    assertFalse(m3 instanceof SparseMatrix);
+
+    /*
+     * replace a 1 with a 0 in the product:
+     * it is now > 80% zero so 'sparse'
+     */
+    m2 = new SparseMatrix(new double[][] { { 1, 1, 1, 0 } });
+    m3 = m2.preMultiply(m1);
+    assertTrue(m3 instanceof SparseMatrix);
+  }
+
+  @Test(groups = "Functional")
+  public void testFillRatio()
+  {
+    SparseMatrix m1 = new SparseMatrix(new double[][] { { 2, 0, 4, 1, 0 },
+    { 0, 6, 0, 0, 0 } });
+    assertEquals(m1.getFillRatio(), 0.4f);
+  }
+
+  /**
+   * Verify that the results of method tred() are the same if the calculation is
+   * redone
+   */
+  @Test(groups = "Functional")
+  public void testTred_reproducible()
+  {
+    /*
+     * make a pseudo-random symmetric matrix as required for tred/tqli
+     */
+    int rows = 10;
+    int cols = rows;
+    double[][] d = getSparseValues(rows, cols, 3);
+  
+    /*
+     * make a copy of the values so m1, m2 are not
+     * sharing arrays!
+     */
+    double[][] d1 = new double[rows][cols];
+    for (int row = 0; row < rows; row++)
+    {
+      for (int col = 0; col < cols; col++)
+      {
+        d1[row][col] = d[row][col];
+      }
+    }
+    Matrix m1 = new SparseMatrix(d);
+    Matrix m2 = new SparseMatrix(d1);
+    assertMatricesMatch(m1, m2, 1.0e16); // sanity check
+    m1.tred();
+    m2.tred();
+    assertMatricesMatch(m1, m2, 0.00001d);
+  }
+}
\ No newline at end of file
diff --git a/test/jalview/renderer/ResidueShaderTest.java b/test/jalview/renderer/ResidueShaderTest.java
new file mode 100644 (file)
index 0000000..76fd9b4
--- /dev/null
@@ -0,0 +1,166 @@
+package jalview.renderer;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.analysis.Conservation;
+import jalview.datamodel.Profile;
+import jalview.datamodel.ProfileI;
+import jalview.datamodel.Profiles;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.PIDColourScheme;
+
+import java.awt.Color;
+import java.util.Collections;
+
+import org.testng.annotations.Test;
+
+public class ResidueShaderTest
+{
+
+  @Test(groups = "Functional")
+  public void testAboveThreshold()
+  {
+    /*
+     * make up profiles for this alignment:
+     * AR-Q
+     * AR--
+     * SR-T
+     * SR-T
+     */
+    ProfileI[] profiles = new ProfileI[4];
+    profiles[0] = new Profile(4, 0, 2, "AS");
+    profiles[1] = new Profile(4, 0, 4, "R");
+    profiles[2] = new Profile(4, 4, 0, "");
+    profiles[3] = new Profile(4, 1, 2, "T");
+    ResidueShader ccs = new ResidueShader(new PIDColourScheme());
+    ccs.setConsensus(new Profiles(profiles));
+
+    /*
+     * no threshold
+     */
+    ccs.setThreshold(0, true);
+    assertTrue(ccs.aboveThreshold('a', 0));
+    assertTrue(ccs.aboveThreshold('S', 0));
+    assertTrue(ccs.aboveThreshold('W', 0));
+    assertTrue(ccs.aboveThreshold('R', 1));
+    assertTrue(ccs.aboveThreshold('W', 2));
+    assertTrue(ccs.aboveThreshold('t', 3));
+    assertTrue(ccs.aboveThreshold('Q', 3));
+
+    /*
+     * with threshold, include gaps
+     */
+    ccs.setThreshold(60, false);
+    assertFalse(ccs.aboveThreshold('a', 0));
+    assertFalse(ccs.aboveThreshold('S', 0));
+    assertTrue(ccs.aboveThreshold('R', 1));
+    assertFalse(ccs.aboveThreshold('W', 2));
+    assertFalse(ccs.aboveThreshold('t', 3)); // 50% < 60%
+
+    /*
+     * with threshold, ignore gaps
+     */
+    ccs.setThreshold(60, true);
+    assertFalse(ccs.aboveThreshold('a', 0));
+    assertFalse(ccs.aboveThreshold('S', 0));
+    assertTrue(ccs.aboveThreshold('R', 1));
+    assertFalse(ccs.aboveThreshold('W', 2));
+    assertTrue(ccs.aboveThreshold('t', 3)); // 67% > 60%
+  }
+
+  /**
+   * Test colour bleaching based on conservation score and conservation slider.
+   * Scores of 10 or 11 should leave colours unchanged. Gap is always white.
+   */
+  @Test(groups = "Functional")
+  public void testApplyConservation()
+  {
+    ResidueShader ccs = new ResidueShader(new PIDColourScheme());
+
+    // no conservation present - no fading
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 12));
+
+    /*
+     * stub Conservation to return a given consensus string
+     */
+    final String consSequence = "0123456789+*-";
+    Conservation cons = new Conservation(null,
+            Collections.<SequenceI> emptyList(), 0, 0)
+    {
+      @Override
+      public SequenceI getConsSequence()
+      {
+        return new Sequence("seq", consSequence);
+      }
+    };
+    ccs.setConservation(cons);
+
+    // column out of range:
+    assertEquals(Color.RED,
+            ccs.applyConservation(Color.RED, consSequence.length()));
+
+    /*
+     * with 100% threshold, 'fade factor' is 
+     * (11-score)/10 * 100/20 = (11-score)/2
+     * which is >= 1 for all scores i.e. all fade to white except +, *
+     */
+    ccs.setConservationInc(100);
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 0));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 1));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 2));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 3));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 4));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 5));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 6));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 7));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 8));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 9));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 10));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 11));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 12));
+
+    /*
+     * with 0% threshold, there should be no fading
+     */
+    ccs.setConservationInc(0);
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 0));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 1));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 2));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 3));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 4));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 5));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 6));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 7));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 8));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 9));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 10));
+    assertEquals(Color.RED, ccs.applyConservation(Color.RED, 11));
+    assertEquals(Color.WHITE, ccs.applyConservation(Color.RED, 12)); // gap
+
+    /*
+     * with 40% threshold, 'fade factor' is 
+     * (11-score)/10 * 40/20 = (11-score)/5
+     * which is {>1, >1, >1, >1, >1, >1, 1, 0.8, 0.6, 0.4} for score 0-9
+     * e.g. score 7 colour fades 80% of the way to white (255, 255, 255)
+     */
+    ccs.setConservationInc(40);
+    Color colour = new Color(155, 105, 55);
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 0));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 1));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 2));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 3));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 4));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 5));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 6));
+    assertEquals(new Color(235, 225, 215), ccs.applyConservation(colour, 7));
+    assertEquals(new Color(215, 195, 175), ccs.applyConservation(colour, 8));
+    assertEquals(new Color(195, 165, 135), ccs.applyConservation(colour, 9));
+    assertEquals(colour, ccs.applyConservation(colour, 10));
+    assertEquals(colour, ccs.applyConservation(colour, 11));
+    assertEquals(Color.WHITE, ccs.applyConservation(colour, 12));
+  }
+
+}
diff --git a/test/jalview/renderer/ScaleRendererTest.java b/test/jalview/renderer/ScaleRendererTest.java
new file mode 100644 (file)
index 0000000..cf1039f
--- /dev/null
@@ -0,0 +1,73 @@
+package jalview.renderer;
+
+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.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.renderer.ScaleRenderer.ScaleMark;
+
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class ScaleRendererTest
+{
+  @Test(groups = "Functional")
+  public void testCalculateMarks()
+  {
+    String data = ">Seq/20-45\nABCDEFGHIJKLMNOPQRSTUVWXYS\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(data,
+            DataSourceType.PASTE);
+    AlignViewport av = af.getViewport();
+
+    /*
+     * scale has minor ticks at 5 and 15, 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);
+
+    assertFalse(marks.get(0).major);
+    assertEquals(marks.get(0).column, 4);
+    assertNull(marks.get(0).text);
+
+    assertTrue(marks.get(1).major);
+    assertEquals(marks.get(1).column, 9);
+    assertEquals(marks.get(1).text, "10");
+
+    assertFalse(marks.get(2).major);
+    assertEquals(marks.get(2).column, 14);
+    assertNull(marks.get(2).text);
+
+    assertTrue(marks.get(3).major);
+    assertEquals(marks.get(3).column, 19);
+    assertEquals(marks.get(3).text, "20");
+
+    /*
+     * now hide columns 9-11 and 18-20 (base 1)
+     * scale marks are now in the same columns as before, but
+     * with column numbering adjusted for hidden columns
+     */
+    av.hideColumns(8, 10);
+    av.hideColumns(17, 19);
+    marks = new ScaleRenderer().calculateMarks(av, 0, 25);
+    assertEquals(marks.size(), 4);
+    assertFalse(marks.get(0).major);
+    assertEquals(marks.get(0).column, 4);
+    assertNull(marks.get(0).text);
+    assertTrue(marks.get(1).major);
+    assertEquals(marks.get(1).column, 9);
+    assertEquals(marks.get(1).text, "13"); // +3 hidden columns
+    assertFalse(marks.get(2).major);
+    assertEquals(marks.get(2).column, 14);
+    assertNull(marks.get(2).text);
+    assertTrue(marks.get(3).major);
+    assertEquals(marks.get(3).column, 19);
+    assertEquals(marks.get(3).text, "26"); // +6 hidden columns
+  }
+}
diff --git a/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java b/test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java
new file mode 100644 (file)
index 0000000..7556dff
--- /dev/null
@@ -0,0 +1,446 @@
+package jalview.renderer.seqfeatures;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.api.FeatureColourI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.gui.FeatureRenderer;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
+
+import java.awt.Color;
+import java.util.List;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for feature colour determination, including but not limited to
+ * <ul>
+ * <li>gap position</li>
+ * <li>no features present</li>
+ * <li>features present but show features turned off</li>
+ * <li>features displayed but selected feature turned off</li>
+ * <li>features displayed but feature group turned off</li>
+ * <li>feature displayed but none at the specified position</li>
+ * <li>multiple features at position, with no transparency</li>
+ * <li>multiple features at position, with transparency</li>
+ * <li>score graduated feature colour</li>
+ * <li>contact feature start at the selected position</li>
+ * <li>contact feature end at the selected position</li>
+ * <li>contact feature straddling the selected position (not shown)</li>
+ * </ul>
+ */
+public class FeatureColourFinderTest
+{
+  private AlignViewport av;
+
+  private SequenceI seq;
+
+  private FeatureColourFinder finder;
+
+  private AlignFrame af;
+
+  private FeatureRenderer fr;
+
+  @BeforeTest(alwaysRun = true)
+  public void setUp()
+  {
+    // aligned column 8 is sequence position 6
+    String s = ">s1\nABCDE---FGHIJKLMNOPQRSTUVWXYZ\n";
+    af = new FileLoader().LoadFileWaitTillLoaded(s,
+            DataSourceType.PASTE);
+    av = af.getViewport();
+    seq = av.getAlignment().getSequenceAt(0);
+    fr = af.getFeatureRenderer();
+    finder = new FeatureColourFinder(fr);
+  }
+
+  /**
+   * Clear down any sequence features before each test (not as easy as it
+   * sounds...)
+   */
+  @BeforeMethod(alwaysRun = true)
+  public void setUpBeforeTest()
+  {
+    List<SequenceFeature> sfs = seq.getSequenceFeatures();
+    for (SequenceFeature sf : sfs)
+    {
+      seq.deleteFeature(sf);
+    }
+    fr.findAllFeatures(true);
+
+    /*
+     * reset all feature groups to visible
+     */
+    for (String group : fr.getGroups(false))
+    {
+      fr.setGroupVisibility(group, true);
+    }
+
+    fr.clearRenderOrder();
+    av.setShowSequenceFeatures(true);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_noFeatures()
+  {
+    av.setShowSequenceFeatures(false);
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.blue);
+
+    av.setShowSequenceFeatures(true);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.blue);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_noFeaturesShown()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(false);
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.blue);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_singleFeatureAtPosition()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    fr.setColour("Metal", new FeatureColour(Color.red));
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.red);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_gapPosition()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12, 0f,
+            null));
+    fr.setColour("Metal", new FeatureColour(Color.red));
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+    Color c = finder.findFeatureColour(null, seq, 6);
+    assertEquals(c, Color.white);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_multipleFeaturesAtPositionNoTransparency()
+  {
+    /*
+     * featuresAdded -> FeatureRendererModel.updateRenderOrder which adds any
+     * new features 'on top' (but reverses the order of any added features)
+     */
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    FeatureColour red = new FeatureColour(Color.red);
+    fr.setColour("Metal", red);
+    fr.featuresAdded();
+    seq.addSequenceFeature(new SequenceFeature("Domain", "Domain", 4, 15,
+            Float.NaN, "DomainGroup"));
+    FeatureColour green = new FeatureColour(Color.green);
+    fr.setColour("Domain", green);
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+
+    /*
+     * expect Domain (green) to be rendered above Metal (red)
+     */
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.green);
+
+    /*
+     * now promote Metal above Domain
+     * - 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 };
+    fr.setFeaturePriority(data);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.red);
+
+    /*
+     * ..and turn off display of Metal
+     */
+    data[0][2] = false;
+    fr.setFeaturePriority(data);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.green);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_singleFeatureNotAtPosition()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 8, 12,
+            Float.NaN, "MetalGroup"));
+    fr.setColour("Metal", new FeatureColour(Color.red));
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+    // column 2 = sequence position 3
+    Color c = finder.findFeatureColour(Color.blue, seq, 2);
+    assertEquals(c, Color.blue);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_featureTypeNotDisplayed()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    FeatureColour red = new FeatureColour(Color.red);
+    fr.setColour("Metal", red);
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.red);
+
+    /*
+     * 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 };
+    fr.setFeaturePriority(data);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.blue);
+
+    /*
+     * turn display of Metal back on
+     */
+    data[0] = new Object[] { "Metal", red, true };
+    fr.setFeaturePriority(data);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.red);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_featureGroupNotDisplayed()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    FeatureColour red = new FeatureColour(Color.red);
+    fr.setColour("Metal", red);
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.red);
+
+    /*
+     * turn off display of MetalGroup
+     */
+    fr.setGroupVisibility("MetalGroup", false);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.blue);
+
+    /*
+     * turn display of MetalGroup back on
+     */
+    fr.setGroupVisibility("MetalGroup", true);
+    c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.red);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_contactFeature()
+  {
+    /*
+     * currently contact feature == type "Disulphide Bond" or "Disulfide Bond" !!
+     */
+    seq.addSequenceFeature(new SequenceFeature("Disulphide Bond",
+            "Contact", 2, 12, Float.NaN, "Disulphide"));
+    fr.setColour("Disulphide Bond", new FeatureColour(Color.red));
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+
+    /*
+     * Contact positions are residues 2 and 12
+     * which are columns 1 and 14
+     * positions in between don't count for a contact feature!
+     */
+    Color c = finder.findFeatureColour(Color.blue, seq, 10);
+    assertEquals(c, Color.blue);
+    c = finder.findFeatureColour(Color.blue, seq, 8);
+    assertEquals(c, Color.blue);
+    c = finder.findFeatureColour(Color.blue, seq, 1);
+    assertEquals(c, Color.red);
+    c = finder.findFeatureColour(Color.blue, seq, 14);
+    assertEquals(c, Color.red);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_graduatedFeatureColour()
+  {
+    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 2,
+            2, 0f, "KdGroup"));
+    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 4,
+            4, 5f, "KdGroup"));
+    seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 7,
+            7, 10f, "KdGroup"));
+
+    /*
+     * graduated colour from 0 to 10
+     */
+    Color min = new Color(100, 50, 150);
+    Color max = new Color(200, 0, 100);
+    FeatureColourI fc = new FeatureColour(min, max, 0, 10);
+    fr.setColour("kd", fc);
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+
+    /*
+     * position 2, column 1, score 0 - minimum colour in range
+     */
+    Color c = finder.findFeatureColour(Color.blue, seq, 1);
+    assertEquals(c, min);
+
+    /*
+     * position 7, column 9, score 10 - maximum colour in range
+     */
+    c = finder.findFeatureColour(Color.blue, seq, 9);
+    assertEquals(c, max);
+
+    /*
+     * position 4, column 3, score 5 - half way from min to max
+     */
+    c = finder.findFeatureColour(Color.blue, seq, 3);
+    assertEquals(c, new Color(150, 25, 125));
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_transparencySingleFeature()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    FeatureColour red = new FeatureColour(Color.red);
+    fr.setColour("Metal", red);
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+  
+    /*
+     * the FeatureSettings transparency slider has range 0-70 which
+     * corresponds to a transparency value of 1 - 0.3
+     * A value of 0.4 gives a combination of
+     * 0.4 * red(255, 0, 0) + 0.6 * cyan(0, 255, 255) = (102, 153, 153)
+     */
+    fr.setTransparency(0.4f);
+    Color c = finder.findFeatureColour(Color.cyan, seq, 10);
+    assertEquals(c, new Color(102, 153, 153));
+  }
+
+  @Test(groups = "Functional")
+  public void testFindFeatureColour_transparencyTwoFeatures()
+  {
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    FeatureColour red = new FeatureColour(Color.red);
+    fr.setColour("Metal", red);
+    fr.featuresAdded();
+    seq.addSequenceFeature(new SequenceFeature("Domain", "Domain", 4, 15,
+            Float.NaN, "DomainGroup"));
+    FeatureColour green = new FeatureColour(Color.green);
+    fr.setColour("Domain", green);
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+  
+    /*
+     * Domain (green) rendered above Metal (red) above background (cyan)
+     * 1) 0.6 * red(255, 0, 0) + 0.4 * cyan(0, 255, 255) = (153, 102, 102)
+     * 2) 0.6* green(0, 255, 0) + 0.4 * (153, 102, 102) = (61, 194, 41) rounded
+     */
+    fr.setTransparency(0.6f);
+    Color c = finder.findFeatureColour(Color.cyan, seq, 10);
+    assertEquals(c, new Color(61, 194, 41));
+  
+    /*
+     * now promote Metal above Domain
+     * - currently no way other than mimicking reordering of
+     * table in Feature Settings
+     * Metal (red) rendered above Domain (green) above background (cyan)
+     * 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 };
+    fr.setFeaturePriority(data);
+    c = finder.findFeatureColour(Color.cyan, seq, 10);
+    assertEquals(c, new Color(153, 102, 41));
+  
+    /*
+     * ..and turn off display of Metal
+     * Domain (green) above background (pink)
+     * 0.6 * green(0, 255, 0) + 0.4 * pink(255, 175, 175) = (102, 223, 70)
+     */
+    data[0][2] = false;
+    fr.setFeaturePriority(data);
+    c = finder.findFeatureColour(Color.pink, seq, 10);
+    assertEquals(c, new Color(102, 223, 70));
+  }
+
+  @Test(groups = "Functional")
+  public void testNoFeaturesDisplayed()
+  {
+    /*
+     * no features on alignment to render
+     */
+    assertTrue(finder.noFeaturesDisplayed());
+
+    /*
+     * add a feature
+     * it will be automatically set visible but we leave
+     * the viewport configured not to show features
+     */
+    av.setShowSequenceFeatures(false);
+    seq.addSequenceFeature(new SequenceFeature("Metal", "Metal", 2, 12,
+            Float.NaN, "MetalGroup"));
+    FeatureColour red = new FeatureColour(Color.red);
+    fr.setColour("Metal", red);
+    fr.featuresAdded();
+    assertTrue(finder.noFeaturesDisplayed());
+
+    /*
+     * turn on feature display
+     */
+    av.setShowSequenceFeatures(true);
+    assertFalse(finder.noFeaturesDisplayed());
+
+    /*
+     * turn off display of Metal
+     */
+    Object[][] data = new Object[1][];
+    data[0] = new Object[] { "Metal", red, false };
+    fr.setFeaturePriority(data);
+    assertTrue(finder.noFeaturesDisplayed());
+
+    /*
+     * turn display of Metal back on
+     */
+    fr.setVisible("Metal");
+    assertFalse(finder.noFeaturesDisplayed());
+
+    /*
+     * turn off MetalGroup - has no effect here since the group of a
+     * sequence feature instance is independent of its type
+     */
+    fr.setGroupVisibility("MetalGroup", false);
+    assertFalse(finder.noFeaturesDisplayed());
+
+    /*
+     * a finder with no feature renderer
+     */
+    FeatureColourFinder finder2 = new FeatureColourFinder(null);
+    assertTrue(finder2.noFeaturesDisplayed());
+  }
+}
diff --git a/test/jalview/renderer/seqfeatures/FeatureRendererTest.java b/test/jalview/renderer/seqfeatures/FeatureRendererTest.java
new file mode 100644 (file)
index 0000000..3e27aba
--- /dev/null
@@ -0,0 +1,357 @@
+package jalview.renderer.seqfeatures;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import jalview.api.AlignViewportI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+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
+     */
+    Object[][] data = new Object[3][];
+    FeatureColourI colour = new FeatureColour(Color.RED);
+    data[0] = new Object[] { "Rfam", colour, true };
+    data[1] = new Object[] { "Pfam", colour, false };
+    data[2] = new Object[] { "Scop", colour, false };
+    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
+     */
+    Object[][] data = new Object[4][];
+    FeatureColourI colour = new FeatureColour(Color.RED);
+    data[0] = new Object[] { "Type1", colour, true };
+    data[1] = new Object[] { "Type2", colour, false };
+    data[2] = new Object[] { "Type3", colour, true };
+    data[3] = new Object[] { "Disulphide Bond", colour, true };
+    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));
+  }
+
+  @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, null); // empty list, does nothing
+
+    SequenceI seq = av.getAlignment().getSequenceAt(0);
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, "group1");
+    seq.addSequenceFeature(sf1);
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 5, 11, "group2");
+    seq.addSequenceFeature(sf2);
+    SequenceFeature sf3 = new SequenceFeature("Cath", "", 5, 11, "group3");
+    seq.addSequenceFeature(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Cath", "", 6, 8, "group4");
+    seq.addSequenceFeature(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Cath", "", 6, 9, "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, new FeatureColour(Color.blue));
+    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 group 3 - sf3 is removed, sf2 is retained
+     */
+    fr.setGroupVisibility("group3", false);
+    features = seq.getSequenceFeatures();
+    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertTrue(features.contains(sf2));
+    assertFalse(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * hide group 2, show group 3 - sf2 is removed, sf3 is retained
+     */
+    fr.setGroupVisibility("group2", false);
+    fr.setGroupVisibility("group3", true);
+    features = seq.getSequenceFeatures();
+    fr.filterFeaturesForDisplay(features, null);
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertFalse(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * no filtering of co-located features with graduated colour scheme
+     * sf2 is removed as its group is hidden
+     */
+    features = seq.getSequenceFeatures();
+    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
+            Color.white, 0f, 1f));
+    assertEquals(features.size(), 4);
+    assertTrue(features.contains(sf1));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf4));
+    assertTrue(features.contains(sf5));
+
+    /*
+     * filtering of co-located features with colour by label
+     */
+    features = seq.getSequenceFeatures();
+    FeatureColour fc = new FeatureColour(Color.black);
+    fc.setColourByLabel(true);
+    fr.filterFeaturesForDisplay(features, fc);
+    assertEquals(features.size(), 3);
+    assertTrue(features.contains(sf1) || features.contains(sf4));
+    assertFalse(features.contains(sf1) && features.contains(sf4));
+    assertFalse(features.contains(sf2));
+    assertTrue(features.contains(sf3));
+    assertTrue(features.contains(sf5));
+  }
+}
diff --git a/test/jalview/schemes/AnnotationColourGradientTest.java b/test/jalview/schemes/AnnotationColourGradientTest.java
new file mode 100644 (file)
index 0000000..b7a5164
--- /dev/null
@@ -0,0 +1,300 @@
+package jalview.schemes;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.GraphLine;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+
+import java.awt.Color;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class AnnotationColourGradientTest
+{
+  final static int WIDTH = 11;
+
+  final static int THRESHOLD_FIVE = 5;
+
+  private AlignmentAnnotation ann;
+
+  private SequenceI seq;
+
+  private AlignmentI al;
+
+  Color minColour = new Color(50, 200, 150);
+
+  Color maxColour = new Color(150, 100, 250);
+
+  /**
+   * Setup creates an annotation over 11 columns with values 0-10 and threshold
+   * 5
+   */
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Annotation[] anns = new Annotation[WIDTH];
+    /*
+     * set annotations with values 0-10, graded colours
+     */
+    for (int col = 0; col < WIDTH; col++)
+    {
+      int hue = col * 20;
+      Color colour = new Color(hue, hue, hue);
+      anns[col] = new Annotation("a", "a", 'a', col, colour);
+    }
+
+    seq = new Sequence("Seq", "");
+    al = new Alignment(new SequenceI[]{ seq});
+    
+    /*
+     * AlignmentAnnotation constructor works out min-max range
+     */
+    ann = new AlignmentAnnotation("", "", anns);
+    ann.setThreshold(new GraphLine(THRESHOLD_FIVE, "", Color.RED));
+    seq.addAlignmentAnnotation(ann);
+  }
+
+  @Test(groups = "Functional")
+  public void testShadeCalculation_noThreshold()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.NO_THRESHOLD);
+    for (int col = 0; col < WIDTH; col++)
+    {
+      Color result = testee.shadeCalculation(ann, col);
+      /*
+       * column <n> is n/10 of the way from minCol to maxCol
+       */
+      Color expected = new Color(50 + 10 * col, 200 - 10 * col,
+              150 + 10 * col);
+      assertEquals(result, expected, "for column " + col);
+    }
+  }
+
+  /**
+   * Test the 'colour above threshold' case
+   */
+  @Test(groups = "Functional")
+  public void testShadeCalculation_aboveThreshold()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.ABOVE_THRESHOLD);
+    for (int col = 0; col < WIDTH; col++)
+    {
+      Color result = testee.shadeCalculation(ann, col);
+      /*
+       * colour is derived regardless of the threshold value 
+       * (the renderer will suppress colouring if above/below threshold)
+       */
+      Color expected = new Color(50 + 10 * col, 200 - 10 * col,
+              150 + 10 * col);
+      assertEquals(result, expected, "for column " + col);
+    }
+
+    /*
+     * now make 6-10 the span of the colour range
+     * (annotation value == column number in this test)
+     */
+    testee.setThresholdIsMinMax(true);
+    for (int col = 0; col < THRESHOLD_FIVE; col++)
+    {
+      /*
+       * colours below the threshold are computed as before
+       */
+      Color expected = new Color(50 + 10 * col, 200 - 10 * col,
+              150 + 10 * col);
+      Color result = testee.shadeCalculation(ann, col);
+      assertEquals(result, expected, "for column " + col);
+    }
+    for (int col = THRESHOLD_FIVE; col < WIDTH; col++)
+    {
+      /*
+       * colours for values >= threshold are graduated
+       * range is 6-10 so steps of 100/5 = 20
+       */
+      int factor = col - THRESHOLD_FIVE;
+      Color expected = new Color(50 + 20 * factor, 200 - 20 * factor,
+              150 + 20 * factor);
+      Color result = testee.shadeCalculation(ann, col);
+      assertEquals(result, expected, "for column " + col);
+    }
+  }
+
+  /**
+   * Test the 'colour below threshold' case
+   */
+  @Test(groups = "Functional")
+  public void testShadeCalculation_belowThreshold()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.BELOW_THRESHOLD);
+
+    for (int col = 0; col < WIDTH; col++)
+    {
+      Color result = testee.shadeCalculation(ann, col);
+      /*
+       * colour is derived regardless of the threshold value 
+       * (the renderer will suppress colouring if above/below threshold)
+       */
+      Color expected = new Color(50 + 10 * col, 200 - 10 * col,
+              150 + 10 * col);
+      assertEquals(result, expected, "for column " + col);
+    }
+
+    /*
+     * now make 0-5 the span of the colour range
+     * (annotation value == column number in this test)
+     */
+    testee.setThresholdIsMinMax(true);
+    for (int col = THRESHOLD_FIVE + 1; col < WIDTH; col++)
+    {
+      /*
+       * colours above the threshold are computed as before
+       */
+      Color expected = new Color(50 + 10 * col, 200 - 10 * col,
+              150 + 10 * col);
+      Color result = testee.shadeCalculation(ann, col);
+      assertEquals(result, expected, "for column " + col);
+    }
+
+    for (int col = 0; col <= THRESHOLD_FIVE; col++)
+    {
+      /*
+       * colours for values <= threshold are graduated
+       * range is 0-5 so steps of 100/5 = 20
+       */
+      Color expected = new Color(50 + 20 * col, 200 - 20 * col,
+              150 + 20 * col);
+      Color result = testee.shadeCalculation(ann, col);
+      assertEquals(result, expected, "for column " + col);
+    }
+  }
+
+  /**
+   * Test the 'colour above threshold' case
+   */
+  @Test(groups = "Functional")
+  public void testFindColour_aboveThreshold()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.ABOVE_THRESHOLD);
+    testee = (AnnotationColourGradient) testee.getInstance(al, null);
+
+    for (int col = 0; col < WIDTH; col++)
+    {
+      Color result = testee.findColour('a', col, seq);
+      /*
+       * expect white below threshold of 5
+       */
+      Color expected = col < 5 ? Color.white : new Color(50 + 10 * col,
+              200 - 10 * col,
+              150 + 10 * col);
+      assertEquals(result, expected, "for column " + col);
+    }
+  
+    /*
+     * now make 6-10 the span of the colour range
+     * (annotation value == column number in this test)
+     */
+    testee.setThresholdIsMinMax(true);
+    for (int col = 0; col < WIDTH; col++)
+    {
+      /*
+       * colours for values >= threshold are graduated
+       * range is 6-10 so steps of 100/5 = 20
+       */
+      int factor = col - THRESHOLD_FIVE;
+      Color expected = col < 5 ? Color.white : new Color(50 + 20 * factor,
+              200 - 20 * factor,
+              150 + 20 * factor);
+      Color result = testee.findColour('a', col, seq);
+      assertEquals(result, expected, "for column " + col);
+    }
+  }
+
+  /**
+   * Test the 'colour below threshold' case
+   */
+  @Test(groups = "Functional")
+  public void testFindColour_belowThreshold()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.BELOW_THRESHOLD);
+    testee = (AnnotationColourGradient) testee.getInstance(al, null);
+  
+    for (int col = 0; col < WIDTH; col++)
+    {
+      Color result = testee.findColour('a', col, seq);
+      Color expected = col > 5 ? Color.white : new Color(50 + 10 * col,
+              200 - 10 * col, 150 + 10 * col);
+      assertEquals(result, expected, "for column " + col);
+    }
+  
+    /*
+     * now make 0-5 the span of the colour range
+     * (annotation value == column number in this test)
+     */
+    testee.setThresholdIsMinMax(true);
+    for (int col = 0; col < WIDTH; col++)
+    {
+      /*
+       * colours for values <= threshold are graduated
+       * range is 0-5 so steps of 100/5 = 20
+       */
+      Color expected = col > 5 ? Color.white : new Color(50 + 20 * col,
+              200 - 20 * col, 150 + 20 * col);
+      Color result = testee.findColour('a', col, seq);
+      assertEquals(result, expected, "for column " + col);
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testFindColour_noThreshold()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.NO_THRESHOLD);
+    testee = (AnnotationColourGradient) testee.getInstance(al, null);
+
+    for (int col = 0; col < WIDTH; col++)
+    {
+      Color result = testee.findColour('a', col, seq);
+      /*
+       * column <n> is n/10 of the way from minCol to maxCol
+       */
+      Color expected = new Color(50 + 10 * col, 200 - 10 * col,
+              150 + 10 * col);
+      assertEquals(result, expected, "for column " + col);
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testFindColour_originalColours()
+  {
+    AnnotationColourGradient testee = new AnnotationColourGradient(ann,
+            minColour, maxColour, AnnotationColourGradient.NO_THRESHOLD);
+    testee = (AnnotationColourGradient) testee.getInstance(al, null);
+
+    /*
+     * flag corresponding to 'use original colours' checkbox
+     * - just use the individual annotation colours
+     */
+    testee.setPredefinedColours(true);
+
+    /*
+     * the annotation colour is returned, except for column 0 where it is
+     * black - in this case the colour scheme colour overrides it
+     */
+    for (int col = 0; col < WIDTH; col++)
+    {
+      int hue = col * 20;
+      Color c = col == 0 ? minColour : new Color(hue, hue, hue);
+      assertEquals(testee.findColour('a', col, seq), c, "for column " + col);
+    }
+  }
+}
diff --git a/test/jalview/schemes/Blosum62ColourSchemeTest.java b/test/jalview/schemes/Blosum62ColourSchemeTest.java
new file mode 100644 (file)
index 0000000..0b5b6bd
--- /dev/null
@@ -0,0 +1,56 @@
+package jalview.schemes;
+
+import static org.testng.Assert.assertEquals;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+
+public class Blosum62ColourSchemeTest
+{
+  /**
+   * Test the method that determines colour as:
+   * <ul>
+   * <li>white if there is no consensus</li>
+   * <li>white if 'residue' is a gap</li>
+   * <li>dark blue if residue matches consensus (or joint consensus)</li>
+   * <li>else, total the residue's Blosum score with the consensus residue(s)</li>
+   * <ul>
+   * <li>if positive, light blue, else white</li>
+   * </ul>
+   * <ul>
+   */
+  @Test
+  public void testFindColour()
+  {
+    ColourSchemeI blosum = new Blosum62ColourScheme();
+    Color lightBlue = new Color(204, 204, 255);
+    Color darkBlue = new Color(154, 154, 255);
+
+    /*
+     * findColour does not use column, sequence or pid score
+     * we assume consensus residue is computed as upper case
+     */
+    assertEquals(blosum.findColour('A', 0, null, "A", 0f), darkBlue);
+    assertEquals(blosum.findColour('a', 0, null, "A", 0f), darkBlue);
+
+    /*
+     * L has a Blosum score of 
+     * -1 with A
+     * -4 with B
+     * 0 with F
+     * 2 with I
+     * -1 with T
+     * 1 with V
+     * etc
+     */
+    assertEquals(blosum.findColour('L', 0, null, "A", 0f), Color.white); // -1
+    assertEquals(blosum.findColour('L', 0, null, "B", 0f), Color.white); // -4
+    assertEquals(blosum.findColour('L', 0, null, "F", 0f), Color.white); // 0
+    assertEquals(blosum.findColour('L', 0, null, "I", 0f), lightBlue); // 2
+    assertEquals(blosum.findColour('L', 0, null, "TV", 0f), Color.white); // 0
+    assertEquals(blosum.findColour('L', 0, null, "IV", 0f), lightBlue); // 3
+    assertEquals(blosum.findColour('L', 0, null, "IT", 0f), lightBlue); // 1
+    assertEquals(blosum.findColour('L', 0, null, "IAT", 0f), Color.white); // 0
+  }
+}
diff --git a/test/jalview/schemes/ClustalxColourSchemeTest.java b/test/jalview/schemes/ClustalxColourSchemeTest.java
new file mode 100644 (file)
index 0000000..d35ca76
--- /dev/null
@@ -0,0 +1,134 @@
+package jalview.schemes;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.AlignmentI;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+
+public class ClustalxColourSchemeTest
+{
+  // @formatter:off
+  private static final String FASTA = 
+          ">seq1\nAAANNNRQ\n" + 
+          ">seq2\nAAANNNRQ\n" + 
+          ">seq3\nAAANNNRQ\n" + 
+          ">seq4\nAAANNNRQ\n" + 
+          ">seq5\nAAANYYKQ\n" + 
+          ">seq6\nAAANYYKQ\n" + 
+          ">seq7\nAVKWYYKQ\n" + 
+          ">seq8\nKKKWYYQQ\n" + 
+          ">seq9\nKKKWWYQQ\n" + 
+          ">seq0\nKKKWWWQW\n";
+  // @formatter:on
+
+  @Test(groups = "Functional")
+  public void testFindColour()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(FASTA,
+            DataSourceType.PASTE);
+    AlignmentI al = af.getViewport().getAlignment();
+    ClustalxColourScheme cs = new ClustalxColourScheme(al, null);
+
+    /*
+     * column 1 is 70% A which is above Clustalx threshold of 60%
+     */
+    Color clustalBlue = new Color(0.5f, 0.7f, 0.9f);
+    assertEquals(cs.findColour('A', 0, al.getSequenceAt(0)), clustalBlue);
+
+    /*
+     * column 2 is 70% A or V which is above Clustalx threshold for group
+     */
+    assertEquals(cs.findColour('A', 0, al.getSequenceAt(1)), clustalBlue);
+
+    /*
+     * column 3 is 60% A which is not above Clustalx threshold
+     * the Ks in the other rows are not in the same Clustalx group
+     */
+    assertEquals(cs.findColour('A', 2, al.getSequenceAt(1)), Color.white);
+
+    /*
+     * column 4 is 60% N which is above Clustalx threshold of 50%
+     */
+    Color clustalGreen = new Color(0.1f, 0.8f, 0.1f);
+    assertEquals(cs.findColour('N', 3, al.getSequenceAt(1)), clustalGreen);
+
+    /*
+     * column 5 is 40% N and 40% Y which fails to pass the threshold of
+     * 50% N or 85% either
+     */
+    assertEquals(cs.findColour('N', 4, al.getSequenceAt(1)), Color.white);
+
+    /*
+     * column 6 is 40% N and 50% Y which fails to pass the threshold of
+     * 85% for either
+     */
+    assertEquals(cs.findColour('N', 5, al.getSequenceAt(1)), Color.white);
+
+    /*
+     * column 7 is 40% R and 30% K which combine to make > 60%
+     */
+    Color clustalRed = new Color(0.9f, 0.2f, 0.1f);
+    assertEquals(cs.findColour('R', 6, al.getSequenceAt(1)), clustalRed);
+    assertEquals(cs.findColour('K', 6, al.getSequenceAt(7)), clustalRed);
+
+    /*
+     * column 8 is >85% Q which qualifies K and R to be red
+     */
+    assertEquals(cs.findColour('R', 7, al.getSequenceAt(1)), clustalRed);
+    assertEquals(cs.findColour('K', 7, al.getSequenceAt(1)), clustalRed);
+
+    // TODO more test cases; check if help documentation matches implementation
+  }
+
+  // @formatter:on
+  
+  /**
+   * Test for colour calculation when the consensus percentage ignores gapped
+   * sequences
+   */
+  @Test(groups = "Functional")
+  public void testFindColour_ignoreGaps()
+  {
+    /*
+     * CCC
+     * CCC
+     * -CC
+     * first column is 66% C (blue) including gaps
+     * or 100% C ignoring gaps
+     */
+    String fasta = ">seq1\nCCC\n>seq2\nccc\n>seq3\n-CC\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(fasta,
+            DataSourceType.PASTE);
+    AlignmentI al = af.getViewport().getAlignment();
+    ClustalxColourScheme cs = new ClustalxColourScheme(al, null);
+  
+    /*
+     * column 1 is 66% C which is above Clustalx threshold of 60%
+     */
+    Color clustalBlue = ClustalxColourScheme.ClustalColour.BLUE.colour;
+    assertEquals(cs.findColour('C', 0, al.getSequenceAt(0)), clustalBlue);
+
+    /*
+     * set directly to ignore gaps
+     */
+    cs.setIncludeGaps(false);
+    Color clustalPink = ClustalxColourScheme.ClustalColour.PINK.colour;
+    assertEquals(cs.findColour('C', 0, al.getSequenceAt(0)), clustalPink);
+
+    /*
+     * set ignore gaps on the viewport...
+     */
+    cs.setIncludeGaps(true);
+    assertEquals(cs.findColour('C', 0, al.getSequenceAt(0)), clustalBlue);
+    af.getViewport().setIgnoreGapsConsensus(true, af.alignPanel);
+    // next test fails: colour scheme does not read ignore gaps flag from
+    // viewport
+    // assertEquals(cs.findColour('C', 0, al.getSequenceAt(0)), clustalPink);
+  }
+}
diff --git a/test/jalview/schemes/ColourSchemePropertyTest.java b/test/jalview/schemes/ColourSchemePropertyTest.java
new file mode 100644 (file)
index 0000000..11562b8
--- /dev/null
@@ -0,0 +1,135 @@
+package jalview.schemes;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+
+public class ColourSchemePropertyTest
+{
+  @Test(groups = "Functional")
+  public void testGetColourName()
+  {
+    SequenceI seq = new Sequence("Seq1", "abcd");
+    AlignmentI al = new Alignment(new SequenceI[] { seq });
+    ColourSchemeI cs = new ClustalxColourScheme(al, null);
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Clustal");
+    cs = new Blosum62ColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Blosum62");
+    cs = new PIDColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "% Identity");
+    cs = new HydrophobicColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Hydrophobic");
+    cs = new ZappoColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Zappo");
+    cs = new TaylorColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Taylor");
+    cs = new HelixColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs),
+            "Helix Propensity");
+    cs = new StrandColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs),
+            "Strand Propensity");
+    cs = new TurnColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Turn Propensity");
+    cs = new BuriedColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Buried Index");
+    cs = new NucleotideColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "Nucleotide");
+    cs = new PurinePyrimidineColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs),
+            "Purine/Pyrimidine");
+    cs = new TCoffeeColourScheme(al);
+    assertEquals(ColourSchemeProperty.getColourName(cs), "T-Coffee Scores");
+    cs = new RNAHelicesColour(al);
+    assertEquals(ColourSchemeProperty.getColourName(cs), "RNA Helices");
+    cs = new RNAInteractionColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs),
+            "RNA Interaction type");
+    cs = new UserColourScheme();
+    assertEquals(ColourSchemeProperty.getColourName(cs), "User Defined");
+
+    /*
+     * UserColourScheme may have a bespoke name
+     */
+    ((UserColourScheme) cs).setName("stripy");
+    assertEquals(ColourSchemeProperty.getColourName(cs), "stripy");
+    ((UserColourScheme) cs).setName("");
+    assertEquals(ColourSchemeProperty.getColourName(cs), "User Defined");
+    ((UserColourScheme) cs).setName(null);
+    assertEquals(ColourSchemeProperty.getColourName(cs), "User Defined");
+
+    assertEquals(ColourSchemeProperty.getColourName(null), "None");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColourScheme()
+  {
+    SequenceI seq = new Sequence("Seq1", "abcd");
+    AlignmentI al = new Alignment(new SequenceI[] { seq });
+    // the strings here correspond to JalviewColourScheme.toString() values
+    ColourSchemeI cs = ColourSchemeProperty.getColourScheme(al, "Clustal");
+    assertTrue(cs instanceof ClustalxColourScheme);
+    // not case-sensitive
+    cs = ColourSchemeProperty.getColourScheme(al, "CLUSTAL");
+    assertTrue(cs instanceof ClustalxColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "clustal");
+    assertTrue(cs instanceof ClustalxColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Blosum62");
+    assertTrue(cs instanceof Blosum62ColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "% Identity");
+    assertTrue(cs instanceof PIDColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Zappo");
+    assertTrue(cs instanceof ZappoColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Taylor");
+    assertTrue(cs instanceof TaylorColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Hydrophobic");
+    assertTrue(cs instanceof HydrophobicColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Helix Propensity");
+    assertTrue(cs instanceof HelixColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Strand Propensity");
+    assertTrue(cs instanceof StrandColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Turn Propensity");
+    assertTrue(cs instanceof TurnColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Buried Index");
+    assertTrue(cs instanceof BuriedColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Nucleotide");
+    assertTrue(cs instanceof NucleotideColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "Purine/Pyrimidine");
+    assertTrue(cs instanceof PurinePyrimidineColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "T-Coffee Scores");
+    assertTrue(cs instanceof TCoffeeColourScheme);
+    cs = ColourSchemeProperty.getColourScheme(al, "RNA Helices");
+    assertTrue(cs instanceof RNAHelicesColour);
+    // 'None' is a special value
+    assertNull(ColourSchemeProperty.getColourScheme(al, "None"));
+    assertNull(ColourSchemeProperty.getColourScheme(al, "none"));
+    // default is to convert the name into a fixed colour
+    cs = ColourSchemeProperty.getColourScheme(al, "elephants");
+    assertTrue(cs instanceof UserColourScheme);
+
+    /*
+     * explicit aa colours
+     */
+    UserColourScheme ucs = (UserColourScheme) ColourSchemeProperty
+            .getColourScheme(al,
+            "R,G=red;C=blue;c=green;Q=10,20,30;S,T=11ffdd");
+    assertEquals(ucs.findColour('H'), Color.white);
+    assertEquals(ucs.findColour('R'), Color.red);
+    assertEquals(ucs.findColour('r'), Color.red);
+    assertEquals(ucs.findColour('G'), Color.red);
+    assertEquals(ucs.findColour('C'), Color.blue);
+    assertEquals(ucs.findColour('c'), Color.green);
+    assertEquals(ucs.findColour('Q'), new Color(10, 20, 30));
+    assertEquals(ucs.findColour('S'), new Color(0x11ffdd));
+    assertEquals(ucs.findColour('T'), new Color(0x11ffdd));
+  }
+}
diff --git a/test/jalview/schemes/ColourSchemesTest.java b/test/jalview/schemes/ColourSchemesTest.java
new file mode 100644 (file)
index 0000000..0aaa38c
--- /dev/null
@@ -0,0 +1,299 @@
+package jalview.schemes;
+
+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.bin.Cache;
+import jalview.bin.Jalview;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.SequenceRenderer;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.schemes.ClustalxColourScheme.ClustalColour;
+
+import java.awt.Color;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class ColourSchemesTest
+{
+  /*
+   * a colour scheme that alternates Taylor and Zappo
+   * colouring by column
+   */
+  class Stripy extends ResidueColourScheme
+  {
+    private ResidueColourScheme odd;
+
+    private ResidueColourScheme even;
+
+    private Stripy()
+    {
+    }
+
+    /**
+     * constructor given colours for odd and even columns
+     * 
+     * @param odd
+     * @param even
+     */
+    private Stripy(ColourSchemeI cs1, ColourSchemeI cs2)
+    {
+      odd = (ResidueColourScheme) cs1;
+      even = (ResidueColourScheme) cs2;
+    }
+
+    @Override
+    public ColourSchemeI getInstance(AnnotatedCollectionI sg,
+            Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
+    {
+      final ColourSchemeI cs1 = ColourSchemes.getInstance()
+              .getColourScheme(JalviewColourScheme.Taylor.toString(),
+                      (AnnotatedCollectionI) null);
+      final ColourSchemeI cs2 = ColourSchemes.getInstance()
+              .getColourScheme(JalviewColourScheme.Zappo.toString(),
+                      (AnnotatedCollectionI) null);
+      return new Stripy(cs1, cs2);
+    }
+
+    @Override
+    public Color findColour(char c, int j, SequenceI seq)
+    {
+      if (j % 2 == 1)
+      {
+        return odd.findColour(c, j, seq);
+      }
+      else
+      {
+        return even.findColour(c, j, seq);
+      }
+    }
+
+    @Override
+    public String getSchemeName()
+    {
+      return "stripy";
+    }
+  };
+  
+  /*
+   * a colour scheme that is Clustal but using AWT colour equivalents
+   */
+  class MyClustal extends ResidueColourScheme
+  {
+    ClustalxColourScheme delegate;
+
+    private MyClustal()
+    {
+    }
+
+    private MyClustal(AnnotatedCollectionI sg,
+            Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
+    {
+      delegate = new ClustalxColourScheme(sg, hiddenRepSequences);
+    }
+
+    @Override
+    public Color findColour(char c, int j, SequenceI seq)
+    {
+      Color col = delegate.findColour(c, j, seq);
+      Color result = col;
+      if (col.equals(ClustalColour.BLUE.colour))
+      {
+        result = Color.blue;
+      }
+      else if (col.equals(ClustalColour.CYAN.colour))
+      {
+        result = Color.cyan;
+      }
+      else if (col.equals(ClustalColour.GREEN.colour))
+      {
+        result = Color.green;
+      }
+      else if (col.equals(ClustalColour.MAGENTA.colour))
+      {
+        result = Color.magenta;
+      }
+      else if (col.equals(ClustalColour.ORANGE.colour))
+      {
+        result = Color.orange;
+      }
+      else if (col.equals(ClustalColour.PINK.colour))
+      {
+        result = Color.pink;
+      }
+      else if (col.equals(ClustalColour.RED.colour))
+      {
+        result = Color.red;
+      }
+      else if (col.equals(ClustalColour.YELLOW.colour))
+      {
+        result = Color.yellow;
+      }
+      return result;
+    }
+
+    @Override
+    public ColourSchemeI getInstance(AnnotatedCollectionI sg,
+            Map<SequenceI, SequenceCollectionI> hiddenRepSequences)
+    {
+      return new MyClustal(sg,              hiddenRepSequences);
+    }
+
+    @Override
+    public String getSchemeName()
+    {
+      return "MyClustal";
+    }
+
+  }
+
+  @BeforeClass(alwaysRun = true)
+  public static void setUpBeforeClass() throws Exception
+  {
+    /*
+     * use read-only test properties file
+     */
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Jalview.main(new String[] { "-nonews" });
+  }
+
+  @AfterClass(alwaysRun = true)
+  public static void tearDownAfterClass() throws Exception
+  {
+    Desktop.instance.closeAll_actionPerformed(null);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColourSchemes()
+  {
+    /*
+     * this just verifies that built-in colour schemes are loaded into ColourSchemes
+     * in the order in which they are declared in the JalviewColourScheme enum
+     * (this also determines their order in Colour menus)
+     */
+    Iterator<ColourSchemeI> schemes = ColourSchemes.getInstance().getColourSchemes().iterator();
+    JalviewColourScheme[] jalviewSchemes = JalviewColourScheme.values();
+    int i = 0;
+    while (schemes.hasNext() && i < jalviewSchemes.length)
+    {
+      assertTrue(schemes.next().getSchemeName()
+              .equals(jalviewSchemes[i].toString()));
+      i++;
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColourScheme()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            ">seq1\nAGLRTWQU", DataSourceType.PASTE);
+    ColourSchemes schemes = ColourSchemes.getInstance();
+
+    AnnotatedCollectionI al = af.getViewport().getAlignment();
+
+    for (JalviewColourScheme cs : JalviewColourScheme.values())
+    {
+      ColourSchemeI registered = schemes.getColourScheme(cs.toString(), al);
+      assertSame(registered.getClass(), cs.getSchemeClass());
+    }
+    af.closeMenuItem_actionPerformed(true);
+  }
+  
+  @Test(groups = "Functional")
+  public void testRegisterColourScheme()
+  {
+    ColourSchemes.getInstance().registerColourScheme(new Stripy());
+    ColourSchemes.getInstance().registerColourScheme(new MyClustal());
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    /*
+     * set a breakpoint here to see and play with the newly registered
+     *  colour schemes in the AlignFrame colour menu
+     */
+    SequenceRenderer sr = new SequenceRenderer(af.getViewport());
+    SequenceI seq = af.getViewport().getAlignment().findName("FER_CAPAA");
+
+    /*
+     * set and check Taylor colours
+     */
+    af.changeColour_actionPerformed(JalviewColourScheme.Taylor.toString());
+    Color taylor1 = sr.getResidueColour(seq, 88, null); // E 255,0,102
+    Color taylor2 = sr.getResidueColour(seq, 89, null); // A 204,255,0
+    Color taylor3 = sr.getResidueColour(seq, 90, null); // G 255,153,0
+    assertEquals(taylor1, new Color(255, 0, 102));
+    assertEquals(taylor2, new Color(204, 255, 0));
+    assertEquals(taylor3, new Color(255, 153, 0));
+
+    /*
+     * set and check Zappo colours
+     */
+    af.changeColour_actionPerformed(JalviewColourScheme.Zappo.toString());
+    Color zappo1 = sr.getResidueColour(seq, 88, null); // E red
+    Color zappo2 = sr.getResidueColour(seq, 89, null); // A pink
+    Color zappo3 = sr.getResidueColour(seq, 90, null); // G magenta
+    assertEquals(zappo1, Color.red);
+    assertEquals(zappo2, Color.pink);
+    assertEquals(zappo3, Color.magenta);
+
+    /*
+     * set 'stripy' colours - odd columns are Taylor and even are Zappo 
+     */
+    af.changeColour_actionPerformed("stripy");
+    Color stripy1 = sr.getResidueColour(seq, 88, null);
+    Color stripy2 = sr.getResidueColour(seq, 89, null);
+    Color stripy3 = sr.getResidueColour(seq, 90, null);
+    assertEquals(stripy1, zappo1);
+    assertEquals(stripy2, taylor2);
+    assertEquals(stripy3, zappo3);
+
+    /*
+     * set and check Clustal colours
+     */
+    af.changeColour_actionPerformed(JalviewColourScheme.Clustal.toString());
+    Color clustal1 = sr.getResidueColour(seq, 88, null);
+    Color clustal2 = sr.getResidueColour(seq, 89, null);
+    Color clustal3 = sr.getResidueColour(seq, 90, null);
+    assertEquals(clustal1, ClustalColour.MAGENTA.colour);
+    assertEquals(clustal2, ClustalColour.BLUE.colour);
+    assertEquals(clustal3, ClustalColour.ORANGE.colour);
+
+    /*
+     * set 'MyClustal' colours - uses AWT colour equivalents
+     */
+    af.changeColour_actionPerformed("MyClustal");
+    Color myclustal1 = sr.getResidueColour(seq, 88, null);
+    Color myclustal2 = sr.getResidueColour(seq, 89, null);
+    Color myclustal3 = sr.getResidueColour(seq, 90, null);
+    assertEquals(myclustal1, Color.MAGENTA);
+    assertEquals(myclustal2, Color.BLUE);
+    assertEquals(myclustal3, Color.ORANGE);
+  }
+
+  /**
+   * Tests for check if scheme name exists. Built-in scheme names are the
+   * toString() values of enum JalviewColourScheme.
+   */
+  @Test(groups = "Functional")
+  public void testNameExists()
+  {
+    ColourSchemes cs = ColourSchemes.getInstance();
+    assertFalse(cs.nameExists(null));
+    assertFalse(cs.nameExists(""));
+    assertTrue(cs.nameExists("Clustal"));
+    assertTrue(cs.nameExists("CLUSTAL"));
+    assertFalse(cs.nameExists("CLUSTAL "));
+    assertTrue(cs.nameExists("% Identity"));
+    assertFalse(cs.nameExists("PID"));
+  }
+}
index 47613a1..1beca80 100644 (file)
@@ -27,6 +27,7 @@ import static org.testng.AssertJUnit.fail;
 
 import jalview.datamodel.SequenceFeature;
 import jalview.gui.JvOptionPane;
+import jalview.util.ColorUtils;
 import jalview.util.Format;
 
 import java.awt.Color;
@@ -86,7 +87,8 @@ public class FeatureColourTest
   public void testIsColored_simpleColour()
   {
     FeatureColour fc = new FeatureColour(Color.RED);
-    assertTrue(fc.isColored(new SequenceFeature()));
+    assertTrue(fc
+            .isColored(new SequenceFeature("Cath", "", 1, 2, 0f, null)));
   }
 
   @Test(groups = { "Functional" })
@@ -94,7 +96,8 @@ public class FeatureColourTest
   {
     FeatureColour fc = new FeatureColour();
     fc.setColourByLabel(true);
-    assertTrue(fc.isColored(new SequenceFeature()));
+    assertTrue(fc
+            .isColored(new SequenceFeature("Cath", "", 1, 2, 0f, null)));
   }
 
   @Test(groups = { "Functional" })
@@ -111,13 +114,15 @@ public class FeatureColourTest
     assertEquals(Color.WHITE, fc.getColor(sf));
 
     // score 120 is adjusted to top of range
-    sf.setScore(120f);
+    sf = new SequenceFeature(sf, sf.getBegin(), sf.getEnd(),
+            sf.getFeatureGroup(), 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);
+    sf = new SequenceFeature(sf, sf.getBegin(), sf.getEnd(),
+            sf.getFeatureGroup(), 36f);
     assertTrue(fc.isColored(sf));
     assertEquals(new Color(204, 204, 204), fc.getColor(sf));
 
@@ -127,7 +132,9 @@ public class FeatureColourTest
     // colour is still returned though ?!?
     assertEquals(new Color(204, 204, 204), fc.getColor(sf));
 
-    sf.setScore(84); // above threshold now
+    sf = new SequenceFeature(sf, sf.getBegin(), sf.getEnd(),
+            sf.getFeatureGroup(), 84f);
+    // above threshold now
     assertTrue(fc.isColored(sf));
     assertEquals(new Color(51, 51, 51), fc.getColor(sf));
   }
@@ -136,7 +143,8 @@ public class FeatureColourTest
   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" })
@@ -146,7 +154,7 @@ public class FeatureColourTest
     fc.setColourByLabel(true);
     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
             null);
-    Color expected = UserColourScheme.createColourFromName("desc");
+    Color expected = ColorUtils.createColourFromName("desc");
     assertEquals(expected, fc.getColor(sf));
   }
 
diff --git a/test/jalview/schemes/JalviewColourSchemeTest.java b/test/jalview/schemes/JalviewColourSchemeTest.java
new file mode 100644 (file)
index 0000000..d9403d2
--- /dev/null
@@ -0,0 +1,48 @@
+package jalview.schemes;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+public class JalviewColourSchemeTest
+{
+  @Test(groups = "Functional")
+  public void testGetSchemeClass()
+  {
+    assertTrue(JalviewColourScheme.Clustal.getSchemeClass() == ClustalxColourScheme.class);
+    assertTrue(JalviewColourScheme.Blosum62.getSchemeClass() == Blosum62ColourScheme.class);
+    assertTrue(JalviewColourScheme.PID.getSchemeClass() == PIDColourScheme.class);
+    assertTrue(JalviewColourScheme.Hydrophobic.getSchemeClass() == HydrophobicColourScheme.class);
+    assertTrue(JalviewColourScheme.Zappo.getSchemeClass() == ZappoColourScheme.class);
+    assertTrue(JalviewColourScheme.Taylor.getSchemeClass() == TaylorColourScheme.class);
+    assertTrue(JalviewColourScheme.Helix.getSchemeClass() == HelixColourScheme.class);
+    assertTrue(JalviewColourScheme.Strand.getSchemeClass() == StrandColourScheme.class);
+    assertTrue(JalviewColourScheme.Turn.getSchemeClass() == TurnColourScheme.class);
+    assertTrue(JalviewColourScheme.Buried.getSchemeClass() == BuriedColourScheme.class);
+    assertTrue(JalviewColourScheme.Nucleotide.getSchemeClass() == NucleotideColourScheme.class);
+    assertTrue(JalviewColourScheme.PurinePyrimidine.getSchemeClass() == PurinePyrimidineColourScheme.class);
+    assertTrue(JalviewColourScheme.TCoffee.getSchemeClass() == TCoffeeColourScheme.class);
+    assertTrue(JalviewColourScheme.RNAHelices.getSchemeClass() == RNAHelicesColour.class);
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    assertEquals(JalviewColourScheme.Clustal.toString(), "Clustal");
+    assertEquals(JalviewColourScheme.Blosum62.toString(), "Blosum62");
+    assertEquals(JalviewColourScheme.PID.toString(), "% Identity");
+    assertEquals(JalviewColourScheme.Zappo.toString(), "Zappo");
+    assertEquals(JalviewColourScheme.Taylor.toString(), "Taylor");
+    assertEquals(JalviewColourScheme.Hydrophobic.toString(), "Hydrophobic");
+    assertEquals(JalviewColourScheme.Helix.toString(), "Helix Propensity");
+    assertEquals(JalviewColourScheme.Strand.toString(), "Strand Propensity");
+    assertEquals(JalviewColourScheme.Turn.toString(), "Turn Propensity");
+    assertEquals(JalviewColourScheme.Buried.toString(), "Buried Index");
+    assertEquals(JalviewColourScheme.Nucleotide.toString(), "Nucleotide");
+    assertEquals(JalviewColourScheme.PurinePyrimidine.toString(),
+            "Purine/Pyrimidine");
+    assertEquals(JalviewColourScheme.TCoffee.toString(), "T-Coffee Scores");
+    assertEquals(JalviewColourScheme.RNAHelices.toString(), "RNA Helices");
+  }
+}
diff --git a/test/jalview/schemes/PIDColourSchemeTest.java b/test/jalview/schemes/PIDColourSchemeTest.java
new file mode 100644 (file)
index 0000000..fa4b5d9
--- /dev/null
@@ -0,0 +1,119 @@
+package jalview.schemes;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.AlignViewport;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+
+public class PIDColourSchemeTest
+{
+  static final Color white = Color.white;
+
+  static final Color over40 = new Color(204, 204, 255);
+
+  static final Color over60 = new Color(153, 153, 255);
+
+  static final Color over80 = new Color(100, 100, 255);
+
+  /**
+   * Test findColour for cases:
+   * <ul>
+   * <li>gap: white</li>
+   * <li>no match to consensus: white</li>
+   * <li>match consensus with pid > 80%: 100,100,255</li>
+   * <li>match consensus with pid > 60%: 153, 153, 255</li>
+   * <li>match consensus with pid > 40%: 204, 204, 255</li>
+   * <li>match consensus with pid <= 40%: white</li>
+   * <li>joint consensus matching</li>
+   * <li>case insensitive matching</li>
+   * <ul>
+   */
+  @Test(groups = "Functional")
+  public void testFindColour()
+  {
+    ColourSchemeI scheme = new PIDColourScheme();
+
+    /*
+     * doesn't use column or sequence
+     * we assume consensus residue is computed as upper case
+     */
+    assertEquals(scheme.findColour('A', 0, null, "A", 0f), white);
+    assertEquals(scheme.findColour('A', 0, null, "A", 40f), white);
+    assertEquals(scheme.findColour('A', 0, null, "A", 40.1f), over40);
+    assertEquals(scheme.findColour('A', 0, null, "A", 60f), over40);
+    assertEquals(scheme.findColour('A', 0, null, "A", 60.1f), over60);
+    assertEquals(scheme.findColour('A', 0, null, "A", 80f), over60);
+    assertEquals(scheme.findColour('A', 0, null, "A", 80.1f), over80);
+    assertEquals(scheme.findColour('A', 0, null, "A", 100f), over80);
+    assertEquals(scheme.findColour('A', 0, null, "KFV", 100f), white);
+
+    assertEquals(scheme.findColour('a', 0, null, "A", 80f), over60);
+    assertEquals(scheme.findColour('A', 0, null, "AC", 80f), over60);
+    assertEquals(scheme.findColour('A', 0, null, "KCA", 80f), over60);
+  }
+
+  /**
+   * Test that changing the 'ignore gaps in consensus' in the viewport (an
+   * option on the annotation label popup menu) results in a change to the
+   * colouring
+   */
+  @Test(groups = "Functional")
+  public void testFindColour_ignoreGaps()
+  {
+    /*
+     * AAAAA
+     * AAAAA
+     * -CCCC
+     * FFFFF
+     * 
+     * first column consensus is A
+     * first column PID is 50%, or 67% ignoring gaps
+     */
+    String seqs = ">seq1\nAAAAA\n>seq2\nAAAAA\n>seq3\n-CCCC\n>seq4\nFFFFF\n";
+
+    /*
+     * load data and wait for consensus to be computed
+     */
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqs,
+            DataSourceType.PASTE);
+    AlignViewport viewport = af.getViewport();
+    viewport.setIgnoreGapsConsensus(false, af.alignPanel);
+    while (viewport.getConsensusSeq() == null)
+    {
+      synchronized (this)
+      {
+        try
+        {
+          wait(50);
+        } catch (InterruptedException e)
+        {
+        }
+      }
+    }
+    af.changeColour_actionPerformed(JalviewColourScheme.PID.toString());
+
+    SequenceI seq = viewport.getAlignment().getSequenceAt(0);
+
+    /*
+     * including gaps, A should be coloured for 50% consensus
+     */
+    Color c = viewport
+            .getResidueShading().findColour('A', 0, seq);
+    assertEquals(c, over40);
+
+    /*
+     * now choose to ignore gaps; colour should be for 67%
+     */
+    viewport.setIgnoreGapsConsensus(true, af.alignPanel);
+    c = viewport
+            .getResidueShading().findColour('A', 0, seq);
+    assertEquals(c, over60);
+  }
+}
index d3a4fff..b45e0d3 100644 (file)
@@ -24,18 +24,26 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
-import jalview.datamodel.Profile;
-import jalview.datamodel.ProfileI;
-import jalview.datamodel.Profiles;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AnnotatedCollectionI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
-
-import java.awt.Color;
+import jalview.io.TCoffeeScoreFile;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 public class ResidueColourSchemeTest
 {
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+
+  }
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -45,135 +53,149 @@ public class ResidueColourSchemeTest
   }
 
   @Test(groups = "Functional")
-  public void testAboveThreshold()
+  public void testIsApplicableTo()
   {
+    SequenceI pep1 = new Sequence("pep1", "APQTWLS");
+    SequenceI pep2 = new Sequence("pep2", "AILFQYG");
+    SequenceI dna1 = new Sequence("dna1", "ACTGAC");
+    SequenceI dna2 = new Sequence("dna2", "TCCAAG");
+    AlignmentI peptide = new Alignment(new SequenceI[] { pep1, pep2 });
+    AlignmentI nucleotide = new Alignment(new SequenceI[] { dna1, dna2 });
+
     /*
-     * make up profiles for this alignment:
-     * AR-Q
-     * AR--
-     * SR-T
-     * SR-T
+     * peptide-specific colour schemes
      */
-    ProfileI[] profiles = new ProfileI[4];
-    profiles[0] = new Profile(4, 0, 2, "AS");
-    profiles[1] = new Profile(4, 0, 4, "R");
-    profiles[2] = new Profile(4, 4, 0, "");
-    profiles[3] = new Profile(4, 1, 2, "T");
-    ResidueColourScheme rcs = new ResidueColourScheme();
-    rcs.setConsensus(new Profiles(profiles));
-    
+    assertTrue(new ClustalxColourScheme(peptide, null)
+            .isApplicableTo(peptide));
+    assertFalse(new ClustalxColourScheme(nucleotide, null)
+            .isApplicableTo(nucleotide));
+    assertTrue(new Blosum62ColourScheme().isApplicableTo(peptide));
+    assertFalse(new Blosum62ColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new BuriedColourScheme().isApplicableTo(peptide));
+    assertFalse(new BuriedColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new HelixColourScheme().isApplicableTo(peptide));
+    assertFalse(new HelixColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new HydrophobicColourScheme().isApplicableTo(peptide));
+    assertFalse(new HydrophobicColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new StrandColourScheme().isApplicableTo(peptide));
+    assertFalse(new StrandColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new TaylorColourScheme().isApplicableTo(peptide));
+    assertFalse(new TaylorColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new TurnColourScheme().isApplicableTo(peptide));
+    assertFalse(new TurnColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new ZappoColourScheme().isApplicableTo(peptide));
+    assertFalse(new ZappoColourScheme().isApplicableTo(nucleotide));
+
+    /*
+     * nucleotide-specific colour schemes
+     */
+    assertFalse(new NucleotideColourScheme().isApplicableTo(peptide));
+    assertTrue(new NucleotideColourScheme().isApplicableTo(nucleotide));
+    assertFalse(new PurinePyrimidineColourScheme().isApplicableTo(peptide));
+    assertTrue(new PurinePyrimidineColourScheme()
+            .isApplicableTo(nucleotide));
+    assertFalse(new RNAInteractionColourScheme().isApplicableTo(peptide));
+    assertTrue(new RNAInteractionColourScheme().isApplicableTo(nucleotide));
+
     /*
-     * no threshold
+     * indifferent
      */
-    rcs.setThreshold(0, true);
-    assertTrue(rcs.aboveThreshold('a', 0));
-    assertTrue(rcs.aboveThreshold('S', 0));
-    assertFalse(rcs.aboveThreshold('W', 0));
-    assertTrue(rcs.aboveThreshold('R', 1));
-    assertFalse(rcs.aboveThreshold('W', 2));
-    assertTrue(rcs.aboveThreshold('t', 3));
-    assertFalse(rcs.aboveThreshold('Q', 3));
+    assertTrue(new UserColourScheme().isApplicableTo(peptide));
+    assertTrue(new UserColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new ScoreColourScheme(new int[] {}, new double[] {}, 0, 0d)
+            .isApplicableTo(peptide));
+    assertTrue(new ScoreColourScheme(new int[] {}, new double[] {}, 0, 0d)
+            .isApplicableTo(nucleotide));
+    ResidueColourScheme rcs = new PIDColourScheme();
+    assertTrue(rcs.isApplicableTo(peptide));
+    assertTrue(rcs.isApplicableTo(nucleotide));
+    assertTrue(new PIDColourScheme().isApplicableTo(peptide));
+    assertTrue(new PIDColourScheme().isApplicableTo(nucleotide));
+    assertTrue(new FollowerColourScheme().isApplicableTo(peptide));
+    assertTrue(new FollowerColourScheme().isApplicableTo(nucleotide));
 
     /*
-     * with threshold, include gaps
+     * TCoffee colour requires the presence of TCoffee score annotation
      */
-    rcs.setThreshold(60, false);
-    assertFalse(rcs.aboveThreshold('a', 0));
-    assertFalse(rcs.aboveThreshold('S', 0));
-    assertTrue(rcs.aboveThreshold('R', 1));
-    assertFalse(rcs.aboveThreshold('W', 2));
-    assertFalse(rcs.aboveThreshold('t', 3)); // 50% < 60%
+    assertFalse(new TCoffeeColourScheme(peptide).isApplicableTo(peptide));
+    assertFalse(new TCoffeeColourScheme(nucleotide)
+            .isApplicableTo(nucleotide));
+    AlignmentAnnotation aa = new AlignmentAnnotation("T-COFFEE", "", null);
+    aa.setCalcId(TCoffeeScoreFile.TCOFFEE_SCORE);
+    peptide.addAnnotation(aa);
+    aa = new AlignmentAnnotation("T-COFFEE", "", null);
+    aa.setCalcId(TCoffeeScoreFile.TCOFFEE_SCORE);
+    nucleotide.addAnnotation(aa);
+    assertTrue(new TCoffeeColourScheme(peptide).isApplicableTo(peptide));
+    assertTrue(new TCoffeeColourScheme(nucleotide)
+            .isApplicableTo(nucleotide));
 
     /*
-     * with threshold, ignore gaps
+     * RNAHelices requires the presence of rna secondary structure
      */
-    rcs.setThreshold(60, true);
-    assertFalse(rcs.aboveThreshold('a', 0));
-    assertFalse(rcs.aboveThreshold('S', 0));
-    assertTrue(rcs.aboveThreshold('R', 1));
-    assertFalse(rcs.aboveThreshold('W', 2));
-    assertTrue(rcs.aboveThreshold('t', 3)); // 67% > 60%
+    assertFalse(new RNAHelicesColour(peptide).isApplicableTo(peptide));
+    assertFalse(new RNAHelicesColour(nucleotide).isApplicableTo(nucleotide));
+    // add secondary structure (small but perfectly formed)
+    Annotation[] ss = new Annotation[2];
+    ss[0] = new Annotation("", "", '{', 0f);
+    ss[1] = new Annotation("", "", '}', 0f);
+    nucleotide.addAnnotation(new AlignmentAnnotation("SS", "", ss));
+    assertTrue(new RNAHelicesColour(nucleotide).isApplicableTo(nucleotide));
   }
 
-  /**
-   * Test colour bleaching based on conservation score and conservation slider.
-   * Scores of 10 or 11 should leave colours unchanged. Gap is always white.
-   */
   @Test(groups = "Functional")
-  public void testApplyConservation()
+  public void testIsApplicableTo_dynamicColourScheme()
   {
-    ResidueColourScheme rcs = new ResidueColourScheme();
-
-    // no conservation present - no fading
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 12));
-    
-    // cheat by setting conservation sequence directly
-    // rather than calculating it - good enough for this test
-    String consensus = "0123456789+*-";
-    rcs.conservation = consensus.toCharArray();
-
-    // column out of range:
-    assertEquals(Color.RED,
-            rcs.applyConservation(Color.RED, consensus.length()));
-
+    SequenceI pep1 = new Sequence("pep1", "APQTWLS");
+    SequenceI pep2 = new Sequence("pep2", "AILFQYG");
+    AlignmentI peptide = new Alignment(new SequenceI[] { pep1, pep2 });
+  
     /*
-     * with 100% threshold, 'fade factor' is 
-     * (11-score)/10 * 100/20 = (11-score)/2
-     * which is >= 1 for all scores i.e. all fade to white except +, *
+     * demonstrate that we can 'plug in' a colour scheme with specified
+     * criteria for applicability; here, that there are more than 2 sequences
      */
-    rcs.setConservationInc(100);
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 0));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 1));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 2));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 3));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 4));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 5));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 6));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 7));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 8));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 9));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 10));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 11));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 12));
+    ColourSchemeI cs = new UserColourScheme()
+    {
+      @Override
+      public boolean isApplicableTo(AnnotatedCollectionI ac)
+      {
+        AlignmentI al = ac.getContext() == null ? (AlignmentI) ac
+                : (AlignmentI) ac.getContext();
+        return al.getSequences().size() > 2;
+      }
+    };
+    assertFalse(cs.isApplicableTo(peptide));
+    peptide.addSequence(pep1);
+    assertTrue(cs.isApplicableTo(peptide));
+  }
 
-    /*
-     * with 0% threshold, there should be no fading
-     */
-    rcs.setConservationInc(0);
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 0));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 1));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 2));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 3));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 4));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 5));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 6));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 7));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 8));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 9));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 10));
-    assertEquals(Color.RED, rcs.applyConservation(Color.RED, 11));
-    assertEquals(Color.WHITE, rcs.applyConservation(Color.RED, 12)); // gap
+  @Test(groups = "Functional")
+  public void testGetName()
+  {
+    SequenceI pep1 = new Sequence("pep1", "APQTWLS");
+    AlignmentI peptide = new Alignment(new SequenceI[] { pep1 });
 
-    /*
-     * with 40% threshold, 'fade factor' is 
-     * (11-score)/10 * 40/20 = (11-score)/5
-     * which is {>1, >1, >1, >1, >1, >1, 1, 0.8, 0.6, 0.4} for score 0-9
-     * e.g. score 7 colour fades 80% of the way to white (255, 255, 255)
-     */
-    rcs.setConservationInc(40);
-    Color colour = new Color(155, 105, 55);
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 0));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 1));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 2));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 3));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 4));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 5));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 6));
-    assertEquals(new Color(235, 225, 215), rcs.applyConservation(colour, 7));
-    assertEquals(new Color(215, 195, 175), rcs.applyConservation(colour, 8));
-    assertEquals(new Color(195, 165, 135), rcs.applyConservation(colour, 9));
-    assertEquals(colour, rcs.applyConservation(colour, 10));
-    assertEquals(colour, rcs.applyConservation(colour, 11));
-    assertEquals(Color.WHITE, rcs.applyConservation(colour, 12));
+    assertEquals("Blosum62", new Blosum62ColourScheme().getSchemeName());
+    assertEquals("Buried Index", new BuriedColourScheme().getSchemeName());
+    assertEquals("Helix Propensity", new HelixColourScheme().getSchemeName());
+    assertEquals("Hydrophobic", new HydrophobicColourScheme().getSchemeName());
+    assertEquals("Strand Propensity", new StrandColourScheme().getSchemeName());
+    assertEquals("Taylor", new TaylorColourScheme().getSchemeName());
+    assertEquals("Turn Propensity", new TurnColourScheme().getSchemeName());
+    assertEquals("Zappo", new ZappoColourScheme().getSchemeName());
+    assertEquals("Nucleotide", new NucleotideColourScheme().getSchemeName());
+    assertEquals("Purine/Pyrimidine",
+            new PurinePyrimidineColourScheme().getSchemeName());
+    assertEquals("RNA Interaction type",
+            new RNAInteractionColourScheme().getSchemeName());
+    assertEquals("User Defined", new UserColourScheme().getSchemeName());
+    assertEquals("Score", new ScoreColourScheme(new int[] {},
+            new double[] {}, 0, 0d).getSchemeName());
+    assertEquals("% Identity", new PIDColourScheme().getSchemeName());
+    assertEquals("Follower", new FollowerColourScheme().getSchemeName());
+    assertEquals("T-Coffee Scores",
+            new TCoffeeColourScheme(peptide).getSchemeName());
+    assertEquals("RNA Helices",
+            new RNAHelicesColour(peptide).getSchemeName());
   }
 }
diff --git a/test/jalview/schemes/ScoreMatrixPrinter.java b/test/jalview/schemes/ScoreMatrixPrinter.java
deleted file mode 100644 (file)
index a743163..0000000
+++ /dev/null
@@ -1,67 +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.schemes;
-
-import jalview.api.analysis.ScoreModelI;
-import jalview.gui.JvOptionPane;
-
-import java.util.Map;
-
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-public class ScoreMatrixPrinter
-{
-
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
-  @Test(groups = { "Functional" })
-  public void printAllMatrices()
-  {
-    for (Map.Entry<String, ScoreModelI> sm : ResidueProperties.scoreMatrices
-            .entrySet())
-    {
-      System.out.println("Matrix " + sm.getKey());
-      System.out.println(sm.getValue().toString());
-    }
-  }
-
-  @Test(groups = { "Functional" })
-  public void printHTMLMatrices()
-  {
-    for (Map.Entry<String, ScoreModelI> _sm : ResidueProperties.scoreMatrices
-            .entrySet())
-    {
-      if (_sm.getValue() instanceof ScoreMatrix)
-      {
-        ScoreMatrix sm = (ScoreMatrix) _sm.getValue();
-        System.out.println("Matrix " + _sm.getKey());
-        System.out.println(sm.outputMatrix(true));
-      }
-    }
-  }
-
-}
index 645d5b8..497014e 100644 (file)
@@ -21,8 +21,6 @@
 package jalview.schemes;
 
 import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertNull;
-import static org.testng.AssertJUnit.assertSame;
 
 import jalview.gui.JvOptionPane;
 
@@ -42,40 +40,44 @@ public class UserColourSchemeTest
   }
 
   @Test(groups = "Functional")
-  public void testGetColourFromString()
+  public void testParseAppletParameter()
   {
-    /*
-     * by colour name - if known to AWT, and included in
-     * 
-     * @see ColourSchemeProperty.getAWTColorFromName()
-     */
-    assertSame(Color.RED, UserColourScheme.getColourFromString("red"));
-    assertSame(Color.RED, UserColourScheme.getColourFromString("Red"));
-    assertSame(Color.RED, UserColourScheme.getColourFromString(" RED "));
+    UserColourScheme cs = new UserColourScheme("white");
+    cs.parseAppletParameter("D,E=red; K,R,H=0022FF; c=10 , 20,30");
+    assertEquals(Color.RED, cs.findColour('D'));
+    assertEquals(Color.RED, cs.findColour('d'));
+    assertEquals(Color.RED, cs.findColour('E'));
+    assertEquals(Color.RED, cs.findColour('e'));
+    Color c1 = new Color(0x0022ff);
+    assertEquals(c1, cs.findColour('K'));
+    assertEquals(c1, cs.findColour('R'));
+    assertEquals(c1, cs.findColour('h'));
+    Color c2 = new Color(10, 20, 30);
+    assertEquals(c2, cs.findColour('c'));
 
-    /*
-     * by RGB hex code
-     */
-    String hexColour = Integer.toHexString(Color.RED.getRGB() & 0xffffff);
-    assertEquals(Color.RED, UserColourScheme.getColourFromString(hexColour));
-    // 'hex' prefixes _not_ wanted here
-    assertNull(UserColourScheme.getColourFromString("0x" + hexColour));
-    assertNull(UserColourScheme.getColourFromString("#" + hexColour));
-
-    /*
-     * by RGB triplet
-     */
-    String rgb = String.format("%d,%d,%d", Color.red.getRed(),
-            Color.red.getGreen(), Color.red.getBlue());
-    assertEquals(Color.RED, UserColourScheme.getColourFromString(rgb));
+    cs = new UserColourScheme("white");
+    cs.parseAppletParameter("D,E=red; K,R,H=0022FF; c=10 , 20,30;t=orange;lowercase=blue;s=pink");
+    assertEquals(Color.RED, cs.findColour('D'));
+    assertEquals(Color.blue, cs.findColour('d'));
+    assertEquals(Color.RED, cs.findColour('E'));
+    assertEquals(Color.blue, cs.findColour('e'));
+    assertEquals(c1, cs.findColour('K'));
+    assertEquals(c1, cs.findColour('R'));
+    assertEquals(Color.blue, cs.findColour('h'));
+    assertEquals(c2, cs.findColour('c'));
+    // 'lowercase' sets all lower-case not already set to the given colour
+    assertEquals(Color.orange, cs.findColour('t'));
+    assertEquals(Color.blue, cs.findColour('k'));
+    assertEquals(Color.blue, cs.findColour('a'));
+    assertEquals(Color.pink, cs.findColour('s'));
+  }
 
-    /*
-     * odds and ends
-     */
-    assertNull(UserColourScheme.getColourFromString(null));
-    assertNull(UserColourScheme.getColourFromString("rubbish"));
-    assertEquals(Color.WHITE, UserColourScheme.getColourFromString("-1"));
-    assertNull(UserColourScheme.getColourFromString(String
-            .valueOf(Integer.MAX_VALUE)));
+  @Test(groups = "Functional")
+  public void testToAppletParameter()
+  {
+    UserColourScheme cs = new UserColourScheme(
+            "E,D=red; K,R,H=0022FF; c=10 , 20,30");
+    String param = cs.toAppletParameter();
+    assertEquals("D,E=ff0000;H,K,R=0022ff;c=0a141e", param);
   }
 }
index a7e52ff..a59fbde 100644 (file)
@@ -145,7 +145,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,7 +155,7 @@ 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());
index b74a089..c125ef6 100644 (file)
@@ -24,19 +24,27 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.api.SequenceRenderer;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
+import jalview.schemes.ColourSchemeI;
 import jalview.structure.AtomSpec;
+import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel.SuperposeData;
 
+import java.awt.Color;
 import java.util.Arrays;
+import java.util.BitSet;
 import java.util.List;
 
 import org.testng.annotations.BeforeClass;
@@ -130,7 +138,7 @@ public class AAStructureBindingModelTest
     testee = new AAStructureBindingModel(ssm, pdbFiles, seqs, null)
     {
       @Override
-      public String[] getPdbFile()
+      public String[] getStructureFiles()
       {
         return new String[] { "INLINE1YCS", "INLINE3A6S", "INLINE1OOT" };
       }
@@ -155,6 +163,60 @@ public class AAStructureBindingModelTest
       {
         return null;
       }
+
+      @Override
+      public void setJalviewColourScheme(ColourSchemeI cs)
+      {
+      }
+
+      @Override
+      public String superposeStructures(AlignmentI[] als, int[] alm,
+              HiddenColumns[] alc)
+      {
+        return null;
+      }
+
+      @Override
+      public void setBackgroundColour(Color col)
+      {
+      }
+
+      @Override
+      protected StructureMappingcommandSet[] getColourBySequenceCommands(
+              String[] files, SequenceRenderer sr, AlignmentViewPanel avp)
+      {
+        return null;
+      }
+
+      @Override
+      public SequenceRenderer getSequenceRenderer(
+              AlignmentViewPanel alignment)
+      {
+        return null;
+      }
+
+      @Override
+      protected void colourBySequence(
+              StructureMappingcommandSet[] colourBySequenceCommands)
+      {
+      }
+
+      @Override
+      public void colourByChain()
+      {
+      }
+
+      @Override
+      public void colourByCharge()
+      {
+      }
+
+      @Override
+      public FeatureRenderer getFeatureRenderer(
+              AlignmentViewPanel alignment)
+      {
+        return null;
+      }
     };
   }
 
@@ -168,17 +230,20 @@ public class AAStructureBindingModelTest
     /*
      * create a data bean to hold data per structure file
      */
-    SuperposeData[] structs = new SuperposeData[testee.getPdbFile().length];
+    SuperposeData[] structs = new SuperposeData[testee.getStructureFiles().length];
     for (int i = 0; i < structs.length; i++)
     {
       structs[i] = testee.new SuperposeData(al.getWidth());
     }
     /*
-     * initialise array of 'superposable columns' to true (would be false for
+     * initialise BitSet of 'superposable columns' to true (would be false for
      * hidden columns)
      */
-    boolean[] matched = new boolean[al.getWidth()];
-    Arrays.fill(matched, true);
+    BitSet matched = new BitSet();
+    for (int i = 0; i < al.getWidth(); i++)
+    {
+      matched.set(i);
+    }
 
     int refStructure = testee
             .findSuperposableResidues(al, matched, structs);
@@ -188,12 +253,12 @@ public class AAStructureBindingModelTest
     /*
      * only ungapped, structure-mapped columns are superposable
      */
-    assertFalse(matched[0]); // gap in first sequence
-    assertTrue(matched[1]);
-    assertFalse(matched[2]); // gap in third sequence
-    assertFalse(matched[3]); // gap in fourth sequence
-    assertTrue(matched[4]);
-    assertTrue(matched[5]); // gap in second sequence
+    assertFalse(matched.get(0)); // gap in first sequence
+    assertTrue(matched.get(1));
+    assertFalse(matched.get(2)); // gap in third sequence
+    assertFalse(matched.get(3)); // gap in fourth sequence
+    assertTrue(matched.get(4));
+    assertTrue(matched.get(5)); // gap in second sequence
 
     assertEquals("1YCS", structs[0].pdbId);
     assertEquals("3A6S", structs[1].pdbId);
@@ -218,13 +283,17 @@ public class AAStructureBindingModelTest
       structs[i] = testee.new SuperposeData(al.getWidth());
     }
     /*
-     * initialise array of 'superposable columns' to true (would be false for
+     * initialise BitSet of 'superposable columns' to true (would be false for
      * hidden columns)
      */
-    boolean[] matched = new boolean[al.getWidth()];
-    Arrays.fill(matched, true);
+    BitSet matched = new BitSet();
+    for (int i = 0; i < al.getWidth(); i++)
+    {
+      matched.set(i);
+    }
+
     // treat column 5 of the alignment as hidden
-    matched[4] = false;
+    matched.clear(4);
 
     int refStructure = testee
             .findSuperposableResidues(al, matched, structs);
@@ -232,11 +301,11 @@ public class AAStructureBindingModelTest
     assertEquals(0, refStructure);
 
     // only ungapped, structure-mapped columns are not superposable
-    assertFalse(matched[0]);
-    assertTrue(matched[1]);
-    assertFalse(matched[2]);
-    assertFalse(matched[3]);
-    assertFalse(matched[4]); // superposable, but hidden, column
-    assertTrue(matched[5]);
+    assertFalse(matched.get(0));
+    assertTrue(matched.get(1));
+    assertFalse(matched.get(2));
+    assertFalse(matched.get(3));
+    assertFalse(matched.get(4)); // superposable, but hidden, column
+    assertTrue(matched.get(5));
   }
 }
diff --git a/test/jalview/urls/AppletUrlProviderFactoryTest.java b/test/jalview/urls/AppletUrlProviderFactoryTest.java
new file mode 100644 (file)
index 0000000..2a967e4
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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.urls;
+
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.applet.AppletUrlProviderFactory;
+import jalview.util.UrlConstants;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class AppletUrlProviderFactoryTest {
+
+  @Test(groups = { "Functional" })
+  public void testCreateUrlProvider()
+  {
+    final String defaultUrl = UrlConstants.DEFAULT_STRING.substring(
+            UrlConstants.DEFAULT_STRING.indexOf(UrlConstants.SEP) + 1,
+            UrlConstants.DEFAULT_STRING.length());
+    Map<String, String> urlList = new HashMap<String, String>()
+    {
+      {
+        put("Test1", "http://identifiers.org/uniprot/$DB_ACCESSION$");
+        put("Test2", defaultUrl);
+      }
+    };
+
+    UrlProviderFactoryI factory = new AppletUrlProviderFactory("Test2",
+            urlList);
+    UrlProviderI prov = factory.createUrlProvider();
+
+    // default url correctly set
+    Assert.assertEquals(prov.getPrimaryUrlId(), "Test2");
+    Assert.assertEquals(prov.getPrimaryUrl("FER_CAPAN"),
+            defaultUrl.replace("$SEQUENCE_ID$",
+                    "FER_CAPAN"));
+
+    List<UrlLinkDisplay> allLinks = prov.getLinksForTable();
+
+    // 2 links in provider
+    Assert.assertEquals(allLinks.size(), 2);
+
+    // first link set correctly
+    Assert.assertEquals(allLinks.get(0).getId(), "Test1");
+    Assert.assertEquals(allLinks.get(0).getDescription(), "Test1");
+    Assert.assertEquals(allLinks.get(0).getUrl(),
+            "http://identifiers.org/uniprot/$DB_ACCESSION$");
+    Assert.assertFalse(allLinks.get(0).getIsPrimary());
+    Assert.assertTrue(allLinks.get(0).getIsSelected());
+
+    // second link set correctly
+    Assert.assertEquals(allLinks.get(1).getId(), "Test2");
+    Assert.assertEquals(allLinks.get(1).getDescription(), "Test2");
+    Assert.assertEquals(allLinks.get(1).getUrl(), defaultUrl);
+    Assert.assertTrue(allLinks.get(1).getIsPrimary());
+    Assert.assertTrue(allLinks.get(1).getIsSelected());
+  }
+}
diff --git a/test/jalview/urls/CustomUrlProviderTest.java b/test/jalview/urls/CustomUrlProviderTest.java
new file mode 100644 (file)
index 0000000..f9ed893
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.urls;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.util.UrlConstants;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Vector;
+
+import org.testng.annotations.Test;
+
+public class CustomUrlProviderTest
+{
+
+  private static final String cachedList = "TEST|http://someurl.blah/$DB_ACCESSION$|"
+          + "ANOTHER|http://test/t$SEQUENCE_ID$|"
+          + "TEST2|http://address/$SEQUENCE_ID$|SRS|"
+          + "http://theSRSlink/$SEQUENCE_ID$";
+
+  private static final String unselectedList = "NON1|http://x/y/$DB_ACCESSION$|"
+          + "NON2|http://a/b/c/$DB_ACCESSION";
+
+  private static final HashMap<String, String> urlMap = new HashMap<String, String>()
+  {
+    {
+      put("TEST","http://someurl.blah/$DB_ACCESSION$");
+      put("ANOTHER","http://test/t$SEQUENCE_ID$");
+      put("TEST2", "http://address/$SEQUENCE_ID$");
+      put("SRS", "http://theSRSlink/$SEQUENCE_ID$");
+    }
+  };
+  
+  private static final HashMap<String, String> unselUrlMap = new HashMap<String, String>()
+  {
+    {
+      put("NON1", "http://x/y/$DB_ACCESSION$");
+      put("NON2", "http://a/b/c/$DB_ACCESSION");
+    }
+  };
+
+  private static final String[] dlinks = {
+      "TEST|http://someurl.blah/$DB_ACCESSION$",
+      "ANOTHER|http://test/t$SEQUENCE_ID$",
+      "TEST2|http://address/$SEQUENCE_ID$",
+ UrlConstants.DEFAULT_STRING };
+
+  private static final String[] unselDlinks = {
+      "NON1|http://x/y/$DB_ACCESSION$", "NON2|http://a/b/c/$DB_ACCESSION" };
+
+  private static final Vector<String> displayLinks = new Vector<String>(
+          Arrays.asList(dlinks));
+
+  private static final Vector<String> unselDisplayLinks = new Vector<String>(
+          Arrays.asList(unselDlinks));
+
+  private static final String[] dlinks2 = { "a|http://x.y.z/$SEQUENCE_ID$" };
+
+  private static final Vector<String> displayLinks2 = new Vector<String>(
+          Arrays.asList(dlinks2));
+
+  private static final String[] list1 = { "a" };
+
+  private static final String[] list2 = { "http://x.y.z/$SEQUENCE_ID$" };
+
+  private static final Vector<String> names = new Vector<String>(
+          Arrays.asList(list1));
+
+  private static final Vector<String> urls = new Vector<String>(
+          Arrays.asList(list2));
+
+  /*
+   * Test default url is set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testDefaultUrl()
+  {
+    UrlProviderI customProv = new CustomUrlProvider(cachedList,
+            unselectedList);
+    
+    // default url can be set
+    assertTrue(customProv.setPrimaryUrl("ANOTHER"));
+
+    // supplied replacement id must be more than 4 chars
+    String result = customProv.getPrimaryUrl("123");
+    assertEquals(null, result);
+
+    // default url can be retrieved given a sequence id
+    result = customProv.getPrimaryUrl("seqid");
+    assertEquals("http://test/tseqid", result);
+
+    // if there is no default url it sets the default to null
+    assertFalse(customProv.setPrimaryUrl("No default"));
+    result = customProv.getPrimaryUrl("testid");
+    assertEquals(null, result);
+    
+    // choosing the default picks the DEFAULT_STRING option
+    customProv.choosePrimaryUrl();
+    result = customProv.getPrimaryUrl("seqid");
+    assertEquals(
+            UrlConstants.DEFAULT_STRING.split("\\|")[1].split("\\$")[0]
+            + "seqid",
+            result);
+  }
+
+  /*
+   * Test urls are set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testUrlLinks()
+  {
+    // creation from cached url list works + old links upgraded
+    UrlProviderI customProv = new CustomUrlProvider(cachedList,
+            unselectedList);
+    assertTrue(displayLinks.containsAll(customProv.getLinksForMenu()));
+
+    // creation from map works + old links upgraded
+    UrlProviderI customProv2 = new CustomUrlProvider(urlMap, unselUrlMap);
+    assertTrue(displayLinks.containsAll(customProv2.getLinksForMenu()));
+
+    // writing url links as a string works
+    // because UrlProvider does not guarantee order of links, we can't just
+    // compare the output of writeUrlsAsString to a string, hence the hoops here
+    String result = customProv.writeUrlsAsString(true);
+    UrlProviderI up = new CustomUrlProvider(result, "");
+    assertTrue(displayLinks.containsAll(up.getLinksForMenu()));
+
+    result = customProv.writeUrlsAsString(false);
+    up = new CustomUrlProvider("", result);
+    assertTrue(unselDisplayLinks.containsAll(up.getLinksForMenu()));
+
+    result = customProv2.writeUrlsAsString(true);
+    UrlProviderI up2 = new CustomUrlProvider(result, "");
+    assertTrue(displayLinks.containsAll(up2.getLinksForMenu()));
+
+    result = customProv2.writeUrlsAsString(false);
+    up2 = new CustomUrlProvider("", result);
+    assertTrue(displayLinks.containsAll(up2.getLinksForMenu()));
+  }
+}
diff --git a/test/jalview/urls/DesktopUrlProviderFactoryTest.java b/test/jalview/urls/DesktopUrlProviderFactoryTest.java
new file mode 100644 (file)
index 0000000..cc55034
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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.urls;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class DesktopUrlProviderFactoryTest
+{
+  private static final String testIdOrgString = "{\"Local\": [{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+          + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+          + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+          + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+          + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+          + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+          + "\"url\":\"http://identifiers.org/interpro\"},"
+          + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]}";
+
+  @BeforeMethod(alwaysRun = true)
+  public void setup()
+  {
+    // make a dummy identifiers.org download file
+    File temp = null;
+
+    try
+    {
+      temp = File.createTempFile("tempfile", ".tmp");
+      temp.deleteOnExit();
+      BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
+      bw.write(testIdOrgString);
+      bw.close();
+    } catch (IOException e)
+    {
+      System.out
+              .println("Error initialising DesktopUrlProviderFactoryTest test: "
+                      + e.getMessage());
+    }
+
+    IdOrgSettings.setDownloadLocation(temp.getPath());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCreateUrlProvider()
+  {
+    String defaultUrlString = "Test1";
+    String defaultUrl = "http://blah.blah/$SEQUENCE_ID$";
+    String cachedUrlList = "MIR:00000005|MIR:00000011|Test1|http://blah.blah/$SEQUENCE_ID$|"
+            + "Test2|http://test2/$DB_ACCESSION$|Test3|http://test3/$SEQUENCE_ID$";
+    String userUrlList = "MIR:00000372|Test4|httpL//another.url/$SEQUENCE_ID$";
+
+    DesktopUrlProviderFactory factory = new DesktopUrlProviderFactory(
+            defaultUrlString, cachedUrlList, userUrlList);
+    UrlProviderI prov = factory.createUrlProvider();
+
+    // default url correctly set
+    Assert.assertEquals(prov.getPrimaryUrlId(), "Test1");
+    Assert.assertEquals(prov.getPrimaryUrl("FER_CAPAN"),
+            defaultUrl.replace("$SEQUENCE_ID$", "FER_CAPAN"));
+
+    List<String> menulinks = prov.getLinksForMenu();
+    List<UrlLinkDisplay> allLinks = prov.getLinksForTable();
+
+    // 8 links in provider - 4 from id file, 4 custom links
+    Assert.assertEquals(allLinks.size(), 8);
+
+    // 5 links in menu (cachedUrlList)
+    Assert.assertEquals(menulinks.size(), 5);
+
+    Assert.assertTrue(menulinks
+            .contains("Test1|http://blah.blah/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("Test2|http://test2/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks
+            .contains("Test3|http://test3/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("UniProt Knowledgebase|http://identifiers.org/uniprot/$DB_ACCESSION$|uniprot"));
+    Assert.assertTrue(menulinks
+            .contains("InterPro|http://identifiers.org/interpro/$DB_ACCESSION$|interpro"));
+  }
+}
diff --git a/test/jalview/urls/IdentifiersUrlProviderTest.java b/test/jalview/urls/IdentifiersUrlProviderTest.java
new file mode 100644 (file)
index 0000000..eed58b0
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * 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.urls;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import jalview.urls.api.UrlProviderI;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Vector;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class IdentifiersUrlProviderTest
+{
+  private static final String testIdOrgFile = "{\"Local\": [{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+          + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+          + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+          + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+          + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+          + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+          + "\"url\":\"http://identifiers.org/interpro\"},"
+          + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]}";
+  
+  private static final String[] dlinks = {
+      "UniProt Knowledgebase|http://identifiers.org/uniprot/$DB_ACCESSION$|uniprot",
+      "InterPro|http://identifiers.org/interpro/$DB_ACCESSION$|interpro",
+      "ENA|http://identifiers.org/ena.embl/$DB_ACCESSION$|ena.embl" };
+
+  private static final String[] dlinks1 = {
+      "MIR:00000011|http://identifiers.org/interpro/$DB_ACCESSION$",
+      "MIR:00000372|http://identifiers.org/ena.embl/$DB_ACCESSION$" };
+
+  private static final String[] dlinks2 = {
+      "MIR:00000005|http://identifiers.org/uniprot/$DB_ACCESSION$",
+      "MIR:00000011|http://identifiers.org/interpro/$DB_ACCESSION$" };
+
+  private static final String stringLinks = "MIR:00000005|http://identifiers.org/uniprot/$DB_ACCESSION$"
+          + "MIR:00000011|http://identifiers.org/interpro/$DB_ACCESSION$"
+          + "MIR:00000372|http://identifiers.org/ena.embl/$DB_ACCESSION$";
+
+  private static final String[] unselDlinks = { "ChEBI|http://identifiers.org/chebi/$DB_ACCESSION$" };
+
+  private static final Vector<String> displayLinks = new Vector<String>(
+        Arrays.asList(dlinks));
+  
+  private static final Vector<String> unselDisplayLinks = new Vector<String>(
+          Arrays.asList(unselDlinks));
+
+  private static final Vector<String> displayLinks1 = new Vector<String>(
+          Arrays.asList(dlinks1));
+
+  private static final Vector<String> displayLinks2 = new Vector<String>(
+          Arrays.asList(dlinks2));
+
+  private static final HashMap<String, String> urlMap = new HashMap<String, String>()
+  {
+    {
+      put("MIR:00000005", "http://identifiers.org/uniprot/$DB_ACCESSION$");
+      put("MIR:00000011", "http://identifiers.org/interpro/$DB_ACCESSION$");
+      put("MIR:00000372", "http://identifiers.org/ena.embl/$DB_ACCESSION$");
+    }
+  };
+
+  private String testfile = "";
+
+
+  @BeforeClass(alwaysRun = true)
+  public void setup()
+  {
+    // setup test ids in a file
+    File outFile = null;
+    try
+    {
+      outFile = File.createTempFile("testidsfile", "txt");
+      outFile.deleteOnExit();
+
+      FileWriter fw = new FileWriter(outFile);
+      fw.write(testIdOrgFile);
+      fw.close();
+
+      testfile = outFile.getAbsolutePath();
+
+    } catch (Exception ex)
+    {
+      System.err.println(ex);
+    }
+
+    IdOrgSettings.setDownloadLocation(testfile);
+  }
+
+  /*
+   * Test urls are set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testUrlLinks()
+  {
+    // creation from cached id list
+    String idList = "MIR:00000005|MIR:00000011|MIR:00000372";
+    UrlProviderI idProv = new IdentifiersUrlProvider(idList);
+    
+    assertTrue(displayLinks.containsAll(idProv.getLinksForMenu()));
+
+    // because UrlProvider does not guarantee order of links, we can't just
+    // compare the output of writeUrlsAsString to a string, hence the hoops here
+    String result = idProv.writeUrlsAsString(true);
+    UrlProviderI up = new IdentifiersUrlProvider(result);
+    assertTrue(displayLinks.containsAll(up.getLinksForMenu()));
+
+    result = idProv.writeUrlsAsString(false);
+    up = new IdentifiersUrlProvider(result);
+    assertTrue(unselDisplayLinks.containsAll(up.getLinksForMenu()));
+
+  }
+
+  /*
+   * Test default is set and returned correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testDefaultUrl()
+  {
+    // creation from cached id list
+    String idList = "MIR:00000005|MIR:00000011|MIR:00000372";
+    UrlProviderI idProv = new IdentifiersUrlProvider(idList);
+    
+    // initially no default
+    assertEquals(null, idProv.getPrimaryUrl("seqid"));
+    
+    // set and then retrieve default
+    assertTrue(idProv.setPrimaryUrl("MIR:00000005"));
+    assertEquals("http://identifiers.org/uniprot/seqid",
+            idProv.getPrimaryUrl("seqid"));
+
+    // ids less than length 4 return null
+    assertEquals(null,
+            idProv.getPrimaryUrl("123"));
+
+    // attempt to set bad default
+    assertFalse(idProv.setPrimaryUrl("MIR:00001234"));
+    // default set to null (as default should have been set elsewhere)
+    assertEquals(null, idProv.getPrimaryUrl("seqid"));
+
+    // chooseDefaultUrl not implemented for IdentifiersUrlProvider
+    assertEquals(null, idProv.choosePrimaryUrl());
+  }
+}
diff --git a/test/jalview/urls/UrlLinkDisplayTest.java b/test/jalview/urls/UrlLinkDisplayTest.java
new file mode 100644 (file)
index 0000000..ff7d957
--- /dev/null
@@ -0,0 +1,146 @@
+package jalview.urls;
+
+import jalview.util.UrlLink;
+
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class UrlLinkDisplayTest {
+
+  @Test(groups = { "Functional" })
+  public void testDisplayColumnNames()
+  {
+    // 5 column names returned although 6 names internal to UrlLinkDisplay
+    List<String> names = UrlLinkDisplay.getDisplayColumnNames();
+    Assert.assertEquals(names.size(), 5);
+  }
+
+  @Test(groups = { "Functional" })
+  public void getValue()
+  {
+    UrlLink link = new UrlLink("Test Name",
+            "http://identifiers.org/$DB_ACCESSION$", "TestDB");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.PRIMARY));
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.ID), "Test");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.DATABASE), "TestDB");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.NAME), "Test Name");
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.SELECTED));
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.URL),
+            "http://identifiers.org/$DB_ACCESSION$");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testIsEditable()
+  {
+    // only default and selected columns are editable ever
+    // default only editable if link contains $SEQUENCE_ID$
+
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$",
+ "TestName");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.PRIMARY));
+    Assert.assertTrue(u.isEditable(UrlLinkDisplay.SELECTED));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.ID));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.URL));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.NAME));
+    Assert.assertFalse(u.isEditable(UrlLinkDisplay.DATABASE));
+
+    UrlLink vlink = new UrlLink("Test Sequence ID Url",
+            "http://myurl/$SEQUENCE_ID$", "TestName");
+    UrlLinkDisplay v = new UrlLinkDisplay("Test", vlink, false, false);
+
+    Assert.assertTrue(v.isEditable(UrlLinkDisplay.PRIMARY));
+    Assert.assertTrue(v.isEditable(UrlLinkDisplay.SELECTED));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.ID));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.URL));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.NAME));
+    Assert.assertFalse(v.isEditable(UrlLinkDisplay.DATABASE));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testName()
+  {
+    UrlLink link = new UrlLink("Test Url",
+            "http://identifiers.org/$DB_ACCESSION$", "TestName");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    // Name initially as input in link
+    Assert.assertEquals(u.getDBName(), "TestName");
+
+    // Setting updates name
+    u.setDBName("NewName");
+    Assert.assertEquals(u.getDBName(), "NewName");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDescription()
+  {
+    UrlLink link = new UrlLink("Test Name",
+            "http://identifiers.org/$DB_ACCESSION$", "TestDB");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    // Desc initially as input in link
+    Assert.assertEquals(u.getDescription(), "Test Name");
+
+    // Setting updates name
+    u.setDescription("New Desc");
+    Assert.assertEquals(u.getDescription(), "New Desc");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testUrl()
+  {
+    UrlLink link = new UrlLink("Test Name",
+            "http://identifiers.org/$DB_ACCESSION$", "TestDB");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    // Url initially as input in link
+    Assert.assertEquals(u.getUrl(), "http://identifiers.org/$DB_ACCESSION$");
+
+    // Setting updates url
+    u.setUrl("http://something.new/$SEQUENCE_ID$");
+    Assert.assertEquals(u.getUrl(), "http://something.new/$SEQUENCE_ID$");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetSetValue()
+  {
+    UrlLink link = new UrlLink("Test Name",
+            "http://identifiers.org/$DB_ACCESSION$", "TestDB");
+    UrlLinkDisplay u = new UrlLinkDisplay("Test", link, false, false);
+
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.PRIMARY));
+    Assert.assertFalse((boolean) u.getValue(UrlLinkDisplay.SELECTED));
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.DATABASE), "TestDB");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.NAME), "Test Name");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.ID), "Test");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.URL),
+            "http://identifiers.org/$DB_ACCESSION$");
+
+    u.setValue(UrlLinkDisplay.PRIMARY, true);
+    Assert.assertTrue((boolean) u.getValue(UrlLinkDisplay.PRIMARY));
+
+    u.setValue(UrlLinkDisplay.SELECTED, true);
+    Assert.assertTrue((boolean) u.getValue(UrlLinkDisplay.SELECTED));
+
+    u.setValue(UrlLinkDisplay.NAME, "New Desc");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.NAME), "New Desc");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.DATABASE), "TestDB");
+
+    u.setValue(UrlLinkDisplay.DATABASE, "NewName");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.DATABASE), "NewName");
+
+    u.setValue(UrlLinkDisplay.ID, "New ID");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.ID), "New ID");
+
+    u.setValue(UrlLinkDisplay.URL, "http://something.new/$SEQUENCE_ID$");
+    Assert.assertEquals(u.getValue(UrlLinkDisplay.URL),
+            "http://something.new/$SEQUENCE_ID$");
+  }
+}
diff --git a/test/jalview/urls/UrlLinkTableModelTest.java b/test/jalview/urls/UrlLinkTableModelTest.java
new file mode 100644 (file)
index 0000000..ab190ef
--- /dev/null
@@ -0,0 +1,348 @@
+/*
+ * 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.urls;
+
+import static jalview.util.UrlConstants.DELIM;
+import static jalview.util.UrlConstants.SEP;
+import static jalview.util.UrlConstants.SEQUENCE_ID;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.util.MessageManager;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.TableModelListener;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class UrlLinkTableModelTest {
+
+  private static final String inmenu = "TEST|http://someurl.blah/$DB_ACCESSION$|"
+          + "ANOTHER|http://test/t$SEQUENCE_ID$|"
+          + "TEST2|http://address/$SEQUENCE_ID$|SRS|"
+          + "http://theSRSlink/$SEQUENCE_ID$|"
+          + "MIR:00000005|MIR:00000011|MIR:00000372";
+
+  private static final String notinmenu = "Not1|http://not.in.menu/$DB_ACCESSION$|"
+          + "Not2|http://not.in.menu.either/$DB_ACCESSION$";
+
+  private static final String testIdOrgString = "{\"Local\": [{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+          + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+          + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+          + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+          + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+          + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+          + "\"url\":\"http://identifiers.org/interpro\"},"
+          + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]}";
+
+  private UrlProviderI prov;
+
+  @BeforeMethod(alwaysRun = true)
+  public void setup()
+  {
+    // set up UrlProvider data as the source for the TableModel
+    // the data gets updated by the TableModel, so needs to be reinitialised for
+    // each test
+
+    // make a dummy identifiers.org download file
+    File temp = null;
+    try
+    {
+      temp = File.createTempFile("tempfile", ".tmp");
+      temp.deleteOnExit();
+      BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
+      bw.write(testIdOrgString);
+      bw.close();
+    } catch (IOException e)
+    {
+      System.out.println("Error initialising UrlLinkTableModel test: "
+              + e.getMessage());
+    }
+
+    // set up custom and identifiers.org url providers
+    IdOrgSettings.setDownloadLocation(temp.getPath());
+    IdentifiersUrlProvider idprov = new IdentifiersUrlProvider(inmenu);
+    CustomUrlProvider cprov = new CustomUrlProvider(inmenu, notinmenu);
+    List<UrlProviderI> provlist = new ArrayList<UrlProviderI>();
+    provlist.add(idprov);
+    provlist.add(cprov);
+
+    prov = new UrlProvider("TEST2", provlist);
+  }
+
+  /*
+   * Test that the table model is correctly initialised
+   * Display columns and default row are set; data provider listening event set up
+   */
+  @Test(groups = { "Functional" })
+  public void testInitialisation()
+  {
+    int defaultCol = 4;
+    int dbCol = 0;
+    int descCol = 1;
+
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // exactly one table model listener
+    TableModelListener[] listeners = m
+            .getListeners(TableModelListener.class);
+    Assert.assertEquals(listeners.length, 1);
+
+    // default row exists, there is exactly 1, and it matches the supplied
+    // default
+    int count = 0;
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      boolean isDefault = (boolean) m.getValueAt(row, defaultCol);
+      if (isDefault)
+      {
+        count++;
+        String defaultDBName = (String) m.getValueAt(row, dbCol);
+        Assert.assertEquals(defaultDBName, "TEST2");
+
+        String defaultDesc = (String) m.getValueAt(row, descCol);
+        Assert.assertEquals(defaultDesc, "TEST2");
+      }
+    }
+    Assert.assertEquals(count, 1);
+  }
+
+  /*
+   * Test row and column counts
+   */
+  @Test(groups = { "Functional" })
+  public void testCounts()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // correct numbers of column and rows
+    Assert.assertEquals(m.getColumnCount(), 5);
+    Assert.assertEquals(m.getRowCount(), 10);
+  }
+
+  /*
+   * Test column access
+   */
+  @Test(groups = { "Functional" })
+  public void testColumns()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // check column names
+    Assert.assertEquals(m.getColumnName(0),
+            MessageManager.formatMessage("label.database"));
+    Assert.assertEquals(m.getColumnName(1),
+            MessageManager.formatMessage("label.name"));
+    Assert.assertEquals(m.getColumnName(2),
+            MessageManager.formatMessage("label.url"));
+    Assert.assertEquals(m.getColumnName(3),
+            MessageManager.formatMessage("label.inmenu"));
+    Assert.assertEquals(m.getColumnName(4),
+            MessageManager.formatMessage("label.primary"));
+
+    // check column classes
+    Assert.assertEquals(m.getColumnClass(0), String.class);
+    Assert.assertEquals(m.getColumnClass(1), String.class);
+    Assert.assertEquals(m.getColumnClass(2), String.class);
+    Assert.assertEquals(m.getColumnClass(3), Boolean.class);
+    Assert.assertEquals(m.getColumnClass(4), Boolean.class);
+  }
+
+  /*
+   * Test row insertion
+   */
+  @Test(groups = { "Functional" })
+  public void testRowInsert()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    m.insertRow("newname", "newurl");
+
+    // check table has new row inserted
+    Assert.assertEquals(m.getValueAt(10, 0), "newname");
+    Assert.assertEquals(m.getValueAt(10, 1), "newname");
+    Assert.assertEquals(m.getValueAt(10, 2), "newurl");
+    Assert.assertEquals(m.getValueAt(10, 3), true);
+    Assert.assertEquals(m.getValueAt(10, 4), false);
+
+    // check data source has new row insrte
+    Assert.assertTrue(prov.getLinksForMenu().contains(
+            "newname" + SEP + "newurl"));
+  }
+
+  /*
+   * Test row deletion
+   */
+  @Test(groups = { "Functional" })
+  public void testRowDelete()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // get name and url at row 0
+    String name = (String) m.getValueAt(0, 0);
+    String url = (String) m.getValueAt(0, 1);
+
+    m.removeRow(0);
+
+    // check table no longer has row 0 elements in it
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      Assert.assertNotEquals(m.getValueAt(row, 0), name);
+    }
+
+    // check data source likewise
+    Assert.assertFalse(prov.getLinksForMenu().contains(name + SEP + url));
+  }
+
+  /*
+   * Test value setting and getting
+   */
+  @Test(groups = { "Functional" })
+  public void testValues()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    // get original default
+    int olddefault;
+    boolean isDefault = false;
+    for (olddefault = 0; olddefault < m.getRowCount() && !isDefault; olddefault++)
+    {
+      isDefault = (boolean) m.getValueAt(olddefault, 3);
+    }
+
+    // set new values, one in each row
+    m.setValueAt("dbnamechanged", 6, 0);
+    m.setValueAt("descchanged", 6, 1);
+    m.setValueAt("urlchanged", 7, 2);
+    m.setValueAt(false, 8, 3);
+    m.setValueAt(true, 6, 4);
+
+    m.setValueAt("dbnamechanged", 5, 0);
+
+    // check values updated in table
+    Assert.assertEquals(m.getValueAt(6, 0), "descchanged"); // custom url can't
+                                                            // change db name
+    Assert.assertEquals(m.getValueAt(6, 1), "descchanged");
+    Assert.assertEquals(m.getValueAt(7, 2), "urlchanged");
+    Assert.assertFalse((boolean) m.getValueAt(8, 3));
+    Assert.assertTrue((boolean) m.getValueAt(6, 4));
+    Assert.assertFalse((boolean) m.getValueAt(olddefault, 4));
+
+    Assert.assertEquals(m.getValueAt(5, 0), "dbnamechanged");
+
+    // check default row is exactly one row still
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      isDefault = (boolean) m.getValueAt(row, 4);
+
+      // if isDefault is true, row is 9
+      // if isDefault is false, row is not 9
+      Assert.assertFalse(isDefault && !(row == 6));
+    }
+
+    // check table updated
+    Assert.assertTrue(prov.writeUrlsAsString(true).contains(
+            "descchanged" + SEP + m.getValueAt(6, 2)));
+    Assert.assertTrue(prov.writeUrlsAsString(true).contains(
+            m.getValueAt(7, 1) + SEP + "urlchanged"));
+    Assert.assertTrue(prov.writeUrlsAsString(false).contains(
+            (String) m.getValueAt(8, 1)));
+    Assert.assertEquals(prov.getPrimaryUrl("seqid"), m.getValueAt(6, 2)
+            .toString().replace(DELIM + SEQUENCE_ID + DELIM, "seqid"));
+  }
+
+  /*
+   * Test cell editability
+   */
+  @Test(groups = { "Functional" })
+  public void testEditable()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      Assert.assertFalse(m.isCellEditable(row, 0));
+      Assert.assertFalse(m.isCellEditable(row, 1));
+      Assert.assertFalse(m.isCellEditable(row, 2));
+      Assert.assertTrue(m.isCellEditable(row, 3));
+
+      if ((row == 4) || (row == 6) || (row == 7))
+      {
+        Assert.assertTrue(m.isCellEditable(row, 4));
+      }
+      else
+      {
+        Assert.assertFalse(m.isCellEditable(row, 4));
+      }
+    }
+  }
+
+  /*
+   * Test row 'deletability'
+   */
+  @Test(groups = { "Functional" })
+  public void testDeletable()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      if (row > 4)
+      {
+        Assert.assertTrue(m.isRowDeletable(row));
+      }
+      else
+      {
+        Assert.assertFalse(m.isRowDeletable(row));
+      }
+    }
+  }
+
+  /*
+   * Test indirect row editability
+   */
+  @Test(groups = { "Functional" })
+  public void testRowEditable()
+  {
+    UrlLinkTableModel m = new UrlLinkTableModel(prov);
+
+    for (int row = 0; row < m.getRowCount(); row++)
+    {
+      if (row > 3)
+      {
+        Assert.assertTrue(m.isRowEditable(row));
+      }
+      else
+      {
+        Assert.assertFalse(m.isRowEditable(row));
+      }
+    }
+  }
+}
diff --git a/test/jalview/urls/UrlProviderTest.java b/test/jalview/urls/UrlProviderTest.java
new file mode 100644 (file)
index 0000000..460ebe9
--- /dev/null
@@ -0,0 +1,120 @@
+package jalview.urls;
+
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
+import jalview.util.UrlConstants;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+
+public class UrlProviderTest {
+  
+  // Test identifiers.org download file
+  private static final String testIdOrgString = "{\"Local\": [{\"id\":\"MIR:00000002\",\"name\":\"ChEBI\",\"pattern\":\"^CHEBI:\\d+$\","
+         + "\"definition\":\"Chemical Entities of Biological Interest (ChEBI)\",\"prefix\":\"chebi\","
+         + "\"url\":\"http://identifiers.org/chebi\"},{\"id\":\"MIR:00000005\",\"name\":\"UniProt Knowledgebase\","
+         + "\"pattern\":\"^([A-N,R-Z][0-9]([A-Z][A-Z, 0-9][A-Z, 0-9][0-9]){1,2})|([O,P,Q][0-9][A-Z, 0-9][A-Z, 0-9][A-Z, 0-9][0-9])(\\.\\d+)?$\","
+         + "\"definition\":\"The UniProt Knowledgebase (UniProtKB)\",\"prefix\":\"uniprot\",\"url\":\"http://identifiers.org/uniprot\"},"
+         + "{\"id\":\"MIR:00000011\",\"name\":\"InterPro\",\"pattern\":\"^IPR\\d{6}$\",\"definition\":\"InterPro\",\"prefix\":\"interpro\","
+         + "\"url\":\"http://identifiers.org/interpro\"},"
+         + "{\"id\":\"MIR:00000372\",\"name\":\"ENA\",\"pattern\":\"^[A-Z]+[0-9]+(\\.\\d+)?$\",\"definition\":\"The European Nucleotide Archive (ENA),\""
+          + "\"prefix\":\"ena.embl\",\"url\":\"http://identifiers.org/ena.embl\"}]}";
+
+  private UrlProviderI prov;
+
+  @BeforeMethod(alwaysRun = true)
+  public void setup()
+  {
+   // make a dummy identifiers.org download file
+   File temp = null;
+
+   try
+   {
+     temp = File.createTempFile("tempfile", ".tmp");
+     temp.deleteOnExit();
+     BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
+     bw.write(testIdOrgString);
+     bw.close();
+   } catch (IOException e)
+   {
+      System.out.println("Error initialising UrlProviderTest test: "
+              + e.getMessage());
+   }
+
+   IdOrgSettings.setDownloadLocation(temp.getPath());
+
+    String defaultUrlString = "No default";
+    String cachedUrlList = "MIR:00000005|MIR:00000011|Test1|http://blah.blah/$SEQUENCE_ID$|"
+            + "Test2|http://test2/$DB_ACCESSION$|Test3|http://test3/$SEQUENCE_ID$";
+    String userUrlList = "MIR:00000372|Test4|httpL//another.url/$SEQUENCE_ID$";
+
+    DesktopUrlProviderFactory factory = new DesktopUrlProviderFactory(
+            defaultUrlString, cachedUrlList, userUrlList);
+    prov = factory.createUrlProvider();
+ }
+
+  @Test(groups = { "Functional" })
+  public void testInitUrlProvider()
+  {
+    String emblUrl = UrlConstants.DEFAULT_STRING.substring(
+            UrlConstants.DEFAULT_STRING.indexOf(UrlConstants.SEP) + 1,
+            UrlConstants.DEFAULT_STRING.length());
+
+    // chooses EMBL url when default Url id does not exist in provided url lists
+    Assert.assertEquals(prov.getPrimaryUrlId(), UrlConstants.DEFAULT_LABEL);
+    Assert.assertEquals(prov.getPrimaryUrl("FER_CAPAN"),
+            emblUrl.replace("$SEQUENCE_ID$", "FER_CAPAN"));
+
+    List<String> menulinks = prov.getLinksForMenu();
+    List<UrlLinkDisplay> allLinks = prov.getLinksForTable();
+
+    // 9 links in provider - 4 from id file, 4 custom links, 1 additional
+    // default
+    Assert.assertEquals(allLinks.size(), 9);
+
+    // 6 links in menu (cachedUrlList) + new default
+    Assert.assertEquals(menulinks.size(), 6);
+
+    Assert.assertTrue(menulinks
+            .contains("Test1|http://blah.blah/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("Test2|http://test2/$DB_ACCESSION$"));
+    Assert.assertTrue(menulinks
+            .contains("Test3|http://test3/$SEQUENCE_ID$"));
+    Assert.assertTrue(menulinks
+            .contains("UniProt Knowledgebase|http://identifiers.org/uniprot/$DB_ACCESSION$|uniprot"));
+    Assert.assertTrue(menulinks
+            .contains("InterPro|http://identifiers.org/interpro/$DB_ACCESSION$|interpro"));
+    Assert.assertTrue(menulinks.contains(UrlConstants.DEFAULT_LABEL
+            + UrlConstants.SEP + emblUrl));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetDefaultUrl()
+  {
+    // set custom url as default
+    Assert.assertTrue(prov.setPrimaryUrl("Test1"));
+    Assert.assertEquals(prov.getPrimaryUrlId(), "Test1");
+
+    // set identifiers url as default
+    Assert.assertTrue(prov.setPrimaryUrl("MIR:00000011"));
+    Assert.assertEquals(prov.getPrimaryUrlId(), "MIR:00000011");
+  }
+
+  @Test(
+    groups = { "Functional" },
+    expectedExceptions = { IllegalArgumentException.class })
+  public void testSetDefaultUrlWrongly()
+  {
+    // don't allow default to be a non-key
+    prov.setPrimaryUrl("not-a-key");
+  }
+}
index 9a5d093..fa4091f 100644 (file)
@@ -166,4 +166,81 @@ public class ColorUtilsTest
     assertEquals(new Color(46, 31, 16), // with rounding down
             ColorUtils.bleachColour(colour, -0.7f));
   }
+
+  @Test(groups = "Functional")
+  public void testParseColourString()
+  {
+    /*
+     * by colour name - if known to AWT, and included in
+     * 
+     * @see ColourSchemeProperty.getAWTColorFromName()
+     */
+    assertSame(Color.RED, ColorUtils.parseColourString("red"));
+    assertSame(Color.RED, ColorUtils.parseColourString("Red"));
+    assertSame(Color.RED, ColorUtils.parseColourString(" RED "));
+
+    /*
+     * by RGB hex code
+     */
+    String hexColour = Integer.toHexString(Color.RED.getRGB() & 0xffffff);
+    assertEquals("ff0000", hexColour);
+    assertEquals(Color.RED, ColorUtils.parseColourString(hexColour));
+    // 'hex' prefixes _not_ wanted here
+    assertNull(ColorUtils.parseColourString("0x" + hexColour));
+    assertNull(ColorUtils.parseColourString("#" + hexColour));
+    // out of range, but Color constructor just or's the rgb value with 0
+    assertEquals(Color.black, ColorUtils.parseColourString("1000000"));
+
+    /*
+     * by RGB triplet
+     */
+    Color c = Color.pink;
+    String rgb = String.format("%d,%d,%d", c.getRed(), c.getGreen(),
+            c.getBlue());
+    assertEquals("255,175,175", rgb);
+    assertEquals(c, ColorUtils.parseColourString(rgb));
+    assertEquals(c, ColorUtils.parseColourString("255, 175 , 175"));
+
+    /*
+     * odds and ends
+     */
+    assertNull(ColorUtils.parseColourString(null));
+    assertNull(ColorUtils.parseColourString("rubbish"));
+    assertEquals(Color.WHITE, ColorUtils.parseColourString("-1"));
+    assertNull(ColorUtils.parseColourString(String
+            .valueOf(Integer.MAX_VALUE)));
+    assertNull(ColorUtils.parseColourString("100,200,300")); // out of range
+    assertNull(ColorUtils.parseColourString("100,200")); // too few
+    assertNull(ColorUtils.parseColourString("100,200,100,200")); // too many
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAWTColorFromName() {
+    assertEquals(Color.white, ColorUtils.getAWTColorFromName("white"));
+    assertEquals(Color.white, ColorUtils.getAWTColorFromName("White"));
+    assertEquals(Color.white, ColorUtils.getAWTColorFromName("WHITE"));
+    assertEquals(Color.pink, ColorUtils.getAWTColorFromName("pink"));
+    assertNull(ColorUtils.getAWTColorFromName("mauve")); // no such name
+    assertNull(ColorUtils.getAWTColorFromName(""));
+    assertNull(ColorUtils.getAWTColorFromName(null));
+  }
+
+  @Test(groups = "Functional")
+  public void testCreateColourFromName()
+  {
+    assertEquals(Color.white, ColorUtils.createColourFromName(null));
+    assertEquals(new Color(20, 20, 20), ColorUtils.createColourFromName(""));
+    assertEquals(new Color(98, 131, 171),
+            ColorUtils.createColourFromName("None")); // no special treatment!
+    assertEquals(new Color(123, 211, 122),
+            ColorUtils.createColourFromName("hello world"));
+    assertEquals(new Color(27, 147, 112),
+            ColorUtils.createColourFromName("HELLO WORLD"));
+    /*
+     * the algorithm makes the same values for r,g,b if 
+     * the string consists of 3 repeating substrings
+     */
+    assertEquals(new Color(184, 184, 184),
+            ColorUtils.createColourFromName("HELLO HELLO HELLO "));
+  }
 }
index f955879..6f6841d 100644 (file)
@@ -115,7 +115,7 @@ public class ComparisonTest
   @Test(groups = { "Functional" })
   public void testPID_includingGaps()
   {
-    String seq1 = "ABCDEF";
+    String seq1 = "ABCDEFG"; // extra length here is ignored
     String seq2 = "abcdef";
     assertEquals("identical", 100f, Comparison.PID(seq1, seq2), 0.001f);
 
@@ -129,12 +129,14 @@ public class ComparisonTest
     int length = seq1.length();
 
     // match gap-residue, match gap-gap: 9/10 identical
+    // TODO should gap-gap be included in a PID score? JAL-791
     assertEquals(90f, Comparison.PID(seq1, seq2, 0, length, true, false),
             0.001f);
     // overloaded version of the method signature above:
     assertEquals(90f, Comparison.PID(seq1, seq2), 0.001f);
 
     // don't match gap-residue, match gap-gap: 7/10 identical
+    // TODO should gap-gap be included in a PID score?
     assertEquals(70f, Comparison.PID(seq1, seq2, 0, length, false, false),
             0.001f);
   }
@@ -163,7 +165,8 @@ public class ComparisonTest
   public void testPID_ungappedOnly()
   {
     // 5 identical, 2 gap-gap, 2 gap-residue, 1 mismatch
-    String seq1 = "a--b-cdefh";
+    // the extra length of seq1 is ignored
+    String seq1 = "a--b-cdefhr";
     String seq2 = "a---bcdefg";
     int length = seq1.length();
 
index b84e770..19c8438 100644 (file)
@@ -33,6 +33,7 @@ import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
@@ -300,49 +301,60 @@ public class MappingUtilsTest
     setupMappedAlignments();
 
     ColumnSelection colsel = new ColumnSelection();
+    HiddenColumns hidden = new HiddenColumns();
 
     /*
      * Column 0 in protein picks up Seq2/L, Seq3/G which map to cols 0-4 and 0-3
      * in dna respectively, overall 0-4
      */
     colsel.addElement(0);
-    ColumnSelection cs = MappingUtils.mapColumnSelection(colsel,
-            proteinView, dnaView);
+    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns hs = new HiddenColumns();
+    MappingUtils.mapColumnSelection(colsel, hidden, proteinView, dnaView,
+            cs, hs);
     assertEquals("[0, 1, 2, 3, 4]", cs.getSelected().toString());
 
     /*
      * Column 1 in protein picks up Seq1/K which maps to cols 0-3 in dna
      */
+    cs.clear();
     colsel.clear();
     colsel.addElement(1);
-    cs = MappingUtils.mapColumnSelection(colsel, proteinView, dnaView);
+    MappingUtils.mapColumnSelection(colsel, hidden, proteinView, dnaView,
+            cs, hs);
     assertEquals("[0, 1, 2, 3]", cs.getSelected().toString());
 
     /*
      * Column 2 in protein picks up gaps only - no mapping
      */
+    cs.clear();
     colsel.clear();
     colsel.addElement(2);
-    cs = MappingUtils.mapColumnSelection(colsel, proteinView, dnaView);
+    MappingUtils.mapColumnSelection(colsel, hidden, proteinView,
+            dnaView, cs, hs);
     assertEquals("[]", cs.getSelected().toString());
 
     /*
      * Column 3 in protein picks up Seq1/P, Seq2/Q, Seq3/S which map to columns
      * 6-9, 6-10, 5-8 respectively, overall to 5-10
      */
+    cs.clear();
     colsel.clear();
     colsel.addElement(3);
-    cs = MappingUtils.mapColumnSelection(colsel, proteinView, dnaView);
+    MappingUtils.mapColumnSelection(colsel, hidden, proteinView,
+            dnaView, cs, hs);
     assertEquals("[5, 6, 7, 8, 9, 10]", cs.getSelected().toString());
 
     /*
      * Combine selection of columns 1 and 3 to get a discontiguous mapped
      * selection
      */
+    cs.clear();
     colsel.clear();
     colsel.addElement(1);
     colsel.addElement(3);
-    cs = MappingUtils.mapColumnSelection(colsel, proteinView, dnaView);
+    MappingUtils.mapColumnSelection(colsel, hidden, proteinView,
+            dnaView, cs, hs);
     assertEquals("[0, 1, 2, 3, 5, 6, 7, 8, 9, 10]", cs.getSelected()
             .toString());
   }
@@ -407,14 +419,17 @@ public class MappingUtilsTest
     setupMappedAlignments();
 
     ColumnSelection colsel = new ColumnSelection();
+    HiddenColumns hidden = new HiddenColumns();
 
     /*
      * Column 0 in dna picks up first bases which map to residue 1, columns 0-1
      * in protein.
      */
+    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns hs = new HiddenColumns();
     colsel.addElement(0);
-    ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, dnaView,
-            proteinView);
+    MappingUtils.mapColumnSelection(colsel, hidden, dnaView, proteinView,
+            cs, hs);
     assertEquals("[0, 1]", cs.getSelected().toString());
 
     /*
@@ -424,7 +439,9 @@ public class MappingUtilsTest
     colsel.addElement(3);
     colsel.addElement(4);
     colsel.addElement(5);
-    cs = MappingUtils.mapColumnSelection(colsel, dnaView, proteinView);
+    cs.clear();
+    MappingUtils.mapColumnSelection(colsel, hidden, dnaView, proteinView,
+            cs, hs);
     assertEquals("[0, 1, 3]", cs.getSelected().toString());
   }
 
@@ -432,8 +449,10 @@ public class MappingUtilsTest
   public void testMapColumnSelection_null() throws IOException
   {
     setupMappedAlignments();
-    ColumnSelection cs = MappingUtils.mapColumnSelection(null, dnaView,
-            proteinView);
+    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns hs = new HiddenColumns();
+    MappingUtils.mapColumnSelection(null, null, dnaView, proteinView, cs,
+            hs);
     assertTrue("mapped selection not empty", cs.getSelected().isEmpty());
   }
 
@@ -882,69 +901,80 @@ public class MappingUtilsTest
     setupMappedAlignments();
 
     ColumnSelection proteinSelection = new ColumnSelection();
+    HiddenColumns hiddenCols = new HiddenColumns();
 
     /*
      * Column 0 in protein picks up Seq2/L, Seq3/G which map to cols 0-4 and 0-3
      * in dna respectively, overall 0-4
      */
-    proteinSelection.hideColumns(0);
-    ColumnSelection dnaSelection = MappingUtils.mapColumnSelection(
-            proteinSelection, proteinView, dnaView);
+    proteinSelection.hideSelectedColumns(0, hiddenCols);
+    ColumnSelection dnaSelection = new ColumnSelection();
+    HiddenColumns dnaHidden = new HiddenColumns();
+    MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
+            proteinView, dnaView, dnaSelection, dnaHidden);
     assertEquals("[]", dnaSelection.getSelected().toString());
-    List<int[]> hidden = dnaSelection.getHiddenColumns();
+    List<int[]> hidden = dnaHidden.getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[0, 4]", Arrays.toString(hidden.get(0)));
 
     /*
      * Column 1 in protein picks up Seq1/K which maps to cols 0-3 in dna
      */
-    proteinSelection.revealAllHiddenColumns();
+    dnaSelection = new ColumnSelection();
+    dnaHidden = new HiddenColumns();
+    hiddenCols.revealAllHiddenColumns(proteinSelection);
     // the unhidden columns are now marked selected!
     assertEquals("[0]", proteinSelection.getSelected().toString());
     // deselect these or hideColumns will be expanded to include 0
     proteinSelection.clear();
-    proteinSelection.hideColumns(1);
-    dnaSelection = MappingUtils.mapColumnSelection(proteinSelection,
-            proteinView, dnaView);
-    hidden = dnaSelection.getHiddenColumns();
+    proteinSelection.hideSelectedColumns(1, hiddenCols);
+    MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
+            proteinView, dnaView, dnaSelection, dnaHidden);
+    hidden = dnaHidden.getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
 
     /*
      * Column 2 in protein picks up gaps only - no mapping
      */
-    proteinSelection.revealAllHiddenColumns();
+    dnaSelection = new ColumnSelection();
+    dnaHidden = new HiddenColumns();
+    hiddenCols.revealAllHiddenColumns(proteinSelection);
     proteinSelection.clear();
-    proteinSelection.hideColumns(2);
-    dnaSelection = MappingUtils.mapColumnSelection(proteinSelection,
-            proteinView, dnaView);
-    assertTrue(dnaSelection.getHiddenColumns().isEmpty());
+    proteinSelection.hideSelectedColumns(2, hiddenCols);
+    MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
+            proteinView, dnaView, dnaSelection, dnaHidden);
+    assertTrue(dnaHidden.getHiddenRegions().isEmpty());
 
     /*
      * Column 3 in protein picks up Seq1/P, Seq2/Q, Seq3/S which map to columns
      * 6-9, 6-10, 5-8 respectively, overall to 5-10
      */
-    proteinSelection.revealAllHiddenColumns();
+    dnaSelection = new ColumnSelection();
+    dnaHidden = new HiddenColumns();
+    hiddenCols.revealAllHiddenColumns(proteinSelection);
     proteinSelection.clear();
-    proteinSelection.hideColumns(3); // 5-10 hidden in dna
+    proteinSelection.hideSelectedColumns(3, hiddenCols); // 5-10 hidden in dna
     proteinSelection.addElement(1); // 0-3 selected in dna
-    dnaSelection = MappingUtils.mapColumnSelection(proteinSelection,
-            proteinView, dnaView);
+    MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
+            proteinView, dnaView, dnaSelection, dnaHidden);
     assertEquals("[0, 1, 2, 3]", dnaSelection.getSelected().toString());
-    hidden = dnaSelection.getHiddenColumns();
+    hidden = dnaHidden.getHiddenRegions();
     assertEquals(1, hidden.size());
     assertEquals("[5, 10]", Arrays.toString(hidden.get(0)));
 
     /*
      * Combine hiding columns 1 and 3 to get discontiguous hidden columns
      */
-    proteinSelection.revealAllHiddenColumns();
+    dnaSelection = new ColumnSelection();
+    dnaHidden = new HiddenColumns();
+    hiddenCols.revealAllHiddenColumns(proteinSelection);
     proteinSelection.clear();
-    proteinSelection.hideColumns(1);
-    proteinSelection.hideColumns(3);
-    dnaSelection = MappingUtils.mapColumnSelection(proteinSelection,
-            proteinView, dnaView);
-    hidden = dnaSelection.getHiddenColumns();
+    proteinSelection.hideSelectedColumns(1, hiddenCols);
+    proteinSelection.hideSelectedColumns(3, hiddenCols);
+    MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
+            proteinView, dnaView, dnaSelection, dnaHidden);
+    hidden = dnaHidden.getHiddenRegions();
     assertEquals(2, hidden.size());
     assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
     assertEquals("[5, 10]", Arrays.toString(hidden.get(1)));
diff --git a/test/jalview/util/SetUtilsTest.java b/test/jalview/util/SetUtilsTest.java
new file mode 100644 (file)
index 0000000..ad17d4f
--- /dev/null
@@ -0,0 +1,46 @@
+package jalview.util;
+
+import static org.testng.Assert.assertEquals;
+
+import java.awt.Color;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.testng.annotations.Test;
+
+public class SetUtilsTest
+{
+  @Test(groups = "Functional")
+  public void testCountDisjunction()
+  {
+    Set<Color> s1 = new HashSet<Color>();
+    assertEquals(SetUtils.countDisjunction(null, null), 0);
+    assertEquals(SetUtils.countDisjunction(s1, null), 0);
+    assertEquals(SetUtils.countDisjunction(null, s1), 0);
+    s1.add(Color.white);
+    assertEquals(SetUtils.countDisjunction(s1, null), 1);
+    assertEquals(SetUtils.countDisjunction(null, s1), 1);
+    assertEquals(SetUtils.countDisjunction(s1, null), 1);
+    assertEquals(SetUtils.countDisjunction(s1, s1), 0);
+
+    Set<Object> s2 = new HashSet<Object>();
+    assertEquals(SetUtils.countDisjunction(s2, s2), 0);
+    assertEquals(SetUtils.countDisjunction(s1, s2), 1);
+    assertEquals(SetUtils.countDisjunction(s2, s1), 1);
+
+    s1.add(Color.yellow);
+    s1.add(Color.blue);
+    s2.add(new Color(Color.yellow.getRGB()));
+
+    /*
+     * now s1 is {white, yellow, blue}
+     *     s2 is {yellow'}
+     */
+    assertEquals(SetUtils.countDisjunction(s1, s2), 2);
+    s2.add(Color.blue);
+    assertEquals(SetUtils.countDisjunction(s1, s2), 1);
+    s2.add(Color.pink);
+    assertEquals(SetUtils.countDisjunction(s1, s2), 2);
+
+  }
+}
index d07206f..4092cf2 100644 (file)
@@ -75,8 +75,8 @@ public class UrlLinkTest
             + DELIM + URL_SUFFIX);
     assertEquals(DB, ul.getTarget());
     assertEquals(DB, ul.getLabel());
-    assertEquals(URL_PREFIX, ul.getUrl_prefix());
-    assertEquals(URL_SUFFIX, ul.getUrl_suffix());
+    assertEquals(URL_PREFIX, ul.getUrlPrefix());
+    assertEquals(URL_SUFFIX, ul.getUrlSuffix());
     assertTrue(ul.isDynamic());
     assertFalse(ul.usesDBAccession());
     assertNull(ul.getRegexReplace());
@@ -88,8 +88,8 @@ public class UrlLinkTest
             + URL_SUFFIX);
     assertEquals(DB, ul.getTarget());
     assertEquals(DB, ul.getLabel());
-    assertEquals(URL_PREFIX, ul.getUrl_prefix());
-    assertEquals(URL_SUFFIX, ul.getUrl_suffix());
+    assertEquals(URL_PREFIX, ul.getUrlPrefix());
+    assertEquals(URL_SUFFIX, ul.getUrlSuffix());
     assertTrue(ul.isDynamic());
     assertTrue(ul.usesDBAccession());
     assertNull(ul.getRegexReplace());
@@ -100,7 +100,7 @@ public class UrlLinkTest
     ul = new UrlLink(DB + SEP + URL_PREFIX + URL_SUFFIX.substring(1));
     assertEquals(DB, ul.getTarget());
     assertEquals(DB, ul.getLabel());
-    assertEquals(URL_PREFIX + URL_SUFFIX.substring(1), ul.getUrl_prefix());
+    assertEquals(URL_PREFIX + URL_SUFFIX.substring(1), ul.getUrlPrefix());
     assertFalse(ul.isDynamic());
     assertFalse(ul.usesDBAccession());
     assertNull(ul.getRegexReplace());
@@ -119,8 +119,8 @@ public class UrlLinkTest
             + REGEX_NESTED + DELIM + URL_SUFFIX);
     assertEquals(DB, ul.getTarget());
     assertEquals(DB, ul.getLabel());
-    assertEquals(URL_PREFIX, ul.getUrl_prefix());
-    assertEquals(URL_SUFFIX, ul.getUrl_suffix());
+    assertEquals(URL_PREFIX, ul.getUrlPrefix());
+    assertEquals(URL_SUFFIX, ul.getUrlSuffix());
     assertTrue(ul.isDynamic());
     assertFalse(ul.usesDBAccession());
     assertEquals(REGEX_NESTED.substring(2, REGEX_NESTED.length() - 2),
@@ -133,8 +133,8 @@ public class UrlLinkTest
             + REGEX_NESTED + DELIM + URL_SUFFIX);
     assertEquals(DB, ul.getTarget());
     assertEquals(DB, ul.getLabel());
-    assertEquals(URL_PREFIX, ul.getUrl_prefix());
-    assertEquals(URL_SUFFIX, ul.getUrl_suffix());
+    assertEquals(URL_PREFIX, ul.getUrlPrefix());
+    assertEquals(URL_SUFFIX, ul.getUrlSuffix());
     assertTrue(ul.isDynamic());
     assertTrue(ul.usesDBAccession());
     assertEquals(REGEX_NESTED.substring(2, REGEX_NESTED.length() - 2),
@@ -147,8 +147,8 @@ public class UrlLinkTest
             + REGEX_RUBBISH + DELIM + URL_SUFFIX);
     assertEquals(DB, ul.getTarget());
     assertEquals(DB, ul.getLabel());
-    assertEquals(URL_PREFIX, ul.getUrl_prefix());
-    assertEquals(URL_SUFFIX, ul.getUrl_suffix());
+    assertEquals(URL_PREFIX, ul.getUrlPrefix());
+    assertEquals(URL_SUFFIX, ul.getUrlSuffix());
     assertTrue(ul.isDynamic());
     assertTrue(ul.usesDBAccession());
     assertEquals(REGEX_RUBBISH.substring(2, REGEX_RUBBISH.length() - 2),
@@ -277,10 +277,10 @@ public class UrlLinkTest
     String key = DB + SEP + URL_PREFIX;
     assertEquals(1, linkset.size());
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), DB);
-    assertEquals(linkset.get(key).get(1), DB);
-    assertEquals(linkset.get(key).get(2), null);
-    assertEquals(linkset.get(key).get(3), URL_PREFIX);
+    assertEquals(DB, linkset.get(key).get(0));
+    assertEquals(DB, linkset.get(key).get(1));
+    assertEquals(null, linkset.get(key).get(2));
+    assertEquals(URL_PREFIX, linkset.get(key).get(3));
   }
 
   /**
@@ -297,10 +297,10 @@ public class UrlLinkTest
     String key = DB + SEP + URL_PREFIX + URL_SUFFIX;
     assertEquals(1, linkset.size());
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), DB);
-    assertEquals(linkset.get(key).get(1), DB);
-    assertEquals(linkset.get(key).get(2), null);
-    assertEquals(linkset.get(key).get(3), URL_PREFIX + URL_SUFFIX);
+    assertEquals(DB, linkset.get(key).get(0));
+    assertEquals(DB, linkset.get(key).get(1));
+    assertEquals(null, linkset.get(key).get(2));
+    assertEquals(URL_PREFIX + URL_SUFFIX, linkset.get(key).get(3));
   }
 
   /**
@@ -348,11 +348,11 @@ public class UrlLinkTest
             + URL_SUFFIX;
     assertEquals(1, linkset.size());
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), DB);
-    assertEquals(linkset.get(key).get(1), DB);
-    assertEquals(linkset.get(key).get(2), seq0.getName());
-    assertEquals(linkset.get(key).get(3), URL_PREFIX + seq0.getName()
-            + URL_SUFFIX);
+    assertEquals(DB, linkset.get(key).get(0));
+    assertEquals(DB, linkset.get(key).get(1));
+    assertEquals(seq0.getName(), linkset.get(key).get(2));
+    assertEquals(URL_PREFIX + seq0.getName() + URL_SUFFIX, linkset.get(key)
+            .get(3));
 
     // Test where link takes a db annotation id and only has one dbref
     ul = new UrlLink(links.get(1));
@@ -360,14 +360,14 @@ public class UrlLinkTest
     ul.createLinksFromSeq(seq0, linkset);
 
     key = "P83527|http://www.uniprot.org/uniprot/P83527";
-    assertEquals(1, linkset.size());
+    assertEquals(linkset.size(), 1);
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), DBRefSource.UNIPROT);
-    assertEquals(linkset.get(key).get(1), DBRefSource.UNIPROT + SEP
-            + "P83527");
-    assertEquals(linkset.get(key).get(2), "P83527");
-    assertEquals(linkset.get(key).get(3),
-            "http://www.uniprot.org/uniprot/P83527");
+    assertEquals(DBRefSource.UNIPROT, linkset.get(key).get(0));
+    assertEquals(DBRefSource.UNIPROT + SEP + "P83527", linkset.get(key)
+            .get(1));
+    assertEquals("P83527", linkset.get(key).get(2));
+    assertEquals("http://www.uniprot.org/uniprot/P83527", linkset.get(key)
+            .get(3));
 
     // Test where link takes a db annotation id and has multiple dbrefs
     ul = new UrlLink(links.get(2));
@@ -378,27 +378,27 @@ public class UrlLinkTest
     // check each link made it in correctly
     key = "IPR001041|http://www.ebi.ac.uk/interpro/entry/IPR001041";
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), "INTERPRO");
-    assertEquals(linkset.get(key).get(1), "INTERPRO" + SEP + "IPR001041");
-    assertEquals(linkset.get(key).get(2), "IPR001041");
-    assertEquals(linkset.get(key).get(3),
-            "http://www.ebi.ac.uk/interpro/entry/IPR001041");
+    assertEquals("INTERPRO", linkset.get(key).get(0));
+    assertEquals("INTERPRO" + SEP + "IPR001041", linkset.get(key).get(1));
+    assertEquals("IPR001041", linkset.get(key).get(2));
+    assertEquals("http://www.ebi.ac.uk/interpro/entry/IPR001041", linkset
+            .get(key).get(3));
 
     key = "IPR006058|http://www.ebi.ac.uk/interpro/entry/IPR006058";
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), "INTERPRO");
-    assertEquals(linkset.get(key).get(1), "INTERPRO" + SEP + "IPR006058");
-    assertEquals(linkset.get(key).get(2), "IPR006058");
-    assertEquals(linkset.get(key).get(3),
-            "http://www.ebi.ac.uk/interpro/entry/IPR006058");
+    assertEquals("INTERPRO", linkset.get(key).get(0));
+    assertEquals("INTERPRO" + SEP + "IPR006058", linkset.get(key).get(1));
+    assertEquals("IPR006058", linkset.get(key).get(2));
+    assertEquals("http://www.ebi.ac.uk/interpro/entry/IPR006058", linkset
+            .get(key).get(3));
 
     key = "IPR012675|http://www.ebi.ac.uk/interpro/entry/IPR012675";
     assertTrue(linkset.containsKey(key));
-    assertEquals(linkset.get(key).get(0), "INTERPRO");
-    assertEquals(linkset.get(key).get(1), "INTERPRO" + SEP + "IPR012675");
-    assertEquals(linkset.get(key).get(2), "IPR012675");
-    assertEquals(linkset.get(key).get(3),
-            "http://www.ebi.ac.uk/interpro/entry/IPR012675");
+    assertEquals("INTERPRO", linkset.get(key).get(0));
+    assertEquals("INTERPRO" + SEP + "IPR012675", linkset.get(key).get(1));
+    assertEquals("IPR012675", linkset.get(key).get(2));
+    assertEquals("http://www.ebi.ac.uk/interpro/entry/IPR012675", linkset
+            .get(key).get(3));
 
     // Test where there are no matching dbrefs for the link
     ul = new UrlLink(DB + SEP + URL_PREFIX + DELIM + DB_ACCESSION + DELIM
@@ -408,4 +408,35 @@ public class UrlLinkTest
     assertTrue(linkset.isEmpty());
   }
 
+  /**
+   * Test links where label and target are both included
+   */
+  @Test(groups = { "Functional" })
+  public void testLinksWithTargets()
+  {
+    UrlLink ul = new UrlLink(
+            "Protein Data Bank | http://www.identifiers.org/pdb/$"
+                    + DB_ACCESSION + "$" + " | pdb");
+
+    assertEquals("Protein Data Bank", ul.getLabel());
+    assertEquals("pdb", ul.getTarget());
+    assertEquals("http://www.identifiers.org/pdb/$" + DB_ACCESSION + "$",
+            ul.getUrlWithToken());
+
+    assertEquals("Protein Data Bank|http://www.identifiers.org/pdb/$"
+            + DB_ACCESSION + "$" + "|pdb", ul.toStringWithTarget());
+
+    ul = new UrlLink("Protein Data Bank",
+            "http://www.identifiers.org/pdb/$" + DB_ACCESSION + "$", "pdb");
+
+    assertEquals("Protein Data Bank", ul.getLabel());
+    assertEquals("pdb", ul.getTarget());
+    assertEquals("http://www.identifiers.org/pdb/$" + DB_ACCESSION + "$",
+            ul.getUrlWithToken());
+
+    assertEquals("Protein Data Bank|http://www.identifiers.org/pdb/$"
+            + DB_ACCESSION + "$" + "|pdb", ul.toStringWithTarget());
+
+  }
+
 }
diff --git a/test/jalview/viewmodel/OverviewDimensionsHideHiddenTest.java b/test/jalview/viewmodel/OverviewDimensionsHideHiddenTest.java
new file mode 100644 (file)
index 0000000..0e931eb
--- /dev/null
@@ -0,0 +1,982 @@
+/*
+ * 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.viewmodel;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.analysis.AlignmentGenerator;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+
+import java.util.Hashtable;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Test(singleThreaded = true)
+public class OverviewDimensionsHideHiddenTest
+{
+  AlignmentI al;
+
+  OverviewDimensionsHideHidden od;
+
+  // cached widths and heights
+  int boxWidth;
+  int boxHeight;
+  int viewHeight;
+  int viewWidth;
+  int alheight;
+  int alwidth;
+
+  ViewportRanges vpranges;
+
+  Hashtable<SequenceI, SequenceCollectionI> hiddenRepSequences = new Hashtable<SequenceI, SequenceCollectionI>();
+
+  HiddenColumns hiddenCols = new HiddenColumns();
+
+  @BeforeClass(alwaysRun = true)
+  public void setUpAlignment()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    al = gen.generate(157, 525, 123, 5, 5);
+  }
+
+  @BeforeMethod(alwaysRun = true)
+  public void setUp()
+  {
+    if (!hiddenRepSequences.isEmpty())
+    {
+      al.getHiddenSequences().showAll(hiddenRepSequences);
+    }
+    ColumnSelection colsel = new ColumnSelection();
+    hiddenCols.revealAllHiddenColumns(colsel);
+    
+    vpranges = new ViewportRanges(al);
+    vpranges.setViewportStartAndHeight(0, 18);
+    vpranges.setViewportStartAndWidth(0, 63);
+
+    viewHeight = vpranges.getEndSeq() - vpranges.getStartSeq() + 1;
+    viewWidth = vpranges.getEndRes() - vpranges.getStartRes() + 1;
+
+    HiddenColumns hiddenCols = new HiddenColumns();
+
+    od = new OverviewDimensionsHideHidden(vpranges, true);
+    // Initial box sizing - default path through code
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+
+    mouseClick(od, 0, 0);
+    moveViewport(0, 0);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+  }
+
+  @AfterClass(alwaysRun = true)
+  public void cleanUp()
+  {
+    al = null;
+  }
+
+  /**
+   * Test that the OverviewDimensions constructor sets width and height
+   * correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testConstructor()
+  {
+    SequenceI seqa = new Sequence("Seq1", "ABC");
+    SequenceI seqb = new Sequence("Seq2", "ABC");
+    SequenceI seqc = new Sequence("Seq3", "ABC");
+    SequenceI seqd = new Sequence("Seq4", "ABC");
+    SequenceI seqe = new Sequence("Seq5",
+            "ABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+    int defaultGraphHeight = 20;
+    int maxWidth = 400;
+    int minWidth = 120;
+    int maxSeqHeight = 300;
+    int minSeqHeight = 40;
+
+    // test for alignment with width > height
+    SequenceI[] seqs1 = new SequenceI[] { seqa, seqb };
+    Alignment al1 = new Alignment(seqs1);
+    ViewportRanges props = new ViewportRanges(al1);
+
+    OverviewDimensions od = new OverviewDimensionsHideHidden(props, true);
+    int scaledHeight = 267;
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), scaledHeight);
+    assertEquals(od.getWidth(), maxWidth);
+    assertEquals(od.getHeight(), scaledHeight + defaultGraphHeight);
+
+    // test for alignment with width < height
+    SequenceI[] seqs2 = new SequenceI[] { seqa, seqb, seqc, seqd };
+    Alignment al2 = new Alignment(seqs2);
+    props = new ViewportRanges(al2);
+
+    od = new OverviewDimensionsHideHidden(props, true);
+    int scaledWidth = 300;
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), maxSeqHeight);
+    assertEquals(od.getWidth(), scaledWidth);
+    assertEquals(od.getHeight(), scaledWidth + defaultGraphHeight);
+
+    // test for alignment with width > height and sequence height scaled below
+    // min value
+    SequenceI[] seqs3 = new SequenceI[] { seqe };
+    Alignment al3 = new Alignment(seqs3);
+    props = new ViewportRanges(al3);
+
+    od = new OverviewDimensionsHideHidden(props, true);
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), minSeqHeight);
+    assertEquals(od.getWidth(), maxWidth);
+    assertEquals(od.getHeight(), minSeqHeight + defaultGraphHeight);
+
+    // test for alignment with width < height and width scaled below min value
+    SequenceI[] seqs4 = new SequenceI[] { seqa, seqb, seqc, seqd, seqa,
+        seqb, seqc, seqd, seqa, seqb, seqc, seqd, seqa, seqb, seqc, seqd };
+    Alignment al4 = new Alignment(seqs4);
+    props = new ViewportRanges(al4);
+
+    od = new OverviewDimensionsHideHidden(props, true);
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), maxSeqHeight);
+    assertEquals(od.getWidth(), minWidth);
+    assertEquals(od.getHeight(), maxSeqHeight + defaultGraphHeight);
+
+    Alignment al5 = new Alignment(seqs4);
+    props = new ViewportRanges(al5);
+
+    od = new OverviewDimensionsHideHidden(props, false);
+    assertEquals(od.getGraphHeight(), 0);
+    assertEquals(od.getSequencesHeight(), maxSeqHeight);
+    assertEquals(od.getWidth(), minWidth);
+    assertEquals(od.getHeight(), maxSeqHeight);
+  }
+
+  /**
+   * Test that validation after mouse adjustments to boxX and boxY sets box
+   * dimensions and scroll values correctly, when there are no hidden rows or
+   * columns.
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromMouseClick()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // negative boxX value reset to 0
+    mouseClick(od, -5, 10);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartSeq(),
+            Math.round((float) 10 * alheight / od.getSequencesHeight()));
+    assertEquals(vpranges.getStartRes(), 0);
+
+    // negative boxY value reset to 0
+    mouseClick(od, 6, -2);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) 6 * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // overly large boxX value reset to width-boxWidth
+    mouseClick(od, 100, 6);
+    assertEquals(od.getBoxX(), od.getWidth() - od.getBoxWidth());
+    assertEquals(od.getBoxY(), 6);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+    // overly large boxY value reset to sequenceHeight - boxHeight
+    mouseClick(od, 10, 520);
+    assertEquals(od.getBoxX(), 10);
+    assertEquals(od.getBoxY(), od.getSequencesHeight() - od.getBoxHeight());
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+
+    // here (float) od.getBoxY() * alheight / od.getSequencesHeight() = 507.5
+    // and round rounds to 508; however we get 507 working with row values
+    // hence the subtraction of 1
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()) - 1);
+
+    // click past end of alignment, as above
+    mouseClick(od, 3000, 5);
+    assertEquals(od.getBoxX(), od.getWidth() - od.getBoxWidth());
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+    // move viewport so startRes non-zero and then mouseclick
+    moveViewportH(50);
+
+    // click at viewport position
+    int oldboxx = od.getBoxX();
+    int oldboxy = od.getBoxY();
+    mouseClick(od, od.getBoxX() + 5, od.getBoxY() + 2);
+    assertEquals(od.getBoxX(), oldboxx + 5);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(od.getBoxY(), oldboxy + 2);
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+    // click at top corner
+    mouseClick(od, 0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden cols at the start
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenColsAtStart()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide cols at start and check updated box position is correct
+    int lastHiddenCol = 30;
+    hiddenCols.hideColumns(0, lastHiddenCol);
+
+    testBoxIsAtClickPoint(0, 0);
+
+    // click to right of hidden columns, box moves to click point
+    testBoxIsAtClickPoint(40, 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) 40 * alwidth / od.getWidth()));
+
+    // click to right of hidden columns such that box runs over right hand side
+    // of alignment
+    // box position is adjusted away from the edge
+    // overly large boxX value reset to width-boxWidth
+    int xpos = 100;
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(), Math.round(od.getWidth()) - boxWidth);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden cols in the middle
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenColsInMiddle()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    testBoxIsAtClickPoint(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    
+    // hide columns 63-73, no change to box position or dimensions
+    int firstHidden = 63;
+    int lastHidden = 73;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    testBoxIsAtClickPoint(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so that it overlaps with hidden cols on one side
+    // box width, boxX and scrollCol as for unhidden case
+    int xpos = 55 - boxWidth; // 55 is position in overview approx halfway
+                              // between cols 60 and 70
+    mouseClick(od, xpos, 0);
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round(xpos * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so that it completely covers hidden cols
+    // box width, boxX and scrollCol as for unhidden case
+    xpos = 33;
+    mouseClick(od, xpos, 0);
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) xpos * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so boxX is in hidden cols, box overhangs at right
+    // boxX and scrollCol at left of hidden area, box width unchanged
+    xpos = 50;
+    mouseClick(od, xpos, 0);
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) xpos * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so boxX is to right of hidden cols, but does not go beyond full
+    // width of alignment
+    // box width, boxX and scrollCol all as for non-hidden case
+    xpos = 75;
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round(xpos * alwidth / od.getWidth()));
+    
+    // move box so it goes beyond full width of alignment
+    // boxX, scrollCol adjusted back, box width normal
+    xpos = 3000;
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(), Math.round(od.getWidth()) - boxWidth);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden cols at the end of
+   * the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenColsAtEnd()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide columns 140-164, no change to box position or dimensions
+    int firstHidden = 140;
+    int lastHidden = 164;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // click to left of hidden cols, without overlapping
+    // boxX, scrollCol and width as normal
+    int xpos = 5;
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) xpos * alwidth / od.getWidth()));
+
+    // click to left of hidden cols, with overlap
+    // boxX and scrollCol adjusted for hidden cols, width normal
+    xpos = Math.round((float) 145 * od.getWidth() / alwidth) - boxWidth;
+    mouseClick(od, xpos, 0);
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) xpos * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // click off end of alignment
+    // boxX and scrollCol adjusted backwards, width normal
+    xpos = 3000;
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(), Math.round(od.getWidth()) - boxWidth);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+  }
+
+  /**
+   * Test that the box position is set correctly when set from the viewport,
+   * with no hidden rows or columns
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewport()
+  {
+    // move viewport to start of alignment
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to right
+    moveViewportH(70);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 70 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport down
+    moveViewportV(100);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 70 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(),
+            Math.round(100 * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to bottom right
+    moveViewport(98, 508);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 98 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(),
+            Math.round((float) 508 * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden columns
+   * at the start
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenColsAtStart()
+  {
+    int firstHidden = 0;
+    int lastHidden = 20;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    // move viewport to start of alignment
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to end of alignment - need to make startRes by removing
+    // hidden cols because of how viewport/overview are implemented
+    moveViewport(98 - lastHidden - 1, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (98 - lastHidden - 1) * od.getWidth()
+                    / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden columns
+   * in the middle
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenColsInMiddle()
+  {
+    int firstHidden = 68;
+    int lastHidden = 78;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    // move viewport before hidden columns
+    moveViewport(3, 0);
+
+    assertEquals(od.getBoxX(),
+            Math.round((float) 3 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to left of hidden columns with overlap
+    moveViewport(10, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 10 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to straddle hidden columns
+    moveViewport(63, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 63 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to right of hidden columns, no overlap
+    moveViewport(80 - (lastHidden - firstHidden + 1), 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (80 - (lastHidden - firstHidden + 1))
+                    * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden columns
+   * at the end
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenColsAtEnd()
+  {
+    int firstHidden = 152;
+    int lastHidden = 164;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    // move viewport before hidden columns
+    moveViewport(3, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 3 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to hidden columns
+    // viewport can't actually extend into hidden cols,
+    // so move to the far right edge of the viewport
+    moveViewport(firstHidden - viewWidth, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (firstHidden - viewWidth)
+                    * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden rows at
+   * the start
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenRowsAtStart()
+  {
+    int firstHidden = 0;
+    int lastHidden = 20;
+    hideSequences(firstHidden, lastHidden);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+
+    // move viewport to start of alignment:
+    // box moves to below hidden rows, height remains same
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to end of alignment
+    moveViewport(0, 525 - viewHeight - lastHidden - 1);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) (525 - viewHeight - lastHidden - 1)
+                    * od.getSequencesHeight()
+                    / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden rows in
+   * the middle
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenRowsInMiddle()
+  {
+    int firstHidden = 200;
+    int lastHidden = 210;
+    hideSequences(firstHidden, lastHidden);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+
+    // move viewport to start of alignment:
+    // box, height etc as in non-hidden case
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to straddle hidden rows
+    moveViewport(0, 198);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), Math.round ((float)198 * od.getSequencesHeight()
+            / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden rows at
+   * the bottom
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenRowsAtEnd()
+  {
+    int firstHidden = 500;
+    int lastHidden = 524;
+    hideSequences(firstHidden, lastHidden);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+
+    // move viewport to start of alignment:
+    // box, height etc as in non-hidden case
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to end of alignment
+    // viewport sits above hidden rows and does not include them
+    moveViewport(0, firstHidden - viewHeight - 1);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) (firstHidden - viewHeight - 1)
+                    * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden rows at the start
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenRowsAtStart()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide rows at start and check updated box position is correct
+    int lastHiddenRow = 30;
+    hideSequences(0, lastHiddenRow);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click below hidden rows
+    mouseClick(od, 0, 150);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 150);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden rows at the middle
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenRowsInMiddle()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide rows in middle and check updated box position is correct
+    // no changes
+    int firstHiddenRow = 50;
+    int lastHiddenRow = 54;
+    hideSequences(firstHiddenRow, lastHiddenRow);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click above hidden rows, so that box overlaps
+    int ypos = 35; // column value in residues
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click so that box straddles hidden rows
+    ypos = 44; // column value in residues
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden rows at the end of
+   * the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenRowsAtEnd()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide rows at end and check updated box position is correct
+    // no changes
+    int firstHidden = 500;
+    int lastHidden = 524;
+    hideSequences(firstHidden, lastHidden);
+
+    // calculate with visible values
+    alheight = vpranges.getVisibleAlignmentHeight();
+    alwidth = vpranges.getVisibleAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click above hidden rows
+    int ypos = 40; // row 40
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click above hidden rows so box overlaps
+    // boxY, boxHeight remains same
+    ypos = 497; // row 497
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) firstHidden * od.getSequencesHeight()
+                    / alheight)
+                    - boxHeight);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /*
+   * Move viewport horizontally: startRes + previous width gives new horizontal extent. Vertical extent stays the same.
+   */
+  private void moveViewportH(int startRes)
+  {
+    vpranges.setViewportStartAndWidth(startRes, viewWidth);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+
+  /*
+   * Move viewport vertically: startSeq and endSeq give new vertical extent. Horizontal extent stays the same.
+   */
+  private void moveViewportV(int startSeq)
+  {
+    vpranges.setViewportStartAndHeight(startSeq, viewHeight);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+
+  /*
+   * Move viewport horizontally and vertically.
+   */
+  private void moveViewport(int startRes, int startSeq)
+  {
+    vpranges.setViewportStartAndWidth(startRes, viewWidth);
+    vpranges.setViewportStartAndHeight(startSeq, viewHeight);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+
+  /*
+   * Mouse click as position x,y in overview window
+   */
+  private void mouseClick(OverviewDimensions od, int x, int y)
+  {
+    od.updateViewportFromMouse(x, y, al.getHiddenSequences(), hiddenCols);
+
+    // updates require an OverviewPanel to exist which it doesn't here
+    // so call setBoxPosition() as it would be called by the AlignmentPanel
+    // normally
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+  
+  /*
+   * Test that the box is positioned with the top left corner at xpos, ypos
+   * and with the original width and height
+   */
+  private void testBoxIsAtClickPoint(int xpos, int ypos)
+  {
+    mouseClick(od, xpos, ypos);
+    assertEquals(od.getBoxX(), xpos);
+    assertEquals(od.getBoxY(), ypos);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+  }
+
+  /*
+   * Hide sequences between start and end
+   */
+  private void hideSequences(int start, int end)
+  {
+    SequenceI[] allseqs = al.getSequencesArray();
+    SequenceGroup theseSeqs = new SequenceGroup();
+    
+    for (int i = start; i <= end; i++)
+    {
+      theseSeqs.addSequence(allseqs[i], false);
+      al.getHiddenSequences().hideSequence(allseqs[i]);
+    }
+
+    hiddenRepSequences.put(allseqs[start], theseSeqs);
+  }
+}
diff --git a/test/jalview/viewmodel/OverviewDimensionsShowHiddenTest.java b/test/jalview/viewmodel/OverviewDimensionsShowHiddenTest.java
new file mode 100644 (file)
index 0000000..1bc3bfa
--- /dev/null
@@ -0,0 +1,1025 @@
+/*
+ * 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.viewmodel;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.analysis.AlignmentGenerator;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+
+import java.util.Hashtable;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Test(singleThreaded = true)
+public class OverviewDimensionsShowHiddenTest
+{
+  AlignmentI al;
+  OverviewDimensionsShowHidden od;
+
+  // cached widths and heights
+  int boxWidth;
+  int boxHeight;
+  int viewHeight;
+  int viewWidth;
+  int alheight;
+  int alwidth;
+
+  ViewportRanges vpranges;
+
+  Hashtable<SequenceI, SequenceCollectionI> hiddenRepSequences = new Hashtable<SequenceI, SequenceCollectionI>();
+
+  HiddenColumns hiddenCols = new HiddenColumns();
+
+  @BeforeClass(alwaysRun = true)
+  public void setUpAlignment()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    al = gen.generate(157, 525, 123, 5, 5);
+  }
+
+  @BeforeMethod(alwaysRun = true)
+  public void setUp()
+  {
+    if (!hiddenRepSequences.isEmpty())
+    {
+      al.getHiddenSequences().showAll(hiddenRepSequences);
+    }
+    ColumnSelection colsel = new ColumnSelection();
+    hiddenCols.revealAllHiddenColumns(colsel);
+    
+    vpranges = new ViewportRanges(al);
+    vpranges.setViewportStartAndHeight(0, 18);
+    vpranges.setViewportStartAndWidth(0, 63);
+
+    viewHeight = vpranges.getEndSeq() - vpranges.getStartSeq() + 1;
+    viewWidth = vpranges.getEndRes() - vpranges.getStartRes() + 1;
+
+    HiddenColumns hiddenCols = new HiddenColumns();
+
+    od = new OverviewDimensionsShowHidden(vpranges, true);
+    // Initial box sizing - default path through code
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+
+    mouseClick(od, 0, 0);
+    moveViewport(0, 0);
+
+    // calculate before hidden columns so we get absolute values
+    alheight = vpranges.getAbsoluteAlignmentHeight();
+    alwidth = vpranges.getAbsoluteAlignmentWidth();
+
+    boxWidth = Math.round((float) (vpranges.getEndRes()
+            - vpranges.getStartRes() + 1)
+            * od.getWidth() / alwidth);
+    boxHeight = Math.round((float) (vpranges.getEndSeq()
+            - vpranges.getStartSeq() + 1)
+            * od.getSequencesHeight() / alheight);
+  }
+
+  @AfterClass(alwaysRun = true)
+  public void cleanUp()
+  {
+    al = null;
+  }
+
+  /**
+   * Test that the OverviewDimensions constructor sets width and height
+   * correctly
+   */
+  @Test(groups = { "Functional" })
+  public void testConstructor()
+  {
+    SequenceI seqa = new Sequence("Seq1", "ABC");
+    SequenceI seqb = new Sequence("Seq2", "ABC");
+    SequenceI seqc = new Sequence("Seq3", "ABC");
+    SequenceI seqd = new Sequence("Seq4", "ABC");
+    SequenceI seqe = new Sequence("Seq5",
+            "ABCABCABCABCABCABCABCABCBACBACBACBAC");
+
+    int defaultGraphHeight = 20;
+    int maxWidth = 400;
+    int minWidth = 120;
+    int maxSeqHeight = 300;
+    int minSeqHeight = 40;
+
+    // test for alignment with width > height
+    SequenceI[] seqs1 = new SequenceI[] { seqa, seqb };
+    Alignment al1 = new Alignment(seqs1);
+    ViewportRanges props = new ViewportRanges(al1);
+
+    OverviewDimensions od = new OverviewDimensionsShowHidden(props, true);
+    int scaledHeight = 267;
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), scaledHeight);
+    assertEquals(od.getWidth(), maxWidth);
+    assertEquals(od.getHeight(), scaledHeight + defaultGraphHeight);
+
+    // test for alignment with width < height
+    SequenceI[] seqs2 = new SequenceI[] { seqa, seqb, seqc, seqd };
+    Alignment al2 = new Alignment(seqs2);
+    props = new ViewportRanges(al2);
+
+    od = new OverviewDimensionsShowHidden(props, true);
+    int scaledWidth = 300;
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), maxSeqHeight);
+    assertEquals(od.getWidth(), scaledWidth);
+    assertEquals(od.getHeight(), scaledWidth + defaultGraphHeight);
+
+    // test for alignment with width > height and sequence height scaled below
+    // min value
+    SequenceI[] seqs3 = new SequenceI[] { seqe };
+    Alignment al3 = new Alignment(seqs3);
+    props = new ViewportRanges(al3);
+
+    od = new OverviewDimensionsShowHidden(props, true);
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), minSeqHeight);
+    assertEquals(od.getWidth(), maxWidth);
+    assertEquals(od.getHeight(), minSeqHeight + defaultGraphHeight);
+
+    // test for alignment with width < height and width scaled below min value
+    SequenceI[] seqs4 = new SequenceI[] { seqa, seqb, seqc, seqd, seqa,
+        seqb, seqc, seqd, seqa, seqb, seqc, seqd, seqa, seqb, seqc, seqd };
+    Alignment al4 = new Alignment(seqs4);
+    props = new ViewportRanges(al4);
+
+    od = new OverviewDimensionsShowHidden(props, true);
+    assertEquals(od.getGraphHeight(), defaultGraphHeight);
+    assertEquals(od.getSequencesHeight(), maxSeqHeight);
+    assertEquals(od.getWidth(), minWidth);
+    assertEquals(od.getHeight(), maxSeqHeight + defaultGraphHeight);
+
+    Alignment al5 = new Alignment(seqs4);
+    props = new ViewportRanges(al5);
+
+    od = new OverviewDimensionsShowHidden(props, false);
+    assertEquals(od.getGraphHeight(), 0);
+    assertEquals(od.getSequencesHeight(), maxSeqHeight);
+    assertEquals(od.getWidth(), minWidth);
+    assertEquals(od.getHeight(), maxSeqHeight);
+  }
+
+  /**
+   * Test that validation after mouse adjustments to boxX and boxY sets box
+   * dimensions and scroll values correctly, when there are no hidden rows or
+   * columns.
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromMouseClick()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // negative boxX value reset to 0
+    mouseClick(od, -5, 10);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartSeq(),
+            Math.round((float) 10 * alheight / od.getSequencesHeight()));
+    assertEquals(vpranges.getStartRes(), 0);
+
+    // negative boxY value reset to 0
+    mouseClick(od, 6, -2);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) 6 * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // overly large boxX value reset to width-boxWidth
+    mouseClick(od, 100, 6);
+    assertEquals(od.getBoxX(), od.getWidth() - od.getBoxWidth());
+    assertEquals(od.getBoxY(), 6);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+    // overly large boxY value reset to sequenceHeight - boxHeight
+    mouseClick(od, 10, 520);
+    assertEquals(od.getBoxX(), 10);
+    assertEquals(od.getBoxY(), od.getSequencesHeight() - od.getBoxHeight());
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+
+    // here (float) od.getBoxY() * alheight / od.getSequencesHeight() = 507.5
+    // and round rounds to 508; however we get 507 working with row values
+    // hence the subtraction of 1
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()) - 1);
+
+    // click past end of alignment, as above
+    mouseClick(od, 3000, 5);
+    assertEquals(od.getBoxX(), od.getWidth() - od.getBoxWidth());
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+    // move viewport so startRes non-zero and then mouseclick
+    moveViewportH(50);
+
+    // click at viewport position
+    int oldboxx = od.getBoxX();
+    int oldboxy = od.getBoxY();
+    mouseClick(od, od.getBoxX() + 5, od.getBoxY() + 2);
+    assertEquals(od.getBoxX(), oldboxx + 5);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(od.getBoxY(), oldboxy + 2);
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+    // click at top corner
+    mouseClick(od, 0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden cols at the start
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenColsAtStart()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide cols at start and check updated box position is correct
+    // changes boxX but not boxwidth
+    int lastHiddenCol = 30;
+    hiddenCols.hideColumns(0, lastHiddenCol);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (lastHiddenCol + 1) * od.getWidth()
+                    / alwidth));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // try to click in hidden cols, check box does not move
+    int xpos = 10;
+    mouseClick(od, xpos, 0);
+    assertEquals(
+            od.getBoxX(),
+            Math.round((float) (lastHiddenCol + 1) * od.getWidth()
+                    / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(), 0);
+
+    // click to right of hidden columns, box moves to click point
+    testBoxIsAtClickPoint(40, 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) 40 * alwidth / od.getWidth())
+                    - (lastHiddenCol + 1));
+
+    // click to right of hidden columns such that box runs over right hand side
+    // of alignment
+    // box position is adjusted away from the edge
+    // overly large boxX value reset to width-boxWidth
+    xpos = 100;
+    mouseClick(od, xpos, 5);
+    assertEquals(od.getBoxX(), od.getWidth() - od.getBoxWidth());
+    assertEquals(od.getBoxY(), 5);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth())
+                    - (lastHiddenCol + 1));
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden cols in the middle
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenColsInMiddle()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    
+    // hide columns 63-73, no change to box position or dimensions
+    int firstHidden = 63;
+    int lastHidden = 73;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so that it overlaps with hidden cols on one side
+    // box width changes, boxX and scrollCol as for unhidden case
+    int xpos = 55 - boxWidth; // 55 is position in overview approx halfway
+                              // between cols 60 and 70
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(), xpos);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(
+            od.getBoxWidth(),
+            Math.round(boxWidth + (float) (lastHidden - firstHidden + 1)
+                    * od.getWidth() / alwidth));
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round(xpos * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so that it completely covers hidden cols
+    // box width changes, boxX and scrollCol as for hidden case
+    xpos = 33;
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(), xpos);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(
+            od.getBoxWidth(),
+            Math.round(boxWidth + (float) (lastHidden - firstHidden + 1)
+                    * od.getWidth() / alwidth));
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) xpos * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so boxX is in hidden cols, box overhangs at right
+    // boxX and scrollCol at left of hidden area, box width extends across
+    // hidden region
+    xpos = 50;
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (firstHidden - 1) * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(
+            od.getBoxWidth(),
+            boxWidth
+                    + Math.round((float) (lastHidden - firstHidden + 1)
+                            * od.getWidth() / alwidth));
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(), firstHidden - 1);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // move box so boxX is to right of hidden cols, but does not go beyond full
+    // width of alignment
+    // box width, boxX and scrollCol all as for non-hidden case
+    xpos = 75;
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round(xpos * alwidth / od.getWidth())
+                    - (lastHidden - firstHidden + 1));
+    
+    // move box so it goes beyond full width of alignment
+    // boxX, scrollCol adjusted back, box width normal
+    xpos = 3000;
+    mouseClick(od, xpos, 5);
+    assertEquals(od.getBoxX(), od.getWidth() - od.getBoxWidth());
+    assertEquals(od.getBoxY(), 5);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(
+            vpranges.getStartRes(),
+            Math.round(((float) od.getBoxX() * alwidth / od.getWidth())
+                    - (lastHidden - firstHidden + 1)));
+    assertEquals(
+            vpranges.getStartSeq(),
+            Math.round((float) od.getBoxY() * alheight
+                    / od.getSequencesHeight()));
+
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden cols at the end of
+   * the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenColsAtEnd()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide columns 140-164, no change to box position or dimensions
+    int firstHidden = 140;
+    int lastHidden = 164;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // click to left of hidden cols, without overlapping
+    // boxX, scrollCol and width as normal
+    int xpos = 5;
+    testBoxIsAtClickPoint(xpos, 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) xpos * alwidth / od.getWidth()));
+
+    // click to left of hidden cols, with overlap
+    // boxX and scrollCol adjusted for hidden cols, width normal
+    xpos = Math.round((float) 145 * od.getWidth() / alwidth) - boxWidth;
+    mouseClick(od, xpos, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (firstHidden - 1) * od.getWidth() / alwidth)
+                    - boxWidth + 1);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // click in hidden cols
+    // boxX and scrollCol adjusted for hidden cols, width normal
+    xpos = 115;
+    assertEquals(od.getBoxX(),
+            Math.round((float) (firstHidden - 1) * od.getWidth() / alwidth)
+                    - boxWidth + 1);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // click off end of alignment
+    // boxX and scrollCol adjusted for hidden cols, width normal
+    xpos = 3000;
+    assertEquals(od.getBoxX(),
+            Math.round((float) (firstHidden - 1) * od.getWidth() / alwidth)
+                    - boxWidth + 1);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(),
+            Math.round((float) od.getBoxX() * alwidth / od.getWidth()));
+    assertEquals(vpranges.getStartSeq(), 0);
+  }
+
+  /**
+   * Test that the box position is set correctly when set from the viewport,
+   * with no hidden rows or columns
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewport()
+  {
+    // move viewport to start of alignment
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to right
+    moveViewportH(70);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 70 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport down
+    moveViewportV(100);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 70 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(),
+            Math.round(100 * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to bottom right
+    moveViewport(98, 508);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 98 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(),
+            Math.round((float) 508 * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden columns
+   * at the start
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenColsAtStart()
+  {
+    int firstHidden = 0;
+    int lastHidden = 20;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    // move viewport to start of alignment
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (lastHidden + 1) * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to end of alignment - need to make startRes by removing
+    // hidden cols because of how viewport/overview are implemented
+    moveViewport(98 - lastHidden - 1, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 98 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden columns
+   * in the middle
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenColsInMiddle()
+  {
+    int firstHidden = 68;
+    int lastHidden = 78;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    // move viewport before hidden columns
+    moveViewport(3, 0);
+
+    assertEquals(od.getBoxX(),
+            Math.round((float) 3 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to left of hidden columns with overlap
+    moveViewport(10, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 10 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(
+            od.getBoxWidth(),
+            boxWidth
+                    + Math.round((float) (lastHidden - firstHidden + 1)
+                            * od.getWidth() / alwidth));
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to straddle hidden columns
+    moveViewport(63, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 63 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(
+            od.getBoxWidth(),
+            boxWidth
+                    + Math.round((lastHidden - firstHidden + 1)
+                            * od.getWidth() / alwidth));
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to right of hidden columns, no overlap
+    moveViewport(80 - (lastHidden - firstHidden + 1), 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 80 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden columns
+   * at the end
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenColsAtEnd()
+  {
+    int firstHidden = 152;
+    int lastHidden = 164;
+    hiddenCols.hideColumns(firstHidden, lastHidden);
+
+    // move viewport before hidden columns
+    moveViewport(3, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) 3 * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to hidden columns
+    // viewport can't actually extend into hidden cols,
+    // so move to the far right edge of the viewport
+    moveViewport(firstHidden - viewWidth, 0);
+    assertEquals(od.getBoxX(),
+            Math.round((float) (firstHidden - viewWidth)
+                    * od.getWidth() / alwidth));
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden rows at
+   * the start
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenRowsAtStart()
+  {
+    int firstHidden = 0;
+    int lastHidden = 20;
+    hideSequences(firstHidden, lastHidden);
+
+    // move viewport to start of alignment:
+    // box moves to below hidden rows, height remains same
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) (lastHidden + 1) * od.getSequencesHeight()
+                    / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to end of alignment
+    moveViewport(0, 525 - viewHeight - lastHidden - 1);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) (525 - viewHeight) * od.getSequencesHeight()
+                    / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden rows in
+   * the middle
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenRowsInMiddle()
+  {
+    int firstHidden = 200;
+    int lastHidden = 210;
+    hideSequences(firstHidden, lastHidden);
+
+    // move viewport to start of alignment:
+    // box, height etc as in non-hidden case
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to straddle hidden rows
+    moveViewport(0, 198);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), Math.round ((float)198 * od.getSequencesHeight()
+            / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(
+            od.getBoxHeight(),
+            Math.round((float) (viewHeight + lastHidden - firstHidden + 1)
+                    * od.getSequencesHeight() / alheight));
+  }
+
+  /**
+   * Test that the box position is set correctly when there are hidden rows at
+   * the bottom
+   */
+  @Test(groups = { "Functional" })
+  public void testSetBoxFromViewportHiddenRowsAtEnd()
+  {
+    int firstHidden = 500;
+    int lastHidden = 524;
+    hideSequences(firstHidden, lastHidden);
+
+    // move viewport to start of alignment:
+    // box, height etc as in non-hidden case
+    moveViewport(0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // move viewport to end of alignment
+    // viewport sits above hidden rows and does not include them
+    moveViewport(0, firstHidden - viewHeight - 1);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) (firstHidden - viewHeight - 1)
+                    * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden rows at the start
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenRowsAtStart()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide rows at start and check updated box position is correct
+    // changes boxY but not boxheight
+    int lastHiddenRow = 30;
+    hideSequences(0, lastHiddenRow);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) (lastHiddenRow + 1)
+                    * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click in hidden rows - same result
+    mouseClick(od, 0, 0);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) (lastHiddenRow + 1)
+                    * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click below hidden rows
+    mouseClick(od, 0, 150);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 150);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden rows at the middle
+   * of the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenRowsInMiddle()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide rows in middle and check updated box position is correct
+    // no changes
+    int firstHiddenRow = 50;
+    int lastHiddenRow = 54;
+    hideSequences(firstHiddenRow, lastHiddenRow);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click above hidden rows, so that box overlaps
+    int ypos = 35; // column value in residues
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(
+            od.getBoxHeight(),
+            boxHeight
+                    + Math.round((float) (lastHiddenRow - firstHiddenRow + 1)
+                            * od.getSequencesHeight() / alheight));
+
+    // click so that box straddles hidden rows
+    ypos = 44; // column value in residues
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(
+            od.getBoxHeight(),
+            boxHeight
+                    + Math.round((float) (lastHiddenRow - firstHiddenRow + 1)
+                            * od.getSequencesHeight() / alheight));
+  }
+
+  /**
+   * Test setting of the box position, when there are hidden rows at the end of
+   * the alignment
+   */
+  @Test(groups = { "Functional" })
+  public void testFromMouseWithHiddenRowsAtEnd()
+  {
+    od.updateViewportFromMouse(0, 0, al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+    assertEquals(vpranges.getStartRes(), 0);
+    assertEquals(vpranges.getStartSeq(), 0);
+
+    // hide rows at end and check updated box position is correct
+    // no changes
+    int firstHidden = 500;
+    int lastHidden = 524;
+    hideSequences(firstHidden, lastHidden);
+
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(), 0);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click above hidden rows
+    int ypos = 40; // row 40
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(od.getBoxY(),
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click above hidden rows so box overlaps
+    // boxY moved upwards, boxHeight remains same
+    ypos = 497; // row 497
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((float) (firstHidden - viewHeight)
+                    * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+    // click within hidden rows
+    ypos = 505;
+    mouseClick(od, 0,
+            Math.round((float) ypos * od.getSequencesHeight() / alheight));
+    assertEquals(od.getBoxX(), 0);
+    assertEquals(
+            od.getBoxY(),
+            Math.round((firstHidden - viewHeight) * od.getSequencesHeight()
+                    / alheight));
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+  }
+
+  /*
+   * Move viewport horizontally: startRes + previous width gives new horizontal extent. Vertical extent stays the same.
+   */
+  private void moveViewportH(int startRes)
+  {
+    vpranges.setViewportStartAndWidth(startRes, viewWidth);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+
+  /*
+   * Move viewport vertically: startSeq and endSeq give new vertical extent. Horizontal extent stays the same.
+   */
+  private void moveViewportV(int startSeq)
+  {
+    vpranges.setViewportStartAndHeight(startSeq, viewHeight);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+
+  /*
+   * Move viewport horizontally and vertically.
+   */
+  private void moveViewport(int startRes, int startSeq)
+  {
+    vpranges.setViewportStartAndWidth(startRes, viewWidth);
+    vpranges.setViewportStartAndHeight(startSeq, viewHeight);
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+
+  /*
+   * Mouse click as position x,y in overview window
+   */
+  private void mouseClick(OverviewDimensions od, int x, int y)
+  {
+    od.updateViewportFromMouse(x, y, al.getHiddenSequences(), hiddenCols);
+
+    // updates require an OverviewPanel to exist which it doesn't here
+    // so call setBoxPosition() as it would be called by the AlignmentPanel
+    // normally
+    od.setBoxPosition(al.getHiddenSequences(), hiddenCols);
+  }
+  
+  /*
+   * Test that the box is positioned with the top left corner at xpos, ypos
+   * and with the original width and height
+   */
+  private void testBoxIsAtClickPoint(int xpos, int ypos)
+  {
+    mouseClick(od, xpos, ypos);
+    assertEquals(od.getBoxX(), xpos);
+    assertEquals(od.getBoxY(), ypos);
+    assertEquals(od.getBoxWidth(), boxWidth);
+    assertEquals(od.getBoxHeight(), boxHeight);
+
+  }
+
+  /*
+   * Hide sequences between start and end
+   */
+  private void hideSequences(int start, int end)
+  {
+    SequenceI[] allseqs = al.getSequencesArray();
+    SequenceGroup theseSeqs = new SequenceGroup();
+    
+    for (int i = start; i <= end; i++)
+    {
+      theseSeqs.addSequence(allseqs[i], false);
+      al.getHiddenSequences().hideSequence(allseqs[i]);
+    }
+
+    hiddenRepSequences.put(allseqs[start], theseSeqs);
+  }
+}
diff --git a/test/jalview/viewmodel/ViewportRangesTest.java b/test/jalview/viewmodel/ViewportRangesTest.java
new file mode 100644 (file)
index 0000000..636f8dd
--- /dev/null
@@ -0,0 +1,568 @@
+package jalview.viewmodel;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import jalview.analysis.AlignmentGenerator;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.HiddenSequences;
+
+import java.beans.PropertyChangeEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+public class ViewportRangesTest {
+
+  AlignmentGenerator gen = new AlignmentGenerator(false);
+
+  AlignmentI al = gen.generate(20, 30, 1, 5, 5);
+
+  AlignmentI smallAl = gen.generate(7, 2, 2, 5, 5);
+
+  @BeforeMethod(alwaysRun = true)
+  public void cleanUp()
+  {
+    ColumnSelection sel = new ColumnSelection();
+    al.getHiddenColumns().revealAllHiddenColumns(sel);
+    al.getHiddenSequences().showAll(null);
+    smallAl.getHiddenColumns().revealAllHiddenColumns(sel);
+    smallAl.getHiddenSequences().showAll(null);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testViewportRanges() 
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    
+    assertEquals(vr.getStartRes(),0);
+    assertEquals(vr.getEndRes(), al.getWidth()-1);
+    assertEquals(vr.getStartSeq(), 0);
+    assertEquals(vr.getEndSeq(), al.getHeight() - 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetAbsoluteAlignmentHeight()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+
+    assertEquals(vr.getAbsoluteAlignmentHeight(), al.getHeight());
+
+    al.getHiddenSequences().hideSequence(al.getSequenceAt(3));
+    assertEquals(vr.getAbsoluteAlignmentHeight(), al.getHeight() + 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetAbsoluteAlignmentWidth()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    assertEquals(vr.getAbsoluteAlignmentWidth(), al.getWidth());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetEndRes()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setEndRes(-1);
+    assertEquals(vr.getEndRes(), 0);
+
+    vr.setEndRes(al.getWidth() - 1);
+    assertEquals(vr.getEndRes(), al.getWidth() - 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetEndSeq()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setEndSeq(-1);
+    assertEquals(vr.getEndSeq(), 0);
+
+    vr.setEndSeq(al.getHeight());
+    assertEquals(vr.getEndSeq(), al.getHeight() - 1);
+
+    vr.setEndRes(al.getHeight() - 1);
+    assertEquals(vr.getEndSeq(), al.getHeight() - 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetStartRes()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setStartRes(-1);
+    assertEquals(vr.getStartRes(), 0);
+
+    vr.setStartRes(al.getWidth());
+    assertEquals(vr.getStartRes(), al.getWidth() - 1);
+
+    vr.setStartRes(al.getWidth() - 1);
+    assertEquals(vr.getStartRes(), al.getWidth() - 1);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetStartSeq()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setStartSeq(-1);
+    assertEquals(vr.getStartSeq(), 0);
+
+    vr.setStartSeq(al.getHeight() - vr.getViewportHeight() + 1);
+    assertEquals(vr.getStartSeq(), al.getHeight() - vr.getViewportHeight());
+
+    vr.setStartSeq(al.getHeight() - vr.getViewportHeight());
+    assertEquals(vr.getStartSeq(), al.getHeight() - vr.getViewportHeight());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetStartEndRes()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setStartEndRes(-1, -1);
+    assertEquals(vr.getStartRes(), 0);
+    assertEquals(vr.getEndRes(), 0);
+
+    vr.setStartEndRes(5, 19);
+    assertEquals(vr.getStartRes(), 5);
+    assertEquals(vr.getEndRes(), 19);
+
+    vr.setStartEndRes(al.getWidth(), al.getWidth());
+    assertEquals(vr.getEndRes(), al.getWidth() - 1);
+
+    ViewportRanges vrsmall = new ViewportRanges(smallAl);
+    vrsmall.setStartEndRes(al.getWidth(), al.getWidth());
+    assertEquals(vrsmall.getEndRes(), 6);
+
+    // make visible alignment width = 0
+    smallAl.getHiddenColumns().hideColumns(0, 6);
+    vrsmall.setStartEndRes(0, 4);
+    assertEquals(vrsmall.getStartRes(), 0);
+    assertEquals(vrsmall.getEndRes(), 0);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetStartEndSeq()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setStartEndSeq(-1, -1);
+    assertEquals(vr.getStartSeq(), 0);
+    assertEquals(vr.getEndSeq(), 0);
+
+    vr.setStartEndSeq(5, 19);
+    assertEquals(vr.getStartSeq(), 5);
+    assertEquals(vr.getEndSeq(), 19);
+
+    vr.setStartEndSeq(al.getHeight(), al.getHeight());
+    assertEquals(vr.getEndSeq(), al.getHeight() - 1);
+
+    // make visible alignment height = 0
+    smallAl.getHiddenSequences().hideSequence(smallAl.getSequenceAt(0));
+    smallAl.getHiddenSequences().hideSequence(smallAl.getSequenceAt(0));
+    ViewportRanges vrsmall = new ViewportRanges(smallAl);
+    vrsmall.setStartEndSeq(0, 3);
+    assertEquals(vrsmall.getStartSeq(), 0);
+    assertEquals(vrsmall.getEndSeq(), 0);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetViewportHeight()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportHeight(13);
+    assertEquals(vr.getViewportHeight(), 13);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetViewportWidth()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportWidth(13);
+    assertEquals(vr.getViewportWidth(), 13);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetViewportStartAndHeight()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndHeight(2, 6);
+    assertEquals(vr.getViewportHeight(), 6);
+    assertEquals(vr.getStartSeq(), 2);
+
+    // reset -ve values of start to 0
+    vr.setViewportStartAndHeight(-1, 7);
+    assertEquals(vr.getViewportHeight(), 7);
+    assertEquals(vr.getStartSeq(), 0);
+
+    // reset out of bounds start values to within bounds
+    vr.setViewportStartAndHeight(35, 5);
+    assertEquals(vr.getViewportHeight(), 5);
+    assertEquals(vr.getStartSeq(), 24);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetViewportStartAndWidth()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndWidth(2, 6);
+    assertEquals(vr.getViewportWidth(), 6);
+    assertEquals(vr.getStartRes(), 2);
+
+    // reset -ve values of start to 0
+    vr.setViewportStartAndWidth(-1, 7);
+    assertEquals(vr.getViewportWidth(), 7);
+    assertEquals(vr.getStartRes(), 0);
+
+    // reset out of bounds start values to within bounds
+    vr.setViewportStartAndWidth(35, 5);
+    assertEquals(vr.getViewportWidth(), 5);
+    assertEquals(vr.getStartRes(), 16);
+
+    // small alignment doesn't get bounds reset
+    ViewportRanges vrsmall = new ViewportRanges(smallAl);
+    vrsmall.setViewportStartAndWidth(0, 63);
+    assertEquals(vrsmall.getViewportWidth(), 7);
+    assertEquals(vrsmall.getStartRes(), 0);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testPageUpDown()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndHeight(8, 6);
+    vr.pageDown();
+    assertEquals(vr.getStartSeq(), 13);
+
+    vr.pageUp();
+    assertEquals(vr.getStartSeq(), 8);
+
+    vr.pageUp();
+    assertEquals(vr.getStartSeq(), 3);
+
+    vr.pageUp();
+    // pageup does not go beyond 0, viewport height stays the same
+    assertEquals(vr.getStartSeq(), 0);
+    assertEquals(vr.getViewportHeight(), 6);
+
+    vr.pageDown();
+    vr.pageDown();
+    vr.pageDown();
+    vr.pageDown();
+    vr.pageDown();
+
+    // pagedown to bottom does not go beyond end, and height stays same
+    assertEquals(vr.getStartSeq(), 24);
+    assertEquals(vr.getViewportHeight(), 6);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testScrollUp()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndHeight(1, 5);
+    vr.scrollUp(true);
+    assertEquals(vr.getStartSeq(), 0);
+    // can't scroll above top
+    vr.scrollUp(true);
+    assertEquals(vr.getStartSeq(), 0);
+
+    vr.setViewportStartAndHeight(24, 5);
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 25);
+    // can't scroll beyond bottom
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 25);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testScrollUpWithHidden()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+
+    // hide last sequence
+    HiddenSequences hidden = new HiddenSequences(al);
+    hidden.hideSequence(al.getSequenceAt(29));
+
+    vr.setViewportStartAndHeight(1, 5);
+    vr.scrollUp(true);
+    assertEquals(vr.getStartSeq(), 0);
+    // can't scroll above top
+    vr.scrollUp(true);
+    assertEquals(vr.getStartSeq(), 0);
+
+    vr.setViewportStartAndHeight(23, 5);
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 24);
+    // can't scroll beyond bottom
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 24);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testScrollRight()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndWidth(1, 5);
+    vr.scrollRight(false);
+    assertEquals(vr.getStartRes(), 0);
+    // can't scroll left past start
+    vr.scrollRight(false);
+    assertEquals(vr.getStartRes(), 0);
+
+    vr.setViewportStartAndWidth(15, 5);
+    vr.scrollRight(true);
+    assertEquals(vr.getStartRes(), 16);
+    // can't scroll right past end
+    vr.scrollRight(true);
+    assertEquals(vr.getStartRes(), 16);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testScrollRightWithHidden()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+
+    // hide last 2 columns
+    HiddenColumns cols = new HiddenColumns();
+    cols.hideColumns(19, 20);
+    al.setHiddenColumns(cols);
+
+    vr.setViewportStartAndWidth(1, 5);
+    vr.scrollRight(false);
+    assertEquals(vr.getStartRes(), 0);
+    // can't scroll left past start
+    vr.scrollRight(false);
+    assertEquals(vr.getStartRes(), 0);
+
+    vr.setViewportStartAndWidth(13, 5);
+    vr.scrollRight(true);
+    assertEquals(vr.getStartRes(), 14);
+    // can't scroll right past last visible col
+    vr.scrollRight(true);
+    assertEquals(vr.getStartRes(), 14);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testScrollToWrappedVisible()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndWidth(5, 10);
+
+    vr.scrollToWrappedVisible(0);
+    assertEquals(vr.getStartRes(), 0);
+
+    vr.scrollToWrappedVisible(10);
+    assertEquals(vr.getStartRes(), 10);
+
+    vr.scrollToWrappedVisible(15);
+    assertEquals(vr.getStartRes(), 10);
+  }
+
+  // leave until JAL-2388 is merged and we can do without viewport
+  /*@Test(groups = { "Functional" })
+  public void testScrollToVisible()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportStartAndWidth(12,5);
+    vr.setViewportStartAndHeight(10,6);
+    vr.scrollToVisible(13,14)
+    
+    // no change
+    assertEquals(vr.getStartRes(), 12);
+    assertEquals(vr.getStartSeq(), 10);
+    
+    vr.scrollToVisible(5,6);
+    assertEquals(vr.getStartRes(), 5);
+    assertEquals(vr.getStartSeq(), 6);
+    
+    // test for hidden columns too
+  }*/
+
+  @Test(groups = { "Functional" })
+  public void testEventFiring()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    MockPropChangeListener l = new MockPropChangeListener(vr);
+    List<String> emptylist = new ArrayList<>();
+
+    vr.setViewportWidth(5);
+    vr.setViewportHeight(5);
+    l.reset();
+
+    // one event fired when startRes is called with new value
+    vr.setStartRes(4);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+
+    // no event fired for same value
+    vr.setStartRes(4);
+    assertTrue(l.verify(0, emptylist));
+    l.reset();
+
+    vr.setEndRes(10);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+
+    // no event fired for same value
+    vr.setEndRes(10);
+    assertTrue(l.verify(0, emptylist));
+    l.reset();
+
+    vr.setStartSeq(4);
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    vr.setStartSeq(4);
+    assertTrue(l.verify(0, emptylist));
+    l.reset();
+
+    vr.setEndSeq(10);
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    vr.setEndSeq(10);
+    assertTrue(l.verify(0, emptylist));
+    l.reset();
+
+    vr.setStartEndRes(2, 15);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+
+    vr.setStartEndRes(2, 15);
+    assertTrue(l.verify(0, emptylist));
+    l.reset();
+
+    // check new value fired by event is corrected startres
+    vr.setStartEndRes(-1, 5);
+    assertTrue(l.verify(1, Arrays.asList("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)));
+    l.reset();
+
+    vr.setStartEndSeq(2, 15);
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    vr.setStartEndSeq(2, 15);
+    assertTrue(l.verify(0, emptylist));
+    l.reset();
+
+    vr.setStartEndRes(2, 2); // so seq and res values should be different, in
+                             // case of transposing in code
+    l.reset();
+
+    // check new value fired by event is corrected startseq
+    vr.setStartEndSeq(-1, 5);
+    assertTrue(l.verify(1, Arrays.asList("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)));
+    l.reset();
+
+    // reset for later tests
+    vr.setStartEndSeq(2, 15);
+    l.reset();
+
+    // test viewport height and width setting triggers event
+    vr.setViewportHeight(10);
+    assertTrue(l.verify(1, Arrays.asList("endseq")));
+    l.reset();
+
+    vr.setViewportWidth(18);
+    assertTrue(l.verify(1, Arrays.asList("endres")));
+    l.reset();
+
+    // already has seq start set to 2, so triggers endseq
+    vr.setViewportStartAndHeight(2, 16);
+    assertTrue(l.verify(1, Arrays.asList("endseq")));
+    l.reset();
+
+    vr.setViewportStartAndWidth(1, 14);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+
+    // test page up/down triggers event
+    vr.pageUp();
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    vr.pageDown();
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    // test scrolling triggers event
+    vr.scrollUp(true);
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    vr.scrollUp(false);
+    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    l.reset();
+
+    vr.scrollRight(true);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+
+    vr.scrollRight(false);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+
+    vr.scrollToVisible(10, 10);
+    assertTrue(l.verify(4,
+            Arrays.asList("startseq", "startseq", "startseq", "startseq")));
+    l.reset();
+
+    vr.scrollToWrappedVisible(5);
+    assertTrue(l.verify(1, Arrays.asList("startres")));
+    l.reset();
+  }
+}
+
+// mock listener for property change events
+class MockPropChangeListener implements ViewportListenerI
+{
+  private int firecount = 0;
+
+  private List<String> events = new ArrayList<>();
+
+  private List<Integer> newvalues = new ArrayList<>();
+
+  public MockPropChangeListener(ViewportRanges vr)
+  {
+    vr.addPropertyChangeListener(this);
+  }
+
+  @Override
+  public void propertyChange(PropertyChangeEvent evt)
+  {
+    firecount++;
+    events.add(evt.getPropertyName());
+    newvalues.add((Integer) evt.getNewValue());
+  }
+
+  public boolean verify(int count, List<String> eventslist,
+          List<Integer> valueslist)
+  {
+    return (count == firecount) && events.equals(eventslist)
+            && newvalues.equals(valueslist);
+  }
+
+  public boolean verify(int count, List<String> eventslist)
+  {
+    return (count == firecount) && events.equals(eventslist);
+  }
+
+  public void reset()
+  {
+    firecount = 0;
+    events.clear();
+    newvalues.clear();
+  }
+}
diff --git a/test/jalview/ws/dbsources/RemoteFormatTest.java b/test/jalview/ws/dbsources/RemoteFormatTest.java
new file mode 100644 (file)
index 0000000..90d4472
--- /dev/null
@@ -0,0 +1,121 @@
+package jalview.ws.dbsources;
+
+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 jalview.analysis.AlignSeq;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.SequenceI;
+import jalview.ext.ensembl.EnsemblGenomes;
+import jalview.fts.api.FTSData;
+import jalview.fts.api.FTSDataColumnI;
+import jalview.fts.api.FTSRestClientI;
+import jalview.fts.core.FTSRestRequest;
+import jalview.fts.core.FTSRestResponse;
+import jalview.fts.service.uniprot.UniProtFTSRestClient;
+import jalview.ws.SequenceFetcher;
+import jalview.ws.seqfetcher.DbSourceProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * A class to verify that remotely fetched data has an expected format and can
+ * be successfully processed by Jalview. This is intended as a first line of
+ * defence and early warning of service affecting changes to data fetched
+ * externally.
+ * <p>
+ * This is class is not intended to cover remote services e.g. alignment. Nor
+ * should it duplicate tests already provided by other classes (such as
+ * PDBFTSRestClientTest). Or maybe we will relocate those tests here...
+ */
+public class RemoteFormatTest
+{
+  SequenceFetcher sf;
+
+  @BeforeTest(alwaysRun = true)
+  public void setUp() throws Exception
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    // ensure 'add annotation from structure' is selected
+    Cache.applicationProperties.setProperty("STRUCT_FROM_PDB",
+            Boolean.TRUE.toString());
+    Cache.applicationProperties.setProperty("ADD_SS_ANN",
+            Boolean.TRUE.toString());
+
+    sf = new SequenceFetcher(false);
+  }
+
+  @DataProvider(name = "AccessionData")
+  protected Object[][] getAccessions()
+  {
+    return new Object[][] { { DBRefSource.UNIPROT, "P30419" },
+        { DBRefSource.PDB, "1QIP" }, { DBRefSource.EMBL, "X53828" },
+        { DBRefSource.EMBLCDS, "CAA37824" },
+        { DBRefSource.ENSEMBL, "ENSG00000157764" },
+        { new EnsemblGenomes().getDbSource(), "DDB_G0283883" },
+        { new PfamFull().getDbSource(), "PF03760" },
+        { new PfamSeed().getDbSource(), "PF03760" },
+        { new RfamSeed().getDbSource(), "RF00014" } };
+  }
+
+  @Test(groups = "Network", dataProvider = "AccessionData")
+  public void testFetchAccession(String dbSource, String accessionId)
+          throws Exception
+  {
+    System.out.println("Fetching " + accessionId + " from " + dbSource);
+    List<DbSourceProxy> sps = sf.getSourceProxy(dbSource);
+    assertFalse(sps.isEmpty());
+    AlignmentI al = sps.get(0).getSequenceRecords(accessionId);
+    assertNotNull(al);
+    assertTrue(al.getHeight() > 0);
+    SequenceI sq = al.getSequenceAt(0);
+    // suppress this check as only Uniprot and PDB acquire PDB refs
+    // assertTrue(sq.getAllPDBEntries().size() > 0, "No PDBEntry on sequence.");
+    assertTrue(sq.getDBRefs().length > 0, "No DBRef on sequence.");
+    // suppress this test as only certain databases provide 'primary' dbrefs
+    // assertFalse(sq.getPrimaryDBRefs().isEmpty());
+    int length = AlignSeq.extractGaps("-. ", sq.getSequenceAsString())
+            .length();
+    assertEquals(sq.getEnd() - sq.getStart() + 1, length,
+            "Sequence start/end doesn't match number of residues in sequence");
+  }
+
+  @Test(groups = { "Network" })
+  public void testUniprotFreeTextSearch() throws Exception
+  {
+    List<FTSDataColumnI> wantedFields = new ArrayList<FTSDataColumnI>();
+    FTSRestClientI client = UniProtFTSRestClient.getInstance();
+    wantedFields.add(client.getDataColumnByNameOrCode("id"));
+    wantedFields.add(client.getDataColumnByNameOrCode("entry name"));
+    wantedFields.add(client.getDataColumnByNameOrCode("organism"));
+    wantedFields.add(client.getDataColumnByNameOrCode("reviewed")); // Status
+    wantedFields.add(client.getDataColumnByNameOrCode("length"));
+  
+    FTSRestRequest request = new FTSRestRequest();
+    request.setAllowEmptySeq(false);
+    request.setResponseSize(100);
+    request.setFieldToSearchBy("Search All");
+    request.setSearchTerm("metanephrops"); // lobster!
+    request.setWantedFields(wantedFields);
+  
+    FTSRestResponse response;
+    response = client.executeRequest(request);
+    assertTrue(response.getNumberOfItemsFound() > 20);
+    assertTrue(response.getSearchSummary() != null);
+    assertTrue(response.getSearchSummary().size() > 20);
+    // verify we successfully filtered out the header row (JAL-2485)
+    FTSData header = response.getSearchSummary().iterator().next();
+    assertFalse(
+            header.getSummaryData()[0].toString().equalsIgnoreCase("Entry"),
+            "Failed to filter out summary header row");
+  }
+}
index 2f548d0..2d4be71 100644 (file)
@@ -26,9 +26,9 @@ 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;
@@ -97,13 +97,12 @@ public class UniprotTest
     /*
      * Check sequence features
      */
-    Vector<SequenceFeature> features = entry.getFeature();
+    Vector<UniprotFeature> features = entry.getFeature();
     assertEquals(3, features.size());
-    SequenceFeature sf = features.get(0);
+    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);
@@ -139,10 +138,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" })
index 2d317e4..e8b6c2b 100644 (file)
@@ -44,7 +44,11 @@ import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
-@Test(groups = { "External" })
+/*
+ * All methods in this class are set to the Network group because setUpBeforeClass will fail
+ * if there is no network.
+ */
+@Test(singleThreaded = true)
 public class DisorderAnnotExportImport
 {
 
@@ -65,12 +69,19 @@ public class DisorderAnnotExportImport
 
   public static jalview.gui.AlignFrame af = null;
 
-  @BeforeClass(inheritGroups = true)
+  @BeforeClass(alwaysRun = true)
   public static void setUpBeforeClass() throws Exception
   {
     Cache.loadProperties("test/jalview/io/testProps.jvprops");
     Cache.initLogger();
     disc = JalviewJabawsTestUtils.getJabawsDiscoverer();
+
+    while (disc.isRunning())
+    {
+      // don't get services until discoverer has finished
+      Thread.sleep(100);
+    }
+
     iupreds = new ArrayList<Jws2Instance>();
     for (Jws2Instance svc : disc.getServices())
     {
@@ -100,7 +111,7 @@ public class DisorderAnnotExportImport
   /**
    * test for patches to JAL-1294
    */
-  @Test
+  @Test(groups = { "External", "Network" })
   public void testDisorderAnnotExport()
   {
     disorderClient = new AADisorderClient(iupreds.get(0), af, null, null);
@@ -166,7 +177,8 @@ public class DisorderAnnotExportImport
                       DataSourceType.PASTE));
 
       // test for consistency in io
-      StockholmFileTest.testAlignmentEquivalence(al, al_new, true);
+      StockholmFileTest.testAlignmentEquivalence(al, al_new, true, false,
+              false);
       return;
     } catch (Exception e)
     {
diff --git a/test/jalview/ws/jabaws/JpredJabaStructExportImport.java b/test/jalview/ws/jabaws/JpredJabaStructExportImport.java
deleted file mode 100644 (file)
index d5b6ed1..0000000
+++ /dev/null
@@ -1,325 +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.jabaws;
-
-import static org.testng.AssertJUnit.assertNotNull;
-import static org.testng.AssertJUnit.assertTrue;
-
-import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
-import jalview.gui.Jalview2XML;
-import jalview.gui.JvOptionPane;
-import jalview.io.AnnotationFile;
-import jalview.io.DataSourceType;
-import jalview.io.FileFormat;
-import jalview.io.FormatAdapter;
-import jalview.io.StockholmFileTest;
-import jalview.ws.jws2.JPred301Client;
-import jalview.ws.jws2.JabaParamStore;
-import jalview.ws.jws2.Jws2Discoverer;
-import jalview.ws.jws2.SequenceAnnotationWSClient;
-import jalview.ws.jws2.jabaws2.Jws2Instance;
-import jalview.ws.params.AutoCalcSetting;
-
-import java.awt.Component;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.swing.JMenu;
-import javax.swing.JMenuItem;
-
-import org.testng.Assert;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
-import compbio.metadata.Argument;
-import compbio.metadata.WrongParameterException;
-
-public class JpredJabaStructExportImport
-{
-
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
-  public static String testseqs = "examples/uniref50.fa";
-
-  public static Jws2Discoverer disc;
-
-  public static Jws2Instance jpredws;
-
-  jalview.ws.jws2.JPred301Client jpredClient;
-
-  public static jalview.gui.AlignFrame af = null;
-
-  @BeforeClass(alwaysRun = true)
-  public static void setUpBeforeClass() throws Exception
-  {
-    Cache.loadProperties("test/jalview/io/testProps.jvprops");
-    Cache.initLogger();
-    disc = JalviewJabawsTestUtils.getJabawsDiscoverer(false);
-
-    for (Jws2Instance svc : disc.getServices())
-    {
-      if (svc.getServiceTypeURI().toLowerCase().contains("jpred"))
-      {
-        jpredws = svc;
-      }
-    }
-
-    System.out.println("State of jpredws: " + jpredws);
-    Assert.assertNotNull(jpredws, "jpredws is null!");
-    jalview.io.FileLoader fl = new jalview.io.FileLoader(false);
-    af = fl.LoadFileWaitTillLoaded(testseqs, jalview.io.DataSourceType.FILE);
-    assertNotNull("Couldn't load test data ('" + testseqs + "')", af);
-  }
-
-  @AfterClass(alwaysRun = true)
-  public static void tearDownAfterClass() throws Exception
-  {
-    if (af != null)
-    {
-      af.setVisible(false);
-      af.dispose();
-    }
-  }
-
-  @Test(groups = { "Functional" })
-  public void testJPredStructOneSeqOnly()
-  {
-    af.selectAllSequenceMenuItem_actionPerformed(null);
-    af.getViewport()
-            .getSelectionGroup()
-            .addOrRemove(
-                    af.getViewport().getSelectionGroup().getSequenceAt(0),
-                    false);
-    af.hideSelSequences_actionPerformed(null);
-    jpredClient = new JPred301Client(jpredws, af, null, null);
-
-    assertTrue(
-            "Didn't find any default args to check for. Buggy implementation of hardwired arguments in client.",
-            jpredClient.selectDefaultArgs().size() > 0);
-
-    boolean success = false;
-    af.getViewport().getCalcManager().startWorker(jpredClient);
-    do
-    {
-      try
-      {
-        Thread.sleep(500);
-        List<Argument> args = JabaParamStore.getJabafromJwsArgs(af
-                .getViewport()
-                .getCalcIdSettingsFor(jpredClient.getCalcId())
-                .getArgumentSet()), defargs = jpredClient
-                .selectDefaultArgs();
-        for (Argument rg : args)
-        {
-          for (Argument defg : defargs)
-          {
-            if (defg.equals(rg))
-            {
-              success = true;
-            }
-          }
-        }
-        if (!success)
-        {
-          jpredClient.cancelCurrentJob();
-          Assert.fail("Jpred Client didn't run with hardwired default parameters.");
-        }
-
-      } catch (InterruptedException x)
-      {
-      }
-      ;
-    } while (af.getViewport().getCalcManager().isWorking());
-
-  }
-
-  @Test(groups = { "Functional" })
-  public void testJPredStructExport()
-  {
-
-    jpredClient = new JPred301Client(jpredws, af, null, null);
-
-    af.getViewport().getCalcManager().startWorker(jpredClient);
-
-    do
-    {
-      try
-      {
-        Thread.sleep(50);
-      } catch (InterruptedException x)
-      {
-      }
-      ;
-    } while (af.getViewport().getCalcManager().isWorking());
-
-    AlignmentI orig_alig = af.getViewport().getAlignment();
-
-    testAnnotationFileIO("Testing JPredWS Annotation IO", orig_alig);
-
-  }
-
-  public static void testAnnotationFileIO(String testname, AlignmentI al)
-  {
-    try
-    {
-      // what format would be appropriate for RNAalifold annotations?
-      String aligfileout = FileFormat.Pfam.getWriter(null).print(
-              al.getSequencesArray(), true);
-
-      String anfileout = new AnnotationFile()
-              .printAnnotationsForAlignment(al);
-      assertTrue(
-              "Test "
-                      + testname
-                      + "\nAlignment annotation file was not regenerated. Null string",
-              anfileout != null);
-      assertTrue(
-              "Test "
-                      + testname
-                      + "\nAlignment annotation file was not regenerated. Empty string",
-              anfileout.length() > "JALVIEW_ANNOTATION".length());
-
-      System.out.println("Output annotation file:\n" + anfileout
-              + "\n<<EOF\n");
-
-      // again what format would be appropriate?
-      AlignmentI al_new = new FormatAdapter().readFile(aligfileout,
-              DataSourceType.PASTE, FileFormat.Fasta);
-      assertTrue(
-              "Test "
-                      + testname
-                      + "\nregenerated annotation file did not annotate alignment.",
-              new AnnotationFile().readAnnotationFile(al_new, anfileout,
-                      DataSourceType.PASTE));
-
-      // test for consistency in io
-      StockholmFileTest.testAlignmentEquivalence(al, al_new, false);
-      return;
-    } catch (Exception e)
-    {
-      e.printStackTrace();
-    }
-    Assert.fail("Test "
-            + testname
-            + "\nCouldn't complete Annotation file roundtrip input/output/input test.");
-  }
-
-  @Test(groups = { "Functional" })
-  public void testJpredwsSettingsRecovery()
-  {
-    Assert.fail("not implemnented");
-    List<compbio.metadata.Argument> opts = new ArrayList<compbio.metadata.Argument>();
-    for (compbio.metadata.Argument rg : (List<compbio.metadata.Argument>) jpredws
-            .getRunnerConfig().getArguments())
-    {
-      if (rg.getDescription().contains("emperature"))
-      {
-        try
-        {
-          rg.setValue("292");
-        } catch (WrongParameterException q)
-        {
-          Assert.fail("Couldn't set the temperature parameter "
-                  + q.getStackTrace());
-        }
-        opts.add(rg);
-      }
-      if (rg.getDescription().contains("max"))
-      {
-        opts.add(rg);
-      }
-    }
-    jpredClient = new JPred301Client(jpredws, af, null, opts);
-
-    af.getViewport().getCalcManager().startWorker(jpredClient);
-
-    do
-    {
-      try
-      {
-        Thread.sleep(50);
-      } catch (InterruptedException x)
-      {
-      }
-      ;
-    } while (af.getViewport().getCalcManager().isWorking());
-    AutoCalcSetting oldacs = af.getViewport().getCalcIdSettingsFor(
-            jpredClient.getCalcId());
-    String oldsettings = oldacs.getWsParamFile();
-    // write out parameters
-    jalview.gui.AlignFrame nalf = null;
-    assertTrue("Couldn't write out the Jar file",
-            new Jalview2XML(false).saveAlignment(af,
-                    "testJPredWS_param.jar", "trial parameter writeout"));
-    assertTrue("Couldn't read back the Jar file", (nalf = new Jalview2XML(
-            false).loadJalviewAlign("testJpredWS_param.jar")) != null);
-    if (nalf != null)
-    {
-      AutoCalcSetting acs = af.getViewport().getCalcIdSettingsFor(
-              jpredClient.getCalcId());
-      assertTrue("Calc ID settings not recovered from viewport stash",
-              acs.equals(oldacs));
-      assertTrue(
-              "Serialised Calc ID settings not identical to those recovered from viewport stash",
-              acs.getWsParamFile().equals(oldsettings));
-      JMenu nmenu = new JMenu();
-      new SequenceAnnotationWSClient()
-              .attachWSMenuEntry(nmenu, jpredws, af);
-      assertTrue("Couldn't get menu entry for service",
-              nmenu.getItemCount() > 0);
-      for (Component itm : nmenu.getMenuComponents())
-      {
-        if (itm instanceof JMenuItem)
-        {
-          JMenuItem i = (JMenuItem) itm;
-          if (i.getText().equals(
-                  jpredws.getAlignAnalysisUI().getAAconToggle()))
-          {
-            i.doClick();
-            break;
-          }
-        }
-      }
-      while (af.getViewport().isCalcInProgress())
-      {
-        try
-        {
-          Thread.sleep(200);
-        } catch (Exception x)
-        {
-        }
-        ;
-      }
-      AutoCalcSetting acs2 = af.getViewport().getCalcIdSettingsFor(
-              jpredClient.getCalcId());
-      assertTrue(
-              "Calc ID settings after recalculation has not been recovered.",
-              acs2.getWsParamFile().equals(oldsettings));
-    }
-  }
-}
index 998524a..6af831f 100644 (file)
@@ -66,6 +66,11 @@ public class MinJabawsClientTests
       MsaWS msaservice = null;
       for (Services service : registry.getSupportedServices())
       {
+        if (service == null)
+        {
+          // the 'unsupported service'
+          continue;
+        }
         if (service.equals(Services.ClustalOWS))
         {
           msaservice = (MsaWS) Jws2Client.connect(url, service);
index f1430f6..089c29f 100644 (file)
@@ -55,6 +55,11 @@ import org.testng.annotations.Test;
 import compbio.metadata.Argument;
 import compbio.metadata.WrongParameterException;
 
+/*
+ * All methods in this class are set to the Network group because setUpBeforeClass will fail
+ * if there is no network.
+ */
+@Test(singleThreaded = true)
 public class RNAStructExportImport
 {
 
@@ -84,6 +89,12 @@ public class RNAStructExportImport
     Cache.initLogger();
     disc = JalviewJabawsTestUtils.getJabawsDiscoverer(false);
 
+    while (disc.isRunning())
+    {
+      // don't get services until discoverer has finished
+      Thread.sleep(100);
+    }
+
     for (Jws2Instance svc : disc.getServices())
     {
 
@@ -139,7 +150,7 @@ public class RNAStructExportImport
     }
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testRNAAliFoldValidStructure()
   {
 
@@ -172,7 +183,7 @@ public class RNAStructExportImport
     }
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testRNAStructExport()
   {
 
@@ -191,12 +202,14 @@ public class RNAStructExportImport
     } while (af.getViewport().getCalcManager().isWorking());
 
     AlignmentI orig_alig = af.getViewport().getAlignment();
-
-    testAnnotationFileIO("Testing RNAalifold Annotation IO", orig_alig);
+    // JBPNote: this assert fails (2.10.2) because the 'Reference Positions'
+    // annotation is mistakenly recognised as an RNA annotation row when read in
+    // as an annotation file.
+    verifyAnnotationFileIO("Testing RNAalifold Annotation IO", orig_alig);
 
   }
 
-  public static void testAnnotationFileIO(String testname, AlignmentI al)
+  static void verifyAnnotationFileIO(String testname, AlignmentI al)
   {
     try
     {
@@ -231,7 +244,8 @@ public class RNAStructExportImport
                       DataSourceType.PASTE));
 
       // test for consistency in io
-      StockholmFileTest.testAlignmentEquivalence(al, al_new, false);
+      StockholmFileTest.testAlignmentEquivalence(al, al_new, false, false,
+              false);
       return;
     } catch (Exception e)
     {
@@ -242,7 +256,7 @@ public class RNAStructExportImport
             + "\nCouldn't complete Annotation file roundtrip input/output/input test.");
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testRnaalifoldSettingsRecovery()
   {
     List<Argument> opts = new ArrayList<Argument>();
index 0662e5b..c0aa2ee 100644 (file)
@@ -42,6 +42,11 @@ import compbio.metadata.Preset;
 import compbio.metadata.PresetManager;
 import compbio.metadata.WrongParameterException;
 
+/*
+ * All methods in this class are set to the Network group because setUpBeforeClass will fail
+ * if there is no network.
+ */
+@Test(singleThreaded = true)
 public class ParameterUtilsTest
 {
 
@@ -69,7 +74,7 @@ public class ParameterUtilsTest
     disc = JalviewJabawsTestUtils.getJabawsDiscoverer();
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testWriteParameterSet() throws WrongParameterException
   {
     for (Jws2Instance service : disc.getServices())
@@ -129,7 +134,7 @@ public class ParameterUtilsTest
             || serviceTests.contains(service.serviceType.toLowerCase());
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testCopyOption()
   {
     for (Jws2Instance service : disc.getServices())
@@ -153,7 +158,7 @@ public class ParameterUtilsTest
 
   /**
    */
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testCopyParameter()
   {
     for (Jws2Instance service : disc.getServices())
index 98ca303..f1dafcb 100644 (file)
  */
 package jalview.ws.seqfetcher;
 
+import static org.testng.Assert.assertTrue;
+
+import jalview.bin.Cache;
 import jalview.gui.JvOptionPane;
 
-import org.testng.AssertJUnit;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
@@ -36,15 +38,12 @@ public class DasSequenceFetcher
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Network" })
   public void testDasRegistryContact()
   {
-    jalview.bin.Cache.getDasSourceRegistry().refreshSources();
-    AssertJUnit
-            .assertTrue(
-                    "Expected to find at least one DAS source at the registry. Check config.",
-                    jalview.bin.Cache.getDasSourceRegistry().getSources()
-                            .size() > 0);
+    Cache.getDasSourceRegistry().refreshSources();
+    assertTrue(Cache.getDasSourceRegistry().getSources().isEmpty(),
+            "Expected to find no DAS sources at the registry. Check config.");
   }
 
 }
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 d805e47..b92766e 100644 (file)
@@ -38,6 +38,8 @@ import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
 
 import org.testng.Assert;
 import org.testng.FileAssert;
@@ -280,7 +282,19 @@ public class SiftsClientTest
               "A", testSeq, null);
       Assert.assertEquals(testSeq.getStart(), 1);
       Assert.assertEquals(testSeq.getEnd(), 147);
-      Assert.assertEquals(actualMapping, expectedMapping);
+      // Can't do Assert.assertEquals(actualMapping, expectedMapping);
+      // because this fails in our version of TestNG
+      Assert.assertEquals(actualMapping.size(), expectedMapping.size());
+      Iterator<Map.Entry<Integer, int[]>> it = expectedMapping.entrySet()
+              .iterator();
+      while (it.hasNext())
+      {
+        Map.Entry<Integer, int[]> pair = it.next();
+        Assert.assertTrue(actualMapping.containsKey(pair.getKey()));
+        Assert.assertEquals(actualMapping.get(pair.getKey()),
+                pair.getValue());
+      }
+
     } catch (Exception e)
     {
       e.printStackTrace();
@@ -399,7 +413,21 @@ groups = { "Network" },
 
     Assert.assertEquals(strucMapping.getMappingDetailsOutput(),
             expectedMappingOutput);
-    Assert.assertEquals(strucMapping.getMapping(), expectedMapping);
+
+    // Can't do Assert.assertEquals(strucMapping.getMapping(), expectedMapping);
+    // because this fails in our version of TestNG
+    Assert.assertEquals(strucMapping.getMapping().size(),
+            expectedMapping.size());
+    Iterator<Map.Entry<Integer, int[]>> it = expectedMapping.entrySet()
+            .iterator();
+    while (it.hasNext())
+    {
+      Map.Entry<Integer, int[]> pair = it.next();
+      Assert.assertTrue(strucMapping.getMapping()
+              .containsKey(pair.getKey()));
+      Assert.assertEquals(strucMapping.getMapping().get(pair.getKey()),
+              pair.getValue());
+    }
   }
 
   @Test(groups = { "Network" })
@@ -486,4 +514,22 @@ groups = { "Network" },
     Assert.assertNull(entityP);
 
   }
+
+  @Test(groups = { "Network" })
+  public void getLeadingIntegerFromString()
+  {
+    Assert.assertEquals(
+            SiftsClient.getLeadingIntegerValue("1234abcd", -1), 1234);
+    Assert.assertEquals(
+            SiftsClient.getLeadingIntegerValue("1234", -1),
+            1234);
+    Assert.assertEquals(
+            SiftsClient.getLeadingIntegerValue("abcd", -1), -1);
+    Assert.assertEquals(
+            SiftsClient.getLeadingIntegerValue("abcd1234", -1), -1);
+    Assert.assertEquals(
+            SiftsClient.getLeadingIntegerValue("None", -1), -1);
+    Assert.assertEquals(
+            SiftsClient.getLeadingIntegerValue("Null", -1), -1);
+  }
 }
diff --git a/test/jalview/ws/utils/UrlDownloadClientTest.java b/test/jalview/ws/utils/UrlDownloadClientTest.java
new file mode 100644 (file)
index 0000000..2bd8dd0
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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.utils;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class UrlDownloadClientTest {
+
+  /**
+   * Test that url is successfully loaded into download file
+   */
+  @Test(groups = { "Network" }, enabled = true)
+  public void UrlDownloadTest()
+  {
+    UrlDownloadClient client = new UrlDownloadClient();
+    String urlstring = "http://identifiers.org/rest/collections/";
+    String outfile = "testfile.tmp";
+
+    try
+    {
+      client.download(urlstring, outfile);
+    } catch (IOException e)
+    {
+      Assert.fail("Exception was thrown from UrlDownloadClient download: "
+              + e.getMessage());
+      File f = new File(outfile);
+      if (f.exists())
+      {
+        f.delete();
+      }
+    }
+
+    // download file exists
+    File f = new File(outfile);
+    Assert.assertTrue(f.exists());
+
+    // download file has a believable size
+    // identifiers.org file typically at least 250K
+    Assert.assertTrue(f.length() > 250000);
+
+    if (f.exists())
+    {
+      f.delete();
+    }
+
+  }
+
+  /**
+   * Test that garbage in results in IOException
+   */
+  @Test(
+    groups = { "Network" },
+    enabled = true,
+    expectedExceptions = { IOException.class })
+  public void DownloadGarbageUrlTest() throws IOException
+  {
+    UrlDownloadClient client = new UrlDownloadClient();
+    String urlstring = "identifiers.org/rest/collections/";
+    String outfile = "testfile.tmp";
+
+    client.download(urlstring, outfile);
+  }
+}
diff --git a/test/junit/extensions/PA.java b/test/junit/extensions/PA.java
new file mode 100644 (file)
index 0000000..57c873f
--- /dev/null
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2004-2012 Sebastian Dietrich (Sebastian.Dietrich@e-movimento.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package junit.extensions;
+
+import java.util.Collection;
+
+/**
+ * This class is used to access a method or field of an object no matter what the access modifier of the method or field. The syntax
+ * for accessing fields and methods is out of the ordinary because this class uses reflection to peel away protection.
+ * <p>
+ * a.k.a. The "ObjectMolester"
+ * <p>
+ * Here is an example of using this to access a private member: <br>
+ * Given the following class <code>MyClass</code>: <br>
+ * 
+ * <pre>
+ * public class MyClass {
+ *    private String name; // private attribute
+ * 
+ *    // private constructor
+ *    private MyClass() {
+ *       super();
+ *    }
+ * 
+ *    // private method
+ *    private void setName(String newName) {
+ *       this.name = newName;
+ *    }
+ * }
+ * </pre>
+ * 
+ * We now want to access the class: <br>
+ * 
+ * <pre>
+ * MyClass myObj = PA.instantiate(MyClass.class);
+ * PA.invokeMethod(myObj, &quot;setName(java.lang.String)&quot;, &quot;myNewName&quot;);
+ * String name = PA.getValue(myObj, &quot;name&quot;);
+ * </pre>
+ * 
+ * This class extends {@link PrivilegedAccessor} by re-throwing checked {@link Exception}s as {@link RuntimeException}s.
+ * 
+ * 
+ * @see PrivilegedAccessor
+ * 
+ * @author Sebastian Dietrich (sebastian.dietrich@e-movimento.com)
+ * @author Lubos Bistak (lubos@bistak.sk)
+ */
+public class PA {
+   private final Object instanceOrClass;
+
+   /**
+    * Private constructor to make it impossible to instantiate this class from outside of PA.
+    * 
+    * @param instanceOrClass
+    */
+   private PA(Object instanceOrClass) {
+      this.instanceOrClass = instanceOrClass;
+   }
+
+   /**
+    * Returns a string representation of the given object. The string has the following format: "<classname> {<attributes and values>}"
+    * whereas <attributes and values> is a comma separated list with <attributeName>=<attributeValue> <atributes and values> includes
+    * all attributes of the objects class followed by the attributes of its superclass (if any) and so on.
+    * 
+    * @param instanceOrClass the object or class to get a string representation of
+    * @return a string representation of the given object
+    * 
+    * @see PrivilegedAccessor#toString(Object)
+    */
+   public static String toString(final Object instanceOrClass) {
+      return PrivilegedAccessor.toString(instanceOrClass);
+   }
+
+   /**
+    * Gets the name of all fields (public, private, protected, default) of the given instance or class. This includes as well all
+    * fields (public, private, protected, default) of all its super classes.
+    * 
+    * @param instanceOrClass the instance or class to get the fields of
+    * @return the collection of field names of the given instance or class
+    * 
+    * @see PrivilegedAccessor#getFieldNames(Object)
+    */
+   public static Collection<String> getFieldNames(final Object instanceOrClass) {
+      return PrivilegedAccessor.getFieldNames(instanceOrClass);
+   }
+
+   /**
+    * Gets the signatures of all methods (public, private, protected, default) of the given instance or class. This includes as well
+    * all methods (public, private, protected, default) of all its super classes. This does not include constructors.
+    * 
+    * @param instanceOrClass the instance or class to get the method signatures of
+    * @return the collection of method signatures of the given instance or class
+    * 
+    * @see PrivilegedAccessor#getMethodSignatures(Object)
+    */
+   public static Collection<String> getMethodSignatures(final Object instanceOrClass) {
+      return PrivilegedAccessor.getMethodSignatures(instanceOrClass);
+   }
+
+   /**
+    * Gets the value of the named field and returns it as an object. If instanceOrClass is a class then a static field is returned.
+    * 
+    * @param instanceOrClass the instance or class to get the field from
+    * @param fieldName the name of the field
+    * @return an object representing the value of the field
+    * @throws IllegalArgumentException if the field does not exist
+    * 
+    * @see PrivilegedAccessor#getValue(Object,String)
+    */
+   public static Object getValue(final Object instanceOrClass, final String fieldName) {
+      try {
+         return PrivilegedAccessor.getValue(instanceOrClass, fieldName);
+      } catch (Exception e) {
+         throw new IllegalArgumentException("Can't get value of " + fieldName + " from " + instanceOrClass, e);
+      }
+   }
+
+   /**
+    * Gets the value of the named field and returns it as an object.
+    * 
+    * @param fieldName the name of the field
+    * @return an object representing the value of the field
+    * @throws IllegalArgumentException if the field does not exist
+    * 
+    * @see PA#getValue(Object,String)
+    */
+   public Object getValue(final String fieldName) {
+      return PA.getValue(instanceOrClass, fieldName);
+   }
+
+   /**
+    * Instantiates an object of the given class with the given arguments and the given argument types. If you want to instantiate a
+    * member class, you must provide the object it is a member of as first argument.
+    * 
+    * @param fromClass the class to instantiate an object from
+    * @param arguments the arguments to pass to the constructor
+    * @param argumentTypes the fully qualified types of the arguments of the constructor
+    * @return an object of the given type
+    * @throws IllegalArgumentException if the class can't be instantiated. This could be the case if the number of actual and formal
+    *            parameters differ; if an unwrapping conversion for primitive arguments fails; if, after possible unwrapping, a
+    *            parameter value cannot be converted to the corresponding formal parameter type by a method invocation conversion; if
+    *            this Constructor object enforces Java language access control and the underlying constructor is inaccessible; if the
+    *            underlying constructor throws an exception; if the constructor could not be found; or if the class that declares the
+    *            underlying constructor represents an abstract class.
+    * 
+    * @see PrivilegedAccessor#instantiate(Class,Class[],Object[])
+    */
+   public static <T> T instantiate(final Class<? extends T> fromClass, final Class<?>[] argumentTypes, final Object... arguments) {
+      try {
+         return PrivilegedAccessor.instantiate(fromClass, argumentTypes, correctVarargs(arguments));
+      } catch (Exception e) {
+         throw new IllegalArgumentException("Can't instantiate class " + fromClass + " with arguments " + arguments, e);
+      }
+   }
+
+   /**
+    * Instantiates an object of the given class with the given arguments. If you want to instantiate a member class, you must provide
+    * the object it is a member of as first argument.
+    * 
+    * @param fromClass the class to instantiate an object from
+    * @param arguments the arguments to pass to the constructor
+    * @return an object of the given type
+    * @throws IllegalArgumentException if the class can't be instantiated. This could be the case if the number of actual and formal
+    *            parameters differ; if an unwrapping conversion for primitive arguments fails; or if, after possible unwrapping, a
+    *            parameter value cannot be converted to the corresponding formal parameter type by a method invocation conversion; if
+    *            this Constructor object enforces Java language access control and the underlying constructor is inaccessible; if the
+    *            underlying constructor throws an exception; if the constructor could not be found; or if the class that declares the
+    *            underlying constructor represents an abstract class.
+    * 
+    * @see PrivilegedAccessor#instantiate(Class,Object[])
+    */
+   public static <T> T instantiate(final Class<? extends T> fromClass, final Object... arguments) {
+      try {
+         return PrivilegedAccessor.instantiate(fromClass, correctVarargs(arguments));
+      } catch (Exception e) {
+         throw new IllegalArgumentException("Can't instantiate class " + fromClass + " with arguments " + arguments, e);
+      }
+   }
+
+   /**
+    * Calls a method on the given object instance with the given arguments. Arguments can be object types or representations for
+    * primitives.
+    * 
+    * @param instanceOrClass the instance or class to invoke the method on
+    * @param methodSignature the name of the method and the parameters <br>
+    *           (e.g. "myMethod(java.lang.String, com.company.project.MyObject)")
+    * @param arguments an array of objects to pass as arguments
+    * @return the return value of this method or null if void
+    * @throws IllegalArgumentException if the method could not be invoked. This could be the case if the method is inaccessible; if the
+    *            underlying method throws an exception; if no method with the given <code>methodSignature</code> could be found; or if
+    *            an argument couldn't be converted to match the expected type
+    * 
+    * @see PrivilegedAccessor#invokeMethod(Object,String,Object[])
+    */
+   public static Object invokeMethod(final Object instanceOrClass, final String methodSignature, final Object... arguments) {
+      try {
+         return PrivilegedAccessor.invokeMethod(instanceOrClass, methodSignature, correctVarargs(arguments));
+      } catch (Exception e) {
+         throw new IllegalArgumentException("Can't invoke method " + methodSignature + " on " + instanceOrClass + " with arguments "
+            + arguments, e);
+      }
+   }
+
+   /**
+    * Calls a method with the given arguments. Arguments can be object types or representations for primitives.
+    * 
+    * @param methodSignature the name of the method and the parameters <br>
+    *           (e.g. "myMethod(java.lang.String, com.company.project.MyObject)")
+    * @param arguments an array of objects to pass as arguments
+    * @return the return value of this method or null if void
+    * @throws IllegalArgumentException if the method could not be invoked. This could be the case if the method is inaccessible; if the
+    *            underlying method throws an exception; if no method with the given <code>methodSignature</code> could be found; or if
+    *            an argument couldn't be converted to match the expected type
+    * @see PA#invokeMethod(Object, String, Object...)
+    */
+   public Object invokeMethod(final String methodSignature, final Object... arguments) {
+      return PA.invokeMethod(instanceOrClass, methodSignature, arguments);
+   }
+
+   /**
+    * Corrects varargs to their initial form. If you call a method with an object-array as last argument the Java varargs mechanism
+    * converts this array in single arguments. This method returns an object array if the arguments are all of the same type.
+    * 
+    * @param arguments the possibly converted arguments of a vararg method
+    * @return arguments possibly converted
+    */
+   private static Object[] correctVarargs(final Object... arguments) {
+      if ((arguments == null) || changedByVararg(arguments)) return new Object[] {arguments};
+      return arguments;
+   }
+
+   /**
+    * Tests if the arguments were changed by vararg. Arguments are changed by vararg if they are of a non primitive array type. E.g.
+    * arguments[] = Object[String[]] is converted to String[] while e.g. arguments[] = Object[int[]] is not converted and stays
+    * Object[int[]]
+    * 
+    * Unfortunately we can't detect the difference for arg = Object[primitive] since arguments[] = Object[Object[primitive]] which is
+    * converted to Object[primitive] and arguments[] = Object[primitive] which stays Object[primitive]
+    * 
+    * and we can't detect the difference for arg = Object[non primitive] since arguments[] = Object[Object[non primitive]] is converted
+    * to Object[non primitive] and arguments[] = Object[non primitive] stays Object[non primitive]
+    * 
+    * @param parameters the parameters
+    * @return true if parameters were changes by varargs, false otherwise
+    */
+   private static boolean changedByVararg(final Object[] parameters) {
+      if ((parameters.length == 0) || (parameters[0] == null)) return false;
+
+      if (parameters.getClass() == Object[].class) return false;
+
+      return true;
+   }
+
+   /**
+    * Sets the value of the named field. If fieldName denotes a static field, provide a class, otherwise provide an instance. If the
+    * fieldName denotes a final field, this method could fail with an IllegalAccessException, since setting the value of final fields
+    * at other times than instantiation can have unpredictable effects.<br/>
+    * <br/>
+    * Example:<br/>
+    * <br/>
+    * <code>
+    * String myString = "Test"; <br/>
+    * <br/>
+    * //setting the private field value<br/>
+    * PrivilegedAccessor.setValue(myString, "value", new char[] {'T', 'e', 's', 't'});<br/>
+    * <br/>
+    * //setting the static final field serialVersionUID - MIGHT FAIL<br/>
+    * PrivilegedAccessor.setValue(myString.getClass(), "serialVersionUID", 1);<br/>
+    * <br/>
+    * </code>
+    * 
+    * @param instanceOrClass the instance or class to set the field
+    * @param fieldName the name of the field
+    * @param value the new value of the field
+    * @throws IllegalArgumentException if the value could not be set. This could be the case if no field with the given
+    *            <code>fieldName</code> can be found; or if the field was final
+    * 
+    * @see PrivilegedAccessor.setValue(Object,String,Object)
+    */
+   public static PA setValue(final Object instanceOrClass, final String fieldName, final Object value) {
+      try {
+         PrivilegedAccessor.setValue(instanceOrClass, fieldName, value);
+      } catch (Exception e) {
+         throw new IllegalArgumentException("Can't set value " + value + " at " + fieldName + " in " + instanceOrClass, e);
+      }
+      return new PA(instanceOrClass);
+   }
+
+   /**
+    * Sets the value of the named field. If fieldName denotes a static field, provide a class, otherwise provide an instance. If the
+    * fieldName denotes a final field, this method could fail with an IllegalAccessException, since setting the value of final fields
+    * at other times than instantiation can have unpredictable effects.<br/>
+    * <br/>
+    * Example:<br/>
+    * <br/>
+    * <code>
+    * String myString = "Test"; <br/>
+    * <br/>
+    * //setting the private field value<br/>
+    * PrivilegedAccessor.setValue(myString, "value", new char[] {'T', 'e', 's', 't'});<br/>
+    * <br/>
+    * //setting the static final field serialVersionUID - MIGHT FAIL<br/>
+    * PrivilegedAccessor.setValue(myString.getClass(), "serialVersionUID", 1);<br/>
+    * <br/>
+    * </code>
+    * 
+    * @param fieldName the name of the field
+    * @param value the new value of the field
+    * @throws IllegalArgumentException if the value could not be set. This could be the case if no field with the given
+    *            <code>fieldName</code> can be found; or if the field was final
+    * 
+    * @see PA.setValue(Object,String,Object)
+    */
+   public PA setValue(final String fieldName, final Object value) {
+      PA.setValue(instanceOrClass, fieldName, value);
+      return this;
+   }
+}
diff --git a/test/junit/extensions/PrivilegedAccessor.java b/test/junit/extensions/PrivilegedAccessor.java
new file mode 100644 (file)
index 0000000..23f1c6e
--- /dev/null
@@ -0,0 +1,647 @@
+/*
+ * Copyright 2004-2012 Sebastian Dietrich (Sebastian.Dietrich@e-movimento.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package junit.extensions;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.security.InvalidParameterException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * This class is used to access a method or field of an object no matter what the access modifier of the method or field. The syntax
+ * for accessing fields and methods is out of the ordinary because this class uses reflection to peel away protection.
+ * <p>
+ * a.k.a. The "ObjectMolester"
+ * <p>
+ * Here is an example of using this to access a private member: <br>
+ * <code>myObject</code> is an object of type <code>MyClass</code>. <code>setName(String)</code> is a private method of
+ * <code>MyClass</code>.
+ * 
+ * <pre>
+ * PrivilegedAccessor.invokeMethod(myObject, &quot;setName(java.lang.String)&quot;, &quot;newName&quot;);
+ * </pre>
+ * 
+ * @author Charlie Hubbard (chubbard@iss.net)
+ * @author Prashant Dhokte (pdhokte@iss.net)
+ * @author Sebastian Dietrich (sebastian.dietrich@e-movimento.com)
+ * 
+ * @deprecated use PA instead. PA improves the functionality of PrivilegedAccessor by introducing support for varargs and removal of
+ *             the necessity to catch exceptions.
+ */
+@Deprecated
+final class PrivilegedAccessor
+{
+   /**
+    * Private constructor to make it impossible to instantiate this class.
+    */
+   private PrivilegedAccessor() {
+      assert false : "You mustn't instantiate PrivilegedAccessor, use its methods statically";
+   }
+
+   /**
+    * Returns a string representation of the given object. The string has the following format: "<classname> {<attributes and values>}"
+    * whereas <attributes and values> is a comma separated list with <attributeName>=<attributeValue> <atributes and values> includes
+    * all attributes of the objects class followed by the attributes of its superclass (if any) and so on.
+    * 
+    * @param instanceOrClass the object or class to get a string representation of
+    * @return a string representation of the given object
+    */
+   public static String toString(final Object instanceOrClass) {
+      Collection<String> fields = getFieldNames(instanceOrClass);
+
+      if (fields.isEmpty())
+      {
+        return getClass(instanceOrClass).getName();
+      }
+
+      StringBuffer stringBuffer = new StringBuffer();
+
+      stringBuffer.append(getClass(instanceOrClass).getName() + " {");
+
+      for (String fieldName : fields) {
+         try {
+            stringBuffer.append(fieldName + "=" + getValue(instanceOrClass, fieldName) + ", ");
+         } catch (NoSuchFieldException e) {
+            assert false : "It should always be possible to get a field that was just here";
+         }
+      }
+
+      stringBuffer.replace(stringBuffer.lastIndexOf(", "), stringBuffer.length(), "}");
+      return stringBuffer.toString();
+   }
+
+   /**
+    * Gets the name of all fields (public, private, protected, default) of the given instance or class. This includes as well all
+    * fields (public, private, protected, default) of all its super classes.
+    * 
+    * @param instanceOrClass the instance or class to get the fields of
+    * @return the collection of field names of the given instance or class
+    */
+   public static Collection<String> getFieldNames(final Object instanceOrClass) {
+      if (instanceOrClass == null)
+      {
+        return Collections.EMPTY_LIST;
+      }
+
+      Class<?> clazz = getClass(instanceOrClass);
+      Field[] fields = clazz.getDeclaredFields();
+      Collection<String> fieldNames = new ArrayList<String>(fields.length);
+
+      for (Field field : fields) {
+         fieldNames.add(field.getName());
+      }
+      fieldNames.addAll(getFieldNames(clazz.getSuperclass()));
+
+      return fieldNames;
+   }
+
+   /**
+    * Gets the signatures of all methods (public, private, protected, default) of the given instance or class. This includes as well
+    * all methods (public, private, protected, default) of all its super classes. This does not include constructors.
+    * 
+    * @param instanceOrClass the instance or class to get the method signatures of
+    * @return the collection of method signatures of the given instance or class
+    */
+   public static Collection<String> getMethodSignatures(final Object instanceOrClass) {
+      if (instanceOrClass == null)
+      {
+        return Collections.EMPTY_LIST;
+      }
+
+      Class<?> clazz = getClass(instanceOrClass);
+      Method[] methods = clazz.getDeclaredMethods();
+      Collection<String> methodSignatures = new ArrayList<String>(methods.length + Object.class.getDeclaredMethods().length);
+
+      for (Method method : methods) {
+         methodSignatures.add(method.getName() + "(" + getParameterTypesAsString(method.getParameterTypes()) + ")");
+      }
+      methodSignatures.addAll(getMethodSignatures(clazz.getSuperclass()));
+
+      return methodSignatures;
+   }
+
+   /**
+    * Gets the value of the named field and returns it as an object. If instanceOrClass is a class then a static field is returned.
+    * 
+    * @param instanceOrClass the instance or class to get the field from
+    * @param fieldName the name of the field
+    * @return an object representing the value of the field
+    * @throws NoSuchFieldException if the field does not exist
+    */
+   public static Object getValue(final Object instanceOrClass, final String fieldName) throws NoSuchFieldException {
+      Field field = getField(instanceOrClass, fieldName);
+      try {
+         return field.get(instanceOrClass);
+      } catch (IllegalAccessException e) {
+         assert false : "getField() should have setAccessible(true), so an IllegalAccessException should not occur in this place";
+         return null;
+      }
+   }
+
+   /**
+    * Instantiates an object of the given class with the given arguments. If you want to instantiate a member class, you must provide
+    * the object it is a member of as first argument.
+    * 
+    * @param fromClass the class to instantiate an object from
+    * @param args the arguments to pass to the constructor
+    * @return an object of the given type
+    * @throws IllegalArgumentException if the number of actual and formal parameters differ; if an unwrapping conversion for primitive
+    *            arguments fails; or if, after possible unwrapping, a parameter value cannot be converted to the corresponding formal
+    *            parameter type by a method invocation conversion.
+    * @throws IllegalAccessException if this Constructor object enforces Java language access control and the underlying constructor is
+    *            inaccessible.
+    * @throws InvocationTargetException if the underlying constructor throws an exception.
+    * @throws NoSuchMethodException if the constructor could not be found
+    * @throws InstantiationException if the class that declares the underlying constructor represents an abstract class.
+    * 
+    * @see PrivilegedAccessor#instantiate(Class,Class[],Object[])
+    */
+   public static <T> T instantiate(final Class<? extends T> fromClass, final Object[] args) throws IllegalArgumentException,
+      InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+      return instantiate(fromClass, getParameterTypes(args), args);
+   }
+
+   /**
+    * Instantiates an object of the given class with the given arguments and the given argument types. If you want to instantiate a
+    * member class, you must provide the object it is a member of as first argument.
+    * 
+    * 
+    * @param fromClass the class to instantiate an object from
+    * @param args the arguments to pass to the constructor
+    * @param argumentTypes the fully qualified types of the arguments of the constructor
+    * @return an object of the given type
+    * @throws IllegalArgumentException if the number of actual and formal parameters differ; if an unwrapping conversion for primitive
+    *            arguments fails; or if, after possible unwrapping, a parameter value cannot be converted to the corresponding formal
+    *            parameter type by a method invocation conversion.
+    * @throws IllegalAccessException if this Constructor object enforces Java language access control and the underlying constructor is
+    *            inaccessible.
+    * @throws InvocationTargetException if the underlying constructor throws an exception.
+    * @throws NoSuchMethodException if the constructor could not be found
+    * @throws InstantiationException if the class that declares the underlying constructor represents an abstract class.
+    * 
+    * @see PrivilegedAccessor#instantiate(Class,Object[])
+    */
+   public static <T> T instantiate(final Class<? extends T> fromClass, final Class<?>[] argumentTypes, final Object[] args)
+      throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException,
+      NoSuchMethodException {
+      return getConstructor(fromClass, argumentTypes).newInstance(args);
+   }
+
+   /**
+    * Calls a method on the given object instance with the given arguments. Arguments can be object types or representations for
+    * primitives.
+    * 
+    * @param instanceOrClass the instance or class to invoke the method on
+    * @param methodSignature the name of the method and the parameters <br>
+    *           (e.g. "myMethod(java.lang.String, com.company.project.MyObject)")
+    * @param arguments an array of objects to pass as arguments
+    * @return the return value of this method or null if void
+    * @throws IllegalAccessException if the method is inaccessible
+    * @throws InvocationTargetException if the underlying method throws an exception.
+    * @throws NoSuchMethodException if no method with the given <code>methodSignature</code> could be found
+    * @throws IllegalArgumentException if an argument couldn't be converted to match the expected type
+    */
+   public static Object invokeMethod(final Object instanceOrClass, final String methodSignature, final Object[] arguments)
+      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+      if ((methodSignature.indexOf('(') == -1) || (methodSignature.indexOf('(') >= methodSignature.indexOf(')')))
+      {
+        throw new NoSuchMethodException(methodSignature);
+      }
+      Class<?>[] parameterTypes = getParameterTypes(methodSignature);
+      return getMethod(instanceOrClass, getMethodName(methodSignature), parameterTypes).invoke(instanceOrClass,
+         getCorrectedArguments(parameterTypes, arguments));
+   }
+
+   /**
+    * Gets the given arguments corrected to match the given methodSignature. Correction is necessary for array arguments not to be
+    * mistaken by varargs.
+    * 
+    * @param parameterTypes the method signatue the given arguments should match
+    * @param arguments the arguments that should be corrected
+    * @return the corrected arguments
+    */
+   private static Object[] getCorrectedArguments(Class<?>[] parameterTypes, Object[] arguments) {
+      if (arguments == null)
+      {
+        return arguments;
+      }
+      if (parameterTypes.length > arguments.length)
+      {
+        return arguments;
+      }
+      if (parameterTypes.length < arguments.length)
+      {
+        return getCorrectedArguments(parameterTypes, new Object[] {arguments});
+      }
+
+      Object[] correctedArguments = new Object[arguments.length];
+      int currentArgument = 0;
+      for (Class<?> parameterType : parameterTypes) {
+         correctedArguments[currentArgument] = getCorrectedArgument(parameterType, arguments[currentArgument]);
+         currentArgument++;
+      }
+      return correctedArguments;
+   }
+
+   /**
+    * Gets the given argument corrected to match the given parameterType. Correction is necessary for array arguments not to be
+    * mistaken by varargs.
+    * 
+    * @param parameterType the type to match the given argument upon
+    * @param argument the argument to match the given parameterType
+    * @return the corrected argument
+    */
+   private static Object getCorrectedArgument(Class<?> parameterType, Object argument) {
+      if (!parameterType.isArray() || (argument == null)) {
+         return argument; // normal argument for normal parameterType
+      }
+
+      if (!argument.getClass().isArray()) {
+         return new Object[] {argument};
+      }
+
+      if (parameterType.equals(argument.getClass()))
+       {
+        return argument; // no need to cast
+      }
+
+      // (typed) array argument for (object) array parameterType, elements need to be casted
+      Object correctedArrayArgument = Array.newInstance(parameterType.getComponentType(), Array.getLength(argument));
+      for (int index = 0; index < Array.getLength(argument); index++) {
+         if (parameterType.getComponentType().isPrimitive()) { // rely on autoboxing
+            Array.set(correctedArrayArgument, index, Array.get(argument, index));
+         } else { // cast to expected type
+            try {
+               Array.set(correctedArrayArgument, index, parameterType.getComponentType().cast(Array.get(argument, index)));
+            } catch (ClassCastException e) {
+               throw new IllegalArgumentException("Argument " + argument + " of type " + argument.getClass()
+                  + " does not match expected argument type " + parameterType + ".");
+            }
+         }
+      }
+      return correctedArrayArgument;
+   }
+
+   /**
+    * Sets the value of the named field. If fieldName denotes a static field, provide a class, otherwise provide an instance. If the
+    * fieldName denotes a final field, this method could fail with an IllegalAccessException, since setting the value of final fields
+    * at other times than instantiation can have unpredictable effects.<br/>
+    * <br/>
+    * Example:<br/>
+    * <br/>
+    * <code>
+    * String myString = "Test"; <br/>
+    * <br/>
+    * //setting the private field value<br/>
+    * PrivilegedAccessor.setValue(myString, "value", new char[] {'T', 'e', 's', 't'});<br/>
+    * <br/>
+    * //setting the static final field serialVersionUID - MIGHT FAIL<br/>
+    * PrivilegedAccessor.setValue(myString.getClass(), "serialVersionUID", 1);<br/>
+    * <br/>
+    * </code>
+    * 
+    * @param instanceOrClass the instance or class to set the field
+    * @param fieldName the name of the field
+    * @param value the new value of the field
+    * @throws NoSuchFieldException if no field with the given <code>fieldName</code> can be found
+    * @throws IllegalAccessException possibly if the field was final
+    */
+   public static void setValue(final Object instanceOrClass, final String fieldName, final Object value) throws NoSuchFieldException,
+      IllegalAccessException {
+      Field field = getField(instanceOrClass, fieldName);
+      if (Modifier.isFinal(field.getModifiers())) {
+         PrivilegedAccessor.setValue(field, "modifiers", field.getModifiers() ^ Modifier.FINAL);
+      }
+      field.set(instanceOrClass, value);
+   }
+
+   /**
+    * Gets the class with the given className.
+    * 
+    * @param className the name of the class to get
+    * @return the class for the given className
+    * @throws ClassNotFoundException if the class could not be found
+    */
+   private static Class<?> getClassForName(final String className) throws ClassNotFoundException {
+      if (className.indexOf('[') > -1) {
+         Class<?> clazz = getClassForName(className.substring(0, className.indexOf('[')));
+         return Array.newInstance(clazz, 0).getClass();
+      }
+
+      if (className.indexOf("...") > -1) {
+         Class<?> clazz = getClassForName(className.substring(0, className.indexOf("...")));
+         return Array.newInstance(clazz, 0).getClass();
+      }
+
+      try {
+         return Class.forName(className, false, Thread.currentThread().getContextClassLoader());
+      } catch (ClassNotFoundException e) {
+         return getSpecialClassForName(className);
+      }
+   }
+
+   /**
+    * Maps string representation of primitives to their corresponding classes.
+    */
+   private static final Map<String, Class<?>> PRIMITIVE_MAPPER = new HashMap<String, Class<?>>(8);
+
+   /**
+    * Fills the map with all java primitives and their corresponding classes.
+    */
+   static {
+      PRIMITIVE_MAPPER.put("int", Integer.TYPE);
+      PRIMITIVE_MAPPER.put("float", Float.TYPE);
+      PRIMITIVE_MAPPER.put("double", Double.TYPE);
+      PRIMITIVE_MAPPER.put("short", Short.TYPE);
+      PRIMITIVE_MAPPER.put("long", Long.TYPE);
+      PRIMITIVE_MAPPER.put("byte", Byte.TYPE);
+      PRIMITIVE_MAPPER.put("char", Character.TYPE);
+      PRIMITIVE_MAPPER.put("boolean", Boolean.TYPE);
+   }
+
+   /**
+    * Gets special classes for the given className. Special classes are primitives and "standard" Java types (like String)
+    * 
+    * @param className the name of the class to get
+    * @return the class for the given className
+    * @throws ClassNotFoundException if the class could not be found
+    */
+   private static Class<?> getSpecialClassForName(final String className) throws ClassNotFoundException {
+      if (PRIMITIVE_MAPPER.containsKey(className))
+      {
+        return PRIMITIVE_MAPPER.get(className);
+      }
+
+      if (missesPackageName(className))
+      {
+        return getStandardClassForName(className);
+      }
+
+      throw new ClassNotFoundException(className);
+   }
+
+   /**
+    * Gets a 'standard' java class for the given className.
+    * 
+    * @param className the className
+    * @return the class for the given className (if any)
+    * @throws ClassNotFoundException of no 'standard' java class was found for the given className
+    */
+   private static Class<?> getStandardClassForName(String className) throws ClassNotFoundException {
+      try {
+         return Class.forName("java.lang." + className, false, Thread.currentThread().getContextClassLoader());
+      } catch (ClassNotFoundException e) {
+         try {
+            return Class.forName("java.util." + className, false, Thread.currentThread().getContextClassLoader());
+         } catch (ClassNotFoundException e1) {
+            throw new ClassNotFoundException(className);
+         }
+      }
+   }
+
+   /**
+    * Tests if the given className possibly misses its package name.
+    * 
+    * @param className the className
+    * @return true if the className might miss its package name, otherwise false
+    */
+   private static boolean missesPackageName(String className) {
+      if (className.contains("."))
+      {
+        return false;
+      }
+      if (className.startsWith(className.substring(0, 1).toUpperCase()))
+      {
+        return true;
+      }
+      return false;
+   }
+
+   /**
+    * Gets the constructor for a given class with the given parameters.
+    * 
+    * @param type the class to instantiate
+    * @param parameterTypes the types of the parameters
+    * @return the constructor
+    * @throws NoSuchMethodException if the method could not be found
+    */
+   private static <T> Constructor<T> getConstructor(final Class<T> type, final Class<?>[] parameterTypes) throws NoSuchMethodException {
+      Constructor<T> constructor = type.getDeclaredConstructor(parameterTypes);
+      constructor.setAccessible(true);
+      return constructor;
+   }
+
+   /**
+    * Return the named field from the given instance or class. Returns a static field if instanceOrClass is a class.
+    * 
+    * @param instanceOrClass the instance or class to get the field from
+    * @param fieldName the name of the field to get
+    * @return the field
+    * @throws NoSuchFieldException if no such field can be found
+    * @throws InvalidParameterException if instanceOrClass was null
+    */
+   private static Field getField(final Object instanceOrClass, final String fieldName) throws NoSuchFieldException,
+      InvalidParameterException {
+      if (instanceOrClass == null)
+      {
+        throw new InvalidParameterException("Can't get field on null object/class");
+      }
+
+      Class<?> type = getClass(instanceOrClass);
+
+      try {
+         Field field = type.getDeclaredField(fieldName);
+         field.setAccessible(true);
+         return field;
+      } catch (NoSuchFieldException e) {
+         if (type.getSuperclass() == null)
+        {
+          throw e;
+        }
+         return getField(type.getSuperclass(), fieldName);
+      }
+   }
+
+   /**
+    * Gets the class of the given parameter. If the parameter is a class, it is returned, if it is an object, its class is returned
+    * 
+    * @param instanceOrClass the instance or class to get the class of
+    * @return the class of the given parameter
+    */
+   private static Class<?> getClass(final Object instanceOrClass) {
+      if (instanceOrClass instanceof Class)
+      {
+        return (Class<?>) instanceOrClass;
+      }
+
+      return instanceOrClass.getClass();
+   }
+
+   /**
+    * Return the named method with a method signature matching classTypes from the given class.
+    * 
+    * @param type the class to get the method from
+    * @param methodName the name of the method to get
+    * @param parameterTypes the parameter-types of the method to get
+    * @return the method
+    * @throws NoSuchMethodException if the method could not be found
+    */
+   private static Method getMethod(final Class<?> type, final String methodName, final Class<?>[] parameterTypes)
+      throws NoSuchMethodException {
+      try {
+         return type.getDeclaredMethod(methodName, parameterTypes);
+      } catch (NoSuchMethodException e) {
+         if (type.getSuperclass() == null)
+        {
+          throw e;
+        }
+         return getMethod(type.getSuperclass(), methodName, parameterTypes);
+      }
+   }
+
+   /**
+    * Gets the method with the given name and parameters from the given instance or class. If instanceOrClass is a class, then we get a
+    * static method.
+    * 
+    * @param instanceOrClass the instance or class to get the method of
+    * @param methodName the name of the method
+    * @param parameterTypes the parameter-types of the method to get
+    * @return the method
+    * @throws NoSuchMethodException if the method could not be found
+    */
+   private static Method getMethod(final Object instanceOrClass, final String methodName, final Class<?>[] parameterTypes)
+      throws NoSuchMethodException {
+      Class<?> type;
+
+      type = getClass(instanceOrClass);
+
+      Method accessMethod = getMethod(type, methodName, parameterTypes);
+      accessMethod.setAccessible(true);
+      return accessMethod;
+   }
+
+   /**
+    * Gets the name of a method.
+    * 
+    * @param methodSignature the signature of the method
+    * @return the name of the method
+    */
+   private static String getMethodName(final String methodSignature) {
+      try {
+         return methodSignature.substring(0, methodSignature.indexOf('(')).trim();
+      } catch (StringIndexOutOfBoundsException e) {
+         assert false : "Signature must have been checked before this method was called";
+         return null;
+      }
+   }
+
+   /**
+    * Gets the types of the parameters.
+    * 
+    * @param parameters the parameters
+    * @return the class-types of the arguments
+    */
+   private static Class<?>[] getParameterTypes(final Object[] parameters) {
+      if (parameters == null)
+      {
+        return new Class[0];
+      }
+
+      Class<?>[] typesOfParameters = new Class[parameters.length];
+
+      for (int i = 0; i < parameters.length; i++) {
+         typesOfParameters[i] = parameters[i].getClass();
+      }
+      return typesOfParameters;
+   }
+
+   /**
+    * Gets the types of the given parameters. If the parameters don't match the given methodSignature an IllegalArgumentException is
+    * thrown.
+    * 
+    * @param methodSignature the signature of the method
+    * @return the parameter types as class[]
+    * @throws NoSuchMethodException if the method could not be found
+    * @throws IllegalArgumentException if one of the given parameters doesn't math the given methodSignature
+    */
+   private static Class<?>[] getParameterTypes(final String methodSignature) throws NoSuchMethodException, IllegalArgumentException {
+      String signature = getSignatureWithoutBraces(methodSignature);
+
+      StringTokenizer tokenizer = new StringTokenizer(signature, ", *");
+      Class<?>[] typesInSignature = new Class[tokenizer.countTokens()];
+
+      for (int x = 0; tokenizer.hasMoreTokens(); x++) {
+         String className = tokenizer.nextToken();
+         try {
+            typesInSignature[x] = getClassForName(className);
+         } catch (ClassNotFoundException e) {
+            NoSuchMethodException noSuchMethodException = new NoSuchMethodException(methodSignature);
+            noSuchMethodException.initCause(e);
+            throw noSuchMethodException;
+         }
+      }
+      return typesInSignature;
+   }
+
+   /**
+    * Gets the parameter types as a string.
+    * 
+    * @param classTypes the types to get as names.
+    * @return the parameter types as a string
+    * 
+    * @see java.lang.Class#argumentTypesToString(Class[])
+    */
+   private static String getParameterTypesAsString(final Class<?>[] classTypes) {
+      assert classTypes != null : "getParameterTypes() should have been called before this method and should have provided not-null classTypes";
+      if (classTypes.length == 0)
+      {
+        return "";
+      }
+
+      StringBuilder parameterTypes = new StringBuilder();
+      for (Class<?> clazz : classTypes) {
+         assert clazz != null : "getParameterTypes() should have been called before this method and should have provided not-null classTypes";
+         parameterTypes.append(clazz.getName()).append(", ");
+      }
+
+      return parameterTypes.substring(0, parameterTypes.length() - 2);
+   }
+
+   /**
+    * Removes the braces around the methods signature.
+    * 
+    * @param methodSignature the signature with braces
+    * @return the signature without braces
+    */
+   private static String getSignatureWithoutBraces(final String methodSignature) {
+      try {
+         return methodSignature.substring(methodSignature.indexOf('(') + 1, methodSignature.indexOf(')'));
+      } catch (IndexOutOfBoundsException e) {
+         assert false : "signature must have been checked before this method";
+         return null;
+      }
+   }
+
+}