diff --git a/robots/src/gui/AppStateManager.java b/robots/src/gui/AppStateManager.java new file mode 100644 index 0000000..1404641 --- /dev/null +++ b/robots/src/gui/AppStateManager.java @@ -0,0 +1,76 @@ +package gui; + +import java.beans.PropertyVetoException; +import javax.swing.JDesktopPane; +import javax.swing.JFrame; +import javax.swing.JInternalFrame; +import log.Logger; + +public class AppStateManager { + private final WindowConfigManager configManager; + + public AppStateManager() { + this.configManager = new WindowConfigManager(); + } + + public void load() { + configManager.load(); + Logger.debug("Менеджер состояний: загрузка конфигурации запущена"); + } + + public void save() { + configManager.save(); + } + + public void restoreMain(JFrame mainFrame, int defaultX, int defaultY, int defaultW, int defaultH) { + mainFrame.setBounds( + configManager.getInt("main.x", defaultX), + configManager.getInt("main.y", defaultY), + configManager.getInt("main.w", defaultW), + configManager.getInt("main.h", defaultH) + ); + mainFrame.setExtendedState(configManager.getInt("main.state", JFrame.NORMAL)); + } + + public void restoreInternalFrame(JInternalFrame frame, String prefix, int defaultX, int defaultY, int defaultW, int defaultH) { + int w = configManager.getInt(prefix + ".w", defaultW); + int h = configManager.getInt(prefix + ".h", defaultH); + int x = configManager.getInt(prefix + ".x", defaultX); + int y = configManager.getInt(prefix + ".y", defaultY); + + frame.setBounds(x, y, w, h); + + try { + if (configManager.getBool(prefix + ".max", false)) { + frame.setMaximum(true); + } else if (configManager.getBool(prefix + ".icon", false)) { + frame.setIcon(true); + } + } catch (PropertyVetoException e) { + Logger.error("Не удалось восстановить состояние окна '" + prefix + "': " + e.getMessage()); + } + } + + public void saveMain(JFrame mainFrame) { + configManager.saveMain( + mainFrame.getX(), mainFrame.getY(), + mainFrame.getWidth(), mainFrame.getHeight(), + mainFrame.getExtendedState() + ); + } + + public void saveInternalFrame(JInternalFrame frame, String prefix) { + configManager.saveInternal(prefix, + frame.getX(), frame.getY(), + frame.getWidth(), frame.getHeight(), + frame.isIcon(), frame.isMaximum() + ); + } + + public void saveAllFrames(JDesktopPane desktopPane) { + for (JInternalFrame frame : desktopPane.getAllFrames()) { + String prefix = frame.getTitle().replaceAll("\\s+", "_").toLowerCase(); + saveInternalFrame(frame, prefix); + } + } +} \ No newline at end of file diff --git a/robots/src/gui/GameVisualizer.java b/robots/src/gui/GameVisualizer.java index f82cfd8..785cd4c 100644 --- a/robots/src/gui/GameVisualizer.java +++ b/robots/src/gui/GameVisualizer.java @@ -4,207 +4,64 @@ import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.util.Timer; import java.util.TimerTask; - import javax.swing.JPanel; -public class GameVisualizer extends JPanel -{ - private final Timer m_timer = initTimer(); - - private static Timer initTimer() - { - Timer timer = new Timer("events generator", true); - return timer; - } - - private volatile double m_robotPositionX = 100; - private volatile double m_robotPositionY = 100; - private volatile double m_robotDirection = 0; +public class GameVisualizer extends JPanel implements RobotModel.Observer { + private final RobotModel model; + private final Timer timer = new Timer("events generator", true); - 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() - { - m_timer.schedule(new TimerTask() - { - @Override - public void run() - { - onRedrawEvent(); - } + public GameVisualizer(RobotModel model) { + this.model = model; + model.addObserver(this); // Подписываемся на обновления + + timer.schedule(new TimerTask() { + @Override public void run() { EventQueue.invokeLater(GameVisualizer.this::repaint); } }, 0, 50); - m_timer.schedule(new TimerTask() - { - @Override - public void run() - { - onModelUpdateEvent(); - } + + timer.schedule(new TimerTask() { + @Override public void run() { model.update(); } }, 0, 10); - addMouseListener(new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - setTargetPosition(e.getPoint()); - repaint(); + + addMouseListener(new MouseAdapter() { + @Override public void mouseClicked(MouseEvent e) { + model.setTarget(e.getX(), e.getY()); } }); setDoubleBuffered(true); } - protected void setTargetPosition(Point p) - { - m_targetPositionX = p.x; - m_targetPositionY = p.y; - } - - 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; - } + private static int round(double v) { return (int)(v + 0.5); } - 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); - } - @Override - public void paint(Graphics g) - { + public void paint(Graphics g) { super.paint(g); - Graphics2D g2d = (Graphics2D)g; - drawRobot(g2d, round(m_robotPositionX), round(m_robotPositionY), m_robotDirection); - drawTarget(g2d, m_targetPositionX, m_targetPositionY); + Graphics2D g2d = (Graphics2D) g; + drawRobot(g2d, round(model.getX()), round(model.getY()), model.getDir()); + drawTarget(g2d, model.getTargetX(), model.getTargetY()); } - - 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 dir) { + g.setTransform(AffineTransform.getRotateInstance(dir, x, y)); + g.setColor(Color.MAGENTA); g.fillOval(x-15, y-5, 30, 10); + g.setColor(Color.BLACK); g.drawOval(x-15, y-5, 30, 10); + g.setColor(Color.WHITE); g.fillOval(x+5, y-2, 5, 5); + g.setColor(Color.BLACK); g.drawOval(x+5, y-2, 5, 5); } - - 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); - g.setColor(Color.MAGENTA); - fillOval(g, robotCenterX, robotCenterY, 30, 10); - g.setColor(Color.BLACK); - drawOval(g, robotCenterX, robotCenterY, 30, 10); - g.setColor(Color.WHITE); - fillOval(g, robotCenterX + 10, robotCenterY, 5, 5); - g.setColor(Color.BLACK); - drawOval(g, robotCenterX + 10, robotCenterY, 5, 5); + + private void drawTarget(Graphics2D g, int x, int y) { + g.setTransform(new AffineTransform()); + g.setColor(Color.GREEN); g.fillOval(x-2, y-2, 5, 5); + g.setColor(Color.BLACK); g.drawOval(x-2, y-2, 5, 5); } - - private void drawTarget(Graphics2D g, int x, int y) - { - AffineTransform t = AffineTransform.getRotateInstance(0, 0, 0); - g.setTransform(t); - g.setColor(Color.GREEN); - fillOval(g, x, y, 5, 5); - g.setColor(Color.BLACK); - drawOval(g, x, y, 5, 5); + + @Override + public void onStateChanged(double x, double y, double dir, int tx, int ty) { + // Перерисовка при изменении модели + repaint(); } -} +} \ No newline at end of file diff --git a/robots/src/gui/GameWindow.java b/robots/src/gui/GameWindow.java index ecb63c0..9adfa15 100644 --- a/robots/src/gui/GameWindow.java +++ b/robots/src/gui/GameWindow.java @@ -1,20 +1,25 @@ package gui; import java.awt.BorderLayout; - import javax.swing.JInternalFrame; import javax.swing.JPanel; -public class GameWindow extends JInternalFrame -{ - private final GameVisualizer m_visualizer; - public GameWindow() - { +public class GameWindow extends JInternalFrame { + private final RobotModel model; + private final GameVisualizer visualizer; + private final RobotInfoWindow infoWindow; + + public GameWindow() { super("Игровое поле", true, true, true, true); - m_visualizer = new GameVisualizer(); + model = new RobotModel(); + visualizer = new GameVisualizer(model); + infoWindow = new RobotInfoWindow(model); + JPanel panel = new JPanel(new BorderLayout()); - panel.add(m_visualizer, BorderLayout.CENTER); + panel.add(visualizer, BorderLayout.CENTER); getContentPane().add(panel); pack(); } -} + + public RobotInfoWindow getInfoWindow() { return infoWindow; } +} \ No newline at end of file diff --git a/robots/src/gui/LogWindow.java b/robots/src/gui/LogWindow.java index 723d3e2..0275edb 100644 --- a/robots/src/gui/LogWindow.java +++ b/robots/src/gui/LogWindow.java @@ -3,48 +3,39 @@ import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.TextArea; - import javax.swing.JInternalFrame; import javax.swing.JPanel; - import log.LogChangeListener; import log.LogEntry; import log.LogWindowSource; -public class LogWindow extends JInternalFrame implements LogChangeListener -{ - private LogWindowSource m_logSource; - private TextArea m_logContent; +public class LogWindow extends JInternalFrame implements LogChangeListener { + private final LogWindowSource source; + private final TextArea content; - public LogWindow(LogWindowSource logSource) - { + public LogWindow(LogWindowSource source) { super("Протокол работы", true, true, true, true); - m_logSource = logSource; - m_logSource.registerListener(this); - m_logContent = new TextArea(""); - m_logContent.setSize(200, 500); - + this.source = source; + source.registerListener(this); + content = new TextArea(""); JPanel panel = new JPanel(new BorderLayout()); - panel.add(m_logContent, BorderLayout.CENTER); + panel.add(content, BorderLayout.CENTER); getContentPane().add(panel); pack(); - updateLogContent(); + updateContent(); } - private void updateLogContent() - { - StringBuilder content = new StringBuilder(); - for (LogEntry entry : m_logSource.all()) - { - content.append(entry.getMessage()).append("\n"); - } - m_logContent.setText(content.toString()); - m_logContent.invalidate(); + private void updateContent() { + StringBuilder sb = new StringBuilder(); + for (LogEntry e : source.all()) sb.append(e.getMessage()).append('\n'); + content.setText(sb.toString()); + content.invalidate(); } - - @Override - public void onLogChanged() - { - EventQueue.invokeLater(this::updateLogContent); + + @Override public void onLogChanged() { EventQueue.invokeLater(this::updateContent); } + + public void close() { + source.unregisterListener(this); + dispose(); } -} +} \ No newline at end of file diff --git a/robots/src/gui/MainApplicationFrame.java b/robots/src/gui/MainApplicationFrame.java index 62e943e..6ed15e4 100644 --- a/robots/src/gui/MainApplicationFrame.java +++ b/robots/src/gui/MainApplicationFrame.java @@ -1,156 +1,91 @@ package gui; - import java.awt.Dimension; import java.awt.Toolkit; -import java.awt.event.KeyEvent; - -import javax.swing.JDesktopPane; -import javax.swing.JFrame; -import javax.swing.JInternalFrame; -import javax.swing.JMenu; -import javax.swing.JMenuBar; -import javax.swing.JMenuItem; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; - +import javax.swing.*; import log.Logger; -/** - * Что требуется сделать: - * 1. Метод создания меню перегружен функционалом и трудно читается. - * Следует разделить его на серию более простых методов (или вообще выделить отдельный класс). - * - */ -public class MainApplicationFrame extends JFrame -{ +public class MainApplicationFrame extends JFrame { private final JDesktopPane desktopPane = new JDesktopPane(); - + private LogWindow logWindow; + private GameWindow gameWindow; + private final AppStateManager stateManager = new AppStateManager(); + + private static final int DEFAULT_INSET = 50; + private static final int DEFAULT_INTERNAL_W = 600; + private static final int DEFAULT_INTERNAL_H = 400; + private static final int DEFAULT_LOG_X = 220, DEFAULT_LOG_Y = 10; + private static final int DEFAULT_GAME_X = 10, DEFAULT_GAME_Y = 10; + private static final int DEFAULT_INFO_X = 430, DEFAULT_INFO_Y = 10; + private static final int DEFAULT_INFO_W = 230, DEFAULT_INFO_H = 110; + public MainApplicationFrame() { - //Make the big window be indented 50 pixels from each edge - //of the screen. - int inset = 50; + stateManager.load(); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - setBounds(inset, inset, - screenSize.width - inset*2, - screenSize.height - inset*2); + int defW = screenSize.width - DEFAULT_INSET * 2; + int defH = screenSize.height - DEFAULT_INSET * 2; + stateManager.restoreMain(this, DEFAULT_INSET, DEFAULT_INSET, defW, defH); setContentPane(desktopPane); - - - LogWindow logWindow = createLogWindow(); + + logWindow = createLogWindow(); + // Генерируем префикс из заголовка, чтобы он совпадал с сохранением + restoreWindowWithAutoPrefix(logWindow, DEFAULT_LOG_X, DEFAULT_LOG_Y, DEFAULT_INTERNAL_W, DEFAULT_INTERNAL_H); addWindow(logWindow); - GameWindow gameWindow = new GameWindow(); - gameWindow.setSize(400, 400); + gameWindow = new GameWindow(); + restoreWindowWithAutoPrefix(gameWindow, DEFAULT_GAME_X, DEFAULT_GAME_Y, DEFAULT_INTERNAL_W, DEFAULT_INTERNAL_H); addWindow(gameWindow); - setJMenuBar(generateMenuBar()); - setDefaultCloseOperation(EXIT_ON_CLOSE); + RobotInfoWindow infoWindow = gameWindow.getInfoWindow(); + restoreWindowWithAutoPrefix(infoWindow, DEFAULT_INFO_X, DEFAULT_INFO_Y, DEFAULT_INFO_W, DEFAULT_INFO_H); + addWindow(infoWindow); + + setJMenuBar(new MenuBarBuilder(this).buildMenuBar()); + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new java.awt.event.WindowAdapter() { + @Override public void windowClosing(java.awt.event.WindowEvent e) { exitApplication(); } + }); + Logger.debug("Главное окно инициализировано"); } - - protected LogWindow createLogWindow() - { - LogWindow logWindow = new LogWindow(Logger.getDefaultLogSource()); - logWindow.setLocation(10,10); - logWindow.setSize(300, 800); - setMinimumSize(logWindow.getSize()); - logWindow.pack(); + + // Вспомогательный метод: формирует ключ из заголовка окна и восстанавливает состояние + private void restoreWindowWithAutoPrefix(JInternalFrame frame, int defX, int defY, int defW, int defH) { + String prefix = frame.getTitle().replaceAll("\\s+", "_").toLowerCase(); + stateManager.restoreInternalFrame(frame, prefix, defX, defY, defW, defH); + } + + protected LogWindow createLogWindow() { + LogWindow lw = new LogWindow(Logger.getDefaultLogSource()); Logger.debug("Протокол работает"); - return logWindow; + return lw; } - - protected void addWindow(JInternalFrame frame) - { + + 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 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); - } + public void exitApplication() { + stateManager.saveMain(this); + stateManager.saveAllFrames(desktopPane); + stateManager.save(); + Logger.debug("Конфигурация сохранена"); - 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); + int res = JOptionPane.showConfirmDialog(this, "Вы действительно хотите выйти?", "Выход", JOptionPane.YES_NO_OPTION); + if (res == JOptionPane.YES_OPTION) { + Logger.debug("Приложение закрыто"); + System.exit(0); } - - menuBar.add(lookAndFeelMenu); - menuBar.add(testMenu); - return menuBar; } - - private void setLookAndFeel(String className) - { - try - { + + public void setLookAndFeel(String className) { + try { UIManager.setLookAndFeel(className); SwingUtilities.updateComponentTreeUI(this); - } - catch (ClassNotFoundException | InstantiationException - | IllegalAccessException | UnsupportedLookAndFeelException e) - { - // just ignore + Logger.debug("Тема изменена: " + className); + } catch (Exception e) { + Logger.error("Ошибка темы: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/robots/src/gui/MenuBarBuilder.java b/robots/src/gui/MenuBarBuilder.java new file mode 100644 index 0000000..51465df --- /dev/null +++ b/robots/src/gui/MenuBarBuilder.java @@ -0,0 +1,48 @@ +package gui; + +import java.awt.event.KeyEvent; +import javax.swing.*; +import log.Logger; + +public class MenuBarBuilder { + private final MainApplicationFrame frame; + public MenuBarBuilder(MainApplicationFrame frame) { this.frame = frame; } + + public JMenuBar buildMenuBar() { + JMenuBar menuBar = new JMenuBar(); + menuBar.add(createFileMenu()); + menuBar.add(createLookAndFeelMenu()); + menuBar.add(createTestMenu()); + return menuBar; + } + + private JMenu createFileMenu() { + JMenu menu = new JMenu("Файл"); + menu.setMnemonic(KeyEvent.VK_F); + JMenuItem exit = new JMenuItem("Выход"); + exit.setMnemonic(KeyEvent.VK_X); + exit.addActionListener(e -> { Logger.debug("Выход из приложения"); frame.exitApplication(); }); + menu.add(exit); + return menu; + } + + private JMenu createLookAndFeelMenu() { + JMenu menu = new JMenu("Режим отображения"); + menu.setMnemonic(KeyEvent.VK_V); + JMenuItem sys = new JMenuItem("Системная схема", KeyEvent.VK_S); + sys.addActionListener(e -> frame.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())); + JMenuItem cross = new JMenuItem("Универсальная схема", KeyEvent.VK_U); + cross.addActionListener(e -> frame.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName())); + menu.add(sys); menu.add(cross); + return menu; + } + + private JMenu createTestMenu() { + JMenu menu = new JMenu("Тесты"); + menu.setMnemonic(KeyEvent.VK_T); + JMenuItem logMsg = new JMenuItem("Сообщение в лог", KeyEvent.VK_L); + logMsg.addActionListener(e -> Logger.debug("Тестовое сообщение")); + menu.add(logMsg); + return menu; + } +} \ No newline at end of file diff --git a/robots/src/gui/RobotInfoWindow.java b/robots/src/gui/RobotInfoWindow.java new file mode 100644 index 0000000..5a506c6 --- /dev/null +++ b/robots/src/gui/RobotInfoWindow.java @@ -0,0 +1,53 @@ +package gui; + +import java.awt.BorderLayout; +import java.awt.EventQueue; +import javax.swing.JInternalFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +public class RobotInfoWindow extends JInternalFrame implements RobotModel.Observer { + private final RobotModel model; + private final JLabel lblPos; + private final JLabel lblTarget; + private final JLabel lblDir; + + public RobotInfoWindow(RobotModel model) { + super("Координаты робота", true, true, true, true); + this.model = model; + model.addObserver(this); + + JPanel panel = new JPanel(new BorderLayout(5, 5)); + lblPos = new JLabel("Позиция: (0, 0)"); + lblTarget = new JLabel("Цель: (0, 0)"); + lblDir = new JLabel("Направление: 0°"); + + panel.add(lblPos, BorderLayout.NORTH); + panel.add(lblTarget, BorderLayout.CENTER); + panel.add(lblDir, BorderLayout.SOUTH); + getContentPane().add(panel); + pack(); + setSize(230, 110); + + // Вызываем метод обновления меток + updateLabels(); + } + + // Переименовано из updateUI, чтобы не конфликтовать со системным методом Swing + private void updateLabels() { + lblPos.setText(String.format("Позиция: (%.1f, %.1f)", model.getX(), model.getY())); + lblTarget.setText(String.format("Цель: (%d, %d)", model.getTargetX(), model.getTargetY())); + lblDir.setText(String.format("Направление: %.1f°", Math.toDegrees(model.getDir()))); + } + + @Override + public void onStateChanged(double x, double y, double dir, int tx, int ty) { + // Обновляем метки в потоке событий при изменении модели + EventQueue.invokeLater(this::updateLabels); + } + + public void close() { + model.removeObserver(this); + dispose(); + } +} \ No newline at end of file diff --git a/robots/src/gui/RobotModel.java b/robots/src/gui/RobotModel.java new file mode 100644 index 0000000..2f32bfb --- /dev/null +++ b/robots/src/gui/RobotModel.java @@ -0,0 +1,81 @@ +package gui; + +import java.util.ArrayList; +import java.util.List; + +public class RobotModel { + public interface Observer { + void onStateChanged(double x, double y, double dir, int tx, int ty); + } + + private final List observers = new ArrayList<>(); + private volatile double x = 100, y = 100, dir = 0; + private volatile int targetX = 150, targetY = 100; + + private static final double MAX_V = 0.1; + private static final double MAX_W = 0.001; + + public void addObserver(Observer o) { + synchronized(observers) { observers.add(o); } + } + public void removeObserver(Observer o) { + synchronized(observers) { observers.remove(o); } + } + + private void notifyObservers() { + List snapshot; + synchronized(observers) { snapshot = new ArrayList<>(observers); } + for (Observer o : snapshot) { + o.onStateChanged(x, y, dir, targetX, targetY); + } + } + + public double getX() { return x; } + public double getY() { return y; } + public double getDir() { return dir; } + public int getTargetX() { return targetX; } + public int getTargetY() { return targetY; } + + public void setTarget(int tx, int ty) { + targetX = tx; + targetY = ty; + } + + public void update() { + double dx = targetX - x; + double dy = targetY - y; + double dist = Math.hypot(dx, dy); + if (dist < 0.5) return; + + double angleToTarget = Math.atan2(dy, dx); + while (angleToTarget < 0) angleToTarget += 2 * Math.PI; + while (angleToTarget >= 2 * Math.PI) angleToTarget -= 2 * Math.PI; + + double diff = angleToTarget - dir; + // Исправление бага: всегда поворачиваем кратчайшим путем + while (diff > Math.PI) diff -= 2 * Math.PI; + while (diff < -Math.PI) diff += 2 * Math.PI; + + double angularV = Math.abs(diff) < 0.01 ? 0 : Math.signum(diff) * MAX_W; + move(MAX_V, angularV, 10); + } + + private void move(double v, double w, double dt) { + double newX, newY; + if (Math.abs(w) < 1e-6) { + newX = x + v * dt * Math.cos(dir); + newY = y + v * dt * Math.sin(dir); + } else { + newX = x + (v / w) * (Math.sin(dir + w * dt) - Math.sin(dir)); + newY = y - (v / w) * (Math.cos(dir + w * dt) - Math.cos(dir)); + } + x = newX; + y = newY; + dir = dir + w * dt; + while (dir < 0) dir += 2 * Math.PI; + while (dir >= 2 * Math.PI) dir -= 2 * Math.PI; + + notifyObservers(); + // Logger.debug(...) удален, чтобы не засорять протокол + } +} \ No newline at end of file diff --git a/robots/src/gui/RobotsProgram.java b/robots/src/gui/RobotsProgram.java index ae0930a..05896da 100644 --- a/robots/src/gui/RobotsProgram.java +++ b/robots/src/gui/RobotsProgram.java @@ -1,25 +1,19 @@ package gui; -import java.awt.Frame; - import javax.swing.SwingUtilities; import javax.swing.UIManager; -public class RobotsProgram -{ +public class RobotsProgram { public static void main(String[] args) { - try { - UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel"); -// UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); -// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); -// UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); - } catch (Exception e) { - e.printStackTrace(); - } - SwingUtilities.invokeLater(() -> { - MainApplicationFrame frame = new MainApplicationFrame(); - frame.pack(); - frame.setVisible(true); - frame.setExtendedState(Frame.MAXIMIZED_BOTH); - }); - }} + try { + UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel"); + } catch (Exception e) { + e.printStackTrace(); + } + + SwingUtilities.invokeLater(() -> { + MainApplicationFrame frame = new MainApplicationFrame(); + frame.setVisible(true); + }); + } +} \ No newline at end of file diff --git a/robots/src/gui/WindowConfigManager.java b/robots/src/gui/WindowConfigManager.java new file mode 100644 index 0000000..5e96182 --- /dev/null +++ b/robots/src/gui/WindowConfigManager.java @@ -0,0 +1,65 @@ +package gui; + +import java.io.*; +import java.util.Properties; +import log.Logger; + +public class WindowConfigManager { + private static final String FILE_NAME = ".game_window_config.properties"; + private final Properties props = new Properties(); + private final File file; + + public WindowConfigManager() { + file = new File(System.getProperty("user.home"), FILE_NAME); + } + + public void load() { + if (file.exists()) { + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + Logger.debug("Конфиг загружен"); + } catch (IOException e) { + Logger.error("Ошибка загрузки конфига: " + e.getMessage()); + } + } else { + Logger.debug("Конфиг не найден, используются дефолтные значения"); + } + } + + public void save() { + try (FileOutputStream fos = new FileOutputStream(file)) { + props.store(fos, "App Config"); + Logger.debug("Конфиг сохранен"); + } catch (IOException e) { + Logger.error("Ошибка сохранения конфига: " + e.getMessage()); + } + } + + public int getInt(String key, int def) { + String val = props.getProperty(key); + try { return val != null ? Integer.parseInt(val) : def; } + catch (NumberFormatException e) { Logger.error("Ошибка парсинга " + key); return def; } + } + + public boolean getBool(String key, boolean def) { + String val = props.getProperty(key); + return val != null ? Boolean.parseBoolean(val) : def; + } + + public void saveMain(int x, int y, int w, int h, int state) { + props.setProperty("main.x", String.valueOf(x)); + props.setProperty("main.y", String.valueOf(y)); + props.setProperty("main.w", String.valueOf(w)); + props.setProperty("main.h", String.valueOf(h)); + props.setProperty("main.state", String.valueOf(state)); + } + + public void saveInternal(String prefix, int x, int y, int w, int h, boolean icon, boolean max) { + props.setProperty(prefix + ".x", String.valueOf(x)); + props.setProperty(prefix + ".y", String.valueOf(y)); + props.setProperty(prefix + ".w", String.valueOf(w)); + props.setProperty(prefix + ".h", String.valueOf(h)); + props.setProperty(prefix + ".icon", String.valueOf(icon)); + props.setProperty(prefix + ".max", String.valueOf(max)); + } +} \ No newline at end of file diff --git a/robots/src/log/LogWindowSource.java b/robots/src/log/LogWindowSource.java index ca0ce44..acfca54 100644 --- a/robots/src/log/LogWindowSource.java +++ b/robots/src/log/LogWindowSource.java @@ -2,88 +2,60 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.List; -/** - * Что починить: - * 1. Этот класс порождает утечку ресурсов (связанные слушатели оказываются - * удерживаемыми в памяти) - * 2. Этот класс хранит активные сообщения лога, но в такой реализации он - * их лишь накапливает. Надо же, чтобы количество сообщений в логе было ограничено - * величиной m_iQueueLength (т.е. реально нужна очередь сообщений - * ограниченного размера) - */ -public class LogWindowSource -{ - private int m_iQueueLength; - - private ArrayList m_messages; - private final ArrayList m_listeners; - private volatile LogChangeListener[] m_activeListeners; - - public LogWindowSource(int iQueueLength) - { - m_iQueueLength = iQueueLength; - m_messages = new ArrayList(iQueueLength); - m_listeners = new ArrayList(); +public class LogWindowSource { + private final int maxQueueLength; + private final List messages = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private volatile LogChangeListener[] activeListeners; + + public LogWindowSource(int queueLength) { + this.maxQueueLength = queueLength; } - - public void registerListener(LogChangeListener listener) - { - synchronized(m_listeners) - { - m_listeners.add(listener); - m_activeListeners = null; + + public void registerListener(LogChangeListener listener) { + synchronized(listeners) { + listeners.add(listener); + activeListeners = null; } } - - public void unregisterListener(LogChangeListener listener) - { - synchronized(m_listeners) - { - m_listeners.remove(listener); - m_activeListeners = null; + + public void unregisterListener(LogChangeListener listener) { + synchronized(listeners) { + listeners.remove(listener); + activeListeners = null; } } - - public void append(LogLevel logLevel, String strMessage) - { - LogEntry entry = new LogEntry(logLevel, strMessage); - m_messages.add(entry); - LogChangeListener [] activeListeners = m_activeListeners; - if (activeListeners == null) - { - synchronized (m_listeners) - { - if (m_activeListeners == null) - { - activeListeners = m_listeners.toArray(new LogChangeListener [0]); - m_activeListeners = activeListeners; - } - } - } - for (LogChangeListener listener : activeListeners) - { - listener.onLogChanged(); + + public void append(LogLevel level, String message) { + synchronized(listeners) { + messages.add(new LogEntry(level, message)); + while (messages.size() > maxQueueLength) messages.remove(0); } - } - - public int size() - { - return m_messages.size(); + notifyListeners(); } - public Iterable range(int startFrom, int count) - { - if (startFrom < 0 || startFrom >= m_messages.size()) - { - return Collections.emptyList(); + private void notifyListeners() { + LogChangeListener[] current = activeListeners; + if (current == null) { + synchronized(listeners) { + if (activeListeners == null) { + current = listeners.toArray(new LogChangeListener[0]); + activeListeners = current; + } + } + } + for (LogChangeListener l : current) { + try { l.onLogChanged(); } catch (Exception ignored) {} } - int indexTo = Math.min(startFrom + count, m_messages.size()); - return m_messages.subList(startFrom, indexTo); } - public Iterable all() - { - return m_messages; + public int size() { return messages.size(); } + public Iterable all() { return new ArrayList<>(messages); } + public Iterable range(int from, int count) { + if (from < 0 || from >= messages.size()) return Collections.emptyList(); + int to = Math.min(from + count, messages.size()); + return messages.subList(from, to); } -} +} \ No newline at end of file