Commit eef024ad authored by Piotr Gawron's avatar Piotr Gawron
Browse files

Merge branch '812-wrong-start-and-end-of-reaction-lines' into 'master'

Resolve "wrong start and end of reaction lines"

Closes #812

See merge request minerva/core!771
parents 3fb09925 4441b540
Pipeline #10257 passed with stage
in 12 minutes and 57 seconds
......@@ -68,6 +68,8 @@ minerva (13.1.0~alpha.0) unstable; urgency=low
* Bug fix: plugin contect element width is adjusted when link to tabs are
wrapped in more than one line (#758)
* Bug fix: export to CellDesigner preserve font size (#803)
* Bug fix: layout data was ignored for some reactions when improting from
SBML (#812)
-- Piotr Gawron <piotr.gawron@uni.lu> Tue, 7 May 2019 15:00:00 +0200
......
......@@ -179,12 +179,9 @@ public abstract class CompartmentConverter<T extends Compartment> extends Elemen
@Override
public void draw(T alias, Graphics2D graphics, ConverterParams params, List<ColorSchema> visualizedLayoutsColorSchemas) throws DrawingException {
logger.debug("Draw: "+alias.getElementId());
if (alias.getGlyph() != null) {
logger.debug("as glyph");
drawGlyph(alias, graphics);
} else {
logger.debug("without glyph");
drawImpl(alias, graphics, params);
}
......
......@@ -498,12 +498,9 @@ public abstract class SpeciesConverter<T extends Species> extends ElementConvert
@Override
public final void draw(T species, Graphics2D graphics, ConverterParams params,
List<ColorSchema> visualizedLayoutsColorSchemas) throws DrawingException {
logger.debug("Draw: "+species.getElementId());
if (species.getGlyph() != null) {
logger.debug("as glyph");
drawGlyph(species, graphics);
} else {
logger.debug("without glyph");
drawImpl(species, graphics, params);
}
......
......@@ -21,6 +21,7 @@ import org.sbml.jsbml.SpeciesReference;
import org.sbml.jsbml.ext.layout.AbstractReferenceGlyph;
import org.sbml.jsbml.ext.layout.BoundingBox;
import org.sbml.jsbml.ext.layout.Curve;
import org.sbml.jsbml.ext.layout.CurveSegment;
import org.sbml.jsbml.ext.layout.LineSegment;
import org.sbml.jsbml.ext.layout.Point;
import org.sbml.jsbml.ext.layout.ReactionGlyph;
......@@ -35,8 +36,10 @@ import org.w3c.dom.Node;
import lcsb.mapviewer.common.Configuration;
import lcsb.mapviewer.common.XmlParser;
import lcsb.mapviewer.common.exception.InvalidXmlSchemaException;
import lcsb.mapviewer.converter.model.celldesigner.geometry.helper.PolylineDataFactory;
import lcsb.mapviewer.converter.model.sbml.SbmlBioEntityExporter;
import lcsb.mapviewer.converter.model.sbml.SbmlExtension;
import lcsb.mapviewer.model.graphics.PolylineData;
import lcsb.mapviewer.model.map.InconsistentModelException;
import lcsb.mapviewer.model.map.compartment.Compartment;
import lcsb.mapviewer.model.map.kinetics.SbmlKinetics;
......@@ -295,14 +298,38 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb
if (isExtensionEnabled(SbmlExtension.RENDER)) {
assignStyleToGlyph(reactionGlyph, createStyle(reaction));
}
if (reactionGlyph.getBoundingBox()==null) {
if (reactionGlyph.getBoundingBox() == null) {
reactionGlyph.setBoundingBox(new BoundingBox());
}
if (reactionGlyph.getBoundingBox().getPosition()==null) {
if (reactionGlyph.getBoundingBox().getPosition() == null) {
reactionGlyph.getBoundingBox().setPosition(new Point());
}
reactionGlyph.getBoundingBox().getPosition().setZ(reaction.getZ());
removeColinearPoints(reactionGlyph);
}
private void removeColinearPoints(ReactionGlyph glyph) {
PolylineData line = createLine(glyph.getCurve());
logger.debug(line);
line = PolylineDataFactory.removeCollinearPoints(line);
logger.debug(line);
Curve curve = createCurve(line, false);
glyph.setCurve(curve);
}
private PolylineData createLine(Curve curve) {
PolylineData result = new PolylineData();
if (curve.getCurveSegmentCount() > 0) {
CurveSegment segment = curve.getCurveSegment(0);
result.addPoint(new Point2D.Double(segment.getStart().getX(), segment.getStart().getY()));
}
for (int i = 0; i < curve.getCurveSegmentCount(); i++) {
CurveSegment segment = curve.getCurveSegment(i);
result.addPoint(new Point2D.Double(segment.getEnd().getX(), segment.getEnd().getY()));
}
return result;
}
private LocalStyle createStyle(ReactionNode node) {
......@@ -358,16 +385,20 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb
private SpeciesReferenceGlyph createNodeGlyph(ReactionGlyph reactionGlyph, ReactionNode node) {
SpeciesReferenceGlyph reactantGlyph = reactionGlyph.createSpeciesReferenceGlyph("node_" + getNextId());
reactantGlyph.setSpeciesGlyph(speciesExporter.getSbmlGlyphByElementId(node.getElement().getElementId()).getId());
reactantGlyph.setCurve(createCurve(node, node instanceof Reactant));
Curve curve = createCurve(node.getLine(), node instanceof Reactant);
if (curve.getCurveSegmentCount() == 0) {
logger.warn(new ElementUtils().getElementTag(node) + " Problematic line");
}
reactantGlyph.setCurve(curve);
reactantGlyph.setSpeciesReference(speciesReferenceByReactionNode.get(node));
return reactantGlyph;
}
private Curve createCurve(ReactionNode node, boolean reverse) {
private Curve createCurve(PolylineData polyline, boolean reverse) {
Curve curve = new Curve();
List<Line2D> lines = node.getLine().getLines();
List<Line2D> lines = polyline.getLines();
if (reverse) {
lines = node.getLine().reverse().getLines();
lines = polyline.reverse().getLines();
}
for (Line2D line : lines) {
if (line.getP1().distance(line.getP2()) > Configuration.EPSILON) {
......@@ -377,9 +408,6 @@ public class SbmlReactionExporter extends SbmlBioEntityExporter<Reaction, org.sb
curve.addCurveSegment(segment);
}
}
if (curve.getCurveSegmentCount() == 0) {
logger.warn(new ElementUtils().getElementTag(node) + " Problematic line");
}
return curve;
}
......
......@@ -31,6 +31,7 @@ import org.sbml.jsbml.ext.render.RenderGroup;
import org.w3c.dom.Node;
import lcsb.mapviewer.common.XmlParser;
import lcsb.mapviewer.common.exception.InvalidArgumentException;
import lcsb.mapviewer.common.exception.InvalidStateException;
import lcsb.mapviewer.common.exception.InvalidXmlSchemaException;
import lcsb.mapviewer.converter.InvalidInputDataExecption;
......@@ -117,8 +118,8 @@ public class SbmlReactionParser extends SbmlBioEntityParser {
Reaction reactionWithLayout = source.copy();
// getId doesn't have to be unique, therefore we concatenate with reaction
reactionWithLayout.setIdReaction(glyph.getReaction() + "__" + glyph.getId());
//z index
// z index
if (glyph.getBoundingBox() != null && glyph.getBoundingBox().getPosition() != null
&& glyph.getBoundingBox().getPosition().isSetZ()) {
reactionWithLayout.setZ((int) glyph.getBoundingBox().getPosition().getZ());
......@@ -281,23 +282,6 @@ public class SbmlReactionParser extends SbmlBioEntityParser {
}
private NodeOperator createOutputOperator(Reaction reactionWithLayout, ReactionGlyph reactionGlyph) {
PolylineData line = new PolylineData();
Curve curve = extractCurve(reactionGlyph, reactionWithLayout);
// 1 is for the process in the center
int min = 1;
if (reactionWithLayout.getReactants().size() > 1) {
// with operator in the output we need to leave something for the operator
min++;
}
for (int i = min; i < curve.getCurveSegmentCount(); i++) {
CurveSegment segment = curve.getCurveSegment(i);
if (line.getPoints().size() == 0) {
line.addPoint(new Point2D.Double(segment.getStart().getX(), segment.getStart().getY()));
}
line.addPoint(new Point2D.Double(segment.getEnd().getX(), segment.getEnd().getY()));
}
NodeOperator operator;
if (reactionWithLayout instanceof TruncationReaction) {
operator = new TruncationOperator();
......@@ -306,34 +290,14 @@ public class SbmlReactionParser extends SbmlBioEntityParser {
} else {
operator = new SplitOperator();
}
PolylineData line = extractCurve(reactionGlyph, reactionWithLayout, operator.getClass());
operator.addOutputs(reactionWithLayout.getProducts());
operator.setLine(line.reverse());
return operator;
}
private NodeOperator createInputOperator(Reaction reactionWithLayout, ReactionGlyph reactionGlyph) {
PolylineData line = new PolylineData();
Curve curve = extractCurve(reactionGlyph, reactionWithLayout);
// -1 is for the process in the center
int max = curve.getCurveSegmentCount() - 1;
if (reactionWithLayout.getProducts().size() > 1) {
// with operator in the output we need to leave something for the operator
max--;
}
for (int i = 0; i < max; i++) {
CurveSegment segment = curve.getCurveSegment(i);
if (line.getPoints().size() == 0) {
line.addPoint(new Point2D.Double(segment.getStart().getX(), segment.getStart().getY()));
}
line.addPoint(new Point2D.Double(segment.getEnd().getX(), segment.getEnd().getY()));
}
if (line.getPoints().size() == 0) {
Point2D point = reactionWithLayout.getReactants().get(0).getLine().getEndPoint();
line.addPoint(new Point2D.Double(point.getX(), point.getY()));
line.addPoint(new Point2D.Double(point.getX(), point.getY()));
}
NodeOperator operator;
if (reactionWithLayout instanceof HeterodimerAssociationReaction) {
......@@ -341,6 +305,11 @@ public class SbmlReactionParser extends SbmlBioEntityParser {
} else {
operator = new AndOperator();
}
PolylineData line = extractCurve(reactionGlyph, reactionWithLayout, operator.getClass());
if (line.getPoints().size() == 0) {
line.addPoint(reactionWithLayout.getReactants().get(0).getLine().getEndPoint());
line.addPoint(reactionWithLayout.getReactants().get(0).getLine().getEndPoint());
}
operator.addInputs(reactionWithLayout.getReactants());
operator.setLine(line);
return operator;
......@@ -352,9 +321,14 @@ public class SbmlReactionParser extends SbmlBioEntityParser {
* specification).
*
* @param reactionGlyph
* @param operator
* class of {@link NodeOperator} that requires the line (this line can
* be separated into 3 different segments - reactant, center, product
* parts)
* @return
*/
private Curve extractCurve(ReactionGlyph reactionGlyph, Reaction reaction) {
private PolylineData extractCurve(ReactionGlyph reactionGlyph, Reaction reaction,
Class<? extends NodeOperator> operator) {
Curve curve = reactionGlyph.getCurve();
if (curve == null) {
curve = new Curve(getSbmlModel().getLevel(), getSbmlModel().getVersion());
......@@ -370,7 +344,73 @@ public class SbmlReactionParser extends SbmlBioEntityParser {
+ reactionGlyph.getId() + ")");
}
}
return curve;
if (curve.getCurveSegmentCount() == 0) {
return new PolylineData();
}
int requiredLines = 0;
if (reaction.getReactants().size() > 1) {
requiredLines++;
}
if (reaction.getProducts().size() > 1) {
requiredLines++;
}
int existingLines = curve.getCurveSegmentCount();
int breakPoint;
if (requiredLines == 2) {
breakPoint = existingLines / 2;
} else if (reaction.getReactants().size() > 1) {
breakPoint = existingLines;
} else if (reaction.getProducts().size() > 1) {
breakPoint = 0;
} else {
throw new InvalidArgumentException(
new ElementUtils().getElementTag(reaction) + "Reaction has no products and reactants");
}
PolylineData result = new PolylineData();
if (operator == TruncationOperator.class ||
operator == DissociationOperator.class ||
operator == SplitOperator.class) {
// Product operator
if (existingLines >= requiredLines) {
CurveSegment segment = curve.getCurveSegment(breakPoint);
result.addPoint(new Point2D.Double(segment.getStart().getX(), segment.getStart().getY()));
for (int i = breakPoint; i < curve.getCurveSegmentCount(); i++) {
segment = curve.getCurveSegment(i);
result.addPoint(new Point2D.Double(segment.getEnd().getX(), segment.getEnd().getY()));
}
} else {
CurveSegment segment = curve.getCurveSegment(0);
double startX = segment.getStart().getX() + (segment.getEnd().getX() - segment.getStart().getX()) / 2;
double startY = segment.getStart().getY() + (segment.getEnd().getY() - segment.getStart().getY()) / 2;
result.addPoint(new Point2D.Double(startX, startY));
result.addPoint(new Point2D.Double(segment.getEnd().getX(), segment.getEnd().getY()));
}
} else if (operator == AssociationOperator.class || operator == AndOperator.class) {
// Reactant operator
if (existingLines >= requiredLines) {
CurveSegment segment = curve.getCurveSegment(0);
result.addPoint(new Point2D.Double(segment.getStart().getX(), segment.getStart().getY()));
for (int i = 0; i < breakPoint; i++) {
segment = curve.getCurveSegment(i);
result.addPoint(new Point2D.Double(segment.getEnd().getX(), segment.getEnd().getY()));
}
} else {
CurveSegment segment = curve.getCurveSegment(0);
result.addPoint(new Point2D.Double(segment.getStart().getX(), segment.getStart().getY()));
double endX = segment.getStart().getX() + (segment.getEnd().getX() - segment.getStart().getX()) / 2;
double endY = segment.getStart().getY() + (segment.getEnd().getY() - segment.getStart().getY()) / 2;
result.addPoint(new Point2D.Double(endX, endY));
}
} else {
throw new InvalidArgumentException("Unknown operator class: " + operator);
}
return result;
}
private Class<? extends ReactionNode> getReactionNodeClass(SpeciesReferenceGlyph speciesRefernceGlyph) {
......
......@@ -563,19 +563,6 @@ public class SbmlExporterTest extends SbmlTestFunctions {
}
@Test
public void testExportImportOfInputOperator() throws Exception {
Model originalModel = new CellDesignerXmlParser()
.createModel(new ConverterParams().filename("testFiles/cell_designer_problems/heterodimer_association.xml"));
Model model = getModelAfterSerializing(originalModel);
List<Line2D> lines1 = originalModel.getReactions().iterator().next().getLines();
List<Line2D> lines2 = model.getReactions().iterator().next().getLines();
ListComparator<Line2D> comparator = new ListComparator<>(new LineComparator(Configuration.EPSILON));
assertEquals(0, comparator.compare(lines1, lines2));
}
@Test
public void testExportImportOfAdvancedInputOperator() throws Exception {
Model originalModel = new CellDesignerXmlParser().createModel(new ConverterParams()
......@@ -585,24 +572,11 @@ public class SbmlExporterTest extends SbmlTestFunctions {
Reaction r2 = model.getReactions().iterator().next();
Line2D line1 = r1.getCenterLine();
Line2D line2 = r2.getCenterLine();
assertEquals(0, new LineComparator().compare(line1, line2));
}
@Test
public void testExportImportOfOutputOperator() throws Exception {
Model originalModel = new CellDesignerXmlParser()
.createModel(new ConverterParams().filename("testFiles/cell_designer_problems/dissociation.xml"));
Model model = getModelAfterSerializing(originalModel);
List<Line2D> lines1 = originalModel.getReactions().iterator().next().getLines();
List<Line2D> lines2 = model.getReactions().iterator().next().getLines();
ListComparator<Line2D> comparator = new ListComparator<>(new LineComparator(Configuration.EPSILON));
assertEquals(0, comparator.compare(lines1, lines2));
}
@Test
public void testExportImportBooleanGate() throws Exception {
Model originalModel = new CellDesignerXmlParser()
......
......@@ -7,21 +7,18 @@ import static org.junit.Assert.assertTrue;
import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import lcsb.mapviewer.commands.CreateHierarchyCommand;
import lcsb.mapviewer.common.EventStorageLoggerAppender;
import lcsb.mapviewer.common.Configuration;
import lcsb.mapviewer.converter.ConverterParams;
import lcsb.mapviewer.converter.InvalidInputDataExecption;
import lcsb.mapviewer.model.map.compartment.Compartment;
import lcsb.mapviewer.model.map.model.Model;
import lcsb.mapviewer.model.map.reaction.Reactant;
import lcsb.mapviewer.model.map.reaction.Reaction;
import lcsb.mapviewer.model.map.reaction.ReactionNode;
import lcsb.mapviewer.model.map.species.Element;
......@@ -29,28 +26,10 @@ import lcsb.mapviewer.model.map.species.Gene;
import lcsb.mapviewer.model.map.species.GenericProtein;
import lcsb.mapviewer.model.map.species.Species;
public class SbmlParserTest {
public class SbmlParserTest extends SbmlTestFunctions {
Logger logger = Logger.getLogger(SbmlParserTest.class);
SbmlParser parser = new SbmlParser();
private EventStorageLoggerAppender appender;
@Before
public final void _setUp() throws Exception {
Logger.getRootLogger().removeAppender(appender);
appender = new EventStorageLoggerAppender();
Logger.getRootLogger().addAppender(appender);
}
@After
public final void _tearDown() throws Exception {
Logger.getRootLogger().removeAppender(appender);
}
protected List<LoggingEvent> getWarnings() {
return appender.getWarnings();
}
@Test
public void testParseCompartment() throws FileNotFoundException, InvalidInputDataExecption {
Model model = parser.createModel(
......@@ -188,12 +167,6 @@ public class SbmlParserTest {
assertEquals(1, model.getCompartments().iterator().next().getMiriamData().size());
}
@Test(expected = InvalidInputDataExecption.class)
public void testInvalidReactionLayout() throws Exception {
parser.createModel(
new ConverterParams().filename("testFiles/invalidReaction/reaction_with_missing_part_of_layout.xml"));
}
@Test
public void testEmpyModel() throws Exception {
Model model = parser.createModel(new ConverterParams().filename("testFiles/small/empty.xml"));
......@@ -259,4 +232,19 @@ public class SbmlParserTest {
assertNotNull("Creation date is not defined", model.getCreationDate());
}
@Test
public void testParseProblematicLayout() throws Exception {
try {
Model model = parser
.createModel(new ConverterParams().filename("testFiles/small/problematic_layout.xml"));
Reaction reaction = model.getReactions().iterator().next();
Reactant reactant = reaction.getReactants().get(0);
assertEquals("Reactant line should start at position 120, 40", 0,
reactant.getLine().getBeginPoint().distance(120, 40), Configuration.EPSILON);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<sbml xmlns="http://www.sbml.org/sbml/level3/version1/core" xmlns:layout="http://www.sbml.org/sbml/level3/version1/layout/version1" level="3" version="1" layout:required="false">
<model substanceUnits="mole" timeUnits="second" extentUnits="mole">
<listOfUnitDefinitions>
<unitDefinition id="per_second">
<listOfUnits>
<unit kind="second" exponent="-1" scale="0" multiplier="1"/>
</listOfUnits>
</unitDefinition>
</listOfUnitDefinitions>
<listOfCompartments>
<compartment id="default" constant="false"/>
</listOfCompartments>
<listOfSpecies>
<species sboTerm="SBO:0000252" id="species_1" name="s2" compartment="default" initialAmount="0" hasOnlySubstanceUnits="true" boundaryCondition="false" constant="false"/>
<species sboTerm="SBO:0000252" id="species_0" name="s1" compartment="default" initialAmount="0" hasOnlySubstanceUnits="true" boundaryCondition="false" constant="false"/>
<species sboTerm="SBO:0000252" id="species_2" name="s3" compartment="default" initialAmount="0" hasOnlySubstanceUnits="true" boundaryCondition="false" constant="false"/>
</listOfSpecies>
<listOfReactions>
<reaction sboTerm="SBO:0000176" id="re3" reversible="false" fast="false">
<listOfReactants>
<speciesReference species="species_1" constant="false"/>
<speciesReference species="species_0" constant="false"/>
</listOfReactants>
<listOfProducts>
<speciesReference species="species_2" constant="false"/>
</listOfProducts>
</reaction>
</listOfReactions>
<layout:listOfLayouts xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:layout="http://www.sbml.org/sbml/level3/version1/layout/version1">
<layout:layout layout:id="predicted_layout">
<layout:dimensions layout:width="564" layout:height="217"/>
<layout:listOfCompartmentGlyphs>
<layout:compartmentGlyph layout:id="cg_default" layout:compartment="default">
<layout:boundingBox layout:id="bb_cg_default">
<layout:position layout:x="0" layout:y="0"/>
<layout:dimensions layout:width="484" layout:height="177"/>
</layout:boundingBox>
</layout:compartmentGlyph>
</layout:listOfCompartmentGlyphs>
<layout:listOfSpeciesGlyphs>
<layout:speciesGlyph layout:id="sg_re3_0_sa2" layout:species="species_1">
<layout:boundingBox layout:id="bb_sg_re3_0_sa2">
<layout:position layout:x="40" layout:y="20"/>
<layout:dimensions layout:width="80" layout:height="40"/>
</layout:boundingBox>
</layout:speciesGlyph>
<layout:speciesGlyph layout:id="sg_re3_0_sa1" layout:species="species_0">
<layout:boundingBox layout:id="bb_sg_re3_0_sa1">
<layout:position layout:x="46" layout:y="117"/>
<layout:dimensions layout:width="80" layout:height="40"/>
</layout:boundingBox>
</layout:speciesGlyph>
<layout:speciesGlyph layout:id="sg_re3_0_sa3" layout:species="species_2">
<layout:boundingBox layout:id="bb_sg_re3_0_sa3">
<layout:position layout:x="364" layout:y="96"/>
<layout:dimensions layout:width="80" layout:height="40"/>
</layout:boundingBox>
</layout:speciesGlyph>
</layout:listOfSpeciesGlyphs>
<layout:listOfReactionGlyphs>
<layout:reactionGlyph layout:id="rg_re3_0" layout:reaction="re3">
<layout:curve>
<layout:listOfCurveSegments>
<layout:curveSegment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="LineSegment">
<layout:start layout:x="209.6" layout:y="70.4"/>
<layout:end layout:x="242" layout:y="78"/>
</layout:curveSegment>
</layout:listOfCurveSegments>
</layout:curve>
<layout:listOfSpeciesReferenceGlyphs>
<layout:speciesReferenceGlyph layout:id="srg_re3_0_sa2_SUBSTRATE_1" layout:speciesGlyph="sg_re3_0_sa2">
<layout:curve>
<layout:listOfCurveSegments>
<layout:curveSegment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="LineSegment">
<layout:start layout:x="209.6" layout:y="70.4"/>
<layout:end layout:x="120" layout:y="40"/>
</layout:curveSegment>
</layout:listOfCurveSegments>
</layout:curve>
</layout:speciesReferenceGlyph>
<layout:speciesReferenceGlyph layout:id="srg_re3_0_sa1_SIDESUBSTRATE_1" layout:speciesGlyph="sg_re3_0_sa1">
<layout:curve>
<layout:listOfCurveSegments>
<layout:curveSegment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="LineSegment">
<layout:start layout:x="209.6" layout:y="70.4"/>
<layout:end layout:x="126" layout:y="137"/>
</layout:curveSegment>
</layout:listOfCurveSegments>
</layout:curve>
</layout:speciesReferenceGlyph>
<layout:speciesReferenceGlyph layout:id="srg_re3_0_sa3_PRODUCT_1" layout:speciesGlyph="sg_re3_0_sa3">
<layout:curve>
<layout:listOfCurveSegments>
<layout:curveSegment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="LineSegment">
<layout:start layout:x="242" layout:y="78"/>
<layout:end layout:x="364" layout:y="116"/>
</layout:curveSegment>
</layout:listOfCurveSegments>
</layout:curve>
</layout:speciesReferenceGlyph>
</layout:listOfSpeciesReferenceGlyphs>
</layout:reactionGlyph>
</layout:listOfReactionGlyphs>
<layout:listOfTextGlyphs>
<layout:textGlyph layout:id="tg_re3_0_sa2" layout:originOfText="sg_re3_0_sa2" layout:graphicalObject="sg_re3_0_sa2">
<layout:boundingBox layout:id="bb_tg_re3_0_sa2">
<layout:position layout:x="40" layout:y="20"/>
<layout:dimensions layout:width="80" layout:height="40"/>
</layout:boundingBox>
</layout:textGlyph>