diff --git a/core/src/net/sf/openrocket/gui/SpinnerEditor.java b/core/src/net/sf/openrocket/gui/SpinnerEditor.java index 843eed6a7..6d2ca1b89 100644 --- a/core/src/net/sf/openrocket/gui/SpinnerEditor.java +++ b/core/src/net/sf/openrocket/gui/SpinnerEditor.java @@ -9,13 +9,13 @@ import javax.swing.JSpinner; * @author Sampo Niskanen */ -public class SpinnerEditor extends JSpinner.NumberEditor { -//public class SpinnerEditor extends JSpinner.DefaultEditor { +//public class SpinnerEditor extends JSpinner.NumberEditor { +public class SpinnerEditor extends JSpinner.DefaultEditor { public SpinnerEditor(JSpinner spinner) { - //super(spinner); - super(spinner,"0.0##"); - //getTextField().setEditable(true); + super(spinner); + //super(spinner,"0.0##"); + getTextField().setEditable(true); } } diff --git a/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java b/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java index cd7066f85..12676fcb3 100644 --- a/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java +++ b/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java @@ -10,10 +10,10 @@ import java.util.EventListener; import java.util.EventObject; import javax.swing.AbstractAction; +import javax.swing.AbstractSpinnerModel; import javax.swing.Action; import javax.swing.BoundedRangeModel; import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; @@ -23,6 +23,7 @@ import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.FractionUtil; import net.sf.openrocket.util.Invalidatable; import net.sf.openrocket.util.Invalidator; import net.sf.openrocket.util.MathUtil; @@ -47,23 +48,23 @@ import net.sf.openrocket.util.StateChangeListener; public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable { private static final LogHelper log = Application.getLogger(); - + public static final DoubleModel ZERO = new DoubleModel(0); - + //////////// JSpinner Model //////////// - + /** * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel * to be compatible with the NumberEditor, but only has the necessary methods defined. */ - private class ValueSpinnerModel extends SpinnerNumberModel implements Invalidatable { - + public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable { + @Override public Object getValue() { - return currentUnit.toUnit(DoubleModel.this.getValue()); + return currentUnit.toString(DoubleModel.this.getValue()); } - + @Override public void setValue(Object value) { if (firing > 0) { @@ -72,16 +73,28 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat " value=" + value + ", currently firing events"); return; } - Number num = (Number) value; + Number num = 0; + if ( value instanceof Number ) { + num = (Number)value; + } else if ( value instanceof String ) { + try { + String newValString = (String)value; + num = FractionUtil.parseFraction(newValString); + } + catch ( java.lang.NumberFormatException nfex ) { + num = 0.0d; + } + } + double newValue = num.doubleValue(); double converted = currentUnit.fromUnit(newValue); - + log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue + " converted=" + converted); DoubleModel.this.setValue(converted); - + } - + @Override public Object getNextValue() { double d = currentUnit.toUnit(DoubleModel.this.getValue()); @@ -93,7 +106,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat d = max; return d; } - + @Override public Object getPreviousValue() { double d = currentUnit.toUnit(DoubleModel.this.getValue()); @@ -105,35 +118,33 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat d = min; return d; } - - +/* FIXME - put min & max back @Override public Comparable getMinimum() { return currentUnit.toUnit(minValue); } - + @Override public Comparable getMaximum() { return currentUnit.toUnit(maxValue); } - - +*/ @Override public void addChangeListener(ChangeListener l) { DoubleModel.this.addChangeListener(l); } - + @Override public void removeChangeListener(ChangeListener l) { DoubleModel.this.removeChangeListener(l); } - + @Override public void invalidate() { DoubleModel.this.invalidate(); } } - + /** * Returns a new SpinnerModel with the same base as the DoubleModel. * The values given to the JSpinner are in the currently selected units. @@ -143,50 +154,46 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public SpinnerModel getSpinnerModel() { return new ValueSpinnerModel(); } - - - - //////////// JSlider model //////////// - + private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable { private static final int MAX = 1000; - + /* * Use linear scale value = linear1 * x + linear0 when x < linearPosition * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise */ private final boolean islinear; - + // Linear in range x <= linearPosition private final double linearPosition; - + // May be changing DoubleModels when using linear model private final DoubleModel min, mid, max; - + // Linear multiplier and constant //private final double linear1; //private final double linear0; - + // Non-linear multiplier, exponent and constant private double quad2, quad1, quad0; public ValueSliderModel(DoubleModel min, DoubleModel max) { this.islinear = true; linearPosition = 1.0; - + this.min = min; this.mid = max; // Never use exponential scale this.max = max; - + min.addChangeListener(this); max.addChangeListener(this); - + quad2 = quad1 = quad0 = 0; // Not used } - - + + /** * Generate a linear model from min to max. @@ -194,18 +201,18 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public ValueSliderModel(double min, double max) { this.islinear = true; linearPosition = 1.0; - + this.min = new DoubleModel(min); this.mid = new DoubleModel(max); // Never use exponential scale this.max = new DoubleModel(max); - + quad2 = quad1 = quad0 = 0; // Not used } - + public ValueSliderModel(double min, double mid, double max) { this(min, 0.5, mid, max); } - + public ValueSliderModel(double min, double mid, DoubleModel max) { this(min, 0.5, mid, max); } @@ -226,22 +233,22 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat this.max = max; this.islinear = false; - + max.addChangeListener(this); - + linearPosition = pos; //linear0 = min; //linear1 = (mid-min)/pos; - + if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) { throw new IllegalArgumentException("Bad arguments for ValueSliderModel " + "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos); } - + updateExponentialParameters(); - + } - + private void updateExponentialParameters() { double pos = this.linearPosition; double minValue = this.min.getValue(); @@ -258,11 +265,11 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1); quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1); } - + private double pow2(double x) { return x * x; } - + @Override public int getValue() { double value = DoubleModel.this.getValue(); @@ -270,13 +277,13 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat return 0; if (value >= max.getValue()) return MAX; - + double x; if (value <= mid.getValue()) { // Use linear scale //linear0 = min; //linear1 = (mid-min)/pos; - + x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue()); } else { // Use quadratic scale @@ -286,8 +293,8 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat } return (int) (x * MAX); } - - + + @Override public void setValue(int newValue) { if (firing > 0) { @@ -296,91 +303,91 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat " value=" + newValue + ", currently firing events"); return; } - + double x = (double) newValue / MAX; double scaledValue; - + if (x <= linearPosition) { // Use linear scale //linear0 = min; //linear1 = (mid-min)/pos; - + scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue(); } else { // Use quadratic scale scaledValue = quad2 * x * x + quad1 * x + quad0; } - + double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue))); log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue + " scaledValue=" + scaledValue + " converted=" + converted); DoubleModel.this.setValue(converted); } - - + + // Static get-methods private boolean isAdjusting; - + @Override public int getExtent() { return 0; } - + @Override public int getMaximum() { return MAX; } - + @Override public int getMinimum() { return 0; } - + @Override public boolean getValueIsAdjusting() { return isAdjusting; } - + // Ignore set-values @Override public void setExtent(int newExtent) { } - + @Override public void setMaximum(int newMaximum) { } - + @Override public void setMinimum(int newMinimum) { } - + @Override public void setValueIsAdjusting(boolean b) { isAdjusting = b; } - + @Override public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) { setValueIsAdjusting(adjusting); setValue(value); } - + // Pass change listeners to the underlying model @Override public void addChangeListener(ChangeListener l) { DoubleModel.this.addChangeListener(l); } - + @Override public void removeChangeListener(ChangeListener l) { DoubleModel.this.removeChangeListener(l); } - + @Override public void invalidate() { DoubleModel.this.invalidate(); } - + @Override public void stateChanged(EventObject e) { // Min or max range has changed. @@ -394,48 +401,48 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat fireStateChanged(); } } - - + + public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) { return new ValueSliderModel(min, max); } - + public BoundedRangeModel getSliderModel(double min, double max) { return new ValueSliderModel(min, max); } - + public BoundedRangeModel getSliderModel(double min, double mid, double max) { return new ValueSliderModel(min, mid, max); } - + public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) { return new ValueSliderModel(min, mid, max); } - + public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) { return new ValueSliderModel(min, pos, mid, max); } - - + + //////////// Action model //////////// - + private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable { private boolean oldValue = false; - + public AutomaticActionModel() { oldValue = isAutomatic(); addChangeListener(this); } - - + + @Override public boolean isEnabled() { return isAutomaticAvailable(); } - + @Override public Object getValue(String key) { if (key.equals(Action.SELECTED_KEY)) { @@ -444,7 +451,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat } return super.getValue(key); } - + @Override public void putValue(String key, Object value) { if (firing > 0) { @@ -463,24 +470,24 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat super.putValue(key, value); } } - + // Implement a wrapper to the ChangeListeners ArrayList propertyChangeListeners = new ArrayList(); - + @Override public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeListeners.add(listener); DoubleModel.this.addChangeListener(this); } - + @Override public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeListeners.remove(listener); if (propertyChangeListeners.isEmpty()) DoubleModel.this.removeChangeListener(this); } - + // If the value has changed, generate an event to the listeners @Override public void stateChanged(EventObject e) { @@ -495,18 +502,18 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat ((PropertyChangeListener) l[i]).propertyChange(event); } } - + @Override public void actionPerformed(ActionEvent e) { // Setting performed in putValue } - + @Override public void invalidate() { DoubleModel.this.invalidate(); } } - + /** * Returns a new Action corresponding to the changes of the automatic setting * property of the value model. This may be used directly with e.g. check buttons. @@ -516,13 +523,13 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public Action getAutomaticAction() { return new AutomaticActionModel(); } - - + + //////////// Main model ///////////// - + /* * The main model handles all values in SI units, i.e. no conversion is made within the model. */ @@ -530,34 +537,34 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat private final ChangeSource source; private final String valueName; private final double multiplier; - + private final Method getMethod; private final Method setMethod; - + private final Method getAutoMethod; private final Method setAutoMethod; - + private final ArrayList listeners = new ArrayList(); - + private final UnitGroup units; private Unit currentUnit; - + private final double minValue; private double maxValue; - + private String toString = null; - + private int firing = 0; // >0 when model itself is sending events - + // Used to differentiate changes in valueName and other changes in the component: private double lastValue = 0; private boolean lastAutomatic = false; - + private Invalidator invalidator = new Invalidator(this); - - + + /** * Generate a DoubleModel that contains an internal double value. * @@ -566,7 +573,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public DoubleModel(double value) { this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } - + /** * Generate a DoubleModel that contains an internal double value. * @@ -576,7 +583,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public DoubleModel(double value, UnitGroup unit) { this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } - + /** * Generate a DoubleModel that contains an internal double value. * @@ -587,7 +594,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public DoubleModel(double value, UnitGroup unit, double min) { this(value, unit, min, Double.POSITIVE_INFINITY); } - + /** * Generate a DoubleModel that contains an internal double value. * @@ -600,18 +607,18 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat this.lastValue = value; this.minValue = min; this.maxValue = max; - + source = null; valueName = "Constant value"; multiplier = 1; - + getMethod = setMethod = null; getAutoMethod = setAutoMethod = null; units = unit; currentUnit = units.getDefaultUnit(); } - - + + /** * Generates a new DoubleModel that changes the values of the specified component. * The double value is read and written using the methods "get"/"set" + valueName. @@ -627,37 +634,37 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat this.source = source; this.valueName = valueName; this.multiplier = multiplier; - + this.units = unit; currentUnit = units.getDefaultUnit(); - + this.minValue = min; this.maxValue = max; - + try { getMethod = source.getClass().getMethod("get" + valueName); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("get method for value '" + valueName + "' not present in class " + source.getClass().getCanonicalName()); } - + Method s = null; try { s = source.getClass().getMethod("set" + valueName, double.class); } catch (NoSuchMethodException e1) { } // Ignore setMethod = s; - + // Automatic selection methods - + Method set = null, get = null; - + try { get = source.getClass().getMethod("is" + valueName + "Automatic"); set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class); } catch (NoSuchMethodException e) { } // ignore - + if (set != null && get != null) { getAutoMethod = get; setAutoMethod = set; @@ -665,45 +672,45 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat getAutoMethod = null; setAutoMethod = null; } - + } - + public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit, double min) { this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY); } - + public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) { this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } - + public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min, double max) { this(source, valueName, 1.0, unit, min, max); } - + public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) { this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY); } - + public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) { this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } - + public DoubleModel(ChangeSource source, String valueName) { this(source, valueName, 1.0, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } - + public DoubleModel(ChangeSource source, String valueName, double min) { this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY); } - + public DoubleModel(ChangeSource source, String valueName, double min, double max) { this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max); } - - + + /** * Returns the value of the variable (in SI units). @@ -711,7 +718,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public double getValue() { if (getMethod == null) // Constant value return lastValue; - + try { return (Double) getMethod.invoke(source) * multiplier; } catch (IllegalArgumentException e) { @@ -722,14 +729,14 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat throw Reflection.handleWrappedException(e); } } - + /** * Sets the value of the variable. * @param v New value for parameter in SI units. */ public void setValue(double v) { checkState(true); - + log.debug("Setting value " + v + " for " + this); if (setMethod == null) { if (getMethod != null) { @@ -740,7 +747,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat fireStateChanged(); return; } - + try { setMethod.invoke(source, v / multiplier); } catch (IllegalArgumentException e) { @@ -751,14 +758,14 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat throw Reflection.handleWrappedException(e); } } - + /** * Returns whether setting the value automatically is available. */ public boolean isAutomaticAvailable() { return (getAutoMethod != null) && (setAutoMethod != null); } - + /** * Returns whether the value is currently being set automatically. * Returns false if automatic setting is not available at all. @@ -766,7 +773,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public boolean isAutomatic() { if (getAutoMethod == null) return false; - + try { return (Boolean) getAutoMethod.invoke(source); } catch (IllegalArgumentException e) { @@ -777,20 +784,20 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat throw Reflection.handleWrappedException(e); } } - + /** * Sets whether the value should be set automatically. Simply fires a * state change event if automatic setting is not available. */ public void setAutomatic(boolean auto) { checkState(true); - + if (setAutoMethod == null) { log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available"); fireStateChanged(); // in case something is out-of-sync return; } - + log.debug("Setting automatic to " + auto + " for " + this); lastAutomatic = auto; try { @@ -803,8 +810,8 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat throw Reflection.handleWrappedException(e); } } - - + + /** * Returns the current Unit. At the beginning it is the default unit of the UnitGroup. * @return The most recently set unit. @@ -812,7 +819,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public Unit getCurrentUnit() { return currentUnit; } - + /** * Sets the current Unit. The unit must be one of those included in the UnitGroup. * @param u The unit to set active. @@ -825,8 +832,8 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat currentUnit = u; fireStateChanged(); } - - + + /** * Returns the UnitGroup associated with the parameter value. * @@ -835,8 +842,8 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public UnitGroup getUnitGroup() { return units; } - - + + /** * Add a listener to the model. Adds the model as a listener to the value source if this @@ -846,7 +853,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat @Override public void addChangeListener(EventListener l) { checkState(true); - + if (listeners.isEmpty()) { if (source != null) { source.addChangeListener(this); @@ -854,11 +861,11 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat lastAutomatic = isAutomatic(); } } - + listeners.add(l); log.verbose(this + " adding listener (total " + listeners.size() + "): " + l); } - + /** * Remove a listener from the model. Removes the model from being a listener to the Component * if this was the last listener of the model. @@ -867,15 +874,15 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat @Override public void removeChangeListener(EventListener l) { checkState(false); - + listeners.remove(l); if (listeners.isEmpty() && source != null) { source.removeChangeListener(this); } log.verbose(this + " removing listener (total " + listeners.size() + "): " + l); } - - + + /** * Invalidates this model by removing all listeners and removing this from * listening to the source. After invalidation no listeners can be added to this @@ -885,7 +892,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat public void invalidate() { log.verbose("Invalidating " + this); invalidator.invalidate(); - + if (!listeners.isEmpty()) { log.warn("Invalidating " + this + " while still having listeners " + listeners); } @@ -895,13 +902,13 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat } MemoryManagement.collectable(this); } - - + + private void checkState(boolean error) { invalidator.check(error); } - - + + @Override protected void finalize() throws Throwable { super.finalize(); @@ -909,14 +916,14 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat log.warn(this + " being garbage-collected while having listeners " + listeners); } }; - - + + /** * Fire a ChangeEvent to all listeners. */ protected void fireStateChanged() { checkState(true); - + EventObject event = new EventObject(this); ChangeEvent cevent = new ChangeEvent(this); firing++; @@ -931,7 +938,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat } firing--; } - + /** * Called when the component changes. Checks whether the modeled value has changed, and if * it has, updates lastValue and generates ChangeEvents for all listeners of the model. @@ -939,7 +946,7 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat @Override public void stateChanged(EventObject e) { checkState(true); - + double v = getValue(); boolean b = isAutomatic(); if (lastValue == v && lastAutomatic == b) @@ -948,8 +955,8 @@ public class DoubleModel implements StateChangeListener, ChangeSource, Invalidat lastAutomatic = b; fireStateChanged(); } - - + + /** * Explain the DoubleModel as a String. */ diff --git a/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java b/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java index 9805a02eb..4320f0699 100644 --- a/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java +++ b/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java @@ -3,19 +3,11 @@ */ package net.sf.openrocket.gui.print.visitor; -import com.itextpdf.text.Chunk; -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Element; -import com.itextpdf.text.Font; -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; +import java.util.List; +import java.util.Set; + +import javax.swing.ImageIcon; + import net.sf.openrocket.gui.main.ComponentIcons; import net.sf.openrocket.gui.print.ITextHelper; import net.sf.openrocket.gui.print.PrintUtilities; @@ -45,13 +37,20 @@ import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; -import javax.swing.*; -import java.text.NumberFormat; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import com.itextpdf.text.Chunk; +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Element; +import com.itextpdf.text.Font; +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. @@ -539,8 +538,6 @@ public class PartsDetailVisitorStrategy { Image img = null; java.awt.Image awtImage = new PrintableFinSet(theFinSet).createImage(); - Collection x = theFinSet.getComponentBounds(); - try { img = Image.getInstance(writer, awtImage, 0.25f); } @@ -714,7 +711,7 @@ public class PartsDetailVisitorStrategy { */ protected String toLength (double length) { final Unit defaultUnit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(length)) + defaultUnit.toString(); + return defaultUnit.toStringUnit(length); } /** @@ -726,7 +723,7 @@ public class PartsDetailVisitorStrategy { */ protected String toMass (double mass) { final Unit defaultUnit = UnitGroup.UNITS_MASS.getDefaultUnit(); - return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(mass)) + defaultUnit.toString(); + return defaultUnit.toStringUnit(mass); } /** diff --git a/core/src/net/sf/openrocket/unit/FractionalUnit.java b/core/src/net/sf/openrocket/unit/FractionalUnit.java new file mode 100644 index 000000000..264364780 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/FractionalUnit.java @@ -0,0 +1,188 @@ +package net.sf.openrocket.unit; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; + +public class FractionalUnit extends Unit { + + // This is the base of the fractions. ie, 16d for 1/16ths. + private final int fractionBase; + // This is 1d/fractionBase; + private final double fractionValue; + + // This is the value used when incrementing/decrementing. + private final double incrementValue; + + // If the actual value differs from the decimal representation by more than this, + // we display as decimals. + private final double epsilon; + + private final String unitLabel; + + public FractionalUnit(double multiplier, String unit, String unitLabel, int fractionBase, double incrementValue) { + this( multiplier, unit, unitLabel, fractionBase, incrementValue, 0.1d/fractionBase); + } + + public FractionalUnit(double multiplier, String unit, String unitLabel, int fractionBase, double incrementValue, double epsilon) { + super(multiplier, unit); + this.unitLabel = unitLabel; + this.fractionBase = fractionBase; + this.fractionValue = 1.0d/fractionBase; + this.incrementValue = incrementValue; + this.epsilon = epsilon; + } + + @Override + public double round(double value) { + return roundTo( value, fractionValue ); + } + + private double roundTo( double value, double fraction ) { + double remainder = Math.IEEEremainder( value, fraction ); + return value - remainder; + } + + @Override + public double getNextValue(double value) { + double rounded = roundTo(value, incrementValue); + if ( rounded <= value + epsilon) { + rounded += incrementValue; + } + return rounded; + } + + @Override + public double getPreviousValue(double value) { + double rounded = roundTo(value, incrementValue); + if ( rounded >= value - epsilon ) { + rounded -= incrementValue; + } + return rounded; + } + + @Override + public Tick[] getTicks(double start, double end, double minor, double major) { + // Convert values + start = toUnit(start); + end = toUnit(end); + minor = toUnit(minor); + major = toUnit(major); + + if (minor <= 0 || major <= 0 || major < minor) { + throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major); + } + + ArrayList ticks = new ArrayList(); + + int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable + double minstep; + + // Find the smallest possible step size + double one=1; + while (one > minor) + one /= 2; + while (one < minor) + one *= 2; + minstep = one; + mod2 = 16; + + // Find step size for major ticks + one = 1; + while (one > major) + one /= 10; + while (one < major) + one *= 10; + if (one/2 >= major) { + // major step is round-five, major-notable is next round-ten + double majorstep = one/2; + mod3 = (int)Math.round(majorstep/minstep); + mod4 = mod3*2; + } else { + // major step is round-ten, major-notable is next round-ten + mod3 = (int)Math.round(one/minstep); + mod4 = mod3*10; + } + // Check for clashes between minor-notable and major-nonnotable + if (mod3 == mod2) { + if (mod2==2) + mod2 = 1; // Every minor tick is notable + else + mod2 = 5; // Every fifth minor tick is notable + } + + + // Calculate starting position + int pos = (int)Math.ceil(start/minstep); + // System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4); + while (pos*minstep <= end) { + double unitValue = pos*minstep; + double value = fromUnit(unitValue); + + if (pos%mod4 == 0) + ticks.add(new Tick(value,unitValue,true,true)); + else if (pos%mod3 == 0) + ticks.add(new Tick(value,unitValue,true,false)); + else if (pos%mod2 == 0) + ticks.add(new Tick(value,unitValue,false,true)); + else + ticks.add(new Tick(value,unitValue,false,false)); + + pos++; + } + + return ticks.toArray(new Tick[0]); + } + + + @Override + public String toString(double value) { + + double correctVal = toUnit(value); + double val = round(correctVal); + + + if ( Math.abs( val - correctVal ) > epsilon ) { + NumberFormat decFormat = new DecimalFormat("#.###"); + return decFormat.format(correctVal); + } + + NumberFormat intFormat = new DecimalFormat("#"); + double sign = Math.signum(val); + + double posValue = sign * val; + + double intPart = Math.floor(posValue); + + double frac = Math.rint((posValue - intPart)/fractionValue); + double fracBase = fractionBase; + + // Reduce fraction. + while ( frac > 0 && fracBase > 2 && frac % 2 == 0 ) { + frac /= 2.0; + fracBase /= 2.0; + } + + posValue *= sign; + + if ( frac == 0.0 ) { + return intFormat.format(posValue); + } else if (intPart == 0.0 ){ + return intFormat.format(sign*frac) + "/" + intFormat.format(fracBase); + } else { + return intFormat.format(sign*intPart) + " " + intFormat.format(frac) + "/" + intFormat.format(fracBase); + } + + } + + @Override + public String toStringUnit(double value) { + if (Double.isNaN(value)) + return "N/A"; + + String s = toString(value); + s += " " + unitLabel; + return s; + } + +} diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 5a0b692c9..dcd0003bc 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -84,6 +84,7 @@ public class UnitGroup { UNITS_LENGTH.addUnit(new GeneralUnit(0.01, "cm")); UNITS_LENGTH.addUnit(new GeneralUnit(1, "m")); UNITS_LENGTH.addUnit(new GeneralUnit(0.0254, "in")); + UNITS_LENGTH.addUnit(new FractionalUnit(0.0254, "in/64", "in", 64, 1d/16d)); UNITS_LENGTH.addUnit(new GeneralUnit(0.3048, "ft")); UNITS_LENGTH.setDefaultUnit(1); diff --git a/core/src/net/sf/openrocket/util/FractionUtil.java b/core/src/net/sf/openrocket/util/FractionUtil.java new file mode 100644 index 000000000..2eff84df0 --- /dev/null +++ b/core/src/net/sf/openrocket/util/FractionUtil.java @@ -0,0 +1,53 @@ +package net.sf.openrocket.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class FractionUtil { + + + private final static Pattern fractionPattern = Pattern.compile("(-?\\d+)/(\\d+)"); + private final static Pattern mixedFractionPattern = Pattern.compile("(-?\\d+)\\s+(\\d+)/(\\d+)"); + /** + * Parse a double from a string supporting fraction formats. + * + * Will parse fractions specified with '/'. Mixed numbers separated by ' ' (space). + * If no fraction is found in the input string, it is parsed with Double.parseDouble() + * which my throw the runtime exception java.lang.NumberFormatException. + * + * Valid input may look like: + * + * "1/4" = 0.25d + * "-1/4" = -0.25d + * "1 1/4" = 1.25d + * "-1 1/4" = 1.25d + * "1.25" = 1.25d + * + * @param str + * @return + */ + public static Double parseFraction( String str ) { + + if ( str == null ) { + throw new java.lang.NumberFormatException("null String"); + } + + Matcher m1 = mixedFractionPattern.matcher(str); + if ( m1.find() ) { + double wholepart = Double.parseDouble(m1.group(1)); + double num = Double.parseDouble(m1.group(2)); + double den = Double.parseDouble(m1.group(3)); + return wholepart + Math.copySign(num,wholepart) / den; + } + + Matcher m2 = fractionPattern.matcher(str); + if( m2.find() ) { + double num = Double.parseDouble(m2.group(1)); + double den = Double.parseDouble(m2.group(2)); + return num / den; + } + + return Double.parseDouble(str); + } + +} diff --git a/core/test/net/sf/openrocket/unit/FractionalUnitTest.java b/core/test/net/sf/openrocket/unit/FractionalUnitTest.java new file mode 100644 index 000000000..1c2b0d5e2 --- /dev/null +++ b/core/test/net/sf/openrocket/unit/FractionalUnitTest.java @@ -0,0 +1,206 @@ +package net.sf.openrocket.unit; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class FractionalUnitTest { + + private final static Unit testUnit = new FractionalUnit(1, "unit", "unit", 4, 0.5); + private final static Unit testUnitApprox = new FractionalUnit(1, "unit", "unit", 16, 0.5, 0.02); + + private final static Unit inchUnit = new FractionalUnit(0.0254, "in/64", "in", 64, 1d/16d); + + @Test + public void testRound() { + + assertEquals(-1d, testUnit.round(-1.125), 0.0); // rounds to -1 since mod is even + assertEquals(-1d, testUnit.round(-1.0), 0.0); + assertEquals(-1d, testUnit.round(-.875), 0.0); // rounds to -1 since mod is even + + assertEquals(-0.75d, testUnit.round(-.874), 0.0); + assertEquals(-0.75d, testUnit.round(-.75), 0.0); + assertEquals(-0.75d, testUnit.round(-.626), 0.0); + + assertEquals(-0.5d, testUnit.round(-.625), 0.0); // rounds to -.5 since mod is even + assertEquals(-0.5d, testUnit.round(-.5), 0.0); + assertEquals(-0.5d, testUnit.round(-.375), 0.0); // rounds to -.5 since mod is even + + assertEquals(-0.25d, testUnit.round(-.374), 0.0); + assertEquals(-0.25d, testUnit.round(-.25), 0.0); + assertEquals(-0.25d, testUnit.round(-.126), 0.0); + + assertEquals(0d, testUnit.round(-.125), 0.0); + assertEquals(0d, testUnit.round(0), 0.0); + assertEquals(0d, testUnit.round(.125), 0.0); + + assertEquals(0.25d, testUnit.round(.126), 0.0); + assertEquals(0.25d, testUnit.round(.25), 0.0); + assertEquals(0.25d, testUnit.round(.374), 0.0); + + assertEquals(0.5d, testUnit.round(.375), 0.0); // rounds to .5 since mod is even + assertEquals(0.5d, testUnit.round(.5), 0.0); + assertEquals(0.5d, testUnit.round(.625), 0.0); // rounds to .5 since mod is even + + assertEquals(0.75d, testUnit.round(.626), 0.0); + assertEquals(0.75d, testUnit.round(.75), 0.0); + assertEquals(0.75d, testUnit.round(.874), 0.0); + + assertEquals(1d, testUnit.round(.875), 0.0); // rounds to 1 since mod is even + assertEquals(1d, testUnit.round(1.0), 0.0); + assertEquals(1d, testUnit.round(1.125), 0.0); // rounds to 1 since mod is even + + } + + @Test + public void testIncrement() { + + assertEquals( -1d, testUnit.getNextValue(-1.2), 0.0); + assertEquals( -1d, testUnit.getNextValue(-1.4), 0.0); + + assertEquals( -0.5d, testUnit.getNextValue(-0.7), 0.0); + assertEquals( -0.5d, testUnit.getNextValue(-0.9), 0.0); + assertEquals( -0.5d, testUnit.getNextValue(-1.0), 0.0); + + assertEquals( 0.0d, testUnit.getNextValue(-0.05), 0.0 ); + assertEquals( 0.0d, testUnit.getNextValue(-0.062), 0.0 ); + assertEquals( 0.0d, testUnit.getNextValue(-0.07), 0.0 ); + assertEquals( 0.0d, testUnit.getNextValue(-0.11), 0.0 ); + + assertEquals( 0.5d, testUnit.getNextValue(0), 0.0 ); + assertEquals( 0.5d, testUnit.getNextValue(0.01), 0.0 ); + assertEquals( 0.5d, testUnit.getNextValue(0.062), 0.0 ); + assertEquals( 0.5d, testUnit.getNextValue(0.0625), 0.0); + + assertEquals( 1d, testUnit.getNextValue(0.51), 0.0); + assertEquals( 1d, testUnit.getNextValue(0.7), 0.0); + } + + @Test + public void testDecrement() { + + assertEquals( -1.5d, testUnit.getPreviousValue(-1.2), 0.0); + assertEquals( -1.5d, testUnit.getPreviousValue(-1.4), 0.0); + assertEquals( -1.5d, testUnit.getPreviousValue(-1.0), 0.0); + + assertEquals( -1d, testUnit.getPreviousValue(-0.7), 0.0); + assertEquals( -1d, testUnit.getPreviousValue(-0.9), 0.0); + + assertEquals( -0.5d, testUnit.getPreviousValue(-0.01), 0.0 ); + assertEquals( -0.5d, testUnit.getPreviousValue(-0.05), 0.0 ); + assertEquals( -0.5d, testUnit.getPreviousValue(-0.062), 0.0 ); + assertEquals( -0.5d, testUnit.getPreviousValue(-0.07), 0.0 ); + assertEquals( -0.5d, testUnit.getPreviousValue(0), 0.0 ); + + assertEquals( 0.0d, testUnit.getPreviousValue(0.49), 0.0 ); + assertEquals( 0.0d, testUnit.getPreviousValue(0.262), 0.0 ); + assertEquals( 0.0d, testUnit.getPreviousValue(0.51), 0.0); + + assertEquals( 0.5d, testUnit.getPreviousValue(0.7), 0.0); + + assertEquals( 1.0d, testUnit.getPreviousValue(1.2), 0.0); + } + + @Test + public void testToStringDefaultPrecision() { + + // default epsilon is 0.025 + assertEquals("-1.2", testUnit.toString(-1.2)); + assertEquals("-1 1/4", testUnit.toString(-1.225)); + assertEquals("-1 1/4", testUnit.toString(-1.227)); + assertEquals("-1 1/4", testUnit.toString(-1.25)); + assertEquals("-1 1/4", testUnit.toString(-1.25)); + assertEquals("-1 1/4", testUnit.toString(-1.275)); + assertEquals("-1.3", testUnit.toString(-1.3)); + + assertEquals("-0.2", testUnit.toString(-.2)); + assertEquals("-1/4", testUnit.toString(-.225)); + assertEquals("-1/4", testUnit.toString(-.25)); + assertEquals("-1/4", testUnit.toString(-.274)); + //assertEquals("-1/4", testUnit.toString(-.275)); // this has roundoff error which pushes it over epsilon + assertEquals("-0.3", testUnit.toString(-.3)); + + assertEquals("-0.1", testUnit.toString(-.1)); + assertEquals("0", testUnit.toString(-0.024)); + assertEquals("0", testUnit.toString(0)); + assertEquals("0", testUnit.toString(.024)); + assertEquals("0.1", testUnit.toString(.1)); + + assertEquals("0.2", testUnit.toString(.2)); + assertEquals("1/4", testUnit.toString(.225)); + assertEquals("1/4", testUnit.toString(.25)); + assertEquals("1/4", testUnit.toString(.274)); + assertEquals("0.3", testUnit.toString(.3)); + + assertEquals("1.2", testUnit.toString(1.2)); + assertEquals("1 1/4", testUnit.toString(1.225)); + assertEquals("1 1/4", testUnit.toString(1.25)); + assertEquals("1 1/4", testUnit.toString(1.275)); + assertEquals("1.3", testUnit.toString(1.3)); + + } + + @Test + public void testToStringWithPrecision() { + + // epsilon is .02 + assertEquals("-1 3/16", testUnitApprox.toString(-1.2)); + assertEquals("-1.225", testUnitApprox.toString(-1.225)); + assertEquals("-1 1/4", testUnitApprox.toString(-1.25)); + assertEquals("-1.275", testUnitApprox.toString(-1.275)); + assertEquals("-1 5/16", testUnitApprox.toString(-1.3)); + + assertEquals("-3/16", testUnitApprox.toString(-.2)); + assertEquals("-0.225", testUnitApprox.toString(-.225)); + assertEquals("-1/4", testUnitApprox.toString(-.25)); + assertEquals("-0.275", testUnitApprox.toString(-.275)); + assertEquals("-5/16", testUnitApprox.toString(-.3)); + + assertEquals("-0.1", testUnitApprox.toString(-.1)); + assertEquals("-0.024", testUnitApprox.toString(-0.024)); + assertEquals("0", testUnitApprox.toString(0)); + assertEquals("0.024", testUnitApprox.toString(.024)); + assertEquals("0.1", testUnitApprox.toString(.1)); + + assertEquals("3/16", testUnitApprox.toString(.2)); + assertEquals("0.225", testUnitApprox.toString(.225)); + assertEquals("1/4", testUnitApprox.toString(.25)); + assertEquals("0.275", testUnitApprox.toString(.275)); + assertEquals("5/16", testUnitApprox.toString(.3)); + + assertEquals("1 3/16", testUnitApprox.toString(1.2)); + assertEquals("1.225", testUnitApprox.toString(1.225)); + assertEquals("1 1/4", testUnitApprox.toString(1.25)); + assertEquals("1.275", testUnitApprox.toString(1.275)); + assertEquals("1 5/16", testUnitApprox.toString(1.3)); + + } + + @Test + public void testInchToString() { + + // Just some random test points. + assertEquals( "1/64", inchUnit.toString( 1d/64d*0.0254)); + + assertEquals( "-5/64", inchUnit.toString( -5d/64d*0.0254)); + + assertEquals( "4 1/2", inchUnit.toString( 9d/2d*0.0254)); + + assertEquals( "0.002", inchUnit.toString( 0.002*0.0254)); + + // default body tube length: + double length = 8d * 0.025; + + assertEquals( "7 7/8", inchUnit.toString( length) ); + + // had problems with roundoff in decrement. + + double v = inchUnit.toUnit(length); + for ( int i = 0; i< 15; i++ ) { + assertTrue( v > inchUnit.getPreviousValue(v) ); + v = inchUnit.getPreviousValue(v); + } + + } + +} diff --git a/core/test/net/sf/openrocket/util/FractionUtilTest.java b/core/test/net/sf/openrocket/util/FractionUtilTest.java new file mode 100644 index 000000000..1e7549703 --- /dev/null +++ b/core/test/net/sf/openrocket/util/FractionUtilTest.java @@ -0,0 +1,58 @@ +package net.sf.openrocket.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FractionUtilTest { + + @Test + public void testParseFractions() { + + // zeros + assertEquals( 0.0d, FractionUtil.parseFraction("0"), 0.0); + assertEquals( 0.0d, FractionUtil.parseFraction("-0"), 0.0); + assertEquals( 0.0d, FractionUtil.parseFraction("0/2"), 0.0); + assertEquals( 0.0d, FractionUtil.parseFraction("-0/2"), 0.0); + assertEquals( 0.0d, FractionUtil.parseFraction("0 0/4"), 0.0); + assertEquals( 0.0d, FractionUtil.parseFraction("0 -0/4"), 0.0); + + // Simple fraction. + assertEquals( 0.25, FractionUtil.parseFraction("1/4"),0.0); + + // ignores leading & trailing spaces + assertEquals( 0.25, FractionUtil.parseFraction(" 1/4 "),0.0); + + // non reduced fraction + assertEquals( 0.25, FractionUtil.parseFraction("2/8"),0.0); + + // negative number + assertEquals( -0.25, FractionUtil.parseFraction("-1/4"),0.0); + + // improper fraction + assertEquals( 1.75, FractionUtil.parseFraction("7/4"),0.0); + + // negative improper fraction + assertEquals( -1.75, FractionUtil.parseFraction("-7/4"),0.0); + + // two digit numerator & denominators + assertEquals( 11d/16d, FractionUtil.parseFraction("11/16)"),0.0); + assertEquals( -11d/16d, FractionUtil.parseFraction("-11/16)"),0.0); + + // Mixed fractions + assertEquals( 1.25d, FractionUtil.parseFraction("1 1/4"),0.0); + assertEquals( -1.25d, FractionUtil.parseFraction("-1 1/4"),0.0); + + // extra spaces + assertEquals( 1.25d, FractionUtil.parseFraction(" 1 1/4"),0.0); + assertEquals( 1.25d, FractionUtil.parseFraction("1 1/4"),0.0); + assertEquals( 1.25d, FractionUtil.parseFraction("1 1/4 "),0.0); + + assertEquals( 2.75d, FractionUtil.parseFraction("2 3/4"),0.0); + assertEquals( 15.75d, FractionUtil.parseFraction("15 3/4"),0.0); + assertEquals( 2.75d, FractionUtil.parseFraction("1 7/4"),0.0); + + assertEquals( 69d/64d, FractionUtil.parseFraction("1 5/64"),0.0); + assertEquals( -69d/64d, FractionUtil.parseFraction("-1 5/64"),0.0); + } +}