package net.sf.openrocket.gui.dialogs.motor.thrustcurve; import java.awt.Color; import java.awt.Component; import java.awt.Paint; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.prefs.Preferences; import javax.swing.DefaultComboBoxModel; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.RowSorter; import javax.swing.SortOrder; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.database.motor.ThrustCurveMotorSet; import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.dialogs.motor.CloseableDialog; import net.sf.openrocket.gui.dialogs.motor.MotorSelector; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.motor.Manufacturer; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.ThrustCurveMotor; import net.sf.openrocket.rocketcomponent.MotorConfiguration; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; import net.sf.openrocket.utils.MotorCorrelation; import org.jfree.chart.ChartColor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelector { private static final Logger log = LoggerFactory.getLogger(ThrustCurveMotorSelectionPanel.class); private static final Translator trans = Application.getTranslator(); private static final double MOTOR_SIMILARITY_THRESHOLD = 0.95; private static final Paint[] CURVE_COLORS = ChartColor.createDefaultPaintArray(); private static final ThrustCurveMotorComparator MOTOR_COMPARATOR = new ThrustCurveMotorComparator(); private List<ThrustCurveMotorSet> database; private CloseableDialog dialog = null; private final ThrustCurveMotorDatabaseModel model; private final JTable table; private final TableRowSorter<TableModel> sorter; private final MotorRowFilter rowFilter; private final JCheckBox hideSimilarBox; private final JTextField searchField; private final JLabel curveSelectionLabel; private final JComboBox curveSelectionBox; private final DefaultComboBoxModel curveSelectionModel; private final JComboBox delayBox; private final MotorInformationPanel motorInformationPanel; private final MotorFilterPanel motorFilterPanel; private ThrustCurveMotor selectedMotor; private ThrustCurveMotorSet selectedMotorSet; private double selectedDelay; public ThrustCurveMotorSelectionPanel(MotorMount mount, String currentConfig) { this(); setMotorMountAndConfig( mount, currentConfig ); } /** * Sole constructor. * * @param current the currently selected ThrustCurveMotor, or <code>null</code> for none. * @param delay the currently selected ejection charge delay. * @param diameter the diameter of the motor mount. */ public ThrustCurveMotorSelectionPanel() { super(new MigLayout("fill", "[grow][]")); // Construct the database (adding the current motor if not in the db already) database = Application.getThrustCurveMotorSetDatabase().getMotorSets(); model = new ThrustCurveMotorDatabaseModel(database); rowFilter = new MotorRowFilter(model); motorInformationPanel = new MotorInformationPanel(); //// MotorFilter { // Find all the manufacturers: Set<Manufacturer> allManufacturers = new HashSet<Manufacturer>(); for (ThrustCurveMotorSet s : database) { allManufacturers.add(s.getManufacturer()); } motorFilterPanel = new MotorFilterPanel(allManufacturers, rowFilter) { @Override public void onSelectionChanged() { sorter.sort(); scrollSelectionVisible(); } }; } //// GUI JPanel panel = new JPanel(new MigLayout("fill","[][grow]")); //// Select thrust curve: { curveSelectionLabel = new JLabel(trans.get("TCMotorSelPan.lbl.Selectthrustcurve")); panel.add(curveSelectionLabel); curveSelectionModel = new DefaultComboBoxModel(); curveSelectionBox = new JComboBox(curveSelectionModel); curveSelectionBox.setRenderer(new CurveSelectionRenderer(curveSelectionBox.getRenderer())); curveSelectionBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Object value = curveSelectionBox.getSelectedItem(); if (value != null) { select(((MotorHolder) value).getMotor()); } } }); panel.add(curveSelectionBox, "growx, wrap"); } // Ejection charge delay: { panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay"))); delayBox = new JComboBox(); delayBox.setEditable(true); delayBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JComboBox cb = (JComboBox) e.getSource(); String sel = (String) cb.getSelectedItem(); //// None if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.equalsIgnoreCase.None"))) { selectedDelay = Motor.PLUGGED; } else { try { selectedDelay = Double.parseDouble(sel); } catch (NumberFormatException ignore) { } } setDelays(false); } }); panel.add(delayBox, "growx,wrap"); //// (Number of seconds or \"None\") panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.NumberofsecondsorNone"), -3), "skip, wrap"); setDelays(false); } //// Hide very similar thrust curves { hideSimilarBox = new JCheckBox(trans.get("TCMotorSelPan.checkbox.hideSimilar")); GUIUtil.changeFontSize(hideSimilarBox, -1); hideSimilarBox.setSelected(Application.getPreferences().getBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, true)); hideSimilarBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Application.getPreferences().putBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, hideSimilarBox.isSelected()); updateData(); } }); panel.add(hideSimilarBox, "gapleft para, spanx, growx, wrap"); } //// Motor selection table { table = new JTable(model); // Set comparators and widths table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); sorter = new TableRowSorter<TableModel>(model); for (int i = 0; i < ThrustCurveMotorColumns.values().length; i++) { ThrustCurveMotorColumns column = ThrustCurveMotorColumns.values()[i]; sorter.setComparator(i, column.getComparator()); table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth()); } table.setRowSorter(sorter); // force initial sort order to by diameter, total impulse, manufacturer { RowSorter.SortKey[] sortKeys = { new RowSorter.SortKey(ThrustCurveMotorColumns.DIAMETER.ordinal(), SortOrder.ASCENDING), new RowSorter.SortKey(ThrustCurveMotorColumns.TOTAL_IMPULSE.ordinal(), SortOrder.ASCENDING), new RowSorter.SortKey(ThrustCurveMotorColumns.MANUFACTURER.ordinal(), SortOrder.ASCENDING) }; sorter.setSortKeys(Arrays.asList(sortKeys)); } sorter.setRowFilter(rowFilter); // Set selection and double-click listeners table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { int row = table.getSelectedRow(); if (row >= 0) { row = table.convertRowIndexToModel(row); ThrustCurveMotorSet motorSet = model.getMotorSet(row); log.info(Markers.USER_MARKER, "Selected table row " + row + ": " + motorSet); if (motorSet != selectedMotorSet) { select(selectMotor(motorSet)); } } else { log.info(Markers.USER_MARKER, "Selected table row " + row + ", nothing selected"); } } }); table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { if (dialog != null) { dialog.close(true); } } } }); JScrollPane scrollpane = new JScrollPane(); scrollpane.setViewportView(table); panel.add(scrollpane, "grow, width :500:, spanx, wrap"); } // Search field { //// Search: StyledLabel label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Search")); panel.add(label); searchField = new JTextField(); searchField.getDocument().addDocumentListener(new DocumentListener() { @Override public void changedUpdate(DocumentEvent e) { update(); } @Override public void insertUpdate(DocumentEvent e) { update(); } @Override public void removeUpdate(DocumentEvent e) { update(); } private void update() { String text = searchField.getText().trim(); String[] split = text.split("\\s+"); rowFilter.setSearchTerms(Arrays.asList(split)); sorter.sort(); scrollSelectionVisible(); } }); panel.add(searchField, "span, growx"); } this.add(panel, "grow"); // Vertical split this.add(new JSeparator(JSeparator.VERTICAL), "growy, gap para para"); JTabbedPane rightSide = new JTabbedPane(); rightSide.add(trans.get("TCMotorSelPan.btn.filter"), motorFilterPanel); rightSide.add(trans.get("TCMotorSelPan.btn.details"), motorInformationPanel); this.add(rightSide); // Update the panel data updateData(); setDelays(false); } public void setMotorMountAndConfig( MotorMount mount, String currentConfig ) { selectedMotor = null; selectedMotorSet = null; selectedDelay = 0; ThrustCurveMotor motorToSelect = null; if (currentConfig != null && mount != null) { MotorConfiguration motorConf = mount.getMotorConfiguration().get(currentConfig); motorToSelect = (ThrustCurveMotor) motorConf.getMotor(); selectedDelay = motorConf.getEjectionDelay(); } // If current motor is not found in db, add a new ThrustCurveMotorSet containing it if (motorToSelect != null) { ThrustCurveMotorSet motorSetToSelect = null; motorSetToSelect = findMotorSet(motorToSelect); if (motorSetToSelect == null) { database = new ArrayList<ThrustCurveMotorSet>(database); ThrustCurveMotorSet extra = new ThrustCurveMotorSet(); extra.addMotor(motorToSelect); database.add(extra); Collections.sort(database); } } select(motorToSelect); motorFilterPanel.setMotorMount(mount); scrollSelectionVisible(); } @Override public Motor getSelectedMotor() { return selectedMotor; } @Override public double getSelectedDelay() { return selectedDelay; } @Override public JComponent getDefaultFocus() { return searchField; } @Override public void selectedMotor(Motor motorSelection) { if (!(motorSelection instanceof ThrustCurveMotor)) { log.error("Received argument that was not ThrustCurveMotor: " + motorSelection); return; } ThrustCurveMotor motor = (ThrustCurveMotor) motorSelection; ThrustCurveMotorSet set = findMotorSet(motor); if (set == null) { log.error("Could not find set for motor:" + motorSelection); return; } // Store selected motor in preferences node, set all others to false Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); for (ThrustCurveMotor m : set.getMotors()) { String digest = m.getDigest(); prefs.putBoolean(digest, m == motor); } } public void setCloseableDialog(CloseableDialog dialog) { this.dialog = dialog; } /** * Called when a different motor is selected from within the panel. */ private void select(ThrustCurveMotor motor) { if (selectedMotor == motor || motor == null) return; ThrustCurveMotorSet set = findMotorSet(motor); if (set == null) { throw new BugException("Could not find motor from database, motor=" + motor); } boolean updateDelays = (selectedMotorSet != set); selectedMotor = motor; selectedMotorSet = set; updateData(); if (updateDelays) { setDelays(true); } scrollSelectionVisible(); } private void updateData() { if (selectedMotorSet == null) { // No motor selected curveSelectionModel.removeAllElements(); curveSelectionBox.setEnabled(false); curveSelectionLabel.setEnabled(false); motorInformationPanel.clearData(); table.clearSelection(); return; } // Check which thrust curves to display List<ThrustCurveMotor> motors = getFilteredCurves(); final int index = motors.indexOf(selectedMotor); // Update the thrust curve selection box curveSelectionModel.removeAllElements(); for (int i = 0; i < motors.size(); i++) { curveSelectionModel.addElement(new MotorHolder(motors.get(i), i)); } curveSelectionBox.setSelectedIndex(index); if (motors.size() > 1) { curveSelectionBox.setEnabled(true); curveSelectionLabel.setEnabled(true); } else { curveSelectionBox.setEnabled(false); curveSelectionLabel.setEnabled(false); } motorInformationPanel.updateData(motors, selectedMotor); } List<ThrustCurveMotor> getFilteredCurves() { List<ThrustCurveMotor> motors = selectedMotorSet.getMotors(); if (hideSimilarBox.isSelected() && selectedMotor != null) { List<ThrustCurveMotor> filtered = new ArrayList<ThrustCurveMotor>(motors.size()); for (int i = 0; i < motors.size(); i++) { ThrustCurveMotor m = motors.get(i); if (m.equals(selectedMotor)) { filtered.add(m); continue; } double similarity = MotorCorrelation.similarity(selectedMotor, m); log.debug("Motor similarity: " + similarity); if (similarity < MOTOR_SIMILARITY_THRESHOLD) { filtered.add(m); } } motors = filtered; } Collections.sort(motors, MOTOR_COMPARATOR); return motors; } private void scrollSelectionVisible() { if (selectedMotorSet != null) { int index = table.convertRowIndexToView(model.getIndex(selectedMotorSet)); //System.out.println("index=" + index); table.getSelectionModel().setSelectionInterval(index, index); Rectangle rect = table.getCellRect(index, 0, true); rect = new Rectangle(rect.x, rect.y - 100, rect.width, rect.height + 200); table.scrollRectToVisible(rect); } } public static Color getColor(int index) { return (Color) CURVE_COLORS[index % CURVE_COLORS.length]; } /** * Find the ThrustCurveMotorSet that contains a motor. * * @param motor the motor to look for. * @return the ThrustCurveMotorSet, or null if not found. */ private ThrustCurveMotorSet findMotorSet(ThrustCurveMotor motor) { for (ThrustCurveMotorSet set : database) { if (set.getMotors().contains(motor)) { return set; } } return null; } /** * Select the default motor from this ThrustCurveMotorSet. This uses primarily motors * that the user has previously used, and secondarily a heuristic method of selecting which * thrust curve seems to be better or more reliable. * * @param set the motor set * @return the default motor in this set */ private ThrustCurveMotor selectMotor(ThrustCurveMotorSet set) { if (set.getMotorCount() == 0) { throw new BugException("Attempting to select motor from empty ThrustCurveMotorSet: " + set); } if (set.getMotorCount() == 1) { return set.getMotors().get(0); } // Find which motor has been used the most recently List<ThrustCurveMotor> list = set.getMotors(); Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); for (ThrustCurveMotor m : list) { String digest = m.getDigest(); if (prefs.getBoolean(digest, false)) { return m; } } // No motor has been used Collections.sort(list, MOTOR_COMPARATOR); return list.get(0); } /** * Set the values in the delay combo box. If <code>reset</code> is <code>true</code> * then sets the selected value as the value closest to selectedDelay, otherwise * leaves selection alone. */ private void setDelays(boolean reset) { if (selectedMotor == null) { //// None delayBox.setModel(new DefaultComboBoxModel(new String[] { trans.get("TCMotorSelPan.delayBox.None") })); delayBox.setSelectedIndex(0); } else { List<Double> delays = selectedMotorSet.getDelays(); String[] delayStrings = new String[delays.size()]; double currentDelay = selectedDelay; // Store current setting locally for (int i = 0; i < delays.size(); i++) { //// None delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), trans.get("TCMotorSelPan.delayBox.None")); } delayBox.setModel(new DefaultComboBoxModel(delayStrings)); if (reset) { // Find and set the closest value double closest = Double.NaN; for (int i = 0; i < delays.size(); i++) { // if-condition to always become true for NaN if (!(Math.abs(delays.get(i) - currentDelay) > Math.abs(closest - currentDelay))) { closest = delays.get(i); } } if (!Double.isNaN(closest)) { selectedDelay = closest; //// None delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(closest, trans.get("TCMotorSelPan.delayBox.None"))); } else { delayBox.setSelectedItem("None"); } } else { selectedDelay = currentDelay; //// None delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, trans.get("TCMotorSelPan.delayBox.None"))); } } } ////////////////////// private class CurveSelectionRenderer implements ListCellRenderer { private final ListCellRenderer renderer; public CurveSelectionRenderer(ListCellRenderer renderer) { this.renderer = renderer; } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof MotorHolder) { MotorHolder m = (MotorHolder) value; c.setForeground(getColor(m.getIndex())); } return c; } } }