updates for 0.9.4
[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.StyledLabel;
41 import net.sf.openrocket.motor.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 StyledLabel("(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                 this.setLocationByPlatform(true);
288                 GUIUtil.setDisposableDialogOptions(this, okButton);
289                 
290                 // Table can be scrolled only after pack() has been called
291                 setSelectionVisible();
292                 
293                 // Focus the search field
294                 searchField.grabFocus();
295         }
296         
297         private void setSelectionVisible() {
298                 if (selectedMotor != null) {
299                         int index = table.convertRowIndexToView(model.getIndex(selectedMotor));
300                         table.getSelectionModel().setSelectionInterval(index, index);
301                         Rectangle rect = table.getCellRect(index, 0, true);
302                         rect = new Rectangle(rect.x,rect.y-100,rect.width,rect.height+200);
303                         table.scrollRectToVisible(rect);
304                 }
305         }
306         
307         
308         /**
309          * Set the values in the delay combo box.  If <code>reset</code> is <code>true</code>
310          * then sets the selected value as the value closest to selectedDelay, otherwise
311          * leaves selection alone.
312          */
313         private void setDelays(boolean reset) {
314                 if (selectedMotor == null) {
315                         
316                         delayBox.setModel(new DefaultComboBoxModel(new String[] { "None" }));
317                         delayBox.setSelectedIndex(0);
318                         
319                 } else {
320                         
321                         double[] delays = selectedMotor.getStandardDelays();
322                         String[] delayStrings = new String[delays.length];
323                         double currentDelay = selectedDelay;  // Store current setting locally
324                         
325                         for (int i=0; i < delays.length; i++) {
326                                 delayStrings[i] = Motor.getDelayString(delays[i], "None");
327                         }
328                         delayBox.setModel(new DefaultComboBoxModel(delayStrings));
329                         
330                         if (reset) {
331
332                                 // Find and set the closest value
333                                 double closest = Double.NaN;
334                                 for (int i=0; i < delays.length; i++) {
335                                         // if-condition to always become true for NaN
336                                         if (!(Math.abs(delays[i] - currentDelay) > 
337                                                   Math.abs(closest - currentDelay))) {
338                                                 closest = delays[i];
339                                         }
340                                 }
341                                 if (!Double.isNaN(closest)) {
342                                         selectedDelay = closest;
343                                         delayBox.setSelectedItem(Motor.getDelayString(closest, "None"));
344                                 } else {
345                                         delayBox.setSelectedItem("None");
346                                 }
347
348                         } else {
349                                 
350                                 selectedDelay = currentDelay;
351                                 delayBox.setSelectedItem(Motor.getDelayString(currentDelay, "None"));
352                                 
353                         }
354                         
355                 }
356         }
357
358         
359         
360         public Motor getSelectedMotor() {
361                 if (!okClicked)
362                         return null;
363                 return selectedMotor;
364         }
365         
366         
367         public double getSelectedDelay() {
368                 return selectedDelay;
369         }
370         
371
372         
373         
374         ////////////////  JTable elements  ////////////////
375         
376         
377         /**
378          * Enum defining the table columns.
379          */
380         private enum MotorColumns {
381                 MANUFACTURER("Manufacturer",100) {
382                         @Override
383                         public String getValue(Motor m) {
384                                 return m.getManufacturer().getDisplayName();
385                         }
386 //                      @Override
387 //                      public String getToolTipText(Motor m) {
388 //                              return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
389 //                      }
390                         @Override
391                         public Comparator<?> getComparator() {
392                                 return Collator.getInstance();
393                         }
394                 },
395                 DESIGNATION("Designation") {
396                         @Override
397                         public String getValue(Motor m) {
398                                 return m.getDesignation();
399                         }
400 //                      @Override
401 //                      public String getToolTipText(Motor m) {
402 //                              return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
403 //                      }
404                         @Override
405                         public Comparator<?> getComparator() {
406                                 return Motor.getDesignationComparator();
407                         }
408                 },
409                 TYPE("Type") {
410                         @Override
411                         public String getValue(Motor m) {
412                                 return m.getMotorType().getName();
413                         }
414 //                      @Override
415 //                      public String getToolTipText(Motor m) {
416 //                              return m.getMotorType().getDescription();
417 //                      }
418                         @Override
419                         public Comparator<?> getComparator() {
420                                 return Collator.getInstance();
421                         }
422                 },
423                 DIAMETER("Diameter") {
424                         @Override
425                         public String getValue(Motor m) {
426                                 return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
427                                                 m.getDiameter());
428                         }
429                         @Override
430                         public Comparator<?> getComparator() {
431                                 return getNumericalComparator();
432                         }
433                 },
434                 LENGTH("Length") {
435                         @Override
436                         public String getValue(Motor m) {
437                                 return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
438                                                 m.getLength());
439                         }
440                         @Override
441                         public Comparator<?> getComparator() {
442                                 return getNumericalComparator();
443                         }
444                 },
445                 IMPULSE("Impulse") {
446                         @Override
447                         public String getValue(Motor m) {
448                                 return UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(
449                                                 m.getTotalImpulse());
450                         }
451                         @Override
452                         public Comparator<?> getComparator() {
453                                 return getNumericalComparator();
454                         }
455                 },
456                 TIME("Burn time") {
457                         @Override
458                         public String getValue(Motor m) {
459                                 return UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit(
460                                                 m.getAverageTime());
461                         }
462                         @Override
463                         public Comparator<?> getComparator() {
464                                 return getNumericalComparator();
465                         }
466                 };
467                 
468                 
469                 private final String title;
470                 private final int width;
471                 
472                 MotorColumns(String title) {
473                         this(title, 50);
474                 }
475                 
476                 MotorColumns(String title, int width) {
477                         this.title = title;
478                         this.width = width;
479                 }
480                 
481                 
482                 public abstract String getValue(Motor m);
483                 public abstract Comparator<?> getComparator();
484
485                 public String getTitle() {
486                         return title;
487                 }
488                 
489                 public int getWidth() {
490                         return width;
491                 }
492                 
493                 public String getToolTipText(Motor m) {
494                         String tip = "<html>";
495                         tip += "<b>" + m.toString() + "</b>";
496                         tip += " (" + m.getMotorType().getDescription() + ")<br><hr>";
497                         
498                         String desc = m.getDescription().trim();
499                         if (desc.length() > 0) {
500                                 tip += "<i>" + desc.replace("\n", "<br>") + "</i><br><hr>";
501                         }
502                         
503                         tip += ("Diameter: " + 
504                                         UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getDiameter()) +
505                                         "<br>");
506                         tip += ("Length: " + 
507                                         UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getLength()) +
508                                         "<br>");
509                         tip += ("Maximum thrust: " + 
510                                         UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getMaxThrust()) +
511                                         "<br>");
512                         tip += ("Average thrust: " + 
513                                         UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getAverageThrust()) +
514                                         "<br>");
515                         tip += ("Burn time: " + 
516                                         UnitGroup.UNITS_SHORT_TIME.getDefaultUnit()
517                                         .toStringUnit(m.getAverageTime()) + "<br>");
518                         tip += ("Total impulse: " +
519                                         UnitGroup.UNITS_IMPULSE.getDefaultUnit()
520                                         .toStringUnit(m.getTotalImpulse()) + "<br>");
521                         tip += ("Launch mass: " + 
522                                         UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(m.getMass(0)) +
523                                         "<br>");
524                         tip += ("Empty mass: " + 
525                                         UnitGroup.UNITS_MASS.getDefaultUnit()
526                                         .toStringUnit(m.getMass(Double.MAX_VALUE)));
527                         return tip;
528                 }
529                 
530         }
531         
532         
533         /**
534          * The JTable model.  Includes an extra motor, given in the constructor,
535          * if it is not already in the database.
536          */
537         private class MotorDatabaseModel extends AbstractTableModel {
538                 private final Motor extra;
539                 
540                 public MotorDatabaseModel(Motor current) {
541                         if (Databases.MOTOR.contains(current))
542                                 extra = null;
543                         else
544                                 extra = current;
545                 }
546                 
547                 @Override
548                 public int getColumnCount() {
549                         return MotorColumns.values().length;
550                 }
551
552                 @Override
553                 public int getRowCount() {
554                         if (extra == null)
555                                 return Databases.MOTOR.size();
556                         else
557                                 return Databases.MOTOR.size()+1;
558                 }
559
560                 @Override
561                 public Object getValueAt(int rowIndex, int columnIndex) {
562                         MotorColumns column = getColumn(columnIndex);
563                         if (extra == null) {
564                                 return column.getValue(Databases.MOTOR.get(rowIndex));
565                         } else {
566                                 if (rowIndex == 0)
567                                         return column.getValue(extra);
568                                 else
569                                         return column.getValue(Databases.MOTOR.get(rowIndex - 1));
570                         }
571                 }
572                 
573                 @Override
574                 public String getColumnName(int columnIndex) {
575                         return getColumn(columnIndex).getTitle();
576                 }
577                 
578                 
579                 public Motor getMotor(int rowIndex) {
580                         if (extra == null) {
581                                 return Databases.MOTOR.get(rowIndex);
582                         } else {
583                                 if (rowIndex == 0)
584                                         return extra;
585                                 else
586                                         return Databases.MOTOR.get(rowIndex-1);
587                         }
588                 }
589                 
590                 public int getIndex(Motor m) {
591                         if (extra == null) {
592                                 return Databases.MOTOR.indexOf(m);
593                         } else {
594                                 if (extra.equals(m))
595                                         return 0;
596                                 else
597                                         return Databases.MOTOR.indexOf(m)+1;
598                         }
599                 }
600                 
601                 private MotorColumns getColumn(int index) {
602                         return MotorColumns.values()[index];
603                 }
604         }
605
606         
607         ////////  Row filters
608         
609         /**
610          * Abstract adapter class.
611          */
612         private abstract class MotorRowFilter extends RowFilter<TableModel,Integer> {
613                 @Override
614                 public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
615                         int index = entry.getIdentifier();
616                         Motor m = model.getMotor(index);
617                         return filterByDiameter(m) && filterByString(m);
618                 }
619                 
620                 public abstract boolean filterByDiameter(Motor m);
621                 
622                 
623                 public boolean filterByString(Motor m) {
624                         main: for (String s : searchTerms) {
625                                 for (MotorColumns col : MotorColumns.values()) {
626                                         String str = col.getValue(m).toLowerCase();
627                                         if (str.indexOf(s) >= 0)
628                                                 continue main;
629                                 }
630                                 return false;
631                         }
632                         return true;
633                 }
634         }
635         
636         /**
637          * Show all motors.
638          */
639         private class MotorRowFilterAll extends MotorRowFilter {
640                 @Override
641                 public boolean filterByDiameter(Motor m) {
642                         return true;
643                 }
644         }
645         
646         /**
647          * Show motors smaller than the mount.
648          */
649         private class MotorRowFilterSmaller extends MotorRowFilter {
650                 @Override
651                 public boolean filterByDiameter(Motor m) {
652                         return (m.getDiameter() <= diameter + 0.0004);
653                 }
654         }
655         
656         /**
657          * Show motors that fit the mount.
658          */
659         private class MotorRowFilterExact extends MotorRowFilter {
660                 @Override
661                 public boolean filterByDiameter(Motor m) {
662                         return ((m.getDiameter() <= diameter + 0.0004) &&
663                                         (m.getDiameter() >= diameter - 0.0015));
664                 }
665         }
666         
667         
668         private static Comparator<String> numericalComparator = null;
669         private static Comparator<String> getNumericalComparator() {
670                 if (numericalComparator == null)
671                         numericalComparator = new NumericalComparator();
672                 return numericalComparator;
673         }
674         
675         private static class NumericalComparator implements Comparator<String> {
676                 private Pattern pattern = 
677                         Pattern.compile("^\\s*([0-9]*[.,][0-9]+|[0-9]+[.,]?[0-9]*).*?$");
678                 private Collator collator = null;
679                 @Override
680                 public int compare(String s1, String s2) {
681                         Matcher m1, m2;
682                         
683                         m1 = pattern.matcher(s1);
684                         m2 = pattern.matcher(s2);
685                         if (m1.find() && m2.find()) {
686                                 double d1 = Double.parseDouble(m1.group(1));
687                                 double d2 = Double.parseDouble(m2.group(1));
688                                 
689                                 return (int)((d1-d2)*1000);
690                         }
691                         
692                         if (collator == null)
693                                 collator = Collator.getInstance(Locale.US);
694                         return collator.compare(s1, s2);
695                 }
696         }
697         
698 }