From 77a4590a4b78e6477a06700edb2e8bf186b6edac Mon Sep 17 00:00:00 2001 From: SashaRey Date: Sat, 7 Mar 2026 18:03:42 +0500 Subject: [PATCH 1/2] Fix MainApplicationFrame and ignore IDE output --- robots/src/gui/MainApplicationFrame.java | 209 ++++++++++++++++------- 1 file changed, 149 insertions(+), 60 deletions(-) diff --git a/robots/src/gui/MainApplicationFrame.java b/robots/src/gui/MainApplicationFrame.java index 62e943e..390be51 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,100 +18,90 @@ 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(); - + + private static final String CONFIG_FILE_PATH = + System.getProperty("user.home") + "/robot_app.properties"; + + private LogWindow logWindow; + private GameWindow gameWindow; + public MainApplicationFrame() { - //Make the big window be indented 50 pixels from each edge - //of the screen. - int inset = 50; + 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(); + gameWindow.setSize(400, 400); addWindow(gameWindow); + 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.setLocation(10, 10); logWindow.setSize(300, 800); setMinimumSize(logWindow.getSize()); logWindow.pack(); Logger.debug("Протокол работает"); return logWindow; } - + 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) -> { @@ -114,7 +112,7 @@ private JMenuBar generateMenuBar() } { - JMenuItem crossplatformLookAndFeel = new JMenuItem("Универсальная схема", KeyEvent.VK_S); + JMenuItem crossplatformLookAndFeel = new JMenuItem("Универсальная схема", KeyEvent.VK_U); crossplatformLookAndFeel.addActionListener((event) -> { setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); this.invalidate(); @@ -126,9 +124,9 @@ private JMenuBar generateMenuBar() testMenu.setMnemonic(KeyEvent.VK_T); testMenu.getAccessibleContext().setAccessibleDescription( "Тестовые команды"); - + { - JMenuItem addLogMessageItem = new JMenuItem("Сообщение в лог", KeyEvent.VK_S); + JMenuItem addLogMessageItem = new JMenuItem("Сообщение в лог", KeyEvent.VK_L); addLogMessageItem.addActionListener((event) -> { Logger.debug("Новая строка"); }); @@ -139,7 +137,7 @@ private JMenuBar generateMenuBar() menuBar.add(testMenu); return menuBar; } - + private void setLookAndFeel(String className) { try @@ -148,9 +146,100 @@ 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); + + 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); + } + 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()); } } -} +} \ No newline at end of file From b1ad6532b62b9bf622c59ae6d032a2407975e452 Mon Sep 17 00:00:00 2001 From: SashaRey Date: Tue, 17 Mar 2026 22:09:22 +0500 Subject: [PATCH 2/2] Task 3: MVC refactoring, robot model and coordinates window with tests --- robots/.idea/.gitignore | 8 + robots/.idea/.name | 1 + robots/.idea/codeStyles/codeStyleConfig.xml | 5 + robots/.idea/misc.xml | 6 + robots/.idea/vcs.xml | 6 + robots/src/gui/GameVisualizer.java | 173 +++++-------------- robots/src/gui/GameWindow.java | 9 +- robots/src/gui/MainApplicationFrame.java | 76 +++++---- robots/src/gui/RobotCoordinatesWindow.java | 52 ++++++ robots/src/gui/RobotModel.java | 180 ++++++++++++++++++++ robots/src/test/gui/RobotModelTest.java | 90 ++++++++++ 11 files changed, 441 insertions(+), 165 deletions(-) create mode 100644 robots/.idea/.gitignore create mode 100644 robots/.idea/.name create mode 100644 robots/.idea/codeStyles/codeStyleConfig.xml create mode 100644 robots/.idea/misc.xml create mode 100644 robots/.idea/vcs.xml create mode 100644 robots/src/gui/RobotCoordinatesWindow.java create mode 100644 robots/src/gui/RobotModel.java create mode 100644 robots/src/test/gui/RobotModelTest.java 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 390be51..e07d9c7 100644 --- a/robots/src/gui/MainApplicationFrame.java +++ b/robots/src/gui/MainApplicationFrame.java @@ -33,14 +33,18 @@ public class MainApplicationFrame extends JFrame 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() { + public MainApplicationFrame() + { int inset = 50; Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); setBounds(inset, inset, - screenSize.width - inset * 2, + screenSize.width - inset * 2, screenSize.height - inset * 2); setContentPane(desktopPane); @@ -48,18 +52,26 @@ public MainApplicationFrame() { logWindow = createLogWindow(); addWindow(logWindow); - gameWindow = new GameWindow(); + 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(DO_NOTHING_ON_CLOSE); - addWindowListener(new WindowAdapter() { + addWindowListener(new WindowAdapter() + { @Override - public void windowClosing(WindowEvent e) { + public void windowClosing(WindowEvent e) + { exitApplication(); } }); @@ -67,13 +79,13 @@ public void windowClosing(WindowEvent e) { 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) @@ -102,36 +114,28 @@ private JMenuBar generateMenuBar() lookAndFeelMenu.getAccessibleContext().setAccessibleDescription( "Управление режимом отображения приложения"); - { - JMenuItem systemLookAndFeel = new JMenuItem("Системная схема", KeyEvent.VK_S); - systemLookAndFeel.addActionListener((event) -> { - setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - this.invalidate(); - }); - lookAndFeelMenu.add(systemLookAndFeel); - } + 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()); - this.invalidate(); - }); - lookAndFeelMenu.add(crossplatformLookAndFeel); - } + 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_L); - 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); @@ -177,6 +181,7 @@ private void saveWindowSettings() saveWindowProperties(properties, "log", logWindow); saveWindowProperties(properties, "game", gameWindow); + saveWindowProperties(properties, "coordinates", robotCoordinatesWindow); try (FileOutputStream outputStream = new FileOutputStream(CONFIG_FILE_PATH)) { @@ -198,6 +203,7 @@ private void restoreWindowSettings() restoreWindowProperties(properties, "log", logWindow); restoreWindowProperties(properties, "game", gameWindow); + restoreWindowProperties(properties, "coordinates", robotCoordinatesWindow); } catch (IOException e) { @@ -242,4 +248,4 @@ private void restoreWindowProperties(Properties properties, String prefix, JInte Logger.error("Не удалось восстановить состояние окна: " + frame.getTitle()); } } -} \ No newline at end of file +} 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); + } +}