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