diff --git a/.gitignore b/.gitignore index 2c6eb38..3c86586 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,40 @@ /.metadata /robots/.settings /robots/bin -eclipse.bat \ No newline at end of file +eclipse.bat +out/ +/robots/.classpath +/robots/.project +# Translations +*.mo + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# IDEA +.idea/ +*.iml + +# Maven +target/ \ No newline at end of file diff --git a/resources/Messages_en.properties b/resources/Messages_en.properties new file mode 100644 index 0000000..3830e3b --- /dev/null +++ b/resources/Messages_en.properties @@ -0,0 +1,34 @@ +# Main menu +menu.file=File +menu.exit=Exit +menu.lookandfeel=Look & Feel +menu.lookandfeel.system=System +menu.lookandfeel.cross=Cross-platform +menu.test=Tests +menu.test.log=Log message +menu.windows=Windows +menu.windows.showLog=Show log +menu.windows.showGame=Show game field +menu.windows.showCoord=Show coordinates +menu.language=Language +menu.language.russian=Russian +menu.language.english=English + +# Window titles +window.log.title=Log window +window.game.title=Game field +window.coord.title=Robot coordinates + +# Dialogs +exit.confirm.title=Exit confirmation +exit.confirm.message=Do you really want to exit the application? +option.yes=Yes +option.no=No + +# Log +log.start=Log works +log.newline=New line + +# Coordinates format +coord.position=Position: ({0,number,#.#}, {1,number,#.#}) +coord.direction=Direction: {0,number,#.#}\u00B0 \ No newline at end of file diff --git a/resources/Messages_ru.properties b/resources/Messages_ru.properties new file mode 100644 index 0000000..677420e --- /dev/null +++ b/resources/Messages_ru.properties @@ -0,0 +1,34 @@ +# Main menu +menu.file=\u0424\u0430\u0439\u043b +menu.exit=\u0412\u044b\u0445\u043e\u0434 +menu.lookandfeel=\u0420\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f +menu.lookandfeel.system=\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u0430\u044f \u0441\u0445\u0435\u043c\u0430 +menu.lookandfeel.cross=\u0423\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0445\u0435\u043c\u0430 +menu.test=\u0422\u0435\u0441\u0442\u044b +menu.test.log=\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432 \u043b\u043e\u0433 +menu.windows=\u041e\u043a\u043d\u0430 +menu.windows.showLog=\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b +menu.windows.showGame=\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0433\u0440\u043e\u0432\u043e\u0435 \u043f\u043e\u043b\u0435 +menu.windows.showCoord=\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b +menu.language=\u042f\u0437\u044b\u043a +menu.language.russian=\u0420\u0443\u0441\u0441\u043a\u0438\u0439 +menu.language.english=\u0410\u043D\u0433\u043B\u0438\u0439\u0441\u043A\u0438\u0439 + +# Window titles +window.log.title=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0440\u0430\u0431\u043e\u0442\u044b +window.game.title=\u0418\u0433\u0440\u043e\u0432\u043e\u0435 \u043f\u043e\u043b\u0435 +window.coord.title=\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0440\u043e\u0431\u043e\u0442\u0430 + +# Dialogs +exit.confirm.title=\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0432\u044b\u0445\u043e\u0434\u0430 +exit.confirm.message=\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u0439\u0442\u0438 \u0438\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f? +option.yes=\u0414\u0430 +option.no=\u041D\u0435\u0442 + +# Log +log.start=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 +log.newline=\u041d\u043e\u0432\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430 + +# Coordinates format +coord.position=\u041f\u043e\u0437\u0438\u0446\u0438\u044f: ({0,number,#.#}, {1,number,#.#}) +coord.direction=\u041d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435: {0,number,#.#}\u00b0 \ No newline at end of file diff --git a/robots/src/collections/BoundedCircularQueue.java b/robots/src/collections/BoundedCircularQueue.java new file mode 100644 index 0000000..22e8c85 --- /dev/null +++ b/robots/src/collections/BoundedCircularQueue.java @@ -0,0 +1,115 @@ +package collections; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class BoundedCircularQueue extends AbstractQueue { + private final int capacity; + private final Object[] buffer; + private final AtomicInteger head = new AtomicInteger(0); + private final AtomicInteger size = new AtomicInteger(0); + + public BoundedCircularQueue(int capacity) { + if (capacity <= 0) throw new IllegalArgumentException("Capacity must be positive"); + this.capacity = capacity; + this.buffer = new Object[capacity]; + } + + @Override + public boolean offer(E e) { + if (e == null) throw new NullPointerException(); + while (true) { + int currentSize = size.get(); + if (currentSize < capacity) { + if (size.compareAndSet(currentSize, currentSize + 1)) { + int pos = (head.get() + currentSize) % capacity; + buffer[pos] = e; + return true; + } + } else { + int oldHead = head.get(); + int newHead = (oldHead + 1) % capacity; + if (head.compareAndSet(oldHead, newHead)) { + buffer[oldHead] = e; + return true; + } + } + } + } + + @Override + public E poll() { + while (true) { + int currentSize = size.get(); + if (currentSize == 0) return null; + if (size.compareAndSet(currentSize, currentSize - 1)) { + int idx = head.getAndUpdate(h -> (h + 1) % capacity); + @SuppressWarnings("unchecked") + E result = (E) buffer[idx]; + buffer[idx] = null; + return result; + } + } + } + + @Override + public E peek() { + int currentSize = size.get(); + if (currentSize == 0) return null; + @SuppressWarnings("unchecked") + E result = (E) buffer[head.get()]; + return result; + } + + @Override + public int size() { + return size.get(); + } + + @Override + public Iterator iterator() { + return snapshot().iterator(); + } + + public List getAll() { + return snapshot(); + } + + public List getRange(int start, int end) { + int currentSize = size.get(); + if (start < 0 || end > currentSize || start > end) + throw new IndexOutOfBoundsException("Invalid range: " + start + ".." + end); + int currentHead = head.get(); + List result = new ArrayList<>(end - start); + for (int i = start; i < end; i++) { + @SuppressWarnings("unchecked") + E element = (E) buffer[(currentHead + i) % capacity]; + result.add(element); + } + return Collections.unmodifiableList(result); + } + + private List snapshot() { + int currentHead = head.get(); + int currentSize = size.get(); + List result = new ArrayList<>(currentSize); + for (int i = 0; i < currentSize; i++) { + @SuppressWarnings("unchecked") + E element = (E) buffer[(currentHead + i) % capacity]; + result.add(element); + } + return Collections.unmodifiableList(result); + } + + public void clear() { + while (true) { + int oldSize = size.get(); + if (oldSize == 0) return; + if (size.compareAndSet(oldSize, 0)) { + head.set(0); + Arrays.fill(buffer, null); + return; + } + } + } +} diff --git a/robots/src/collections/BoundedCircularQueueTest.java b/robots/src/collections/BoundedCircularQueueTest.java new file mode 100644 index 0000000..0cb5b6f --- /dev/null +++ b/robots/src/collections/BoundedCircularQueueTest.java @@ -0,0 +1,64 @@ +package collections; + +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class BoundedCircularQueueTest { + + @Test + void testOfferAndPoll() { + BoundedCircularQueue q = new BoundedCircularQueue<>(3); + assertTrue(q.offer("A")); + assertTrue(q.offer("B")); + assertTrue(q.offer("C")); + assertEquals(3, q.size()); + assertEquals("A", q.poll()); + assertEquals("B", q.peek()); + assertEquals(2, q.size()); + } + + @Test + void testOverflow() { + BoundedCircularQueue q = new BoundedCircularQueue<>(3); + q.offer("A"); + q.offer("B"); + q.offer("C"); + q.offer("D"); // перезаписывает A + assertEquals(3, q.size()); + assertEquals("B", q.poll()); + assertEquals("C", q.poll()); + assertEquals("D", q.poll()); + assertNull(q.poll()); + } + + @Test + void testGetAll() { + BoundedCircularQueue q = new BoundedCircularQueue<>(5); + for (int i = 0; i < 5; i++) q.offer(i); + assertEquals(List.of(0, 1, 2, 3, 4), q.getAll()); + } + + @Test + void testGetRange() { + BoundedCircularQueue q = new BoundedCircularQueue<>(5); + q.offer("a"); + q.offer("b"); + q.offer("c"); + q.offer("d"); + assertEquals(List.of("b", "c"), q.getRange(1, 3)); + } + + @Test + void testIteratorSnapshot() { + BoundedCircularQueue q = new BoundedCircularQueue<>(3); + q.offer("X"); + q.offer("Y"); + Iterator it = q.iterator(); + q.offer("Z"); // добавление не ломает итератор + assertTrue(it.hasNext()); + assertEquals("X", it.next()); + assertEquals("Y", it.next()); + assertFalse(it.hasNext()); + } +} diff --git a/robots/src/config/ConfigManager.java b/robots/src/config/ConfigManager.java new file mode 100644 index 0000000..a4b0b86 --- /dev/null +++ b/robots/src/config/ConfigManager.java @@ -0,0 +1,89 @@ +package config; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; +import log.Logger; + +public class ConfigManager { + private static final String CONFIG_FILE = System.getProperty("user.home") + File.separator + ".robot-config.properties"; + private final Properties props = new Properties(); + + public void load() { + File file = new File(CONFIG_FILE); + if (!file.exists()) return; + try (FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } catch (IOException e) { + Logger.debug("Не удалось загрузить конфигурацию: " + e.getMessage()); + } + } + + public void save() { + try (FileOutputStream fos = new FileOutputStream(CONFIG_FILE)) { + props.store(fos, "Robot program configuration"); + } catch (IOException e) { + Logger.debug("Не удалось сохранить конфигурацию: " + e.getMessage()); + } + } + + public void setMainWindowBounds(int x, int y, int width, int height, int state) { + setProperty("main.x", x); + setProperty("main.y", y); + setProperty("main.width", width); + setProperty("main.height", height); + setProperty("main.state", state); + } + + public int getMainWindowX(int defaultValue) { return getIntProperty("main.x", defaultValue); } + public int getMainWindowY(int defaultValue) { return getIntProperty("main.y", defaultValue); } + public int getMainWindowWidth(int defaultValue) { return getIntProperty("main.width", defaultValue); } + public int getMainWindowHeight(int defaultValue) { return getIntProperty("main.height", defaultValue); } + public int getMainWindowState(int defaultValue) { return getIntProperty("main.state", defaultValue); } + + public void setWindowBounds(String windowName, int x, int y, int width, int height, boolean icon, boolean maximized) { + setProperty(key(windowName, "x"), x); + setProperty(key(windowName, "y"), y); + setProperty(key(windowName, "width"), width); + setProperty(key(windowName, "height"), height); + setProperty(key(windowName, "icon"), icon); + setProperty(key(windowName, "maximized"), maximized); + } + + public boolean hasWindow(String windowName) { + return props.containsKey(key(windowName, "x")); + } + + public int getWindowX(String windowName, int defaultValue) { return getIntProperty(key(windowName, "x"), defaultValue); } + public int getWindowY(String windowName, int defaultValue) { return getIntProperty(key(windowName, "y"), defaultValue); } + public int getWindowWidth(String windowName, int defaultValue) { return getIntProperty(key(windowName, "width"), defaultValue); } + public int getWindowHeight(String windowName, int defaultValue) { return getIntProperty(key(windowName, "height"), defaultValue); } + public boolean getWindowIcon(String windowName, boolean defaultValue) { return getBooleanProperty(key(windowName, "icon"), defaultValue); } + public boolean getWindowMaximized(String windowName, boolean defaultValue) { return getBooleanProperty(key(windowName, "maximized"), defaultValue); } + + public void setWindowVisible(String windowName, boolean visible) { + setProperty(key(windowName, "visible"), visible); + } + + public boolean getWindowVisible(String windowName, boolean defaultValue) { + return getBooleanProperty(key(windowName, "visible"), defaultValue); + } + + + private String key(String windowName, String suffix) { return windowName + "." + suffix; } + private void setProperty(String key, int value) { props.setProperty(key, Integer.toString(value)); } + private void setProperty(String key, boolean value) { props.setProperty(key, Boolean.toString(value)); } + + private int getIntProperty(String key, int defaultValue) { + String val = props.getProperty(key); + if (val == null) return defaultValue; + try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultValue; } + } + + private boolean getBooleanProperty(String key, boolean defaultValue) { + String val = props.getProperty(key); + return val == null ? defaultValue : Boolean.parseBoolean(val); + } +} diff --git a/robots/src/gui/GameVisualizer.java b/robots/src/gui/GameVisualizer.java index f82cfd8..d38f107 100644 --- a/robots/src/gui/GameVisualizer.java +++ b/robots/src/gui/GameVisualizer.java @@ -1,210 +1,70 @@ package gui; -import java.awt.Color; -import java.awt.EventQueue; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; +import model.RobotController; +import model.RobotModel; +import javax.swing.*; +import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; -import java.util.Timer; -import java.util.TimerTask; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; -import javax.swing.JPanel; +public class GameVisualizer extends JPanel implements PropertyChangeListener { + private final RobotModel model; -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; - - 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(); - } - }, 0, 50); - m_timer.schedule(new TimerTask() - { + public GameVisualizer(RobotModel model, RobotController controller) { + this.model = model; + model.addPropertyChangeListener(this); + addMouseListener(new MouseAdapter() { @Override - public void run() - { - onModelUpdateEvent(); - } - }, 0, 10); - addMouseListener(new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - setTargetPosition(e.getPoint()); - repaint(); + public void mouseClicked(MouseEvent e) { + controller.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; + @Override + public void propertyChange(PropertyChangeEvent evt) { + repaint(); } - 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) - { - super.paint(g); - Graphics2D g2d = (Graphics2D)g; - drawRobot(g2d, round(m_robotPositionX), round(m_robotPositionY), m_robotDirection); - drawTarget(g2d, m_targetPositionX, m_targetPositionY); + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2d = (Graphics2D) g; + drawRobot(g2d, (int)model.getRobotX(), (int)model.getRobotY(), model.getRobotDirection()); + 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 direction) - { - int robotCenterX = round(m_robotPositionX); - int robotCenterY = round(m_robotPositionY); - AffineTransform t = AffineTransform.getRotateInstance(direction, robotCenterX, robotCenterY); + + private void drawRobot(Graphics2D g, int x, int y, double direction) { + AffineTransform t = AffineTransform.getRotateInstance(direction, x, y); g.setTransform(t); 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(new AffineTransform()); } - - private void drawTarget(Graphics2D g, int x, int y) - { - AffineTransform t = AffineTransform.getRotateInstance(0, 0, 0); - g.setTransform(t); + + private void drawTarget(Graphics2D g, int x, int y) { + g.setTransform(new AffineTransform()); g.setColor(Color.GREEN); fillOval(g, x, y, 5, 5); g.setColor(Color.BLACK); drawOval(g, x, y, 5, 5); } + + private void fillOval(Graphics g, int cx, int cy, int w, int h) { + g.fillOval(cx - w/2, cy - h/2, w, h); + } + + private void drawOval(Graphics g, int cx, int cy, int w, int h) { + g.drawOval(cx - w/2, cy - h/2, w, h); + } } diff --git a/robots/src/gui/GameWindow.java b/robots/src/gui/GameWindow.java index ecb63c0..c936533 100644 --- a/robots/src/gui/GameWindow.java +++ b/robots/src/gui/GameWindow.java @@ -1,19 +1,16 @@ package gui; -import java.awt.BorderLayout; +import model.RobotController; +import model.RobotModel; +import javax.swing.*; +import java.awt.*; -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 { + public GameWindow(RobotModel model, RobotController controller) { super("Игровое поле", true, true, true, true); - m_visualizer = new GameVisualizer(); + GameVisualizer visualizer = new GameVisualizer(model, controller); 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/LogWindow.java b/robots/src/gui/LogWindow.java index 723d3e2..050adca 100644 --- a/robots/src/gui/LogWindow.java +++ b/robots/src/gui/LogWindow.java @@ -1,50 +1,50 @@ package gui; -import java.awt.BorderLayout; -import java.awt.EventQueue; -import java.awt.TextArea; - -import javax.swing.JInternalFrame; -import javax.swing.JPanel; - +import localization.LocalizationManager; import log.LogChangeListener; import log.LogEntry; import log.LogWindowSource; +import log.Logger; + +import javax.swing.*; +import java.awt.*; -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 logSource; + private final TextArea logContent; + private final LocalizationManager loc = LocalizationManager.getInstance(); - public LogWindow(LogWindowSource logSource) - { - super("Протокол работы", true, true, true, true); - m_logSource = logSource; - m_logSource.registerListener(this); - m_logContent = new TextArea(""); - m_logContent.setSize(200, 500); - + public LogWindow(LogWindowSource logSource) { + super(LocalizationManager.getInstance().getString("window.log.title"), true, true, true, true); + this.logSource = logSource; + this.logSource.registerListener(this); + logContent = new TextArea(""); + logContent.setEditable(false); JPanel panel = new JPanel(new BorderLayout()); - panel.add(m_logContent, BorderLayout.CENTER); + panel.add(logContent, BorderLayout.CENTER); getContentPane().add(panel); pack(); updateLogContent(); + Logger.debug(loc.getString("log.start")); + loc.addListener(this::refreshLanguage); } - private void updateLogContent() - { - StringBuilder content = new StringBuilder(); - for (LogEntry entry : m_logSource.all()) - { - content.append(entry.getMessage()).append("\n"); + private void refreshLanguage() { + setTitle(loc.getString("window.log.title")); + logSource.clear(); + Logger.debug(loc.getString("log.start")); + } + + private void updateLogContent() { + StringBuilder sb = new StringBuilder(); + for (LogEntry entry : logSource.all()) { + sb.append(entry.getMessage()).append("\n"); } - m_logContent.setText(content.toString()); - m_logContent.invalidate(); + logContent.setText(sb.toString()); } - + @Override - public void onLogChanged() - { - EventQueue.invokeLater(this::updateLogContent); + public void onLogChanged() { + SwingUtilities.invokeLater(this::updateLogContent); } } diff --git a/robots/src/gui/MainApplicationFrame.java b/robots/src/gui/MainApplicationFrame.java index 62e943e..6667686 100644 --- a/robots/src/gui/MainApplicationFrame.java +++ b/robots/src/gui/MainApplicationFrame.java @@ -1,156 +1,136 @@ 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 config.ConfigManager; +import localization.LocalizationManager; import log.Logger; +import model.RobotController; +import model.RobotModel; +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +public class MainApplicationFrame extends JFrame { + private static final String LOG_WINDOW_NAME = "LogWindow"; + private static final String GAME_WINDOW_NAME = "GameWindow"; + private static final String COORD_WINDOW_NAME = "CoordWindow"; -/** - * Что требуется сделать: - * 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; - Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - setBounds(inset, inset, - screenSize.width - inset*2, - screenSize.height - inset*2); + private final ConfigManager configManager = new ConfigManager(); + private final WindowStateManager windowStateManager = new WindowStateManager(configManager); + private final LocalizationManager localization = LocalizationManager.getInstance(); + + private final RobotModel robotModel; + private final RobotController robotController; + + private LogWindow logWindow; + private GameWindow gameWindow; + private RobotCoordinatesWindow coordWindow; + private MenuBarFactory menuBarFactory; + public MainApplicationFrame(RobotModel model, RobotController controller) { + this.robotModel = model; + this.robotController = controller; + + configManager.load(); + windowStateManager.loadMainWindowState(this); setContentPane(desktopPane); - - - LogWindow logWindow = createLogWindow(); - addWindow(logWindow); - GameWindow gameWindow = new GameWindow(); - gameWindow.setSize(400, 400); - addWindow(gameWindow); + createWindows(); + setupMenuBar(); + + windowStateManager.loadInternalWindowState(logWindow, LOG_WINDOW_NAME); + windowStateManager.loadInternalWindowState(gameWindow, GAME_WINDOW_NAME); + windowStateManager.loadInternalWindowState(coordWindow, COORD_WINDOW_NAME); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + exitApplication(); + } + }); - setJMenuBar(generateMenuBar()); - setDefaultCloseOperation(EXIT_ON_CLOSE); + localization.addListener(this::refreshLanguage); } - - protected LogWindow createLogWindow() - { - LogWindow logWindow = new LogWindow(Logger.getDefaultLogSource()); - logWindow.setLocation(10,10); + + private void createWindows() { + logWindow = new LogWindow(Logger.getDefaultLogSource()); + logWindow.setLocation(10, 10); logWindow.setSize(300, 800); setMinimumSize(logWindow.getSize()); logWindow.pack(); - Logger.debug("Протокол работает"); - return logWindow; + + gameWindow = new GameWindow(robotModel, robotController); + gameWindow.setSize(400, 400); + + coordWindow = new RobotCoordinatesWindow(robotModel); + coordWindow.setSize(250, 100); + coordWindow.setLocation(10, 400); + + addWindow(LOG_WINDOW_NAME, logWindow); + addWindow(GAME_WINDOW_NAME, gameWindow); + addWindow(COORD_WINDOW_NAME, coordWindow); } - - protected void addWindow(JInternalFrame frame) - { + + private void addWindow(String windowName, JInternalFrame frame) { + windowStateManager.setupWindowBehavior(frame, windowName); 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); - } + private void setupMenuBar() { + menuBarFactory = new MenuBarFactory(this); + setJMenuBar(menuBarFactory.createMenuBar()); + } - 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); - } + private void refreshLanguage() { + logWindow.setTitle(localization.getString("window.log.title")); + gameWindow.setTitle(localization.getString("window.game.title")); + coordWindow.setTitle(localization.getString("window.coord.title")); + setJMenuBar(menuBarFactory.createMenuBar()); + SwingUtilities.updateComponentTreeUI(this); + } + + public void exitApplication() { + if (robotController != null) robotController.stopTimer(); + + windowStateManager.saveMainWindowState(this); + windowStateManager.saveInternalWindowState(logWindow, LOG_WINDOW_NAME); + windowStateManager.saveInternalWindowState(gameWindow, GAME_WINDOW_NAME); + windowStateManager.saveInternalWindowState(coordWindow, COORD_WINDOW_NAME); + configManager.save(); - menuBar.add(lookAndFeelMenu); - menuBar.add(testMenu); - return menuBar; + UIManager.put("OptionPane.yesButtonText", localization.getString("option.yes")); + UIManager.put("OptionPane.noButtonText", localization.getString("option.no")); + + int result = JOptionPane.showConfirmDialog( + this, + localization.getString("exit.confirm.message"), + localization.getString("exit.confirm.title"), + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (result == JOptionPane.YES_OPTION) { + dispose(); + } } - - 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 + } catch (Exception e) { Logger.error("Fail in setLookAndFeel"); } + } + + public void showWindowByName(String windowName) { + JInternalFrame target = null; + if (LOG_WINDOW_NAME.equals(windowName)) target = logWindow; + else if (GAME_WINDOW_NAME.equals(windowName)) target = gameWindow; + else if (COORD_WINDOW_NAME.equals(windowName)) target = coordWindow; + if (target != null) { + target.setVisible(true); + try { + if (target.isIcon()) target.setIcon(false); + } catch (java.beans.PropertyVetoException e) { Logger.debug("PropertyVetoException");} + target.toFront(); } } } diff --git a/robots/src/gui/MenuBarFactory.java b/robots/src/gui/MenuBarFactory.java new file mode 100644 index 0000000..141da60 --- /dev/null +++ b/robots/src/gui/MenuBarFactory.java @@ -0,0 +1,84 @@ +package gui; + +import localization.LocalizationManager; +import log.Logger; + +import javax.swing.*; +import java.util.Locale; + +public class MenuBarFactory { + private final MainApplicationFrame frame; + private final LocalizationManager loc = LocalizationManager.getInstance(); + + public MenuBarFactory(MainApplicationFrame frame) { + this.frame = frame; + } + + public JMenuBar createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + menuBar.add(createFileMenu()); + menuBar.add(createLookAndFeelMenu()); + menuBar.add(createTestMenu()); + menuBar.add(createWindowsMenu()); + menuBar.add(createLanguageMenu()); + return menuBar; + } + + private JMenu createFileMenu() { + JMenu fileMenu = new JMenu(loc.getString("menu.file")); + JMenuItem exitItem = new JMenuItem(loc.getString("menu.exit")); + exitItem.addActionListener(e -> frame.exitApplication()); + fileMenu.add(exitItem); + return fileMenu; + } + + private JMenu createLookAndFeelMenu() { + JMenu menu = new JMenu(loc.getString("menu.lookandfeel")); + JMenuItem system = new JMenuItem(loc.getString("menu.lookandfeel.system")); + system.addActionListener(e -> { + frame.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + frame.invalidate(); + }); + JMenuItem cross = new JMenuItem(loc.getString("menu.lookandfeel.cross")); + cross.addActionListener(e -> { + frame.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + frame.invalidate(); + }); + menu.add(system); + menu.add(cross); + return menu; + } + + private JMenu createTestMenu() { + JMenu menu = new JMenu(loc.getString("menu.test")); + JMenuItem logMsg = new JMenuItem(loc.getString("menu.test.log")); + logMsg.addActionListener(e -> Logger.debug(loc.getString("log.newline"))); + menu.add(logMsg); + return menu; + } + + private JMenu createWindowsMenu() { + JMenu menu = new JMenu(loc.getString("menu.windows")); + JMenuItem showLog = new JMenuItem(loc.getString("menu.windows.showLog")); + showLog.addActionListener(e -> frame.showWindowByName("LogWindow")); + JMenuItem showGame = new JMenuItem(loc.getString("menu.windows.showGame")); + showGame.addActionListener(e -> frame.showWindowByName("GameWindow")); + JMenuItem showCoord = new JMenuItem(loc.getString("menu.windows.showCoord")); + showCoord.addActionListener(e -> frame.showWindowByName("CoordWindow")); + menu.add(showLog); + menu.add(showGame); + menu.add(showCoord); + return menu; + } + + private JMenu createLanguageMenu() { + JMenu menu = new JMenu(loc.getString("menu.language")); + JMenuItem russian = new JMenuItem(loc.getString("menu.language.russian")); + russian.addActionListener(e -> LocalizationManager.getInstance().setLocale(new Locale("ru"))); + JMenuItem english = new JMenuItem(loc.getString("menu.language.english")); + english.addActionListener(e -> LocalizationManager.getInstance().setLocale(new Locale("en"))); + menu.add(russian); + menu.add(english); + return menu; + } +} diff --git a/robots/src/gui/RobotCoordinatesWindow.java b/robots/src/gui/RobotCoordinatesWindow.java new file mode 100644 index 0000000..5a70cc2 --- /dev/null +++ b/robots/src/gui/RobotCoordinatesWindow.java @@ -0,0 +1,42 @@ +package gui; + +import localization.LocalizationManager; +import model.RobotModel; +import javax.swing.*; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class RobotCoordinatesWindow extends JInternalFrame implements PropertyChangeListener { + private final JLabel positionLabel; + private final JLabel directionLabel; + private final RobotModel model; + private final LocalizationManager loc = LocalizationManager.getInstance(); + + public RobotCoordinatesWindow(RobotModel model) { + super(LocalizationManager.getInstance().getString("window.coord.title"), true, true, true, true); + this.model = model; + model.addPropertyChangeListener(this); + + JPanel panel = new JPanel(new GridLayout(2, 1)); + positionLabel = new JLabel(); + directionLabel = new JLabel(); + panel.add(positionLabel); + panel.add(directionLabel); + getContentPane().add(panel); + setSize(250, 80); + updateLabels(); + + loc.addListener(this::updateLabels); + } + + private void updateLabels() { + positionLabel.setText(loc.format("coord.position", model.getRobotX(), model.getRobotY())); + directionLabel.setText(loc.format("coord.direction", Math.toDegrees(model.getRobotDirection()))); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + updateLabels(); + } +} diff --git a/robots/src/gui/RobotsProgram.java b/robots/src/gui/RobotsProgram.java index ae0930a..4462265 100644 --- a/robots/src/gui/RobotsProgram.java +++ b/robots/src/gui/RobotsProgram.java @@ -1,25 +1,25 @@ package gui; -import java.awt.Frame; +import model.RobotController; +import model.RobotModel; +import javax.swing.*; +import java.awt.*; -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(() -> { + RobotModel model = new RobotModel(); + RobotController controller = new RobotController(model); + MainApplicationFrame frame = new MainApplicationFrame(model, controller); + frame.pack(); + frame.setVisible(true); + frame.setExtendedState(Frame.MAXIMIZED_BOTH); + }); + } +} diff --git a/robots/src/gui/WindowStateManager.java b/robots/src/gui/WindowStateManager.java new file mode 100644 index 0000000..5f88d84 --- /dev/null +++ b/robots/src/gui/WindowStateManager.java @@ -0,0 +1,97 @@ +package gui; + +import config.ConfigManager; +import log.Logger; + +import javax.swing.*; +import javax.swing.event.InternalFrameAdapter; +import javax.swing.event.InternalFrameEvent; +import java.awt.*; + +public class WindowStateManager { + private final ConfigManager config; + + public WindowStateManager(ConfigManager config) { + this.config = config; + } + + public void loadMainWindowState(JFrame mainFrame) { + int inset = 50; + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + int defaultW = screenSize.width - inset * 2; + int defaultH = screenSize.height - inset * 2; + + mainFrame.setBounds( + config.getMainWindowX(inset), + config.getMainWindowY(inset), + config.getMainWindowWidth(defaultW), + config.getMainWindowHeight(defaultH) + ); + mainFrame.setExtendedState(config.getMainWindowState(JFrame.NORMAL)); + } + + public void saveMainWindowState(JFrame mainFrame) { + config.setMainWindowBounds( + mainFrame.getX(), mainFrame.getY(), + mainFrame.getWidth(), mainFrame.getHeight(), + mainFrame.getExtendedState() + ); + } + + public void loadInternalWindowState(JInternalFrame frame, String windowName) { + int x = frame.getX(); + int y = frame.getY(); + int w = frame.getWidth(); + int h = frame.getHeight(); + boolean icon = false; + boolean maximized = false; + boolean visible = true; + + if (config.hasWindow(windowName)) { + x = config.getWindowX(windowName, x); + y = config.getWindowY(windowName, y); + w = config.getWindowWidth(windowName, w); + h = config.getWindowHeight(windowName, h); + icon = config.getWindowIcon(windowName, false); + maximized = config.getWindowMaximized(windowName, false); + visible = config.getWindowVisible(windowName, true); + } + + frame.setBounds(x, y, w, h); + try { + if (maximized) { + frame.setMaximum(true); + } else if (icon) { + frame.setIcon(true); + } + } catch (java.beans.PropertyVetoException e) { + Logger.debug("Property veto exception"); + } + frame.setVisible(visible); + } + + public void saveInternalWindowState(JInternalFrame frame, String windowName) { + config.setWindowBounds(windowName, + frame.getX(), frame.getY(), + frame.getWidth(), frame.getHeight(), + frame.isIcon(), frame.isMaximum() + ); + config.setWindowVisible(windowName, frame.isVisible()); + } + + public void setupWindowBehavior(JInternalFrame frame, String windowName) { + frame.setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE); + frame.addInternalFrameListener(new InternalFrameAdapter() { + @Override + public void internalFrameClosing(InternalFrameEvent e) { + config.setWindowVisible(windowName, false); + config.save(); + } + @Override + public void internalFrameOpened(InternalFrameEvent e) { + config.setWindowVisible(windowName, true); + config.save(); + } + }); + } +} diff --git a/robots/src/localization/LocalizationManager.java b/robots/src/localization/LocalizationManager.java new file mode 100644 index 0000000..9f16a22 --- /dev/null +++ b/robots/src/localization/LocalizationManager.java @@ -0,0 +1,68 @@ +package localization; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; + +public class LocalizationManager { + private static final String BASE_NAME = "Messages"; + private static LocalizationManager instance; + private ResourceBundle bundle; + private Locale currentLocale; + private final ConcurrentHashMap formatCache = new ConcurrentHashMap<>(); + private final java.util.List listeners = new java.util.ArrayList<>(); + + private LocalizationManager() { + setLocale(new Locale("ru")); + } + + public static LocalizationManager getInstance() { + if (instance == null) { + instance = new LocalizationManager(); + } + return instance; + } + + public void setLocale(Locale locale) { + this.currentLocale = locale; + this.bundle = ResourceBundle.getBundle(BASE_NAME, locale); + formatCache.clear(); + notifyListeners(); + } + + public Locale getCurrentLocale() { + return currentLocale; + } + + public String getString(String key) { + try { + return bundle.getString(key); + } catch (MissingResourceException e) { + return "!" + key + "!"; + } + } + + public String format(String key, Object... args) { + MessageFormat format = formatCache.computeIfAbsent(key, k -> { + String pattern = getString(k); + return new MessageFormat(pattern, currentLocale); + }); + return format.format(args); + } + + public void addListener(Runnable listener) { + listeners.add(listener); + } + + public void removeListener(Runnable listener) { + listeners.remove(listener); + } + + private void notifyListeners() { + for (Runnable listener : listeners) { + listener.run(); + } + } +} diff --git a/robots/src/log/LogBuffer.java b/robots/src/log/LogBuffer.java new file mode 100644 index 0000000..705fe21 --- /dev/null +++ b/robots/src/log/LogBuffer.java @@ -0,0 +1,32 @@ +package log; + +import collections.BoundedCircularQueue; +import java.util.List; + +public class LogBuffer { + private final BoundedCircularQueue buffer; + + public LogBuffer(int capacity) { + buffer = new BoundedCircularQueue<>(capacity); + } + + public void add(LogEntry entry) { + buffer.offer(entry); + } + + public int size() { + return buffer.size(); + } + + public List getAll() { + return buffer.getAll(); + } + + public List getRange(int start, int end) { + return buffer.getRange(start, end); + } + + public void clear() { + buffer.clear(); + } +} diff --git a/robots/src/log/LogWindowSource.java b/robots/src/log/LogWindowSource.java index ca0ce44..26d5aae 100644 --- a/robots/src/log/LogWindowSource.java +++ b/robots/src/log/LogWindowSource.java @@ -1,89 +1,81 @@ package log; +import collections.BoundedCircularQueue; 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 BoundedCircularQueue buffer; + private final List listeners = new ArrayList<>(); + private volatile LogChangeListener[] activeListeners; + + public LogWindowSource(int capacity) { + this.buffer = new BoundedCircularQueue<>(capacity); } - - 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; + + + public void append(LogLevel level, String message) { + LogEntry entry = new LogEntry(level, message); + buffer.add(entry); + + LogChangeListener[] copy = activeListeners; + if (copy == null) { + synchronized (listeners) { + if (activeListeners == null) { + activeListeners = listeners.toArray(new LogChangeListener[0]); + copy = activeListeners; } } } - for (LogChangeListener listener : activeListeners) - { + for (LogChangeListener listener : copy) { listener.onLogChanged(); } } - - public int size() - { - return m_messages.size(); + + public int size() { + return buffer.size(); } - public Iterable range(int startFrom, int count) - { - if (startFrom < 0 || startFrom >= m_messages.size()) - { - return Collections.emptyList(); - } - int indexTo = Math.min(startFrom + count, m_messages.size()); - return m_messages.subList(startFrom, indexTo); + + public Iterable all() { + return buffer.getAll(); + } + + public Iterable range(int startFrom, int count) { + int total = buffer.size(); + if (startFrom < 0 || startFrom >= total) return Collections.emptyList(); + int end = Math.min(startFrom + count, total); + return buffer.getRange(startFrom, end); } - public Iterable all() - { - return m_messages; + public void clear() { + buffer.clear(); + LogChangeListener[] copy = activeListeners; + if (copy == null) { + synchronized (listeners) { + if (activeListeners == null) { + activeListeners = listeners.toArray(new LogChangeListener[0]); + copy = activeListeners; + } + } + } + for (LogChangeListener l : copy) { + l.onLogChanged(); + } } } diff --git a/robots/src/log/Logger.java b/robots/src/log/Logger.java index b008a5d..91c71b6 100644 --- a/robots/src/log/Logger.java +++ b/robots/src/log/Logger.java @@ -4,7 +4,7 @@ public final class Logger { private static final LogWindowSource defaultLogSource; static { - defaultLogSource = new LogWindowSource(100); + defaultLogSource = new LogWindowSource(5); } private Logger() diff --git a/robots/src/model/RobotConstants.java b/robots/src/model/RobotConstants.java new file mode 100644 index 0000000..1ca537b --- /dev/null +++ b/robots/src/model/RobotConstants.java @@ -0,0 +1,8 @@ +package model; + +public final class RobotConstants { + public static final double MAX_VELOCITY = 0.1; + public static final double MAX_ANGULAR_VELOCITY = 0.001; + + private RobotConstants() {} // запрет создания экземпляров +} diff --git a/robots/src/model/RobotController.java b/robots/src/model/RobotController.java new file mode 100644 index 0000000..e3e0ae6 --- /dev/null +++ b/robots/src/model/RobotController.java @@ -0,0 +1,26 @@ +package model; + +import javax.swing.Timer; +public class RobotController { + private final RobotModel model; + private final Timer timer; + private final int TIME_CONST = 10; + + public RobotController(RobotModel model) { + this.model = model; + this.timer = new Timer(TIME_CONST, e -> updateModel()); + timer.start(); + } + + public void setTarget(int x, int y) { + model.setTarget(x, y); + } + + public void stopTimer() { + if (timer.isRunning()) timer.stop(); + } + + public void updateModel() { + model.update( TIME_CONST); + } +} diff --git a/robots/src/model/RobotModel.java b/robots/src/model/RobotModel.java new file mode 100644 index 0000000..517f604 --- /dev/null +++ b/robots/src/model/RobotModel.java @@ -0,0 +1,107 @@ +package model; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.awt.Point; + +public class RobotModel { + public static final String PROP_POSITION = "position"; + public static final String PROP_DIRECTION = "direction"; + public static final String PROP_TARGET = "target"; + + private volatile double robotX = 100; + private volatile double robotY = 100; + private volatile double robotDirection = 0; + private volatile int targetX = 150; + private volatile int targetY = 100; + + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + public double getRobotX() { return robotX; } + public double getRobotY() { return robotY; } + public double getRobotDirection() { return robotDirection; } + public int getTargetX() { return targetX; } + public int getTargetY() { return targetY; } + + public void setTarget(int x, int y) { + targetX = x; + targetY = y; + pcs.firePropertyChange(PROP_TARGET, null, new Point(x, y)); + } + + private void setRobotPosition(double x, double y) { + robotX = x; + robotY = y; + pcs.firePropertyChange(PROP_POSITION, null, new Point((int)x, (int)y)); + } + + private void setRobotDirection(double direction) { + double old = robotDirection; + robotDirection = normalizeRadians(direction); + pcs.firePropertyChange(PROP_DIRECTION, old, robotDirection); + } + + // Логика движения + private void update(double velocity, double angularVelocity, double duration) { + velocity = applyLimits(velocity, 0, RobotConstants.MAX_VELOCITY); + angularVelocity = applyLimits(angularVelocity, -RobotConstants.MAX_ANGULAR_VELOCITY, + RobotConstants.MAX_ANGULAR_VELOCITY); + + double newX, newY; + if (Math.abs(angularVelocity) < 1e-8) { + newX = robotX + velocity * duration * Math.cos(robotDirection); + newY = robotY + velocity * duration * Math.sin(robotDirection); + } else { + newX = robotX + (velocity / angularVelocity) * + (Math.sin(robotDirection + angularVelocity * duration) - Math.sin(robotDirection)); + newY = robotY - (velocity / angularVelocity) * + (Math.cos(robotDirection + angularVelocity * duration) - Math.cos(robotDirection)); + } + double newDirection = robotDirection + angularVelocity * duration; + + setRobotPosition(newX, newY); + setRobotDirection(newDirection); + } + + public void update(double duration) { + double dx = getTargetX() - getRobotX(); + double dy = getTargetY() - getRobotY(); + double distance = Math.hypot(dx, dy); + if (distance < 0.5) return; + + double angleToTarget = normalizeRadians(Math.atan2(dy, dx)); + double robotDir = getRobotDirection(); + double diff = angleToTarget - robotDir; + diff = normalizeRadians(diff); + if (diff > Math.PI) diff -= 2 * Math.PI; + if (diff < -Math.PI) diff += 2 * Math.PI; + + double angularVelocity; + if (Math.abs(diff) < 0.01) angularVelocity = 0; + else angularVelocity = (diff > 0) ? RobotConstants.MAX_ANGULAR_VELOCITY : -RobotConstants.MAX_ANGULAR_VELOCITY; + + double velocity = (Math.abs(diff) < 0.3) ? RobotConstants.MAX_VELOCITY : 0; + + update(velocity, angularVelocity, duration); + } + + private static double applyLimits(double value, double min, double max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + private static double normalizeRadians(double angle) { + while (angle < 0) angle += 2 * Math.PI; + while (angle >= 2 * Math.PI) angle -= 2 * Math.PI; + return angle; + } +}