content: 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) { }
  	}
  }