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