Merge branch 'upstream' into debian
authorBdale Garbee <bdale@gag.com>
Thu, 25 Aug 2011 21:19:46 +0000 (15:19 -0600)
committerBdale Garbee <bdale@gag.com>
Thu, 25 Aug 2011 21:19:46 +0000 (15:19 -0600)
24 files changed:
ChangeLog
ReleaseNotes
build.properties
l10n/messages.properties
pix-src/icon/flag-icon.png [new file with mode: 0644]
pix-src/icon/flag-icon.xcf.gz [new file with mode: 0644]
src/net/sf/openrocket/database/Databases.java
src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java
src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java
src/net/sf/openrocket/gui/main/ExceptionHandler.java
src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
src/net/sf/openrocket/logging/TraceException.java
src/net/sf/openrocket/material/Material.java
src/net/sf/openrocket/motor/MotorDigest.java
src/net/sf/openrocket/preset/ComponentPreset.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/BodyComponent.java
src/net/sf/openrocket/rocketcomponent/BodyTube.java
src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java
src/net/sf/openrocket/rocketcomponent/ExternalComponent.java
src/net/sf/openrocket/rocketcomponent/RocketComponent.java
src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java
src/net/sf/openrocket/simulation/SimulationOptions.java
src/net/sf/openrocket/util/GUIUtil.java
test/net/sf/openrocket/logging/TraceExceptionTest.java [new file with mode: 0644]

index b1435b8bbbf2233a2e5c77a08089b1263a5e3637..51b08c9ea5d07e565959f552afc662bd97ac5427 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,23 @@
+2011-08-25  Sampo Niskanen
+
+       * Released version 1.1.8
+       * [BUG] Ignore synthetic methods in logging traces
+       * [BUG] Ignore JRE bug #6826104
+
+2011-08-24  Sampo Niskanen
+
+       * [BUG] NPE in SimulationOptions.equals
+       * [BUG] Exception in plotting optimization path
+       * [BUG] Exception in saving optimization path
+
+2011-08-17  Justin Seitz
+
+       * Added Blue tube to materials database.
+
+2011-08-13  Sampo Niskanen
+
+       * [BUG] JDK7 returns null font for TitledBorder
+
 2011-08-12  Sampo Niskanen
 
        * Released version 1.1.7
index 9eb4dfdb89b2e0f63eaa72d857dc865fce963859..140644ae3049b5e5b360cd904f139b5460aaf2b6 100644 (file)
@@ -1,3 +1,11 @@
+OpenRocket 1.1.8  (2011-08-25):
+-------------------------------
+
+This release contains bug fixes to the optimization methods.
+It also contains a workaround to a JRE bug that prevents running
+OpenRocket on Java 7.
+
+
 OpenRocket 1.1.7  (2011-08-12):
 -------------------------------
 
index b321d08ec284c66d43721b909280f10bc5679c16..9f9f05ea7dc8d715fc6ae72a0be24d2f241f5a31 100644 (file)
@@ -1,7 +1,7 @@
 
 # The OpenRocket build version
 
-build.version=1.1.7
+build.version=1.1.8
 
 
 # The source of the package.  When building a package for a specific
index 9353c4d8fff5e50786a0f14cb6ef4a7f99bd8ada..34e12adb9e69623013afcc3a387fcd7d21cff4c1 100644 (file)
@@ -1015,6 +1015,7 @@ Databases.materials.Spruce = Spruce
 Databases.materials.StyrofoamgenericEPS = Styrofoam (generic EPS)
 Databases.materials.StyrofoamBluefoamXPS = Styrofoam \"Blue foam\" (XPS)
 Databases.materials.Quantumtubing = Quantum tubing
+Databases.materials.BlueTube = Blue tube
 !SURFACE_MATERIAL
 Databases.materials.Ripstopnylon = Ripstop nylon
 Databases.materials.Mylar = Mylar
diff --git a/pix-src/icon/flag-icon.png b/pix-src/icon/flag-icon.png
new file mode 100644 (file)
index 0000000..cd34128
Binary files /dev/null and b/pix-src/icon/flag-icon.png differ
diff --git a/pix-src/icon/flag-icon.xcf.gz b/pix-src/icon/flag-icon.xcf.gz
new file mode 100644 (file)
index 0000000..7a92c1d
Binary files /dev/null and b/pix-src/icon/flag-icon.xcf.gz differ
index fae1eb47eeb8090748a8f410adf2062d942dce0b..1cfc0e8ec21394010eb0c23cb672aa2989301ccd 100644 (file)
@@ -59,6 +59,7 @@ public class Databases {
                //              BULK_MATERIAL.add(new Material.Bulk("Styrofoam (Blue foam, XPS)", 32, false));
                BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.StyrofoamBluefoamXPS"), 32, false));
                BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Quantumtubing"), 1050, false));
+               BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.BlueTube"), 1300, false));
                
                SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Ripstopnylon"), 0.067, false));
                SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Mylar"), 0.021, false));
index 0ef35e2e7f242eac632fc9079dee62729805806b..1478a2b12847f6bf39f6d1251d444750363763d7 100644 (file)
@@ -606,8 +606,11 @@ public class GeneralOptimizationDialog extends JDialog {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                                log.user("Plotting optimization path, dimensionality=" + selectedModifiers.size());
-                               OptimizationPlotDialog dialog = new OptimizationPlotDialog(optimizationPath, evaluationHistory,
-                                               selectedModifiers, getSelectedParameter(),
+                               OptimizationPlotDialog dialog = new OptimizationPlotDialog(
+                                               Collections.unmodifiableList(optimizationPath),
+                                               Collections.unmodifiableMap(evaluationHistory),
+                                               Collections.unmodifiableList(selectedModifiers),
+                                               getSelectedParameter(),
                                                UnitGroup.stabilityUnits(getSelectedSimulation().getRocket()),
                                                GeneralOptimizationDialog.this);
                                dialog.setVisible(true);
@@ -803,6 +806,7 @@ public class GeneralOptimizationDialog extends JDialog {
                                });
                                timer.setRepeats(false);
                                timer.start();
+                               updateComponents();
                        }
                        
                        @Override
@@ -1125,16 +1129,21 @@ public class GeneralOptimizationDialog extends JDialog {
         * Update the enabled status of all components in the dialog.
         */
        private void updateComponents() {
+               boolean state;
                
                if (updating) {
+                       log.debug("Ignoring updateComponents");
                        return;
                }
                
+               log.debug("Running updateComponents()");
+               
                updating = true;
                
 
                // First enable all components if optimization not running
                if (!running) {
+                       log.debug("Initially enabling all components");
                        for (JComponent c : disableComponents) {
                                c.setEnabled(true);
                        }
@@ -1143,46 +1152,56 @@ public class GeneralOptimizationDialog extends JDialog {
 
                // "Add" button
                SimulationModifier mod = getSelectedAvailableModifier();
-               if (mod != null && !selectedModifiers.contains(mod)) {
-                       addButton.setEnabled(true);
-               } else {
-                       addButton.setEnabled(false);
-               }
+               state = (mod != null && !selectedModifiers.contains(mod));
+               log.debug("addButton enabled: " + state);
+               addButton.setEnabled(state);
                
                // "Remove" button
-               removeButton.setEnabled(selectedModifierTable.getSelectedRow() >= 0);
+               state = (selectedModifierTable.getSelectedRow() >= 0);
+               log.debug("removeButton enabled: " + state);
+               removeButton.setEnabled(state);
                
                // "Remove all" button
-               removeAllButton.setEnabled(!selectedModifiers.isEmpty());
+               state = (!selectedModifiers.isEmpty());
+               log.debug("removeAllButton enabled: " + state);
+               removeAllButton.setEnabled(state);
                
 
                // Optimization goal
                String selected = (String) optimizationGoalCombo.getSelectedItem();
-               if (GOAL_SEEK.equals(selected)) {
-                       optimizationGoalSpinner.setVisible(true);
-                       optimizationGoalUnitSelector.setVisible(true);
-               } else {
-                       optimizationGoalSpinner.setVisible(false);
-                       optimizationGoalUnitSelector.setVisible(false);
-               }
+               state = GOAL_SEEK.equals(selected);
+               log.debug("optimizationGoalSpinner & UnitSelector enabled: " + state);
+               optimizationGoalSpinner.setVisible(state);
+               optimizationGoalUnitSelector.setVisible(state);
                
 
                // Minimum/maximum stability options
-               minimumStabilitySpinner.setEnabled(minimumStabilitySelected.isSelected());
-               minimumStabilityUnitSelector.setEnabled(minimumStabilitySelected.isSelected());
-               maximumStabilitySpinner.setEnabled(maximumStabilitySelected.isSelected());
-               maximumStabilityUnitSelector.setEnabled(maximumStabilitySelected.isSelected());
+               state = minimumStabilitySelected.isSelected();
+               log.debug("minimumStabilitySpinner & UnitSelector enabled: " + state);
+               minimumStabilitySpinner.setEnabled(state);
+               minimumStabilityUnitSelector.setEnabled(state);
+               
+               state = maximumStabilitySelected.isSelected();
+               log.debug("maximumStabilitySpimmer & UnitSelector enabled: " + state);
+               maximumStabilitySpinner.setEnabled(state);
+               maximumStabilityUnitSelector.setEnabled(state);
                
 
                // Plot button (enabled if path exists and dimensionality is 1 or 2)
-               plotButton.setEnabled(!optimizationPath.isEmpty() && (selectedModifiers.size() == 1 || selectedModifiers.size() == 2));
+               state = (!optimizationPath.isEmpty() && (selectedModifiers.size() == 1 || selectedModifiers.size() == 2));
+               log.debug("plotButton enabled: " + state + " optimizationPath.isEmpty=" + optimizationPath.isEmpty() +
+                               " selectedModifiers.size=" + selectedModifiers.size());
+               plotButton.setEnabled(state);
                
                // Save button (enabled if path exists)
-               saveButton.setEnabled(!optimizationPath.isEmpty());
+               state = (!evaluationHistory.isEmpty());
+               log.debug("saveButton enabled: " + state);
+               saveButton.setEnabled(state);
                
 
                // Last disable all components if optimization is running
                if (running) {
+                       log.debug("Disabling all components because optimization is running");
                        for (JComponent c : disableComponents) {
                                c.setEnabled(false);
                        }
@@ -1445,6 +1464,12 @@ public class GeneralOptimizationDialog extends JDialog {
                @Override
                public void setValueAt(Object value, int row, int column) {
                        
+                       if (row >= selectedModifiers.size()) {
+                               throw new BugException("setValueAt with invalid row:  value=" + value + " row=" + row + " column=" + column +
+                                               " selectedModifiers.size=" + selectedModifiers.size() + " selectedModifiers=" + selectedModifiers +
+                                               " selectedModifierTable.getRowCount=" + selectedModifierTable.getRowCount());
+                       }
+                       
                        switch (column) {
                        case PARAMETER:
                                break;
index ab9c9c94c284f50349097b2d04a757e316dab6bf..04885a82cbaa441f06910279b231c67fe85c8f74 100644 (file)
@@ -162,7 +162,12 @@ public class OptimizationPlotDialog extends JDialog {
                double x1 = xUnit.toUnit(modX.getMinValue());
                double x2 = xUnit.toUnit(modX.getMaxValue());
                
-               chart.getXYPlot().getDomainAxis().setRange(x1, x2);
+               if (x1 < x2 - 0.0001) {
+                       log.debug("Setting 1D plot domain axis x1=" + x1 + " x2=" + x2);
+                       chart.getXYPlot().getDomainAxis().setRange(x1, x2);
+               } else {
+                       log.warn("1D plot domain singular x1=" + x1 + " x2=" + x2 + ", not setting");
+               }
                
                // Add lines to show optimization limits
                XYLineAnnotation line = new XYLineAnnotation(x1, -1e19, x1, 1e19);
@@ -296,8 +301,19 @@ public class OptimizationPlotDialog extends JDialog {
                double y1 = yUnit.toUnit(modY.getMinValue());
                double y2 = yUnit.toUnit(modY.getMaxValue());
                
-               chart.getXYPlot().getDomainAxis().setRange(x1, x2);
-               chart.getXYPlot().getRangeAxis().setRange(y1, y2);
+               if (x1 < x2 - 0.0001) {
+                       log.debug("Setting 2D plot domain axis to x1=" + x1 + " x2=" + x2);
+                       chart.getXYPlot().getDomainAxis().setRange(x1, x2);
+               } else {
+                       log.warn("2D plot has singular domain axis: x1=" + x1 + " x2=" + x2);
+               }
+               
+               if (y1 < y2 - 0.0001) {
+                       log.debug("Setting 2D plot range axis to y1=" + y1 + " y2=" + y2);
+                       chart.getXYPlot().getRangeAxis().setRange(y1, y2);
+               } else {
+                       log.warn("2D plot has singular range axis: y1=" + y1 + " y2=" + y2);
+               }
                
                XYBoxAnnotation box = new XYBoxAnnotation(x1, y1, x2, y2);
                chart.getXYPlot().addAnnotation(box);
@@ -308,6 +324,15 @@ public class OptimizationPlotDialog extends JDialog {
                text.setTextAnchor(TextAnchor.BASELINE_LEFT);
                chart.getXYPlot().addAnnotation(text);
                
+
+               if (min < max - 0.0001) {
+                       log.debug("Setting gradient scale range to min=" + min + " max=" + max);
+               } else {
+                       log.warn("2D plot has singular gradient scale, resetting to (0,1): min=" + min + " max=" + max);
+                       min = 0;
+                       max = 1;
+               }
+               
                PaintScale paintScale = new GradientScale(min, max);
                
                XYShapeRenderer shapeRenderer = new XYShapeRenderer();
index 74f3091757ad4dc1cbcf6136228668d489ae7549..83301da729d0645a2e3c26c398eb835002c55551 100644 (file)
@@ -301,6 +301,29 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
                
                // NOTE:  Calling method logs the entire throwable, so log only message here
                
+
+               /*
+                * Detect and ignore bug 6826104 in Sun JRE.
+                */
+               if (t instanceof NullPointerException) {
+                       StackTraceElement[] trace = t.getStackTrace();
+                       
+                       if (trace.length > 3 &&
+                                       trace[0].getClassName().equals("sun.awt.X11.XWindowPeer") &&
+                                       trace[0].getMethodName().equals("restoreTransientFor") &&
+
+                                       trace[1].getClassName().equals("sun.awt.X11.XWindowPeer") &&
+                                       trace[1].getMethodName().equals("removeFromTransientFors") &&
+
+                                       trace[2].getClassName().equals("sun.awt.X11.XWindowPeer") &&
+                                       trace[2].getMethodName().equals("setModalBlocked")) {
+                               log.warn("Ignoring Sun JRE bug (6826104): http://bugs.sun.com/view_bug.do?bug_id=6826104" + t);
+                               return true;
+                       }
+                       
+               }
+               
+
                /*
                 * Detect and ignore bug 6828938 in Sun JRE 1.6.0_14 - 1.6.0_16.
                 */
index 920d4617021b85a143ab1be8fb2c3ccca72d8193..60cc67b8ca209470a42811259ce4ec3adb21d7f8 100644 (file)
@@ -1,6 +1,35 @@
 package net.sf.openrocket.gui.scalefigure;
 
 
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.JViewport;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
 import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
@@ -40,34 +69,6 @@ import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.MathUtil;
 import net.sf.openrocket.util.Prefs;
 
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSlider;
-import javax.swing.JToggleButton;
-import javax.swing.JViewport;
-import javax.swing.SwingUtilities;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-import javax.swing.event.TreeSelectionEvent;
-import javax.swing.event.TreeSelectionListener;
-import javax.swing.tree.TreePath;
-import javax.swing.tree.TreeSelectionModel;
-import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.Point;
-import java.awt.event.ActionEvent;
-import java.awt.event.InputEvent;
-import java.awt.event.MouseEvent;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-
 /**
  * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
  * 
@@ -241,12 +242,13 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
                add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
                                "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
                
-               
+
                //// <html>Click to select &nbsp;&nbsp; Shift+click to select other &nbsp;&nbsp; Double-click to edit &nbsp;&nbsp; Click+drag to move
                infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage"));
                infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
                add(infoMessage, "skip, span, gapleft 25, wrap");
                
+
                addExtras();
        }
        
@@ -263,35 +265,35 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
        public Configuration getConfiguration() {
                return configuration;
        }
-
-    /**
-     * Get the center of pressure figure element.
-     * 
-     * @return center of pressure info
-     */
-    public Caret getExtraCP () {
-        return extraCP;
-    }
-
-    /**
-     * Get the center of gravity figure element.
-     * 
-     * @return center of gravity info
-     */
-    public Caret getExtraCG () {
-        return extraCG;
-    }
-
-    /**
-     * Get the extra text figure element.
-     * 
-     * @return extra text that contains info about the rocket design
-     */
-    public RocketInfo getExtraText () {
-        return extraText;
-    }
-
-    public void setSelectionModel(TreeSelectionModel m) {
+       
+       /**
+        * Get the center of pressure figure element.
+        
+        * @return center of pressure info
+        */
+       public Caret getExtraCP() {
+               return extraCP;
+       }
+       
+       /**
+        * Get the center of gravity figure element.
+        
+        * @return center of gravity info
+        */
+       public Caret getExtraCG() {
+               return extraCG;
+       }
+       
+       /**
+        * Get the extra text figure element.
+        
+        * @return extra text that contains info about the rocket design
+        */
+       public RocketInfo getExtraText() {
+               return extraText;
+       }
+       
+       public void setSelectionModel(TreeSelectionModel m) {
                if (selectionModel != null) {
                        selectionModel.removeTreeSelectionListener(this);
                }
index 01029d7d34e06493b316b6f4c1e7ff5973ca4912..f3fc1714ae9c0e05246cabd4458a377dbeb1a834 100644 (file)
@@ -95,31 +95,33 @@ public class TraceException extends Exception {
                        StackTraceElement[] elements = this.getStackTrace();
                        
                        StringBuilder sb = new StringBuilder();
-                       if (minLevel < elements.length) {
-                               
-                               sb.append("(");
-                               sb.append(toString(elements[minLevel]));
-                               for (int i = minLevel + 1; i <= maxLevel; i++) {
-                                       if (i < elements.length) {
-                                               sb.append(' ').append(toString(elements[i]));
-                                       }
-                               }
-                               sb.append(')');
-                               
-                       } else if (elements.length == 0) {
-                               
-                               sb.append("(no stack trace)");
-                               
+                       sb.append('(');
+                       
+                       if (elements == null || elements.length == 0) {
+                               sb.append("no stack trace");
                        } else {
                                
-                               sb.append('(');
-                               sb.append(toString(elements[0]));
-                               for (int i = 1; i < elements.length; i++) {
-                                       sb.append(' ').append(toString(elements[i]));
+                               int levelCount = 0;
+                               int position = minLevel;
+                               while (levelCount <= (maxLevel - minLevel) && position < elements.length) {
+                                       
+                                       // Ignore synthetic "access$0" methods generated by the JRE
+                                       if (elements[position].getMethodName().contains("$")) {
+                                               position++;
+                                               continue;
+                                       }
+                                       
+                                       if (levelCount > 0) {
+                                               sb.append(' ');
+                                       }
+                                       sb.append(toString(elements[position]));
+                                       levelCount++;
+                                       position++;
                                }
-                               sb.append(" level=").append(minLevel).append(')');
                                
                        }
+                       sb.append(')');
+                       
                        message = sb.toString();
                }
                return message;
index 8febc9480432cefa785ebc2c1b5a262ef51f9e7f..00be68b1c3cdce191dfe53b035d13d31fc086a60 100644 (file)
@@ -15,7 +15,7 @@ import net.sf.openrocket.util.MathUtil;
  */
 
 public abstract class Material implements Comparable<Material> {
-
+       
        public enum Type {
                LINE("Line", UnitGroup.UNITS_DENSITY_LINE),
                SURFACE("Surface", UnitGroup.UNITS_DENSITY_SURFACE),
@@ -23,13 +23,16 @@ public abstract class Material implements Comparable<Material> {
                
                private final String name;
                private final UnitGroup units;
+               
                private Type(String name, UnitGroup units) {
                        this.name = name;
                        this.units = units;
                }
+               
                public UnitGroup getUnitGroup() {
                        return units;
                }
+               
                @Override
                public String toString() {
                        return name;
@@ -43,7 +46,7 @@ public abstract class Material implements Comparable<Material> {
                public Line(String name, double density, boolean userDefined) {
                        super(name, density, userDefined);
                }
-
+               
                @Override
                public Type getType() {
                        return Type.LINE;
@@ -71,7 +74,7 @@ public abstract class Material implements Comparable<Material> {
                public Bulk(String name, double density, boolean userDefined) {
                        super(name, density, userDefined);
                }
-
+               
                @Override
                public Type getType() {
                        return Type.BULK;
@@ -79,7 +82,7 @@ public abstract class Material implements Comparable<Material> {
        }
        
        
-       
+
        private final String name;
        private final double density;
        private final boolean userDefined;
@@ -92,7 +95,7 @@ public abstract class Material implements Comparable<Material> {
        }
        
        
-       
+
        public double getDensity() {
                return density;
        }
@@ -116,7 +119,7 @@ public abstract class Material implements Comparable<Material> {
                return this.getName(this.getType().getUnitGroup().getDefaultUnit());
        }
        
-
+       
        /**
         * Compares this object to another object.  Material objects are equal if and only if
         * their types, names and densities are identical.
@@ -127,30 +130,30 @@ public abstract class Material implements Comparable<Material> {
                        return false;
                if (this.getClass() != o.getClass())
                        return false;
-               Material m = (Material)o;
-               return ((m.name.equals(this.name)) && 
-                               MathUtil.equals(m.density, this.density)); 
+               Material m = (Material) o;
+               return ((m.name.equals(this.name)) && MathUtil.equals(m.density, this.density));
        }
-
-
+       
+       
        /**
         * A hashCode() method giving a hash code compatible with the equals() method.
         */
        @Override
        public int hashCode() {
-               return name.hashCode() + (int)(density*1000);
+               return name.hashCode() + (int) (density * 1000);
        }
-
+       
        
        /**
         * Order the materials according to their name, secondarily according to density.
         */
+       @Override
        public int compareTo(Material o) {
                int c = this.name.compareTo(o.name);
                if (c != 0) {
                        return c;
                } else {
-                       return (int)((this.density - o.density)*1000);
+                       return (int) ((this.density - o.density) * 1000);
                }
        }
        
@@ -158,7 +161,7 @@ public abstract class Material implements Comparable<Material> {
        /**
         * Return a new material of the specified type.
         */
-       public static Material newMaterial(Type type, String name, double density, 
+       public static Material newMaterial(Type type, String name, double density,
                        boolean userDefined) {
                switch (type) {
                case LINE:
@@ -171,7 +174,7 @@ public abstract class Material implements Comparable<Material> {
                        return new Material.Bulk(name, density, userDefined);
                        
                default:
-                       throw new IllegalArgumentException("Unknown material type: "+type);
+                       throw new IllegalArgumentException("Unknown material type: " + type);
                }
        }
        
@@ -193,10 +196,10 @@ public abstract class Material implements Comparable<Material> {
                if (str == null)
                        throw new IllegalArgumentException("Material string is null");
                
-               String[] split = str.split("\\|",3);
+               String[] split = str.split("\\|", 3);
                if (split.length < 3)
-                       throw new IllegalArgumentException("Illegal material string: "+str);
-
+                       throw new IllegalArgumentException("Illegal material string: " + str);
+               
                Type type = null;
                String name;
                double density;
@@ -204,15 +207,15 @@ public abstract class Material implements Comparable<Material> {
                try {
                        type = Type.valueOf(split[0]);
                } catch (Exception e) {
-                       throw new IllegalArgumentException("Illegal material string: "+str, e);
+                       throw new IllegalArgumentException("Illegal material string: " + str, e);
                }
-
+               
                name = split[1];
                
                try {
                        density = Double.parseDouble(split[2]);
                } catch (NumberFormatException e) {
-                       throw new IllegalArgumentException("Illegal material string: "+str, e);
+                       throw new IllegalArgumentException("Illegal material string: " + str, e);
                }
                
                switch (type) {
@@ -226,8 +229,8 @@ public abstract class Material implements Comparable<Material> {
                        return new Material.Line(name, density, userDefined);
                        
                default:
-                       throw new IllegalArgumentException("Illegal material string: "+str);
+                       throw new IllegalArgumentException("Illegal material string: " + str);
                }
        }
-
+       
 }
index e7fb9f5fd7b31261ff7bc90d6cece110b25f3388..cec3cbdc6082a37c7a43354815d7afa5effa9e12 100644 (file)
@@ -7,6 +7,17 @@ import java.security.NoSuchAlgorithmException;
 import net.sf.openrocket.util.Coordinate;
 import net.sf.openrocket.util.TextUtil;
 
+/**
+ * A class that generated a "digest" of a motor.  A digest is a string value that
+ * uniquely identifies a motor (like a hash code or checksum).  Two motors that have
+ * the same digest behave similarly with a very high probability.  The digest can
+ * therefore be used to identify motors that otherwise have the same specifications.
+ * <p>
+ * The digest only uses a limited amount of precision, so that rounding errors won't
+ * cause differing digest results.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
 public class MotorDigest {
        
        private static final double EPSILON = 0.00000000001;
@@ -24,22 +35,25 @@ public class MotorDigest {
                CG_PER_TIME(4, 1000),
                /** Thrust force per time (in mN) */
                FORCE_PER_TIME(5, 1000);
-
+               
                private final int order;
                private final int multiplier;
+               
                DataType(int order, int multiplier) {
                        this.order = order;
                        this.multiplier = multiplier;
                }
+               
                public int getOrder() {
                        return order;
                }
+               
                public int getMultiplier() {
                        return multiplier;
                }
        }
        
-
+       
        private final MessageDigest digest;
        private boolean used = false;
        private int lastOrder = -1;
@@ -54,11 +68,11 @@ public class MotorDigest {
        }
        
        
-       public void update(DataType type, int ... values) {
-
+       public void update(DataType type, int... values) {
+               
                // Check for correct order
                if (lastOrder >= type.getOrder()) {
-                       throw new IllegalArgumentException("Called with type="+type+" order="+type.getOrder()+
+                       throw new IllegalArgumentException("Called with type=" + type + " order=" + type.getOrder() +
                                        " while lastOrder=" + lastOrder);
                }
                lastOrder = type.getOrder();
@@ -70,17 +84,17 @@ public class MotorDigest {
                digest.update(bytes(values.length));
                
                // Digest the values
-               for (int v: values) {
+               for (int v : values) {
                        digest.update(bytes(v));
                }
                
        }
        
        
-       private void update(DataType type, int multiplier, double ... values) {
-
+       private void update(DataType type, int multiplier, double... values) {
+               
                int[] intValues = new int[values.length];
-               for (int i=0; i<values.length; i++) {
+               for (int i = 0; i < values.length; i++) {
                        double v = values[i];
                        v = next(v);
                        v *= multiplier;
@@ -90,7 +104,7 @@ public class MotorDigest {
                update(type, intValues);
        }
        
-       public void update(DataType type, double ... values) {
+       public void update(DataType type, double... values) {
                update(type, type.getMultiplier(), values);
        }
        
@@ -107,16 +121,15 @@ public class MotorDigest {
                byte[] result = digest.digest();
                return TextUtil.hexString(result);
        }
-
        
        
+
        private byte[] bytes(int value) {
                return new byte[] {
-                               (byte) ((value>>>24) & 0xFF), (byte) ((value>>>16) & 0xFF),
-                               (byte) ((value>>>8) & 0xFF), (byte) (value & 0xFF) 
-               };
+                               (byte) ((value >>> 24) & 0xFF), (byte) ((value >>> 16) & 0xFF),
+                               (byte) ((value >>> 8) & 0xFF), (byte) (value & 0xFF) };
        }
-
+       
        
        /**
         * Digest the contents of a thrust curve motor.  The result is a string uniquely
@@ -126,7 +139,7 @@ public class MotorDigest {
         * @return              the digest
         */
        public static String digestMotor(ThrustCurveMotor m) {
-
+               
                // Create the motor digest from data available in RASP files
                MotorDigest motorDigest = new MotorDigest();
                motorDigest.update(DataType.TIME_ARRAY, m.getTimePoints());
@@ -134,7 +147,7 @@ public class MotorDigest {
                Coordinate[] cg = m.getCGPoints();
                double[] cgx = new double[cg.length];
                double[] mass = new double[cg.length];
-               for (int i=0; i<cg.length; i++) {
+               for (int i = 0; i < cg.length; i++) {
                        cgx[i] = cg[i].x;
                        mass[i] = cg[i].weight;
                }
@@ -161,7 +174,7 @@ public class MotorDigest {
                } catch (UnsupportedEncodingException e) {
                        throw new IllegalStateException("UTF-8 encoding not supported by JRE", e);
                }
-
+               
                return TextUtil.hexString(digest.digest());
        }
        
diff --git a/src/net/sf/openrocket/preset/ComponentPreset.java b/src/net/sf/openrocket/preset/ComponentPreset.java
new file mode 100644 (file)
index 0000000..87343e9
--- /dev/null
@@ -0,0 +1,73 @@
+package net.sf.openrocket.preset;
+
+import net.sf.openrocket.motor.Manufacturer;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+/**
+ * A model for a preset component.
+ * <p>
+ * A preset component contains a component class type, manufacturer information,
+ * part information, and a method that returns a prototype of the preset component.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class ComponentPreset {
+       
+       private final Manufacturer manufacturer;
+       private final String partNo;
+       private final String partDescription;
+       private final RocketComponent prototype;
+       
+       
+       public ComponentPreset(Manufacturer manufacturer, String partNo, String partDescription,
+                       RocketComponent prototype) {
+               this.manufacturer = manufacturer;
+               this.partNo = partNo;
+               this.partDescription = partDescription;
+               this.prototype = prototype.copy();
+               
+               if (prototype.getParent() != null) {
+                       throw new IllegalArgumentException("Prototype component cannot have a parent");
+               }
+               if (prototype.getChildCount() > 0) {
+                       throw new IllegalArgumentException("Prototype component cannot have children");
+               }
+       }
+       
+       
+       /**
+        * Return the component class that this preset defines.
+        */
+       public Class<? extends RocketComponent> getComponentClass() {
+               return prototype.getClass();
+       }
+       
+       /**
+        * Return the manufacturer of this preset component.
+        */
+       public Manufacturer getManufacturer() {
+               return manufacturer;
+       }
+       
+       /**
+        * Return the part number.  This is the part identifier (e.g. "BT-50").
+        */
+       public String getPartNo() {
+               return partNo;
+       }
+       
+       /**
+        * Return the part description.  This is a longer description of the component.
+        */
+       public String getPartDescription() {
+               return partDescription;
+       }
+       
+       /**
+        * Return a prototype component.  This component may be modified freely.
+        */
+       public RocketComponent getPrototype() {
+               return prototype.copy();
+       }
+       
+}
index 2bc112fbb11624924fcd699307157bde8764c3b1..a102e87f720871ae46f1341a3f3f05280bb6c858 100644 (file)
@@ -1,6 +1,7 @@
 package net.sf.openrocket.rocketcomponent;
 
 
+
 /**
  * Class to represent a body object.  The object can be described as a function of
  * the cylindrical coordinates x and angle theta as  r = f(x,theta).  The component
@@ -13,7 +14,7 @@ package net.sf.openrocket.rocketcomponent;
  */
 
 public abstract class BodyComponent extends ExternalComponent {
-
+       
        /**
         * Default constructor.  Sets the relative position to POSITION_RELATIVE_AFTER,
         * i.e. body components come after one another.
@@ -21,8 +22,8 @@ public abstract class BodyComponent extends ExternalComponent {
        public BodyComponent() {
                super(RocketComponent.Position.AFTER);
        }
-
-
+       
+       
        /**
         * Get the outer radius of the component at cylindrical coordinate (x,theta).
         *
@@ -33,8 +34,8 @@ public abstract class BodyComponent extends ExternalComponent {
         * @return  Distance to the outer edge of the object
         */
        public abstract double getRadius(double x, double theta);
-
-
+       
+       
        /**
         * Get the inner radius of the component at cylindrical coordinate (x,theta).
         *
@@ -45,11 +46,23 @@ public abstract class BodyComponent extends ExternalComponent {
         * @return  Distance to the inner edge of the object
         */
        public abstract double getInnerRadius(double x, double theta);
-
-
+       
+       
+       @Override
+       protected void loadFromPreset(RocketComponent preset) {
+               BodyComponent c = (BodyComponent) preset;
+               this.setLength(c.getLength());
+               
+               super.loadFromPreset(preset);
+       }
+       
+       
 
        /**
         * Sets the length of the body component.
+        * <p>
+        * Note: This should be overridden by the subcomponents which need to call
+        * clearPreset().  (BodyTube allows changing length without resetting the preset.)
         */
        public void setLength(double length) {
                if (this.length == length)
@@ -57,10 +70,10 @@ public abstract class BodyComponent extends ExternalComponent {
                this.length = Math.max(length, 0);
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
        }
-
+       
        @Override
        public boolean allowsChildren() {
                return true;
        }
-
+       
 }
index d3b6194eb63832b52e84278048b2cb88c97c9d68..64c1b4a99c7608f602e676ce38909dad0cd22919 100644 (file)
@@ -108,6 +108,7 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                if (this.thickness > this.outerRadius)
                        this.thickness = this.outerRadius;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+               clearPreset();
        }
        
        
@@ -128,6 +129,16 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial
                
                autoRadius = auto;
                fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+               clearPreset();
+       }
+       
+       
+       @Override
+       protected void loadFromPreset(RocketComponent preset) {
+               BodyTube c = (BodyTube) preset;
+               this.setOuterRadius(c.getOuterRadius());
+               
+               super.loadFromPreset(preset);
        }
        
        
index 58d3297b5cee3b1be392bf110e62b93d5fe8f259..60123e87656c730040f2e3dfdec15438fd3f41b4 100644 (file)
@@ -4,8 +4,8 @@ import javax.swing.event.ChangeEvent;
 
 public class ComponentChangeEvent extends ChangeEvent {
        private static final long serialVersionUID = 1L;
-
        
+
        /** A change that does not affect simulation results in any way (name, color, etc.) */
        public static final int NONFUNCTIONAL_CHANGE = 1;
        /** A change that affects the mass properties of the rocket */
@@ -13,15 +13,15 @@ public class ComponentChangeEvent extends ChangeEvent {
        /** A change that affects the aerodynamic properties of the rocket */
        public static final int AERODYNAMIC_CHANGE = 4;
        /** A change that affects the mass and aerodynamic properties of the rocket */
-       public static final int BOTH_CHANGE = MASS_CHANGE|AERODYNAMIC_CHANGE; // Mass & Aerodynamic
-
+       public static final int BOTH_CHANGE = MASS_CHANGE | AERODYNAMIC_CHANGE; // Mass & Aerodynamic
+       
        /** A change that affects the rocket tree structure */
        public static final int TREE_CHANGE = 8;
        /** A change caused by undo/redo. */
        public static final int UNDO_CHANGE = 16;
        /** A change in the motor configurations or names */
        public static final int MOTOR_CHANGE = 32;
-       /** A change in the events occurring during flight. */
+       /** A change that affects the events occurring during flight. */
        public static final int EVENT_CHANGE = 64;
        
        /** A bit-field that contains all possible change types. */
@@ -29,7 +29,7 @@ public class ComponentChangeEvent extends ChangeEvent {
        
        private final int type;
        
-
+       
        public ComponentChangeEvent(RocketComponent component, int type) {
                super(component);
                if (type == 0) {
@@ -46,8 +46,8 @@ public class ComponentChangeEvent extends ChangeEvent {
        public RocketComponent getSource() {
                return (RocketComponent) super.getSource();
        }
-
-
+       
+       
        public boolean isAerodynamicChange() {
                return (type & AERODYNAMIC_CHANGE) != 0;
        }
@@ -71,7 +71,7 @@ public class ComponentChangeEvent extends ChangeEvent {
        public boolean isMotorChange() {
                return (type & MOTOR_CHANGE) != 0;
        }
-
+       
        public int getType() {
                return type;
        }
index 64c1a50ec095509c5348f648ace514f3677c253a..e710cb2773d288a14b915e9335112a117a76bfb4 100644 (file)
@@ -4,6 +4,7 @@ import java.util.List;
 
 import net.sf.openrocket.l10n.Translator;
 import net.sf.openrocket.material.Material;
+import net.sf.openrocket.material.Material.Type;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.unit.UnitGroup;
 import net.sf.openrocket.util.Prefs;
@@ -111,7 +112,9 @@ public abstract class ExternalComponent extends RocketComponent {
                if (material.equals(mat))
                        return;
                material = mat;
+               clearPreset();
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               clearPreset();
        }
        
        public Finish getFinish() {
@@ -126,6 +129,31 @@ public abstract class ExternalComponent extends RocketComponent {
        }
        
        
+       @Override
+       protected void loadFromPreset(RocketComponent preset) {
+               super.loadFromPreset(preset);
+               
+               // Surface finish is left unchanged
+               
+               ExternalComponent c = (ExternalComponent) preset;
+               
+               Material mat = c.getMaterial();
+               if (c.isMassOverridden()) {
+                       double mass = c.getOverrideMass();
+                       double volume = getComponentVolume();
+                       double density;
+                       if (volume > 0.00001) {
+                               density = mass / volume;
+                       } else {
+                               density = 1000;
+                       }
+                       mat = Material.newMaterial(Type.BULK, mat.getName(), density, true);
+               }
+               
+               setMaterial(mat);
+       }
+       
+       
        @Override
        protected List<RocketComponent> copyFrom(RocketComponent c) {
                ExternalComponent src = (ExternalComponent) c;
index f9b28ea381d048f5dbbda7d516ca6f6955dad81b..6b906589615dc0fc62ae807a0ebfaba743b1838c 100644 (file)
@@ -12,6 +12,7 @@ import javax.swing.event.ChangeListener;
 
 import net.sf.openrocket.l10n.Translator;
 import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.preset.ComponentPreset;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.ArrayList;
 import net.sf.openrocket.util.BugException;
@@ -123,6 +124,10 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
        // Unique ID of the component
        private String id = null;
        
+       // Preset component this component is based upon
+       private ComponentPreset presetComponent = null;
+       
+
        /**
         * Used to invalidate the component after calling {@link #copyFrom(RocketComponent)}.
         */
@@ -659,6 +664,92 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab
        
        
 
+       /**
+        * Return the preset component that this component is based upon.
+        * 
+        * @return      the preset component, or <code>null</code> if this is not based on a preset.
+        */
+       public final ComponentPreset getPresetComponent() {
+               return presetComponent;
+       }
+       
+       /**
+        * Set the preset component this component is based upon and load all of the 
+        * preset values.
+        * 
+        * @param preset        the preset component to load, or <code>null</code> to clear the preset.
+        */
+       public final void loadPreset(ComponentPreset preset) {
+               if (presetComponent == preset) {
+                       return;
+               }
+               
+               if (preset == null) {
+                       clearPreset();
+                       return;
+               }
+               
+               if (preset.getComponentClass() != this.getClass()) {
+                       throw new IllegalArgumentException("Attempting to load preset of type " + preset.getComponentClass()
+                                               + " into component of type " + this.getClass());
+               }
+               
+               RocketComponent root = getRoot();
+               final Rocket rocket;
+               if (root instanceof Rocket) {
+                       rocket = (Rocket) root;
+               } else {
+                       rocket = null;
+               }
+               
+               try {
+                       if (rocket != null) {
+                               rocket.freeze();
+                       }
+                       
+                       loadFromPreset(preset.getPrototype());
+                       
+                       this.presetComponent = preset;
+                       fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+                       
+               } finally {
+                       if (rocket != null) {
+                               rocket.thaw();
+                       }
+               }
+       }
+       
+       
+       /**
+        * Load component properties from the specified preset.  The preset is guaranteed
+        * to be of the correct type.
+        * <p>
+        * This method should fire the appropriate events related to the changes.  The rocket
+        * is frozen by the caller, so the events will be automatically combined.
+        * <p>
+        * This method must FIRST perform the preset loading and THEN call super.loadFromPreset().
+        * This is because mass setting requires the dimensions to be set beforehand.
+        * 
+        * @param preset        the preset to load from
+        */
+       protected void loadFromPreset(RocketComponent preset) {
+               // No-op
+       }
+       
+       
+       /**
+        * Clear the current component preset.  This does not affect the component properties
+        * otherwise.
+        */
+       public final void clearPreset() {
+               if (presetComponent == null)
+                       return;
+               presetComponent = null;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+
        /**
         * Returns the unique ID of the component.
         *
index 275de2051d5320b1466d36fc6a74b4f96504b256..5dc1835463e9ed3bf2ad72207c0835607e2b167c 100644 (file)
@@ -103,6 +103,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
                this.thickness = MathUtil.clamp(thickness, 0, Math.max(getForeRadius(), getAftRadius()));
                filled = false;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               clearPreset();
        }
        
        
@@ -124,6 +125,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
                        return;
                this.filled = filled;
                fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               clearPreset();
        }
        
        
@@ -143,6 +145,18 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
        
        
 
+       @Override
+       protected void loadFromPreset(RocketComponent preset) {
+               SymmetricComponent c = (SymmetricComponent) preset;
+               this.setThickness(c.getThickness());
+               this.setFilled(c.isFilled());
+               
+               super.loadFromPreset(preset);
+       }
+       
+       
+
+
        /**
         * Calculate volume of the component by integrating over the length of the component.
         * The method caches the result, so subsequent calls are instant.  Subclasses may
@@ -368,7 +382,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
                longitudinalInertia = 0;
                rotationalInertia = 0;
                
-               double volume = 0;
+               double vol = 0;
                
                for (int n = 1; n <= DIVISIONS; n++) {
                        /*
@@ -399,20 +413,20 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
                        longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12
                                        + pow2(x + l / 2));
                        
-                       volume += dV;
+                       vol += dV;
                        
                        // Update for next iteration
                        r1 = r2;
                        x += l;
                }
                
-               if (MathUtil.equals(volume, 0)) {
+               if (MathUtil.equals(vol, 0)) {
                        integrateInertiaSurface();
                        return;
                }
                
-               rotationalInertia /= volume;
-               longitudinalInertia /= volume;
+               rotationalInertia /= vol;
+               longitudinalInertia /= vol;
                
                // Shift longitudinal inertia to CG
                longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0);
index 05a22b1fcab57d0a94d2914b7a18c2e81e12f6f4..a2b39854bbfed73b0ef8a93944b15a2aed59575d 100644 (file)
@@ -17,6 +17,7 @@ import net.sf.openrocket.rocketcomponent.Rocket;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.ChangeSource;
 import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Utils;
 
 /**
  * A class holding simulation options in basic parameter form and which functions
@@ -415,7 +416,7 @@ public class SimulationOptions implements ChangeSource, Cloneable {
                        return false;
                SimulationOptions o = (SimulationOptions) other;
                return ((this.rocket == o.rocket) &&
-                               this.motorID.equals(o.motorID) &&
+                               Utils.equals(this.motorID, o.motorID) &&
                                MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
                                MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
                                MathUtil.equals(this.launchPressure, o.launchPressure) &&
index 831f2381afc44b53f13d84fb536cfc38d87572dd..d8aec3b0ad9d591a7084da38cf3d092bfcc1cc06 100644 (file)
@@ -39,6 +39,7 @@ import javax.swing.JButton;
 import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JDialog;
+import javax.swing.JLabel;
 import javax.swing.JRootPane;
 import javax.swing.JSlider;
 import javax.swing.JSpinner;
@@ -274,14 +275,18 @@ public class GUIUtil {
         */
        public static void changeFontStyle(TitledBorder border, int style) {
                /*
-                * There's been an NPE caused by the font changing, this is debug for it.
+                * The fix of JRE bug #4129681 causes a TitledBorder occasionally to
+                * return a null font.  We try to work around the issue by detecting it
+                * and reverting to the font of a JLabel instead.
                 */
-               if (border == null) {
-                       throw new BugException("border is null");
-               }
                Font font = border.getTitleFont();
                if (font == null) {
-                       throw new BugException("Border font is null");
+                       log.error("Border font is null, reverting to JLabel font");
+                       font = new JLabel().getFont();
+                       if (font == null) {
+                               log.error("JLabel font is null, not modifying font");
+                               return;
+                       }
                }
                font = font.deriveFont(style);
                if (font == null) {
diff --git a/test/net/sf/openrocket/logging/TraceExceptionTest.java b/test/net/sf/openrocket/logging/TraceExceptionTest.java
new file mode 100644 (file)
index 0000000..72f3c13
--- /dev/null
@@ -0,0 +1,89 @@
+package net.sf.openrocket.logging;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class TraceExceptionTest {
+       
+       private TraceException getViaAccess() {
+               
+               /*
+                * The SubClass test bases on the fact that getViaAccess method is defined on a row number < 20
+                * and the return statement is on a row number > 20.
+                * 
+                * The JRE sometimes adds an additional "access$NNN" method call between the calls with the
+                * row number equal to the method definition line.
+                * 
+                * 
+                * 
+                * 
+                * 
+                * 
+                * 
+                */
+
+               return new TraceException(0, 1);
+       }
+       
+       
+       @Test
+       public void testBasic() {
+               TraceException trace = new TraceException();
+               assertMatch("\\(TraceExceptionTest.java:[2-9][0-9]\\)", trace);
+       }
+       
+       
+       @Test
+       public void testOneLevelUp() {
+               // @formatter:off - these need to be on the same line number
+               TraceException trace = getOneLevelUp(); TraceException ref = new TraceException();
+               // @formatter:on
+               assertEquals(ref.getMessage(), trace.getMessage());
+       }
+       
+       private TraceException getOneLevelUp() {
+               return new TraceException(1);
+       }
+       
+       
+       @Test
+       public void testTwoLevels() {
+               TraceException trace = getTwoLevels();
+               assertMatch("\\(TraceExceptionTest.java:[2-9][0-9] TraceExceptionTest.java:[2-9][0-9]\\)", trace);
+       }
+       
+       private TraceException getTwoLevels() {
+               return new TraceException(0, 1);
+       }
+       
+       
+       @Test
+       public void testViaSubclass() {
+               /*
+                * This tests that TraceException.getMessage ignores the synthetic "access$0" method calls.
+                */
+
+               TraceException trace = new SubClass().getTrace();
+               assertMatch("\\(TraceExceptionTest.java:[2-9][0-9] TraceExceptionTest.java:[2-9][0-9]\\)", trace);
+       }
+       
+       private class SubClass {
+               private TraceException getTrace() {
+                       return getViaAccess();
+               }
+       }
+       
+       
+
+
+
+       private void assertMatch(String regex, TraceException trace) {
+               boolean match = trace.getMessage().matches(regex);
+               if (!match) {
+                       trace.printStackTrace();
+                       assertTrue("Was: " + trace.getMessage(), match);
+               }
+       }
+       
+}