From 7d32dea88479102734ee8481982f251ecbc42e93 Mon Sep 17 00:00:00 2001 From: StormageddonDarkLordOfAll Date: Mon, 11 May 2026 17:57:31 +0200 Subject: [PATCH 1/2] first check of just removing redundant spaces redundant spaces are removed from the file --- .../shared/parser/SecondaryFileParser.java | 23 ++++++-- .../parser/SecondaryFileParserTest.java | 56 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java index 2a1ce4b1..ffeca6cc 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParser.java @@ -13,10 +13,15 @@ import pl.poznan.put.structure.formats.DefaultDotBracket; import pl.poznan.put.structure.formats.DotBracket; +import java.util.Arrays; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + @Component public class SecondaryFileParser { private static final Logger LOGGER = LoggerFactory.getLogger(SecondaryFileParser.class); + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private final Converter converter; public DotBracket parseSecondaryFile(String content, InputType inputType, boolean removeIsolated) { @@ -38,9 +43,10 @@ public DotBracket parseSecondaryFile(String content, InputType inputType, boolea private DotBracket convertBpSeqIntoDotBracket(String content, boolean removeIsolated) { try { + String normalizedContent = normalizeSecondaryFileContent(content); BpSeq bpSeq = removeIsolated - ? BpSeq.fromString(content).withoutIsolatedPairs() - : BpSeq.fromString(content); + ? BpSeq.fromString(normalizedContent).withoutIsolatedPairs() + : BpSeq.fromString(normalizedContent); Ct ct = Ct.fromBpSeq(bpSeq); return DefaultDotBracket.copyWithStrands(converter.convert(bpSeq), ct); } catch (IllegalArgumentException exception) { @@ -52,9 +58,10 @@ private DotBracket convertBpSeqIntoDotBracket(String content, boolean removeIsol private DotBracket convertCtIntoDotBracket(String content, boolean removeIsolated) { try { + String normalizedContent = normalizeSecondaryFileContent(content); Ct ct = removeIsolated - ? Ct.fromString(content).withoutIsolatedPairs() - : Ct.fromString(content); + ? Ct.fromString(normalizedContent).withoutIsolatedPairs() + : Ct.fromString(normalizedContent); BpSeq bpSeq = BpSeq.fromCt(ct); return DefaultDotBracket.copyWithStrands(converter.convert(bpSeq), ct); } catch (IllegalArgumentException exception) { @@ -79,6 +86,14 @@ private DotBracket readDotBracketContent(String content, boolean removeIsolated) } } + private String normalizeSecondaryFileContent(String content) { + return Arrays.stream(content.split("\\R")) + .map(String::trim) + .filter(line -> !line.isEmpty()) + .map(line -> WHITESPACE_PATTERN.matcher(line).replaceAll(" ")) + .collect(Collectors.joining("\n")); + } + @Autowired public SecondaryFileParser(Converter converter) { this.converter = converter; diff --git a/src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java new file mode 100644 index 00000000..045afffd --- /dev/null +++ b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/parser/SecondaryFileParserTest.java @@ -0,0 +1,56 @@ +package pl.poznan.put.rnapdbee.engine.shared.parser; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import pl.poznan.put.rnapdbee.engine.shared.domain.InputType; +import pl.poznan.put.structure.formats.BpSeq; +import pl.poznan.put.structure.formats.Converter; +import pl.poznan.put.structure.formats.DefaultDotBracket; +import pl.poznan.put.structure.formats.DotBracket; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SecondaryFileParserTest { + + @Mock + private Converter converter; + + @InjectMocks + private SecondaryFileParser secondaryFileParser; + + @Test + void shouldParseBpSeqWithRedundantSpacesAndEmptyLines() { + String content = "1 A 4\n2 U 3\n\n 3 G 2\n4 C 1\n"; + DotBracket conversionResult = mock(DotBracket.class); + when(conversionResult.sequence()).thenReturn("AAAA"); + when(conversionResult.structure()).thenReturn("()()"); + when(converter.convert(any(BpSeq.class))).thenReturn(conversionResult); + + DotBracket result = secondaryFileParser.parseSecondaryFile(content, InputType.BPSEQ, false); + + assertThat(result).isNotNull(); + verify(converter).convert(any(BpSeq.class)); + } + + @Test + void shouldParseCtWithRedundantSpacesAndEmptyLines() { + String content = " 4 some header\n1 A 0 2 4 1\n 2 U 1 3 3 2\n\n3 G 2 4 2 3\n4 C 3 0 1 4\n"; + DotBracket conversionResult = mock(DotBracket.class); + when(conversionResult.sequence()).thenReturn("AAAA"); + when(conversionResult.structure()).thenReturn("()()"); + when(converter.convert(any(BpSeq.class))).thenReturn(conversionResult); + + DotBracket result = secondaryFileParser.parseSecondaryFile(content, InputType.CT, false); + + assertThat(result).isNotNull(); + verify(converter).convert(any(BpSeq.class)); + } +} From 2b59baf7b49d202a387dd0d57f64af23399d285e Mon Sep 17 00:00:00 2001 From: Jan Pielesiak <57874179+jpielesiak@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:09:06 +0200 Subject: [PATCH 2/2] main to spaces-in-ct (#178) * refactor: gracefully handle missing residues in base pair conversion * build: bump org.immutables:value from 2.12.1 to 2.12.2 (#176) Bumps [org.immutables:value](https://github.com/immutables/immutables) from 2.12.1 to 2.12.2. - [Release notes](https://github.com/immutables/immutables/releases) - [Commits](https://github.com/immutables/immutables/compare/2.12.1...2.12.2) --- updated-dependencies: - dependency-name: org.immutables:value dependency-version: 2.12.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: restart nucleotide numbering per strand in VARNA-TZ visualization * fix: remove onlyDotsMinuses guard from non-canonical drawing fallback * fix: send explicit strandBreaks to VARNA-TZ for strand boundary display The VARNA-TZ service detects strand breaks only via a numbering discontinuity heuristic in SVG post-processing. For 3D input the real PDB residue numbers are often continuous across chains (e.g. 2KBP chain A 1-12, chain B 13-24), so no break was drawn. Add an explicit strandBreaks field (0-based last index of each strand except the final) to StructureData and compute it from combinedStrand.strands() for both 2D and 3D paths. The service honors this in addition to the heuristic. --------- Signed-off-by: dependabot[bot] Co-authored-by: Tomasz Zok Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- devenv.lock | 84 +++++-------------- pom.xml | 2 +- .../basepair/boundary/BasePairAnalyzer.java | 79 +++++++++++++---- .../shared/basepair/domain/BasePairDTO.java | 16 +++- .../shared/image/logic/DrawerManager.java | 73 ++++++++-------- .../logic/drawer/ExternalDrawerVarna.java | 32 ++++++- .../logic/drawer/model/StructureData.java | 5 ++ .../logic/drawer/ExternalDrawerVarnaTest.java | 83 ++++++++++++++++++ 8 files changed, 254 insertions(+), 120 deletions(-) create mode 100644 src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java diff --git a/devenv.lock b/devenv.lock index 6a4523d4..fdfb3f37 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,10 +3,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1761922975, + "lastModified": 1781800860, + "narHash": "sha256-LrEo0eC5ckMvjpBRCuk5q5/vjItKlxnb4n/clHNRZlk=", "owner": "cachix", "repo": "devenv", - "rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1", + "rev": "d59d872d80876d9eeb3e214d3b088bc4a14a9c4f", "type": "github" }, "original": { @@ -16,88 +17,49 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1761588595, - "owner": "edolstra", - "repo": "flake-compat", - "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { + "nixpkgs": { "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs-src": "nixpkgs-src" }, "locked": { - "lastModified": 1760663237, + "lastModified": 1781620901, + "narHash": "sha256-UF6scQlG+6lRkZBUpn/3KNavhOo5G8kDWhjVHcno8uc=", "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", + "repo": "devenv-nixpkgs", + "rev": "2df109b343d3c68efd752e32a444a1d9b9f89afa", "type": "github" }, "original": { "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", + "ref": "rolling", + "repo": "devenv-nixpkgs", "type": "github" } }, - "nixpkgs": { + "nixpkgs-src": { + "flake": false, "locked": { - "lastModified": 1761313199, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "d1c30452ebecfc55185ae6d1c983c09da0c274ff", + "lastModified": 1781454065, + "narHash": "sha256-d2xfDjnfRuf/xYGdu9VVRHiav/2w5hDL/5cw2TuVAXw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9eac87a12312b8f60dd52e1c6e1a265f6fc7f5fc", "type": "github" }, "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "devenv": "devenv", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "git-hooks" - ] + "nixpkgs": "nixpkgs" } } }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index d69a0e51..0e8a1a6f 100644 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,7 @@ org.immutables value - 2.12.1 + 2.12.2 provided diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java index f18ec809..89f86268 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/boundary/BasePairAnalyzer.java @@ -8,6 +8,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,6 +42,7 @@ import pl.poznan.put.rnapdbee.engine.shared.multiplet.MultipletSet; import pl.poznan.put.structure.AnalyzedBasePair; import pl.poznan.put.structure.ImmutableAnalyzedBasePair; +import pl.poznan.put.structure.ImmutableBasePair; // TODO: WebFlux would be really efficient with the 3D->multi 2D analysis as we there perform multiple calls to the // adapters, it could be done in parallel and then joined up after each call is performed. We would save a tone of @@ -124,7 +126,12 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( .filter(pair -> pair.isCanonical(structureModel)) .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_BASE) .withSaenger(basePair.getSaengerType() != null ? SaengerType.mapToBioCommonsForm( @@ -136,13 +143,20 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( : LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List nonCanonical = responseFromAdapter.getBasePairs().stream() .filter(pair -> !pair.isCanonical(structureModel)) .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_BASE) .withSaenger(basePair.getSaengerType() != null ? SaengerType.mapToBioCommonsForm( @@ -154,56 +168,91 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( : LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List stackings = responseFromAdapter.getStackings().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.STACKING) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(mapToBioCommonsForm(basePair.getTopology()))) + .withStackingTopology(mapToBioCommonsForm(basePair.getTopology())); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List basePhosphate = responseFromAdapter.getBasePhosphateInteractions().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_PHOSPHATE) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BasePhosphateType.mapToBioCommonsForm(basePair.getBph())) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List baseRibose = responseFromAdapter.getBaseRiboseInteractions().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_RIBOSE) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BaseRiboseType.mapToBioCommonsForm(basePair.getBr())) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List otherInteractions = responseFromAdapter.getOther().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.OTHER) .withSaenger(Saenger.UNKNOWN) .withLeontisWesthof(LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .collect(Collectors.toList()); List interStrand = responseFromAdapter.getBasePairs().stream() .map(pair -> BasePairDTO.ofBasePairDTOWithNameFromMap(pair, pairIdentifiersWithTheirShortNames)) - .map(basePair -> ImmutableAnalyzedBasePair.of(basePair.toBasePair(structureModel)) + .map(basePair -> { + ImmutableBasePair bp = basePair.toBasePair(structureModel); + if (bp == null) { + return null; + } + return ImmutableAnalyzedBasePair.of(bp) .withInteractionType(InteractionType.BASE_BASE) .withSaenger(basePair.getSaengerType() != null ? SaengerType.mapToBioCommonsForm( @@ -215,7 +264,9 @@ protected BasePairAnalysis performPostAnalysisOnResponseFromAdapter( : LeontisWesthof.UNKNOWN) .withBph(BPh.UNKNOWN) .withBr(BR.UNKNOWN) - .withStackingTopology(StackingTopology.UNKNOWN)) + .withStackingTopology(StackingTopology.UNKNOWN); + }) + .filter(Objects::nonNull) .filter(basePair -> !basePair.basePair() .left() .chainIdentifier() diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java index 37f9f6b0..d68f0c50 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/basepair/domain/BasePairDTO.java @@ -1,6 +1,8 @@ package pl.poznan.put.rnapdbee.engine.shared.basepair.domain; import com.fasterxml.jackson.annotation.JsonProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import pl.poznan.put.pdb.PdbNamedResidueIdentifier; import pl.poznan.put.pdb.analysis.PdbModel; import pl.poznan.put.rnapdbee.engine.shared.basepair.boundary.ChainNumberKey; @@ -19,6 +21,7 @@ */ public class BasePairDTO { + private static final Logger LOGGER = LoggerFactory.getLogger(BasePairDTO.class); private static final Set CANONICAL_ONE_LETTER_NAME_SORTED_PAIRS = new HashSet<>(Arrays.asList("AU", "GU", "CG")); @JsonProperty("nt1") @@ -122,6 +125,9 @@ public static BasePairDTO ofBasePairDTOWithNameFromMap(BasePairDTO basePairDTO, public ImmutableBasePair toBasePair(PdbModel pdbModel) { PdbNamedResidueIdentifier left = mapResidueToPdbNamedResidueIdentifier(nt1, pdbModel); PdbNamedResidueIdentifier right = mapResidueToPdbNamedResidueIdentifier(nt2, pdbModel); + if (left == null || right == null) { + return null; + } return ImmutableBasePair.of(left, right); } @@ -130,14 +136,15 @@ private PdbNamedResidueIdentifier mapResidueToPdbNamedResidueIdentifier(Residue return pdbModel.findResidue(residue).namedResidueIdentifier(); } - throw new IllegalStateException(String.format( - "Residue not found in model: chain='%s', number=%d, icode=%s, name='%s'. " + LOGGER.warn( + "Residue not found in model: chain='{}', number={}, icode={}, name='{}'. " + "This may indicate a mismatch between adapter response and parsed structure " + "(e.g., chain identifier normalization issue).", residue.chainIdentifier(), residue.residueNumber(), residue.insertionCode().orElse("(none)"), - residue.getAuth().getName())); + residue.getAuth().getName()); + return null; } public boolean isCanonical(PdbModel pdbModel) { @@ -147,6 +154,9 @@ public boolean isCanonical(PdbModel pdbModel) { if (leontisWesthofType == LeontisWesthofType.CWW) { PdbNamedResidueIdentifier left = mapResidueToPdbNamedResidueIdentifier(nt1, pdbModel); PdbNamedResidueIdentifier right = mapResidueToPdbNamedResidueIdentifier(nt2, pdbModel); + if (left == null || right == null) { + return false; + } String sequence = Stream.of(left.oneLetterName(), right.oneLetterName()) .map(c -> Character.toString(c)) .map(String::toUpperCase) diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java index f83711f8..2761e685 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/DrawerManager.java @@ -126,54 +126,47 @@ public ImageInformationOutput drawCanonicalAndNonCanonical( LOGGER.info(String.format( "Drawing non-canonical image started with drawer: %s, structure: %s, sequence: %s", visualizationTool, dotBracket.structure(), dotBracket.sequence())); - final boolean onlyDotsMinuses = StringUtils.containsOnly(dotBracket.structure(), ".-"); - final boolean isMainToolVarna = visualizationTool == VisualizationTool.VARNA; final SecondaryStructureDrawer mainDrawer = drawerFactory.loadDrawer(visualizationTool); - if (!onlyDotsMinuses || isMainToolVarna) { - try { - final SVGDocument svgDocument = - mainDrawer.drawSecondaryStructure(dotBracket, structureModel, nonCanonicalBasePairs, stacking); - final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); - return new ImageInformationOutput() - .withSuccessfulDrawer(visualizationTool) - .withFailedDrawer(VisualizationTool.NONE) - .withDrawingResult(DrawingResult.DONE_BY_MAIN_DRAWER) - .withSvgFile(svgDocumentAsByteArray); - } catch (final VisualizationException | IOException e) { - LOGGER.error( - String.format( - "Failed drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", - visualizationTool, dotBracket.structure(), dotBracket.sequence()), - e); - } + try { + final SVGDocument svgDocument = + mainDrawer.drawSecondaryStructure(dotBracket, structureModel, nonCanonicalBasePairs, stacking); + final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); + return new ImageInformationOutput() + .withSuccessfulDrawer(visualizationTool) + .withFailedDrawer(VisualizationTool.NONE) + .withDrawingResult(DrawingResult.DONE_BY_MAIN_DRAWER) + .withSvgFile(svgDocumentAsByteArray); + } catch (final VisualizationException | IOException e) { + LOGGER.error( + String.format( + "Failed drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", + visualizationTool, dotBracket.structure(), dotBracket.sequence()), + e); } - final boolean isBackupToolVarna = visualizationTool.getBackupVisualizationTool() == VisualizationTool.VARNA; final VisualizationTool backupVisualizationTool = visualizationTool.getBackupVisualizationTool(); final SecondaryStructureDrawer backupDrawer = drawerFactory.loadDrawer(visualizationTool.getBackupVisualizationTool()); - if (!onlyDotsMinuses || isBackupToolVarna) { - try { - LOGGER.info(String.format( - "Drawing non-canonical image started with backup drawer: %s, structure: %s, sequence: %s", - backupVisualizationTool, dotBracket.structure(), dotBracket.sequence())); - final SVGDocument svgDocument = backupDrawer.drawSecondaryStructure( - dotBracket, structureModel, nonCanonicalBasePairs, stacking); - final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); - return new ImageInformationOutput() - .withSuccessfulDrawer(backupVisualizationTool) - .withFailedDrawer(visualizationTool) - .withDrawingResult(DrawingResult.DONE_BY_BACKUP_DRAWER) - .withSvgFile(svgDocumentAsByteArray); - } catch (final VisualizationException | IOException e) { - LOGGER.error( - String.format( - "Drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", - visualizationTool, dotBracket.structure(), dotBracket.sequence()), - e); - } + try { + LOGGER.info(String.format( + "Drawing non-canonical image started with backup drawer: %s, structure: %s, sequence: %s", + backupVisualizationTool, dotBracket.structure(), dotBracket.sequence())); + final SVGDocument svgDocument = backupDrawer.drawSecondaryStructure( + dotBracket, structureModel, nonCanonicalBasePairs, stacking); + final byte[] svgDocumentAsByteArray = SVGHelper.export(svgDocument, Format.SVG); + return new ImageInformationOutput() + .withSuccessfulDrawer(backupVisualizationTool) + .withFailedDrawer(visualizationTool) + .withDrawingResult(DrawingResult.DONE_BY_BACKUP_DRAWER) + .withSvgFile(svgDocumentAsByteArray); + } catch (final VisualizationException | IOException e) { + LOGGER.error( + String.format( + "Drawing non-canonical image with drawer: %s, structure: %s, sequence: %s", + visualizationTool, dotBracket.structure(), dotBracket.sequence()), + e); } return ImageInformationOutput.FAILED_INSTANCE; diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java index fe388425..d09682b9 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarna.java @@ -14,14 +14,18 @@ import pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer.model.Stacking; import pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer.model.StructureData; import pl.poznan.put.structure.ClassifiedBasePair; +import pl.poznan.put.structure.DotBracketSymbol; import pl.poznan.put.structure.formats.DotBracket; import pl.poznan.put.structure.formats.DotBracketFromPdb; +import pl.poznan.put.structure.formats.Strand; import pl.poznan.put.utility.svg.SVGHelper; import java.awt.*; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -74,6 +78,17 @@ private static StructureData createStructureData( List stackingInteractions) { var nucleotides = new ArrayList(); var symbols = combinedStrand.symbols(); + + Map symbolToStrandPosition = new HashMap<>(); + if (!(combinedStrand instanceof DotBracketFromPdb)) { + for (Strand strand : combinedStrand.strands()) { + int position = 1; + for (DotBracketSymbol strandSymbol : strand.symbols()) { + symbolToStrandPosition.put(strandSymbol, position++); + } + } + } + for (int i = 0; i < symbols.size(); i++) { var symbol = symbols.get(i); var nucleotide = new Nucleotide(); @@ -82,7 +97,7 @@ private static StructureData createStructureData( nucleotide.number = ((DotBracketFromPdb) combinedStrand).identifier(symbol).residueNumber(); } else { - nucleotide.number = i + 1; + nucleotide.number = symbolToStrandPosition.get(symbol); } nucleotide.character = String.valueOf(symbol.sequence()); if (symbol.isMissing()) { @@ -155,12 +170,27 @@ private static StructureData createStructureData( var structureData = new StructureData(); structureData.nucleotides = nucleotides; structureData.basePairs = basePairs; + structureData.strandBreaks = computeStrandBreaks(combinedStrand); // TODO: disable stacking visualization for now // structureData.stackings = stackings; structureData.drawingAlgorithm = "NAVIEW"; return structureData; } + private static List computeStrandBreaks(DotBracket combinedStrand) { + var strands = combinedStrand.strands(); + if (strands.size() < 2) { + return Collections.emptyList(); + } + var breaks = new ArrayList(); + int offset = 0; + for (int s = 0; s < strands.size() - 1; s++) { + offset += strands.get(s).symbols().size(); + breaks.add(offset - 1); + } + return breaks; + } + private static ModeleBP.Edge map(NucleobaseEdge edge) { switch (edge) { case WATSON_CRICK: diff --git a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java index 7f31e2ab..6e93e870 100644 --- a/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java +++ b/src/main/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/model/StructureData.java @@ -20,6 +20,9 @@ public class StructureData { @JsonProperty("drawingAlgorithm") public String drawingAlgorithm; + @JsonProperty("strandBreaks") + public List strandBreaks; + @Override public String toString() { return "StructureData{" @@ -30,6 +33,8 @@ public String toString() { + " items, drawingAlgorithm='" + drawingAlgorithm + '\'' + + ", strandBreaks=" + + (strandBreaks != null ? strandBreaks : "[]") + ", stackings=" + (stackings != null ? stackings.size() : 0) + " items" diff --git a/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java new file mode 100644 index 00000000..dad3fcad --- /dev/null +++ b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java @@ -0,0 +1,83 @@ +package pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.StringReader; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.batik.anim.dom.SAXSVGDocumentFactory; +import org.apache.batik.util.XMLResourceDescriptor; +import org.junit.jupiter.api.Test; +import org.w3c.dom.svg.SVGDocument; +import pl.poznan.put.rnapdbee.engine.shared.image.logic.drawer.model.StructureData; +import pl.poznan.put.structure.formats.DefaultDotBracket; +import pl.poznan.put.structure.formats.DotBracket; + +class ExternalDrawerVarnaTest { + + private static SVGDocument emptySvg() throws Exception { + String svg = ""; + SAXSVGDocumentFactory factory = + new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); + return factory.createSVGDocument(null, new StringReader(svg)); + } + + private static class FakeVarnaTzClient extends VarnaTzClient { + private StructureData lastStructureData; + + FakeVarnaTzClient() { + super("http://localhost"); + } + + @Override + public SVGDocument draw(StructureData structureData) { + this.lastStructureData = structureData; + try { + return emptySvg(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + StructureData getLastStructureData() { + return lastStructureData; + } + } + + @Test + void shouldRestartNumberingForEachStrand() throws Exception { + FakeVarnaTzClient client = new FakeVarnaTzClient(); + ExternalDrawerVarna drawer = new ExternalDrawerVarna(client); + DotBracket dotBracket = DefaultDotBracket.fromString( + ">strand_A\nAAAGGGAAA\n...(((...\n>strand_B\nAAACCCAAA\n...)))...\n"); + + drawer.drawSecondaryStructure(dotBracket, Collections.emptyList()); + + StructureData structureData = client.getLastStructureData(); + List numbers = structureData.nucleotides.stream() + .map(nucleotide -> nucleotide.number) + .collect(Collectors.toList()); + + assertEquals(18, numbers.size()); + assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9), numbers); + assertEquals(List.of(8), structureData.strandBreaks); + } + + @Test + void shouldUseContinuousNumberingForSingleStrand() throws Exception { + FakeVarnaTzClient client = new FakeVarnaTzClient(); + ExternalDrawerVarna drawer = new ExternalDrawerVarna(client); + DotBracket dotBracket = DefaultDotBracket.fromString(">strand\nAAAGGGUUU\n...((()))\n"); + + drawer.drawSecondaryStructure(dotBracket, Collections.emptyList()); + + StructureData structureData = client.getLastStructureData(); + List numbers = structureData.nucleotides.stream() + .map(nucleotide -> nucleotide.number) + .collect(Collectors.toList()); + + assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9), numbers); + assertEquals(Collections.emptyList(), structureData.strandBreaks); + } +}