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/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
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)
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;
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..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
@@ -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 extends ClassifiedBasePair> 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 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/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 2a1ce4b..ffeca6c 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/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..dad3fca
--- /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);
+ }
+}
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 0000000..045afff
--- /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));
+ }
+}