/*
* PartsDetailVisitorStrategy.java
*/
package net.sf.openrocket.gui.print.visitor;
import java.util.List;
import java.util.Set;
import javax.swing.ImageIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.gui.main.ComponentIcons;
import net.sf.openrocket.gui.print.ITextHelper;
import net.sf.openrocket.gui.print.PrintUtilities;
import net.sf.openrocket.gui.print.PrintableFinSet;
import net.sf.openrocket.material.Material;
import net.sf.openrocket.preset.ComponentPreset;
import net.sf.openrocket.rocketcomponent.BodyComponent;
import net.sf.openrocket.rocketcomponent.BodyTube;
import net.sf.openrocket.rocketcomponent.Bulkhead;
import net.sf.openrocket.rocketcomponent.Coaxial;
import net.sf.openrocket.rocketcomponent.ExternalComponent;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.InnerTube;
import net.sf.openrocket.rocketcomponent.LaunchLug;
import net.sf.openrocket.rocketcomponent.MassObject;
import net.sf.openrocket.rocketcomponent.NoseCone;
import net.sf.openrocket.rocketcomponent.Parachute;
import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
import net.sf.openrocket.rocketcomponent.RecoveryDevice;
import net.sf.openrocket.rocketcomponent.RingComponent;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.ShockCord;
import net.sf.openrocket.rocketcomponent.Stage;
import net.sf.openrocket.rocketcomponent.Streamer;
import net.sf.openrocket.rocketcomponent.Transition;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Image;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Phrase;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.draw.VerticalPositionMark;
/**
* A visitor strategy for creating documentation about parts details.
*/
public class PartsDetailVisitorStrategy {
/**
* The logger.
*/
private static final Logger log = LoggerFactory.getLogger(PartsDetailVisitorStrategy.class);
/**
* The number of columns in the table.
*/
private static final int TABLE_COLUMNS = 7;
/**
* The parts detail is represented as an iText table.
*/
PdfPTable grid;
/**
* The iText document.
*/
protected Document document;
/**
* The direct iText writer.
*/
protected PdfWriter writer;
/**
* The stages selected.
*/
protected Set<Integer> stages;
/**
* State variable to track the level of hierarchy.
*/
protected int level = 0;
private static final String LINES = "Lines: ";
private static final String MASS = "Mass: ";
private static final String LEN = "Len: ";
private static final String THICK = "Thick: ";
private static final String INNER = "in ";
private static final String DIAMETER = "Dia";
private static final String OUTER = "out";
private static final String WIDTH = "Width";
private static final String LENGTH = "Length";
private static final String SHROUD_LINES = "Shroud Lines";
private static final String AFT_DIAMETER = "Aft Dia: ";
private static final String FORE_DIAMETER = "Fore Dia: ";
private static final String PARTS_DETAIL = "Parts Detail";
/**
* Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts.
*
* @param doc The iText document
* @param theWriter The direct iText writer
* @param theStagesToVisit The stages to be visited by this strategy
*/
public PartsDetailVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) {
document = doc;
writer = theWriter;
stages = theStagesToVisit;
PrintUtilities.addText(doc, PrintUtilities.BIG_BOLD, PARTS_DETAIL);
}
/**
* Print the parts detail.
*
* @param root the root component
*/
public void writeToDocument (final RocketComponent root) {
goDeep(root.getChildren());
}
/**
* Recurse through the given rocket component.
*
* @param theRc an array of rocket components; all children will be visited recursively
*/
protected void goDeep (final List<RocketComponent> theRc) {
level++;
for (RocketComponent rocketComponent : theRc) {
handle(rocketComponent);
}
level--;
}
/**
* Add a line to the detail report based upon the type of the component.
*
* @param component the component to print the detail for
*/
private void handle (RocketComponent component) {
//This ugly if-then-else construct is not object oriented. Originally it was an elegant, and very OO savy, design
//using the Visitor pattern. Unfortunately, it was misunderstood and was removed.
if (component instanceof Stage) {
try {
if (grid != null) {
document.add(grid);
}
document.add(ITextHelper.createPhrase(component.getName()));
grid = new PdfPTable(TABLE_COLUMNS);
grid.setWidthPercentage(100);
grid.setHorizontalAlignment(Element.ALIGN_LEFT);
}
catch (DocumentException e) {
}
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof LaunchLug) {
LaunchLug ll = (LaunchLug) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(ll.getMaterial()));
grid.addCell(createOuterInnerDiaCell(ll));
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
}
else if (component instanceof NoseCone) {
NoseCone nc = (NoseCone) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(nc.getMaterial()));
grid.addCell(ITextHelper.createCell(nc.getType().getName(), PdfPCell.BOTTOM));
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof Transition) {
Transition tran = (Transition) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(tran.getMaterial()));
Chunk fore = new Chunk(FORE_DIAMETER + toLength(tran.getForeRadius() * 2));
fore.setFont(PrintUtilities.NORMAL);
Chunk aft = new Chunk(AFT_DIAMETER + toLength(tran.getAftRadius() * 2));
aft.setFont(PrintUtilities.NORMAL);
final PdfPCell cell = ITextHelper.createCell();
cell.addElement(fore);
cell.addElement(aft);
grid.addCell(cell);
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof BodyTube) {
BodyTube bt = (BodyTube) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(bt.getMaterial()));
grid.addCell(createOuterInnerDiaCell(bt));
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof FinSet) {
handleFins((FinSet) component);
}
else if (component instanceof BodyComponent) {
grid.addCell(component.getName());
grid.completeRow();
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof ExternalComponent) {
ExternalComponent ext = (ExternalComponent) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(ext.getMaterial()));
grid.addCell(ITextHelper.createCell());
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof InnerTube) {
InnerTube it = (InnerTube) component;
grid.addCell(iconToImage(component));
final PdfPCell pCell = createNameCell(component.getName(), true, component.getPresetComponent());
grid.addCell(pCell);
grid.addCell(createMaterialCell(it.getMaterial()));
grid.addCell(createOuterInnerDiaCell(it));
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof RadiusRingComponent) {
RadiusRingComponent rrc = (RadiusRingComponent) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(rrc.getMaterial()));
if (component instanceof Bulkhead) {
grid.addCell(createDiaCell(rrc.getOuterRadius()*2));
}
else {
grid.addCell(createOuterInnerDiaCell(rrc));
}
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof RingComponent) {
RingComponent ring = (RingComponent) component;
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(ring.getMaterial()));
grid.addCell(createOuterInnerDiaCell(ring));
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
List<RocketComponent> rc = component.getChildren();
goDeep(rc);
}
else if (component instanceof ShockCord) {
ShockCord ring = (ShockCord) component;
PdfPCell cell = ITextHelper.createCell();
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setPaddingBottom(12f);
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(ring.getMaterial()));
grid.addCell(cell);
grid.addCell(createLengthCell(ring.getCordLength()));
grid.addCell(createMassCell(component.getMass()));
}
else if (component instanceof Parachute) {
Parachute chute = (Parachute) component;
PdfPCell cell = ITextHelper.createCell();
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setPaddingBottom(12f);
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(chute.getMaterial()));
// if (chute.hasSpillHole()) {
// grid.addCell(createOuterInnerDiaCell(chute.getDiameter()/2, chute.getSpillHoleDiameter()/2));
// }
// else {
grid.addCell(createDiaCell(chute.getDiameter()));
// }
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
grid.addCell(iconToImage(null));
grid.addCell(createNameCell(SHROUD_LINES, true, component.getPresetComponent()));
grid.addCell(createMaterialCell(chute.getLineMaterial()));
grid.addCell(createLinesCell(chute.getLineCount()));
grid.addCell(createLengthCell(chute.getLineLength()));
grid.addCell(cell);
}
else if (component instanceof Streamer) {
Streamer ring = (Streamer) component;
PdfPCell cell = ITextHelper.createCell();
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setPaddingBottom(12f);
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(ring.getMaterial()));
grid.addCell(createStrip(ring));
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
}
else if (component instanceof RecoveryDevice) {
RecoveryDevice device = (RecoveryDevice) component;
PdfPCell cell = ITextHelper.createCell();
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setPaddingBottom(12f);
grid.addCell(iconToImage(component));
grid.addCell(createNameCell(component.getName(), true, component.getPresetComponent()));
grid.addCell(createMaterialCell(device.getMaterial()));
grid.addCell(cell);
grid.addCell(createLengthCell(component.getLength()));
grid.addCell(createMassCell(component.getMass()));
}
else if (component instanceof MassObject) {
PdfPCell cell = ITextHelper.createCell();
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setPaddingBottom(12f);
grid.addCell(iconToImage(component));
final PdfPCell nameCell = createNameCell(component.getName(), true, component.getPresetComponent());
nameCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
nameCell.setPaddingBottom(12f);
grid.addCell(nameCell);
grid.addCell(cell);
grid.addCell(createDiaCell(((MassObject) component).getRadius() * 2));
grid.addCell(cell);
grid.addCell(createMassCell(component.getMass()));
}
}
/**
* Close the strategy by adding the last grid to the document.
*/
public void close () {
try {
if (grid != null) {
document.add(grid);
}
}
catch (DocumentException e) {
log.error("Could not write last cell to document.", e);
}
}
/**
* Create a cell to document an outer 'diameter'. This is used for components that have no inner diameter, such as
* a solid parachute or bulkhead.
*
* @param diameter the diameter in default length units
*
* @return a formatted cell containing the diameter
*/
private PdfPCell createDiaCell (final double diameter) {
PdfPCell result = new PdfPCell();
Phrase p = new Phrase();
p.setLeading(12f);
result.setVerticalAlignment(Element.ALIGN_TOP);
result.setBorder(Rectangle.BOTTOM);
Chunk c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(DIAMETER);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.SMALL);
c.append(OUTER);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(" " + toLength(diameter));
p.add(c);
result.addElement(p);
return result;
}
/**
* Create a PDF cell for a streamer.
*
* @param component a component that is a Coaxial
* @return the PDF cell that has the streamer documented
*/
private PdfPCell createStrip (final Streamer component) {
PdfPCell result = new PdfPCell();
Phrase p = new Phrase();
p.setLeading(12f);
result.setVerticalAlignment(Element.ALIGN_TOP);
result.setBorder(Rectangle.BOTTOM);
Chunk c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(LENGTH);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(" " + toLength(component.getStripLength()));
p.add(c);
result.addElement(p);
Phrase pw = new Phrase();
pw.setLeading(14f);
c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(WIDTH);
pw.add(c);
c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(" " + toLength(component.getStripWidth()));
pw.add(c);
result.addElement(pw);
return result;
}
/**
* Create a PDF cell that documents both an outer and an inner diameter of a component.
*
* @param component a component that is a Coaxial
*
* @return the PDF cell that has the outer and inner diameters documented
*/
private PdfPCell createOuterInnerDiaCell (final Coaxial component) {
return createOuterInnerDiaCell(component, INNER);
}
/**
* Create a PDF cell that documents both an outer and an inner diameter of a component.
*
* @param component a component that is a Coaxial
* @param innerLabel the label to use for the inner label subscript
*
* @return the PDF cell that has the outer and inner diameters documented
*/
private PdfPCell createOuterInnerDiaCell (final Coaxial component, final String innerLabel) {
return createOuterInnerDiaCell(component.getOuterRadius(), component.getInnerRadius(), innerLabel);
}
/**
* Create a PDF cell that documents both an outer and an inner diameter of a component.
*
* @param outerRadius the outer radius
* @param innerRadius the inner radius
* @param innerLabel the label to use for the inner label subscript
*
* @return the PDF cell that has the outer and inner diameters documented
*/
private PdfPCell createOuterInnerDiaCell (final double outerRadius, final double innerRadius, final String innerLabel) {
PdfPCell result = new PdfPCell();
Phrase p = new Phrase();
p.setLeading(12f);
result.setVerticalAlignment(Element.ALIGN_TOP);
result.setBorder(Rectangle.BOTTOM);
Chunk c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(DIAMETER);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.SMALL);
c.append(OUTER);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(" " + toLength(outerRadius * 2));
p.add(c);
createInnerDiaCell(innerRadius, result, innerLabel);
result.addElement(p);
return result;
}
/**
* Add inner diameter data to a cell.
*
* @param innerRadius the inner radius
* @param cell the PDF cell to add the inner diameter data to
* @param innerLabel the label to use for the inner label subscript
*/
private void createInnerDiaCell (final double innerRadius, PdfPCell cell, final String innerLabel) {
Phrase p = new Phrase();
p.setLeading(14f);
Chunk c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(DIAMETER);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.SMALL);
c.append(innerLabel);
p.add(c);
c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(" " + toLength(innerRadius * 2));
p.add(c);
cell.addElement(p);
}
/**
* Add PDF cells for a fin set.
*
* @param theFinSet the fin set
*/
private void handleFins (FinSet theFinSet) {
Image img = null;
java.awt.Image awtImage = new PrintableFinSet(theFinSet).createImage();
try {
img = Image.getInstance(writer, awtImage, 0.25f);
}
catch (Exception e) {
log.error("Could not write image to document.", e);
}
grid.addCell(iconToImage(theFinSet));
grid.addCell(createNameCell(theFinSet.getName() + " (" + theFinSet.getFinCount() + ")", true));
grid.addCell(createMaterialCell(theFinSet.getMaterial()));
grid.addCell(ITextHelper.createCell(THICK + toLength(theFinSet.getThickness()), PdfPCell.BOTTOM));
final PdfPCell pCell = new PdfPCell();
pCell.setBorder(Rectangle.BOTTOM);
pCell.addElement(img);
grid.addCell(ITextHelper.createCell());
grid.addCell(createMassCell(theFinSet.getMass()));
List<RocketComponent> rc = theFinSet.getChildren();
goDeep(rc);
}
/**
* Create a length formatted cell.
*
* @param length the length, in default length units
*
* @return a PdfPCell that is formatted with the length
*/
protected PdfPCell createLengthCell (double length) {
return ITextHelper.createCell(LEN + toLength(length), PdfPCell.BOTTOM);
}
/**
* Create a mass formatted cell.
*
* @param mass the mass, in default mass units
*
* @return a PdfPCell that is formatted with the mass
*/
protected PdfPCell createMassCell (double mass) {
return ITextHelper.createCell(MASS + toMass(mass), PdfPCell.BOTTOM);
}
/**
* Create a (shroud) line count formatted cell.
*
* @param count the number of shroud lines
*
* @return a PdfPCell that is formatted with the line count
*/
protected PdfPCell createLinesCell (int count) {
return ITextHelper.createCell(LINES + count, PdfPCell.BOTTOM);
}
/**
* Create a cell formatted for a name (or any string for that matter).
*
* @param v the string to format into a PDF cell
* @param withIndent if true, then an indention is made scaled to the level of the part in the parent hierarchy
*
* @return a PdfPCell that is formatted with the string <code>v</code>
*/
protected PdfPCell createNameCell (String v, boolean withIndent) {
return createNameCell(v, withIndent, null);
}
/**
* Create a cell formatted for a name (or any string for that matter).
*
* @param v the string to format into a PDF cell
* @param withIndent if true, then an indention is made scaled to the level of the part in the parent hierarchy
* @param preset the component's preset, if it has one
*
* @return a PdfPCell that is formatted with the string <code>v</code>
*/
protected PdfPCell createNameCell (String v, boolean withIndent, ComponentPreset preset) {
PdfPCell result = new PdfPCell();
result.setColspan(2);
result.setBorder(Rectangle.BOTTOM);
Paragraph para = new Paragraph();
para.setLeading(12f, 0);
Chunk c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
Chunk tab1 =
new Chunk(new VerticalPositionMark(), (level - 2) * 10, true);
if (withIndent) {
para.add(new Chunk(tab1));
}
c.append(v);
para.add(c);
//Add the preset's manufacturer and part no in a subscript font.
if (preset != null) {
para.add(Chunk.NEWLINE);
c = new Chunk();
if (withIndent) {
para.add(new Chunk(tab1));
}
c.setFont(PrintUtilities.SMALL);
StringBuffer sb = new StringBuffer();
sb.append(preset.getManufacturer()).append(" ").append(preset.getPartNo());
c.append(sb.toString());
para.add(c);
}
result.addElement(para);
return result;
}
/**
* Create a cell that describes a material.
*
* @param material the material
*
* @return a PdfPCell that is formatted with a description of the material
*/
protected PdfPCell createMaterialCell (Material material) {
PdfPCell cell = ITextHelper.createCell();
cell.setLeading(13f, 0);
Chunk c = new Chunk();
c.setFont(PrintUtilities.NORMAL);
c.append(toMaterialName(material));
cell.addElement(c);
Chunk density = new Chunk();
density.setFont(PrintUtilities.SMALL);
density.append(toMaterialDensity(material));
cell.addElement(density);
return cell;
}
/**
* Get the icon of the particular type of rocket component and conver it to an image in a PDF cell.
*
* @param visitable the rocket component to create a cell with it's image
*
* @return a PdfPCell that is just an image that can be put into a PDF
*/
protected PdfPCell iconToImage (final RocketComponent visitable) {
if (visitable != null) {
final ImageIcon icon = (ImageIcon) ComponentIcons.getLargeIcon(visitable.getClass());
try {
if (icon != null) {
Image im = Image.getInstance(icon.getImage(), null);
if (im != null) {
im.scaleToFit(icon.getIconWidth() * 0.6f, icon.getIconHeight() * 0.6f);
PdfPCell cell = new PdfPCell(im);
cell.setFixedHeight(icon.getIconHeight() * 0.6f);
cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setBorder(PdfPCell.NO_BORDER);
return cell;
}
}
}
catch (Exception e) {
}
}
PdfPCell cell = new PdfPCell();
cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);
cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE);
cell.setBorder(PdfPCell.NO_BORDER);
return cell;
}
/**
* Format the length as a displayable string.
*
* @param length the length (assumed to be in default length units)
*
* @return a string representation of the length with unit abbreviation
*/
protected String toLength (double length) {
final Unit defaultUnit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
return defaultUnit.toStringUnit(length);
}
/**
* Format the mass as a displayable string.
*
* @param mass the mass (assumed to be in default mass units)
*
* @return a string representation of the mass with mass abbreviation
*/
protected String toMass (double mass) {
final Unit defaultUnit = UnitGroup.UNITS_MASS.getDefaultUnit();
return defaultUnit.toStringUnit(mass);
}
/**
* Get a displayable string of the material's name.
*
* @param material the material to output
*
* @return the material name
*/
protected String toMaterialName (Material material) {
return material.getName();
}
/**
* Format the material density as a displayable string.
*
* @param material the material to output
*
* @return a string representation of the material density
*/
protected String toMaterialDensity (Material material) {
return " (" + material.getType().getUnitGroup().getDefaultUnit().toStringUnit(material.getDensity()) + ")";
}
}