package aima.gui.swing.applications.robotics.components; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Arrays; import java.util.Properties; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import aima.core.robotics.impl.datatypes.Angle; import aima.gui.swing.framework.util.GuiBase; import aima.gui.swing.framework.util.ListTableModel; /** * This class is a panel where the user can set angles for whatever use you may find entertaining and/or productive. * One can add to, edit and delete these angles. * * @author Arno von Borries * @author Jan Phillip Kretzschmar * @author Andreas Walscheid */ public class AnglePanel extends Settings.SpecialSetting { /** * The default key that is used to store the angles in the properties file when using the convenience constructor {@code AnglePanel(String title)}. */ public static final String DEFAULT_ANGLES_KEY = "ANGLES"; private static final long serialVersionUID = 1L; private static final String JL_ANGLE_TEXT ="Selected Angle:"; private static final String JL_NUMBER_OF_ANGLES_TEXT = "Angle Count: "; private static final String BTN_ADD_ANGLE_TEXT = "Add Angle"; private static final String BTN_DELETE_ANGLE_TEXT = "Delete Angle"; private static final String TEXT_SHIFT_ANGLE = "Shift Angle by:"; private static final String ANGLES_COLUMN_NAME = "Angles"; private static final int RADIUS = 75; private final String anglesKey; private JLabel jLAngleCount; private JTextField jTFChangeAngle; private JButton btnDeleteAngle; private JTextField jTShiftValueAngle; private CirclePanel circlePanel; private ListTableModel angleModel = new ListTableModel(ANGLES_COLUMN_NAME); private JTable jTAngles; private int selectedAngleIndex; private double[] previousAngles; private double[] angles = {-90.0d,-45.0d,0.0d,45.0d,90.0d}; private ChangeListener listener; /** * Convenience constructor using a default key for the properties file. * @param title the title of the angle panel. */ public AnglePanel(String title) { this(DEFAULT_ANGLES_KEY, title); } /** * Default constructor with no parameters. * @param key the key identifying the angles in the properties. * @param title the title of the angle panel. */ public AnglePanel(String key, String title) { anglesKey = key; setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); JLabel jLTitle = new JLabel(title); jLTitle.setBorder(GuiBase.getClearanceBorder()); //leftPanel: JLabel jLAngle = new JLabel(JL_ANGLE_TEXT); jTFChangeAngle = new JTextField(); jTFChangeAngle.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if(e.getKeyCode() == KeyEvent.VK_ENTER && angles != null) { try{ angles[selectedAngleIndex] = validateInput(jTFChangeAngle); updateGui(selectedAngleIndex); } catch(NumberFormatException f) { GuiBase.showMessageBox("Please enter a valid number!"); jTFChangeAngle.setText(angles[selectedAngleIndex] + "\u00BA"); } } } }); jLAngleCount = new JLabel(JL_NUMBER_OF_ANGLES_TEXT + "0"); JButton btnAddAngle = new JButton(BTN_ADD_ANGLE_TEXT); btnAddAngle.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { addAngle(); } }); JButton btnDeleteAngle = new JButton(BTN_DELETE_ANGLE_TEXT); btnDeleteAngle.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { deleteAngle(); } }); JLabel jLShiftAngle = new JLabel(TEXT_SHIFT_ANGLE); jTShiftValueAngle = new JTextField(); JButton btnShiftAngleCreate = new JButton(BTN_ADD_ANGLE_TEXT); btnShiftAngleCreate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { final double ang = validateInput(jTShiftValueAngle); final double angleValue = angles[selectedAngleIndex]; addAngle(); angles[selectedAngleIndex] = angleValue + ang; updateGui(selectedAngleIndex); } catch(NumberFormatException e) { GuiBase.showMessageBox("Please enter a valid number!"); } } }); JPanel leftPanel = new JPanel(); leftPanel.setBorder(GuiBase.getClearanceBorder()); //leftPanel.setPreferredSize(new Dimension(100,1)); leftPanel.setLayout(new GridLayout(0,1,GuiBase.getClearance(),GuiBase.getClearance())); leftPanel.add(jLAngle); leftPanel.add(jTFChangeAngle); leftPanel.add(jLAngleCount); leftPanel.add(btnAddAngle); leftPanel.add(btnDeleteAngle); leftPanel.add(jLShiftAngle); leftPanel.add(jTShiftValueAngle); leftPanel.add(btnShiftAngleCreate); //circlePanel: circlePanel = new CirclePanel(); circlePanel.setBorder(GuiBase.getClearanceBorder()); //rightPanel: jTAngles = new JTable(angleModel); jTAngles.setFillsViewportHeight(true); jTAngles.getSelectionModel().addListSelectionListener( new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { selectedAngleIndex = jTAngles.getSelectedRow(); jTFChangeAngle.setText(angles[selectedAngleIndex] + "\u00BA"); repaint(); } }); JScrollPane scrollPane = new JScrollPane(jTAngles, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); JPanel rightPanel = new JPanel(); rightPanel.setBorder(GuiBase.getClearanceBorder()); rightPanel.setPreferredSize(new Dimension(100,1)); rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS)); rightPanel.add(scrollPane); //Put all panels together: JPanel mainPanel = new JPanel(); mainPanel.setBorder(BorderFactory.createLineBorder(GuiBase.getTextColor(), 1)); mainPanel.setLayout(new BorderLayout()); mainPanel.setAlignmentX(LEFT_ALIGNMENT); jLTitle.setLabelFor(mainPanel); mainPanel.add(leftPanel, BorderLayout.WEST); mainPanel.add(circlePanel, BorderLayout.CENTER); mainPanel.add(rightPanel, BorderLayout.EAST); add(jLTitle); add(mainPanel); for(double angle: angles) { angleModel.add(angle + "\u00BA"); } } private double validateInput(JTextField textField) { //delete the degree sign if it is included in the text: if(textField.getText().contains("\u00BA")) { textField.setText(textField.getText().replace("\u00BA", "")); } //if dots and commas are in the text field a NumberFormatException will be thrown if(textField.getText().contains(",") && textField.getText().contains(".")) { throw new NumberFormatException(); } //if the number contains only a comma it will be replaced with a dot if(textField.getText().contains(",")) { textField.setText(textField.getText().replace(",", ".")); } double ang = Double.parseDouble(jTShiftValueAngle.getText()); ang = ang % 360; if(ang > 180.0d) ang -= 360.0d; else if(ang < -180.0d) ang += 360.0d; return ang; } /** * Returns all angles as an array of {@link Angle}. * @return an array of {@link Angle}. */ public Angle[] getAngles() { if(angles == null) { final Angle[] result = {new Angle(0.0d)}; return result; } Angle[] result = new Angle[angles.length]; for(int i=0; i < angles.length; i++) { result[i] = new Angle(Math.toRadians(angles[i])); } Arrays.sort(result); return result; } @Override public void notifyChangeListener() { listener.notify(getAngles()); } /** * Sets the change listener that will be notified of any changes, like how change listeners are supposed to work. * @param listener the listener to be registered. */ public void setChangeListener(ChangeListener listener) { this.listener = listener; } /** * Adds an angle and updates all relevant GUI components. */ private void addAngle() { if(angles == null) { angles = new double[1]; selectedAngleIndex = 0; btnDeleteAngle.setEnabled(true); } else { double[] tmpAngles = new double[angles.length + 1]; for(int i = 0 ; i < angles.length; i++) { tmpAngles[i] = angles[i]; } angles = tmpAngles; } angles[angles.length -1] = 0.0d; angleModel.add(angles[angles.length -1] + ""); selectedAngleIndex = angles.length - 1; jTFChangeAngle.setText(angles[selectedAngleIndex] + "\u00BA"); jLAngleCount.setText(JL_NUMBER_OF_ANGLES_TEXT + angles.length); repaint(); } /** * Deletes the selected angle and updates all relevant GUI components. */ private void deleteAngle() { if(angles == null) return; if(angles.length == 1) { angles = null; jTFChangeAngle.setText(""); angleModel.removeValueAt(0); jLAngleCount.setText(JL_NUMBER_OF_ANGLES_TEXT +"0"); btnDeleteAngle.setEnabled(false); } else { double[] tmpAngles = new double[angles.length -1]; for(int i = 0; i < angles.length - 1; i++){ if(i >= selectedAngleIndex) { tmpAngles[i] = angles[i + 1]; } else { tmpAngles[i] = angles[i]; } } angles = tmpAngles; angleModel.removeValueAt(selectedAngleIndex); selectedAngleIndex = 0; jTFChangeAngle.setText(angles[selectedAngleIndex] + "\u00BA"); jLAngleCount.setText("Count of Angles: " + angles.length); } repaint(); } /** * Updates all GUI components. */ private void updateGui() { angleModel.clear(); selectedAngleIndex = 0; jTFChangeAngle.setText(GuiBase.getFormat().format(angles[selectedAngleIndex]) + "\u00BA"); for(int i = 0; i < angles.length; i++) angleModel.add(angles[i] + "\u00BA"); jLAngleCount.setText(JL_NUMBER_OF_ANGLES_TEXT + angleModel.getRowCount()); repaint(); } /** * Updates the GUI components for the angle with the passed index. * @param angleIndex the index of the angle. */ private void updateGui(int angleIndex) { jTFChangeAngle.setText(GuiBase.getFormat().format(angles[angleIndex]) + "\u00BA"); angleModel.setValueAt(angleIndex, angles[angleIndex] + "\u00BA"); repaint(); } @Override public void loadSettings(Properties values) { String saveString = values.getProperty(anglesKey); if(saveString != null) { try { ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(saveString.getBytes())); angles = (double[]) stream.readObject(); previousAngles = angles; updateGui(); } catch (Exception e) { e.printStackTrace(); } } } @Override public void saveSettings(Properties values) { String saveString = null; try { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream stream = new ObjectOutputStream(byteStream); stream.writeObject(angles); saveString = byteStream.toString(); } catch (Exception e) { e.printStackTrace(); } if(saveString != null) values.put(anglesKey, saveString); } @Override public void revertGui() { angles = previousAngles; repaint(); updateGui(); } @Override public void saveGui() { if(listener != null) { Angle[] radiantAngles = new Angle[angles.length]; for(int i=0; i<angles.length;i++) { radiantAngles[i] = new Angle(Math.toRadians(angles[i])); } Arrays.sort(radiantAngles, null); listener.notify(radiantAngles); } previousAngles = angles; } /** * Defines the method infrastructure of any and all possible and legal implementations of a ChangeListener which can be registered in an {@link AnglePanel}. */ public interface ChangeListener { /** * Notifies the ChangeListener of a change in the angle array. * @param angles the array of {@link Angle}. */ public void notify(Angle[] angles); } /** * A simple drawing all angles onto a circle / polar coordinate system. */ private class CirclePanel extends JPanel implements MouseListener { private static final long serialVersionUID = 1L; private int centerX; private int centerY; private CirclePanel() { addMouseListener(this); } @Override public void paint(Graphics g) { super.paint(g); Graphics2D g2d = (Graphics2D) g; g2d.setColor(GuiBase.getTextColor()); centerX = (int) (getWidth() / 2); centerY = (int) (getHeight() / 2); //paints a coordinate system and draws a circle //around the zero point g2d.drawOval(centerX - RADIUS, centerY - RADIUS, 2 * RADIUS, 2 * RADIUS); g2d.drawLine(centerX - (int) (1.5 * RADIUS), centerY, centerX + (int) (1.5 * RADIUS), centerY); g2d.drawLine(centerX, centerY - (int) (1.5 * RADIUS), centerX, centerY + (int) (1.5 * RADIUS)); //paints every angle on the circular track if(angles != null) { int i=0; for(; i < selectedAngleIndex; i++) { drawAngle(g2d, i); } i++; for(; i < angles.length; i++) { drawAngle(g2d, i); } //Selected angle has to be drawn last: g2d.setColor(Color.RED); drawAngle(g2d, selectedAngleIndex); } } /** * Draws an angle onto the provided {@link Graphics2D}. * @param g2d the graphics of this panel onto which will be drawn. * @param i the index of the angle to be drawn. */ private void drawAngle(Graphics2D g2d, int i) { final int xCircle = (int) (centerX + RADIUS * Math.cos(Math.toRadians(angles[i]))); final int yCircle = (int) (centerY + RADIUS * Math.sin(Math.toRadians(angles[i]))); g2d.drawLine(getWidth() / 2, getHeight() / 2, xCircle, flipY(yCircle)); g2d.fillOval(xCircle - 10, flipY(yCircle) - 10, 20, 20); } /** * Flips the y value from a Cartesian coordinate system into a java coordinate system. * @param y the Y-coordinate form a Cartesian coordinate system. * @return the Y-coordinate for the coordinate system in java. */ private int flipY(int y) { return getHeight() - y; } @Override public void mouseClicked(MouseEvent arg0) { //At a double mouse click, the index of the clicked angle is searched for. If an index is found, the angle is marked with selectedAngleIndex. if(arg0.getClickCount() == 2) { if(angles != null) { int i = 0; for(; i < angles.length; i++) { int xCircle = (int) (centerX + RADIUS * Math.cos(Math.toRadians(angles[i]))); int yCircle = (int) (centerY + RADIUS * Math.sin(Math.toRadians(angles[i]))); yCircle = flipY(yCircle); if((arg0.getX() <= xCircle + 10 && arg0.getX() >= xCircle - 10) && (arg0.getY() <= yCircle + 10 && arg0.getY() >= yCircle - 10)) { break; } } if(i < angles.length) { selectedAngleIndex = i; repaint(); jTFChangeAngle.setText(angles[selectedAngleIndex] + "\u00BA"); } } } } @Override public void mouseEntered(MouseEvent arg0) { } @Override public void mouseExited(MouseEvent arg0) { } @Override public void mousePressed(MouseEvent arg0) { } @Override public void mouseReleased(MouseEvent arg0) { } } }