major optimization updates
[debian/openrocket] / src / net / sf / openrocket / gui / dialogs / optimization / GeneralOptimizationDialog.java
index 39f9d2b2f47919c4b5903c418a4f457a8b3e4216..fe0e37744ad17b37974d708efb2035739fe8f49e 100644 (file)
 package net.sf.openrocket.gui.dialogs.optimization;
 
+import java.awt.Component;
 import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.ServiceLoader;
 
+import javax.swing.BorderFactory;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
 import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.ListSelectionModel;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableColumnModel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreePath;
 
+import net.miginfocom.swing.MigLayout;
 import net.sf.openrocket.document.OpenRocketDocument;
-import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationParameter;
-import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationParameterService;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.components.CsvOptionPanel;
+import net.sf.openrocket.gui.components.DescriptionArea;
+import net.sf.openrocket.gui.components.DoubleCellEditor;
+import net.sf.openrocket.gui.components.StyledLabel;
+import net.sf.openrocket.gui.components.StyledLabel.Style;
+import net.sf.openrocket.gui.components.UnitCellEditor;
+import net.sf.openrocket.gui.components.UnitSelector;
+import net.sf.openrocket.gui.scalefigure.RocketFigure;
+import net.sf.openrocket.gui.scalefigure.ScaleScrollPane;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.optimization.general.OptimizationException;
+import net.sf.openrocket.optimization.general.Point;
+import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter;
+import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal;
+import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain;
 import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier;
-import net.sf.openrocket.optimization.rocketoptimization.SimulationModifierService;
+import net.sf.openrocket.optimization.rocketoptimization.domains.IdentitySimulationDomain;
+import net.sf.openrocket.optimization.rocketoptimization.domains.StabilityDomain;
+import net.sf.openrocket.optimization.rocketoptimization.goals.MaximizationGoal;
+import net.sf.openrocket.optimization.rocketoptimization.goals.MinimizationGoal;
+import net.sf.openrocket.optimization.rocketoptimization.goals.ValueSeekGoal;
+import net.sf.openrocket.optimization.services.OptimizationServiceHelper;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.unit.CaliberUnit;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.unit.Value;
 import net.sf.openrocket.util.BugException;
+import net.sf.openrocket.util.Chars;
+import net.sf.openrocket.util.FileHelper;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Named;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.TextUtil;
 
+import com.itextpdf.text.Font;
+
+/**
+ * General rocket optimization dialog.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
 public class GeneralOptimizationDialog extends JDialog {
+       private static final LogHelper log = Application.getLogger();
+       private static final Translator trans = Application.getTranslator();
+       
+       private static final Collator collator = Collator.getInstance();
+       
+
+       private static final String GOAL_MAXIMIZE = trans.get("goal.maximize");
+       private static final String GOAL_MINIMIZE = trans.get("goal.minimize");
+       private static final String GOAL_SEEK = trans.get("goal.seek");
        
-       private final List<RocketOptimizationParameter> optimizationParameters = new ArrayList<RocketOptimizationParameter>();
+       private static final String START_TEXT = trans.get("btn.start");
+       private static final String STOP_TEXT = trans.get("btn.stop");
+       
+
+
+       private final List<OptimizableParameter> optimizationParameters = new ArrayList<OptimizableParameter>();
        private final Map<Object, List<SimulationModifier>> simulationModifiers =
                        new HashMap<Object, List<SimulationModifier>>();
        
 
-       private final OpenRocketDocument document;
+       private final OpenRocketDocument baseDocument;
+       private OpenRocketDocument documentCopy;
+       
+
+       private final JButton addButton;
+       private final JButton removeButton;
+       private final JButton removeAllButton;
+       
+       private final ParameterSelectionTableModel selectedModifierTableModel;
+       private final JTable selectedModifierTable;
+       private final DescriptionArea selectedModifierDescription;
+       private final SimulationModifierTree availableModifierTree;
+       
+       private final JComboBox simulationSelectionCombo;
+       private final JComboBox optimizationParameterCombo;
+       
+       private final JComboBox optimizationGoalCombo;
+       private final JSpinner optimizationGoalSpinner;
+       private final UnitSelector optimizationGoalUnitSelector;
+       private final DoubleModel optimizationSeekValue;
+       
+       private DoubleModel minimumStability;
+       private DoubleModel maximumStability;
+       private final JCheckBox minimumStabilitySelected;
+       private final JSpinner minimumStabilitySpinner;
+       private final UnitSelector minimumStabilityUnitSelector;
+       private final JCheckBox maximumStabilitySelected;
+       private final JSpinner maximumStabilitySpinner;
+       private final UnitSelector maximumStabilityUnitSelector;
+       
+       private final JLabel bestValueLabel;
+       private final JLabel stepCountLabel;
+       private final JLabel evaluationCountLabel;
+       private final JLabel stepSizeLabel;
        
+       private final RocketFigure figure;
+       private final JToggleButton startButton;
+       private final JButton plotButton;
+       private final JButton saveButton;
+       
+       private final JButton applyButton;
+       private final JButton resetButton;
+       private final JButton closeButton;
+       
+       private final List<SimulationModifier> selectedModifiers = new ArrayList<SimulationModifier>();
+       
+       /** List of components to disable while optimization is running */
+       private final List<JComponent> disableComponents = new ArrayList<JComponent>();
+       
+       /** Whether optimization is currently running or not */
+       private boolean running = false;
+       /** The optimization worker that is running */
+       private OptimizationWorker worker = null;
+       
+
+       private double bestValue = Double.NaN;
+       private Unit bestValueUnit = Unit.NOUNIT2;
+       private int stepCount = 0;
+       private int evaluationCount = 0;
+       private double stepSize = 0;
+       
+       private final Map<Point, FunctionEvaluationData> evaluationHistory = new LinkedHashMap<Point, FunctionEvaluationData>();
+       private final List<Point> optimizationPath = new LinkedList<Point>();
+       
+
+       private boolean updating = false;
+       
+       
+       /**
+        * Sole constructor.
+        * 
+        * @param document      the document
+        * @param parent        the parent window
+        */
        public GeneralOptimizationDialog(OpenRocketDocument document, Window parent) {
-               this.document = document;
+               super(parent, trans.get("title"));
+               
+               this.baseDocument = document;
+               this.documentCopy = document.copy();
                
                loadOptimizationParameters();
                loadSimulationModifiers();
+               
+               JPanel sub;
+               JLabel label;
+               JScrollPane scroll;
+               String tip;
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+
+               ChangeListener clearHistoryChangeListener = new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               clearHistory();
+                       }
+               };
+               ActionListener clearHistoryActionListener = new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               clearHistory();
+                       }
+               };
+               
+
+
+               //// Selected modifiers table
+               
+               selectedModifierTableModel = new ParameterSelectionTableModel();
+               selectedModifierTable = new JTable(selectedModifierTableModel);
+               selectedModifierTable.setDefaultRenderer(Double.class, new DoubleCellRenderer());
+               selectedModifierTable.setRowSelectionAllowed(true);
+               selectedModifierTable.setColumnSelectionAllowed(false);
+               selectedModifierTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               
+               // Make sure spinner editor fits into the cell height
+               selectedModifierTable.setRowHeight(new JSpinner().getPreferredSize().height - 4);
+               
+               selectedModifierTable.setDefaultEditor(Double.class, new DoubleCellEditor());
+               selectedModifierTable.setDefaultEditor(Unit.class, new UnitCellEditor() {
+                       @Override
+                       protected UnitGroup getUnitGroup(Unit value, int row, int column) {
+                               return selectedModifiers.get(row).getUnitGroup();
+                       }
+               });
+               
+               disableComponents.add(selectedModifierTable);
+               
+               selectedModifierTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               updateComponents();
+                       }
+               });
+               
+               // Set column widths
+               TableColumnModel columnModel = selectedModifierTable.getColumnModel();
+               columnModel.getColumn(0).setPreferredWidth(150);
+               columnModel.getColumn(1).setPreferredWidth(40);
+               columnModel.getColumn(2).setPreferredWidth(40);
+               columnModel.getColumn(3).setPreferredWidth(40);
+               
+               scroll = new JScrollPane(selectedModifierTable);
+               
+               label = new StyledLabel(trans.get("lbl.paramsToOptimize"), Style.BOLD);
+               disableComponents.add(label);
+               panel.add(label, "split 3, flowy");
+               panel.add(scroll, "wmin 300lp, height 200lp, grow");
+               selectedModifierDescription = new DescriptionArea(2, -3);
+               disableComponents.add(selectedModifierDescription);
+               panel.add(selectedModifierDescription, "growx");
+               
+
+
+               //// Add/remove buttons
+               sub = new JPanel(new MigLayout("fill"));
+               
+               addButton = new JButton(Chars.LEFT_ARROW + " " + trans.get("btn.add") + "   ");
+               addButton.setToolTipText(trans.get("btn.add.ttip"));
+               addButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               SimulationModifier mod = getSelectedAvailableModifier();
+                               if (mod != null) {
+                                       addModifier(mod);
+                                       clearHistory();
+                               } else {
+                                       log.error("Attempting to add simulation modifier when none is selected");
+                               }
+                       }
+               });
+               disableComponents.add(addButton);
+               sub.add(addButton, "wrap para, sg button");
+               
+               removeButton = new JButton("   " + trans.get("btn.remove") + " " + Chars.RIGHT_ARROW);
+               removeButton.setToolTipText(trans.get("btn.remove.ttip"));
+               removeButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               SimulationModifier mod = getSelectedModifier();
+                               if (mod == null) {
+                                       log.error("Attempting to remove simulation modifier when none is selected");
+                                       return;
+                               }
+                               removeModifier(mod);
+                               clearHistory();
+                       }
+               });
+               disableComponents.add(removeButton);
+               sub.add(removeButton, "wrap para*2, sg button");
+               
+               removeAllButton = new JButton(trans.get("btn.removeAll"));
+               removeAllButton.setToolTipText(trans.get("btn.removeAll.ttip"));
+               removeAllButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Removing all selected modifiers");
+                               selectedModifiers.clear();
+                               selectedModifierTableModel.fireTableDataChanged();
+                               availableModifierTree.repaint();
+                               clearHistory();
+                       }
+               });
+               disableComponents.add(removeAllButton);
+               sub.add(removeAllButton, "wrap para, sg button");
+               
+               panel.add(sub);
+               
+
+
+               //// Available modifier tree
+               availableModifierTree = new SimulationModifierTree(documentCopy.getRocket(), simulationModifiers, selectedModifiers);
+               availableModifierTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
+                       @Override
+                       public void valueChanged(TreeSelectionEvent e) {
+                               updateComponents();
+                       }
+               });
+               
+               // Handle double-click
+               availableModifierTree.addMouseListener(new MouseAdapter() {
+                       @Override
+                       public void mousePressed(MouseEvent e) {
+                               if (e.getClickCount() == 2) {
+                                       SimulationModifier mod = getSelectedAvailableModifier();
+                                       if (mod != null) {
+                                               addModifier(mod);
+                                               clearHistory();
+                                       } else {
+                                               log.user("Double-clicked non-available option");
+                                       }
+                               }
+                       }
+               });
+               
+               disableComponents.add(availableModifierTree);
+               scroll = new JScrollPane(availableModifierTree);
+               label = new StyledLabel(trans.get("lbl.availableParams"), Style.BOLD);
+               disableComponents.add(label);
+               panel.add(label, "split 2, flowy");
+               panel.add(scroll, "width 300lp, height 200lp, grow, wrap para*2");
+               
+
+
+
+               ////  Optimization options sub-panel
+               
+               sub = new JPanel(new MigLayout("fill"));
+               TitledBorder border = BorderFactory.createTitledBorder(trans.get("lbl.optimizationOpts"));
+               GUIUtil.changeFontStyle(border, Font.BOLD);
+               sub.setBorder(border);
+               disableComponents.add(sub);
+               
+
+               //// Simulation to optimize
+               
+               label = new JLabel(trans.get("lbl.optimizeSim"));
+               tip = trans.get("lbl.optimizeSim.ttip");
+               label.setToolTipText(tip);
+               disableComponents.add(label);
+               sub.add(label, "");
+               
+               simulationSelectionCombo = new JComboBox();
+               simulationSelectionCombo.setToolTipText(tip);
+               populateSimulations();
+               simulationSelectionCombo.addActionListener(clearHistoryActionListener);
+               disableComponents.add(simulationSelectionCombo);
+               sub.add(simulationSelectionCombo, "growx, wrap unrel");
+               
+
+
+               //// Value to optimize
+               label = new JLabel(trans.get("lbl.optimizeValue"));
+               tip = trans.get("lbl.optimizeValue.ttip");
+               label.setToolTipText(tip);
+               disableComponents.add(label);
+               sub.add(label, "");
+               
+               optimizationParameterCombo = new JComboBox();
+               optimizationParameterCombo.setToolTipText(tip);
+               populateParameters();
+               optimizationParameterCombo.addActionListener(clearHistoryActionListener);
+               disableComponents.add(optimizationParameterCombo);
+               sub.add(optimizationParameterCombo, "growx, wrap unrel");
+               
+
+
+               //// Optimization goal
+               label = new JLabel(trans.get("lbl.optimizeGoal"));
+               tip = trans.get("lbl.optimizeGoal");
+               label.setToolTipText(tip);
+               disableComponents.add(label);
+               sub.add(label, "");
+               
+               optimizationGoalCombo = new JComboBox(new String[] { GOAL_MAXIMIZE, GOAL_MINIMIZE, GOAL_SEEK });
+               optimizationGoalCombo.setToolTipText(tip);
+               optimizationGoalCombo.setEditable(false);
+               optimizationGoalCombo.addActionListener(clearHistoryActionListener);
+               disableComponents.add(optimizationGoalCombo);
+               sub.add(optimizationGoalCombo, "growx");
+               
+
+               //// Optimization custom value
+               optimizationSeekValue = new DoubleModel(0, UnitGroup.UNITS_NONE);
+               optimizationSeekValue.addChangeListener(clearHistoryChangeListener);
+               
+               optimizationGoalSpinner = new JSpinner(optimizationSeekValue.getSpinnerModel());
+               tip = trans.get("lbl.optimizeGoalValue.ttip");
+               optimizationGoalSpinner.setToolTipText(tip);
+               optimizationGoalSpinner.setEditor(new SpinnerEditor(optimizationGoalSpinner));
+               disableComponents.add(optimizationGoalSpinner);
+               sub.add(optimizationGoalSpinner, "width 30lp");
+               
+               optimizationGoalUnitSelector = new UnitSelector(optimizationSeekValue);
+               optimizationGoalUnitSelector.setToolTipText(tip);
+               disableComponents.add(optimizationGoalUnitSelector);
+               sub.add(optimizationGoalUnitSelector, "width 20lp, wrap unrel");
+               
+
+               panel.add(sub, "grow");
+               
+
+
+               ////  Required stability sub-panel
+               
+               sub = new JPanel(new MigLayout("fill"));
+               border = BorderFactory.createTitledBorder(trans.get("lbl.requireStability"));
+               GUIUtil.changeFontStyle(border, Font.BOLD);
+               sub.setBorder(border);
+               disableComponents.add(sub);
+               
+
+
+               double ref = CaliberUnit.calculateCaliber(baseDocument.getRocket());
+               minimumStability = new DoubleModel(ref, UnitGroup.stabilityUnits(ref));
+               maximumStability = new DoubleModel(5 * ref, UnitGroup.stabilityUnits(ref));
+               minimumStability.addChangeListener(clearHistoryChangeListener);
+               maximumStability.addChangeListener(clearHistoryChangeListener);
+               
+
+               //// Minimum stability
+               tip = trans.get("lbl.requireMinStability.ttip");
+               minimumStabilitySelected = new JCheckBox(trans.get("lbl.requireMinStability"));
+               minimumStabilitySelected.setSelected(true);
+               minimumStabilitySelected.setToolTipText(tip);
+               minimumStabilitySelected.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               updateComponents();
+                       }
+               });
+               disableComponents.add(minimumStabilitySelected);
+               sub.add(minimumStabilitySelected);
+               
+               minimumStabilitySpinner = new JSpinner(minimumStability.getSpinnerModel());
+               minimumStabilitySpinner.setToolTipText(tip);
+               minimumStabilitySpinner.setEditor(new SpinnerEditor(minimumStabilitySpinner));
+               disableComponents.add(minimumStabilitySpinner);
+               sub.add(minimumStabilitySpinner, "growx");
+               
+               minimumStabilityUnitSelector = new UnitSelector(minimumStability);
+               minimumStabilityUnitSelector.setToolTipText(tip);
+               disableComponents.add(minimumStabilityUnitSelector);
+               sub.add(minimumStabilityUnitSelector, "growx, wrap unrel");
+               
+
+               //// Maximum stability
+               tip = trans.get("lbl.requireMaxStability.ttip");
+               maximumStabilitySelected = new JCheckBox(trans.get("lbl.requireMaxStability"));
+               maximumStabilitySelected.setToolTipText(tip);
+               maximumStabilitySelected.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               updateComponents();
+                       }
+               });
+               disableComponents.add(maximumStabilitySelected);
+               sub.add(maximumStabilitySelected);
+               
+               maximumStabilitySpinner = new JSpinner(maximumStability.getSpinnerModel());
+               maximumStabilitySpinner.setToolTipText(tip);
+               maximumStabilitySpinner.setEditor(new SpinnerEditor(maximumStabilitySpinner));
+               disableComponents.add(maximumStabilitySpinner);
+               sub.add(maximumStabilitySpinner, "growx");
+               
+               maximumStabilityUnitSelector = new UnitSelector(maximumStability);
+               maximumStabilityUnitSelector.setToolTipText(tip);
+               disableComponents.add(maximumStabilityUnitSelector);
+               sub.add(maximumStabilityUnitSelector, "growx, wrap para");
+               
+
+
+               //              DescriptionArea desc = new DescriptionArea("Stability requirements are verified during each time step of the simulation.",
+               //                              2, -2, false);
+               //              desc.setViewportBorder(null);
+               //              disableComponents.add(desc);
+               //              sub.add(desc, "span, growx");
+               
+
+               panel.add(sub, "span 2, grow, wrap para*2");
+               
+
+
+
+               ////  Rocket figure
+               figure = new RocketFigure(getSelectedSimulation().getConfiguration());
+               figure.setBorderPixels(1, 1);
+               ScaleScrollPane figureScrollPane = new ScaleScrollPane(figure);
+               figureScrollPane.setFitting(true);
+               panel.add(figureScrollPane, "span, split, height 200lp, grow");
+               
+
+               sub = new JPanel(new MigLayout("fill"));
+               
+
+               label = new JLabel(trans.get("status.bestValue"));
+               tip = trans.get("status.bestValue.ttip");
+               label.setToolTipText(tip);
+               sub.add(label, "gapright unrel");
+               
+               bestValueLabel = new JLabel();
+               bestValueLabel.setToolTipText(tip);
+               sub.add(bestValueLabel, "wmin 60lp, wrap rel");
+               
+
+               label = new JLabel(trans.get("status.stepCount"));
+               tip = trans.get("status.stepCount.ttip");
+               label.setToolTipText(tip);
+               sub.add(label, "gapright unrel");
+               
+               stepCountLabel = new JLabel();
+               stepCountLabel.setToolTipText(tip);
+               sub.add(stepCountLabel, "wrap rel");
+               
+
+               label = new JLabel(trans.get("status.evalCount"));
+               tip = trans.get("status.evalCount");
+               label.setToolTipText(tip);
+               sub.add(label, "gapright unrel");
+               
+               evaluationCountLabel = new JLabel();
+               evaluationCountLabel.setToolTipText(tip);
+               sub.add(evaluationCountLabel, "wrap rel");
+               
+
+               label = new JLabel(trans.get("status.stepSize"));
+               tip = trans.get("status.stepSize.ttip");
+               label.setToolTipText(tip);
+               sub.add(label, "gapright unrel");
+               
+               stepSizeLabel = new JLabel();
+               stepSizeLabel.setToolTipText(tip);
+               sub.add(stepSizeLabel, "wrap para");
+               
+
+               //// Start/Stop button
+               
+               startButton = new JToggleButton(START_TEXT);
+               startButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (updating) {
+                                       log.debug("Updating, ignoring event");
+                                       return;
+                               }
+                               if (running) {
+                                       log.user("Stopping optimization");
+                                       stopOptimization();
+                               } else {
+                                       log.user("Starting optimization");
+                                       startOptimization();
+                               }
+                       }
+               });
+               sub.add(startButton, "span, growx, wrap para*2");
+               
+
+               plotButton = new JButton(trans.get("btn.plotPath"));
+               plotButton.setToolTipText(trans.get("btn.plotPath.ttip"));
+               plotButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Plotting optimization path, dimensionality=" + selectedModifiers.size());
+                               OptimizationPlotDialog dialog = new OptimizationPlotDialog(optimizationPath, evaluationHistory,
+                                               selectedModifiers, getSelectedParameter(),
+                                               UnitGroup.stabilityUnits(getSelectedSimulation().getRocket()),
+                                               GeneralOptimizationDialog.this);
+                               dialog.setVisible(true);
+                       }
+               });
+               disableComponents.add(plotButton);
+               sub.add(plotButton, "span, growx, wrap");
+               
+
+               saveButton = new JButton(trans.get("btn.save"));
+               saveButton.setToolTipText(trans.get("btn.save.ttip"));
+               saveButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("User selected save path");
+                               savePath();
+                       }
+               });
+               disableComponents.add(saveButton);
+               sub.add(saveButton, "span, growx");
+               
+
+
+               panel.add(sub, "wrap para*2");
+               
+
+
+
+               ////  Bottom buttons
+               
+               applyButton = new JButton(trans.get("btn.apply"));
+               applyButton.setToolTipText(trans.get("btn.apply.ttip"));
+               applyButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Applying optimization changes");
+                               applyDesign();
+                       }
+               });
+               disableComponents.add(applyButton);
+               panel.add(applyButton, "span, split, gapright para, right");
+               
+               resetButton = new JButton(trans.get("btn.reset"));
+               resetButton.setToolTipText(trans.get("btn.reset.ttip"));
+               resetButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Resetting optimization design");
+                               resetDesign();
+                       }
+               });
+               disableComponents.add(resetButton);
+               panel.add(resetButton, "gapright para, right");
+               
+               closeButton = new JButton(trans.get("btn.close"));
+               closeButton.setToolTipText(trans.get("btn.close.ttip"));
+               closeButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               log.user("Closing optimization dialog");
+                               stopOptimization();
+                               GeneralOptimizationDialog.this.dispose();
+                       }
+               });
+               panel.add(closeButton, "right");
+               
+
+               this.add(panel);
+               clearHistory();
+               updateComponents();
+               GUIUtil.setDisposableDialogOptions(this, null);
        }
        
        
-       private void loadOptimizationParameters() {
-               ServiceLoader<RocketOptimizationParameterService> loader =
-                               ServiceLoader.load(RocketOptimizationParameterService.class);
+       private void startOptimization() {
+               if (running) {
+                       log.info("Optimization already running");
+                       return;
+               }
+               
+
+               if (selectedModifiers.isEmpty()) {
+                       JOptionPane.showMessageDialog(this, trans.get("error.selectParams.text"),
+                                       trans.get("error.selectParams.title"), JOptionPane.ERROR_MESSAGE);
+                       updating = true;
+                       startButton.setSelected(false);
+                       startButton.setText(START_TEXT);
+                       updating = false;
+                       return;
+               }
+               
+
+               running = true;
+               
+               // Update the button status
+               updating = true;
+               startButton.setSelected(true);
+               startButton.setText(STOP_TEXT);
+               updating = false;
+               
+
+               // Create a copy of the simulation (we're going to modify the original in the current thread)
+               Simulation simulation = getSelectedSimulation();
+               Rocket rocketCopy = simulation.getRocket().copyWithOriginalID();
+               simulation = simulation.duplicateSimulation(rocketCopy);
+               
+               OptimizableParameter parameter = getSelectedParameter();
+               
+               OptimizationGoal goal;
+               String value = (String) optimizationGoalCombo.getSelectedItem();
+               if (GOAL_MAXIMIZE.equals(value)) {
+                       goal = new MaximizationGoal();
+               } else if (GOAL_MINIMIZE.equals(value)) {
+                       goal = new MinimizationGoal();
+               } else if (GOAL_SEEK.equals(value)) {
+                       goal = new ValueSeekGoal(optimizationSeekValue.getValue());
+               } else {
+                       throw new BugException("optimizationGoalCombo had invalid value: " + value);
+               }
+               
+               SimulationDomain domain;
+               if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) {
+                       double min, max;
+                       boolean minAbsolute, maxAbsolute;
+                       
+                       /*
+                        * Make minAbsolute/maxAbsolute consistent with each other to produce reasonable
+                        * result in plot tool tips.  Yes, this is a bit ugly.
+                        */
+
+                       // Min stability
+                       Unit unit = minimumStability.getCurrentUnit();
+                       if (unit instanceof CaliberUnit) {
+                               min = unit.toUnit(minimumStability.getValue());
+                               minAbsolute = false;
+                       } else {
+                               min = minimumStability.getValue();
+                               minAbsolute = true;
+                       }
+                       
+                       // Max stability
+                       unit = maximumStability.getCurrentUnit();
+                       if (unit instanceof CaliberUnit) {
+                               max = unit.toUnit(maximumStability.getValue());
+                               maxAbsolute = false;
+                       } else {
+                               max = maximumStability.getValue();
+                               maxAbsolute = true;
+                       }
+                       
+
+                       if (!minimumStabilitySelected.isSelected()) {
+                               min = Double.NaN;
+                               minAbsolute = maxAbsolute;
+                       }
+                       if (!maximumStabilitySelected.isSelected()) {
+                               max = Double.NaN;
+                               maxAbsolute = minAbsolute;
+                       }
+                       
+                       domain = new StabilityDomain(min, minAbsolute, max, maxAbsolute);
+               } else {
+                       domain = new IdentitySimulationDomain();
+               }
+               
+               SimulationModifier[] modifiers = selectedModifiers.toArray(new SimulationModifier[0]);
+               
+               // Create and start the background worker
+               worker = new OptimizationWorker(simulation, parameter, goal, domain, modifiers) {
+                       @Override
+                       protected void done(OptimizationException exception) {
+                               log.info("Optimization finished, exception=" + exception, exception);
+                               
+                               if (exception != null) {
+                                       JOptionPane.showMessageDialog(GeneralOptimizationDialog.this,
+                                                       new Object[] {
+                                                                       trans.get("error.optimizationFailure.text"),
+                                                                       exception.getLocalizedMessage()
+                                       }, trans.get("error.optimizationFailure.title"), JOptionPane.ERROR_MESSAGE);
+                               }
+                               
+                               worker = null;
+                               stopOptimization();
+                       }
+                       
+                       @Override
+                       protected void functionEvaluated(List<FunctionEvaluationData> data) {
+                               for (FunctionEvaluationData d : data) {
+                                       evaluationHistory.put(d.getPoint(), d);
+                                       evaluationCount++;
+                               }
+                               updateCounters();
+                       }
+                       
+                       @Override
+                       protected void optimizationStepTaken(List<OptimizationStepData> data) {
+                               
+                               // Add starting point to the path
+                               if (optimizationPath.isEmpty()) {
+                                       optimizationPath.add(data.get(0).getOldPoint());
+                               }
+                               
+                               // Add other points to the path
+                               for (OptimizationStepData d : data) {
+                                       optimizationPath.add(d.getNewPoint());
+                               }
+                               
+                               // Get function value from the latest point
+                               OptimizationStepData latest = data.get(data.size() - 1);
+                               Point newPoint = latest.getNewPoint();
+                               
+                               FunctionEvaluationData pointValue = evaluationHistory.get(newPoint);
+                               if (pointValue != null) {
+                                       bestValue = pointValue.getParameterValue().getValue();
+                               } else {
+                                       log.error("History does not contain point " + newPoint);
+                                       bestValue = Double.NaN;
+                               }
+                               
+                               // Update the simulation
+                               Simulation sim = getSelectedSimulation();
+                               for (int i = 0; i < newPoint.dim(); i++) {
+                                       try {
+                                               selectedModifiers.get(i).modify(sim, newPoint.get(i));
+                                       } catch (OptimizationException e) {
+                                               throw new BugException("Simulation modifier failed to modify the base simulation " +
+                                                               "modifier=" + selectedModifiers.get(i), e);
+                                       }
+                               }
+                               figure.updateFigure();
+                               
+                               // Update other counter data
+                               stepCount += data.size();
+                               stepSize = latest.getStepSize();
+                               updateCounters();
+                       }
+               };
+               worker.start();
+               
+
+               clearHistory();
                
-               for (RocketOptimizationParameterService g : loader) {
-                       optimizationParameters.addAll(g.getParameters(document));
+               updateComponents();
+       }
+       
+       private void stopOptimization() {
+               if (!running) {
+                       log.info("Optimization not running");
+                       return;
+               }
+               
+               if (worker != null && worker.isAlive()) {
+                       log.info("Worker still running, interrupting it and setting to null");
+                       worker.interrupt();
+                       worker = null;
+                       return;
+               }
+               
+               running = false;
+               
+               // Update the button status
+               updating = true;
+               startButton.setSelected(false);
+               startButton.setText(START_TEXT);
+               updating = false;
+               
+
+               updateComponents();
+       }
+       
+       
+
+
+       /**
+        * Reset the current optimization history and values.  This does not reset the design.
+        */
+       private void clearHistory() {
+               evaluationHistory.clear();
+               optimizationPath.clear();
+               bestValue = Double.NaN;
+               bestValueUnit = getSelectedParameter().getUnitGroup().getDefaultUnit();
+               stepCount = 0;
+               evaluationCount = 0;
+               stepSize = 0.5;
+               updateCounters();
+               updateComponents();
+       }
+       
+       
+       private void applyDesign() {
+               // TODO: MEDIUM: Apply also potential changes to simulations
+               Rocket src = getSelectedSimulation().getRocket().copyWithOriginalID();
+               Rocket dest = baseDocument.getRocket();
+               try {
+                       baseDocument.startUndo(trans.get("undoText"));
+                       dest.freeze();
+                       
+                       // Remove all children
+                       while (dest.getChildCount() > 0) {
+                               dest.removeChild(0);
+                       }
+                       
+                       // Move all children to the destination rocket
+                       while (src.getChildCount() > 0) {
+                               RocketComponent c = src.getChild(0);
+                               src.removeChild(0);
+                               dest.addChild(c);
+                       }
+                       
+               } finally {
+                       dest.thaw();
+                       baseDocument.stopUndo();
+               }
+       }
+       
+       
+       private void resetDesign() {
+               clearHistory();
+               
+               documentCopy = baseDocument.copy();
+               
+               loadOptimizationParameters();
+               loadSimulationModifiers();
+               
+               // Replace selected modifiers with corresponding new modifiers
+               List<SimulationModifier> newSelected = new ArrayList<SimulationModifier>();
+               for (SimulationModifier original : selectedModifiers) {
+                       List<SimulationModifier> newModifiers = simulationModifiers.get(original.getRelatedObject());
+                       if (newModifiers != null) {
+                               int index = newModifiers.indexOf(original);
+                               if (index >= 0) {
+                                       newSelected.add(newModifiers.get(index));
+                               }
+                       }
+               }
+               selectedModifiers.clear();
+               selectedModifiers.addAll(newSelected);
+               selectedModifierTableModel.fireTableDataChanged();
+               
+               // Update the available modifier tree
+               availableModifierTree.populateTree(documentCopy.getRocket(), simulationModifiers);
+               availableModifierTree.expandComponents();
+               
+
+               // Update selectable simulations
+               populateSimulations();
+               
+               // Update selectable parameters
+               populateParameters();
+               
+       }
+       
+       
+       private void populateSimulations() {
+               String current = null;
+               Object selection = simulationSelectionCombo.getSelectedItem();
+               if (selection != null) {
+                       current = selection.toString();
                }
                
+
+               List<Named<Simulation>> simulations = new ArrayList<Named<Simulation>>();
+               Rocket rocket = documentCopy.getRocket();
+               
+               for (Simulation s : documentCopy.getSimulations()) {
+                       String id = s.getConfiguration().getMotorConfigurationID();
+                       String name = createSimulationName(s.getName(), rocket.getMotorConfigurationNameOrDescription(id));
+                       simulations.add(new Named<Simulation>(s, name));
+               }
+               
+               for (String id : rocket.getMotorConfigurationIDs()) {
+                       if (id == null) {
+                               continue;
+                       }
+                       Simulation sim = new Simulation(rocket);
+                       sim.getConfiguration().setMotorConfigurationID(id);
+                       String name = createSimulationName(trans.get("basicSimulationName"), rocket.getMotorConfigurationNameOrDescription(id));
+                       simulations.add(new Named<Simulation>(sim, name));
+               }
+               
+
+               Simulation sim = new Simulation(rocket);
+               sim.getConfiguration().setMotorConfigurationID(null);
+               String name = createSimulationName(trans.get("noSimulationName"), rocket.getMotorConfigurationNameOrDescription(null));
+               simulations.add(new Named<Simulation>(sim, name));
+               
+
+               simulationSelectionCombo.setModel(new DefaultComboBoxModel(simulations.toArray()));
+               
+               if (current != null) {
+                       for (int i = 0; i < simulations.size(); i++) {
+                               if (simulations.get(i).toString().equals(current)) {
+                                       simulationSelectionCombo.setSelectedIndex(i);
+                                       break;
+                               }
+                       }
+               }
+       }
+       
+       
+       private void populateParameters() {
+               String current = null;
+               Object selection = optimizationParameterCombo.getSelectedItem();
+               if (selection != null) {
+                       current = selection.toString();
+               } else {
+                       // Default to apogee altitude event if it is not the first one in the list
+                       current = trans.get("MaximumAltitudeParameter.name");
+               }
+               
+               List<Named<OptimizableParameter>> parameters = new ArrayList<Named<OptimizableParameter>>();
+               for (OptimizableParameter p : optimizationParameters) {
+                       parameters.add(new Named<OptimizableParameter>(p, p.getName()));
+               }
+               
+               optimizationParameterCombo.setModel(new DefaultComboBoxModel(parameters.toArray()));
+               
+               for (int i = 0; i < parameters.size(); i++) {
+                       if (parameters.get(i).toString().equals(current)) {
+                               optimizationParameterCombo.setSelectedIndex(i);
+                               break;
+                       }
+               }
+       }
+       
+       private void updateCounters() {
+               bestValueLabel.setText(bestValueUnit.toStringUnit(bestValue));
+               stepCountLabel.setText("" + stepCount);
+               evaluationCountLabel.setText("" + evaluationCount);
+               stepSizeLabel.setText(UnitGroup.UNITS_RELATIVE.toStringUnit(stepSize));
+       }
+       
+       
+       private void loadOptimizationParameters() {
+               optimizationParameters.clear();
+               optimizationParameters.addAll(OptimizationServiceHelper.getOptimizableParameters(documentCopy));
+               
                if (optimizationParameters.isEmpty()) {
                        throw new BugException("No rocket optimization parameters found, distribution built wrong.");
                }
                
-               Collections.sort(optimizationParameters, new Comparator<RocketOptimizationParameter>() {
+               Collections.sort(optimizationParameters, new Comparator<OptimizableParameter>() {
                        @Override
-                       public int compare(RocketOptimizationParameter o1, RocketOptimizationParameter o2) {
+                       public int compare(OptimizableParameter o1, OptimizableParameter o2) {
                                return o1.getName().compareTo(o2.getName());
                        }
                });
@@ -57,18 +1057,16 @@ public class GeneralOptimizationDialog extends JDialog {
        
        
        private void loadSimulationModifiers() {
-               ServiceLoader<SimulationModifierService> loader = ServiceLoader.load(SimulationModifierService.class);
+               simulationModifiers.clear();
                
-               for (SimulationModifierService g : loader) {
-                       for (SimulationModifier m : g.getModifiers(document)) {
-                               Object key = m.getRelatedObject();
-                               List<SimulationModifier> list = simulationModifiers.get(key);
-                               if (list == null) {
-                                       list = new ArrayList<SimulationModifier>();
-                                       simulationModifiers.put(key, list);
-                               }
-                               list.add(m);
+               for (SimulationModifier m : OptimizationServiceHelper.getSimulationModifiers(documentCopy)) {
+                       Object key = m.getRelatedObject();
+                       List<SimulationModifier> list = simulationModifiers.get(key);
+                       if (list == null) {
+                               list = new ArrayList<SimulationModifier>();
+                               simulationModifiers.put(key, list);
                        }
+                       list.add(m);
                }
                
                for (Object key : simulationModifiers.keySet()) {
@@ -83,4 +1081,464 @@ public class GeneralOptimizationDialog extends JDialog {
                
        }
        
+       
+
+       private void addModifier(SimulationModifier mod) {
+               if (!selectedModifiers.contains(mod)) {
+                       log.user(1, "Adding simulation modifier " + mod);
+                       selectedModifiers.add(mod);
+                       Collections.sort(selectedModifiers, new SimulationModifierComparator());
+                       selectedModifierTableModel.fireTableDataChanged();
+                       availableModifierTree.repaint();
+               } else {
+                       log.user(1, "Attempting to add an already existing simulation modifier " + mod);
+               }
+       }
+       
+       
+       private void removeModifier(SimulationModifier mod) {
+               log.user(1, "Removing simulation modifier " + mod);
+               selectedModifiers.remove(mod);
+               selectedModifierTableModel.fireTableDataChanged();
+               availableModifierTree.repaint();
+       }
+       
+       
+
+       /**
+        * Update the enabled status of all components in the dialog.
+        */
+       private void updateComponents() {
+               
+               if (updating) {
+                       return;
+               }
+               
+               updating = true;
+               
+
+               // First enable all components if optimization not running
+               if (!running) {
+                       for (JComponent c : disableComponents) {
+                               c.setEnabled(true);
+                       }
+               }
+               
+
+               // "Add" button
+               SimulationModifier mod = getSelectedAvailableModifier();
+               if (mod != null && !selectedModifiers.contains(mod)) {
+                       addButton.setEnabled(true);
+               } else {
+                       addButton.setEnabled(false);
+               }
+               
+               // "Remove" button
+               removeButton.setEnabled(selectedModifierTable.getSelectedRow() >= 0);
+               
+               // "Remove all" button
+               removeAllButton.setEnabled(!selectedModifiers.isEmpty());
+               
+
+               // 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);
+               }
+               
+
+               // Minimum/maximum stability options
+               minimumStabilitySpinner.setEnabled(minimumStabilitySelected.isSelected());
+               minimumStabilityUnitSelector.setEnabled(minimumStabilitySelected.isSelected());
+               maximumStabilitySpinner.setEnabled(maximumStabilitySelected.isSelected());
+               maximumStabilityUnitSelector.setEnabled(maximumStabilitySelected.isSelected());
+               
+
+               // Plot button (enabled if path exists and dimensionality is 1 or 2)
+               plotButton.setEnabled(!optimizationPath.isEmpty() && (selectedModifiers.size() == 1 || selectedModifiers.size() == 2));
+               
+               // Save button (enabled if path exists)
+               saveButton.setEnabled(!optimizationPath.isEmpty());
+               
+
+               // Last disable all components if optimization is running
+               if (running) {
+                       for (JComponent c : disableComponents) {
+                               c.setEnabled(false);
+                       }
+               }
+               
+
+               // Update description text
+               mod = getSelectedModifier();
+               if (mod != null) {
+                       selectedModifierDescription.setText(mod.getDescription());
+               } else {
+                       selectedModifierDescription.setText("");
+               }
+               
+
+               // Update the figure
+               figure.setConfiguration(getSelectedSimulation().getConfiguration());
+               
+               updating = false;
+       }
+       
+       
+       private void savePath() {
+               
+               if (evaluationHistory.isEmpty()) {
+                       throw new BugException("evaluation history is empty");
+               }
+               
+               CsvOptionPanel csvOptions = new CsvOptionPanel(GeneralOptimizationDialog.class,
+                               trans.get("export.header"), trans.get("export.header.ttip"));
+               
+
+               JFileChooser chooser = new JFileChooser();
+               chooser.setFileFilter(FileHelper.CSV_FILE_FILTER);
+               chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
+               chooser.setAccessory(csvOptions);
+               
+               if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION)
+                       return;
+               
+               File file = chooser.getSelectedFile();
+               if (file == null)
+                       return;
+               
+               file = FileHelper.ensureExtension(file, "csv");
+               if (!FileHelper.confirmWrite(file, this)) {
+                       return;
+               }
+               
+               String fieldSeparator = csvOptions.getFieldSeparator();
+               String commentCharacter = csvOptions.getCommentCharacter();
+               boolean includeHeader = csvOptions.getSelectionOption(0);
+               csvOptions.storePreferences();
+               
+               log.info("Saving optimization path to " + file + ", fieldSeparator=" + fieldSeparator +
+                               ", commentCharacter=" + commentCharacter + ", includeHeader=" + includeHeader);
+               
+               try {
+                       Writer writer = new BufferedWriter(new FileWriter(file));
+                       
+                       // Write header
+                       if (includeHeader) {
+                               FunctionEvaluationData data = evaluationHistory.values().iterator().next();
+                               
+                               writer.write(commentCharacter);
+                               for (SimulationModifier mod : selectedModifiers) {
+                                       writer.write(mod.getRelatedObject().toString() + ": " + mod.getName() + " / " +
+                                                       mod.getUnitGroup().getDefaultUnit().getUnit());
+                                       writer.write(fieldSeparator);
+                               }
+                               if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) {
+                                       writer.write(trans.get("export.stability") + " / " + data.getDomainReference().getUnit().getUnit());
+                                       writer.write(fieldSeparator);
+                               }
+                               writer.write(getSelectedParameter().getName() + " / " +
+                                               getSelectedParameter().getUnitGroup().getDefaultUnit().getUnit());
+                               
+                               writer.write("\n");
+                       }
+                       
+
+                       for (FunctionEvaluationData data : evaluationHistory.values()) {
+                               Value[] state = data.getState();
+                               
+                               for (int i = 0; i < state.length; i++) {
+                                       writer.write(TextUtil.doubleToString(state[i].getUnitValue()));
+                                       writer.write(fieldSeparator);
+                               }
+                               
+                               if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) {
+                                       writer.write(TextUtil.doubleToString(data.getDomainReference().getUnitValue()));
+                                       writer.write(fieldSeparator);
+                               }
+                               
+                               if (data.getParameterValue() != null) {
+                                       writer.write(TextUtil.doubleToString(data.getParameterValue().getUnitValue()));
+                               } else {
+                                       writer.write("N/A");
+                               }
+                               writer.write("\n");
+                       }
+                       
+                       writer.close();
+                       log.info("File successfully saved");
+                       
+               } catch (IOException e) {
+                       FileHelper.errorWriting(e, this);
+               }
+               
+       }
+       
+       /**
+        * Return the currently selected available simulation modifier from the modifier tree,
+        * or <code>null</code> if none selected.
+        */
+       private SimulationModifier getSelectedAvailableModifier() {
+               TreePath treepath = availableModifierTree.getSelectionPath();
+               if (treepath != null) {
+                       Object o = ((DefaultMutableTreeNode) treepath.getLastPathComponent()).getUserObject();
+                       if (o instanceof SimulationModifier) {
+                               return (SimulationModifier) o;
+                       }
+               }
+               return null;
+       }
+       
+       /**
+        * Return the currently selected simulation.
+        * @return      the selected simulation.
+        */
+       @SuppressWarnings("unchecked")
+       private Simulation getSelectedSimulation() {
+               return ((Named<Simulation>) simulationSelectionCombo.getSelectedItem()).get();
+       }
+       
+       
+       /**
+        * Return the currently selected simulation modifier from the table,
+        * or <code>null</code> if none selected.
+        * @return      the selected modifier or <code>null</code>.
+        */
+       private SimulationModifier getSelectedModifier() {
+               int row = selectedModifierTable.getSelectedRow();
+               if (row < 0) {
+                       return null;
+               }
+               row = selectedModifierTable.convertRowIndexToModel(row);
+               return selectedModifiers.get(row);
+       }
+       
+       
+       /**
+        * Return the currently selected optimization parameter.
+        * @return      the selected optimization parameter.
+        */
+       @SuppressWarnings("unchecked")
+       private OptimizableParameter getSelectedParameter() {
+               return ((Named<OptimizableParameter>) optimizationParameterCombo.getSelectedItem()).get();
+       }
+       
+       
+       private Unit getModifierUnit(int index) {
+               return selectedModifiers.get(index).getUnitGroup().getDefaultUnit();
+       }
+       
+       private String createSimulationName(String simulationName, String motorConfiguration) {
+               String name;
+               boolean hasParenthesis = motorConfiguration.matches("^[\\[\\(].*[\\]\\)]$");
+               name = simulationName + " ";
+               if (!hasParenthesis) {
+                       name += "(";
+               }
+               name += motorConfiguration;
+               if (!hasParenthesis) {
+                       name += ")";
+               }
+               return name;
+       }
+       
+       /**
+        * The table model for the parameter selection.
+        * 
+        * [Body tube: Length]  [min]  [max]  [unit]
+        */
+       private class ParameterSelectionTableModel extends AbstractTableModel {
+               
+               private static final int PARAMETER = 0;
+               private static final int CURRENT = 1;
+               private static final int MIN = 2;
+               private static final int MAX = 3;
+               private static final int COUNT = 4;
+               
+               @Override
+               public int getColumnCount() {
+                       return COUNT;
+               }
+               
+               @Override
+               public int getRowCount() {
+                       return selectedModifiers.size();
+               }
+               
+               @Override
+               public String getColumnName(int column) {
+                       switch (column) {
+                       case PARAMETER:
+                               return trans.get("table.col.parameter");
+                       case CURRENT:
+                               return trans.get("table.col.current");
+                       case MIN:
+                               return trans.get("table.col.min");
+                       case MAX:
+                               return trans.get("table.col.max");
+                       default:
+                               throw new IndexOutOfBoundsException("column=" + column);
+                       }
+                       
+               }
+               
+               @Override
+               public Class<?> getColumnClass(int column) {
+                       switch (column) {
+                       case PARAMETER:
+                               return String.class;
+                       case CURRENT:
+                               return Double.class;
+                       case MIN:
+                               return Double.class;
+                       case MAX:
+                               return Double.class;
+                       default:
+                               throw new IndexOutOfBoundsException("column=" + column);
+                       }
+               }
+               
+               @Override
+               public Object getValueAt(int row, int column) {
+                       
+                       SimulationModifier modifier = selectedModifiers.get(row);
+                       
+                       switch (column) {
+                       case PARAMETER:
+                               return modifier.getRelatedObject().toString() + ": " + modifier.getName();
+                       case CURRENT:
+                               try {
+                                       return getModifierUnit(row).toUnit(modifier.getCurrentSIValue(getSelectedSimulation()));
+                               } catch (OptimizationException e) {
+                                       throw new BugException("Could not read current SI value from modifier " + modifier, e);
+                               }
+                       case MIN:
+                               return getModifierUnit(row).toUnit(modifier.getMinValue());
+                       case MAX:
+                               return getModifierUnit(row).toUnit(modifier.getMaxValue());
+                       default:
+                               throw new IndexOutOfBoundsException("column=" + column);
+                       }
+                       
+               }
+               
+               @Override
+               public void setValueAt(Object value, int row, int column) {
+                       
+                       switch (column) {
+                       case PARAMETER:
+                               break;
+                       
+                       case MIN:
+                               double min = (Double) value;
+                               min = getModifierUnit(row).fromUnit(min);
+                               selectedModifiers.get(row).setMinValue(min);
+                               break;
+                       
+                       case CURRENT:
+                               break;
+                       
+                       case MAX:
+                               double max = (Double) value;
+                               max = getModifierUnit(row).fromUnit(max);
+                               selectedModifiers.get(row).setMaxValue(max);
+                               break;
+                       
+                       default:
+                               throw new IndexOutOfBoundsException("column=" + column);
+                       }
+                       this.fireTableRowsUpdated(row, row);
+                       
+               }
+               
+               @Override
+               public boolean isCellEditable(int row, int column) {
+                       switch (column) {
+                       case PARAMETER:
+                               return false;
+                       case CURRENT:
+                               return false;
+                       case MIN:
+                               return true;
+                       case MAX:
+                               return true;
+                       default:
+                               throw new IndexOutOfBoundsException("column=" + column);
+                       }
+               }
+               
+       }
+       
+       
+       private class DoubleCellRenderer extends DefaultTableCellRenderer {
+               @Override
+               public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+                               boolean hasFocus, int row, int column) {
+                       
+                       super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+                       
+                       double val = (Double) value;
+                       Unit unit = getModifierUnit(row);
+                       
+                       val = unit.fromUnit(val);
+                       this.setText(unit.toStringUnit(val));
+                       
+                       return this;
+               }
+       }
+       
+       
+       private static class SimulationModifierComparator implements Comparator<SimulationModifier> {
+               
+               @Override
+               public int compare(SimulationModifier mod1, SimulationModifier mod2) {
+                       Object rel1 = mod1.getRelatedObject();
+                       Object rel2 = mod2.getRelatedObject();
+                       
+                       /*
+                        * Primarily order by related object:
+                        * 
+                        * - RocketComponents first
+                        * - Two RocketComponents are ordered based on their position in the rocket
+                        */
+                       if (!rel1.equals(rel2)) {
+                               
+                               if (rel1 instanceof RocketComponent) {
+                                       if (rel2 instanceof RocketComponent) {
+                                               
+                                               RocketComponent root = ((RocketComponent) rel1).getRoot();
+                                               for (RocketComponent c : root) {
+                                                       if (c.equals(rel1)) {
+                                                               return -1;
+                                                       }
+                                                       if (c.equals(rel2)) {
+                                                               return 1;
+                                                       }
+                                               }
+                                               
+                                               throw new BugException("Error sorting modifiers, mod1=" + mod1 + " rel1=" + rel1 +
+                                                               " mod2=" + mod2 + " rel2=" + rel2);
+                                               
+                                       } else {
+                                               return -1;
+                                       }
+                               } else {
+                                       if (rel2 instanceof RocketComponent) {
+                                               return 1;
+                                       }
+                               }
+                               
+                       }
+                       
+                       // Secondarily sort by name
+                       return collator.compare(mod1.getName(), mod2.getName());
+               }
+       }
+       
+
+
 }