added svn:ignores
[debian/openrocket] / src / net / sf / openrocket / gui / dialogs / motor / thrustcurve / ThrustCurveMotorSelectionPanel.java
1 package net.sf.openrocket.gui.dialogs.motor.thrustcurve;
2
3 import java.awt.BasicStroke;
4 import java.awt.Color;
5 import java.awt.Component;
6 import java.awt.Cursor;
7 import java.awt.Font;
8 import java.awt.Paint;
9 import java.awt.Rectangle;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.ActionListener;
12 import java.awt.event.MouseAdapter;
13 import java.awt.event.MouseEvent;
14 import java.util.ArrayList;
15 import java.util.Collections;
16 import java.util.List;
17 import java.util.prefs.Preferences;
18
19 import javax.swing.BorderFactory;
20 import javax.swing.DefaultComboBoxModel;
21 import javax.swing.JCheckBox;
22 import javax.swing.JComboBox;
23 import javax.swing.JComponent;
24 import javax.swing.JLabel;
25 import javax.swing.JLayeredPane;
26 import javax.swing.JList;
27 import javax.swing.JPanel;
28 import javax.swing.JScrollPane;
29 import javax.swing.JSeparator;
30 import javax.swing.JTable;
31 import javax.swing.JTextArea;
32 import javax.swing.JTextField;
33 import javax.swing.ListCellRenderer;
34 import javax.swing.ListSelectionModel;
35 import javax.swing.RowFilter;
36 import javax.swing.SwingUtilities;
37 import javax.swing.event.DocumentEvent;
38 import javax.swing.event.DocumentListener;
39 import javax.swing.event.ListSelectionEvent;
40 import javax.swing.event.ListSelectionListener;
41 import javax.swing.table.TableModel;
42 import javax.swing.table.TableRowSorter;
43
44 import net.miginfocom.swing.MigLayout;
45 import net.sf.openrocket.database.ThrustCurveMotorSet;
46 import net.sf.openrocket.gui.components.StyledLabel;
47 import net.sf.openrocket.gui.components.StyledLabel.Style;
48 import net.sf.openrocket.gui.dialogs.motor.CloseableDialog;
49 import net.sf.openrocket.gui.dialogs.motor.MotorSelector;
50 import net.sf.openrocket.logging.LogHelper;
51 import net.sf.openrocket.motor.Motor;
52 import net.sf.openrocket.motor.MotorDigest;
53 import net.sf.openrocket.motor.ThrustCurveMotor;
54 import net.sf.openrocket.startup.Application;
55 import net.sf.openrocket.unit.UnitGroup;
56 import net.sf.openrocket.util.BugException;
57 import net.sf.openrocket.util.GUIUtil;
58 import net.sf.openrocket.util.Icons;
59 import net.sf.openrocket.util.Prefs;
60 import net.sf.openrocket.utils.MotorCorrelation;
61
62 import org.jfree.chart.ChartColor;
63 import org.jfree.chart.ChartFactory;
64 import org.jfree.chart.ChartPanel;
65 import org.jfree.chart.JFreeChart;
66 import org.jfree.chart.axis.ValueAxis;
67 import org.jfree.chart.plot.PlotOrientation;
68 import org.jfree.chart.plot.XYPlot;
69 import org.jfree.chart.title.TextTitle;
70 import org.jfree.data.xy.XYSeries;
71 import org.jfree.data.xy.XYSeriesCollection;
72
73 public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelector {
74         private static final LogHelper log = Application.getLogger();
75         
76         private static final double MOTOR_SIMILARITY_THRESHOLD = 0.95;
77         
78         private static final int SHOW_ALL = 0;
79         private static final int SHOW_SMALLER = 1;
80         private static final int SHOW_EXACT = 2;
81         private static final String[] SHOW_DESCRIPTIONS = {
82                         "Show all motors",
83                         "Show motors with diameter less than that of the motor mount",
84                         "Show motors with diameter equal to that of the motor mount"
85         };
86         private static final int SHOW_MAX = 2;
87         
88         private static final int ZOOM_ICON_POSITION_NEGATIVE_X = 50;
89         private static final int ZOOM_ICON_POSITION_POSITIVE_Y = 12;
90         
91         private static final Paint[] CURVE_COLORS = ChartColor.createDefaultPaintArray();
92         
93         private static final Color NO_COMMENT_COLOR = Color.GRAY;
94         private static final Color WITH_COMMENT_COLOR = Color.BLACK;
95         
96         private static final ThrustCurveMotorComparator MOTOR_COMPARATOR = new ThrustCurveMotorComparator();
97         
98
99
100         private final List<ThrustCurveMotorSet> database;
101         
102         private final double diameter;
103         private CloseableDialog dialog = null;
104         
105
106         private final ThrustCurveMotorDatabaseModel model;
107         private final JTable table;
108         private final TableRowSorter<TableModel> sorter;
109         
110         private final JCheckBox hideSimilarBox;
111         
112         private final JTextField searchField;
113         private String[] searchTerms = new String[0];
114         
115
116         private final JLabel curveSelectionLabel;
117         private final JComboBox curveSelectionBox;
118         private final DefaultComboBoxModel curveSelectionModel;
119         
120         private final JLabel totalImpulseLabel;
121         private final JLabel avgThrustLabel;
122         private final JLabel maxThrustLabel;
123         private final JLabel burnTimeLabel;
124         private final JLabel launchMassLabel;
125         private final JLabel emptyMassLabel;
126         private final JLabel dataPointsLabel;
127         private final JLabel digestLabel;
128         
129         private final JTextArea comment;
130         private final Font noCommentFont;
131         private final Font withCommentFont;
132         
133         private final JFreeChart chart;
134         private final ChartPanel chartPanel;
135         private final JLabel zoomIcon;
136         
137         private final JComboBox delayBox;
138         
139         private ThrustCurveMotor selectedMotor;
140         private ThrustCurveMotorSet selectedMotorSet;
141         private double selectedDelay;
142         
143         
144         /**
145          * Sole constructor.
146          * 
147          * @param current       the currently selected ThrustCurveMotor, or <code>null</code> for none.
148          * @param delay         the currently selected ejection charge delay.
149          * @param diameter      the diameter of the motor mount.
150          */
151         public ThrustCurveMotorSelectionPanel(ThrustCurveMotor current, double delay, double diameter) {
152                 super(new MigLayout("fill", "[grow][]"));
153                 
154                 this.diameter = diameter;
155                 
156
157                 // Construct the database (adding the current motor if not in the db already)
158                 List<ThrustCurveMotorSet> db;
159                 db = Application.getMotorSetDatabase().getMotorSets();
160                 
161                 // If current motor is not found in db, add a new ThrustCurveMotorSet containing it
162                 if (current != null) {
163                         selectedMotor = current;
164                         for (ThrustCurveMotorSet motorSet : db) {
165                                 if (motorSet.getMotors().contains(current)) {
166                                         selectedMotorSet = motorSet;
167                                         break;
168                                 }
169                         }
170                         if (selectedMotorSet == null) {
171                                 db = new ArrayList<ThrustCurveMotorSet>(db);
172                                 ThrustCurveMotorSet extra = new ThrustCurveMotorSet();
173                                 extra.addMotor(current);
174                                 selectedMotorSet = extra;
175                                 db.add(extra);
176                                 Collections.sort(db);
177                         }
178                 }
179                 database = db;
180                 
181
182
183                 ////  GUI
184                 
185                 JPanel panel;
186                 JLabel label;
187                 
188                 panel = new JPanel(new MigLayout("fill"));
189                 this.add(panel, "grow");
190                 
191
192
193                 // Selection label
194                 label = new StyledLabel("Select rocket motor:", Style.BOLD);
195                 panel.add(label, "spanx, wrap para");
196                 
197                 // Diameter selection
198                 JComboBox filterComboBox = new JComboBox(SHOW_DESCRIPTIONS);
199                 filterComboBox.addActionListener(new ActionListener() {
200                         @Override
201                         public void actionPerformed(ActionEvent e) {
202                                 JComboBox cb = (JComboBox) e.getSource();
203                                 int sel = cb.getSelectedIndex();
204                                 if ((sel < 0) || (sel > SHOW_MAX))
205                                         sel = SHOW_ALL;
206                                 switch (sel) {
207                                 case SHOW_ALL:
208                                         sorter.setRowFilter(new MotorRowFilterAll());
209                                         break;
210                                 
211                                 case SHOW_SMALLER:
212                                         sorter.setRowFilter(new MotorRowFilterSmaller());
213                                         break;
214                                 
215                                 case SHOW_EXACT:
216                                         sorter.setRowFilter(new MotorRowFilterExact());
217                                         break;
218                                 
219                                 default:
220                                         throw new BugException("Invalid selection mode sel=" + sel);
221                                 }
222                                 Prefs.putChoise("MotorDiameterMatch", sel);
223                                 scrollSelectionVisible();
224                         }
225                 });
226                 panel.add(filterComboBox, "spanx, growx, wrap rel");
227                 
228
229                 hideSimilarBox = new JCheckBox("Hide very similar thrust curves");
230                 GUIUtil.changeFontSize(hideSimilarBox, -1);
231                 hideSimilarBox.setSelected(Prefs.getBoolean(Prefs.MOTOR_HIDE_SIMILAR, true));
232                 hideSimilarBox.addActionListener(new ActionListener() {
233                         @Override
234                         public void actionPerformed(ActionEvent e) {
235                                 Prefs.putBoolean(Prefs.MOTOR_HIDE_SIMILAR, hideSimilarBox.isSelected());
236                                 updateData();
237                         }
238                 });
239                 panel.add(hideSimilarBox, "gapleft para, spanx, growx, wrap para");
240                 
241
242                 // Motor selection table
243                 model = new ThrustCurveMotorDatabaseModel(database);
244                 table = new JTable(model);
245                 
246
247                 // Set comparators and widths
248                 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
249                 sorter = new TableRowSorter<TableModel>(model);
250                 for (int i = 0; i < ThrustCurveMotorColumns.values().length; i++) {
251                         ThrustCurveMotorColumns column = ThrustCurveMotorColumns.values()[i];
252                         sorter.setComparator(i, column.getComparator());
253                         table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth());
254                 }
255                 table.setRowSorter(sorter);
256                 
257                 // Set selection and double-click listeners
258                 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
259                         @Override
260                         public void valueChanged(ListSelectionEvent e) {
261                                 int row = table.getSelectedRow();
262                                 if (row >= 0) {
263                                         row = table.convertRowIndexToModel(row);
264                                         ThrustCurveMotorSet motorSet = model.getMotorSet(row);
265                                         log.user("Selected table row " + row + ": " + motorSet);
266                                         if (motorSet != selectedMotorSet) {
267                                                 select(selectMotor(motorSet));
268                                         }
269                                 } else {
270                                         log.user("Selected table row " + row + ", nothing selected");
271                                 }
272                         }
273                 });
274                 table.addMouseListener(new MouseAdapter() {
275                         @Override
276                         public void mouseClicked(MouseEvent e) {
277                                 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
278                                         if (dialog != null) {
279                                                 dialog.close(true);
280                                         }
281                                 }
282                         }
283                 });
284                 
285
286                 JScrollPane scrollpane = new JScrollPane();
287                 scrollpane.setViewportView(table);
288                 panel.add(scrollpane, "grow, width :500:, height :300:, spanx, wrap para");
289                 
290
291
292
293                 // Motor mount diameter label
294                 label = new StyledLabel("Motor mount diameter: " +
295                                 UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter));
296                 panel.add(label, "gapright 30lp, spanx, split");
297                 
298
299
300                 // Search field
301                 label = new StyledLabel("Search:");
302                 panel.add(label, "");
303                 
304                 searchField = new JTextField();
305                 searchField.getDocument().addDocumentListener(new DocumentListener() {
306                         @Override
307                         public void changedUpdate(DocumentEvent e) {
308                                 update();
309                         }
310                         
311                         @Override
312                         public void insertUpdate(DocumentEvent e) {
313                                 update();
314                         }
315                         
316                         @Override
317                         public void removeUpdate(DocumentEvent e) {
318                                 update();
319                         }
320                         
321                         private void update() {
322                                 String text = searchField.getText().trim();
323                                 String[] split = text.split("\\s+");
324                                 ArrayList<String> list = new ArrayList<String>();
325                                 for (String s : split) {
326                                         s = s.trim().toLowerCase();
327                                         if (s.length() > 0) {
328                                                 list.add(s);
329                                         }
330                                 }
331                                 searchTerms = list.toArray(new String[0]);
332                                 sorter.sort();
333                                 scrollSelectionVisible();
334                         }
335                 });
336                 panel.add(searchField, "growx, wrap");
337                 
338
339
340                 // Vertical split
341                 this.add(panel, "grow");
342                 this.add(new JSeparator(JSeparator.VERTICAL), "growy, gap para para");
343                 panel = new JPanel(new MigLayout("fill"));
344                 
345
346
347                 // Thrust curve selection
348                 curveSelectionLabel = new JLabel("Select thrust curve:");
349                 panel.add(curveSelectionLabel);
350                 
351                 curveSelectionModel = new DefaultComboBoxModel();
352                 curveSelectionBox = new JComboBox(curveSelectionModel);
353                 curveSelectionBox.setRenderer(new CurveSelectionRenderer(curveSelectionBox.getRenderer()));
354                 curveSelectionBox.addActionListener(new ActionListener() {
355                         @Override
356                         public void actionPerformed(ActionEvent e) {
357                                 Object value = curveSelectionBox.getSelectedItem();
358                                 if (value != null) {
359                                         select(((MotorHolder) value).getMotor());
360                                 }
361                         }
362                 });
363                 panel.add(curveSelectionBox, "growx, wrap para");
364                 
365
366
367
368
369                 // Ejection charge delay
370                 panel.add(new JLabel("Ejection charge delay:"));
371                 
372                 delayBox = new JComboBox();
373                 delayBox.setEditable(true);
374                 delayBox.addActionListener(new ActionListener() {
375                         @Override
376                         public void actionPerformed(ActionEvent e) {
377                                 JComboBox cb = (JComboBox) e.getSource();
378                                 String sel = (String) cb.getSelectedItem();
379                                 if (sel.equalsIgnoreCase("None")) {
380                                         selectedDelay = Motor.PLUGGED;
381                                 } else {
382                                         try {
383                                                 selectedDelay = Double.parseDouble(sel);
384                                         } catch (NumberFormatException ignore) {
385                                         }
386                                 }
387                                 setDelays(false);
388                         }
389                 });
390                 panel.add(delayBox, "growx, wrap rel");
391                 panel.add(new StyledLabel("(Number of seconds or \"None\")", -3), "skip, wrap para");
392                 setDelays(false);
393                 
394
395                 panel.add(new JSeparator(), "spanx, growx, wrap para");
396                 
397
398
399                 // Thrust curve info
400                 panel.add(new JLabel("Total impulse:"));
401                 totalImpulseLabel = new JLabel();
402                 panel.add(totalImpulseLabel, "wrap");
403                 
404                 panel.add(new JLabel("Avg. thrust:"));
405                 avgThrustLabel = new JLabel();
406                 panel.add(avgThrustLabel, "wrap");
407                 
408                 panel.add(new JLabel("Max. thrust:"));
409                 maxThrustLabel = new JLabel();
410                 panel.add(maxThrustLabel, "wrap");
411                 
412                 panel.add(new JLabel("Burn time:"));
413                 burnTimeLabel = new JLabel();
414                 panel.add(burnTimeLabel, "wrap");
415                 
416                 panel.add(new JLabel("Launch mass:"));
417                 launchMassLabel = new JLabel();
418                 panel.add(launchMassLabel, "wrap");
419                 
420                 panel.add(new JLabel("Empty mass:"));
421                 emptyMassLabel = new JLabel();
422                 panel.add(emptyMassLabel, "wrap");
423                 
424                 panel.add(new JLabel("Data points:"));
425                 dataPointsLabel = new JLabel();
426                 panel.add(dataPointsLabel, "wrap para");
427                 
428                 if (System.getProperty("openrocket.debug.motordigest") != null) {
429                         panel.add(new JLabel("Digest:"));
430                         digestLabel = new JLabel();
431                         panel.add(digestLabel, "w :300:, wrap para");
432                 } else {
433                         digestLabel = null;
434                 }
435                 
436
437                 comment = new JTextArea(5, 5);
438                 GUIUtil.changeFontSize(comment, -2);
439                 withCommentFont = comment.getFont();
440                 noCommentFont = withCommentFont.deriveFont(Font.ITALIC);
441                 comment.setLineWrap(true);
442                 comment.setWrapStyleWord(true);
443                 comment.setEditable(false);
444                 scrollpane = new JScrollPane(comment);
445                 panel.add(scrollpane, "spanx, growx, wrap para");
446                 
447
448
449
450                 // Thrust curve plot
451                 chart = ChartFactory.createXYLineChart(
452                                 null, // title
453                                 null, // xAxisLabel
454                                 null, // yAxisLabel
455                                 null, // dataset
456                                 PlotOrientation.VERTICAL,
457                                 false, // legend
458                                 false, // tooltips
459                                 false // urls
460                                 );
461                 
462
463                 // Add the data and formatting to the plot
464                 XYPlot plot = chart.getXYPlot();
465                 
466                 changeLabelFont(plot.getRangeAxis(), -2);
467                 changeLabelFont(plot.getDomainAxis(), -2);
468                 
469                 chart.setTitle(new TextTitle("Thrust curve:", this.getFont()));
470                 chart.setBackgroundPaint(this.getBackground());
471                 plot.setBackgroundPaint(Color.WHITE);
472                 plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
473                 plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
474                 
475                 chartPanel = new ChartPanel(chart,
476                                 false, // properties
477                                 false, // save
478                                 false, // print
479                                 false, // zoom
480                                 false); // tooltips
481                 chartPanel.setMouseZoomable(false);
482                 chartPanel.setPopupMenu(null);
483                 chartPanel.setMouseWheelEnabled(false);
484                 chartPanel.setRangeZoomable(false);
485                 chartPanel.setDomainZoomable(false);
486                 
487                 chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
488                 chartPanel.addMouseListener(new MouseAdapter() {
489                         @Override
490                         public void mouseClicked(MouseEvent e) {
491                                 if (selectedMotor == null || selectedMotorSet == null)
492                                         return;
493                                 if (e.getButton() == MouseEvent.BUTTON1) {
494                                         // Open plot dialog
495                                         List<ThrustCurveMotor> motors = getFilteredCurves();
496                                         ThrustCurveMotorPlotDialog plotDialog = new ThrustCurveMotorPlotDialog(motors,
497                                                         motors.indexOf(selectedMotor),
498                                                         SwingUtilities.getWindowAncestor(ThrustCurveMotorSelectionPanel.this));
499                                         plotDialog.setVisible(true);
500                                 }
501                         }
502                 });
503                 
504                 JLayeredPane layer = new CustomLayeredPane();
505                 
506                 layer.setBorder(BorderFactory.createLineBorder(Color.BLUE));
507                 
508                 layer.add(chartPanel, (Integer) 0);
509                 
510                 zoomIcon = new JLabel(Icons.ZOOM_IN);
511                 zoomIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
512                 layer.add(zoomIcon, (Integer) 1);
513                 
514
515                 panel.add(layer, "width 300:300:, height 180:180:, grow, spanx");
516                 
517
518
519                 this.add(panel, "grow");
520                 
521
522
523                 // Sets the filter:
524                 int showMode = Prefs.getChoise(Prefs.MOTOR_DIAMETER_FILTER, SHOW_MAX, SHOW_EXACT);
525                 filterComboBox.setSelectedIndex(showMode);
526                 
527
528                 // Update the panel data
529                 updateData();
530                 selectedDelay = delay;
531                 setDelays(false);
532                 
533         }
534         
535         
536         @Override
537         public Motor getSelectedMotor() {
538                 return selectedMotor;
539         }
540         
541         
542         @Override
543         public double getSelectedDelay() {
544                 return selectedDelay;
545         }
546         
547         
548         @Override
549         public JComponent getDefaultFocus() {
550                 return searchField;
551         }
552         
553         @Override
554         public void selectedMotor(Motor motorSelection) {
555                 if (!(motorSelection instanceof ThrustCurveMotor)) {
556                         log.error("Received argument that was not ThrustCurveMotor: " + motorSelection);
557                         return;
558                 }
559                 
560                 ThrustCurveMotor motor = (ThrustCurveMotor) motorSelection;
561                 ThrustCurveMotorSet set = findMotorSet(motor);
562                 if (set == null) {
563                         log.error("Could not find set for motor:" + motorSelection);
564                         return;
565                 }
566                 
567                 // Store selected motor in preferences node, set all others to false
568                 Preferences prefs = Prefs.getNode(Prefs.PREFERRED_THRUST_CURVE_MOTOR_NODE);
569                 for (ThrustCurveMotor m : set.getMotors()) {
570                         String digest = MotorDigest.digestMotor(m);
571                         prefs.putBoolean(digest, m == motor);
572                 }
573         }
574         
575         public void setCloseableDialog(CloseableDialog dialog) {
576                 this.dialog = dialog;
577         }
578         
579         
580
581         private void changeLabelFont(ValueAxis axis, float size) {
582                 Font font = axis.getTickLabelFont();
583                 font = font.deriveFont(font.getSize2D() + size);
584                 axis.setTickLabelFont(font);
585         }
586         
587         
588         /**
589          * Called when a different motor is selected from within the panel.
590          */
591         private void select(ThrustCurveMotor motor) {
592                 if (selectedMotor == motor)
593                         return;
594                 
595                 ThrustCurveMotorSet set = findMotorSet(motor);
596                 if (set == null) {
597                         throw new BugException("Could not find motor from database, motor=" + motor);
598                 }
599                 
600                 boolean updateDelays = (selectedMotorSet != set);
601                 
602                 selectedMotor = motor;
603                 selectedMotorSet = set;
604                 updateData();
605                 if (updateDelays) {
606                         setDelays(true);
607                 }
608         }
609         
610         
611         private void updateData() {
612                 
613                 if (selectedMotorSet == null) {
614                         // No motor selected
615                         curveSelectionModel.removeAllElements();
616                         curveSelectionBox.setEnabled(false);
617                         curveSelectionLabel.setEnabled(false);
618                         totalImpulseLabel.setText("");
619                         avgThrustLabel.setText("");
620                         maxThrustLabel.setText("");
621                         burnTimeLabel.setText("");
622                         launchMassLabel.setText("");
623                         emptyMassLabel.setText("");
624                         dataPointsLabel.setText("");
625                         if (digestLabel != null) {
626                                 digestLabel.setText("");
627                         }
628                         setComment("");
629                         chart.getXYPlot().setDataset(new XYSeriesCollection());
630                         return;
631                 }
632                 
633
634                 // Check which thrust curves to display
635                 List<ThrustCurveMotor> motors = getFilteredCurves();
636                 final int index = motors.indexOf(selectedMotor);
637                 
638
639                 // Update the thrust curve selection box
640                 curveSelectionModel.removeAllElements();
641                 for (int i = 0; i < motors.size(); i++) {
642                         curveSelectionModel.addElement(new MotorHolder(motors.get(i), i));
643                 }
644                 curveSelectionBox.setSelectedIndex(index);
645                 
646                 if (motors.size() > 1) {
647                         curveSelectionBox.setEnabled(true);
648                         curveSelectionLabel.setEnabled(true);
649                 } else {
650                         curveSelectionBox.setEnabled(false);
651                         curveSelectionLabel.setEnabled(false);
652                 }
653                 
654
655                 // Update thrust curve data
656                 totalImpulseLabel.setText(UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(
657                                 selectedMotor.getTotalImpulseEstimate()));
658                 avgThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(
659                                 selectedMotor.getAverageThrustEstimate()));
660                 maxThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(
661                                 selectedMotor.getMaxThrustEstimate()));
662                 burnTimeLabel.setText(UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit(
663                                 selectedMotor.getBurnTimeEstimate()));
664                 launchMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(
665                                 selectedMotor.getLaunchCG().weight));
666                 emptyMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(
667                                 selectedMotor.getEmptyCG().weight));
668                 dataPointsLabel.setText("" + (selectedMotor.getTimePoints().length - 1));
669                 if (digestLabel != null) {
670                         digestLabel.setText(MotorDigest.digestMotor(selectedMotor));
671                 }
672                 
673                 setComment(selectedMotor.getDescription());
674                 
675
676                 // Update the plot
677                 XYPlot plot = chart.getXYPlot();
678                 
679                 XYSeriesCollection dataset = new XYSeriesCollection();
680                 for (int i = 0; i < motors.size(); i++) {
681                         ThrustCurveMotor m = motors.get(i);
682                         
683                         XYSeries series = new XYSeries("Thrust");
684                         double[] time = m.getTimePoints();
685                         double[] thrust = m.getThrustPoints();
686                         
687                         for (int j = 0; j < time.length; j++) {
688                                 series.add(time[j], thrust[j]);
689                         }
690                         
691                         dataset.addSeries(series);
692                         
693                         boolean selected = (i == index);
694                         plot.getRenderer().setSeriesStroke(i, new BasicStroke(selected ? 3 : 1));
695                         plot.getRenderer().setSeriesPaint(i, getColor(i));
696                 }
697                 
698                 plot.setDataset(dataset);
699         }
700         
701         
702         private List<ThrustCurveMotor> getFilteredCurves() {
703                 List<ThrustCurveMotor> motors = selectedMotorSet.getMotors();
704                 if (hideSimilarBox.isSelected()) {
705                         List<ThrustCurveMotor> filtered = new ArrayList<ThrustCurveMotor>(motors.size());
706                         for (int i = 0; i < motors.size(); i++) {
707                                 ThrustCurveMotor m = motors.get(i);
708                                 if (m.equals(selectedMotor)) {
709                                         filtered.add(m);
710                                         continue;
711                                 }
712                                 
713                                 double similarity = MotorCorrelation.similarity(selectedMotor, m);
714                                 log.debug("Motor similarity: " + similarity);
715                                 if (similarity < MOTOR_SIMILARITY_THRESHOLD) {
716                                         filtered.add(m);
717                                 }
718                         }
719                         motors = filtered;
720                 }
721                 
722                 Collections.sort(motors, MOTOR_COMPARATOR);
723                 
724                 return motors;
725         }
726         
727         
728         private void setComment(String s) {
729                 s = s.trim();
730                 if (s.length() == 0) {
731                         comment.setText("No description available.");
732                         comment.setFont(noCommentFont);
733                         comment.setForeground(NO_COMMENT_COLOR);
734                 } else {
735                         comment.setText(s);
736                         comment.setFont(withCommentFont);
737                         comment.setForeground(WITH_COMMENT_COLOR);
738                 }
739                 comment.setCaretPosition(0);
740         }
741         
742         private void scrollSelectionVisible() {
743                 if (selectedMotorSet != null) {
744                         int index = table.convertRowIndexToView(model.getIndex(selectedMotorSet));
745                         System.out.println("index=" + index);
746                         table.getSelectionModel().setSelectionInterval(index, index);
747                         Rectangle rect = table.getCellRect(index, 0, true);
748                         rect = new Rectangle(rect.x, rect.y - 100, rect.width, rect.height + 200);
749                         table.scrollRectToVisible(rect);
750                 }
751         }
752         
753         
754         public static Color getColor(int index) {
755                 return (Color) CURVE_COLORS[index % CURVE_COLORS.length];
756         }
757         
758         
759         /**
760          * Find the ThrustCurveMotorSet that contains a motor.
761          * 
762          * @param motor         the motor to look for.
763          * @return                      the ThrustCurveMotorSet, or null if not found.
764          */
765         private ThrustCurveMotorSet findMotorSet(ThrustCurveMotor motor) {
766                 for (ThrustCurveMotorSet set : database) {
767                         if (set.getMotors().contains(motor)) {
768                                 return set;
769                         }
770                 }
771                 
772                 return null;
773         }
774         
775         
776
777         /**
778          * Select the default motor from this ThrustCurveMotorSet.  This uses primarily motors
779          * that the user has previously used, and secondarily a heuristic method of selecting which
780          * thrust curve seems to be better or more reliable.
781          * 
782          * @param set   the motor set
783          * @return              the default motor in this set
784          */
785         private ThrustCurveMotor selectMotor(ThrustCurveMotorSet set) {
786                 if (set.getMotorCount() == 0) {
787                         throw new BugException("Attempting to select motor from empty ThrustCurveMotorSet: " + set);
788                 }
789                 if (set.getMotorCount() == 1) {
790                         return set.getMotors().get(0);
791                 }
792                 
793
794                 // Find which motor has been used the most recently
795                 List<ThrustCurveMotor> list = set.getMotors();
796                 Preferences prefs = Prefs.getNode(Prefs.PREFERRED_THRUST_CURVE_MOTOR_NODE);
797                 for (ThrustCurveMotor m : list) {
798                         String digest = MotorDigest.digestMotor(m);
799                         if (prefs.getBoolean(digest, false)) {
800                                 return m;
801                         }
802                 }
803                 
804                 // No motor has been used
805                 Collections.sort(list, MOTOR_COMPARATOR);
806                 return list.get(0);
807         }
808         
809         
810         /**
811          * Set the values in the delay combo box.  If <code>reset</code> is <code>true</code>
812          * then sets the selected value as the value closest to selectedDelay, otherwise
813          * leaves selection alone.
814          */
815         private void setDelays(boolean reset) {
816                 if (selectedMotor == null) {
817                         
818                         delayBox.setModel(new DefaultComboBoxModel(new String[] { "None" }));
819                         delayBox.setSelectedIndex(0);
820                         
821                 } else {
822                         
823                         List<Double> delays = selectedMotorSet.getDelays();
824                         String[] delayStrings = new String[delays.size()];
825                         double currentDelay = selectedDelay; // Store current setting locally
826                         
827                         for (int i = 0; i < delays.size(); i++) {
828                                 delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), "None");
829                         }
830                         delayBox.setModel(new DefaultComboBoxModel(delayStrings));
831                         
832                         if (reset) {
833                                 
834                                 // Find and set the closest value
835                                 double closest = Double.NaN;
836                                 for (int i = 0; i < delays.size(); i++) {
837                                         // if-condition to always become true for NaN
838                                         if (!(Math.abs(delays.get(i) - currentDelay) > Math.abs(closest - currentDelay))) {
839                                                 closest = delays.get(i);
840                                         }
841                                 }
842                                 if (!Double.isNaN(closest)) {
843                                         selectedDelay = closest;
844                                         delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(closest, "None"));
845                                 } else {
846                                         delayBox.setSelectedItem("None");
847                                 }
848                                 
849                         } else {
850                                 
851                                 selectedDelay = currentDelay;
852                                 delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, "None"));
853                                 
854                         }
855                         
856                 }
857         }
858         
859         
860
861
862         //////////////////////
863         
864
865         private class CurveSelectionRenderer implements ListCellRenderer {
866                 
867                 private final ListCellRenderer renderer;
868                 
869                 public CurveSelectionRenderer(ListCellRenderer renderer) {
870                         this.renderer = renderer;
871                 }
872                 
873                 @Override
874                 public Component getListCellRendererComponent(JList list, Object value, int index,
875                                 boolean isSelected, boolean cellHasFocus) {
876                         
877                         Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
878                         if (value instanceof MotorHolder) {
879                                 MotorHolder m = (MotorHolder) value;
880                                 c.setForeground(getColor(m.getIndex()));
881                         }
882                         
883                         return c;
884                 }
885                 
886         }
887         
888         
889         ////////  Row filters
890         
891         /**
892          * Abstract adapter class.
893          */
894         private abstract class MotorRowFilter extends RowFilter<TableModel, Integer> {
895                 @Override
896                 public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
897                         int index = entry.getIdentifier();
898                         ThrustCurveMotorSet m = model.getMotorSet(index);
899                         return filterByDiameter(m) && filterByString(m);
900                 }
901                 
902                 public abstract boolean filterByDiameter(ThrustCurveMotorSet m);
903                 
904                 
905                 public boolean filterByString(ThrustCurveMotorSet m) {
906                         main: for (String s : searchTerms) {
907                                 for (ThrustCurveMotorColumns col : ThrustCurveMotorColumns.values()) {
908                                         String str = col.getValue(m).toString().toLowerCase();
909                                         if (str.indexOf(s) >= 0)
910                                                 continue main;
911                                 }
912                                 return false;
913                         }
914                         return true;
915                 }
916         }
917         
918         /**
919          * Show all motors.
920          */
921         private class MotorRowFilterAll extends MotorRowFilter {
922                 @Override
923                 public boolean filterByDiameter(ThrustCurveMotorSet m) {
924                         return true;
925                 }
926         }
927         
928         /**
929          * Show motors smaller than the mount.
930          */
931         private class MotorRowFilterSmaller extends MotorRowFilter {
932                 @Override
933                 public boolean filterByDiameter(ThrustCurveMotorSet m) {
934                         return (m.getDiameter() <= diameter + 0.0004);
935                 }
936         }
937         
938         /**
939          * Show motors that fit the mount.
940          */
941         private class MotorRowFilterExact extends MotorRowFilter {
942                 @Override
943                 public boolean filterByDiameter(ThrustCurveMotorSet m) {
944                         return ((m.getDiameter() <= diameter + 0.0004) && (m.getDiameter() >= diameter - 0.0015));
945                 }
946         }
947         
948         
949         /**
950          * Custom layered pane that sets the bounds of the components on every layout.
951          */
952         public class CustomLayeredPane extends JLayeredPane {
953                 @Override
954                 public void doLayout() {
955                         synchronized (getTreeLock()) {
956                                 int w = getWidth();
957                                 int h = getHeight();
958                                 chartPanel.setBounds(0, 0, w, h);
959                                 zoomIcon.setBounds(w - ZOOM_ICON_POSITION_NEGATIVE_X, ZOOM_ICON_POSITION_POSITIVE_Y, 50, 50);
960                         }
961                 }
962         }
963 }