diff --git a/robots/.idea/.gitignore b/robots/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/robots/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/robots/.idea/.name b/robots/.idea/.name
new file mode 100644
index 0000000..c677269
--- /dev/null
+++ b/robots/.idea/.name
@@ -0,0 +1 @@
+Robots
\ No newline at end of file
diff --git a/robots/.idea/codeStyles/codeStyleConfig.xml b/robots/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/robots/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/robots/.idea/misc.xml b/robots/.idea/misc.xml
new file mode 100644
index 0000000..8444aa9
--- /dev/null
+++ b/robots/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/robots/.idea/vcs.xml b/robots/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/robots/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/robots/src/gui/GameVisualizer.java b/robots/src/gui/GameVisualizer.java
index f82cfd8..659b354 100644
--- a/robots/src/gui/GameVisualizer.java
+++ b/robots/src/gui/GameVisualizer.java
@@ -8,34 +8,29 @@
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
+import java.util.Observable;
+import java.util.Observer;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JPanel;
-public class GameVisualizer extends JPanel
+public class GameVisualizer extends JPanel implements Observer
{
- private final Timer m_timer = initTimer();
-
- private static Timer initTimer()
+ private final Timer timer = initTimer();
+ private final RobotModel model;
+
+ private static Timer initTimer()
{
- Timer timer = new Timer("events generator", true);
- return timer;
+ return new Timer("events generator", true);
}
-
- private volatile double m_robotPositionX = 100;
- private volatile double m_robotPositionY = 100;
- private volatile double m_robotDirection = 0;
-
- private volatile int m_targetPositionX = 150;
- private volatile int m_targetPositionY = 100;
-
- private static final double maxVelocity = 0.1;
- private static final double maxAngularVelocity = 0.001;
-
- public GameVisualizer()
+
+ public GameVisualizer(RobotModel model)
{
- m_timer.schedule(new TimerTask()
+ this.model = model;
+ this.model.addObserver(this);
+
+ timer.schedule(new TimerTask()
{
@Override
public void run()
@@ -43,7 +38,7 @@ public void run()
onRedrawEvent();
}
}, 0, 50);
- m_timer.schedule(new TimerTask()
+ timer.schedule(new TimerTask()
{
@Override
public void run()
@@ -57,154 +52,80 @@ public void run()
public void mouseClicked(MouseEvent e)
{
setTargetPosition(e.getPoint());
- repaint();
}
});
setDoubleBuffered(true);
}
- protected void setTargetPosition(Point p)
+ protected void setTargetPosition(Point point)
{
- m_targetPositionX = p.x;
- m_targetPositionY = p.y;
+ model.setTargetPosition(point);
}
-
+
protected void onRedrawEvent()
{
EventQueue.invokeLater(this::repaint);
}
- private static double distance(double x1, double y1, double x2, double y2)
- {
- double diffX = x1 - x2;
- double diffY = y1 - y2;
- return Math.sqrt(diffX * diffX + diffY * diffY);
- }
-
- private static double angleTo(double fromX, double fromY, double toX, double toY)
- {
- double diffX = toX - fromX;
- double diffY = toY - fromY;
-
- return asNormalizedRadians(Math.atan2(diffY, diffX));
- }
-
protected void onModelUpdateEvent()
{
- double distance = distance(m_targetPositionX, m_targetPositionY,
- m_robotPositionX, m_robotPositionY);
- if (distance < 0.5)
- {
- return;
- }
- double velocity = maxVelocity;
- double angleToTarget = angleTo(m_robotPositionX, m_robotPositionY, m_targetPositionX, m_targetPositionY);
- double angularVelocity = 0;
- if (angleToTarget > m_robotDirection)
- {
- angularVelocity = maxAngularVelocity;
- }
- if (angleToTarget < m_robotDirection)
- {
- angularVelocity = -maxAngularVelocity;
- }
-
- moveRobot(velocity, angularVelocity, 10);
- }
-
- private static double applyLimits(double value, double min, double max)
- {
- if (value < min)
- return min;
- if (value > max)
- return max;
- return value;
- }
-
- private void moveRobot(double velocity, double angularVelocity, double duration)
- {
- velocity = applyLimits(velocity, 0, maxVelocity);
- angularVelocity = applyLimits(angularVelocity, -maxAngularVelocity, maxAngularVelocity);
- double newX = m_robotPositionX + velocity / angularVelocity *
- (Math.sin(m_robotDirection + angularVelocity * duration) -
- Math.sin(m_robotDirection));
- if (!Double.isFinite(newX))
- {
- newX = m_robotPositionX + velocity * duration * Math.cos(m_robotDirection);
- }
- double newY = m_robotPositionY - velocity / angularVelocity *
- (Math.cos(m_robotDirection + angularVelocity * duration) -
- Math.cos(m_robotDirection));
- if (!Double.isFinite(newY))
- {
- newY = m_robotPositionY + velocity * duration * Math.sin(m_robotDirection);
- }
- m_robotPositionX = newX;
- m_robotPositionY = newY;
- double newDirection = asNormalizedRadians(m_robotDirection + angularVelocity * duration);
- m_robotDirection = newDirection;
+ model.update();
}
- private static double asNormalizedRadians(double angle)
- {
- while (angle < 0)
- {
- angle += 2*Math.PI;
- }
- while (angle >= 2*Math.PI)
- {
- angle -= 2*Math.PI;
- }
- return angle;
- }
-
private static int round(double value)
{
- return (int)(value + 0.5);
+ return (int) (value + 0.5);
}
-
+
@Override
- public void paint(Graphics g)
+ public void paint(Graphics graphics)
{
- super.paint(g);
- Graphics2D g2d = (Graphics2D)g;
- drawRobot(g2d, round(m_robotPositionX), round(m_robotPositionY), m_robotDirection);
- drawTarget(g2d, m_targetPositionX, m_targetPositionY);
+ super.paint(graphics);
+ Graphics2D g2d = (Graphics2D) graphics;
+ drawRobot(g2d, round(model.getRobotPositionX()), round(model.getRobotPositionY()), model.getRobotDirection());
+ drawTarget(g2d, model.getTargetPositionX(), model.getTargetPositionY());
}
-
+
private static void fillOval(Graphics g, int centerX, int centerY, int diam1, int diam2)
{
g.fillOval(centerX - diam1 / 2, centerY - diam2 / 2, diam1, diam2);
}
-
+
private static void drawOval(Graphics g, int centerX, int centerY, int diam1, int diam2)
{
g.drawOval(centerX - diam1 / 2, centerY - diam2 / 2, diam1, diam2);
}
-
+
private void drawRobot(Graphics2D g, int x, int y, double direction)
{
- int robotCenterX = round(m_robotPositionX);
- int robotCenterY = round(m_robotPositionY);
- AffineTransform t = AffineTransform.getRotateInstance(direction, robotCenterX, robotCenterY);
- g.setTransform(t);
+ AffineTransform oldTransform = g.getTransform();
+ AffineTransform rotateTransform = AffineTransform.getRotateInstance(direction, x, y);
+ g.setTransform(rotateTransform);
g.setColor(Color.MAGENTA);
- fillOval(g, robotCenterX, robotCenterY, 30, 10);
+ fillOval(g, x, y, 30, 10);
g.setColor(Color.BLACK);
- drawOval(g, robotCenterX, robotCenterY, 30, 10);
+ drawOval(g, x, y, 30, 10);
g.setColor(Color.WHITE);
- fillOval(g, robotCenterX + 10, robotCenterY, 5, 5);
+ fillOval(g, x + 10, y, 5, 5);
g.setColor(Color.BLACK);
- drawOval(g, robotCenterX + 10, robotCenterY, 5, 5);
+ drawOval(g, x + 10, y, 5, 5);
+ g.setTransform(oldTransform);
}
-
+
private void drawTarget(Graphics2D g, int x, int y)
{
- AffineTransform t = AffineTransform.getRotateInstance(0, 0, 0);
- g.setTransform(t);
+ AffineTransform oldTransform = g.getTransform();
+ g.setTransform(AffineTransform.getRotateInstance(0, 0, 0));
g.setColor(Color.GREEN);
fillOval(g, x, y, 5, 5);
g.setColor(Color.BLACK);
drawOval(g, x, y, 5, 5);
+ g.setTransform(oldTransform);
+ }
+
+ @Override
+ public void update(Observable observable, Object arg)
+ {
+ onRedrawEvent();
}
}
diff --git a/robots/src/gui/GameWindow.java b/robots/src/gui/GameWindow.java
index ecb63c0..bf66723 100644
--- a/robots/src/gui/GameWindow.java
+++ b/robots/src/gui/GameWindow.java
@@ -7,13 +7,14 @@
public class GameWindow extends JInternalFrame
{
- private final GameVisualizer m_visualizer;
- public GameWindow()
+ private final GameVisualizer visualizer;
+
+ public GameWindow(RobotModel model)
{
super("Игровое поле", true, true, true, true);
- m_visualizer = new GameVisualizer();
+ visualizer = new GameVisualizer(model);
JPanel panel = new JPanel(new BorderLayout());
- panel.add(m_visualizer, BorderLayout.CENTER);
+ panel.add(visualizer, BorderLayout.CENTER);
getContentPane().add(panel);
pack();
}
diff --git a/robots/src/gui/MainApplicationFrame.java b/robots/src/gui/MainApplicationFrame.java
index 62e943e..e07d9c7 100644
--- a/robots/src/gui/MainApplicationFrame.java
+++ b/robots/src/gui/MainApplicationFrame.java
@@ -2,7 +2,15 @@
import java.awt.Dimension;
import java.awt.Toolkit;
+import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.beans.PropertyVetoException;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
import javax.swing.JDesktopPane;
import javax.swing.JFrame;
@@ -10,136 +18,130 @@
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import log.Logger;
-/**
- * Что требуется сделать:
- * 1. Метод создания меню перегружен функционалом и трудно читается.
- * Следует разделить его на серию более простых методов (или вообще выделить отдельный класс).
- *
- */
public class MainApplicationFrame extends JFrame
{
private final JDesktopPane desktopPane = new JDesktopPane();
-
- public MainApplicationFrame() {
- //Make the big window be indented 50 pixels from each edge
- //of the screen.
- int inset = 50;
+
+ private static final String CONFIG_FILE_PATH =
+ System.getProperty("user.home") + "/robot_app.properties";
+
+ private final RobotModel robotModel = new RobotModel();
+
+ private LogWindow logWindow;
+ private GameWindow gameWindow;
+ private RobotCoordinatesWindow robotCoordinatesWindow;
+
+ public MainApplicationFrame()
+ {
+ int inset = 50;
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
setBounds(inset, inset,
- screenSize.width - inset*2,
- screenSize.height - inset*2);
+ screenSize.width - inset * 2,
+ screenSize.height - inset * 2);
setContentPane(desktopPane);
-
-
- LogWindow logWindow = createLogWindow();
+
+ logWindow = createLogWindow();
addWindow(logWindow);
- GameWindow gameWindow = new GameWindow();
- gameWindow.setSize(400, 400);
+ gameWindow = new GameWindow(robotModel);
+ gameWindow.setLocation(320, 10);
+ gameWindow.setSize(400, 400);
addWindow(gameWindow);
+ robotCoordinatesWindow = new RobotCoordinatesWindow(robotModel);
+ robotCoordinatesWindow.setLocation(10, 520);
+ robotCoordinatesWindow.setSize(300, 180);
+ addWindow(robotCoordinatesWindow);
+
+ restoreWindowSettings();
+
setJMenuBar(generateMenuBar());
- setDefaultCloseOperation(EXIT_ON_CLOSE);
+
+ setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+ addWindowListener(new WindowAdapter()
+ {
+ @Override
+ public void windowClosing(WindowEvent e)
+ {
+ exitApplication();
+ }
+ });
}
-
+
protected LogWindow createLogWindow()
{
- LogWindow logWindow = new LogWindow(Logger.getDefaultLogSource());
- logWindow.setLocation(10,10);
- logWindow.setSize(300, 800);
- setMinimumSize(logWindow.getSize());
- logWindow.pack();
+ LogWindow createdLogWindow = new LogWindow(Logger.getDefaultLogSource());
+ createdLogWindow.setLocation(10, 10);
+ createdLogWindow.setSize(300, 500);
+ setMinimumSize(createdLogWindow.getSize());
+ createdLogWindow.pack();
Logger.debug("Протокол работает");
- return logWindow;
+ return createdLogWindow;
}
-
+
protected void addWindow(JInternalFrame frame)
{
desktopPane.add(frame);
frame.setVisible(true);
}
-
-// protected JMenuBar createMenuBar() {
-// JMenuBar menuBar = new JMenuBar();
-//
-// //Set up the lone menu.
-// JMenu menu = new JMenu("Document");
-// menu.setMnemonic(KeyEvent.VK_D);
-// menuBar.add(menu);
-//
-// //Set up the first menu item.
-// JMenuItem menuItem = new JMenuItem("New");
-// menuItem.setMnemonic(KeyEvent.VK_N);
-// menuItem.setAccelerator(KeyStroke.getKeyStroke(
-// KeyEvent.VK_N, ActionEvent.ALT_MASK));
-// menuItem.setActionCommand("new");
-//// menuItem.addActionListener(this);
-// menu.add(menuItem);
-//
-// //Set up the second menu item.
-// menuItem = new JMenuItem("Quit");
-// menuItem.setMnemonic(KeyEvent.VK_Q);
-// menuItem.setAccelerator(KeyStroke.getKeyStroke(
-// KeyEvent.VK_Q, ActionEvent.ALT_MASK));
-// menuItem.setActionCommand("quit");
-//// menuItem.addActionListener(this);
-// menu.add(menuItem);
-//
-// return menuBar;
-// }
-
+
private JMenuBar generateMenuBar()
{
JMenuBar menuBar = new JMenuBar();
-
+
+ JMenu fileMenu = new JMenu("Файл");
+ fileMenu.setMnemonic(KeyEvent.VK_F);
+
+ JMenuItem exitItem = new JMenuItem("Выход", KeyEvent.VK_X);
+ exitItem.setAccelerator(KeyStroke.getKeyStroke(
+ KeyEvent.VK_Q, InputEvent.CTRL_DOWN_MASK));
+ exitItem.addActionListener((event) -> exitApplication());
+
+ fileMenu.add(exitItem);
+ menuBar.add(fileMenu);
+
JMenu lookAndFeelMenu = new JMenu("Режим отображения");
lookAndFeelMenu.setMnemonic(KeyEvent.VK_V);
lookAndFeelMenu.getAccessibleContext().setAccessibleDescription(
"Управление режимом отображения приложения");
-
- {
- JMenuItem systemLookAndFeel = new JMenuItem("Системная схема", KeyEvent.VK_S);
- systemLookAndFeel.addActionListener((event) -> {
- setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
- this.invalidate();
- });
- lookAndFeelMenu.add(systemLookAndFeel);
- }
- {
- JMenuItem crossplatformLookAndFeel = new JMenuItem("Универсальная схема", KeyEvent.VK_S);
- crossplatformLookAndFeel.addActionListener((event) -> {
- setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
- this.invalidate();
- });
- lookAndFeelMenu.add(crossplatformLookAndFeel);
- }
+ JMenuItem systemLookAndFeel = new JMenuItem("Системная схема", KeyEvent.VK_S);
+ systemLookAndFeel.addActionListener((event) -> {
+ setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ invalidate();
+ });
+ lookAndFeelMenu.add(systemLookAndFeel);
+
+ JMenuItem crossplatformLookAndFeel = new JMenuItem("Универсальная схема", KeyEvent.VK_U);
+ crossplatformLookAndFeel.addActionListener((event) -> {
+ setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+ invalidate();
+ });
+ lookAndFeelMenu.add(crossplatformLookAndFeel);
JMenu testMenu = new JMenu("Тесты");
testMenu.setMnemonic(KeyEvent.VK_T);
testMenu.getAccessibleContext().setAccessibleDescription(
"Тестовые команды");
-
- {
- JMenuItem addLogMessageItem = new JMenuItem("Сообщение в лог", KeyEvent.VK_S);
- addLogMessageItem.addActionListener((event) -> {
- Logger.debug("Новая строка");
- });
- testMenu.add(addLogMessageItem);
- }
+
+ JMenuItem addLogMessageItem = new JMenuItem("Сообщение в лог", KeyEvent.VK_L);
+ addLogMessageItem.addActionListener((event) -> Logger.debug("Новая строка"));
+ testMenu.add(addLogMessageItem);
menuBar.add(lookAndFeelMenu);
menuBar.add(testMenu);
return menuBar;
}
-
+
private void setLookAndFeel(String className)
{
try
@@ -148,9 +150,102 @@ private void setLookAndFeel(String className)
SwingUtilities.updateComponentTreeUI(this);
}
catch (ClassNotFoundException | InstantiationException
- | IllegalAccessException | UnsupportedLookAndFeelException e)
+ | IllegalAccessException | UnsupportedLookAndFeelException e)
+ {
+ // ignore
+ }
+ }
+
+ private void exitApplication()
+ {
+ UIManager.put("OptionPane.yesButtonText", "Да");
+ UIManager.put("OptionPane.noButtonText", "Нет");
+
+ int result = JOptionPane.showConfirmDialog(
+ this,
+ "Вы действительно хотите выйти?",
+ "Подтверждение выхода",
+ JOptionPane.YES_NO_OPTION);
+
+ if (result == JOptionPane.YES_OPTION)
+ {
+ saveWindowSettings();
+ dispose();
+ System.exit(0);
+ }
+ }
+
+ private void saveWindowSettings()
+ {
+ Properties properties = new Properties();
+
+ saveWindowProperties(properties, "log", logWindow);
+ saveWindowProperties(properties, "game", gameWindow);
+ saveWindowProperties(properties, "coordinates", robotCoordinatesWindow);
+
+ try (FileOutputStream outputStream = new FileOutputStream(CONFIG_FILE_PATH))
+ {
+ properties.store(outputStream, "Window settings");
+ }
+ catch (IOException e)
+ {
+ Logger.error("Не удалось сохранить настройки окон");
+ }
+ }
+
+ private void restoreWindowSettings()
+ {
+ Properties properties = new Properties();
+
+ try (FileInputStream inputStream = new FileInputStream(CONFIG_FILE_PATH))
+ {
+ properties.load(inputStream);
+
+ restoreWindowProperties(properties, "log", logWindow);
+ restoreWindowProperties(properties, "game", gameWindow);
+ restoreWindowProperties(properties, "coordinates", robotCoordinatesWindow);
+ }
+ catch (IOException e)
+ {
+ // если файла ещё нет, просто запускаемся с настройками по умолчанию
+ }
+ }
+
+ private void saveWindowProperties(Properties properties, String prefix, JInternalFrame frame)
+ {
+ properties.setProperty(prefix + ".x", Integer.toString(frame.getX()));
+ properties.setProperty(prefix + ".y", Integer.toString(frame.getY()));
+ properties.setProperty(prefix + ".width", Integer.toString(frame.getWidth()));
+ properties.setProperty(prefix + ".height", Integer.toString(frame.getHeight()));
+ properties.setProperty(prefix + ".icon", Boolean.toString(frame.isIcon()));
+ properties.setProperty(prefix + ".maximum", Boolean.toString(frame.isMaximum()));
+ }
+
+ private void restoreWindowProperties(Properties properties, String prefix, JInternalFrame frame)
+ {
+ int x = Integer.parseInt(properties.getProperty(prefix + ".x", Integer.toString(frame.getX())));
+ int y = Integer.parseInt(properties.getProperty(prefix + ".y", Integer.toString(frame.getY())));
+ int width = Integer.parseInt(properties.getProperty(prefix + ".width", Integer.toString(frame.getWidth())));
+ int height = Integer.parseInt(properties.getProperty(prefix + ".height", Integer.toString(frame.getHeight())));
+ boolean icon = Boolean.parseBoolean(properties.getProperty(prefix + ".icon", "false"));
+ boolean maximum = Boolean.parseBoolean(properties.getProperty(prefix + ".maximum", "false"));
+
+ frame.setBounds(x, y, width, height);
+
+ try
+ {
+ if (maximum)
+ {
+ frame.setMaximum(true);
+ }
+ if (icon)
+ {
+ frame.setIcon(true);
+ }
+ }
+ catch (PropertyVetoException e)
{
- // just ignore
+ Logger.error("Не удалось восстановить состояние окна: " + frame.getTitle());
}
}
}
diff --git a/robots/src/gui/RobotCoordinatesWindow.java b/robots/src/gui/RobotCoordinatesWindow.java
new file mode 100644
index 0000000..2a9572c
--- /dev/null
+++ b/robots/src/gui/RobotCoordinatesWindow.java
@@ -0,0 +1,52 @@
+package gui;
+
+import java.awt.BorderLayout;
+import java.awt.EventQueue;
+import java.awt.TextArea;
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.swing.JInternalFrame;
+import javax.swing.JPanel;
+
+public class RobotCoordinatesWindow extends JInternalFrame implements Observer
+{
+ private final RobotModel model;
+ private final TextArea coordinatesContent;
+
+ public RobotCoordinatesWindow(RobotModel model)
+ {
+ super("Координаты робота", true, true, true, true);
+ this.model = model;
+ this.model.addObserver(this);
+
+ coordinatesContent = new TextArea("");
+ coordinatesContent.setEditable(false);
+ coordinatesContent.setSize(250, 140);
+
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.add(coordinatesContent, BorderLayout.CENTER);
+ getContentPane().add(panel);
+ pack();
+ updateCoordinates();
+ }
+
+ private void updateCoordinates()
+ {
+ String content = String.format(
+ "x = %.2f%ny = %.2f%nangle = %.4f rad%ntarget = (%d, %d)",
+ model.getRobotPositionX(),
+ model.getRobotPositionY(),
+ model.getRobotDirection(),
+ model.getTargetPositionX(),
+ model.getTargetPositionY());
+ coordinatesContent.setText(content);
+ coordinatesContent.invalidate();
+ }
+
+ @Override
+ public void update(Observable observable, Object arg)
+ {
+ EventQueue.invokeLater(this::updateCoordinates);
+ }
+}
diff --git a/robots/src/gui/RobotModel.java b/robots/src/gui/RobotModel.java
new file mode 100644
index 0000000..b6cb919
--- /dev/null
+++ b/robots/src/gui/RobotModel.java
@@ -0,0 +1,180 @@
+package gui;
+
+import java.awt.Point;
+import java.util.Observable;
+
+public class RobotModel extends Observable
+{
+ private static final double DEFAULT_X = 100;
+ private static final double DEFAULT_Y = 100;
+ private static final double DEFAULT_DIRECTION = 0;
+ private static final int DEFAULT_TARGET_X = 150;
+ private static final int DEFAULT_TARGET_Y = 100;
+
+ private static final double MAX_VELOCITY = 0.1;
+ private static final double MAX_ANGULAR_VELOCITY = 0.001;
+ private static final double UPDATE_DURATION = 10;
+ private static final double TARGET_REACHED_DISTANCE = 0.5;
+
+ private double robotPositionX;
+ private double robotPositionY;
+ private double robotDirection;
+
+ private int targetPositionX;
+ private int targetPositionY;
+
+ public RobotModel()
+ {
+ this(DEFAULT_X, DEFAULT_Y, DEFAULT_DIRECTION, DEFAULT_TARGET_X, DEFAULT_TARGET_Y);
+ }
+
+ public RobotModel(double robotPositionX, double robotPositionY, double robotDirection,
+ int targetPositionX, int targetPositionY)
+ {
+ this.robotPositionX = robotPositionX;
+ this.robotPositionY = robotPositionY;
+ this.robotDirection = asNormalizedRadians(robotDirection);
+ this.targetPositionX = targetPositionX;
+ this.targetPositionY = targetPositionY;
+ }
+
+ public synchronized double getRobotPositionX()
+ {
+ return robotPositionX;
+ }
+
+ public synchronized double getRobotPositionY()
+ {
+ return robotPositionY;
+ }
+
+ public synchronized double getRobotDirection()
+ {
+ return robotDirection;
+ }
+
+ public synchronized int getTargetPositionX()
+ {
+ return targetPositionX;
+ }
+
+ public synchronized int getTargetPositionY()
+ {
+ return targetPositionY;
+ }
+
+ public synchronized void setTargetPosition(Point point)
+ {
+ targetPositionX = point.x;
+ targetPositionY = point.y;
+ notifyModelChanged();
+ }
+
+ public synchronized void update()
+ {
+ double distanceToTarget = distance(targetPositionX, targetPositionY, robotPositionX, robotPositionY);
+ if (distanceToTarget < TARGET_REACHED_DISTANCE)
+ {
+ return;
+ }
+
+ double angleToTarget = angleTo(robotPositionX, robotPositionY, targetPositionX, targetPositionY);
+ double angleDiff = asNormalizedRelativeRadians(angleToTarget - robotDirection);
+ double angularVelocity = 0;
+
+ if (angleDiff > 0)
+ {
+ angularVelocity = MAX_ANGULAR_VELOCITY;
+ }
+ else if (angleDiff < 0)
+ {
+ angularVelocity = -MAX_ANGULAR_VELOCITY;
+ }
+
+ moveRobot(MAX_VELOCITY, angularVelocity, UPDATE_DURATION);
+ notifyModelChanged();
+ }
+
+ private void notifyModelChanged()
+ {
+ setChanged();
+ notifyObservers();
+ }
+
+ private static double distance(double x1, double y1, double x2, double y2)
+ {
+ double diffX = x1 - x2;
+ double diffY = y1 - y2;
+ return Math.sqrt(diffX * diffX + diffY * diffY);
+ }
+
+ private static double angleTo(double fromX, double fromY, double toX, double toY)
+ {
+ double diffX = toX - fromX;
+ double diffY = toY - fromY;
+ return asNormalizedRadians(Math.atan2(diffY, diffX));
+ }
+
+ private static double applyLimits(double value, double min, double max)
+ {
+ if (value < min)
+ {
+ return min;
+ }
+ if (value > max)
+ {
+ return max;
+ }
+ return value;
+ }
+
+ private void moveRobot(double velocity, double angularVelocity, double duration)
+ {
+ velocity = applyLimits(velocity, 0, MAX_VELOCITY);
+ angularVelocity = applyLimits(angularVelocity, -MAX_ANGULAR_VELOCITY, MAX_ANGULAR_VELOCITY);
+
+ double newX = robotPositionX + velocity / angularVelocity *
+ (Math.sin(robotDirection + angularVelocity * duration) - Math.sin(robotDirection));
+ if (!Double.isFinite(newX))
+ {
+ newX = robotPositionX + velocity * duration * Math.cos(robotDirection);
+ }
+
+ double newY = robotPositionY - velocity / angularVelocity *
+ (Math.cos(robotDirection + angularVelocity * duration) - Math.cos(robotDirection));
+ if (!Double.isFinite(newY))
+ {
+ newY = robotPositionY + velocity * duration * Math.sin(robotDirection);
+ }
+
+ robotPositionX = newX;
+ robotPositionY = newY;
+ robotDirection = asNormalizedRadians(robotDirection + angularVelocity * duration);
+ }
+
+ static double asNormalizedRadians(double angle)
+ {
+ while (angle < 0)
+ {
+ angle += 2 * Math.PI;
+ }
+ while (angle >= 2 * Math.PI)
+ {
+ angle -= 2 * Math.PI;
+ }
+ return angle;
+ }
+
+ static double asNormalizedRelativeRadians(double angle)
+ {
+ while (angle <= -Math.PI)
+ {
+ angle += 2 * Math.PI;
+ }
+ while (angle > Math.PI)
+ {
+ angle -= 2 * Math.PI;
+ }
+ return angle;
+ }
+}
diff --git a/robots/src/test/gui/RobotModelTest.java b/robots/src/test/gui/RobotModelTest.java
new file mode 100644
index 0000000..5a11845
--- /dev/null
+++ b/robots/src/test/gui/RobotModelTest.java
@@ -0,0 +1,90 @@
+package gui;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.awt.Point;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Test;
+
+public class RobotModelTest
+{
+ private static final double EPS = 1e-6;
+
+ @Test
+ public void robotMovesTowardTargetAfterUpdate()
+ {
+ RobotModel model = new RobotModel(100, 100, 0, 150, 100);
+
+ model.update();
+
+ assertTrue(model.getRobotPositionX() > 100.0);
+ assertEquals(100.0, model.getRobotPositionY(), 1e-3);
+ }
+
+ @Test
+ public void robotDoesNotMoveWhenAlreadyAtTarget()
+ {
+ RobotModel model = new RobotModel(100, 100, 0, 100, 100);
+
+ model.update();
+
+ assertEquals(100.0, model.getRobotPositionX(), EPS);
+ assertEquals(100.0, model.getRobotPositionY(), EPS);
+ assertEquals(0.0, model.getRobotDirection(), EPS);
+ }
+
+ @Test
+ public void robotTurnsShortestWayAcrossZeroAngle()
+ {
+ RobotModel model = new RobotModel(100, 100, 2 * Math.PI - 0.001, 200, 100);
+
+ model.update();
+
+ assertTrue(model.getRobotDirection() < 0.1);
+ }
+
+ @Test
+ public void setTargetPositionNotifiesObservers()
+ {
+ RobotModel model = new RobotModel();
+ AtomicInteger notifications = new AtomicInteger(0);
+ model.addObserver(new Observer()
+ {
+ @Override
+ public void update(Observable observable, Object arg)
+ {
+ notifications.incrementAndGet();
+ }
+ });
+
+ model.setTargetPosition(new Point(300, 200));
+
+ assertEquals(1, notifications.get());
+ }
+
+ @Test
+ public void updateNotifiesObserversWhenRobotMoves()
+ {
+ RobotModel model = new RobotModel(100, 100, 0, 150, 100);
+ AtomicInteger notifications = new AtomicInteger(0);
+ model.addObserver((observable, arg) -> notifications.incrementAndGet());
+
+ model.update();
+
+ assertEquals(1, notifications.get());
+ }
+
+ @Test
+ public void relativeAngleNormalizationKeepsAngleInMinusPiToPi()
+ {
+ double normalized = RobotModel.asNormalizedRelativeRadians(-1.5 * Math.PI);
+
+ assertTrue(normalized > -Math.PI);
+ assertTrue(normalized <= Math.PI);
+ assertEquals(Math.PI / 2, normalized, EPS);
+ }
+}