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