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