From e83178004e1ff3541b6eb7a31428adca53e081d5 Mon Sep 17 00:00:00 2001 From: Tomasz Zok Date: Thu, 14 May 2026 12:33:21 +0200 Subject: [PATCH 1/5] refactor: gracefully handle missing residues in base pair conversion --- .../basepair/boundary/BasePairAnalyzer.java | 79 +++++++++++++++---- .../shared/basepair/domain/BasePairDTO.java | 16 +++- 2 files changed, 78 insertions(+), 17 deletions(-) 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 f18ec80..89f8626 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 37f9f6b..d68f0c5 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) From 3d694a2336a9e2b7081c92717bb551ac14b29907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 04:54:17 +0000 Subject: [PATCH 2/5] 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> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d69a0e5..0e8a1a6 100644 --- a/pom.xml +++ b/pom.xml @@ -108,7 +108,7 @@ org.immutables value - 2.12.1 + 2.12.2 provided From 3c8fe6d74efced9f8f000b899267765612f20e2c Mon Sep 17 00:00:00 2001 From: Tomasz Zok Date: Wed, 17 Jun 2026 07:12:00 +0000 Subject: [PATCH 3/5] fix: restart nucleotide numbering per strand in VARNA-TZ visualization --- .../logic/drawer/ExternalDrawerVarna.java | 17 +++- .../logic/drawer/ExternalDrawerVarnaTest.java | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java 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 fe38842..5325780 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()) { 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 0000000..0e62a07 --- /dev/null +++ b/src/test/java/pl/poznan/put/rnapdbee/engine/shared/image/logic/drawer/ExternalDrawerVarnaTest.java @@ -0,0 +1,81 @@ +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); + } + + @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); + } +} From ccf15cb221c57e770fa3129628e2af99a7ae4e93 Mon Sep 17 00:00:00 2001 From: Tomasz Zok Date: Fri, 19 Jun 2026 13:56:19 +0000 Subject: [PATCH 4/5] fix: remove onlyDotsMinuses guard from non-canonical drawing fallback --- devenv.lock | 84 +++++-------------- .../shared/image/logic/DrawerManager.java | 73 ++++++++-------- 2 files changed, 56 insertions(+), 101 deletions(-) diff --git a/devenv.lock b/devenv.lock index 6a4523d..fdfb3f3 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/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 f83711f..2761e68 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; From 3d92e4152243eb7f92e3fed9b84921ca71788fc6 Mon Sep 17 00:00:00 2001 From: Tomasz Zok Date: Fri, 19 Jun 2026 15:09:55 +0000 Subject: [PATCH 5/5] 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. --- .../image/logic/drawer/ExternalDrawerVarna.java | 15 +++++++++++++++ .../image/logic/drawer/model/StructureData.java | 5 +++++ .../logic/drawer/ExternalDrawerVarnaTest.java | 2 ++ 3 files changed, 22 insertions(+) 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 5325780..d09682b 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 @@ -170,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 7f31e2a..6e93e87 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 index 0e62a07..dad3fca 100644 --- 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 @@ -61,6 +61,7 @@ void shouldRestartNumberingForEachStrand() throws Exception { 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 @@ -77,5 +78,6 @@ void shouldUseContinuousNumberingForSingleStrand() throws Exception { .collect(Collectors.toList()); assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9), numbers); + assertEquals(Collections.emptyList(), structureData.strandBreaks); } }