From 136c0793b90b72b928c4d77dc109dd5c644e00d3 Mon Sep 17 00:00:00 2001 From: gmungoc Date: Fri, 9 Jun 2017 16:28:28 +0100 Subject: [PATCH] JAL-2446 merged to spike branch --- .classpath | 2 +- examples/appletParameters.html | 7 +- examples/example.json | 2 +- examples/exampleFeatures.txt | 2 +- examples/groovy/colourConserved.groovy | 88 ++ examples/groovy/colourSchemes.groovy | 155 +++ examples/groovy/colourUnconserved.groovy | 84 ++ examples/groovy/featuresCounter.groovy | 73 ++ examples/groovy/visibleFeaturesCounter.groovy | 89 ++ examples/testdata/example_annot_file.jva | 2 +- help/help.jhm | 11 +- help/helpTOC.xml | 10 +- help/html/colourSchemes/clustal.html | 88 +- help/html/colourSchemes/user.html | 11 +- help/html/colourSchemes/userDefined_java7.gif | Bin 38939 -> 90049 bytes help/html/features/chimera.html | 37 +- help/html/features/groovy.html | 11 +- help/html/features/overview.html | 5 + help/html/features/preferences.html | 58 +- help/html/features/splitView.html | 6 +- help/html/groovy/featuresCounter.html | 123 ++ help/html/io/tcoffeescores.html | 16 +- help/html/menus/desktopMenu.html | 1 + help/html/menus/popupMenu.html | 6 +- help/html/releases.html | 160 +++ help/html/webServices/JABAWS.html | 15 +- help/html/webServices/jnet.html | 20 +- help/html/webServices/jnetprediction.gif | Bin 14715 -> 63895 bytes help/html/webServices/msaclient.html | 23 +- help/html/webServices/proteinDisorder.html | 4 +- help/html/webServices/urllinks.html | 97 +- help/html/whatsNew.html | 83 +- lib/jabaws-min-client-2.2.0.jar | Bin 0 -> 540901 bytes resources/lang/Messages.properties | 115 +- resources/lang/Messages_es.properties | 124 +- resources/scoreModel/blosum62.scm | 34 + resources/scoreModel/blosum80.scm | 32 + resources/scoreModel/dna.scm | 27 + resources/scoreModel/pam250.scm | 32 + resources/uniprot_mapping.xml | 20 +- src/MCview/AppletPDBCanvas.java | 24 +- src/MCview/AppletPDBViewer.java | 19 +- src/MCview/PDBCanvas.java | 22 +- src/MCview/PDBChain.java | 57 +- src/MCview/PDBViewer.java | 784 ------------ src/jalview/analysis/AAFrequency.java | 32 +- src/jalview/analysis/AlignSeq.java | 379 ++---- src/jalview/analysis/AlignmentSorter.java | 331 +++-- src/jalview/analysis/AlignmentUtils.java | 339 +++-- src/jalview/analysis/AverageDistanceTree.java | 121 ++ src/jalview/analysis/Conservation.java | 229 ++-- src/jalview/analysis/CrossRef.java | 33 +- src/jalview/analysis/Dna.java | 32 +- src/jalview/analysis/NJTree.java | 1300 +------------------ src/jalview/analysis/PCA.java | 198 +-- src/jalview/analysis/Rna.java | 181 ++- src/jalview/analysis/SeqsetUtils.java | 27 +- src/jalview/analysis/TreeBuilder.java | 460 +++++++ src/jalview/analysis/TreeModel.java | 673 ++++++++++ .../analysis/scoremodels/DistanceScoreModel.java | 40 + .../analysis/scoremodels/FeatureDistanceModel.java | 242 ++++ .../analysis/scoremodels/FeatureScoreModel.java | 161 --- src/jalview/analysis/scoremodels/PIDModel.java | 243 ++++ .../analysis/scoremodels/PIDScoreModel.java | 75 -- src/jalview/analysis/scoremodels/ScoreMatrix.java | 594 +++++++++ src/jalview/analysis/scoremodels/ScoreModels.java | 141 +++ .../analysis/scoremodels/SimilarityParams.java | 130 ++ .../analysis/scoremodels/SimilarityScoreModel.java | 43 + .../{SWScoreModel.java => SmithWatermanModel.java} | 59 +- src/jalview/api/AlignViewportI.java | 52 +- src/jalview/api/AlignmentColsCollectionI.java | 13 + src/jalview/api/AlignmentRowsCollectionI.java | 24 + src/jalview/api/ComplexAlignFile.java | 4 +- src/jalview/api/FeatureRenderer.java | 73 +- src/jalview/api/FeaturesDisplayedI.java | 13 +- src/jalview/api/SequenceRenderer.java | 6 +- src/jalview/api/SiftsClientI.java | 2 +- src/jalview/api/ViewStyleI.java | 14 + src/jalview/api/analysis/PairwiseScoreModelI.java | 22 + src/jalview/api/analysis/ScoreModelI.java | 89 +- src/jalview/api/analysis/SimilarityParamsI.java | 43 + src/jalview/appletgui/APopupMenu.java | 373 ++++-- src/jalview/appletgui/AlignFrame.java | 256 ++-- src/jalview/appletgui/AlignViewport.java | 66 +- src/jalview/appletgui/AlignmentPanel.java | 452 +++---- src/jalview/appletgui/AnnotationColourChooser.java | 123 +- src/jalview/appletgui/AnnotationColumnChooser.java | 83 +- src/jalview/appletgui/AnnotationLabels.java | 6 +- src/jalview/appletgui/AnnotationPanel.java | 35 +- src/jalview/appletgui/AnnotationRowFilter.java | 43 +- src/jalview/appletgui/AppletJmol.java | 22 +- src/jalview/appletgui/AppletJmolBinding.java | 16 +- src/jalview/appletgui/CutAndPasteTransfer.java | 6 +- src/jalview/appletgui/ExtJmol.java | 6 +- src/jalview/appletgui/FeatureRenderer.java | 275 ++-- src/jalview/appletgui/FeatureSettings.java | 165 +-- src/jalview/appletgui/Finder.java | 18 +- src/jalview/appletgui/FontChooser.java | 95 +- src/jalview/appletgui/IdCanvas.java | 45 +- src/jalview/appletgui/IdPanel.java | 247 ++-- src/jalview/appletgui/OverviewCanvas.java | 167 +++ src/jalview/appletgui/OverviewPanel.java | 463 ++----- src/jalview/appletgui/PCAPanel.java | 43 +- src/jalview/appletgui/RedundancyPanel.java | 13 +- src/jalview/appletgui/ScalePanel.java | 118 +- src/jalview/appletgui/SeqCanvas.java | 105 +- src/jalview/appletgui/SeqPanel.java | 379 +++--- src/jalview/appletgui/SequenceRenderer.java | 34 +- src/jalview/appletgui/SliderPanel.java | 94 +- src/jalview/appletgui/TreeCanvas.java | 79 +- src/jalview/appletgui/TreePanel.java | 114 +- src/jalview/appletgui/UserDefinedColours.java | 57 +- src/jalview/bin/Cache.java | 92 +- src/jalview/bin/Jalview.java | 21 +- src/jalview/bin/JalviewLite.java | 24 +- src/jalview/commands/EditCommand.java | 140 ++- src/jalview/controller/AlignViewController.java | 121 +- src/jalview/datamodel/Alignment.java | 209 +-- src/jalview/datamodel/AlignmentAnnotation.java | 32 +- src/jalview/datamodel/AlignmentI.java | 70 +- src/jalview/datamodel/AlignmentView.java | 47 +- src/jalview/datamodel/AllColsCollection.java | 52 + src/jalview/datamodel/AllColsIterator.java | 73 ++ src/jalview/datamodel/AllRowsCollection.java | 63 + src/jalview/datamodel/AllRowsIterator.java | 77 ++ src/jalview/datamodel/AnnotatedCollectionI.java | 18 + src/jalview/datamodel/Annotation.java | 20 + src/jalview/datamodel/BinarySequence.java | 28 +- src/jalview/datamodel/CigarArray.java | 6 +- src/jalview/datamodel/ColumnSelection.java | 1324 +++----------------- src/jalview/datamodel/ContiguousI.java | 8 + src/jalview/datamodel/HiddenColumns.java | 1299 +++++++++++++++++++ src/jalview/datamodel/HiddenSequences.java | 87 +- src/jalview/datamodel/Mapping.java | 27 +- src/jalview/datamodel/Range.java | 52 + src/jalview/datamodel/SearchResults.java | 15 +- src/jalview/datamodel/SeqCigar.java | 12 +- src/jalview/datamodel/Sequence.java | 618 +++++++-- src/jalview/datamodel/SequenceCollectionI.java | 6 + src/jalview/datamodel/SequenceCursor.java | 125 ++ src/jalview/datamodel/SequenceFeature.java | 229 ++-- src/jalview/datamodel/SequenceGroup.java | 196 ++- src/jalview/datamodel/SequenceI.java | 66 +- src/jalview/datamodel/SequenceNode.java | 6 +- src/jalview/datamodel/VisibleColsCollection.java | 53 + src/jalview/datamodel/VisibleColsIterator.java | 131 ++ src/jalview/datamodel/VisibleRowsCollection.java | 60 + src/jalview/datamodel/VisibleRowsIterator.java | 99 ++ .../datamodel/features/FeatureLocationI.java | 12 + src/jalview/datamodel/features/FeatureStore.java | 1036 +++++++++++++++ src/jalview/datamodel/features/NCList.java | 626 +++++++++ src/jalview/datamodel/features/NCNode.java | 255 ++++ .../datamodel/features/RangeComparator.java | 78 ++ .../datamodel/features/SequenceFeatures.java | 494 ++++++++ .../datamodel/features/SequenceFeaturesI.java | 204 +++ src/jalview/datamodel/xdb/embl/EmblEntry.java | 62 +- .../datamodel/xdb/uniprot/UniprotEntry.java | 107 ++ .../datamodel/xdb/uniprot/UniprotFeature.java | 78 ++ src/jalview/datamodel/xdb/uniprot/UniprotFile.java | 42 + .../datamodel/xdb/uniprot/UniprotProteinName.java | 47 + .../datamodel/xdb/uniprot/UniprotSequence.java | 58 + src/jalview/ext/android/ContainerHelpers.java | 6 +- src/jalview/ext/android/SparseDoubleArray.java | 443 +++++++ src/jalview/ext/ensembl/EnsemblCdna.java | 20 + src/jalview/ext/ensembl/EnsemblGene.java | 64 +- src/jalview/ext/ensembl/EnsemblRestClient.java | 29 +- src/jalview/ext/ensembl/EnsemblSeqProxy.java | 121 +- src/jalview/ext/jmol/JalviewJmolBinding.java | 219 ++-- src/jalview/ext/jmol/JmolCommands.java | 32 +- src/jalview/ext/rbvi/chimera/AtomSpecModel.java | 4 +- src/jalview/ext/rbvi/chimera/ChimeraCommands.java | 99 +- .../ext/rbvi/chimera/JalviewChimeraBinding.java | 261 ++-- src/jalview/ext/varna/VarnaCommands.java | 14 +- src/jalview/fts/api/GFTSPanelI.java | 8 + src/jalview/fts/core/GFTSPanel.java | 105 +- src/jalview/fts/service/pdb/PDBFTSPanel.java | 20 +- .../fts/service/uniprot/UniProtFTSRestClient.java | 10 +- .../fts/service/uniprot/UniprotFTSPanel.java | 22 +- src/jalview/gui/AlignFrame.java | 878 ++++--------- src/jalview/gui/AlignViewport.java | 102 +- src/jalview/gui/AlignmentPanel.java | 693 +++++----- src/jalview/gui/AnnotationColourChooser.java | 231 ++-- src/jalview/gui/AnnotationColumnChooser.java | 280 ++--- src/jalview/gui/AnnotationExporter.java | 17 +- src/jalview/gui/AnnotationLabels.java | 35 +- src/jalview/gui/AnnotationPanel.java | 50 +- src/jalview/gui/AnnotationRowFilter.java | 329 +++-- src/jalview/gui/AppJmol.java | 454 +------ src/jalview/gui/AppJmolBinding.java | 36 +- src/jalview/gui/AppVarna.java | 3 +- src/jalview/gui/AppVarnaBinding.java | 2 +- src/jalview/gui/CalculationChooser.java | 593 +++++++++ src/jalview/gui/ChimeraViewFrame.java | 639 ++-------- src/jalview/gui/ColourMenuHelper.java | 297 +++++ src/jalview/gui/ComboBoxTooltipRenderer.java | 42 + src/jalview/gui/Console.java | 26 +- src/jalview/gui/CutAndPasteTransfer.java | 9 +- src/jalview/gui/DasSourceBrowser.java | 2 +- src/jalview/gui/Desktop.java | 126 +- src/jalview/gui/FeatureRenderer.java | 460 ++++--- src/jalview/gui/FeatureSettings.java | 289 +++-- src/jalview/gui/Finder.java | 107 +- src/jalview/gui/FontChooser.java | 169 ++- src/jalview/gui/IdCanvas.java | 47 +- src/jalview/gui/IdPanel.java | 99 +- src/jalview/gui/JDatabaseTree.java | 22 +- src/jalview/gui/Jalview2XML.java | 378 +++--- src/jalview/gui/Jalview2XML_V1.java | 46 +- src/jalview/gui/JalviewChimeraBindingModel.java | 16 +- src/jalview/gui/JalviewDialog.java | 4 + src/jalview/gui/OverviewCanvas.java | 182 +++ src/jalview/gui/OverviewPanel.java | 520 ++------ src/jalview/gui/PCAPanel.java | 181 +-- src/jalview/gui/PopupMenu.java | 800 ++++-------- src/jalview/gui/Preferences.java | 382 ++++-- src/jalview/gui/RedundancyPanel.java | 23 +- src/jalview/gui/ScalePanel.java | 125 +- src/jalview/gui/SeqCanvas.java | 103 +- src/jalview/gui/SeqPanel.java | 497 +++++--- src/jalview/gui/SequenceFetcher.java | 11 +- src/jalview/gui/SequenceRenderer.java | 61 +- src/jalview/gui/SliderPanel.java | 315 +++-- src/jalview/gui/SplitFrame.java | 7 +- src/jalview/gui/StructureChooser.java | 23 +- src/jalview/gui/StructureViewerBase.java | 505 +++++++- src/jalview/gui/TextColourChooser.java | 138 +- src/jalview/gui/TreeCanvas.java | 111 +- src/jalview/gui/TreePanel.java | 279 ++--- src/jalview/gui/UserDefinedColours.java | 833 +++++------- src/jalview/gui/VamsasApplication.java | 13 +- src/jalview/gui/WsParamSetManager.java | 3 +- src/jalview/gui/WsPreferences.java | 6 +- src/jalview/io/AnnotationFile.java | 139 +- src/jalview/io/AppletFormatAdapter.java | 17 +- src/jalview/io/FeaturesFile.java | 467 ++++--- src/jalview/io/FileFormat.java | 15 + src/jalview/io/FileFormats.java | 4 +- src/jalview/io/FileLoader.java | 62 +- src/jalview/io/FormatAdapter.java | 18 +- src/jalview/io/HTMLOutput.java | 9 +- src/jalview/io/HtmlFile.java | 14 +- src/jalview/io/IdentifyFile.java | 27 +- src/jalview/io/JSONFile.java | 141 ++- src/jalview/io/JalviewFileChooser.java | 10 +- src/jalview/io/ScoreMatrixFile.java | 433 +++++++ src/jalview/io/SequenceAnnotationReport.java | 186 +-- src/jalview/io/StockholmFile.java | 164 ++- src/jalview/io/StructureFile.java | 6 +- src/jalview/io/TCoffeeScoreFile.java | 65 +- src/jalview/io/VamsasAppDatastore.java | 2 +- src/jalview/io/cache/AppCache.java | 153 +++ src/jalview/io/cache/JvCacheableInputBox.java | 287 +++++ src/jalview/io/gff/ExonerateHelper.java | 8 +- src/jalview/io/gff/Gff3Helper.java | 9 +- src/jalview/io/gff/GffHelperBase.java | 17 +- src/jalview/io/gff/InterProScanHelper.java | 19 +- src/jalview/io/packed/JalviewDataset.java | 4 +- src/jalview/io/vamsas/Datasetsequence.java | 27 +- src/jalview/io/vamsas/Sequencefeature.java | 42 +- src/jalview/io/vamsas/Tree.java | 29 +- src/jalview/javascript/JsSelectionSender.java | 3 +- .../javascript/MouseOverStructureListener.java | 7 +- src/jalview/jbgui/GAlignFrame.java | 853 +++---------- src/jalview/jbgui/GDesktop.java | 41 +- src/jalview/jbgui/GFinder.java | 99 +- src/jalview/jbgui/GFontChooser.java | 107 +- src/jalview/jbgui/GPCAPanel.java | 149 +-- src/jalview/jbgui/GPreferences.java | 531 ++++++-- src/jalview/jbgui/GSequenceLink.java | 125 +- src/jalview/jbgui/GSliderPanel.java | 35 +- src/jalview/jbgui/GStructureChooser.java | 96 +- src/jalview/jbgui/GStructureViewer.java | 316 +---- src/jalview/jbgui/GUserDefinedColours.java | 72 +- src/jalview/math/Matrix.java | 369 ++++-- src/jalview/math/MatrixI.java | 97 ++ src/jalview/math/SparseMatrix.java | 218 ++++ src/jalview/renderer/AnnotationRenderer.java | 32 +- src/jalview/renderer/OverviewRenderer.java | 208 +++ src/jalview/renderer/ResidueShader.java | 375 ++++++ src/jalview/renderer/ResidueShaderI.java | 64 + src/jalview/renderer/ScaleRenderer.java | 8 +- .../renderer/seqfeatures/FeatureColourFinder.java | 124 ++ .../renderer/seqfeatures/FeatureRenderer.java | 567 ++++----- src/jalview/schemes/AnnotationColourGradient.java | 316 +++-- src/jalview/schemes/Blosum62ColourScheme.java | 92 +- src/jalview/schemes/BuriedColourScheme.java | 29 + src/jalview/schemes/ClustalxColourScheme.java | 150 +-- src/jalview/schemes/ColourSchemeI.java | 118 +- src/jalview/schemes/ColourSchemeLoader.java | 125 ++ src/jalview/schemes/ColourSchemeProperty.java | 604 +-------- src/jalview/schemes/ColourSchemes.java | 170 +++ src/jalview/schemes/CovariationColourScheme.java | 46 +- src/jalview/schemes/FeatureColour.java | 19 +- src/jalview/schemes/FollowerColourScheme.java | 41 +- src/jalview/schemes/HelixColourScheme.java | 29 + src/jalview/schemes/HydrophobicColourScheme.java | 29 + src/jalview/schemes/JalviewColourScheme.java | 66 + src/jalview/schemes/NucleotideColourScheme.java | 62 +- src/jalview/schemes/PIDColourScheme.java | 74 +- .../schemes/PurinePyrimidineColourScheme.java | 65 +- src/jalview/schemes/RNAHelicesColour.java | 50 +- src/jalview/schemes/RNAHelicesColourChooser.java | 47 +- .../schemes/RNAInteractionColourScheme.java | 68 +- src/jalview/schemes/ResidueColourScheme.java | 292 ++--- src/jalview/schemes/ResidueProperties.java | 228 ---- src/jalview/schemes/ScoreColourScheme.java | 50 +- src/jalview/schemes/ScoreMatrix.java | 173 --- src/jalview/schemes/StrandColourScheme.java | 29 + src/jalview/schemes/TCoffeeColourScheme.java | 69 +- src/jalview/schemes/TaylorColourScheme.java | 31 +- src/jalview/schemes/TurnColourScheme.java | 29 + src/jalview/schemes/UserColourScheme.java | 312 +++-- src/jalview/schemes/ZappoColourScheme.java | 31 +- src/jalview/structure/SelectionListener.java | 4 +- src/jalview/structure/StructureListener.java | 2 +- .../structure/StructureSelectionManager.java | 24 +- .../structures/models/AAStructureBindingModel.java | 100 +- src/jalview/urls/CustomUrlProvider.java | 342 +++++ src/jalview/urls/IdOrgSettings.java | 55 + src/jalview/urls/IdentifiersUrlProvider.java | 269 ++++ src/jalview/urls/UrlLinkDisplay.java | 220 ++++ src/jalview/urls/UrlLinkTableModel.java | 277 ++++ src/jalview/urls/UrlProvider.java | 251 ++++ src/jalview/urls/UrlProviderImpl.java | 123 ++ src/jalview/urls/api/UrlProviderFactoryI.java | 34 + src/jalview/urls/api/UrlProviderI.java | 117 ++ .../urls/applet/AppletUrlProviderFactory.java | 64 + .../urls/desktop/DesktopUrlProviderFactory.java | 71 ++ src/jalview/util/ColorUtils.java | 163 +++ src/jalview/util/Comparison.java | 14 +- src/jalview/util/ImageMaker.java | 10 +- src/jalview/util/IntRangeComparator.java | 31 + src/jalview/util/LinkedIdentityHashSet.java | 4 +- src/jalview/util/MappingUtils.java | 23 +- src/jalview/util/ParseHtmlBodyAndLinks.java | 2 +- src/jalview/util/SetUtils.java | 43 + src/jalview/util/UrlConstants.java | 19 +- src/jalview/util/UrlLink.java | 240 ++-- src/jalview/viewmodel/AlignmentViewport.java | 382 +++--- src/jalview/viewmodel/OverviewDimensions.java | 243 ++++ .../viewmodel/OverviewDimensionsHideHidden.java | 130 ++ .../viewmodel/OverviewDimensionsShowHidden.java | 210 ++++ src/jalview/viewmodel/PCAModel.java | 84 +- src/jalview/viewmodel/ViewportListenerI.java | 8 + src/jalview/viewmodel/ViewportProperties.java | 40 + src/jalview/viewmodel/ViewportRanges.java | 519 ++++++++ .../seqfeatures/FeatureRendererModel.java | 349 ++++-- .../viewmodel/seqfeatures/FeaturesDisplayed.java | 18 +- src/jalview/viewmodel/styles/ViewStyle.java | 20 + .../workers/AlignmentAnnotationFactory.java | 32 +- ...nterWorker.java => ColumnCounterSetWorker.java} | 142 ++- src/jalview/workers/ConsensusThread.java | 9 +- src/jalview/workers/ConservationThread.java | 11 +- ...eatureCounterI.java => FeatureSetCounterI.java} | 24 +- src/jalview/ws/DBRefFetcher.java | 28 +- src/jalview/ws/DasSequenceFeatureFetcher.java | 4 +- src/jalview/ws/dbsources/Uniprot.java | 14 +- .../dbsources/das/datamodel/DasSourceRegistry.java | 6 +- src/jalview/ws/jws1/JPredThread.java | 19 +- src/jalview/ws/jws1/MsaWSThread.java | 6 +- src/jalview/ws/jws1/SeqSearchWSThread.java | 2 +- src/jalview/ws/jws2/AADisorderClient.java | 13 +- src/jalview/ws/jws2/JPred301Client.java | 299 ----- src/jalview/ws/jws2/Jws2Discoverer.java | 73 +- src/jalview/ws/jws2/MsaWSThread.java | 10 +- .../ws/jws2/jabaws2/Jws2InstanceFactory.java | 6 +- src/jalview/ws/rest/RestClient.java | 12 +- src/jalview/ws/rest/RestJobThread.java | 26 +- src/jalview/ws/rest/params/Alignment.java | 4 +- src/jalview/ws/sifts/SiftsClient.java | 83 +- src/jalview/ws/utils/UrlDownloadClient.java | 110 ++ test/MCview/PDBChainTest.java | 27 +- test/jalview/analysis/AlignSeqTest.java | 38 +- test/jalview/analysis/AlignmentSorterTest.java | 131 ++ test/jalview/analysis/AlignmentUtilsTests.java | 91 +- test/jalview/analysis/DnaTest.java | 14 +- test/jalview/analysis/RnaTest.java | 55 +- test/jalview/analysis/SeqsetUtilsTest.java | 23 +- test/jalview/analysis/TestAlignSeq.java | 2 +- .../scoremodels/FeatureDistanceModelTest.java | 355 ++++++ .../scoremodels/FeatureScoreModelTest.java | 193 --- .../jalview/analysis/scoremodels/PIDModelTest.java | 176 +++ .../analysis/scoremodels/ScoreMatrixTest.java | 588 +++++++++ .../analysis/scoremodels/ScoreModelsTest.java | 105 ++ test/jalview/commands/EditCommandTest.java | 231 ++++ .../datamodel/AlignmentAnnotationTests.java | 42 + test/jalview/datamodel/AlignmentTest.java | 140 +++ test/jalview/datamodel/AlignmentViewTest.java | 69 + test/jalview/datamodel/AllColsIteratorTest.java | 102 ++ test/jalview/datamodel/AllRowsIteratorTest.java | 130 ++ test/jalview/datamodel/ColumnSelectionTest.java | 464 ++----- test/jalview/datamodel/HiddenColumnsTest.java | 588 +++++++++ test/jalview/datamodel/HiddenSequencesTest.java | 136 +- test/jalview/datamodel/SequenceFeatureTest.java | 60 +- test/jalview/datamodel/SequenceGroupTest.java | 293 +++++ test/jalview/datamodel/SequenceTest.java | 665 +++++++++- .../jalview/datamodel/VisibleColsIteratorTest.java | 198 +++ .../jalview/datamodel/VisibleRowsIteratorTest.java | 233 ++++ .../datamodel/features/FeatureStoreTest.java | 830 ++++++++++++ test/jalview/datamodel/features/NCListTest.java | 682 ++++++++++ test/jalview/datamodel/features/NCNodeTest.java | 136 ++ .../datamodel/features/RangeComparatorTest.java | 65 + .../datamodel/features/SequenceFeaturesTest.java | 1217 ++++++++++++++++++ .../jalview/ext/android/SparseDoubleArrayTest.java | 53 + test/jalview/ext/ensembl/EnsemblCdnaTest.java | 17 +- test/jalview/ext/ensembl/EnsemblCdsTest.java | 14 +- test/jalview/ext/ensembl/EnsemblGeneTest.java | 55 +- test/jalview/ext/ensembl/EnsemblGenomeTest.java | 20 +- test/jalview/ext/ensembl/EnsemblSeqProxyTest.java | 21 +- test/jalview/ext/jmol/JmolCommandsTest.java | 81 +- test/jalview/ext/jmol/JmolParserTest.java | 11 +- test/jalview/ext/jmol/JmolViewerTest.java | 2 + .../ext/rbvi/chimera/ChimeraCommandsTest.java | 115 +- .../ext/rbvi/chimera/JalviewChimeraView.java | 26 +- test/jalview/fts/service/pdb/PDBFTSPanelTest.java | 8 +- test/jalview/gui/AlignFrameTest.java | 515 +++++++- test/jalview/gui/AlignViewportTest.java | 90 +- test/jalview/gui/AlignmentPanelTest.java | 221 ++++ test/jalview/gui/AnnotationChooserTest.java | 170 +-- test/jalview/gui/AnnotationRowFilterTest.java | 121 ++ test/jalview/gui/PopupMenuTest.java | 15 +- test/jalview/gui/SeqPanelTest.java | 91 ++ test/jalview/gui/SequenceRendererTest.java | 26 +- test/jalview/gui/StructureChooserTest.java | 4 +- test/jalview/io/AnnotatedPDBFileInputTest.java | 37 +- test/jalview/io/AnnotationFileIOTest.java | 14 +- test/jalview/io/FeaturesFileTest.java | 249 +++- test/jalview/io/FileFormatsTest.java | 31 +- test/jalview/io/FileLoaderTest.java | 22 + test/jalview/io/IdentifyFileTest.java | 3 +- test/jalview/io/JSONFileTest.java | 258 +++- test/jalview/io/Jalview2xmlTests.java | 308 +++-- test/jalview/io/JalviewFileViewTest.java | 6 +- test/jalview/io/NewickFileTests.java | 8 +- test/jalview/io/PhylipFileTests.java | 3 +- test/jalview/io/RNAMLfileTest.java | 2 +- test/jalview/io/ScoreMatrixFileTest.java | 506 ++++++++ test/jalview/io/SequenceAnnotationReportTest.java | 138 ++ test/jalview/io/StockholmFileTest.java | 368 +++++- test/jalview/io/cache/AppCacheTest.java | 61 + test/jalview/io/cache/JvCacheableInputBoxTest.java | 70 ++ test/jalview/io/gff/Gff3HelperTest.java | 46 + test/jalview/io/gff/InterProScanHelperTest.java | 12 + test/jalview/io/testProps.jvprops | 1 + test/jalview/math/MatrixTest.java | 397 +++++- test/jalview/math/SparseMatrixTest.java | 416 ++++++ test/jalview/renderer/ResidueShaderTest.java | 166 +++ test/jalview/renderer/ScaleRendererTest.java | 73 ++ .../seqfeatures/FeatureColourFinderTest.java | 446 +++++++ .../renderer/seqfeatures/FeatureRendererTest.java | 357 ++++++ .../schemes/AnnotationColourGradientTest.java | 300 +++++ test/jalview/schemes/Blosum62ColourSchemeTest.java | 56 + test/jalview/schemes/ClustalxColourSchemeTest.java | 134 ++ test/jalview/schemes/ColourSchemePropertyTest.java | 135 ++ test/jalview/schemes/ColourSchemesTest.java | 299 +++++ test/jalview/schemes/FeatureColourTest.java | 22 +- test/jalview/schemes/JalviewColourSchemeTest.java | 48 + test/jalview/schemes/PIDColourSchemeTest.java | 119 ++ test/jalview/schemes/ResidueColourSchemeTest.java | 254 ++-- test/jalview/schemes/ScoreMatrixPrinter.java | 67 - test/jalview/schemes/UserColourSchemeTest.java | 70 +- .../structure/StructureSelectionManagerTest.java | 4 +- .../models/AAStructureBindingModelTest.java | 111 +- .../jalview/urls/AppletUrlProviderFactoryTest.java | 81 ++ test/jalview/urls/CustomUrlProviderTest.java | 163 +++ .../urls/DesktopUrlProviderFactoryTest.java | 109 ++ test/jalview/urls/IdentifiersUrlProviderTest.java | 172 +++ test/jalview/urls/UrlLinkDisplayTest.java | 146 +++ test/jalview/urls/UrlLinkTableModelTest.java | 348 +++++ test/jalview/urls/UrlProviderTest.java | 120 ++ test/jalview/util/ColorUtilsTest.java | 77 ++ test/jalview/util/ComparisonTest.java | 7 +- test/jalview/util/MappingUtilsTest.java | 102 +- test/jalview/util/SetUtilsTest.java | 46 + test/jalview/util/UrlLinkTest.java | 123 +- .../OverviewDimensionsHideHiddenTest.java | 982 +++++++++++++++ .../OverviewDimensionsShowHiddenTest.java | 1025 +++++++++++++++ test/jalview/viewmodel/ViewportRangesTest.java | 568 +++++++++ test/jalview/ws/dbsources/RemoteFormatTest.java | 121 ++ test/jalview/ws/dbsources/UniprotTest.java | 15 +- .../ws/jabaws/DisorderAnnotExportImport.java | 20 +- .../ws/jabaws/JpredJabaStructExportImport.java | 325 ----- test/jalview/ws/jabaws/MinJabawsClientTests.java | 5 + test/jalview/ws/jabaws/RNAStructExportImport.java | 28 +- test/jalview/ws/jws2/ParameterUtilsTest.java | 11 +- test/jalview/ws/seqfetcher/DasSequenceFetcher.java | 15 +- test/jalview/ws/seqfetcher/DbRefFetcherTest.java | 9 +- test/jalview/ws/sifts/SiftsClientTest.java | 50 +- test/jalview/ws/utils/UrlDownloadClientTest.java | 85 ++ test/junit/extensions/PA.java | 331 +++++ test/junit/extensions/PrivilegedAccessor.java | 647 ++++++++++ 491 files changed, 50646 insertions(+), 20885 deletions(-) create mode 100644 examples/groovy/colourConserved.groovy create mode 100644 examples/groovy/colourSchemes.groovy create mode 100644 examples/groovy/colourUnconserved.groovy create mode 100644 examples/groovy/featuresCounter.groovy create mode 100644 examples/groovy/visibleFeaturesCounter.groovy create mode 100644 help/html/groovy/featuresCounter.html mode change 100755 => 100644 help/html/webServices/jnetprediction.gif create mode 100644 lib/jabaws-min-client-2.2.0.jar create mode 100644 resources/scoreModel/blosum62.scm create mode 100644 resources/scoreModel/blosum80.scm create mode 100644 resources/scoreModel/dna.scm create mode 100644 resources/scoreModel/pam250.scm delete mode 100755 src/MCview/PDBViewer.java create mode 100644 src/jalview/analysis/AverageDistanceTree.java create mode 100644 src/jalview/analysis/TreeBuilder.java create mode 100644 src/jalview/analysis/TreeModel.java create mode 100644 src/jalview/analysis/scoremodels/DistanceScoreModel.java create mode 100644 src/jalview/analysis/scoremodels/FeatureDistanceModel.java delete mode 100644 src/jalview/analysis/scoremodels/FeatureScoreModel.java create mode 100644 src/jalview/analysis/scoremodels/PIDModel.java delete mode 100644 src/jalview/analysis/scoremodels/PIDScoreModel.java create mode 100644 src/jalview/analysis/scoremodels/ScoreMatrix.java create mode 100644 src/jalview/analysis/scoremodels/ScoreModels.java create mode 100644 src/jalview/analysis/scoremodels/SimilarityParams.java create mode 100644 src/jalview/analysis/scoremodels/SimilarityScoreModel.java rename src/jalview/analysis/scoremodels/{SWScoreModel.java => SmithWatermanModel.java} (63%) create mode 100644 src/jalview/api/AlignmentColsCollectionI.java create mode 100644 src/jalview/api/AlignmentRowsCollectionI.java create mode 100644 src/jalview/api/analysis/PairwiseScoreModelI.java create mode 100644 src/jalview/api/analysis/SimilarityParamsI.java create mode 100644 src/jalview/appletgui/OverviewCanvas.java create mode 100644 src/jalview/datamodel/AllColsCollection.java create mode 100644 src/jalview/datamodel/AllColsIterator.java create mode 100644 src/jalview/datamodel/AllRowsCollection.java create mode 100644 src/jalview/datamodel/AllRowsIterator.java create mode 100644 src/jalview/datamodel/ContiguousI.java create mode 100644 src/jalview/datamodel/HiddenColumns.java create mode 100644 src/jalview/datamodel/Range.java create mode 100644 src/jalview/datamodel/SequenceCursor.java create mode 100644 src/jalview/datamodel/VisibleColsCollection.java create mode 100644 src/jalview/datamodel/VisibleColsIterator.java create mode 100644 src/jalview/datamodel/VisibleRowsCollection.java create mode 100644 src/jalview/datamodel/VisibleRowsIterator.java create mode 100644 src/jalview/datamodel/features/FeatureLocationI.java create mode 100644 src/jalview/datamodel/features/FeatureStore.java create mode 100644 src/jalview/datamodel/features/NCList.java create mode 100644 src/jalview/datamodel/features/NCNode.java create mode 100644 src/jalview/datamodel/features/RangeComparator.java create mode 100644 src/jalview/datamodel/features/SequenceFeatures.java create mode 100644 src/jalview/datamodel/features/SequenceFeaturesI.java create mode 100755 src/jalview/datamodel/xdb/uniprot/UniprotEntry.java create mode 100644 src/jalview/datamodel/xdb/uniprot/UniprotFeature.java create mode 100755 src/jalview/datamodel/xdb/uniprot/UniprotFile.java create mode 100755 src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java create mode 100755 src/jalview/datamodel/xdb/uniprot/UniprotSequence.java create mode 100644 src/jalview/ext/android/SparseDoubleArray.java create mode 100644 src/jalview/gui/CalculationChooser.java create mode 100644 src/jalview/gui/ColourMenuHelper.java create mode 100644 src/jalview/gui/ComboBoxTooltipRenderer.java create mode 100644 src/jalview/gui/OverviewCanvas.java create mode 100644 src/jalview/io/ScoreMatrixFile.java create mode 100644 src/jalview/io/cache/AppCache.java create mode 100644 src/jalview/io/cache/JvCacheableInputBox.java create mode 100644 src/jalview/math/MatrixI.java create mode 100644 src/jalview/math/SparseMatrix.java create mode 100644 src/jalview/renderer/OverviewRenderer.java create mode 100644 src/jalview/renderer/ResidueShader.java create mode 100644 src/jalview/renderer/ResidueShaderI.java create mode 100644 src/jalview/renderer/seqfeatures/FeatureColourFinder.java create mode 100644 src/jalview/schemes/ColourSchemeLoader.java create mode 100644 src/jalview/schemes/ColourSchemes.java create mode 100644 src/jalview/schemes/JalviewColourScheme.java delete mode 100644 src/jalview/schemes/ScoreMatrix.java create mode 100644 src/jalview/urls/CustomUrlProvider.java create mode 100644 src/jalview/urls/IdOrgSettings.java create mode 100644 src/jalview/urls/IdentifiersUrlProvider.java create mode 100644 src/jalview/urls/UrlLinkDisplay.java create mode 100644 src/jalview/urls/UrlLinkTableModel.java create mode 100644 src/jalview/urls/UrlProvider.java create mode 100644 src/jalview/urls/UrlProviderImpl.java create mode 100644 src/jalview/urls/api/UrlProviderFactoryI.java create mode 100644 src/jalview/urls/api/UrlProviderI.java create mode 100644 src/jalview/urls/applet/AppletUrlProviderFactory.java create mode 100644 src/jalview/urls/desktop/DesktopUrlProviderFactory.java create mode 100644 src/jalview/util/IntRangeComparator.java create mode 100644 src/jalview/util/SetUtils.java create mode 100644 src/jalview/viewmodel/OverviewDimensions.java create mode 100644 src/jalview/viewmodel/OverviewDimensionsHideHidden.java create mode 100644 src/jalview/viewmodel/OverviewDimensionsShowHidden.java create mode 100644 src/jalview/viewmodel/ViewportListenerI.java create mode 100644 src/jalview/viewmodel/ViewportProperties.java create mode 100644 src/jalview/viewmodel/ViewportRanges.java rename src/jalview/workers/{ColumnCounterWorker.java => ColumnCounterSetWorker.java} (60%) rename src/jalview/workers/{FeatureCounterI.java => FeatureSetCounterI.java} (76%) delete mode 100644 src/jalview/ws/jws2/JPred301Client.java create mode 100644 src/jalview/ws/utils/UrlDownloadClient.java create mode 100644 test/jalview/analysis/AlignmentSorterTest.java create mode 100644 test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java delete mode 100644 test/jalview/analysis/scoremodels/FeatureScoreModelTest.java create mode 100644 test/jalview/analysis/scoremodels/PIDModelTest.java create mode 100644 test/jalview/analysis/scoremodels/ScoreMatrixTest.java create mode 100644 test/jalview/analysis/scoremodels/ScoreModelsTest.java create mode 100644 test/jalview/datamodel/AllColsIteratorTest.java create mode 100644 test/jalview/datamodel/AllRowsIteratorTest.java create mode 100644 test/jalview/datamodel/HiddenColumnsTest.java create mode 100644 test/jalview/datamodel/SequenceGroupTest.java create mode 100644 test/jalview/datamodel/VisibleColsIteratorTest.java create mode 100644 test/jalview/datamodel/VisibleRowsIteratorTest.java create mode 100644 test/jalview/datamodel/features/FeatureStoreTest.java create mode 100644 test/jalview/datamodel/features/NCListTest.java create mode 100644 test/jalview/datamodel/features/NCNodeTest.java create mode 100644 test/jalview/datamodel/features/RangeComparatorTest.java create mode 100644 test/jalview/datamodel/features/SequenceFeaturesTest.java create mode 100644 test/jalview/ext/android/SparseDoubleArrayTest.java create mode 100644 test/jalview/gui/AlignmentPanelTest.java create mode 100644 test/jalview/gui/AnnotationRowFilterTest.java create mode 100644 test/jalview/gui/SeqPanelTest.java create mode 100644 test/jalview/io/FileLoaderTest.java create mode 100644 test/jalview/io/ScoreMatrixFileTest.java create mode 100644 test/jalview/io/cache/AppCacheTest.java create mode 100644 test/jalview/io/cache/JvCacheableInputBoxTest.java create mode 100644 test/jalview/math/SparseMatrixTest.java create mode 100644 test/jalview/renderer/ResidueShaderTest.java create mode 100644 test/jalview/renderer/ScaleRendererTest.java create mode 100644 test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java create mode 100644 test/jalview/renderer/seqfeatures/FeatureRendererTest.java create mode 100644 test/jalview/schemes/AnnotationColourGradientTest.java create mode 100644 test/jalview/schemes/Blosum62ColourSchemeTest.java create mode 100644 test/jalview/schemes/ClustalxColourSchemeTest.java create mode 100644 test/jalview/schemes/ColourSchemePropertyTest.java create mode 100644 test/jalview/schemes/ColourSchemesTest.java create mode 100644 test/jalview/schemes/JalviewColourSchemeTest.java create mode 100644 test/jalview/schemes/PIDColourSchemeTest.java delete mode 100644 test/jalview/schemes/ScoreMatrixPrinter.java create mode 100644 test/jalview/urls/AppletUrlProviderFactoryTest.java create mode 100644 test/jalview/urls/CustomUrlProviderTest.java create mode 100644 test/jalview/urls/DesktopUrlProviderFactoryTest.java create mode 100644 test/jalview/urls/IdentifiersUrlProviderTest.java create mode 100644 test/jalview/urls/UrlLinkDisplayTest.java create mode 100644 test/jalview/urls/UrlLinkTableModelTest.java create mode 100644 test/jalview/urls/UrlProviderTest.java create mode 100644 test/jalview/util/SetUtilsTest.java create mode 100644 test/jalview/viewmodel/OverviewDimensionsHideHiddenTest.java create mode 100644 test/jalview/viewmodel/OverviewDimensionsShowHiddenTest.java create mode 100644 test/jalview/viewmodel/ViewportRangesTest.java create mode 100644 test/jalview/ws/dbsources/RemoteFormatTest.java delete mode 100644 test/jalview/ws/jabaws/JpredJabaStructExportImport.java create mode 100644 test/jalview/ws/utils/UrlDownloadClientTest.java create mode 100644 test/junit/extensions/PA.java create mode 100644 test/junit/extensions/PrivilegedAccessor.java diff --git a/.classpath b/.classpath index 8aef745..c4a2832 100644 --- a/.classpath +++ b/.classpath @@ -39,7 +39,7 @@ - + diff --git a/examples/appletParameters.html b/examples/appletParameters.html index 277fd69..04cc235 100644 --- a/examples/appletParameters.html +++ b/examples/appletParameters.html @@ -115,7 +115,7 @@ the applet can be interacted with via its defaultColour One of:
Clustal, Blosum62, % Identity, Hydrophobic, Zappo, Taylor, Helix - Propensity, Strand Propensity, Turn Propensity, Buried Index, Nucleotide, T-Coffee Scores, RNA Helices + Propensity, Strand Propensity, Turn Propensity, Buried Index, Nucleotide, Purine/Pyrimidine, T-Coffee Scores, RNA Helices Default is no colour. @@ -159,6 +159,11 @@ the applet can be interacted with via its Default is true. + showOccupancy + true or false + Default is true. + + sortBy Id , Pairwise Identity, or Length Sorts the alignment on startup diff --git a/examples/example.json b/examples/example.json index 5f6e784..93c19db 100644 --- a/examples/example.json +++ b/examples/example.json @@ -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 diff --git a/examples/exampleFeatures.txt b/examples/exampleFeatures.txt index 2de9817..83dc4b1 100755 --- a/examples/exampleFeatures.txt +++ b/examples/exampleFeatures.txt @@ -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 index 0000000..4a15922 --- /dev/null +++ b/examples/groovy/colourConserved.groovy @@ -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 . + * 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 map -> conserved() }, + + /* + * method only needed if colour scheme has to recalculate + * values when an alignment is modified + */ + alignmentChanged: { AnnotatedCollectionI coll, Map 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 index 0000000..d5ca973 --- /dev/null +++ b/examples/groovy/colourSchemes.groovy @@ -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 map -> candy() }, + + /* + * method only needed if colour scheme has to recalculate + * values when an alignment is modified + */ + alignmentChanged: { AnnotatedCollectionI coll, Map 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 index 0000000..68730f3 --- /dev/null +++ b/examples/groovy/colourUnconserved.groovy @@ -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 . + * 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 map -> unconserved() }, + + /* + * method only needed if colour scheme has to recalculate + * values when an alignment is modified + */ + alignmentChanged: { AnnotatedCollectionI coll, Map 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 index 0000000..dc4c97c --- /dev/null +++ b/examples/groovy/featuresCounter.groovy @@ -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 . + * 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 index 0000000..b3180f8 --- /dev/null +++ b/examples/groovy/visibleFeaturesCounter.groovy @@ -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 . + * 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) diff --git a/examples/testdata/example_annot_file.jva b/examples/testdata/example_annot_file.jva index 794f42b..1779247 100644 --- a/examples/testdata/example_annot_file.jva +++ b/examples/testdata/example_annot_file.jva @@ -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 diff --git a/help/help.jhm b/help/help.jhm index f69ed00..54cce2a 100755 --- a/help/help.jhm +++ b/help/help.jhm @@ -22,7 +22,7 @@ - + @@ -75,6 +75,7 @@ + @@ -130,7 +131,8 @@ - + + @@ -150,9 +152,14 @@ + + + + + diff --git a/help/helpTOC.xml b/help/helpTOC.xml index 54abd53..a8aadf5 100755 --- a/help/helpTOC.xml +++ b/help/helpTOC.xml @@ -24,6 +24,11 @@ + + + + + @@ -131,6 +136,9 @@ + + + @@ -154,7 +162,7 @@ - + diff --git a/help/html/colourSchemes/clustal.html b/help/html/colourSchemes/clustal.html index 7b68c84..b3d44db 100755 --- a/help/html/colourSchemes/clustal.html +++ b/help/html/colourSchemes/clustal.html @@ -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.

-

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.

+

The table below gives these criteria as clauses: {>X%,xx,y}, + where X is the threshold percentage presence for any of the xx (or y) + residue types. +
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).

 

@@ -52,70 +53,81 @@ td { diff --git a/help/html/colourSchemes/user.html b/help/html/colourSchemes/user.html index 7239e4d..c2fde1c 100755 --- a/help/html/colourSchemes/user.html +++ b/help/html/colourSchemes/user.html @@ -27,7 +27,7 @@ User Defined Colours

- +

You may define any number of new colour schemes, each with a unique @@ -41,6 +41,9 @@ The Case Sensitive option allows you to choose distinct colours for upper and lower case residue codes.

+ The Colour All Lower Case option allows you to apply a selected colour + to all lower case residues. +

Click Apply or OK to set your new colours on the active alignment window.
Click Cancel to undo your changes if you pressed the Apply @@ -53,11 +56,5 @@
Any saved colour schemes will be automatically loaded the next time you use Jalview.
-
- 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). -

diff --git a/help/html/colourSchemes/userDefined_java7.gif b/help/html/colourSchemes/userDefined_java7.gif index f0ced1100a53893fa786daf5291e92f3a0495036..de80b848781b8cbe3d15274e8811895ee0b4a01d 100644 GIT binary patch delta 88446 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%0Ap4uo7n_&sG3co{6 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 diff --git a/help/html/features/chimera.html b/help/html/features/chimera.html index 5ae00af..68ac465 100644 --- a/help/html/features/chimera.html +++ b/help/html/features/chimera.html @@ -211,10 +211,41 @@ 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.
+ trace.

- -

  • Help
    +
  • EXPERIMENTAL FEATURES
    + + These are only available if the Tools→Enable + Experimental Features option is enabled. (Since Jalview 2.10.2) +
      +
    • Write Jalview features
      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).
      +
      If you use this option, please remember to select + the Refresh Menus option in Chimera's Render by + Attribute dialog box in order to see the attributes + derived from Jalview sequence features. +

      + View + this function's issue in Jalview's bug tracker
    • +
    • Fetch Chimera Attributes
      This + submenu lists available Chimera residue attributes that + can be imported as Jalview features on associated + sequences.
      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 Graduated feature colour + scheme. +
      View + this function's issue in Jalview's bug tracker
    • +
  • +
  • Help
    • Chimera Help
      diff --git a/help/html/features/groovy.html b/help/html/features/groovy.html index 254f92e..ead4436 100644 --- a/help/html/features/groovy.html +++ b/help/html/features/groovy.html @@ -108,9 +108,18 @@ print currentAlFrame.getTitle(); 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 featureCounter.groovy + href="../groovy/featuresCounter.html">featuresCounter.groovy example for more information.

      +

      + Creating custom colourschemes
      + You can create your own alignment colourschemes with a groovy script. We've provided two examples:
      +

      +

      diff --git a/help/html/features/overview.html b/help/html/features/overview.html index 9d36a1c..4f26592 100755 --- a/help/html/features/overview.html +++ b/help/html/features/overview.html @@ -31,6 +31,11 @@

      The red box indicates the currently viewed region of the alignment, this may be moved by clicking and dragging with the mouse.

      +

      Right-click (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).

      + The option to include/exclude hidden regions in the + overview was introduced in Jalview 2.10.2.

      diff --git a/help/html/features/preferences.html b/help/html/features/preferences.html index 6a8c86c..acd7ba6 100755 --- a/help/html/features/preferences.html +++ b/help/html/features/preferences.html @@ -46,8 +46,13 @@ and displaying structure information.
    • The "Connections" - Preferences tab allows you to change the links made from Jalview - to your default web browser. + Preferences tab allows you to configure Jalview's internet + settings and specify your default web browser. +
    • +
    • The "Links" + Preferences tab shows the currently configured URL + Links shown in the Link submenu in the Sequence + ID popup menu.
    • The "Output" Preferences tab contains settings affecting the export of @@ -84,10 +89,11 @@

      Show Annotations - 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 Conservation, Quality and Consensus - 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 Conservation, Quality, + Occupancy and Consensus for the alignment may + be shown or hidden by default using the checkboxes adjacent and + below.

      Show group: Conservation and Consensus controls the display @@ -212,13 +218,6 @@ Preferences tab

      - URL Link From Sequence ID
      These definitions are - used to generate URLs from a sequence's ID or database cross - references. Read more about configuring - URL links here. -

      -

      Default Browser (Unix)
      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 @@ -240,6 +239,37 @@ statement for more information.

      + The "Links" Preferences + tab +

      +

      + This panel shows a table, and two sections - Edit and Filter. + The table shows the available URL link definitions (consisting of a + database, Name, and URL template string), a checkbox In + Menu which indicates if the link is enabled, and Double + Click 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. +

      +

      Edit Links
      This section contains three buttons, + New, Edit and Delete, which allow you to + create, modify and remove user-defined URL links from the Sequence + ID's links submenu. +

      +

      + Filter
      The Filter text box allows you to + quickly show rows in the table containing a particular text string. + The Custom only button limits the entries in the table to + just those you have configured yourself via the Edit + Links buttons. Press Show all to clear any filters. +

      +

      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 identifiers.org website, and the names and URLs are not + user editable. + Read more about configuring + URL links. +

      +

      Output Preferences tab

      @@ -307,7 +337,7 @@ and PDB file association (if available). The Jalview id/start-end option is ignored if Modeller output is selected.

      - Editing Preferences tab + e"Editinge" Preferences tab

      There are currently three options available which can be selected / deselected.

      diff --git a/help/html/features/splitView.html b/help/html/features/splitView.html index 03b993c..3862c39 100644 --- a/help/html/features/splitView.html +++ b/help/html/features/splitView.html @@ -67,7 +67,11 @@ alignments, the "Format→Font" 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).

      + 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. (Added in 2.10.2)
    • "View→Protein" (in the cDNA panel) or "View→Nucleotide" (in the protein panel) diff --git a/help/html/groovy/featuresCounter.html b/help/html/groovy/featuresCounter.html new file mode 100644 index 0000000..3b6705b --- /dev/null +++ b/help/html/groovy/featuresCounter.html @@ -0,0 +1,123 @@ + + + +Extending Jalview with Groovy - Feature Counter Example + + +

      + Extending Jalview with Groovy - A customisable + feature counter

      The groovy script below shows how to + add a new calculation track to a Jalview alignment window. +

      +

      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.

      +

      To try it for yourself:

      +
        +
      1. Copy and paste it into the groovy script console
      2. +
      3. Load the example Feredoxin project (the one that opens by + default when you first launched Jalview)
      4. +
      5. Select Calculations→Execute Groovy + Script from the alignment window's menu bar to run the script on + the current view. +
      6. +
      + Please note: The 2.10.2 feature counting interface is not compatible with earlier versions.

      +
      http://www.jalview.org/examples/groovy/featuresCounter.groovy + - rendered with hilite.me +
      /*
      + * 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/help/html/io/tcoffeescores.html b/help/html/io/tcoffeescores.html index 08d1889..e67e3af 100644 --- a/help/html/io/tcoffeescores.html +++ b/help/html/io/tcoffeescores.html @@ -79,6 +79,20 @@ cons 999999999999999999999999999851000110321100001134 1QCF ----------6878742356789999999999889 cons 00100000006877641356789999999999889 - +
  • + + - - + + + - - + - + + + + + - + + - - - + + + + - - - + + - - - + + + + - - + - - - + + - - - + + + + - + - + + - - - + + + + - - - + + + + - - - + + + +
    CategoryColour Residue at positionApplied Colour{ Threshhold, Residue group }{ Threshold, Residue group }
    HydrophobicBLUE A,I,L,M,F,W,VBLUE{+60%, WLVIMAFCHP}{>60%, WLVIMAFCHP}
    R,KC{>60%, WLVIMAFCHP}
    Positive charge RED{+60%,KR},{+80%, K,R,Q}K,R{>60%,KR},{>80%, K,R,Q}
    NGREEN{+50%, N}, {+85%, N,Y}Negative chargeMAGENTAE{>60%,KR},{>50%,QE},{>85%,E,Q,D}
    CBLUE{+60%, WLVIMAFCHP}D{>60%,KR}, {>85%, K,R,Q}, {>50%,ED}
    CPINK{100%, C}PolarGREENN{>50%, N}, {>85%, N,Y}
    QGREEN{+60%,KR},{+50%,QE},{+85%,Q,E,K,R}{>60%,KR},{>50%,QE},{>85%,Q,E,K,R}
    EMAGENTA{+60%,KR},{+50%,QE},{+85%,E,Q,D}S,T{>60%, WLVIMAFCHP}, {>50%, TS}, {>85%,S,T}
    DMAGENTA{+60%,KR}, {+85%, K,R,Q}, {+50%,ED}CysteinesPINKC{>85%, C}
    GGlycines ORANGE{+0%, G}G{>0%, G}
    H,YCYAN{+60%, WLVIMAFCHP}, {+85%, - W,Y,A,C,P,Q,F,H,I,L,M,V}ProlinesYELLOWP{>0%, P}
    PYELLOW{+0%, P}AromaticCYANH,Y{>60%, WLVIMAFCHP}, {>85%, + W,Y,A,C,P,Q,F,H,I,L,M,V}
    S,TGREEN{+60%, WLVIMAFCHP}, {+50%, TS}, {+85%,S,T}UnconservedWHITEany / gapIf none of the above criteria are met
    + + + + + + + + + + + + + +
    Score0123456789
    RGB colour102, 102, 2550, 255, 0102, 255, 0204, 255, 0255, 255, 0255, 204, 0255, 153, 0255, 102, 0255, 51, 0255, 34, 0
    diff --git a/help/html/menus/desktopMenu.html b/help/html/menus/desktopMenu.html index 20e784b..a93ce4b 100755 --- a/help/html/menus/desktopMenu.html +++ b/help/html/menus/desktopMenu.html @@ -86,6 +86,7 @@ the Groovy Console for interactive scripting.
    +
  • Enable Experimental Features Enable or disable features still under development in Jalview's user interface. This setting is remembered in your preferences.
  • Vamsas For more details, read the diff --git a/help/html/menus/popupMenu.html b/help/html/menus/popupMenu.html index d42f854..7625606 100755 --- a/help/html/menus/popupMenu.html +++ b/help/html/menus/popupMenu.html @@ -189,8 +189,10 @@ href="../features/varna.html">VARNA.
  • Hide Insertions
    - Hides columns containing gaps in the current sequence or - selected region, and reveals columns not including gaps. + 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)
  • Hide Sequences
    Hides the currently selected sequences in this alignment view.
  • diff --git a/help/html/releases.html b/help/html/releases.html index 6f44b3d..5c8da91 100755 --- a/help/html/releases.html +++ b/help/html/releases.html @@ -21,6 +21,29 @@ --> Release History +

    @@ -47,6 +70,140 @@

    + 2.10.2
    + 30/5/2017
    +
    + +
    + General +
      +
    • More robust colours and shader model for alignments and groups
    • +
    • Custom shading schemes created via groovy scripts
    • +
    • linked scrolling of CDS/Protein views via Overview or sequence motif search operations
    • +
    • Efficiency improvements for interacting with alignment and overview windows
    • +
    • Hidden columns and sequences can be omitted in Overview
    • +
    • + Posterior probability annotation from + Stockholm files imported as sequence associated annotation +
    • +
    • + Sequence names don't include file + extension when importing structure files without embedded + names or PDB accessions +
    • +
    • Amend sequence features dialog box can be opened by double clicking gaps within sequence feature extent
    • +
    + Application +
      +
    • + + Experimental Features Checkbox in Desktop's Tools + menu to hide or show untested features in the application. +
    • +
    • Warning in alignment status bar when there are not enough columns to superimpose structures in Chimera
    • +
    • Faster Chimera/Jalview communication by file-based command exchange
    • +
    • URLs for viewing database cross-references provided by identifiers.org and the EMBL-EBI's MIRIAM DB
    • +
    • Updated JABAWS client to v2.2
    • +
    + Experimental features +
      +
    • + New entries in the Chimera menu + to transfer Chimera's structure attributes as Jalview + features, and vice-versa. +
    • +
    + Applet +
      +
    • +
    + Test Suite +
  • Added PrivilegedAccessor to test suite
  • +
  • Prevent or clear modal dialogs raised during tests
  • +
  • + +
  • + General +
      +
    • + Fixed incorrect value in BLOSUM 62 score + matrix - C->R should be '3'
      Old matrix restored with + this one-line groovy script:
      jalview.analysis.scoremodels.ScoreModels.instance.BLOSUM62.@matrix[4][1]=3 +
    • +
    • + Fixed Jalview's treatment of gaps in PCA + and substitution matrix based Tree calculations.
      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.
      Jalview now treats gaps in the same way as + SeqSpace (ie it scores them as 0). To restore pre-2.10.2 + behaviour
      + jalview.viewmodel.PCAModel.scoreGapAsAny=true // for + 2.10.1 mode
      + jalview.viewmodel.PCAModel.scoreGapAsAny=false // to + restore 2.10.2 mode +
    • +
    • Reopening Colour by annotation dialog doesn't reselect a specific sequence's associated annotation after it was used for colouring a view
    • +
    • Hidden regions in alignment views are not coloured in linked structure views
    • +
    • Current selection lost if popup menu opened on a region of alignment without groups
    • +
    • Popup menu not always shown for regions of an alignment with overlapping groups
    • +
    • Finder double counts if both a sequence's name and description match
    • +
    • Hiding column selection containing two hidden regions results in incorrect hidden regions
    • +
    • PCA calculation could hang when generating output report when working with highly redundant alignments
    • +
    • Cannot configure feature colours with lightGray or darkGray via features file
    • +
    • Overview window visible region moves erratically when hidden rows or columns are present
    • +
    • Per-residue colourschemes applied via the Structure Viewer's colour menu don't correspond to sequence colouring
    • +
    • Protein specific colours only offered in colour and group colour menu for protein alignments
    • +
    • 'Apply to all groups' setting when changing colour does not apply Conservation slider value to all groups
    • +
    • Colour threshold slider doesn't update to reflect currently selected view or group's shading thresholds
    • +
    • Percentage identity and conservation menu items do not show a tick or allow shading to be disabled
    • +
    • Conservation shading or PID threshold lost when base colourscheme changed if slider not visible
    • +
    • Sequence features shown in tooltip for gaps before start of features
    • +
    • Very large alignments take a long time to load
    • +
    • Cannot load Newick trees from eggnog ortholog database
    • +
    + Application +
      +
    • Easier creation of colours for all 'Lower case' residues (button in colourscheme editor debugged and new documentation and tooltips added)
    • +
    • Text colour threshold's 'Cancel' button doesn't restore group-specific text colour thresholds
    • +
    • Feature settings panel does not update as new features are added to alignment
    • +
    • Structure viewer's View -> Colour By view selection menu changes colours of alignment views
    • +
    • Proxy server address and port always appear enabled in Preferences->Connections
    • +
    • Spurious exceptions in console raised from alignment calculation workers after alignment has been closed
    • +
    • Typo in selection popup menu - Create groups now 'Create Group'
    • +
    • CMD/CTRL and G or Shift G for Create/Undefine group doesn't always work
    • +
    • Tree Viewer's Print Dialog doesn't get shown again after pressing 'Cancel'
    • +
    • DAS registry not found exceptions removed from console output
    • +
    • Above PID colour threshold not recovered when alignment view imported from project
    • +
    • No mappings generated between structure and sequences extracted from structure files imported via URL
    • +
    • + Structures loaded via URL are saved in + Jalview Projects rather than fetched via URL again when + the project is loaded and the structure viewed +
    • +
    • Trackpad horizontal scroll gesture adjusts start position in wrap mode
    • +
    • Status bar doesn't show positions for ambiguous amino acids
    • +
    • Hide insertions in PopUp menu excludes gaps in selection, current sequence and only within selected columns
    • +
    + Applet +
      +
    • Features not rendered as transparent on overview or linked structure view
    • +
    • Colour group by conservation doesn't work (since 2.8)
    • +
    • Hitting Cancel after applying user-defined colourscheme doesn't restore original colourscheme
    • +
    + New Known Issues +
      +
    • Protein/CDS view scrolling not always in phase after a sequence motif find operation
    • +
    • Importing annotation file with rows containing just upper and lower case letters are interpreted as WUSS rna secondary structure symbols
    • +
    + +
    + + +
    2.10.1
    29/11/2016
    @@ -188,6 +345,9 @@ Attempting to view structure for Hen lysozyme results in a PDB Client error dialog box +
  • + Structure View's mapping report switched ranges for PDB and sequence for SIFTS
  • + SIFTS 'Not_Observed' residues mapped to non-existant coordindate data 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

    sZjixFrvlC*q&i_6FKbV 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 `|; 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

    Y{6P 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 literal 0 HcmV?d00001 diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index f1e371d..f1ac099 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -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.

    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_ 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).
    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
    when a web service URL cannot be accessed by Jalview
    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 diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index 8cdcd52..7808480 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -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.

    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_ 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 =

    Anotación para {0}

    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.
    Por favor, introduzca la ruta de Chimera,
    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 index 0000000..b0e927d --- /dev/null +++ b/resources/scoreModel/blosum62.scm @@ -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 index 0000000..8153d3b --- /dev/null +++ b/resources/scoreModel/blosum80.scm @@ -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 index 0000000..0d7cbc1 --- /dev/null +++ b/resources/scoreModel/dna.scm @@ -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 index 0000000..898c723 --- /dev/null +++ b/resources/scoreModel/pam250.scm @@ -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 diff --git a/resources/uniprot_mapping.xml b/resources/uniprot_mapping.xml index 4a981ad..6344d1e 100755 --- a/resources/uniprot_mapping.xml +++ b/resources/uniprot_mapping.xml @@ -18,39 +18,39 @@ * The Jalview Authors are detailed in the 'AUTHORS' file. --> - + - + - + - - + + - + - + - + @@ -71,7 +71,7 @@ - + diff --git a/src/MCview/AppletPDBCanvas.java b/src/MCview/AppletPDBCanvas.java index aac796c..39111c3 100644 --- a/src/MCview/AppletPDBCanvas.java +++ b/src/MCview/AppletPDBCanvas.java @@ -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() }; } diff --git a/src/MCview/AppletPDBViewer.java b/src/MCview/AppletPDBViewer.java index 76ee4b0..133565c 100644 --- a/src/MCview/AppletPDBViewer.java +++ b/src/MCview/AppletPDBViewer.java @@ -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); diff --git a/src/MCview/PDBCanvas.java b/src/MCview/PDBCanvas.java index 292de91..83642cc 100644 --- a/src/MCview/PDBCanvas.java +++ b/src/MCview/PDBCanvas.java @@ -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() }; } diff --git a/src/MCview/PDBChain.java b/src/MCview/PDBChain.java index 3c0a1f2..8285d88 100755 --- a/src/MCview/PDBChain.java +++ b/src/MCview/PDBChain.java @@ -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 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 index f108fc2..0000000 --- a/src/MCview/PDBViewer.java +++ /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 . - * 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("

    ") > -1 || data.indexOf("
    ") > -1)) - { - out.println(data); - } - } - out.close(); - in.close(); - } catch (Exception ex) - { - ex.printStackTrace(); - } - } - } -} diff --git a/src/jalview/analysis/AAFrequency.java b/src/jalview/analysis/AAFrequency.java index ffa413b..b806355 100755 --- a/src/jalview/analysis/AAFrequency.java +++ b/src/jalview/analysis/AAFrequency.java @@ -20,10 +20,6 @@ */ 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)); } } diff --git a/src/jalview/analysis/AlignSeq.java b/src/jalview/analysis/AlignSeq.java index 86bf721..07f43da 100755 --- a/src/jalview/analysis/AlignSeq.java +++ b/src/jalview/analysis/AlignSeq.java @@ -20,13 +20,15 @@ */ 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]) diff --git a/src/jalview/analysis/AlignmentSorter.java b/src/jalview/analysis/AlignmentSorter.java index 59cdccf..9943a22 100755 --- a/src/jalview/analysis/AlignmentSorter.java +++ b/src/jalview/analysis/AlignmentSorter.java @@ -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 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 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 labs) { @@ -748,51 +702,41 @@ public class AlignmentSorter return false; } - public static void sortByFeature(List featureLabels, - List groupLabels, int start, int stop, + /** + * Sort sequences by feature score or density, optionally restricted by + * feature types, feature groups, or alignment start/end positions. + *

    + * 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 featureTypes, + List 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 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 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 featureTypes, List 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; } } diff --git a/src/jalview/analysis/AlignmentUtils.java b/src/jalview/analysis/AlignmentUtils.java index fbec4be..69ac947 100644 --- a/src/jalview/analysis/AlignmentUtils.java +++ b/src/jalview/analysis/AlignmentUtils.java @@ -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 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 findCdsPositions(SequenceI dnaSeq) { List result = new ArrayList(); - SequenceFeature[] sfs = dnaSeq.getSequenceFeatures(); - if (sfs == null) + + List 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() - { - @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[]> variants = new LinkedHashMap[]>(); - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); - SequenceFeature[] dnaFeatures = dnaSeq.getSequenceFeatures(); - if (dnaFeatures == null) + List 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[] codonVariants = variants.get(peptidePosition); - if (codonVariants == null) - { - codonVariants = new ArrayList[CODON_LENGTH]; - codonVariants[0] = new ArrayList(); - codonVariants[1] = new ArrayList(); - codonVariants[2] = new ArrayList(); - variants.put(peptidePosition, codonVariants); - } + // feature doesn't lie within coding region + continue; + } + int peptidePosition = mapsTo[0]; + List[] codonVariants = variants.get(peptidePosition); + if (codonVariants == null) + { + codonVariants = new ArrayList[CODON_LENGTH]; + codonVariants[0] = new ArrayList(); + codonVariants[1] = new ArrayList(); + codonVariants[2] = new ArrayList(); + variants.put(peptidePosition, codonVariants); + } - /* - * 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 codonVariant = codonVariants[codonPos]; + if (codon[codonPos] == dnaCol) { - String nucleotide = String.valueOf( - dnaSeq.getCharAt(codon[codonPos] - dnaStart)) - .toUpperCase(); - List 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> buildMappedColumnsMap( + static SortedMap> buildMappedColumnsMap( AlignmentI unaligned, AlignmentI aligned, List unmapped) { /* @@ -2842,7 +2813,7 @@ public class AlignmentUtils * {unalignedSequence, characterPerSequence} at that position. * TreeMap keeps the entries in ascending column order. */ - Map> map = new TreeMap>(); + SortedMap> map = new TreeMap>(); /* * 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 index 0000000..907109e --- /dev/null +++ b/src/jalview/analysis/AverageDistanceTree.java @@ -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); + } + +} diff --git a/src/jalview/analysis/Conservation.java b/src/jalview/analysis/Conservation.java index 7b9da46..2b5a8f6 100755 --- a/src/jalview/analysis/Conservation.java +++ b/src/jalview/analysis/Conservation.java @@ -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 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 seqNums; int maxLength = 0; // used by quality calcs @@ -69,17 +77,17 @@ public class Conservation */ Map[] 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 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 resultHash = new TreeMap(); + SortedMap resultHash = new TreeMap(); 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(); - // 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 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++) diff --git a/src/jalview/analysis/CrossRef.java b/src/jalview/analysis/CrossRef.java index 4ba7e41..103025c 100644 --- a/src/jalview/analysis/CrossRef.java +++ b/src/jalview/analysis/CrossRef.java @@ -619,28 +619,25 @@ public class CrossRef * duplication (e.g. same variation from two * transcripts) */ - SequenceFeature[] sfs = ms.getSequenceFeatures(); - if (sfs != null) + List 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) diff --git a/src/jalview/analysis/Dna.java b/src/jalview/analysis/Dna.java index 799a8ed..2106dc2 100644 --- a/src/jalview/analysis/Dna.java +++ b/src/jalview/analysis/Dna.java @@ -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 featureTypes, - Map 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)) - { - } } } - } } } diff --git a/src/jalview/analysis/NJTree.java b/src/jalview/analysis/NJTree.java index e0e50fb..487e85e 100644 --- a/src/jalview/analysis/NJTree.java +++ b/src/jalview/analysis/NJTree.java @@ -21,1312 +21,116 @@ 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; - - 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 groups = new Vector(); - - SequenceNode maxdist; - - SequenceNode top; - - float maxDistValue; - - float maxheight; - - int ycount; - - Vector 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 leaves = findLeaves(top); - - int i = 0; - int namesleft = seqs.length; - - SequenceNode j; - SequenceI nam; - String realnam; - Vector one2many = new Vector(); - 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(); - 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 list) + protected double findMinDistance() { - Vector 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(); - - 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 findLeaves(SequenceNode nd) - { - Vector leaves = new Vector(); - 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 findLeaves(SequenceNode nd, - Vector 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 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 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; } } diff --git a/src/jalview/analysis/PCA.java b/src/jalview/analysis/PCA.java index 06a139b..3ec7995 100755 --- a/src/jalview/analysis/PCA.java +++ b/src/jalview/analysis/PCA.java @@ -20,144 +20,40 @@ */ 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 getSimpleBPs(CharSequence line) + protected static List getSimpleBPs(CharSequence line) throws WUSSParseException { Hashtable> stacks = new Hashtable>(); - Vector pairs = new Vector(); + List pairs = new ArrayList(); int i = 0; while (i < line.length()) { @@ -195,25 +196,9 @@ public class Rna return pairs; } - public static SequenceFeature[] getBasePairs(List 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 getModeleBP(CharSequence line) - throws WUSSParseException - { - Vector bps = getSimpleBPs(line); - return new ArrayList(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 helices = new Hashtable(); - // 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 result = new ArrayList(); + + 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 helices = new HashMap(); + // Keep track of helix number for each position + + // Go through each base pair and assign positions a helix + List 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()]); + } } diff --git a/src/jalview/analysis/SeqsetUtils.java b/src/jalview/analysis/SeqsetUtils.java index 21ad1cc..27b3041 100755 --- a/src/jalview/analysis/SeqsetUtils.java +++ b/src/jalview/analysis/SeqsetUtils.java @@ -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 sfeat = new Vector(); + List 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 sfeatures = (Vector) sqinfo + .get("SeqFeatures"); Vector pdbid = (Vector) 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 index 0000000..effef9a --- /dev/null +++ b/src/jalview/analysis/TreeBuilder.java @@ -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 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 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 + *

    + * If the score model computes pairwise distance scores, then these are used + * directly to derive the tree + *

    + * 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(); + 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(); + + 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 index 0000000..5a41802 --- /dev/null +++ b/src/jalview/analysis/TreeModel.java @@ -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 . + * 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 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 leaves = findLeaves(top); + + int i = 0; + int namesleft = seqs.length; + + SequenceNode j; + SequenceI nam; + String realnam; + Vector one2many = new Vector(); + // 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 list) + { + Vector 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 findLeaves(SequenceNode nd) + { + Vector leaves = new Vector(); + 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 findLeaves(SequenceNode nd, + Vector 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 groupNodes(float threshold) + { + List groups = new ArrayList(); + _groupNodes(groups, getTopNode(), threshold); + return groups; + } + + protected void _groupNodes(List 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 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 index 0000000..0dd7617 --- /dev/null +++ b/src/jalview/analysis/scoremodels/DistanceScoreModel.java @@ -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 index 0000000..ddbaf73 --- /dev/null +++ b/src/jalview/analysis/scoremodels/FeatureDistanceModel.java @@ -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 . + * 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. + *

    + * 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 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> 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 set1 = sfap.get(sc1); + Set 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> findFeatureTypesAtColumn( + SeqCigar[] seqs, int columnPosition) + { + Map> sfap = new HashMap>(); + for (SeqCigar seq : seqs) + { + int spos = seq.findPosition(columnPosition); + if (spos != -1) + { + /* + * position is not a gap + */ + Set types = new HashSet(); + List 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 index 7c81912..0000000 --- a/src/jalview/analysis/scoremodels/FeatureScoreModel.java +++ /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 . - * 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 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> sfap = new ArrayList>(); - for (int i = 0; i < noseqs; i++) - { - Hashtable types = new Hashtable(); - int spos = seqs[i].findPosition(cpos); - if (spos != -1) - { - List 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 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 index 0000000..721ba45 --- /dev/null +++ b/src/jalview/analysis/scoremodels/PIDModel.java @@ -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 index 0f7a67a..0000000 --- a/src/jalview/analysis/scoremodels/PIDScoreModel.java +++ /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 . - * 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 index 0000000..efaeb43 --- /dev/null +++ b/src/jalview/analysis/scoremodels/ScoreMatrix.java @@ -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 . + * 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. + *

    + * Unmapped characters (not in the alphabet) get an index of -1. + *

    + * 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). + *

    + * 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 must also call + * getMatrixIndex 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 getMatrix 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. + *

    + * 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(""); + sb.append(html ? "" : ""); + } + else + { + sb.append("ScoreMatrix ").append(getName()).append("\n"); + } + for (char sym : symbols) + { + if (html) + { + sb.append(""); + } + else + { + sb.append("\t").append(sym); + } + } + sb.append(html ? "\n" : "\n"); + + /* + * table of scores + */ + for (char c1 : symbols) + { + if (html) + { + sb.append("" : ""); + for (char c2 : symbols) + { + sb.append(html ? "" : ""); + } + sb.append(html ? "\n" : "\n"); + } + if (html) + { + sb.append("
     ").append(sym).append(" 
    "); + } + sb.append(c1).append(html ? "" : "\t") + .append(matrix[symbolIndex[c1]][symbolIndex[c2]]) + .append(html ? "
    "); + } + 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 + *

      + *
    • Sequences:
    • + *
    • FKL
    • + *
    • R-D
    • + *
    • QIA
    • + *
    • GWC
    • + *
    • Score matrix is BLOSUM62
    • + *
    • Gaps treated same as X (unknown)
    • + *
    • product [0, 0] = F.F + K.K + L.L = 6 + 5 + 4 = 15
    • + *
    • product [1, 1] = R.R + -.- + D.D = 5 + -1 + 6 = 10
    • + *
    • product [2, 2] = Q.Q + I.I + A.A = 5 + 4 + 4 = 13
    • + *
    • product [3, 3] = G.G + W.W + C.C = 6 + 11 + 9 = 26
    • + *
    • product[0, 1] = F.R + K.- + L.D = -3 + -1 + -3 = -8 + *
    • and so on
    • + *
    + * 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 index 0000000..654136a --- /dev/null +++ b/src/jalview/analysis/scoremodels/ScoreModels.java @@ -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 models; + + public static ScoreModels getInstance() + { + return instance; + } + + /** + * Private constructor to enforce use of singleton. Registers Jalview's + * "built-in" score models: + *
      + *
    • BLOSUM62
    • + *
    • PAM250
    • + *
    • PID
    • + *
    • DNA
    • + *
    • Sequence Feature Similarity
    • + *
    + */ + private ScoreModels() + { + /* + * using LinkedHashMap keeps models ordered as added + */ + models = new LinkedHashMap(); + 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 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 index 0000000..e5751ca --- /dev/null +++ b/src/jalview/analysis/scoremodels/SimilarityParams.java @@ -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 + * + *
    + * Quantification of the variation in percentage identity for protein sequence alignments
    + * Raghava, GP and Barton, GJ
    + * BMC Bioinformatics. 2006 Sep 19;7:415
    + * 
    + * + * @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 + *
      + *
    • ignores gap-gap
    • + *
    • does not score gap-residue
    • + *
    • includes gap-residue in lengths
    • + *
    • matches on longer of two sequences
    • + *
    + */ + public static final SimilarityParamsI PID1 = new SimilarityParams(false, + false, true, false); + + /** + * as described in the Raghava-Barton paper + *
      + *
    • ignores gap-gap
    • + *
    • ignores gap-residue
    • + *
    • matches on longer of two sequences
    • + *
    + */ + public static final SimilarityParamsI PID2 = new SimilarityParams(false, + false, false, false); + + /** + * as described in the Raghava-Barton paper + *
      + *
    • ignores gap-gap
    • + *
    • ignores gap-residue
    • + *
    • matches on shorter of sequences only
    • + *
    + */ + public static final SimilarityParamsI PID3 = new SimilarityParams(false, + false, false, true); + + /** + * as described in the Raghava-Barton paper + *
      + *
    • ignores gap-gap
    • + *
    • does not score gap-residue
    • + *
    • includes gap-residue in lengths
    • + *
    • matches on shorter of sequences only
    • + *
    + */ + 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 index 0000000..dae1f62 --- /dev/null +++ b/src/jalview/analysis/scoremodels/SimilarityScoreModel.java @@ -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; + } + +} diff --git a/src/jalview/analysis/scoremodels/SWScoreModel.java b/src/jalview/analysis/scoremodels/SmithWatermanModel.java similarity index 63% rename from src/jalview/analysis/scoremodels/SWScoreModel.java rename to src/jalview/analysis/scoremodels/SmithWatermanModel.java index d5d998e..f88101f 100644 --- a/src/jalview/analysis/scoremodels/SWScoreModel.java +++ b/src/jalview/analysis/scoremodels/SmithWatermanModel.java @@ -21,23 +21,42 @@ 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; } } diff --git a/src/jalview/api/AlignViewportI.java b/src/jalview/api/AlignViewportI.java index 7067328..9e6d1c0 100644 --- a/src/jalview/api/AlignViewportI.java +++ b/src/jalview/api/AlignViewportI.java @@ -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 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 index 0000000..603da98 --- /dev/null +++ b/src/jalview/api/AlignmentColsCollectionI.java @@ -0,0 +1,13 @@ +package jalview.api; + +public interface AlignmentColsCollectionI extends Iterable +{ + /** + * 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 index 0000000..09b039d --- /dev/null +++ b/src/jalview/api/AlignmentRowsCollectionI.java @@ -0,0 +1,24 @@ +package jalview.api; + +import jalview.datamodel.SequenceI; + +public interface AlignmentRowsCollectionI extends Iterable +{ + /** + * 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); +} diff --git a/src/jalview/api/ComplexAlignFile.java b/src/jalview/api/ComplexAlignFile.java index 2bf2782..1b579ad 100644 --- a/src/jalview/api/ComplexAlignFile.java +++ b/src/jalview/api/ComplexAlignFile.java @@ -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 diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index f54231e..9d2d7f4 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -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. + *

    + * 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. + *

    + * 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. + *

    + * Returns null if there is no visible feature at the position. + *

    + * 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 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 findFeaturesAtRes(SequenceI sequence, int res); + List findFeaturesAtResidue(SequenceI sequence, int resNo); /** * get current displayed types, in ordering of rendering (on top last) @@ -150,9 +179,9 @@ public interface FeatureRenderer List 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 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(); + } diff --git a/src/jalview/api/FeaturesDisplayedI.java b/src/jalview/api/FeaturesDisplayedI.java index 32b0565..e69785f 100644 --- a/src/jalview/api/FeaturesDisplayedI.java +++ b/src/jalview/api/FeaturesDisplayedI.java @@ -21,12 +21,15 @@ package jalview.api; import java.util.Collection; -import java.util.Iterator; +import java.util.Set; public interface FeaturesDisplayedI { - Iterator getVisibleFeatures(); + /** + * answers an unmodifiable view of the set of visible feature types + */ + Set 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 featureTypes); boolean isRegistered(String type); diff --git a/src/jalview/api/SequenceRenderer.java b/src/jalview/api/SequenceRenderer.java index d708902..54f7fb6 100644 --- a/src/jalview/api/SequenceRenderer.java +++ b/src/jalview/api/SequenceRenderer.java @@ -21,14 +21,14 @@ 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); } diff --git a/src/jalview/api/SiftsClientI.java b/src/jalview/api/SiftsClientI.java index c795f3f..367a0de 100644 --- a/src/jalview/api/SiftsClientI.java +++ b/src/jalview/api/SiftsClientI.java @@ -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; /** diff --git a/src/jalview/api/ViewStyleI.java b/src/jalview/api/ViewStyleI.java index db82dcf..2b554ea 100644 --- a/src/jalview/api/ViewStyleI.java +++ b/src/jalview/api/ViewStyleI.java @@ -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 index 0000000..ecada36 --- /dev/null +++ b/src/jalview/api/analysis/PairwiseScoreModelI.java @@ -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 + +} diff --git a/src/jalview/api/analysis/ScoreModelI.java b/src/jalview/api/analysis/ScoreModelI.java index 31a1c32..7f138cd 100644 --- a/src/jalview/api/analysis/ScoreModelI.java +++ b/src/jalview/api/analysis/ScoreModelI.java @@ -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 . - * 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 index 0000000..581449f --- /dev/null +++ b/src/jalview/api/analysis/SimilarityParamsI.java @@ -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(); +} diff --git a/src/jalview/appletgui/APopupMenu.java b/src/jalview/appletgui/APopupMenu.java index 8f7a15b..d5b7132 100644 --- a/src/jalview/appletgui/APopupMenu.java +++ b/src/jalview/appletgui/APopupMenu.java @@ -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 links) + List 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 tipEntries = new TreeMap(); + SortedMap tipEntries = new TreeMap(); final Map> candidates = new LinkedHashMap>(); 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 seqs = new ArrayList(); + List features = new ArrayList(); 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 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(); } diff --git a/src/jalview/appletgui/AlignFrame.java b/src/jalview/appletgui/AlignFrame.java index 86dc19b..24f882e 100644 --- a/src/jalview/appletgui/AlignFrame.java +++ b/src/jalview/appletgui/AlignFrame.java @@ -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 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) diff --git a/src/jalview/appletgui/AlignViewport.java b/src/jalview/appletgui/AlignViewport.java index 4bd77b6..73cd9e9 100644 --- a/src/jalview/appletgui/AlignViewport.java +++ b/src/jalview/appletgui/AlignViewport.java @@ -20,30 +20,32 @@ */ 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); } } diff --git a/src/jalview/appletgui/AlignmentPanel.java b/src/jalview/appletgui/AlignmentPanel.java index e97c347..e402b9b 100644 --- a/src/jalview/appletgui/AlignmentPanel.java +++ b/src/jalview/appletgui/AlignmentPanel.java @@ -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); + } + } } diff --git a/src/jalview/appletgui/AnnotationColourChooser.java b/src/jalview/appletgui/AnnotationColourChooser.java index 79d2f1f..f516bc9 100644 --- a/src/jalview/appletgui/AnnotationColourChooser.java +++ b/src/jalview/appletgui/AnnotationColourChooser.java @@ -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 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 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(); 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 list = new Vector(); - 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 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 getAnnotationItems() + { + // TODO remove duplication with gui.AnnotationRowFilter + // TODO add 'per sequence only' option / parameter + + annotationLabels = new HashMap(); + Vector list = new Vector(); + 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 diff --git a/src/jalview/appletgui/AnnotationColumnChooser.java b/src/jalview/appletgui/AnnotationColumnChooser.java index a8dff62..60775d3 100644 --- a/src/jalview/appletgui/AnnotationColumnChooser.java +++ b/src/jalview/appletgui/AnnotationColumnChooser.java @@ -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 list = new Vector(); 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 itr = oldSelection.getHiddenColumns() + for (Iterator 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); } diff --git a/src/jalview/appletgui/AnnotationLabels.java b/src/jalview/appletgui/AnnotationLabels.java index b28ccc7..307301d 100755 --- a/src/jalview/appletgui/AnnotationLabels.java +++ b/src/jalview/appletgui/AnnotationLabels.java @@ -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] }); diff --git a/src/jalview/appletgui/AnnotationPanel.java b/src/jalview/appletgui/AnnotationPanel.java index 6012c1a..c658811 100755 --- a/src/jalview/appletgui/AnnotationPanel.java +++ b/src/jalview/appletgui/AnnotationPanel.java @@ -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()); + } + } } diff --git a/src/jalview/appletgui/AnnotationRowFilter.java b/src/jalview/appletgui/AnnotationRowFilter.java index 8d67d71..315ce3b 100644 --- a/src/jalview/appletgui/AnnotationRowFilter.java +++ b/src/jalview/appletgui/AnnotationRowFilter.java @@ -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 diff --git a/src/jalview/appletgui/AppletJmol.java b/src/jalview/appletgui/AppletJmol.java index 9d4779c..c0b4ff0 100644 --- a/src/jalview/appletgui/AppletJmol.java +++ b/src/jalview/appletgui/AppletJmol.java @@ -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; diff --git a/src/jalview/appletgui/AppletJmolBinding.java b/src/jalview/appletgui/AppletJmolBinding.java index f938cad..9b8a235 100644 --- a/src/jalview/appletgui/AppletJmolBinding.java +++ b/src/jalview/appletgui/AppletJmolBinding.java @@ -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 diff --git a/src/jalview/appletgui/CutAndPasteTransfer.java b/src/jalview/appletgui/CutAndPasteTransfer.java index c658734..1e806a5 100644 --- a/src/jalview/appletgui/CutAndPasteTransfer.java +++ b/src/jalview/appletgui/CutAndPasteTransfer.java @@ -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) diff --git a/src/jalview/appletgui/ExtJmol.java b/src/jalview/appletgui/ExtJmol.java index 189fe88..b369318 100644 --- a/src/jalview/appletgui/ExtJmol.java +++ b/src/jalview/appletgui/ExtJmol.java @@ -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 { diff --git a/src/jalview/appletgui/FeatureRenderer.java b/src/jalview/appletgui/FeatureRenderer.java index 2fca07d..435e78d 100644 --- a/src/jalview/appletgui/FeatureRenderer.java +++ b/src/jalview/appletgui/FeatureRenderer.java @@ -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 sequences, + final List 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); + } } diff --git a/src/jalview/appletgui/FeatureSettings.java b/src/jalview/appletgui/FeatureSettings.java index 2c454a4..b0bb372 100755 --- a/src/jalview/appletgui/FeatureSettings.java +++ b/src/jalview/appletgui/FeatureSettings.java @@ -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 visibleChecks = new Vector(); + List displayableTypes = new ArrayList(); + Set foundGroups = new HashSet(); + AlignmentI alignment = av.getAlignment(); + for (int i = 0; i < alignment.getHeight(); i++) { - if (alignment.getSequenceAt(i).getSequenceFeatures() == null) - { - continue; - } + SequenceI seq = alignment.getSequenceAt(i); - tmpfeatures = alignment.getSequenceAt(i).getSequenceFeatures(); - int index = 0; - while (index < tmpfeatures.length) + /* + * get the sequence's groups for positional features + * and keep track of which groups are visible + */ + Set groups = seq.getFeatures().getFeatureGroups(true); + Set visibleGroups = new HashSet(); + 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 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 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 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 diff --git a/src/jalview/appletgui/Finder.java b/src/jalview/appletgui/Finder.java index d2fe69c..41e2a64 100644 --- a/src/jalview/appletgui/Finder.java +++ b/src/jalview/appletgui/Finder.java @@ -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 seqs = new ArrayList(); + List features = new ArrayList(); + 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, diff --git a/src/jalview/appletgui/FontChooser.java b/src/jalview/appletgui/FontChooser.java index 59f6957..991fb96 100644 --- a/src/jalview/appletgui/FontChooser.java +++ b/src/jalview/appletgui/FontChooser.java @@ -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(); } } diff --git a/src/jalview/appletgui/IdCanvas.java b/src/jalview/appletgui/IdCanvas.java index d72e91f..74bbcf5 100755 --- a/src/jalview/appletgui/IdCanvas.java +++ b/src/jalview/appletgui/IdCanvas.java @@ -21,15 +21,18 @@ 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()); + } + } } diff --git a/src/jalview/appletgui/IdPanel.java b/src/jalview/appletgui/IdPanel.java index 182f20e..a5c2e5a 100755 --- a/src/jalview/appletgui/IdPanel.java +++ b/src/jalview/appletgui/IdPanel.java @@ -20,14 +20,12 @@ */ 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 urlList = new HashMap(); + if (viewport.applet != null) { for (int i = 1; i < 10; i++) { - label = av.applet.getParameter("linkLabel_" + i); - url = av.applet.getParameter("linkURL_" + i); + label = viewport.applet.getParameter("linkLabel_" + i); + url = viewport.applet.getParameter("linkURL_" + i); - 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 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(); + } + + 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 index 0000000..23e82df --- /dev/null +++ b/src/jalview/appletgui/OverviewCanvas.java @@ -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 . + * 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); + } + } + +} diff --git a/src/jalview/appletgui/OverviewPanel.java b/src/jalview/appletgui/OverviewPanel.java index 9b2be4c..b3c4a37 100755 --- a/src/jalview/appletgui/OverviewPanel.java +++ b/src/jalview/appletgui/OverviewPanel.java @@ -20,104 +20,68 @@ */ 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(); } - } diff --git a/src/jalview/appletgui/PCAPanel.java b/src/jalview/appletgui/PCAPanel.java index c5ec0c1..5dc57f9 100644 --- a/src/jalview/appletgui/PCAPanel.java +++ b/src/jalview/appletgui/PCAPanel.java @@ -20,9 +20,12 @@ */ 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]); } } diff --git a/src/jalview/appletgui/RedundancyPanel.java b/src/jalview/appletgui/RedundancyPanel.java index 4aea837..6be416c 100644 --- a/src/jalview/appletgui/RedundancyPanel.java +++ b/src/jalview/appletgui/RedundancyPanel.java @@ -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) { } diff --git a/src/jalview/appletgui/ScalePanel.java b/src/jalview/appletgui/ScalePanel.java index 5a156fa..ec3e246 100755 --- a/src/jalview/appletgui/ScalePanel.java +++ b/src/jalview/appletgui/ScalePanel.java @@ -21,10 +21,12 @@ 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(); + } + } diff --git a/src/jalview/appletgui/SeqCanvas.java b/src/jalview/appletgui/SeqCanvas.java index 5d6bb07..46a908e 100755 --- a/src/jalview/appletgui/SeqCanvas.java +++ b/src/jalview/appletgui/SeqCanvas.java @@ -21,20 +21,24 @@ 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()); + } + } + + } + } diff --git a/src/jalview/appletgui/SeqPanel.java b/src/jalview/appletgui/SeqPanel.java index 4278744..9e31efe 100644 --- a/src/jalview/appletgui/SeqPanel.java +++ b/src/jalview/appletgui/SeqPanel.java @@ -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 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 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 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 allFeatures = findFeaturesAtColumn(sequence, + sequence.findPosition(column + 1)); Vector 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(); - } - for (int j = 0; j < allFeatures[i].links.size(); j++) - { - links.addElement(allFeatures[i].links.elementAt(j)); - } + links = new Vector(); } + 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(); diff --git a/src/jalview/appletgui/SequenceRenderer.java b/src/jalview/appletgui/SequenceRenderer.java index 970d20e..38031e4 100755 --- a/src/jalview/appletgui/SequenceRenderer.java +++ b/src/jalview/appletgui/SequenceRenderer.java @@ -20,10 +20,10 @@ */ 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()); diff --git a/src/jalview/appletgui/SliderPanel.java b/src/jalview/appletgui/SliderPanel.java index 35c2a22..9154aa0 100644 --- a/src/jalview/appletgui/SliderPanel.java +++ b/src/jalview/appletgui/SliderPanel.java @@ -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 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) { } diff --git a/src/jalview/appletgui/TreeCanvas.java b/src/jalview/appletgui/TreeCanvas.java index 8292a5a..272a2b3 100755 --- a/src/jalview/appletgui/TreeCanvas.java +++ b/src/jalview/appletgui/TreeCanvas.java @@ -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 leaves = tree.findLeaves(tree.getTopNode()); + Vector 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 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 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 l = tree.findLeaves(tree.getGroups() - .elementAt(i)); + Vector l = tree.findLeaves(groups.get(i)); Vector sequences = new Vector(); 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); diff --git a/src/jalview/appletgui/TreePanel.java b/src/jalview/appletgui/TreePanel.java index b4b8ec2..c7bf6aa 100644 --- a/src/jalview/appletgui/TreePanel.java +++ b/src/jalview/appletgui/TreePanel.java @@ -20,15 +20,17 @@ */ 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) diff --git a/src/jalview/appletgui/UserDefinedColours.java b/src/jalview/appletgui/UserDefinedColours.java index 88098a9..845110e 100644 --- a/src/jalview/appletgui/UserDefinedColours.java +++ b/src/jalview/appletgui/UserDefinedColours.java @@ -20,10 +20,14 @@ */ 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); } diff --git a/src/jalview/bin/Cache.java b/src/jalview/bin/Cache.java index 8412dab..da3cb92 100755 --- a/src/jalview/bin/Cache.java +++ b/src/jalview/bin/Cache.java @@ -21,7 +21,13 @@ 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; *

  • SORT_ALIGNMENT (No sort|Id|Pairwise Identity)
  • *
  • SEQUENCE_LINKS list of name|URL pairs for opening a url with * $SEQUENCE_ID$
  • + *
  • STORED_LINKS list of name|url pairs which user has entered but are not + * currently used + *
  • DEFAULT_LINK name of single url to be used when user double clicks a + * sequence id (must be in SEQUENCE_LINKS or STORED_LINKS) *
  • GROUP_LINKS list of name|URL[|<separator>] tuples - see * jalview.utils.GroupURLLink for more info
  • *
  • DAS_REGISTRY_URL the registry to query
  • @@ -179,6 +190,8 @@ import org.apache.log4j.SimpleLayout; *
  • STRUCTURE_DISPLAY choose from JMOL (default) or CHIMERA for 3D structure * display
  • *
  • CHIMERA_PATH specify full path to Chimera program (if non-standard)
  • + *
  • ID_ORG_HOSTURL location of jalview service providing identifiers.org urls + *
  • * * * 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); + } + } + } } diff --git a/src/jalview/bin/Jalview.java b/src/jalview/bin/Jalview.java index 39c0a5b..954bb34 100755 --- a/src/jalview/bin/Jalview.java +++ b/src/jalview/bin/Jalview.java @@ -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); diff --git a/src/jalview/bin/JalviewLite.java b/src/jalview/bin/JalviewLite.java index d7b064a..dbc707d 100644 --- a/src/jalview/bin/JalviewLite.java +++ b/src/jalview/bin/JalviewLite.java @@ -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() diff --git a/src/jalview/commands/EditCommand.java b/src/jalview/commands/EditCommand.java index 21ff841..9eaeb7a 100644 --- a/src/jalview/commands/EditCommand.java +++ b/src/jalview/commands/EditCommand.java @@ -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 sf = sequence.getFeatures() + .getPositionalFeatures(); - if (sf == null) + if (sf.isEmpty()) { return; } - SequenceFeature[] oldsf = new SequenceFeature[sf.length]; + List oldsf = new ArrayList(); 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(); + command.editedFeatures = new Hashtable>(); } command.editedFeatures.put(seq, oldsf); @@ -1298,7 +1318,7 @@ public class EditCommand implements CommandI Hashtable deletedAnnotations; - Hashtable editedFeatures; + Hashtable> 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() diff --git a/src/jalview/controller/AlignViewController.java b/src/jalview/controller/AlignViewController.java index bc7f212..5c1f403 100644 --- a/src/jalview/controller/AlignViewController.java +++ b/src/jalview/controller/AlignViewController.java @@ -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 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 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 + } } } } diff --git a/src/jalview/datamodel/Alignment.java b/src/jalview/datamodel/Alignment.java index 90bdcae..f5e6fc7 100755 --- a/src/jalview/datamodel/Alignment.java +++ b/src/jalview/datamodel/Alignment.java @@ -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 codonFrameList; @@ -74,16 +72,10 @@ public class Alignment implements AlignmentI { groups = Collections.synchronizedList(new ArrayList()); hiddenSequences = new HiddenSequences(this); - codonFrameList = new ArrayList(); + hiddenCols = new HiddenColumns(); + codonFrameList = new ArrayList<>(); - if (Comparison.isNucleotide(seqs)) - { - type = NUCLEOTIDE; - } - else - { - type = PROTEIN; - } + nucleotide = Comparison.isNucleotide(seqs); sequences = Collections.synchronizedList(new ArrayList()); @@ -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 temp = new ArrayList(); + ArrayList 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 toProcess = new ArrayList(); + + List 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 seqs = new LinkedIdentityHashSet(); + Set 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 cframes = new ArrayList(); + List 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 toappendsq = new ArrayList(); + List 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 findAnnotation(String calcId) { - ArrayList aa = new ArrayList(); - for (AlignmentAnnotation a : getAlignmentAnnotation()) + List 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 findAnnotations(SequenceI seq, String calcId, String label) { - ArrayList aa = new ArrayList(); + ArrayList 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 getSequenceNames() { - Set names = new HashSet(); + Set 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; + } } diff --git a/src/jalview/datamodel/AlignmentAnnotation.java b/src/jalview/datamodel/AlignmentAnnotation.java index bbd3ce4..56bfd74 100755 --- a/src/jalview/datamodel/AlignmentAnnotation.java +++ b/src/jalview/datamodel/AlignmentAnnotation.java @@ -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; + } } diff --git a/src/jalview/datamodel/AlignmentI.java b/src/jalview/datamodel/AlignmentI.java index 2df099a..2e61f9d 100755 --- a/src/jalview/datamodel/AlignmentI.java +++ b/src/jalview/datamodel/AlignmentI.java @@ -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 hiddenCols); + + public void setHiddenColumns(HiddenColumns cols); + } diff --git a/src/jalview/datamodel/AlignmentView.java b/src/jalview/datamodel/AlignmentView.java index 9db9f38..9ca70f2 100644 --- a/src/jalview/datamodel/AlignmentView.java +++ b/src/jalview/datamodel/AlignmentView.java @@ -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 index 0000000..f84ba95 --- /dev/null +++ b/src/jalview/datamodel/AllColsCollection.java @@ -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 . + * 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 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 index 0000000..c1296d5 --- /dev/null +++ b/src/jalview/datamodel/AllColsIterator.java @@ -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 . + * 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 +{ + 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 index 0000000..502ace4 --- /dev/null +++ b/src/jalview/datamodel/AllRowsCollection.java @@ -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 . + * 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 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 index 0000000..b6d45f8 --- /dev/null +++ b/src/jalview/datamodel/AllRowsIterator.java @@ -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 . + * 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 +{ + 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(); + } +} + + diff --git a/src/jalview/datamodel/AnnotatedCollectionI.java b/src/jalview/datamodel/AnnotatedCollectionI.java index 74568e4..2963fd5 100644 --- a/src/jalview/datamodel/AnnotatedCollectionI.java +++ b/src/jalview/datamodel/AnnotatedCollectionI.java @@ -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 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 findAnnotations(SequenceI seq, String calcId, String label); diff --git a/src/jalview/datamodel/Annotation.java b/src/jalview/datamodel/Annotation.java index 71ebbb3..8de8eb2 100755 --- a/src/jalview/datamodel/Annotation.java +++ b/src/jalview/datamodel/Annotation.java @@ -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); + } } diff --git a/src/jalview/datamodel/BinarySequence.java b/src/jalview/datamodel/BinarySequence.java index ed57dce..b7e15a6 100755 --- a/src/jalview/datamodel/BinarySequence.java +++ b/src/jalview/datamodel/BinarySequence.java @@ -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; diff --git a/src/jalview/datamodel/CigarArray.java b/src/jalview/datamodel/CigarArray.java index f6e5862..837a10b 100644 --- a/src/jalview/datamodel/CigarArray.java +++ b/src/jalview/datamodel/CigarArray.java @@ -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[][] diff --git a/src/jalview/datamodel/ColumnSelection.java b/src/jalview/datamodel/ColumnSelection.java index c467f4a..eb2d174 100644 --- a/src/jalview/datamodel/ColumnSelection.java +++ b/src/jalview/datamodel/ColumnSelection.java @@ -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 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 compensateForEdit(int start, int change) - { - List deletedHiddenColumns = null; - selection.compensateForEdits(start, change); - - if (hiddenColumns != null) - { - deletedHiddenColumns = new ArrayList(); - 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 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 shifts, - Vector 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 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 getHiddenColumns() - { - return hiddenColumns == null ? Collections. 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(); - } - - /* - * 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(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 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 0) + if (selection.isEmpty()) { - List visiblecontigs = new ArrayList(); - List 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 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 annels = new Vector(); - Annotation[] els = null; - List 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 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 + *

    + * 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 index 0000000..f2ae4b7 --- /dev/null +++ b/src/jalview/datamodel/ContiguousI.java @@ -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 index 0000000..f0d99e5 --- /dev/null +++ b/src/jalview/datamodel/HiddenColumns.java @@ -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 hiddenColumns; + + /** + * This Method is used to return all the HiddenColumn regions + * + * @return empty list or List of hidden column intervals + */ + public List getHiddenRegions() + { + return hiddenColumns == null ? Collections. 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(); + } + + /* + * 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(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 compensateForEdit(int start, int change, + ColumnSelection sel) + { + List deletedHiddenColumns = null; + + if (hiddenColumns != null) + { + deletedHiddenColumns = new ArrayList(); + 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 0) + { + List visiblecontigs = new ArrayList(); + List 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 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 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 annels = new Vector(); + Annotation[] els = null; + List 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 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 shifts, + Vector 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 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 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 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); + } + } + +} diff --git a/src/jalview/datamodel/HiddenSequences.java b/src/jalview/datamodel/HiddenSequences.java index 9e2cf72..a98b10e 100755 --- a/src/jalview/datamodel/HiddenSequences.java +++ b/src/jalview/datamodel/HiddenSequences.java @@ -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 showAll( Map hiddenRepSequences) { - List revealedSeqs = new ArrayList(); + List 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 showSequence(int alignmentIndex, Map hiddenRepSequences) { - List revealedSeqs = new ArrayList(); + List 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; + } } diff --git a/src/jalview/datamodel/Mapping.java b/src/jalview/datamodel/Mapping.java index 1c196be..c741603 100644 --- a/src/jalview/datamodel/Mapping.java +++ b/src/jalview/datamodel/Mapping.java @@ -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 index 0000000..7886713 --- /dev/null +++ b/src/jalview/datamodel/Range.java @@ -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; + } +} diff --git a/src/jalview/datamodel/SearchResults.java b/src/jalview/datamodel/SearchResults.java index 1bf5475..8d98fc4 100755 --- a/src/jalview/datamodel/SearchResults.java +++ b/src/jalview/datamodel/SearchResults.java @@ -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) diff --git a/src/jalview/datamodel/SeqCigar.java b/src/jalview/datamodel/SeqCigar.java index 98b0de5..9cc7b4a 100644 --- a/src/jalview/datamodel/SeqCigar.java +++ b/src/jalview/datamodel/SeqCigar.java @@ -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); } } diff --git a/src/jalview/datamodel/Sequence.java b/src/jalview/datamodel/Sequence.java index b0faf21..a442cf0 100755 --- a/src/jalview/datamodel/Sequence.java +++ b/src/jalview/datamodel/Sequence.java @@ -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 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 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 subset = new Vector(); + Enumeration 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 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 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 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 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 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++; + } } diff --git a/src/jalview/datamodel/SequenceCollectionI.java b/src/jalview/datamodel/SequenceCollectionI.java index aca79c8..f681f11 100644 --- a/src/jalview/datamodel/SequenceCollectionI.java +++ b/src/jalview/datamodel/SequenceCollectionI.java @@ -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 index 0000000..b5929bf --- /dev/null +++ b/src/jalview/datamodel/SequenceCursor.java @@ -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); + } +} diff --git a/src/jalview/datamodel/SequenceFeature.java b/src/jalview/datamodel/SequenceFeature.java index 15f54b9..9a6cf2b 100755 --- a/src/jalview/datamodel/SequenceFeature.java +++ b/src/jalview/datamodel/SequenceFeature.java @@ -20,8 +20,11 @@ */ 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 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) ((HashMap) cpy.otherDetails) - .clone(); - } catch (Exception e) - { - // ignore - } - } - if (cpy.links != null && cpy.links.size() > 0) - { - links = new Vector(); - 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(); + for (Entry entry : sf.otherDetails.entrySet()) + { + otherDetails.put(entry.getKey(), entry.getValue()); + } + } + if (sf.links != null && sf.links.size() > 0) + { + links = new Vector(); + for (int i = 0, iSize = sf.links.size(); i < iSize; i++) + { + links.addElement(sf.links.elementAt(i)); + } + } } /** @@ -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(); } - 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; } } diff --git a/src/jalview/datamodel/SequenceGroup.java b/src/jalview/datamodel/SequenceGroup.java index 9245761..463b909 100755 --- a/src/jalview/datamodel/SequenceGroup.java +++ b/src/jalview/datamodel/SequenceGroup.java @@ -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 sequences = new ArrayList(); + private List 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(); + 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 allSequences = new ArrayList(); + List 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(); + 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 annot = new ArrayList(); + ArrayList annot = new ArrayList<>(); synchronized (sequences) { for (SequenceI seq : sequences) @@ -1269,10 +1282,14 @@ public class SequenceGroup implements AnnotatedCollectionI @Override public Iterable findAnnotation(String calcId) { - ArrayList aa = new ArrayList(); + List 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 findAnnotations(SequenceI seq, String calcId, String label) { - ArrayList aa = new ArrayList(); + ArrayList 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); + } } diff --git a/src/jalview/datamodel/SequenceI.java b/src/jalview/datamodel/SequenceI.java index 92f797f..6992a8d 100755 --- a/src/jalview/datamodel/SequenceI.java +++ b/src/jalview/datamodel/SequenceI.java @@ -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.. 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 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 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 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(); } diff --git a/src/jalview/datamodel/SequenceNode.java b/src/jalview/datamodel/SequenceNode.java index b2f054c..fa12419 100755 --- a/src/jalview/datamodel/SequenceNode.java +++ b/src/jalview/datamodel/SequenceNode.java @@ -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 index 0000000..86233ab --- /dev/null +++ b/src/jalview/datamodel/VisibleColsCollection.java @@ -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 . + * 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 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 index 0000000..70de1e3 --- /dev/null +++ b/src/jalview/datamodel/VisibleColsIterator.java @@ -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 . + * 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 +{ + private int last; + + private int current; + + private int next; + + private List 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 index 0000000..ce8e8da --- /dev/null +++ b/src/jalview/datamodel/VisibleRowsCollection.java @@ -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 . + * 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 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 index 0000000..a9c782d --- /dev/null +++ b/src/jalview/datamodel/VisibleRowsIterator.java @@ -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 . + * 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 +{ + 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 index 0000000..e651c13 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureLocationI.java @@ -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 index 0000000..3e80966 --- /dev/null +++ b/src/jalview/datamodel/features/FeatureStore.java @@ -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 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 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 nonNestedFeatures; + + /* + * contact features ordered by first contact position + */ + List contactFeatureStarts; + + /* + * contact features ordered by second contact position + */ + List 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 nestedFeatures; + + /* + * Feature groups represented in stored positional features + * (possibly including null) + */ + Set positionalFeatureGroups; + + /* + * Feature groups represented in stored non-positional features + * (possibly including null) + */ + Set 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(); + positionalFeatureGroups = new HashSet(); + nonPositionalFeatureGroups = new HashSet(); + 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(); + } + 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(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(); + } + if (contactFeatureEnds == null) + { + contactFeatureEnds = new ArrayList(); + } + + 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 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 findOverlappingFeatures(long start, long end) + { + List result = new ArrayList(); + + findNonNestedFeatures(start, end, result); + + findContactFeatures(start, end, result); + + if (nestedFeatures != null) + { + result.addAll(nestedFeatures.findOverlaps(start, end)); + } + + return result; + } + + /** + * Adds contact features to the result list where either the second or the + * first contact position lies within the target range + * + * @param from + * @param to + * @param result + */ + protected void findContactFeatures(long from, long to, + List 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 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 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 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 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 getPositionalFeatures() + { + /* + * add non-nested features (may be all features for many cases) + */ + List result = new ArrayList(); + result.addAll(nonNestedFeatures); + + /* + * add any contact features - from the list by start position + */ + if (contactFeatureStarts != null) + { + result.addAll(contactFeatureStarts); + } + + /* + * add any nested features + */ + if (nestedFeatures != null) + { + result.addAll(nestedFeatures.getEntries()); + } + + return result; + } + + /** + * Answers a list of all contact features. If there are none, returns an + * immutable empty list. + * + * @return + */ + public List getContactFeatures() + { + if (contactFeatureStarts == null) + { + return Collections.emptyList(); + } + return new ArrayList(contactFeatureStarts); + } + + /** + * Answers a list of all non-positional features. If there are none, returns + * an immutable empty list. + * + * @return + */ + public List getNonPositionalFeatures() + { + if (nonPositionalFeatures == null) + { + return Collections.emptyList(); + } + return new ArrayList(nonPositionalFeatures); + } + + /** + * Deletes the given feature from the store, returning true if it was found + * (and deleted), else false. This method makes no assumption that the feature + * is in the 'expected' place in the store, in case it has been modified since + * it was added. + * + * @param sf + */ + public synchronized boolean delete(SequenceFeature sf) + { + /* + * try the non-nested positional features first + */ + boolean removed = nonNestedFeatures.remove(sf); + + /* + * if not found, try contact positions (and if found, delete + * from both lists of contact positions) + */ + if (!removed && contactFeatureStarts != null) + { + removed = contactFeatureStarts.remove(sf); + if (removed) + { + contactFeatureEnds.remove(sf); + } + } + + boolean removedNonPositional = false; + + /* + * if not found, try non-positional features + */ + if (!removed && nonPositionalFeatures != null) + { + removedNonPositional = nonPositionalFeatures.remove(sf); + removed = removedNonPositional; + } + + /* + * if not found, try nested features + */ + if (!removed && nestedFeatures != null) + { + removed = nestedFeatures.delete(sf); + } + + if (removed) + { + rescanAfterDelete(); + } + + return removed; + } + + /** + * Rescan all features to recompute any cached values after an entry has been + * deleted. This is expected to be an infrequent event, so performance here is + * not critical. + */ + protected synchronized void rescanAfterDelete() + { + positionalFeatureGroups.clear(); + nonPositionalFeatureGroups.clear(); + totalExtent = 0; + positionalMinScore = Float.NaN; + positionalMaxScore = Float.NaN; + nonPositionalMinScore = Float.NaN; + nonPositionalMaxScore = Float.NaN; + + /* + * scan non-positional features for groups and scores + */ + for (SequenceFeature sf : getNonPositionalFeatures()) + { + nonPositionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + nonPositionalMinScore = min(nonPositionalMinScore, score); + nonPositionalMaxScore = max(nonPositionalMaxScore, score); + } + + /* + * scan positional features for groups, scores and extents + */ + for (SequenceFeature sf : getPositionalFeatures()) + { + positionalFeatureGroups.add(sf.getFeatureGroup()); + float score = sf.getScore(); + positionalMinScore = min(positionalMinScore, score); + positionalMaxScore = max(positionalMaxScore, score); + totalExtent += getFeatureLength(sf); + } + } + + /** + * A helper method to return the minimum of two floats, where a non-NaN value + * is treated as 'less than' a NaN value (unlike Math.min which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float min(float f1, float f2) + { + if (Float.isNaN(f1)) + { + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.min(f1, f2); + } + } + + /** + * A helper method to return the maximum of two floats, where a non-NaN value + * is treated as 'greater than' a NaN value (unlike Math.max which does the + * opposite) + * + * @param f1 + * @param f2 + */ + protected static float max(float f1, float f2) + { + if (Float.isNaN(f1)) + { + return Float.isNaN(f2) ? f1 : f2; + } + else + { + return Float.isNaN(f2) ? f1 : Math.max(f1, f2); + } + } + + /** + * Answers true if this store has no features, else false + * + * @return + */ + public boolean isEmpty() + { + boolean hasFeatures = !nonNestedFeatures.isEmpty() + || (contactFeatureStarts != null && !contactFeatureStarts + .isEmpty()) + || (nonPositionalFeatures != null && !nonPositionalFeatures + .isEmpty()) + || (nestedFeatures != null && nestedFeatures.size() > 0); + + return !hasFeatures; + } + + /** + * Answers the set of distinct feature groups stored, possibly including null, + * as an unmodifiable view of the set. The parameter determines whether the + * groups for positional or for non-positional features are returned. + * + * @param positionalFeatures + * @return + */ + public Set getFeatureGroups(boolean positionalFeatures) + { + if (positionalFeatures) + { + return Collections.unmodifiableSet(positionalFeatureGroups); + } + else + { + return nonPositionalFeatureGroups == null ? Collections + . 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 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 getFeaturesForGroup(boolean positional, + String group) + { + List result = new ArrayList(); + + /* + * if we know features don't include the target group, no need + * to inspect them for matches + */ + if (positional && !positionalFeatureGroups.contains(group) + || !positional && !nonPositionalFeatureGroups.contains(group)) + { + return result; + } + + List 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 index 0000000..a6a23e7 --- /dev/null +++ b/src/jalview/datamodel/features/NCList.java @@ -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 + * + *

    + * 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
    + * 
    + */ +public class NCList +{ + /* + * 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> subranges; + + /** + * Constructor given a list of things that are each located on a contiguous + * interval. Note that the constructor may reorder the list. + *

    + * We assume here that for each range, start <= end. Behaviour for reverse + * ordered ranges is undefined. + * + * @param ranges + */ + public NCList(List 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 ranges) + { + /* + * sort by start ascending so that contained intervals + * follow their containing interval + */ + Collections.sort(ranges, RangeComparator.BY_START_POSITION); + + List 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(ranges.subList(sublist.start, + sublist.end + 1))); + } + + size = ranges.size(); + } + + public NCList(T entry) + { + this(); + subranges.add(new NCNode(entry)); + size = 1; + } + + public NCList() + { + subranges = new ArrayList>(); + } + + /** + * Traverses the sorted ranges to identify sublists, within which each + * interval contains the one that follows it + * + * @param ranges + * @return + */ + protected List buildSubranges(List ranges) + { + List sublists = new ArrayList(); + + if (ranges.isEmpty()) + { + return sublists; + } + + int listStartIndex = 0; + long lastEndPos = Long.MAX_VALUE; + + for (int i = 0; i < ranges.size(); i++) + { + ContiguousI nextInterval = ranges.get(i); + long nextStart = nextInterval.getBegin(); + long nextEnd = nextInterval.getEnd(); + if (nextStart > lastEndPos || nextEnd > lastEndPos) + { + /* + * this interval is not contained in the preceding one + * close off the last sublist + */ + sublists.add(new Range(listStartIndex, i - 1)); + listStartIndex = i; + } + lastEndPos = nextEnd; + } + + sublists.add(new Range(listStartIndex, ranges.size() - 1)); + return sublists; + } + + /** + * Adds one entry to the stored set (with duplicates allowed) + * + * @param entry + */ + public void add(T entry) + { + add(entry, true); + } + + /** + * Adds one entry to the stored set, and returns true, unless allowDuplicates + * is set to false and it is already contained (by object equality test), in + * which case it is not added and this method returns false. + * + * @param entry + * @param allowDuplicates + * @return + */ + public synchronized boolean add(T entry, boolean allowDuplicates) + { + if (!allowDuplicates && contains(entry)) + { + return false; + } + + size++; + long start = entry.getBegin(); + long end = entry.getEnd(); + + /* + * cases: + * - precedes all subranges: add as NCNode on front of list + * - follows all subranges: add as NCNode on end of list + * - enclosed by a subrange - add recursively to subrange + * - encloses one or more subranges - push them inside it + * - none of the above - add as a new node and resort nodes list (?) + */ + + /* + * find the first subrange whose end does not precede entry's start + */ + int candidateIndex = findFirstOverlap(start); + if (candidateIndex == -1) + { + /* + * all subranges precede this one - add it on the end + */ + subranges.add(new NCNode(entry)); + return true; + } + + /* + * search for maximal span of subranges i-k that the new entry + * encloses; or a subrange that encloses the new entry + */ + boolean enclosing = false; + int firstEnclosed = 0; + int lastEnclosed = 0; + boolean overlapping = false; + + for (int j = candidateIndex; j < subranges.size(); j++) + { + NCNode subrange = subranges.get(j); + + if (end < subrange.getBegin() && !overlapping && !enclosing) + { + /* + * new entry lies between subranges j-1 j + */ + subranges.add(j, new NCNode(entry)); + return true; + } + + if (subrange.getBegin() <= start && subrange.getEnd() >= end) + { + /* + * push new entry inside this subrange as it encloses it + */ + subrange.add(entry); + return true; + } + + if (start <= subrange.getBegin()) + { + if (end >= subrange.getEnd()) + { + /* + * new entry encloses this subrange (and possibly preceding ones); + * continue to find the maximal list it encloses + */ + if (!enclosing) + { + firstEnclosed = j; + } + lastEnclosed = j; + enclosing = true; + continue; + } + else + { + /* + * entry spans from before this subrange to inside it + */ + if (enclosing) + { + /* + * entry encloses one or more preceding subranges + */ + addEnclosingRange(entry, firstEnclosed, lastEnclosed); + return true; + } + else + { + /* + * entry spans two subranges but doesn't enclose any + * so just add it + */ + subranges.add(j, new NCNode(entry)); + return true; + } + } + } + else + { + overlapping = true; + } + } + + /* + * drops through to here if new range encloses all others + * or overlaps the last one + */ + if (enclosing) + { + addEnclosingRange(entry, firstEnclosed, lastEnclosed); + } + else + { + subranges.add(new NCNode(entry)); + } + + return true; + } + + /** + * Answers true if this NCList contains the given entry (by object equality + * test), else false + * + * @param entry + * @return + */ + public boolean contains(T entry) + { + /* + * find the first sublist that might overlap, i.e. + * the first whose end position is >= from + */ + int candidateIndex = findFirstOverlap(entry.getBegin()); + + if (candidateIndex == -1) + { + return false; + } + + int to = entry.getEnd(); + + for (int i = candidateIndex; i < subranges.size(); i++) + { + NCNode 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 newNCList = new NCList(); + newNCList.addNodes(subranges.subList(i, j + 1)); + NCNode newNode = new NCNode(entry, newNCList); + for (int k = j; k >= i; k--) + { + subranges.remove(k); + } + subranges.add(i, newNode); + } + + protected void addNodes(List> nodes) + { + for (NCNode 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 findOverlaps(long from, long to) + { + List result = new ArrayList(); + + findOverlaps(from, to, result); + + return result; + } + + /** + * Recursively searches the NCList adding any items that overlap the from-to + * range to the result list + * + * @param from + * @param to + * @param result + */ + protected void findOverlaps(long from, long to, List 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 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 subrange : subranges) + { + if (subrange.getEnd() >= from) + { + return i; + } + i++; + } + } + return -1; + } + + /** + * Formats the tree as a bracketed list e.g. + * + *

    +   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
    +   * 
    + */ + @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 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. + *

    + * Each subrange must lie within start-end (inclusive). Subranges must be + * ordered by start position ascending. + *

    + * + * @param start + * @param end + * @return + */ + boolean isValid(final int start, final int end) + { + int lastStart = start; + for (NCNode 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 getEntries() + { + List result = new ArrayList(); + getEntries(result); + return result; + } + + /** + * Adds all contained entries to the given list + * + * @param result + */ + void getEntries(List result) + { + for (NCNode 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 subrange = subranges.get(i); + NCList 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 index 0000000..c1be242 --- /dev/null +++ b/src/jalview/datamodel/features/NCNode.java @@ -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 + */ +class NCNode 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 subregions; + + /** + * Constructor given a list of ranges + * + * @param ranges + */ + NCNode(List ranges) + { + build(ranges); + } + + /** + * Constructor given a single range + * + * @param range + */ + NCNode(V range) + { + List ranges = new ArrayList(); + ranges.add(range); + build(ranges); + } + + NCNode(V entry, NCList newNCList) + { + region = entry; + subregions = newNCList; + size = 1 + newNCList.size(); + } + + /** + * @param ranges + */ + protected void build(List ranges) + { + size = ranges.size(); + + if (!ranges.isEmpty()) + { + region = ranges.get(0); + } + if (ranges.size() > 1) + { + subregions = new NCList(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. + * + *

    +   * [1-100 [10-30 [10-20]], 15-30 [20-20]]
    +   * 
    + */ + @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 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(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 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 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 index 0000000..26ffee1 --- /dev/null +++ b/src/jalview/datamodel/features/RangeComparator.java @@ -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 +{ + public static final Comparator BY_START_POSITION = new RangeComparator( + true); + + public static final Comparator 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 index 0000000..8f6d496 --- /dev/null +++ b/src/jalview/datamodel/features/SequenceFeatures.java @@ -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 FORWARD_STRAND = new Comparator() + { + @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 REVERSE_STRAND = new Comparator() + { + @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 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()); + featureStore = new TreeMap(); + } + + /** + * Constructor given a list of features + */ + public SequenceFeatures(List 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 findFeatures(int from, int to, + String... type) + { + List result = new ArrayList(); + + 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 getAllFeatures(String... type) + { + List result = new ArrayList(); + + result.addAll(getPositionalFeatures(type)); + + result.addAll(getNonPositionalFeatures()); + + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getFeaturesByOntology(String... ontologyTerm) + { + if (ontologyTerm == null || ontologyTerm.length == 0) + { + return new ArrayList(); + } + + Set 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 getPositionalFeatures(String... type) + { + List result = new ArrayList(); + + 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 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 types = new ArrayList(Arrays.asList(type)); + types.remove(null); + Collections.sort(types); + return types; + } + + /** + * {@inheritDoc} + */ + @Override + public List getContactFeatures(String... type) + { + List result = new ArrayList(); + + for (String featureType : varargToTypes(type)) + { + FeatureStore featureSet = featureStore.get(featureType); + if (featureSet != null) + { + result.addAll(featureSet.getContactFeatures()); + } + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public List getNonPositionalFeatures(String... type) + { + List result = new ArrayList(); + + 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 getFeatureGroups(boolean positionalFeatures, + String... type) + { + Set groups = new HashSet(); + + Iterable 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 getFeatureTypesForGroups(boolean positionalFeatures, + String... groups) + { + Set result = new HashSet(); + + for (Entry featureType : featureStore.entrySet()) + { + Set 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 getFeatureTypes(String... soTerm) + { + Set types = new HashSet(); + for (Entry 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 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. + *

    + * 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 getFeaturesForGroup(boolean positional, + String group, String... type) + { + List result = new ArrayList(); + Iterable 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 index 0000000..58beca2 --- /dev/null +++ b/src/jalview/datamodel/features/SequenceFeaturesI.java @@ -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 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. + *

    + * To filter non-positional features by type, use + * getNonPositionalFeatures(type). + * + * @param type + * @return + */ + List 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 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 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 getPositionalFeatures( + String... type); + + /** + * Answers a list of all contact features, optionally restricted to specified + * types, in no particular guaranteed order + * + * @return + */ + List 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 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 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 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 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 diff --git a/src/jalview/datamodel/xdb/embl/EmblEntry.java b/src/jalview/datamodel/xdb/embl/EmblEntry.java index 4d09bdc..c3d4e66 100644 --- a/src/jalview/datamodel/xdb/embl/EmblEntry.java +++ b/src/jalview/datamodel/xdb/embl/EmblEntry.java @@ -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 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 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 index 0000000..a3537c9 --- /dev/null +++ b/src/jalview/datamodel/xdb/uniprot/UniprotEntry.java @@ -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 . + * 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 name; + + Vector accession; + + Vector feature; + + Vector dbrefs; + + UniprotProteinName protName; + + public void setAccession(Vector items) + { + accession = items; + } + + public void setFeature(Vector items) + { + feature = items; + } + + public Vector getFeature() + { + return feature; + } + + public Vector getAccession() + { + return accession; + } + + public void setProtein(UniprotProteinName names) + { + protName = names; + } + + public UniprotProteinName getProtein() + { + return protName; + } + + public void setName(Vector na) + { + name = na; + } + + public Vector getName() + { + return name; + } + + public UniprotSequence getUniprotSequence() + { + return sequence; + } + + public void setUniprotSequence(UniprotSequence seq) + { + sequence = seq; + } + + public Vector getDbReference() + { + return dbrefs; + } + + public void setDbReference(Vector 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 index 0000000..4a359ff --- /dev/null +++ b/src/jalview/datamodel/xdb/uniprot/UniprotFeature.java @@ -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 index 0000000..9cc0391 --- /dev/null +++ b/src/jalview/datamodel/xdb/uniprot/UniprotFile.java @@ -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 . + * 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 _items; + + public void setUniprotEntries(Vector items) + { + _items = items; + } + + public Vector 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 index 0000000..2335e71 --- /dev/null +++ b/src/jalview/datamodel/xdb/uniprot/UniprotProteinName.java @@ -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 . + * 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 names; + + public void setName(Vector names) + { + this.names = names; + } + + public Vector 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 index 0000000..bdba73f --- /dev/null +++ b/src/jalview/datamodel/xdb/uniprot/UniprotSequence.java @@ -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 . + * 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; + } + +} diff --git a/src/jalview/ext/android/ContainerHelpers.java b/src/jalview/ext/android/ContainerHelpers.java index 4033dcc..26bd142 100644 --- a/src/jalview/ext/android/ContainerHelpers.java +++ b/src/jalview/ext/android/ContainerHelpers.java @@ -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 index 0000000..eaf059c --- /dev/null +++ b/src/jalview/ext/android/SparseDoubleArray.java @@ -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. + * + *

    + * 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%. + *

    + * + *

    + * It is possible to iterate over the items in this container using + * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using + * keyAt(int) 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 valueAt(int). + *

    + */ + +/* + * 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 0 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 0...size()-1, returns the key from + * the indexth key-value mapping that this SparseDoubleArray + * stores. + * + *

    + * The keys corresponding to indices in ascending order are guaranteed to be + * in ascending order, e.g., keyAt(0) will return the smallest + * key and keyAt(size()-1) will return the largest key. + *

    + */ + public int keyAt(int index) + { + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns the value + * from the indexth key-value mapping that this SparseDoubleArray + * stores. + * + *

    + * The values corresponding to indices in ascending order are guaranteed to be + * associated with keys in ascending order, e.g., valueAt(0) will + * return the value associated with the smallest key and + * valueAt(size()-1) will return the value associated with the + * largest key. + *

    + */ + 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} + * + *

    + * 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; + } +} diff --git a/src/jalview/ext/ensembl/EnsemblCdna.java b/src/jalview/ext/ensembl/EnsemblCdna.java index dc000c6..6d031b7 100644 --- a/src/jalview/ext/ensembl/EnsemblCdna.java +++ b/src/jalview/ext/ensembl/EnsemblCdna.java @@ -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 params = new HashMap(); + + 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 getAdditionalParameters() + { + return params; + } + } diff --git a/src/jalview/ext/ensembl/EnsemblGene.java b/src/jalview/ext/ensembl/EnsemblGene.java index 24e3e95..915fa0a 100644 --- a/src/jalview/ext/ensembl/EnsemblGene.java +++ b/src/jalview/ext/ensembl/EnsemblGene.java @@ -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 sfs = gene.getFeatures().getFeaturesByOntology( + soTerms); + for (SequenceFeature sf : sfs) { - SequenceOntologyI so = SequenceOntologyFactory.getInstance(); - List filtered = new ArrayList(); - 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 transcriptFeatures = new ArrayList(); String parentIdentifier = GENE_PREFIX + accId; - SequenceFeature[] sfs = geneSequence.getSequenceFeatures(); + // todo optimise here by transcript type! + List 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); } } } diff --git a/src/jalview/ext/ensembl/EnsemblRestClient.java b/src/jalview/ext/ensembl/EnsemblRestClient.java index ab3b197..2437588 100644 --- a/src/jalview/ext/ensembl/EnsemblRestClient.java +++ b/src/jalview/ext/ensembl/EnsemblRestClient.java @@ -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) diff --git a/src/jalview/ext/ensembl/EnsemblSeqProxy.java b/src/jalview/ext/ensembl/EnsemblSeqProxy.java index 233707b..79d6c0a 100644 --- a/src/jalview/ext/ensembl/EnsemblSeqProxy.java +++ b/src/jalview/ext/ensembl/EnsemblSeqProxy.java @@ -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 params = getAdditionalParameters(); + if (params != null) + { + for (Entry 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 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 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 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 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 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() - { - @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 findFeatures(SequenceI sequence, - String type, String parentId) + String term, String parentId) { List result = new ArrayList(); - SequenceFeature[] sfs = sequence.getSequenceFeatures(); - if (sfs != null) + List 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; } diff --git a/src/jalview/ext/jmol/JalviewJmolBinding.java b/src/jalview/ext/jmol/JalviewJmolBinding.java index 2d2d10e..5de554b 100644 --- a/src/jalview/ext/jmol/JalviewJmolBinding.java +++ b/src/jalview/ext/jmol/JalviewJmolBinding.java @@ -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 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 mset = new ArrayList(); + 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(); chainFile = new Hashtable(); 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 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); diff --git a/src/jalview/ext/jmol/JmolCommands.java b/src/jalview/ext/jmol/JmolCommands.java index d5676c5..3e7ca59 100644 --- a/src/jalview/ext/jmol/JmolCommands.java +++ b/src/jalview/ext/jmol/JmolCommands.java @@ -20,16 +20,21 @@ */ 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 cset = new ArrayList(); + FeatureRenderer fr = viewPanel.getFeatureRenderer(); + FeatureColourFinder finder = new FeatureColourFinder(fr); + AlignViewportI viewport = viewPanel.getAlignViewport(); + HiddenColumns cs = viewport.getAlignment().getHiddenColumns(); + AlignmentI al = viewport.getAlignment(); + List cset = new ArrayList(); 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() : "") + "/" diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java index d62cc3c..f3c9c1e 100644 --- a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java +++ b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java @@ -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]; diff --git a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java index 07c0015..e9ce49b 100644 --- a/src/jalview/ext/rbvi/chimera/ChimeraCommands.java +++ b/src/jalview/ext/rbvi/chimera/ChimeraCommands.java @@ -20,11 +20,15 @@ */ 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 colourMap = buildColoursMap( - ssm, files, sequence, sr, fr, alignment); + Map colourMap = buildColoursMap(ssm, files, + sequence, sr, viewPanel); List 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 /** *

    -   * 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 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 colourMap = new LinkedHashMap();
         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 map,
    +  protected static void addColourRange(Map 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> featureMap = buildFeaturesMap(
    -            ssm, files, seqs, fr, alignment);
    +            ssm, files, seqs, viewPanel);
     
         List 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> buildFeaturesMap(
               StructureSelectionManager ssm, String[] files,
    -          SequenceI[][] seqs, FeatureRenderer fr, AlignmentI alignment)
    +          SequenceI[][] seqs, AlignmentViewPanel viewPanel)
       {
         Map> theMap = new LinkedHashMap>();
     
    +    FeatureRenderer fr = viewPanel.getFeatureRenderer();
    +    if (fr == null)
    +    {
    +      return theMap;
    +    }
    +
         List 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> theMap, int modelNumber)
       {
    -    SequenceFeature[] sfs = seq.getSequenceFeatures();
    -    if (sfs == null)
    -    {
    -      return;
    -    }
    -
    +    List 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("\\'", "'");
    +        sb.append("setattr r ").append(attributeName).append(" '")
    +                .append(featureValue).append("' ");
             sb.append(values.get(value).getAtomSpec());
             commands.add(sb.toString());
           }
    diff --git a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
    index 75ddc9c..b954677 100644
    --- a/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
    +++ b/src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
    @@ -21,15 +21,14 @@
     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
        * 
  • all others - white
  • * */ + @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 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 maps = chimeraMaps.get(getPdbFile()[pdbfnum]); + List 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 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); /* diff --git a/src/jalview/ext/varna/VarnaCommands.java b/src/jalview/ext/varna/VarnaCommands.java index a41e10e..d65f1d5 100644 --- a/src/jalview/ext/varna/VarnaCommands.java +++ b/src/jalview/ext/varna/VarnaCommands.java @@ -20,10 +20,10 @@ */ 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 str = new ArrayList(); @@ -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() : "") + "/" diff --git a/src/jalview/fts/api/GFTSPanelI.java b/src/jalview/fts/api/GFTSPanelI.java index f86c3bc..99c0c51 100644 --- a/src/jalview/fts/api/GFTSPanelI.java +++ b/src/jalview/fts/api/GFTSPanelI.java @@ -137,4 +137,12 @@ public interface GFTSPanelI * @return */ public Map getTempUserPrefs(); + + /** + * Returns unique key used for storing an FTSs instance cache items in the + * cache data structure + * + * @return + */ + public String getCacheKey(); } diff --git a/src/jalview/fts/core/GFTSPanel.java b/src/jalview/fts/core/GFTSPanel.java index a69d9f8..f1db383 100644 --- a/src/jalview/fts/core/GFTSPanel.java +++ b/src/jalview/fts/core/GFTSPanel.java @@ -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 txt_search; protected SequenceFetcher seqFetcher; @@ -146,6 +149,10 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI protected HashSet paginatorCart = new HashSet(); + 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(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 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()); } + } diff --git a/src/jalview/fts/service/pdb/PDBFTSPanel.java b/src/jalview/fts/service/pdb/PDBFTSPanel.java index 1dfabce..b944b9b 100644 --- a/src/jalview/fts/service/pdb/PDBFTSPanel.java +++ b/src/jalview/fts/service/pdb/PDBFTSPanel.java @@ -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 tempUserPrefs = new HashMap(); + 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; + } + + } diff --git a/src/jalview/fts/service/uniprot/UniProtFTSRestClient.java b/src/jalview/fts/service/uniprot/UniProtFTSRestClient.java index 27bfca8..a23df4c 100644 --- a/src/jalview/fts/service/uniprot/UniProtFTSRestClient.java +++ b/src/jalview/fts/service/uniprot/UniProtFTSRestClient.java @@ -157,14 +157,14 @@ public class UniProtFTSRestClient extends FTSRestClient if (foundDataRow != null && foundDataRow.length > 0) { result = new ArrayList(); - 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); diff --git a/src/jalview/fts/service/uniprot/UniprotFTSPanel.java b/src/jalview/fts/service/uniprot/UniprotFTSPanel.java index f04e4fa..ace3600 100644 --- a/src/jalview/fts/service/uniprot/UniprotFTSPanel.java +++ b/src/jalview/fts/service/uniprot/UniprotFTSPanel.java @@ -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 tempUserPrefs = new HashMap(); + 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; + } } diff --git a/src/jalview/gui/AlignFrame.java b/src/jalview/gui/AlignFrame.java index 370e649..a9a970f 100644 --- a/src/jalview/gui/AlignFrame.java +++ b/src/jalview/gui/AlignFrame.java @@ -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 alignPanels = new ArrayList(); @@ -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 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 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 + *
      + *
    • an Annotation file
    • + *
    • a JNet file
    • + *
    • a features file
    • + *
    • else try to interpret as an alignment file
    • + *
    * * @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 diff --git a/src/jalview/gui/AlignViewport.java b/src/jalview/gui/AlignViewport.java index 53d118b..86e1144 100644 --- a/src/jalview/gui/AlignViewport.java +++ b/src/jalview/gui/AlignViewport.java @@ -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); } } diff --git a/src/jalview/gui/AlignmentPanel.java b/src/jalview/gui/AlignmentPanel.java index e61b042..72b1cc9 100644 --- a/src/jalview/gui/AlignmentPanel.java +++ b/src/jalview/gui/AlignmentPanel.java @@ -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("" + ""); - 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(" res) + if (groups[g].getStartRes() < column + && groups[g].getEndRes() > column) { text.append("
    ").append(groups[g].getName()) .append(""); } } - if (features != null) + if (text.length() < 1) { - if (text.length() < 1) - { - text.append(" 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("
    ").append(features[f].getType()) - .append(" ").append(features[f].getBegin()) - .append(":").append(features[f].getEnd()); - } - } - else + text.append("
    ").append(sf.getType()).append(" ") + .append(sf.getBegin()).append(":") + .append(sf.getEnd()); + } + else + { + text.append("
    "); + text.append(sf.getType()); + String description = sf.getDescription(); + if (description != null + && !sf.getType().equals(description)) { - text.append("
    "); - 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("\"", """); + 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; } } diff --git a/src/jalview/gui/AnnotationColourChooser.java b/src/jalview/gui/AnnotationColourChooser.java index 39cde03..253a7ec 100644 --- a/src/jalview/gui/AnnotationColourChooser.java +++ b/src/jalview/gui/AnnotationColourChooser.java @@ -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 oldgroupColours; + private JButton defColours; - /** - * enabled if the user is dragging the slider - try to keep updates to a - * minimun - */ + private Hashtable oldgroupColours; - JComboBox 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 threshold = new JComboBox(); + 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(); 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( - getAnnotationItems(seqAssociated.isSelected())); + Vector annotItems = getAnnotationItems(seqAssociated + .isSelected()); + annotations = new JComboBox(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 getThreshold() + public void originalColours_actionPerformed() { - return threshold; - } - - public void setThreshold(JComboBox 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; + } + } diff --git a/src/jalview/gui/AnnotationColumnChooser.java b/src/jalview/gui/AnnotationColumnChooser.java index 1290d70..9c2a1b9 100644 --- a/src/jalview/gui/AnnotationColumnChooser.java +++ b/src/jalview/gui/AnnotationColumnChooser.java @@ -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 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 threshold = new JComboBox(); - 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(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 itr = oldSelection.getHiddenColumns() + for (Iterator 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 getThreshold() - { - return threshold; - } - - public void setThreshold(JComboBox threshold) - { - this.threshold = threshold; - } - - public JComboBox getAnnotations() - { - return annotations; - } - - public void setAnnotations(JComboBox 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 searchBox = new JvCacheableInputBox( + 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(); + } } diff --git a/src/jalview/gui/AnnotationExporter.java b/src/jalview/gui/AnnotationExporter.java index 0d47e36..42913de 100644 --- a/src/jalview/gui/AnnotationExporter.java +++ b/src/jalview/gui/AnnotationExporter.java @@ -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 displayedFeatureColours = ap - .getFeatureRenderer().getDisplayedFeatureCols(); FeaturesFile formatter = new FeaturesFile(); SequenceI[] sequences = ap.av.getAlignment().getSequencesArray(); Map featureColours = ap.getFeatureRenderer() .getDisplayedFeatureCols(); + List 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 diff --git a/src/jalview/gui/AnnotationLabels.java b/src/jalview/gui/AnnotationLabels.java index 4b774d6..8ca1a4e 100755 --- a/src/jalview/gui/AnnotationLabels.java +++ b/src/jalview/gui/AnnotationLabels.java @@ -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 hiddenCols = av.getColumnSelection().getHiddenColumns(); + List 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(); - for (int[] region : av.getColumnSelection().getHiddenColumns()) + for (int[] region : av.getAlignment().getHiddenColumns() + .getHiddenRegions()) { hiddenColumns.add(new int[] { region[0], region[1] }); } diff --git a/src/jalview/gui/AnnotationPanel.java b/src/jalview/gui/AnnotationPanel.java index b1f0edb..452f002 100755 --- a/src/jalview/gui/AnnotationPanel.java +++ b/src/jalview/gui/AnnotationPanel.java @@ -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()); + } + } } diff --git a/src/jalview/gui/AnnotationRowFilter.java b/src/jalview/gui/AnnotationRowFilter.java index c8bd69c..a3ce528 100644 --- a/src/jalview/gui/AnnotationRowFilter.java +++ b/src/jalview/gui/AnnotationRowFilter.java @@ -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 threshold = new JComboBox(); + + protected JComboBox annotations; + + /* + * map from annotation to its menu item display label + * - so we know which item to pre-select on restore + */ + private Map 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 getAnnotationItems(boolean isSeqAssociated) { + annotationLabels = new HashMap(); + Vector list = new Vector(); 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 threshold) + protected void populateThresholdComboBox(JComboBox 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 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 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 getThreshold() { - return currentAnnotation; + return threshold; } - public void setCurrentAnnotation( - jalview.datamodel.AlignmentAnnotation currentAnnotation) + public void setThreshold(JComboBox thresh) { - this.currentAnnotation = currentAnnotation; + this.threshold = thresh; } - public abstract void valueChanged(boolean updateAllAnnotation); - - public abstract void updateView(); + public JComboBox getAnnotations() + { + return annotations; + } - public abstract void reset(); + public void setAnnotations(JComboBox anns) + { + this.annotations = anns; + } } diff --git a/src/jalview/gui/AppJmol.java b/src/jalview/gui/AppJmol.java index a30f6d3..68a847e 100644 --- a/src/jalview/gui/AppJmol.java +++ b/src/jalview/gui/AppJmol.java @@ -21,24 +21,12 @@ 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(); - } - if (_alignwith == null) - { - _alignwith = new Vector(); - } - - 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 getViewersFor(AlignmentPanel apanel) + { + List result = new ArrayList(); + 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("
    ") > -1 || data.indexOf("
    ") > -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(); - } - 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"; } - } diff --git a/src/jalview/gui/AppJmolBinding.java b/src/jalview/gui/AppJmolBinding.java index 75e0c5e..f822358 100644 --- a/src/jalview/gui/AppJmolBinding.java +++ b/src/jalview/gui/AppJmolBinding.java @@ -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; + } } diff --git a/src/jalview/gui/AppVarna.java b/src/jalview/gui/AppVarna.java index f4a4f4d..a50de77 100644 --- a/src/jalview/gui/AppVarna.java +++ b/src/jalview/gui/AppVarna.java @@ -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) { diff --git a/src/jalview/gui/AppVarnaBinding.java b/src/jalview/gui/AppVarnaBinding.java index 8ab1e61..829fc3e 100644 --- a/src/jalview/gui/AppVarnaBinding.java +++ b/src/jalview/gui/AppVarnaBinding.java @@ -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 index 0000000..8a95594 --- /dev/null +++ b/src/jalview/gui/CalculationChooser.java @@ -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 . + * 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 modelNames; + + JButton calculate; + + private JInternalFrame frame; + + private JCheckBox includeGaps; + + private JCheckBox matchGaps; + + private JCheckBox includeGappedColumns; + + private JCheckBox shorterSequence; + + final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer(); + + List tips = new ArrayList(); + + /** + * 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 buildModelOptionsList() + { + final JComboBox scoreModelsCombo = new JComboBox(); + 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 comboBox, + List toolTips) + { + Object curSel = comboBox.getSelectedItem(); + toolTips.clear(); + DefaultComboBoxModel model = new DefaultComboBoxModel(); + + /* + * 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) + { + } + } +} diff --git a/src/jalview/gui/ChimeraViewFrame.java b/src/jalview/gui/ChimeraViewFrame.java index bc247ca..eadc2ad 100644 --- a/src/jalview/gui/ChimeraViewFrame.java +++ b/src/jalview/gui/ChimeraViewFrame.java @@ -20,62 +20,41 @@ */ 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(); - } - if (_alignwith == null) - { - _alignwith = new Vector(); - } - - // 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 getViewersFor(AlignmentPanel ap) + { + List result = new ArrayList(); + 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 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("
    ") > -1 || data.indexOf("
    ") > -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(); - } - 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 index 0000000..b2b9574 --- /dev/null +++ b/src/jalview/gui/ColourMenuHelper.java @@ -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. + *
      + *
    • None
    • + *
    • Clustal
    • + *
    • ...other 'built-in' colours
    • + *
    • ...any user-defined colours
    • + *
    • User Defined..(only for AlignFrame menu)
    • + *
    + * + * @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 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 index 0000000..b776757 --- /dev/null +++ b/src/jalview/gui/ComboBoxTooltipRenderer.java @@ -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 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 tips) + { + this.tooltips = tips; + } +} diff --git a/src/jalview/gui/Console.java b/src/jalview/gui/Console.java index 280b4c0..de7574c 100644 --- a/src/jalview/gui/Console.java +++ b/src/jalview/gui/Console.java @@ -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); diff --git a/src/jalview/gui/CutAndPasteTransfer.java b/src/jalview/gui/CutAndPasteTransfer.java index 7a0b0af..3eced2f 100644 --- a/src/jalview/gui/CutAndPasteTransfer.java +++ b/src/jalview/gui/CutAndPasteTransfer.java @@ -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) diff --git a/src/jalview/gui/DasSourceBrowser.java b/src/jalview/gui/DasSourceBrowser.java index 8c8f228..c5ec067 100644 --- a/src/jalview/gui/DasSourceBrowser.java +++ b/src/jalview/gui/DasSourceBrowser.java @@ -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); diff --git a/src/jalview/gui/Desktop.java b/src/jalview/gui/Desktop.java index 4ce42dc..d076ba0 100644 --- a/src/jalview/gui/Desktop.java +++ b/src/jalview/gui/Desktop.java @@ -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 links = Preferences.sequenceURLLinks; + List 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)); + } } diff --git a/src/jalview/gui/FeatureRenderer.java b/src/jalview/gui/FeatureRenderer.java index ae911ed..ac56590 100644 --- a/src/jalview/gui/FeatureRenderer.java +++ b/src/jalview/gui/FeatureRenderer.java @@ -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 + *
      + *
    • double-click on a sequence - Amend/Delete features at position
    • + *
    • Create sequence feature from pop-up menu on selected region
    • + *
    • Create features for pattern matches from Find
    • + *
    + * 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 sequences, + final List 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 overlaps = new JComboBox(); + 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 diff --git a/src/jalview/gui/FeatureSettings.java b/src/jalview/gui/FeatureSettings.java index bb5f13c..45b6b0d 100644 --- a/src/jalview/gui/FeatureSettings.java +++ b/src/jalview/gui/FeatureSettings.java @@ -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 typeWidth = null; @Override synchronized public void discoverAllFeatureData() { - Vector allFeatures = new Vector(); - Vector allGroups = new Vector(); - 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 allGroups = new HashSet(); + AlignmentI alignment = af.getViewport().getAlignment(); - int index = 0; - while (index < tmpfeatures.length) + for (int i = 0; i < alignment.getHeight(); i++) + { + SequenceI seq = alignment.getSequenceAt(i); + for (String group : seq.getFeatures().getFeatureGroups(true)) { - if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0) - { - index++; - continue; - } - - if (tmpfeatures[index].getFeatureGroup() != null) - { - group = tmpfeatures[index].featureGroup; - if (!allGroups.contains(group)) - { - allGroups.addElement(group); - checkGroupState(group); - } - } - - if (!allFeatures.contains(tmpfeatures[index].getType())) + if (group != null && !allGroups.contains(group)) { - allFeatures.addElement(tmpfeatures[index].getType()); + allGroups.add(group); + checkGroupState(group); } - index++; } } @@ -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(); // TODO: change avWidth calculation to 'per-sequence' average and use long // rather than float - float[] avWidth = null; - SequenceFeature[] tmpfeatures; - String group = null, type; - Vector visibleChecks = new Vector(); - - // Find out which features should be visible depending on which groups - // are selected / deselected - // and recompute average width ordering + + Set displayableTypes = new HashSet(); + Set foundGroups = new HashSet(); + + /* + * determine which feature types may be visible depending on + * which groups are selected, and recompute average width data + */ for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++) { - tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i) - .getSequenceFeatures(); - if (tmpfeatures == null) - { - continue; - } + SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i); - int index = 0; - while (index < tmpfeatures.length) + /* + * get the sequence's groups for positional features + * and keep track of which groups are visible + */ + Set groups = seq.getFeatures().getFeatureGroups(true); + Set visibleGroups = new HashSet(); + 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 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 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 + *
      + *
    • a new feature type added (and made visible)
    • + *
    • a feature colour changed (in the Amend Features dialog)
    • + *
    + * + * @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 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() diff --git a/src/jalview/gui/Finder.java b/src/jalview/gui/Finder.java index af23ceb..5c917ae 100755 --- a/src/jalview/gui/Finder.java +++ b/src/jalview/gui/Finder.java @@ -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 seqs = new ArrayList(); + List features = new ArrayList(); + + 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(); + } + } } diff --git a/src/jalview/gui/FontChooser.java b/src/jalview/gui/FontChooser.java index 8220aea..06f29e9 100755 --- a/src/jalview/gui/FontChooser.java +++ b/src/jalview/gui/FontChooser.java @@ -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(); } } diff --git a/src/jalview/gui/IdCanvas.java b/src/jalview/gui/IdCanvas.java index 37be8bc..5ce36cb 100755 --- a/src/jalview/gui/IdCanvas.java +++ b/src/jalview/gui/IdCanvas.java @@ -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()); + } + } } diff --git a/src/jalview/gui/IdPanel.java b/src/jalview/gui/IdPanel.java index 59d12d9..29bb522 100755 --- a/src/jalview/gui/IdPanel.java +++ b/src/jalview/gui/IdPanel.java @@ -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 nlinks = new Vector(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 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) diff --git a/src/jalview/gui/JDatabaseTree.java b/src/jalview/gui/JDatabaseTree.java index 2a3d788..d92f6c0 100644 --- a/src/jalview/gui/JDatabaseTree.java +++ b/src/jalview/gui/JDatabaseTree.java @@ -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); diff --git a/src/jalview/gui/Jalview2XML.java b/src/jalview/gui/Jalview2XML.java index 35db33f..358cca6 100644 --- a/src/jalview/gui/Jalview2XML.java +++ b/src/jalview/gui/Jalview2XML.java @@ -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 userColours = new ArrayList(); 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 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 keys = sf.otherDetails.keySet().iterator(); + while (keys.hasNext()) { - String key; - Iterator 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 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 seqs = new Vector(); @@ -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; diff --git a/src/jalview/gui/Jalview2XML_V1.java b/src/jalview/gui/Jalview2XML_V1.java index 52f61b1..614efd2 100755 --- a/src/jalview/gui/Jalview2XML_V1.java +++ b/src/jalview/gui/Jalview2XML_V1.java @@ -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()); diff --git a/src/jalview/gui/JalviewChimeraBindingModel.java b/src/jalview/gui/JalviewChimeraBindingModel.java index bd890d9..c9b35d8 100644 --- a/src/jalview/gui/JalviewChimeraBindingModel.java +++ b/src/jalview/gui/JalviewChimeraBindingModel.java @@ -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); } } diff --git a/src/jalview/gui/JalviewDialog.java b/src/jalview/gui/JalviewDialog.java index 8742253..05f5ffc 100644 --- a/src/jalview/gui/JalviewDialog.java +++ b/src/jalview/gui/JalviewDialog.java @@ -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 index 0000000..6f9fbbf --- /dev/null +++ b/src/jalview/gui/OverviewCanvas.java @@ -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 . + * 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); + } + +} diff --git a/src/jalview/gui/OverviewPanel.java b/src/jalview/gui/OverviewPanel.java index 1c48690..3fa674e 100755 --- a/src/jalview/gui/OverviewPanel.java +++ b/src/jalview/gui/OverviewPanel.java @@ -20,119 +20,84 @@ */ 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(); } } diff --git a/src/jalview/gui/PCAPanel.java b/src/jalview/gui/PCAPanel.java index aa01bf5..d8e6b06 100644 --- a/src/jalview/gui/PCAPanel.java +++ b/src/jalview/gui/PCAPanel.java @@ -20,20 +20,23 @@ */ 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 diff --git a/src/jalview/gui/PopupMenu.java b/src/jalview/gui/PopupMenu.java index e36204c..a8e2f7e 100644 --- a/src/jalview/gui/PopupMenu.java +++ b/src/jalview/gui/PopupMenu.java @@ -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 tipEntries = new TreeMap(); + SortedMap tipEntries = new TreeMap(); final Map> candidates = new LinkedHashMap>(); 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 seqs = new ArrayList(); + List features = new ArrayList(); + /* + * 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(); + } + } diff --git a/src/jalview/gui/Preferences.java b/src/jalview/gui/Preferences.java index 422745a..cccdd2e 100755 --- a/src/jalview/gui/Preferences.java +++ b/src/jalview/gui/Preferences.java @@ -20,11 +20,6 @@ */ 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 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 groupURLLinks; static { - String string = Cache.getDefault("SEQUENCE_LINKS", EMBLEBI_STRING); - sequenceURLLinks = new Vector(); - - 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(); } - Vector 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(); - urlLinks = new Vector(); - for (int i = 0; i < sequenceURLLinks.size(); i++) + + // set up sorting + linkUrlTable.setModel(dataModel); + final TableRowSorter sorter = new TableRowSorter<>( + linkUrlTable.getModel()); + linkUrlTable.setRowSorter(sorter); + List 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 customUrlFilter = new RowFilter() + { + @Override + public boolean include( + Entry entry) + { + return ((UrlLinkTableModel) entry.getModel()).isUserEntry(entry); + } + }; + + final TableRowSorter 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(); - 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); + } + } +} } diff --git a/src/jalview/gui/RedundancyPanel.java b/src/jalview/gui/RedundancyPanel.java index a9d2690..cbbcf70 100755 --- a/src/jalview/gui/RedundancyPanel.java +++ b/src/jalview/gui/RedundancyPanel.java @@ -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() + ""); - } - } - } diff --git a/src/jalview/gui/ScalePanel.java b/src/jalview/gui/ScalePanel.java index 00d465a..cb19539 100755 --- a/src/jalview/gui/ScalePanel.java +++ b/src/jalview/gui/ScalePanel.java @@ -21,12 +21,14 @@ 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(); + } + } diff --git a/src/jalview/gui/SeqCanvas.java b/src/jalview/gui/SeqCanvas.java index d015292..0e5e1b8 100755 --- a/src/jalview/gui/SeqCanvas.java +++ b/src/jalview/gui/SeqCanvas.java @@ -21,11 +21,14 @@ 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 regions = av.getColumnSelection().getHiddenColumns(); + List 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()); + } + } + } } diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index 5b87445..5634ead 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -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 @@ -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 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) // @@ -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 + * + *
    +   * 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)
    +   * 
    + * + * @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 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 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 allFeatures = ap.getFeatureRenderer() - .findFeaturesAtRes(sequence.getDatasetSequence(), - sequence.findPosition(res)); - List links = new ArrayList(); + .findFeaturesAtColumn(sequence, column + 1); + List 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()); diff --git a/src/jalview/gui/SequenceFetcher.java b/src/jalview/gui/SequenceFetcher.java index 8e1d549..da026b5 100755 --- a/src/jalview/gui/SequenceFetcher.java +++ b/src/jalview/gui/SequenceFetcher.java @@ -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; } - } } diff --git a/src/jalview/gui/SequenceRenderer.java b/src/jalview/gui/SequenceRenderer.java index 64fe053..36825ea 100755 --- a/src/jalview/gui/SequenceRenderer.java +++ b/src/jalview/gui/SequenceRenderer.java @@ -20,10 +20,12 @@ */ 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() diff --git a/src/jalview/gui/SliderPanel.java b/src/jalview/gui/SliderPanel.java index a381e8b..0c4e03e 100755 --- a/src/jalview/gui/SliderPanel.java +++ b/src/jalview/gui/SliderPanel.java @@ -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 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; + } } diff --git a/src/jalview/gui/SplitFrame.java b/src/jalview/gui/SplitFrame.java index 6c849c3..1d929e6 100644 --- a/src/jalview/gui/SplitFrame.java +++ b/src/jalview/gui/SplitFrame.java @@ -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()); } } diff --git a/src/jalview/gui/StructureChooser.java b/src/jalview/gui/StructureChooser.java index ee22ae4..3e516a6 100644 --- a/src/jalview/gui/StructureChooser.java +++ b/src/jalview/gui/StructureChooser.java @@ -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); } diff --git a/src/jalview/gui/StructureViewerBase.java b/src/jalview/gui/StructureViewerBase.java index 19291ac..e73ce07 100644 --- a/src/jalview/gui/StructureViewerBase.java +++ b/src/jalview/gui/StructureViewerBase.java @@ -20,28 +20,47 @@ */ 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 getViewersFor(AlignmentPanel alp){ - - List result = new ArrayList(); - 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 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(); + } + if (_alignwith == null) + { + _alignwith = new Vector(); + } + + 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(); + } + 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("
    ") > -1 || data.indexOf("
    ") > -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); + } + } } diff --git a/src/jalview/gui/TextColourChooser.java b/src/jalview/gui/TextColourChooser.java index 49fdaf7..91e05c6 100644 --- a/src/jalview/gui/TextColourChooser.java +++ b/src/jalview/gui/TextColourChooser.java @@ -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 groupColour1; + + Map groupColour2; + + Map 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(); + groupColour2 = new HashMap(); + groupThreshold = new HashMap(); + + 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(); } } diff --git a/src/jalview/gui/TreeCanvas.java b/src/jalview/gui/TreeCanvas.java index 84fd82f..e60ac8e 100755 --- a/src/jalview/gui/TreeCanvas.java +++ b/src/jalview/gui/TreeCanvas.java @@ -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 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 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 l = tree.findLeaves(tree.getGroups() - .elementAt(i)); + Vector l = tree.findLeaves(groups.get(i)); Vector sequences = new Vector(); @@ -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()); diff --git a/src/jalview/gui/TreePanel.java b/src/jalview/gui/TreePanel.java index 6fa4493..32c5702 100755 --- a/src/jalview/gui/TreePanel.java +++ b/src/jalview/gui/TreePanel.java @@ -21,9 +21,13 @@ 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) evt.getNewValue()); + tree.updatePlaceHolders((List) 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 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 + *

    + * Neighbour Joining Using BLOSUM62 + *

    + * 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 Using + */ + final String ttl = MessageManager.formatMessage("label.treecalc_title", + treecalcnm, smn); + return ttl; + } } diff --git a/src/jalview/gui/UserDefinedColours.java b/src/jalview/gui/UserDefinedColours.java index 0df23e0..f75a0a3 100755 --- a/src/jalview/gui/UserDefinedColours.java +++ b/src/jalview/gui/UserDefinedColours.java @@ -20,31 +20,32 @@ */ 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 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 upperCaseButtons; + List upperCaseButtons; - ArrayList lowerCaseButtons; + List 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(); } 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 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 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 caseSensitiveButtons, int buttonIndex) + JButton makeButton(String label, String residue, List 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. + *

      + *
    • Open a file chooser to browse for files with extension .jc
    • + *
    • Load in the colour scheme and transfer it to this panel's buttons
    • + *
    • Register the loaded colour scheme
    • + *
    */ @Override - protected void loadbutton_actionPerformed(ActionEvent e) + protected void loadbutton_actionPerformed() { upperCaseButtons = new ArrayList(); lowerCaseButtons = new ArrayList(); - 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. + *
      + *
    • Check a name has been entered
    • + *
    • Warn if the name already exists, remove any existing scheme of the same + * name if overwriting
    • + *
    • Do the standard file chooser thing to write with extension .jc
    • + *
    • 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
    • + *
    • 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
    • + *
    + * 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(); - } - else - { - selectedButtons.clear(); - } - selectedButtons.add(lcaseColour); + boolean selected = caseSensitive.isSelected(); + resetButtonPanel(selected); + lcaseColour.setEnabled(selected); } } diff --git a/src/jalview/gui/VamsasApplication.java b/src/jalview/gui/VamsasApplication.java index 75ddba5..d58cb5a 100644 --- a/src/jalview/gui/VamsasApplication.java +++ b/src/jalview/gui/VamsasApplication.java @@ -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) { diff --git a/src/jalview/gui/WsParamSetManager.java b/src/jalview/gui/WsParamSetManager.java index b260e1b..1aa0803 100644 --- a/src/jalview/gui/WsParamSetManager.java +++ b/src/jalview/gui/WsParamSetManager.java @@ -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 diff --git a/src/jalview/gui/WsPreferences.java b/src/jalview/gui/WsPreferences.java index 165e8f2..32671d5 100644 --- a/src/jalview/gui/WsPreferences.java +++ b/src/jalview/gui/WsPreferences.java @@ -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); diff --git a/src/jalview/io/AnnotationFile.java b/src/jalview/io/AnnotationFile.java index 82e71b5..c3e71da 100755 --- a/src/jalview/io/AnnotationFile.java +++ b/src/jalview/io/AnnotationFile.java @@ -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 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 hc = cs.getHiddenColumns(); + List hc = cs.getHiddenRegions(); boolean comma = false; for (int[] r : hc) { @@ -535,7 +537,7 @@ public class AnnotationFile return false; } - public void printGroups(List list) + protected void printGroups(List 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); } diff --git a/src/jalview/io/AppletFormatAdapter.java b/src/jalview/io/AppletFormatAdapter.java index c5a80e3..907ff46 100755 --- a/src/jalview/io/AppletFormatAdapter.java +++ b/src/jalview/io/AppletFormatAdapter.java @@ -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; } diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index 6af0cdf..e1fccaf 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -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 SORT_NULL_LAST = new Comparator() + { + @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 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 visible, boolean visOnly, - boolean nonpos) + Map visible, + List 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 en = visible.keySet().iterator(); - while (en.hasNext()) + for (Entry 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 groups = new ArrayList(); - 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 sortedGroups = new ArrayList(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 features = new ArrayList(); + 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("") == -1) - { - out.append(""); - } - - 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(" " + label - + ""); - } - } - - if (sequenceFeature.getDescription().indexOf("") == -1) - { - out.append(""); - } - } - - 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("") == -1) { - break; + out.append(""); } - } 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(" " + label + ""); + } + } - if (!featuresGen) + if (sequenceFeature.getDescription().indexOf("") == -1) + { + out.append(""); + } + } + + 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 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 visible, boolean outputVisibleOnly, + Map visible, + List 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 features = new ArrayList(); + if (includeNonPositionalFeatures) { - for (SequenceFeature sf : features) - { - isnonpos = sf.begin == 0 && sf.end == 0; - if (!includeNonPositionalFeatures && isnonpos) - { - /* - * ignore non-positional features if not wanted - */ - continue; - } - // TODO why the test !isnonpos here? - // what about not visible non-positional features? - if (!isnonpos && outputVisibleOnly - && !visible.containsKey(sf.type)) - { - /* - * ignore not visible features if not wanted - */ - continue; - } + features.addAll(seq.getFeatures().getNonPositionalFeatures()); + } + if (visible != null && !visible.isEmpty()) + { + features.addAll(seq.getFeatures().getPositionalFeatures(types)); + } - source = sf.featureGroup; - if (source == null) - { - source = sf.getDescription(); - } + for (SequenceFeature sf : features) + { + String source = sf.featureGroup; + if (!sf.isNonPositional() && source != null + && !visibleFeatureGroups.contains(source)) + { + // group is not visible + continue; + } - out.append(seq.getName()); - out.append(TAB); - out.append(source); - out.append(TAB); - out.append(sf.type); - out.append(TAB); - out.append(sf.begin); - out.append(TAB); - out.append(sf.end); - out.append(TAB); - out.append(sf.score); - out.append(TAB); - - int strand = sf.getStrand(); - out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); - out.append(TAB); - - String phase = sf.getPhase(); - out.append(phase == null ? "." : phase); - - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) - { - out.append(TAB).append(attributes); - } + if (source == null) + { + source = sf.getDescription(); + } - out.append(newline); + out.append(seq.getName()); + out.append(TAB); + out.append(source); + out.append(TAB); + out.append(sf.type); + out.append(TAB); + out.append(sf.begin); + out.append(TAB); + out.append(sf.end); + out.append(TAB); + out.append(sf.score); + out.append(TAB); + + int strand = sf.getStrand(); + out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); + out.append(TAB); + + String phase = sf.getPhase(); + out.append(phase == null ? "." : phase); + + // miscellaneous key-values (GFF column 9) + String attributes = sf.getAttributes(); + if (attributes != null) + { + out.append(TAB).append(attributes); } + + out.append(newline); } } @@ -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 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); diff --git a/src/jalview/io/FileFormat.java b/src/jalview/io/FileFormat.java index a11147c..3354b88 100644 --- a/src/jalview/io/FileFormat.java +++ b/src/jalview/io/FileFormat.java @@ -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 diff --git a/src/jalview/io/FileFormats.java b/src/jalview/io/FileFormats.java index 158489e..19a61cf 100644 --- a/src/jalview/io/FileFormats.java +++ b/src/jalview/io/FileFormats.java @@ -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, diff --git a/src/jalview/io/FileLoader.java b/src/jalview/io/FileLoader.java index 4f83ab1..9d5fd93 100755 --- a/src/jalview/io/FileLoader.java +++ b/src/jalview/io/FileLoader.java @@ -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) * diff --git a/src/jalview/io/FormatAdapter.java b/src/jalview/io/FormatAdapter.java index d9dd79d..f09e8a0 100755 --- a/src/jalview/io/FormatAdapter.java +++ b/src/jalview/io/FormatAdapter.java @@ -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); } diff --git a/src/jalview/io/HTMLOutput.java b/src/jalview/io/HTMLOutput.java index b0ca25b..77006db 100755 --- a/src/jalview/io/HTMLOutput.java +++ b/src/jalview/io/HTMLOutput.java @@ -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 diff --git a/src/jalview/io/HtmlFile.java b/src/jalview/io/HtmlFile.java index af3fb5d..9256278 100644 --- a/src/jalview/io/HtmlFile.java +++ b/src/jalview/io/HtmlFile.java @@ -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 diff --git a/src/jalview/io/IdentifyFile.java b/src/jalview/io/IdentifyFile.java index 0556e76..be0df21 100755 --- a/src/jalview/io/IdentifyFile.java +++ b/src/jalview/io/IdentifyFile.java @@ -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; diff --git a/src/jalview/io/JSONFile.java b/src/jalview/io/JSONFile.java index 7a12076..14574d0 100644 --- a/src/jalview/io/JSONFile.java +++ b/src/jalview/io/JSONFile.java @@ -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 hiddenColumns; - - private ColumnSelection columnSelection; + private HiddenColumns hiddenColumns; private List 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 hiddenCols = getViewport().getColumnSelection() - .getHiddenColumns(); + List 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 sequenceFeatureToJsonPojo( - SequenceI[] sqs, FeatureRenderer fr) + protected List sequenceFeatureToJsonPojo( + SequenceI[] sqs) { displayedFeatures = (fr == null) ? null : fr.getFeaturesDisplayed(); List sequenceFeaturesPojo = new ArrayList(); @@ -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 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 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 diff --git a/src/jalview/io/JalviewFileChooser.java b/src/jalview/io/JalviewFileChooser.java index 2a0f8b1..98cd162 100755 --- a/src/jalview/io/JalviewFileChooser.java +++ b/src/jalview/io/JalviewFileChooser.java @@ -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 index 0000000..6b2f891 --- /dev/null +++ b/src/jalview/io/ScoreMatrixFile.java @@ -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 + *

    + * 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. + * + *

    + * ScoreMatrix BLOSUM62
    + * 
    + * + * Also accepts 'AAindex' format (as described at + * http://www.genome.jp/aaindex/aaindex_help.html) with the minimum data + * required being + * + *
    + * H accession number (used as score matrix identifier in Jalview)
    + * D description (used for tooltip in Jalview)
    + * M rows = symbolList
    + * and the substitution scores
    + * 
    + */ +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 + * 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 ', 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 ' 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 + * + *
    +   * M rows = ARNDCQEGHILKMFPSTWYV, cols = ARNDCQEGHILKMFPSTWYV
    +   * 
    + * + * 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; + } +} diff --git a/src/jalview/io/SequenceAnnotationReport.java b/src/jalview/io/SequenceAnnotationReport.java index 6c8f40f..c3b076c 100644 --- a/src/jalview/io/SequenceAnnotationReport.java +++ b/src/jalview/io/SequenceAnnotationReport.java @@ -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 comparator = new Comparator() { @@ -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("
    "); + 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("
    "); - } - 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("
    ").append(ELLIPSIS).append(COMMA).append(source) - .append(COMMA).append(ELLIPSIS); + lineLength = 0; + countForSource = 0; + sourceCount++; } - if (ellipsis) + if (sourceCount > MAX_SOURCES && summary) { - sb.append("
    ("); - 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("
    "); + } + 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(""); - return maxWidth; + if (moreSources) + { + sb.append("
    ").append(source) + .append(COMMA).append(ELLIPSIS); + } + if (ellipsis) + { + sb.append("
    ("); + sb.append(MessageManager.getString("label.output_seq_details")); + sb.append(")"); + } + + return maxLineLength; } public void createTooltipAnnotationReport(final StringBuilder tip, diff --git a/src/jalview/io/StockholmFile.java b/src/jalview/io/StockholmFile.java index 2061f29..936d2b9 100644 --- a/src/jalview/io/StockholmFile.java +++ b/src/jalview/io/StockholmFile.java @@ -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; diff --git a/src/jalview/io/StructureFile.java b/src/jalview/io/StructureFile.java index 7fe17c8..ab220f0 100644 --- a/src/jalview/io/StructureFile.java +++ b/src/jalview/io/StructureFile.java @@ -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; } diff --git a/src/jalview/io/TCoffeeScoreFile.java b/src/jalview/io/TCoffeeScoreFile.java index c3ec951..648954f 100644 --- a/src/jalview/io/TCoffeeScoreFile.java +++ b/src/jalview/io/TCoffeeScoreFile.java @@ -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. *

    - * This file is procuded by t_coffee providing the option + * This file is produced by t_coffee providing the option * -output=score_ascii 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 diff --git a/src/jalview/io/VamsasAppDatastore.java b/src/jalview/io/VamsasAppDatastore.java index 2c35547..1cab8ca 100644 --- a/src/jalview/io/VamsasAppDatastore.java +++ b/src/jalview/io/VamsasAppDatastore.java @@ -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 index 0000000..091d30e --- /dev/null +++ b/src/jalview/io/cache/AppCache.java @@ -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> cacheItems; + + private AppCache() + { + cacheItems = new Hashtable>(); + } + + /** + * Method to obtain all the cache items for a given cache key + * + * @param cacheKey + * @return + */ + public LinkedHashSet getAllCachedItemsFor(String cacheKey) + { + LinkedHashSet foundCache = cacheItems.get(cacheKey); + if (foundCache == null) + { + foundCache = new LinkedHashSet(); + 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 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()); + 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 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 index 0000000..444670b --- /dev/null +++ b/src/jalview/io/cache/JvCacheableInputBox.java @@ -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 extends JComboBox +{ + + 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 persistedCacheItems = Arrays.asList(delimitedCacheStr + .split(AppCache.CACHE_DELIMITER)); + + LinkedHashSet foundCacheItems = appCache + .getAllCachedItemsFor(cacheKey); + if (foundCacheItems == null) + { + foundCacheItems = new LinkedHashSet(); + } + // 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 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 cacheItems = appCache.getAllCachedItemsFor(cacheKey); + List reversedCacheItems = new ArrayList(); + reversedCacheItems.addAll(cacheItems); + cacheItems = null; + Collections.reverse(reversedCacheItems); + if (lastSearch.isEmpty()) + { + addItem(""); + } + + if (reversedCacheItems != null && !reversedCacheItems.isEmpty()) + { + LinkedHashSet 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(); + } + +} diff --git a/src/jalview/io/gff/ExonerateHelper.java b/src/jalview/io/gff/ExonerateHelper.java index eb74eea..873fd27 100644 --- a/src/jalview/io/gff/ExonerateHelper.java +++ b/src/jalview/io/gff/ExonerateHelper.java @@ -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> set) { - SequenceFeature sf = super.buildSequenceFeature(gff, set); - sf.setFeatureGroup("exonerate"); + SequenceFeature sf = super.buildSequenceFeature(gff, TYPE_COL, + "exonerate", set); return sf; } diff --git a/src/jalview/io/gff/Gff3Helper.java b/src/jalview/io/gff/Gff3Helper.java index 82e5313..28941f5 100644 --- a/src/jalview/io/gff/Gff3Helper.java +++ b/src/jalview/io/gff/Gff3Helper.java @@ -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> attributes) { - SequenceFeature sf = super.buildSequenceFeature(gff, attributes); + SequenceFeature sf = super.buildSequenceFeature(gff, typeColumn, group, + attributes); String desc = getDescription(sf, attributes); if (desc != null) { diff --git a/src/jalview/io/gff/GffHelperBase.java b/src/jalview/io/gff/GffHelperBase.java index 48c33e5..41f141b 100644 --- a/src/jalview/io/gff/GffHelperBase.java +++ b/src/jalview/io/gff/GffHelperBase.java @@ -337,6 +337,19 @@ public abstract class GffHelperBase implements GffHelperI protected SequenceFeature buildSequenceFeature(String[] gff, Map> 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> 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]); diff --git a/src/jalview/io/gff/InterProScanHelper.java b/src/jalview/io/gff/InterProScanHelper.java index e1334e1..0aa3b74 100644 --- a/src/jalview/io/gff/InterProScanHelper.java +++ b/src/jalview/io/gff/InterProScanHelper.java @@ -73,13 +73,19 @@ public class InterProScanHelper extends Gff3Helper } /** - * - */ + * An override that + *

      + *
    • uses Source (column 2) as feature type instead of the default column 3
    • + *
    • sets "InterProScan" as the feature group
    • + *
    • extracts "signature_desc" attribute as the feature description
    • + *
    + */ @Override protected SequenceFeature buildSequenceFeature(String[] gff, Map> 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; } diff --git a/src/jalview/io/packed/JalviewDataset.java b/src/jalview/io/packed/JalviewDataset.java index c1ca1b7..9f84c16 100644 --- a/src/jalview/io/packed/JalviewDataset.java +++ b/src/jalview/io/packed/JalviewDataset.java @@ -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(); diff --git a/src/jalview/io/vamsas/Datasetsequence.java b/src/jalview/io/vamsas/Datasetsequence.java index 9db7a8e..e1340e2 100644 --- a/src/jalview/io/vamsas/Datasetsequence.java +++ b/src/jalview/io/vamsas/Datasetsequence.java @@ -21,9 +21,12 @@ 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 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; diff --git a/src/jalview/io/vamsas/Sequencefeature.java b/src/jalview/io/vamsas/Sequencefeature.java index 61491b2..363f6f1 100644 --- a/src/jalview/io/vamsas/Sequencefeature.java +++ b/src/jalview/io/vamsas/Sequencefeature.java @@ -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()); } diff --git a/src/jalview/io/vamsas/Tree.java b/src/jalview/io/vamsas/Tree.java index a3781a7..d800d20 100644 --- a/src/jalview/io/vamsas/Tree.java +++ b/src/jalview/io/vamsas/Tree.java @@ -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 leaves = ntree.findLeaves(ntree.getTopNode()); + Vector 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)); diff --git a/src/jalview/javascript/JsSelectionSender.java b/src/jalview/javascript/JsSelectionSender.java index dc08b59..fdf8b58 100644 --- a/src/jalview/javascript/JsSelectionSender.java +++ b/src/jalview/javascript/JsSelectionSender.java @@ -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 diff --git a/src/jalview/javascript/MouseOverStructureListener.java b/src/jalview/javascript/MouseOverStructureListener.java index 4f833bc..c390b17 100644 --- a/src/jalview/javascript/MouseOverStructureListener.java +++ b/src/jalview/javascript/MouseOverStructureListener.java @@ -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 ccomands = new ArrayList(); ArrayList pdbfn = new ArrayList(); 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; diff --git a/src/jalview/jbgui/GAlignFrame.java b/src/jalview/jbgui/GAlignFrame.java index dc17397..88cc0a8 100755 --- a/src/jalview/jbgui/GAlignFrame.java +++ b/src/jalview/jbgui/GAlignFrame.java @@ -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("" + MessageManager.getString("label.rename_tab_eXpand_reGroup") + ""); - 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. - *

    - * 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() { } diff --git a/src/jalview/jbgui/GDesktop.java b/src/jalview/jbgui/GDesktop.java index 63ecdaf..3e3691c 100755 --- a/src/jalview/jbgui/GDesktop.java +++ b/src/jalview/jbgui/GDesktop.java @@ -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 diff --git a/src/jalview/jbgui/GFinder.java b/src/jalview/jbgui/GFinder.java index fef4568..c335b33 100755 --- a/src/jalview/jbgui/GFinder.java +++ b/src/jalview/jbgui/GFinder.java @@ -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 searchBox = new JvCacheableInputBox(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; + } + + + } diff --git a/src/jalview/jbgui/GFontChooser.java b/src/jalview/jbgui/GFontChooser.java index 8c893a2..5c15e80 100755 --- a/src/jalview/jbgui/GFontChooser.java +++ b/src/jalview/jbgui/GFontChooser.java @@ -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() + { } } diff --git a/src/jalview/jbgui/GPCAPanel.java b/src/jalview/jbgui/GPCAPanel.java index 0bc6cac..3715acc 100755 --- a/src/jalview/jbgui/GPCAPanel.java +++ b/src/jalview/jbgui/GPCAPanel.java @@ -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 xCombobox = new JComboBox(); - JLabel jLabel2 = new JLabel(); + protected JComboBox yCombobox = new JComboBox(); - JLabel jLabel3 = new JLabel(); + protected JComboBox zCombobox = new JComboBox(); - 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 - - } } diff --git a/src/jalview/jbgui/GPreferences.java b/src/jalview/jbgui/GPreferences.java index 90053f5..1ad95dd 100755 --- a/src/jalview/jbgui/GPreferences.java +++ b/src/jalview/jbgui/GPreferences.java @@ -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 fontNameCB = new JComboBox(); + 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(); + } + + } } diff --git a/src/jalview/jbgui/GSequenceLink.java b/src/jalview/jbgui/GSequenceLink.java index dbce5f3..ab3ea2c 100755 --- a/src/jalview/jbgui/GSequenceLink.java +++ b/src/jalview/jbgui/GSequenceLink.java @@ -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); + } } diff --git a/src/jalview/jbgui/GSliderPanel.java b/src/jalview/jbgui/GSliderPanel.java index 1362007..0c79c6c 100755 --- a/src/jalview/jbgui/GSliderPanel.java +++ b/src/jalview/jbgui/GSliderPanel.java @@ -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())); + } } /** diff --git a/src/jalview/jbgui/GStructureChooser.java b/src/jalview/jbgui/GStructureChooser.java index 3a064d2..041fefd 100644 --- a/src/jalview/jbgui/GStructureChooser.java +++ b/src/jalview/jbgui/GStructureChooser.java @@ -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) 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 + { + private ListCellRenderer regent; + + private JPanel separatorPanel = new JPanel(new BorderLayout()); + + private JSeparator jSeparator = new JSeparator(); + + public CustomComboSeparatorsRenderer(ListCellRenderer 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 diff --git a/src/jalview/jbgui/GStructureViewer.java b/src/jalview/jbgui/GStructureViewer.java index bd0f1de..d8f3f61 100644 --- a/src/jalview/jbgui/GStructureViewer.java +++ b/src/jalview/jbgui/GStructureViewer.java @@ -21,13 +21,14 @@ 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; - // } - } diff --git a/src/jalview/jbgui/GUserDefinedColours.java b/src/jalview/jbgui/GUserDefinedColours.java index b5695b2..5384cc0 100755 --- a/src/jalview/jbgui/GUserDefinedColours.java +++ b/src/jalview/jbgui/GUserDefinedColours.java @@ -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 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(); } /** * 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() { } diff --git a/src/jalview/math/Matrix.java b/src/jalview/math/Matrix.java index 28b9d67..b39d3c9 100755 --- a/src/jalview/math/Matrix.java +++ b/src/jalview/math/Matrix.java @@ -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 * *
    -   *   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)
        * 
    * * 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 index 0000000..94b9333 --- /dev/null +++ b/src/jalview/math/MatrixI.java @@ -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. + *

    + * If parameter maxToZero 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. + *

    + * If parameter maxToZero is false, then the values are reflected + * about the average of {min, max} (effectively swapping min and max). This + * operation is 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 index 0000000..72f0963 --- /dev/null +++ b/src/jalview/math/SparseMatrix.java @@ -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. + *

    + * 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()); + } +} diff --git a/src/jalview/renderer/AnnotationRenderer.java b/src/jalview/renderer/AnnotationRenderer.java index a0e530c..518c179 100644 --- a/src/jalview/renderer/AnnotationRenderer.java +++ b/src/jalview/renderer/AnnotationRenderer.java @@ -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 index 0000000..9291ca6 --- /dev/null +++ b/src/jalview/renderer/OverviewRenderer.java @@ -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 . + * 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 index 0000000..b6f7fe6 --- /dev/null +++ b/src/jalview/renderer/ResidueShader.java @@ -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 + *

      + *
    • the colour scheme that provides a colour for each aligned residue
    • + *
    • any threshold for colour, based on percentage identity with consensus
    • + *
    • any graduation based on conservation of physico-chemical properties
    • + *
    + * + * @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 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 + *
      + *
    • 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
    • + *
    • 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
    • + *
    + * + * @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. + *

    + * 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. + *

    + * 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 index 0000000..a914a1a --- /dev/null +++ b/src/jalview/renderer/ResidueShaderI.java @@ -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 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 diff --git a/src/jalview/renderer/ScaleRenderer.java b/src/jalview/renderer/ScaleRenderer.java index 82536d4..9fec256 100644 --- a/src/jalview/renderer/ScaleRenderer.java +++ b/src/jalview/renderer/ScaleRenderer.java @@ -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 index 0000000..ba63834 --- /dev/null +++ b/src/jalview/renderer/seqfeatures/FeatureColourFinder.java @@ -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. + *

    + * 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; + } +} diff --git a/src/jalview/renderer/seqfeatures/FeatureRenderer.java b/src/jalview/renderer/seqfeatures/FeatureRenderer.java index 9e0089f..8f4f139 100644 --- a/src/jalview/renderer/seqfeatures/FeatureRenderer.java +++ b/src/jalview/renderer/seqfeatures/FeatureRenderer.java @@ -21,8 +21,10 @@ 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 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. + *

    + * 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 overlaps = seq.findFeatures(column, column, + type); + for (SequenceFeature sequenceFeature : overlaps) + { + if (!featureGroupNotShown(sequenceFeature)) + { + return getColour(sequenceFeature); + } + } + } + + /* + * no displayed feature found at position + */ + return null; + } } diff --git a/src/jalview/schemes/AnnotationColourGradient.java b/src/jalview/schemes/AnnotationColourGradient.java index 89b8f07..220d3ab 100755 --- a/src/jalview/schemes/AnnotationColourGradient.java +++ b/src/jalview/schemes/AnnotationColourGradient.java @@ -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 seqannot = null; + private IdentityHashMap seqannot = null; @Override - public ColourSchemeI applyTo(AnnotatedCollectionI sg, + public ColourSchemeI getInstance(AnnotatedCollectionI sg, Map 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; + } } diff --git a/src/jalview/schemes/Blosum62ColourScheme.java b/src/jalview/schemes/Blosum62ColourScheme.java index c47f171..70f4910 100755 --- a/src/jalview/schemes/Blosum62ColourScheme.java +++ b/src/jalview/schemes/Blosum62ColourScheme.java @@ -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 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 hiddenRepSequences) + public boolean isSimple() { - ColourSchemeI newcs = super.applyTo(sg, hiddenRepSequences); - return newcs; + return false; } } diff --git a/src/jalview/schemes/BuriedColourScheme.java b/src/jalview/schemes/BuriedColourScheme.java index 3c68da0..a3b85b9 100755 --- a/src/jalview/schemes/BuriedColourScheme.java +++ b/src/jalview/schemes/BuriedColourScheme.java @@ -20,7 +20,12 @@ */ 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 hrs) + { + return new BuriedColourScheme(); + } } diff --git a/src/jalview/schemes/ClustalxColourScheme.java b/src/jalview/schemes/ClustalxColourScheme.java index ca4316f..f13a90c 100755 --- a/src/jalview/schemes/ClustalxColourScheme.java +++ b/src/jalview/schemes/ClustalxColourScheme.java @@ -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 colhash = new HashMap(); + 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 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 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; } } diff --git a/src/jalview/schemes/ColourSchemeI.java b/src/jalview/schemes/ColourSchemeI.java index da99a4a..f16ca21 100755 --- a/src/jalview/schemes/ColourSchemeI.java +++ b/src/jalview/schemes/ColourSchemeI.java @@ -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 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 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 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 hiddenRepSequences); - + boolean isSimple(); } diff --git a/src/jalview/schemes/ColourSchemeLoader.java b/src/jalview/schemes/ColourSchemeLoader.java new file mode 100644 index 0000000..8660f3e --- /dev/null +++ b/src/jalview/schemes/ColourSchemeLoader.java @@ -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; + } + +} diff --git a/src/jalview/schemes/ColourSchemeProperty.java b/src/jalview/schemes/ColourSchemeProperty.java index 0d9c39d..3e8e87a 100755 --- a/src/jalview/schemes/ColourSchemeProperty.java +++ b/src/jalview/schemes/ColourSchemeProperty.java @@ -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 + *

      + *
    • any currently registered colour scheme; Jalview by default provides
    • + *
        + *
      • Clustal
      • + *
      • Blosum62
      • + *
      • % Identity
      • + *
      • Hydrophobic
      • + *
      • Zappo
      • + *
      • Taylor
      • + *
      • Helix Propensity
      • + *
      • Strand Propensity
      • + *
      • Turn Propensity
      • + *
      • Buried Index
      • + *
      • Nucleotide
      • + *
      • Purine/Pyrimidine
      • + *
      • T-Coffee Scores
      • + *
      • RNA Helices
      • + *
      + *
    • the name of a programmatically added colour scheme
    • an AWT + * colour name e.g. red
    • an AWT hex rgb colour e.g. ff2288
    • + * residue colours list e.g. D,E=red;K,R,H=0022FF;c=yellow
    * - * @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 index 0000000..269811b --- /dev/null +++ b/src/jalview/schemes/ColourSchemes.java @@ -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 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(); + + 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 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 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()); + } +} diff --git a/src/jalview/schemes/CovariationColourScheme.java b/src/jalview/schemes/CovariationColourScheme.java index 49d5dee..6cb095b 100644 --- a/src/jalview/schemes/CovariationColourScheme.java +++ b/src/jalview/schemes/CovariationColourScheme.java @@ -21,9 +21,14 @@ 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 helixcolorhash = new Hashtable(); - public Hashtable positionsToHelix = new Hashtable(); + public Map positionsToHelix = new Hashtable(); 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 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; + } } diff --git a/src/jalview/schemes/FeatureColour.java b/src/jalview/schemes/FeatureColour.java index 23087a8..dbe4901 100644 --- a/src/jalview/schemes/FeatureColour.java +++ b/src/jalview/schemes/FeatureColour.java @@ -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()); } diff --git a/src/jalview/schemes/FollowerColourScheme.java b/src/jalview/schemes/FollowerColourScheme.java index 35be31b..57c19e5 100644 --- a/src/jalview/schemes/FollowerColourScheme.java +++ b/src/jalview/schemes/FollowerColourScheme.java @@ -20,8 +20,11 @@ */ 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 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; } } diff --git a/src/jalview/schemes/HelixColourScheme.java b/src/jalview/schemes/HelixColourScheme.java index 1242cd3..7123d93 100755 --- a/src/jalview/schemes/HelixColourScheme.java +++ b/src/jalview/schemes/HelixColourScheme.java @@ -20,7 +20,12 @@ */ 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 hrs) + { + return new HelixColourScheme(); + } } diff --git a/src/jalview/schemes/HydrophobicColourScheme.java b/src/jalview/schemes/HydrophobicColourScheme.java index 055000f..69af3c9 100755 --- a/src/jalview/schemes/HydrophobicColourScheme.java +++ b/src/jalview/schemes/HydrophobicColourScheme.java @@ -20,7 +20,12 @@ */ 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 hrs) + { + return new HydrophobicColourScheme(); + } } diff --git a/src/jalview/schemes/JalviewColourScheme.java b/src/jalview/schemes/JalviewColourScheme.java new file mode 100644 index 0000000..185d2b4 --- /dev/null +++ b/src/jalview/schemes/JalviewColourScheme.java @@ -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 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 cl) + { + name = s; + myClass = cl; + } + + /** + * Returns the class of the colour scheme + * + * @return + */ + public Class 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; + } +} diff --git a/src/jalview/schemes/NucleotideColourScheme.java b/src/jalview/schemes/NucleotideColourScheme.java index dff2bf3..abae733 100755 --- a/src/jalview/schemes/NucleotideColourScheme.java +++ b/src/jalview/schemes/NucleotideColourScheme.java @@ -20,9 +20,11 @@ */ 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 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(); } } diff --git a/src/jalview/schemes/PIDColourScheme.java b/src/jalview/schemes/PIDColourScheme.java index 0ad5b5c..657d6b0 100755 --- a/src/jalview/schemes/PIDColourScheme.java +++ b/src/jalview/schemes/PIDColourScheme.java @@ -20,78 +20,88 @@ */ 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 hrs) + { + return new PIDColourScheme(); + } + + @Override + public boolean isSimple() + { + return false; } } diff --git a/src/jalview/schemes/PurinePyrimidineColourScheme.java b/src/jalview/schemes/PurinePyrimidineColourScheme.java index d2b878d..1b36f30 100644 --- a/src/jalview/schemes/PurinePyrimidineColourScheme.java +++ b/src/jalview/schemes/PurinePyrimidineColourScheme.java @@ -20,7 +20,11 @@ */ 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 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(); } } diff --git a/src/jalview/schemes/RNAHelicesColour.java b/src/jalview/schemes/RNAHelicesColour.java index 9729a83..056a167 100644 --- a/src/jalview/schemes/RNAHelicesColour.java +++ b/src/jalview/schemes/RNAHelicesColour.java @@ -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 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 +} diff --git a/src/jalview/schemes/RNAHelicesColourChooser.java b/src/jalview/schemes/RNAHelicesColourChooser.java index b1b3c5a..15cb157 100644 --- a/src/jalview/schemes/RNAHelicesColourChooser.java +++ b/src/jalview/schemes/RNAHelicesColourChooser.java @@ -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 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(); 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 list = new Vector(); 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(); - } - } diff --git a/src/jalview/schemes/RNAInteractionColourScheme.java b/src/jalview/schemes/RNAInteractionColourScheme.java index 794195a..d236803 100644 --- a/src/jalview/schemes/RNAInteractionColourScheme.java +++ b/src/jalview/schemes/RNAInteractionColourScheme.java @@ -20,9 +20,12 @@ */ 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 hrs) + { + return new RNAInteractionColourScheme(); + } } diff --git a/src/jalview/schemes/ResidueColourScheme.java b/src/jalview/schemes/ResidueColourScheme.java index f6b7c5e..03fc129 100755 --- a/src/jalview/schemes/ResidueColourScheme.java +++ b/src/jalview/schemes/ResidueColourScheme.java @@ -20,52 +20,37 @@ */ 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 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. - *

    - * 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. - *

    - * 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 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 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; } } diff --git a/src/jalview/schemes/ResidueProperties.java b/src/jalview/schemes/ResidueProperties.java index 4d46279..751175d 100755 --- a/src/jalview/schemes/ResidueProperties.java +++ b/src/jalview/schemes/ResidueProperties.java @@ -20,10 +20,6 @@ */ 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 scoreMatrices = new Hashtable(); - // 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 ssHash = new Hashtable 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 toDssp3State; static { diff --git a/src/jalview/schemes/ScoreColourScheme.java b/src/jalview/schemes/ScoreColourScheme.java index 7eb920d..aa20121 100755 --- a/src/jalview/schemes/ScoreColourScheme.java +++ b/src/jalview/schemes/ScoreColourScheme.java @@ -20,9 +20,12 @@ */ 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 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 index 5e5fa8d..0000000 --- a/src/jalview/schemes/ScoreMatrix.java +++ /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 . - * 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(""); - } - for (char sym = 'A'; sym <= 'Z'; sym++) - { - if (symbols[sym] >= 0 && symbols[sym] < symMax) - { - if (header) - { - sb.append(html ? "" : ""); - for (char sym2 = 'A'; sym2 <= 'Z'; sym2++) - { - if (symbols[sym2] >= 0 && symbols[sym2] < symMax) - { - sb.append((html ? "" : "")); - } - } - header = false; - sb.append(html ? "\n" : "\n"); - } - if (html) - { - sb.append(""); - } - sb.append((html ? "" : "")); - for (char sym2 = 'A'; sym2 <= 'Z'; sym2++) - { - if (symbols[sym2] >= 0 && symbols[sym2] < symMax) - { - sb.append((html ? "" : "")); - } - } - sb.append(html ? "\n" : "\n"); - } - } - if (html) - { - sb.append("
     " : "\t") + sym2 - + (html ? " 
    " : "") + sym + (html ? "" : "\t") - + matrix[symbols[sym]][symbols[sym2]] - + (html ? "
    "); - } - return sb.toString(); - } -} diff --git a/src/jalview/schemes/StrandColourScheme.java b/src/jalview/schemes/StrandColourScheme.java index 1340de3..5f11c29 100755 --- a/src/jalview/schemes/StrandColourScheme.java +++ b/src/jalview/schemes/StrandColourScheme.java @@ -20,7 +20,12 @@ */ 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 hrs) + { + return new StrandColourScheme(); + } } diff --git a/src/jalview/schemes/TCoffeeColourScheme.java b/src/jalview/schemes/TCoffeeColourScheme.java index 2be51c2..13d1d55 100644 --- a/src/jalview/schemes/TCoffeeColourScheme.java +++ b/src/jalview/schemes/TCoffeeColourScheme.java @@ -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 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 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 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 annots = new ArrayList(); + List annots = new ArrayList(); // Search alignment to get all tcoffee annotation and pick one set of // annotation to use to colour seqs. seqMap = new IdentityHashMap(); @@ -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 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 anns = alcontext + .findAnnotation(TCoffeeScoreFile.TCOFFEE_SCORE); + return anns.iterator().hasNext(); + } + + @Override + public String getSchemeName() + { + return JalviewColourScheme.TCoffee.toString(); + } + + @Override + public boolean isSimple() + { + return false; + } } diff --git a/src/jalview/schemes/TaylorColourScheme.java b/src/jalview/schemes/TaylorColourScheme.java index 8c46bba..ac8abbc 100755 --- a/src/jalview/schemes/TaylorColourScheme.java +++ b/src/jalview/schemes/TaylorColourScheme.java @@ -20,10 +20,39 @@ */ 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 hrs) + { + return new TaylorColourScheme(); } } diff --git a/src/jalview/schemes/TurnColourScheme.java b/src/jalview/schemes/TurnColourScheme.java index 22422ff..67116b8 100755 --- a/src/jalview/schemes/TurnColourScheme.java +++ b/src/jalview/schemes/TurnColourScheme.java @@ -20,7 +20,12 @@ */ 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 hrs) + { + return new TurnColourScheme(); + } } diff --git a/src/jalview/schemes/UserColourScheme.java b/src/jalview/schemes/UserColourScheme.java index 9ae14ca..8e58c20 100755 --- a/src/jalview/schemes/UserColourScheme.java +++ b/src/jalview/schemes/UserColourScheme.java @@ -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 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 + *

      + *
    • an AWT colour name e.g. red
    • + *
    • an AWT hex rgb colour e.g. ff2288
    • + *
    • residue colours list e.g. D,E=red;K,R,H=0022FF;c=yellow
    • + *
    + * + * @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) + * + *
    +   *     D,E=red; K,R,H=0022FF; c=100,50,75
    +   * 
    + * + * 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> colours = new HashMap>(); - 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()); + } + 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()); + } + colours.get(c).add(residue); + } + } } - return currentColour; - } + /* + * step 2: make a list of { A,G,R=12f9d6 } residues/colour specs + */ + List residueColours = new ArrayList(); + for (Entry> 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, ";"); } - } diff --git a/src/jalview/schemes/ZappoColourScheme.java b/src/jalview/schemes/ZappoColourScheme.java index 8fa1656..c32a39c 100755 --- a/src/jalview/schemes/ZappoColourScheme.java +++ b/src/jalview/schemes/ZappoColourScheme.java @@ -20,6 +20,12 @@ */ 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 hrs) + { + return new ZappoColourScheme(); } } diff --git a/src/jalview/structure/SelectionListener.java b/src/jalview/structure/SelectionListener.java index 2877d46..fe878ce 100644 --- a/src/jalview/structure/SelectionListener.java +++ b/src/jalview/structure/SelectionListener.java @@ -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); } diff --git a/src/jalview/structure/StructureListener.java b/src/jalview/structure/StructureListener.java index e5c5d04..9fde3f1 100644 --- a/src/jalview/structure/StructureListener.java +++ b/src/jalview/structure/StructureListener.java @@ -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 diff --git a/src/jalview/structure/StructureSelectionManager.java b/src/jalview/structure/StructureSelectionManager.java index fb8e3f8..db0b47e 100644 --- a/src/jalview/structure/StructureSelectionManager.java +++ b/src/jalview/structure/StructureSelectionManager.java @@ -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 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); } } } diff --git a/src/jalview/structures/models/AAStructureBindingModel.java b/src/jalview/structures/models/AAStructureBindingModel.java index 063eacf..1637631 100644 --- a/src/jalview/structures/models/AAStructureBindingModel.java +++ b/src/jalview/structures/models/AAStructureBindingModel.java @@ -20,21 +20,28 @@ */ 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 index 0000000..07f4068 --- /dev/null +++ b/src/jalview/urls/CustomUrlProvider.java @@ -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 . + * 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 selectedUrls; + + // map of string ids to urlLinks (not selected) + private HashMap 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 inMenuUrlList, + Map storedUrlList) + { + try + { + selectedUrls = parseUrlList(inMenuUrlList); + nonselectedUrls = parseUrlList(storedUrlList); + } catch (Exception ex) + { + System.out + .println(ex.getMessage() + "\nError parsing sequence links"); + } + } + + private HashMap parseUrlStrings(String urlStrings) + { + // cachedUrlList is in form