Merge pull request #591 from wolsen/fix-print

[Fixes #531] Fix printing/export as pdf
This commit is contained in:
Daniel Williams 2020-03-28 20:47:16 -04:00 committed by GitHub
commit 7dbbb6c74d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 184 additions and 99 deletions

View File

@ -371,6 +371,11 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
return this.motors.getDefault(); return this.motors.getDefault();
} }
@Override
public MotorConfigurationSet getMotorConfigurationSet() {
return this.motors;
}
@Override @Override
public MotorConfiguration getMotorConfig( final FlightConfigurationId fcid){ public MotorConfiguration getMotorConfig( final FlightConfigurationId fcid){
return this.motors.get(fcid); return this.motors.get(fcid);
@ -432,7 +437,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
@Override @Override
public int getMotorCount() { public int getMotorCount() {
return this.motors.size(); return this.getClusterConfiguration().getClusterCount();
} }
@Override @Override

View File

@ -126,6 +126,32 @@ public class FlightConfiguration implements FlightConfigurableParameter<FlightCo
_setStageActive( stageNumber, false ); _setStageActive( stageNumber, false );
} }
/**
* Activates all stages as active starting from the specified component
* to the top-most stage in the rocket. Active stages are those stages
* which contribute to the mass of the rocket. Given a rocket with the
* following stages:
*
* <ul>
* <li>StageA - top most stage, containing nose cone etc.</li>
* <li>StageB - middle stage</li>
* <li>StageC - bottom stage</li>
* </ul>
*
* invoking <code>FlightConfiguration.activateStagesThrough(StageB)</code>
* will cause both StageA and StageB to be marked as active, and StageC
* will be marked as inactive.
*
* @param stage the AxialStage to activate all stages up to (inclusive)
*/
public void activateStagesThrough(final AxialStage stage) {
clearAllStages();
for (int i=0; i <= stage.getStageNumber(); i++) {
_setStageActive(i, true);
}
updateMotors();
}
/** /**
* This method flags the specified stage as active, and all other stages as inactive. * This method flags the specified stage as active, and all other stages as inactive.
* *

View File

@ -258,6 +258,11 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab
return this.motors.getDefault(); return this.motors.getDefault();
} }
@Override
public MotorConfigurationSet getMotorConfigurationSet() {
return this.motors;
}
@Override @Override
public MotorConfiguration getMotorConfig( final FlightConfigurationId fcid){ public MotorConfiguration getMotorConfig( final FlightConfigurationId fcid){
return this.motors.get(fcid); return this.motors.get(fcid);
@ -321,7 +326,7 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab
@Override @Override
public int getMotorCount() { public int getMotorCount() {
return this.motors.size(); return this.getClusterConfiguration().getClusterCount();
} }

View File

@ -3,6 +3,7 @@ package net.sf.openrocket.rocketcomponent;
import java.util.Iterator; import java.util.Iterator;
import net.sf.openrocket.motor.MotorConfiguration; import net.sf.openrocket.motor.MotorConfiguration;
import net.sf.openrocket.motor.MotorConfigurationSet;
import net.sf.openrocket.util.ChangeSource; import net.sf.openrocket.util.ChangeSource;
import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.Coordinate;
@ -80,6 +81,13 @@ public interface MotorMount extends ChangeSource, FlightConfigurableComponent {
// duplicate of RocketComponent // duplicate of RocketComponent
public Coordinate[] getLocations(); public Coordinate[] getLocations();
/**
* Returns the set of motors configured for flight/simulation in this motor mount.
* @return the MotorConfigurationSet containing the set of motors configured in
* this motor mount.
*/
public MotorConfigurationSet getMotorConfigurationSet();
/** /**
* *
* @param fcid id for which to return the motor (null retrieves the default) * @param fcid id for which to return the motor (null retrieves the default)

View File

@ -201,7 +201,7 @@ public class FlightConfigurationTest extends BaseTestCase {
InnerTube smmt = (InnerTube)rkt.getChild(0).getChild(1).getChild(2); InnerTube smmt = (InnerTube)rkt.getChild(0).getChild(1).getChild(2);
int expectedMotorCount = 5; int expectedMotorCount = 5;
int actualMotorCount = smmt.getMotorCount(); int actualMotorCount = smmt.getMotorConfigurationSet().size();
assertThat("number of motor configurations doesn't match.", actualMotorCount, equalTo(expectedMotorCount)); assertThat("number of motor configurations doesn't match.", actualMotorCount, equalTo(expectedMotorCount));
} }
@ -295,6 +295,21 @@ public class FlightConfigurationTest extends BaseTestCase {
config.toggleStage(0); config.toggleStage(0);
assertThat(" toggle stage #0: ", config.isStageActive(0), equalTo(false)); assertThat(" toggle stage #0: ", config.isStageActive(0), equalTo(false));
AxialStage sustainer = rkt.getTopmostStage();
AxialStage booster = rkt.getBottomCoreStage();
assertThat(" sustainer stage is stage #0: ", sustainer.getStageNumber(), equalTo(0));
assertThat(" booster stage is stage #1: ", booster.getStageNumber(), equalTo(1));
config.clearAllStages();
config.activateStagesThrough(sustainer);
assertThat(" sustainer stage is active: ", config.isStageActive(sustainer.getStageNumber()), equalTo(true));
assertThat(" booster stage is inactive: ", config.isStageActive(booster.getStageNumber()), equalTo(false));
config.clearAllStages();
config.activateStagesThrough(booster);
assertThat(" sustainer stage is active: ", config.isStageActive(sustainer.getStageNumber()), equalTo(true));
assertThat(" booster stage is active: ", config.isStageActive(booster.getStageNumber()), equalTo(true));
} }
/** /**

View File

@ -28,7 +28,9 @@ import net.sf.openrocket.gui.figureelements.FigureElement;
import net.sf.openrocket.gui.figureelements.RocketInfo; import net.sf.openrocket.gui.figureelements.RocketInfo;
import net.sf.openrocket.gui.scalefigure.RocketPanel; import net.sf.openrocket.gui.scalefigure.RocketPanel;
import net.sf.openrocket.masscalc.MassCalculator; import net.sf.openrocket.masscalc.MassCalculator;
import net.sf.openrocket.masscalc.RigidBody;
import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.MotorConfiguration;
import net.sf.openrocket.rocketcomponent.AxialStage; import net.sf.openrocket.rocketcomponent.AxialStage;
import net.sf.openrocket.rocketcomponent.FlightConfiguration; import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import net.sf.openrocket.rocketcomponent.FlightConfigurationId; import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
@ -162,7 +164,7 @@ public class DesignReport {
PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, ROCKET_DESIGN); PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, ROCKET_DESIGN);
Rocket rocket = rocketDocument.getRocket(); Rocket rocket = rocketDocument.getRocket();
final FlightConfiguration configuration = rocket.getSelectedConfiguration();//.clone(); final FlightConfiguration configuration = rocket.getSelectedConfiguration();
configuration.setAllStages(); configuration.setAllStages();
PdfContentByte canvas = writer.getDirectContent(); PdfContentByte canvas = writer.getDirectContent();
@ -177,8 +179,8 @@ public class DesignReport {
canvas.beginText(); canvas.beginText();
canvas.setFontAndSize(ITextHelper.getBaseFont(), PrintUtilities.NORMAL_FONT_SIZE); canvas.setFontAndSize(ITextHelper.getBaseFont(), PrintUtilities.NORMAL_FONT_SIZE);
int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getHeight()) * 0.4 * (scale / PrintUnit.METERS double figureHeightInPoints = PrintUnit.METERS.toPoints(figure.getFigureHeight());
.toPoints(1))); int figHeightPts = (int) (figureHeightInPoints * SCALE_FUDGE_FACTOR * (scale / PrintUnit.METERS.toPoints(1)));
final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts); final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts);
canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight); canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight);
canvas.moveTextWithLeading(0, -16); canvas.moveTextWithLeading(0, -16);
@ -190,8 +192,7 @@ public class DesignReport {
canvas.newlineShowText(STAGES); canvas.newlineShowText(STAGES);
canvas.showText("" + rocket.getStageCount()); canvas.showText("" + rocket.getStageCount());
if (configuration.hasMotors()) {
if ( configuration.hasMotors()){
if (configuration.getStageCount() > 1) { if (configuration.getStageCount() > 1) {
canvas.newlineShowText(MASS_WITH_MOTORS); canvas.newlineShowText(MASS_WITH_MOTORS);
} else { } else {
@ -213,7 +214,11 @@ public class DesignReport {
canvas.endText(); canvas.endText();
try { try {
//Move the internal pointer of the document below that of what was just written using the direct byte buffer. /*
* Move the internal pointer of the document below the rocket diagram and
* the key attributes. The height of the rocket figure is already calculated
* as diagramHeigt and the height of the attributes text is finalY - initialY.
*/
Paragraph paragraph = new Paragraph(); Paragraph paragraph = new Paragraph();
float finalY = canvas.getYTLM(); float finalY = canvas.getYTLM();
int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight)); int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight));
@ -223,28 +228,26 @@ public class DesignReport {
List<Simulation> simulations = rocketDocument.getSimulations(); List<Simulation> simulations = rocketDocument.getSimulations();
int motorNumber = 0; boolean firstMotor = true;
for( FlightConfigurationId fcid : rocket.getIds()){ for (FlightConfigurationId fcid : rocket.getIds()) {
PdfPTable parent = new PdfPTable(2); PdfPTable parent = new PdfPTable(2);
parent.setWidthPercentage(100); parent.setWidthPercentage(100);
parent.setHorizontalAlignment(Element.ALIGN_LEFT); parent.setHorizontalAlignment(Element.ALIGN_LEFT);
parent.setSpacingBefore(0); parent.setSpacingBefore(0);
parent.setWidths(new int[] { 1, 3 }); parent.setWidths(new int[] { 1, 3 });
/* The first motor information will get no spacing
int leading = 0; * before it, while each subsequent table will need
//The first motor config is always null. Skip it and the top-most motor, then set the leading. * a spacing of 25.
if ( motorNumber > 1) { */
leading = 25; int leading = (firstMotor) ? 0 : 25;
}
FlightData flight = findSimulation( fcid, simulations); FlightData flight = findSimulation( fcid, simulations);
addFlightData(flight, rocket, fcid, parent, leading); addFlightData(flight, rocket, fcid, parent, leading);
addMotorData(rocket, fcid, parent); addMotorData(rocket, fcid, parent);
document.add(parent); document.add(parent);
motorNumber++; firstMotor = false;
} }
} catch (DocumentException e) { } catch (DocumentException e) {
log.error("Could not modify document.", e); log.error("Could not modify document.", e);
@ -274,21 +277,22 @@ public class DesignReport {
theFigure.updateFigure(); theFigure.updateFigure();
double scale = double scale =
(thePageImageableWidth * 2.2) / theFigure.getWidth(); (thePageImageableWidth * 2.2) / theFigure.getFigureWidth();
theFigure.setScale(scale); theFigure.setScale(scale);
/* /* Conveniently, page dimensions are in points-per-inch, which, in
* page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion * Java2D, are the same as pixels-per-inch; thus we don't need any
* conversion for the figure size.
*/ */
theFigure.setSize(thePageImageableWidth, thePageImageableHeight); theFigure.setSize(thePageImageableWidth, thePageImageableHeight);
theFigure.updateFigure(); theFigure.updateFigure();
final DefaultFontMapper mapper = new DefaultFontMapper(); final DefaultFontMapper mapper = new DefaultFontMapper();
Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper); Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper);
final double halfFigureHeight = SCALE_FUDGE_FACTOR * theFigure.getFigureHeightPx() / 2; final double halfFigureHeight = SCALE_FUDGE_FACTOR * theFigure.getFigureHeight() / 2;
int y = PrintUnit.POINTS_PER_INCH; int y = PrintUnit.POINTS_PER_INCH;
//If the y dimension is negative, then it will potentially be drawn off the top of the page. Move the origin //If the y dimension is negative, then it will potentially be drawn off the top of the page. Move the origin
//to allow for this. //to allow for this.
if (theFigure.getHeight() < 0.0d) { if (theFigure.getDimensions().getY() < 0.0d) {
y += (int) halfFigureHeight; y += (int) halfFigureHeight;
} }
g2d.translate(20, y); g2d.translate(20, y);
@ -326,30 +330,26 @@ public class DesignReport {
DecimalFormat ttwFormat = new DecimalFormat("0.00"); DecimalFormat ttwFormat = new DecimalFormat("0.00");
MassCalculator massCalc = new MassCalculator(); if( motorId.hasError() ){
if( !motorId.hasError() ){
throw new IllegalStateException("Attempted to add motor data with an invalid fcid"); throw new IllegalStateException("Attempted to add motor data with an invalid fcid");
} }
rocket.createFlightConfiguration(motorId); rocket.createFlightConfiguration(motorId);
FlightConfiguration config = rocket.getFlightConfiguration( motorId); FlightConfiguration config = rocket.getFlightConfiguration(motorId);
int totalMotorCount = 0; int totalMotorCount = 0;
double totalPropMass = 0; double totalPropMass = 0;
double totalImpulse = 0; double totalImpulse = 0;
double totalTTW = 0; double totalTTW = 0;
int stage = 0;
double stageMass = 0; double stageMass = 0;
boolean topBorder = false; boolean topBorder = false;
for (RocketComponent c : rocket) { for (RocketComponent c : rocket) {
if (c instanceof AxialStage) { if (c instanceof AxialStage) {
config.clearAllStages(); config.activateStagesThrough((AxialStage) c);
config.setOnlyStage(stage); RigidBody launchInfo = MassCalculator.calculateLaunch(config);
stage++; stageMass = launchInfo.getMass();
stageMass = massCalc.getCGAnalysis( config).get(stage).weight;
// Calculate total thrust-to-weight from only lowest stage motors // Calculate total thrust-to-weight from only lowest stage motors
totalTTW = 0; totalTTW = 0;
topBorder = true; topBorder = true;
@ -358,11 +358,19 @@ public class DesignReport {
if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) { if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) {
MotorMount mount = (MotorMount) c; MotorMount mount = (MotorMount) c;
// TODO: refactor this... it's redundant with containing if, and could probably be simplified MotorConfiguration motorConfig = mount.getMotorConfig(motorId);
if (mount.isMotorMount() && (mount.getMotorConfig(motorId) != null) &&(null != mount.getMotorConfig(motorId).getMotor())) { if (null == motorConfig) {
Motor motor = mount.getMotorConfig(motorId).getMotor(); log.warn("Unable to find motorConfig for motorId {}", motorId);
int motorCount = mount.getMotorCount(); continue;
}
Motor motor = motorConfig.getMotor();
if (null == motor) {
log.warn("Motor instance is null for motorId {}", motorId);
continue;
}
int motorCount = mount.getMotorCount();
int border = Rectangle.NO_BORDER; int border = Rectangle.NO_BORDER;
if (topBorder) { if (topBorder) {
@ -408,7 +416,6 @@ public class DesignReport {
totalTTW += ttw * motorCount; totalTTW += ttw * motorCount;
} }
} }
}
if (totalMotorCount > 1) { if (totalMotorCount > 1) {
int border = Rectangle.TOP; int border = Rectangle.TOP;

View File

@ -3,6 +3,8 @@
*/ */
package net.sf.openrocket.gui.print; package net.sf.openrocket.gui.print;
import java.awt.geom.Rectangle2D;
import net.sf.openrocket.gui.scalefigure.RocketFigure; import net.sf.openrocket.gui.scalefigure.RocketFigure;
import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.Rocket;
@ -27,7 +29,15 @@ public class PrintFigure extends RocketFigure {
updateFigure(); updateFigure();
} }
public double getFigureHeightPx() { public double getFigureHeight() {
return this.getSize().height; return this.subjectBounds_m.getHeight();
}
public double getFigureWidth() {
return this.subjectBounds_m.getWidth();
}
public Rectangle2D getDimensions() {
return this.subjectBounds_m.getBounds2D();
} }
} }

View File

@ -19,6 +19,10 @@ public class SymmetricComponentShapes extends RocketComponentShape {
// TODO: LOW: Uses only first component of cluster (not currently clusterable) // TODO: LOW: Uses only first component of cluster (not currently clusterable)
public static RocketComponentShape[] getShapesSide( final RocketComponent component, final Transformation transformation) { public static RocketComponentShape[] getShapesSide( final RocketComponent component, final Transformation transformation) {
return getShapesSide(component, transformation, 1.0d);
}
public static RocketComponentShape[] getShapesSide( final RocketComponent component, final Transformation transformation, final double scaleFactor ) {
SymmetricComponent c = (SymmetricComponent) component; SymmetricComponent c = (SymmetricComponent) component;
@ -82,14 +86,14 @@ public class SymmetricComponentShapes extends RocketComponentShape {
// TODO: LOW: curved path instead of linear // TODO: LOW: curved path instead of linear
Path2D.Double path = new Path2D.Double(); Path2D.Double path = new Path2D.Double();
path.moveTo((nose.x + points.get(len - 1).x) , (nose.y+points.get(len - 1).y) ); path.moveTo((nose.x + points.get(len - 1).x) * scaleFactor, (nose.y+points.get(len - 1).y) * scaleFactor);
for (i = len - 2; i >= 0; i--) { for (i = len - 2; i >= 0; i--) {
path.lineTo((nose.x+points.get(i).x), (nose.y+points.get(i).y) ); path.lineTo((nose.x+points.get(i).x) * scaleFactor, (nose.y+points.get(i).y) * scaleFactor);
} }
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
path.lineTo((nose.x+points.get(i).x) , (nose.y-points.get(i).y) ); path.lineTo((nose.x+points.get(i).x) * scaleFactor, (nose.y-points.get(i).y) * scaleFactor);
} }
path.lineTo((nose.x+points.get(len - 1).x) , (nose.y+points.get(len - 1).y) ); path.lineTo((nose.x+points.get(len - 1).x) * scaleFactor , (nose.y+points.get(len - 1).y) * scaleFactor);
path.closePath(); path.closePath();
//s[len] = path; //s[len] = path;

View File

@ -37,15 +37,15 @@ public class TransitionShapes extends RocketComponentShape {
double r2 = transition.getAftRadius(); double r2 = transition.getAftRadius();
Path2D.Float path = new Path2D.Float(); Path2D.Float path = new Path2D.Float();
path.moveTo( (frontCenter.x), (frontCenter.y+ r1)); path.moveTo( (frontCenter.x) * scaleFactor, (frontCenter.y+ r1) * scaleFactor);
path.lineTo( (frontCenter.x+length), (frontCenter.y+r2)); path.lineTo( (frontCenter.x+length) * scaleFactor, (frontCenter.y+r2) * scaleFactor);
path.lineTo( (frontCenter.x+length), (frontCenter.y-r2)); path.lineTo( (frontCenter.x+length) * scaleFactor, (frontCenter.y-r2) * scaleFactor);
path.lineTo( (frontCenter.x), (frontCenter.y-r1)); path.lineTo( (frontCenter.x) * scaleFactor, (frontCenter.y-r1) * scaleFactor);
path.closePath(); path.closePath();
mainShapes = new RocketComponentShape[] { new RocketComponentShape( path, component) }; mainShapes = new RocketComponentShape[] { new RocketComponentShape( path, component) };
} else { } else {
mainShapes = SymmetricComponentShapes.getShapesSide(component, transformation); mainShapes = SymmetricComponentShapes.getShapesSide(component, transformation, scaleFactor);
} }
Shape foreShoulder=null, aftShoulder=null; Shape foreShoulder=null, aftShoulder=null;
@ -57,7 +57,7 @@ public class TransitionShapes extends RocketComponentShape {
final Transformation offsetTransform = Transformation.getTranslationTransform(-transition.getForeShoulderLength(), 0, 0); final Transformation offsetTransform = Transformation.getTranslationTransform(-transition.getForeShoulderLength(), 0, 0);
final Transformation foreShoulderTransform = transformation.applyTransformation(offsetTransform); final Transformation foreShoulderTransform = transformation.applyTransformation(offsetTransform);
foreShoulder = TubeShapes.getShapesSide( foreShoulderTransform, shoulderLength, shoulderRadius); foreShoulder = TubeShapes.getShapesSide( foreShoulderTransform, shoulderLength, shoulderRadius, scaleFactor);
arrayLength++; arrayLength++;
} }
if (transition.getAftShoulderLength() > 0.0005) { if (transition.getAftShoulderLength() > 0.0005) {
@ -66,7 +66,7 @@ public class TransitionShapes extends RocketComponentShape {
final Transformation offsetTransform = Transformation.getTranslationTransform(transition.getLength(), 0, 0); final Transformation offsetTransform = Transformation.getTranslationTransform(transition.getLength(), 0, 0);
final Transformation aftShoulderTransform = transformation.applyTransformation(offsetTransform); final Transformation aftShoulderTransform = transformation.applyTransformation(offsetTransform);
aftShoulder = TubeShapes.getShapesSide(aftShoulderTransform, shoulderLength, shoulderRadius); aftShoulder = TubeShapes.getShapesSide(aftShoulderTransform, shoulderLength, shoulderRadius, scaleFactor);
arrayLength++; arrayLength++;
} }
if (foreShoulder==null && aftShoulder==null) if (foreShoulder==null && aftShoulder==null)

View File

@ -10,14 +10,19 @@ import net.sf.openrocket.util.Transformation;
public class TubeShapes extends RocketComponentShape { public class TubeShapes extends RocketComponentShape {
public static Shape getShapesSide( final Transformation transformation, final double length, final double radius ){ public static Shape getShapesSide( final Transformation transformation, final double length, final double radius ){
return getShapesSide(transformation, length, radius, 1.0d);
}
public static Shape getShapesSide( final Transformation transformation, final double length, final double radius, final double scaleFactor ){
final Coordinate instanceAbsoluteLocation = transformation.transform(Coordinate.ZERO); final Coordinate instanceAbsoluteLocation = transformation.transform(Coordinate.ZERO);
return new Rectangle2D.Double((instanceAbsoluteLocation.x), //x - the X coordinate of the upper-left corner of the newly constructed Rectangle2D return new Rectangle2D.Double((instanceAbsoluteLocation.x) * scaleFactor, //x - the X coordinate of the upper-left corner of the newly constructed Rectangle2D
(instanceAbsoluteLocation.y-radius), // y - the Y coordinate of the upper-left corner of the newly constructed Rectangle2D (instanceAbsoluteLocation.y-radius) * scaleFactor, // y - the Y coordinate of the upper-left corner of the newly constructed Rectangle2D
length, // w - the width of the newly constructed Rectangle2D length * scaleFactor, // w - the width of the newly constructed Rectangle2D
2*radius); // h - the height of the newly constructed Rectangle2D 2*radius * scaleFactor); // h - the height of the newly constructed Rectangle2D
} }
public static Shape getShapesBack( final Transformation transformation, final double radius ) { public static Shape getShapesBack( final Transformation transformation, final double radius ) {