updates for 0.9.3
[debian/openrocket] / src / net / sf / openrocket / gui / dialogs / MotorChooserDialog.java
1 package net.sf.openrocket.gui.dialogs;
2
3
4 import java.awt.Dialog;
5 import java.awt.Font;
6 import java.awt.Rectangle;
7 import java.awt.Window;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.MouseAdapter;
11 import java.awt.event.MouseEvent;
12 import java.text.Collator;
13 import java.util.ArrayList;
14 import java.util.Comparator;
15 import java.util.Locale;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
18
19 import javax.swing.DefaultComboBoxModel;
20 import javax.swing.JButton;
21 import javax.swing.JComboBox;
22 import javax.swing.JDialog;
23 import javax.swing.JLabel;
24 import javax.swing.JPanel;
25 import javax.swing.JScrollPane;
26 import javax.swing.JTable;
27 import javax.swing.JTextField;
28 import javax.swing.ListSelectionModel;
29 import javax.swing.RowFilter;
30 import javax.swing.event.DocumentEvent;
31 import javax.swing.event.DocumentListener;
32 import javax.swing.event.ListSelectionEvent;
33 import javax.swing.event.ListSelectionListener;
34 import javax.swing.table.AbstractTableModel;
35 import javax.swing.table.TableModel;
36 import javax.swing.table.TableRowSorter;
37
38 import net.miginfocom.swing.MigLayout;
39 import net.sf.openrocket.database.Databases;
40 import net.sf.openrocket.gui.components.ResizeLabel;
41 import net.sf.openrocket.rocketcomponent.Motor;
42 import net.sf.openrocket.unit.UnitGroup;
43 import net.sf.openrocket.util.GUIUtil;
44 import net.sf.openrocket.util.Prefs;
45
46 public class MotorChooserDialog extends JDialog {
47         
48         private static final int SHOW_ALL = 0;
49         private static final int SHOW_SMALLER = 1;
50         private static final int SHOW_EXACT = 2;
51         private static final String[] SHOW_DESCRIPTIONS = {
52                 "Show all motors",
53                 "Show motors with diameter less than that of the motor mount",
54                 "Show motors with diameter equal to that of the motor mount"
55         };
56         private static final int SHOW_MAX = 2;
57
58         private final JTextField searchField; 
59         private String[] searchTerms = new String[0];
60
61         private final double diameter;
62
63         private Motor selectedMotor = null;
64         private double selectedDelay = 0;
65
66         private JTable table;
67         private TableRowSorter<TableModel> sorter;
68         private JComboBox delayBox;
69         private MotorDatabaseModel model;
70         
71         private boolean okClicked = false;
72
73         
74         public MotorChooserDialog(double diameter) {
75                 this(null,5,diameter,null);
76         }
77         
78         public MotorChooserDialog(Motor current, double delay, double diameter) {
79                 this(current,delay,diameter,null);
80         }
81         
82         public MotorChooserDialog(Motor current, double delay, double diameter, Window owner) {
83                 super(owner, "Select a rocket motor", Dialog.ModalityType.APPLICATION_MODAL);
84                 
85                 JButton button;
86
87                 this.selectedMotor = current;
88                 this.selectedDelay = delay;
89                 this.diameter = diameter;
90                 
91                 JPanel panel = new JPanel(new MigLayout("fill", "[grow][]"));
92
93                 // Label
94                 JLabel label = new JLabel("Select a rocket motor:");
95                 label.setFont(label.getFont().deriveFont(Font.BOLD));
96                 panel.add(label,"growx");
97                 
98                 label = new JLabel("Motor mount diameter: " +
99                                 UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter));
100                 panel.add(label,"gapleft para, wrap paragraph");
101                 
102                 
103                 // Diameter selection
104                 JComboBox combo = new JComboBox(SHOW_DESCRIPTIONS);
105                 combo.addActionListener(new ActionListener() {
106                         @Override
107                         public void actionPerformed(ActionEvent e) {
108                                 JComboBox cb = (JComboBox) e.getSource();
109                                 int sel = cb.getSelectedIndex();
110                                 if ((sel < 0) || (sel > SHOW_MAX))
111                                         sel = SHOW_ALL;
112                                 switch (sel) {
113                                 case SHOW_ALL:
114                                         sorter.setRowFilter(new MotorRowFilterAll());
115                                         break;
116                                         
117                                 case SHOW_SMALLER:
118                                         sorter.setRowFilter(new MotorRowFilterSmaller());
119                                         break;
120                                         
121                                 case SHOW_EXACT:
122                                         sorter.setRowFilter(new MotorRowFilterExact());
123                                         break;
124                                         
125                                 default:
126                                         assert(false) : "Should not occur.";    
127                                 }
128                                 Prefs.putChoise("MotorDiameterMatch", sel);
129                                 setSelectionVisible();
130                         }
131                 });
132                 panel.add(combo,"growx 1000");
133
134                 
135                 
136                 label = new JLabel("Search:");
137                 panel.add(label, "gapleft para, split 2");
138                 
139                 searchField = new JTextField();
140                 searchField.getDocument().addDocumentListener(new DocumentListener() {
141                         @Override
142                         public void changedUpdate(DocumentEvent e) {
143                                 update();
144                         }
145                         @Override
146                         public void insertUpdate(DocumentEvent e) {
147                                 update();
148                         }
149                         @Override
150                         public void removeUpdate(DocumentEvent e) {
151                                 update();
152                         }
153                         
154                         private void update() {
155                                 String text = searchField.getText().trim();
156                                 String[] split = text.split("\\s+");
157                                 ArrayList<String> list = new ArrayList<String>();
158                                 for (String s: split) {
159                                         s = s.trim().toLowerCase();
160                                         if (s.length() > 0) {
161                                                 list.add(s);
162                                         }
163                                 }
164                                 searchTerms = list.toArray(new String[0]);
165                                 sorter.sort();
166                         }
167                 });
168                 panel.add(searchField, "growx 1, wrap");
169                 
170                 
171                 
172                 // Table, overridden to show meaningful tooltip texts
173                 model = new MotorDatabaseModel(current);
174                 table = new JTable(model) {
175                         @Override
176                         public String getToolTipText(MouseEvent e) {
177                         java.awt.Point p = e.getPoint();
178                         int colIndex = columnAtPoint(p);
179                         int viewRow = rowAtPoint(p);
180                         if (viewRow < 0)
181                                 return null;
182                         int rowIndex = convertRowIndexToModel(viewRow);
183                         Motor motor = model.getMotor(rowIndex);
184
185                         if (colIndex < 0 || colIndex >= MotorColumns.values().length)
186                                 return null;
187
188                         return MotorColumns.values()[colIndex].getToolTipText(motor);
189                         }
190                 };
191                 
192                 // Set comparators and widths
193                 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
194                 sorter = new TableRowSorter<TableModel>(model);
195                 for (int i=0; i < MotorColumns.values().length; i++) {
196                         MotorColumns column = MotorColumns.values()[i];
197                         sorter.setComparator(i, column.getComparator());
198                         table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth());
199                 }
200                 table.setRowSorter(sorter);
201
202                 // Set selection and double-click listeners
203                 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
204                         @Override
205                         public void valueChanged(ListSelectionEvent e) {
206                                 int row = table.getSelectedRow();
207                                 if (row >= 0) {
208                                         row = table.convertRowIndexToModel(row);
209                                         Motor m = model.getMotor(row);
210                                         if (!m.equals(selectedMotor)) {
211                                                 selectedMotor = model.getMotor(row);
212                                                 setDelays(true);  // Reset delay times
213                                         }
214                                 }
215                         }
216                 });
217                 table.addMouseListener(new MouseAdapter() {
218                         @Override
219                         public void mouseClicked(MouseEvent e) {
220                                 if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
221                                         okClicked = true;
222                                         MotorChooserDialog.this.setVisible(false);
223                                 }
224                         }
225                 });
226                 // (Current selection and scrolling performed later)
227                 
228                 JScrollPane scrollpane = new JScrollPane();
229                 scrollpane.setViewportView(table);
230                 panel.add(scrollpane,"spanx, grow, width :700:, height :300:, wrap paragraph");
231                 
232                 
233                 // Ejection delay
234                 panel.add(new JLabel("Select ejection charge delay:"), "spanx, split 3, gap rel");
235                 
236                 delayBox = new JComboBox();
237                 delayBox.setEditable(true);
238                 delayBox.addActionListener(new ActionListener() {
239                         @Override
240                         public void actionPerformed(ActionEvent e) {
241                                 JComboBox cb = (JComboBox) e.getSource();
242                                 String sel = (String)cb.getSelectedItem();
243                                 if (sel.equalsIgnoreCase("None")) {
244                                         selectedDelay = Motor.PLUGGED;
245                                 } else {
246                                         try {
247                                                 selectedDelay = Double.parseDouble(sel);
248                                         } catch (NumberFormatException ignore) { }
249                                 }
250                                 setDelays(false);
251                         }
252                 });
253                 panel.add(delayBox,"gapright unrel");
254                 panel.add(new ResizeLabel("(Number of seconds or \"None\")", -1), "wrap para");
255                 setDelays(false);
256                 
257                 
258                 JButton okButton = new JButton("OK");
259                 okButton.addActionListener(new ActionListener() {
260                         @Override
261                         public void actionPerformed(ActionEvent e) {
262                                 okClicked = true;
263                                 MotorChooserDialog.this.setVisible(false);
264                         }
265                 });
266                 panel.add(okButton,"spanx, split, tag ok");
267
268                 button = new JButton("Cancel");
269                 button.addActionListener(new ActionListener() {
270                         @Override
271                         public void actionPerformed(ActionEvent e) {
272                                 MotorChooserDialog.this.setVisible(false);
273                         }
274                 });
275                 panel.add(button,"tag cancel");
276
277                                 
278                 // Sets the filter:
279                 int showMode = Prefs.getChoise("MotorDiameterMatch", SHOW_MAX, SHOW_EXACT);
280                 combo.setSelectedIndex(showMode);
281                 
282                 
283                 this.add(panel);
284                 this.pack();
285 //              this.setAlwaysOnTop(true);
286
287                 GUIUtil.setDefaultButton(okButton);
288                 GUIUtil.installEscapeCloseOperation(this);
289                 this.setLocationByPlatform(true);
290                 
291                 // Table can be scrolled only after pack() has been called
292                 setSelectionVisible();
293                 
294                 // Focus the search field
295                 searchField.grabFocus();
296         }
297         
298         private void setSelectionVisible() {
299                 if (selectedMotor != null) {
300                         int index = table.convertRowIndexToView(model.getIndex(selectedMotor));
301                         table.getSelectionModel().setSelectionInterval(index, index);
302                         Rectangle rect = table.getCellRect(index, 0, true);
303                         rect = new Rectangle(rect.x,rect.y-100,rect.width,rect.height+200);
304                         table.scrollRectToVisible(rect);
305                 }
306         }
307         
308         
309         /**
310          * Set the values in the delay combo box.  If <code>reset</code> is <code>true</code>
311          * then sets the selected value as the value closest to selectedDelay, otherwise
312          * leaves selection alone.
313          */
314         private void setDelays(boolean reset) {
315                 if (selectedMotor == null) {
316                         
317                         delayBox.setModel(new DefaultComboBoxModel(new String[] { "None" }));
318                         delayBox.setSelectedIndex(0);
319                         
320                 } else {
321                         
322                         double[] delays = selectedMotor.getStandardDelays();
323                         String[] delayStrings = new String[delays.length];
324                         double currentDelay = selectedDelay;  // Store current setting locally
325                         
326                         for (int i=0; i < delays.length; i++) {
327                                 delayStrings[i] = Motor.getDelayString(delays[i], "None");
328                         }
329                         delayBox.setModel(new DefaultComboBoxModel(delayStrings));
330                         
331                         if (reset) {
332
333                                 // Find and set the closest value
334                                 double closest = Double.NaN;
335                                 for (int i=0; i < delays.length; i++) {
336                                         // if-condition to always become true for NaN
337                                         if (!(Math.abs(delays[i] - currentDelay) > 
338                                                   Math.abs(closest - currentDelay))) {
339                                                 closest = delays[i];
340                                         }
341                                 }
342                                 if (!Double.isNaN(closest)) {
343                                         selectedDelay = closest;
344                                         delayBox.setSelectedItem(Motor.getDelayString(closest, "None"));
345                                 } else {
346                                         delayBox.setSelectedItem("None");
347                                 }
348
349                         } else {
350                                 
351                                 selectedDelay = currentDelay;
352                                 delayBox.setSelectedItem(Motor.getDelayString(currentDelay, "None"));
353                                 
354                         }
355                         
356                 }
357         }
358
359         
360         
361         public Motor getSelectedMotor() {
362                 if (!okClicked)
363                         return null;
364                 return selectedMotor;
365         }
366         
367         
368         public double getSelectedDelay() {
369                 return selectedDelay;
370         }
371         
372
373         
374         
375         ////////////////  JTable elements  ////////////////
376         
377         
378         /**
379          * Enum defining the table columns.
380          */
381         private enum MotorColumns {
382                 MANUFACTURER("Manufacturer",100) {
383                         @Override
384                         public String getValue(Motor m) {
385                                 return m.getManufacturer();
386                         }
387 //                      @Override
388 //                      public String getToolTipText(Motor m) {
389 //                              return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
390 //                      }
391                         @Override
392                         public Comparator<?> getComparator() {
393                                 return Collator.getInstance();
394                         }
395                 },
396                 DESIGNATION("Designation") {
397                         @Override
398                         public String getValue(Motor m) {
399                                 return m.getDesignation();
400                         }
401 //                      @Override
402 //                      public String getToolTipText(Motor m) {
403 //                              return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
404 //                      }
405                         @Override
406                         public Comparator<?> getComparator() {
407                                 return Motor.getDesignationComparator();
408                         }
409                 },
410                 TYPE("Type") {
411                         @Override
412                         public String getValue(Motor m) {
413                                 return m.getMotorType().getName();
414                         }
415 //                      @Override
416 //                      public String getToolTipText(Motor m) {
417 //                              return m.getMotorType().getDescription();
418 //                      }
419                         @Override
420                         public Comparator<?> getComparator() {
421                                 return Collator.getInstance();
422                         }
423                 },
424                 DIAMETER("Diameter") {
425                         @Override
426                         public String getValue(Motor m) {
427                                 return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
428                                                 m.getDiameter());
429                         }
430                         @Override
431                         public Comparator<?> getComparator() {
432                                 return getNumericalComparator();
433                         }
434                 },
435                 LENGTH("Length") {
436                         @Override
437                         public String getValue(Motor m) {
438                                 return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
439                                                 m.getLength());
440                         }
441                         @Override
442                         public Comparator<?> getComparator() {
443                                 return getNumericalComparator();
444                         }
445                 },
446                 IMPULSE("Impulse") {
447                         @Override
448                         public String getValue(Motor m) {
449                                 return UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(
450                                                 m.getTotalImpulse());
451                         }
452                         @Override
453                         public Comparator<?> getComparator() {
454                                 return getNumericalComparator();
455                         }
456                 },
457                 TIME("Burn time") {
458                         @Override
459                         public String getValue(Motor m) {
460                                 return UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit(
461                                                 m.getAverageTime());
462                         }
463                         @Override
464                         public Comparator<?> getComparator() {
465                                 return getNumericalComparator();
466                         }
467                 };
468                 
469                 
470                 private final String title;
471                 private final int width;
472                 
473                 MotorColumns(String title) {
474                         this(title, 50);
475                 }
476                 
477                 MotorColumns(String title, int width) {
478                         this.title = title;
479                         this.width = width;
480                 }
481                 
482                 
483                 public abstract String getValue(Motor m);
484                 public abstract Comparator<?> getComparator();
485
486                 public String getTitle() {
487                         return title;
488                 }
489                 
490                 public int getWidth() {
491                         return width;
492                 }
493                 
494                 public String getToolTipText(Motor m) {
495                         String tip = "<html>";
496                         tip += "<b>" + m.toString() + "</b>";
497                         tip += " (" + m.getMotorType().getDescription() + ")<br><hr>";
498                         
499                         String desc = m.getDescription().trim();
500                         if (desc.length() > 0) {
501                                 tip += "<i>" + desc.replace("\n", "<br>") + "</i><br><hr>";
502                         }
503                         
504                         tip += ("Diameter: " + 
505                                         UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getDiameter()) +
506                                         "<br>");
507                         tip += ("Length: " + 
508                                         UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getLength()) +
509                                         "<br>");
510                         tip += ("Maximum thrust: " + 
511                                         UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getMaxThrust()) +
512                                         "<br>");
513                         tip += ("Average thrust: " + 
514                                         UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getAverageThrust()) +
515                                         "<br>");
516                         tip += ("Burn time: " + 
517                                         UnitGroup.UNITS_SHORT_TIME.getDefaultUnit()
518                                         .toStringUnit(m.getAverageTime()) + "<br>");
519                         tip += ("Total impulse: " +
520                                         UnitGroup.UNITS_IMPULSE.getDefaultUnit()
521                                         .toStringUnit(m.getTotalImpulse()) + "<br>");
522                         tip += ("Launch mass: " + 
523                                         UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(m.getMass(0)) +
524                                         "<br>");
525                         tip += ("Empty mass: " + 
526                                         UnitGroup.UNITS_MASS.getDefaultUnit()
527                                         .toStringUnit(m.getMass(Double.MAX_VALUE)));
528                         return tip;
529                 }
530                 
531         }
532         
533         
534         /**
535          * The JTable model.  Includes an extra motor, given in the constructor,
536          * if it is not already in the database.
537          */
538         private class MotorDatabaseModel extends AbstractTableModel {
539                 private final Motor extra;
540                 
541                 public MotorDatabaseModel(Motor current) {
542                         if (Databases.MOTOR.contains(current))
543                                 extra = null;
544                         else
545                                 extra = current;
546                 }
547                 
548                 @Override
549                 public int getColumnCount() {
550                         return MotorColumns.values().length;
551                 }
552
553                 @Override
554                 public int getRowCount() {
555                         if (extra == null)
556                                 return Databases.MOTOR.size();
557                         else
558                                 return Databases.MOTOR.size()+1;
559                 }
560
561                 @Override
562                 public Object getValueAt(int rowIndex, int columnIndex) {
563                         MotorColumns column = getColumn(columnIndex);
564                         if (extra == null) {
565                                 return column.getValue(Databases.MOTOR.get(rowIndex));
566                         } else {
567                                 if (rowIndex == 0)
568                                         return column.getValue(extra);
569                                 else
570                                         return column.getValue(Databases.MOTOR.get(rowIndex - 1));
571                         }
572                 }
573                 
574                 @Override
575                 public String getColumnName(int columnIndex) {
576                         return getColumn(columnIndex).getTitle();
577                 }
578                 
579                 
580                 public Motor getMotor(int rowIndex) {
581                         if (extra == null) {
582                                 return Databases.MOTOR.get(rowIndex);
583                         } else {
584                                 if (rowIndex == 0)
585                                         return extra;
586                                 else
587                                         return Databases.MOTOR.get(rowIndex-1);
588                         }
589                 }
590                 
591                 public int getIndex(Motor m) {
592                         if (extra == null) {
593                                 return Databases.MOTOR.indexOf(m);
594                         } else {
595                                 if (extra.equals(m))
596                                         return 0;
597                                 else
598                                         return Databases.MOTOR.indexOf(m)+1;
599                         }
600                 }
601                 
602                 private MotorColumns getColumn(int index) {
603                         return MotorColumns.values()[index];
604                 }
605         }
606
607         
608         ////////  Row filters
609         
610         /**
611          * Abstract adapter class.
612          */
613         private abstract class MotorRowFilter extends RowFilter<TableModel,Integer> {
614                 @Override
615                 public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
616                         int index = entry.getIdentifier();
617                         Motor m = model.getMotor(index);
618                         return filterByDiameter(m) && filterByString(m);
619                 }
620                 
621                 public abstract boolean filterByDiameter(Motor m);
622                 
623                 
624                 public boolean filterByString(Motor m) {
625                         main: for (String s : searchTerms) {
626                                 for (MotorColumns col : MotorColumns.values()) {
627                                         String str = col.getValue(m).toLowerCase();
628                                         if (str.indexOf(s) >= 0)
629                                                 continue main;
630                                 }
631                                 return false;
632                         }
633                         return true;
634                 }
635         }
636         
637         /**
638          * Show all motors.
639          */
640         private class MotorRowFilterAll extends MotorRowFilter {
641                 @Override
642                 public boolean filterByDiameter(Motor m) {
643                         return true;
644                 }
645         }
646         
647         /**
648          * Show motors smaller than the mount.
649          */
650         private class MotorRowFilterSmaller extends MotorRowFilter {
651                 @Override
652                 public boolean filterByDiameter(Motor m) {
653                         return (m.getDiameter() <= diameter + 0.0004);
654                 }
655         }
656         
657         /**
658          * Show motors that fit the mount.
659          */
660         private class MotorRowFilterExact extends MotorRowFilter {
661                 @Override
662                 public boolean filterByDiameter(Motor m) {
663                         return ((m.getDiameter() <= diameter + 0.0004) &&
664                                         (m.getDiameter() >= diameter - 0.0015));
665                 }
666         }
667         
668         
669         private static Comparator<String> numericalComparator = null;
670         private static Comparator<String> getNumericalComparator() {
671                 if (numericalComparator == null)
672                         numericalComparator = new NumericalComparator();
673                 return numericalComparator;
674         }
675         
676         private static class NumericalComparator implements Comparator<String> {
677                 private Pattern pattern = 
678                         Pattern.compile("^\\s*([0-9]*[.,][0-9]+|[0-9]+[.,]?[0-9]*).*?$");
679                 private Collator collator = null;
680                 @Override
681                 public int compare(String s1, String s2) {
682                         Matcher m1, m2;
683                         
684                         m1 = pattern.matcher(s1);
685                         m2 = pattern.matcher(s2);
686                         if (m1.find() && m2.find()) {
687                                 double d1 = Double.parseDouble(m1.group(1));
688                                 double d2 = Double.parseDouble(m2.group(1));
689                                 
690                                 return (int)((d1-d2)*1000);
691                         }
692                         
693                         if (collator == null)
694                                 collator = Collator.getInstance(Locale.US);
695                         return collator.compare(s1, s2);
696                 }
697         }
698         
699 }