diff --git a/README.md b/README.md index 5c431d60..a7710f88 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,224 @@ # OpenRobotics -Multi-Robot Warehouse Simulation Platform -Check the repo's Project tab for the Kanban board. +

+ OpenRobotics logo +

-## Description +

Multi-Robot Warehouse Simulation Platform

-OpenRobotics is a JavaFX-based warehouse simulation platform where multiple autonomous robots navigate a configurable warehouse environment to complete pickup and delivery tasks. The simulation supports multiple navigation algorithms (Greedy, Bug, RTA*), coordination policies (Traffic Rules, Reservation-K), real-time 2D visualization, a heatmap overlay, and a results screen with per-robot statistics logged to a remote PostgreSQL database. +

+ CI + Java 21 + JavaFX 21 + Maven + MIT License +

+ +OpenRobotics is a JavaFX-based warehouse simulation platform where multiple autonomous robots navigate a configurable warehouse environment to complete pickup and delivery tasks. The simulation supports multiple navigation algorithms, coordination policies, real-time 2D visualization, a heatmap overlay, and a results screen with per-robot statistics that can also be logged to a PostgreSQL database. + +At its core, the project is aimed at studying how warehouse workloads can be completed efficiently and reliably using teams of robots, and what trade-offs emerge among fleet size, throughput, task distribution, and workload completion time under different navigation and coordination designs. + +## Table of Contents + +- [Features](#features) +- [Built With](#built-with) +- [Navigation and Coordination](#navigation-and-coordination) +- [Architecture Overview](#architecture-overview) +- [Quick Start](#quick-start) +- [Development Commands](#development-commands) +- [Testing and CI](#testing-and-ci) +- [Documentation](#documentation) +- [Project Structure](#project-structure) +- [Authors](#authors) +- [License](#license) ## Features -- 4 screens: Welcome, Setup, Simulation Editor, Results -- Config file loading and saving (JSON) -- Template maps and random map generation -- Drag-and-drop entity placement in the editor -- Navigation algorithms: Greedy, Bug, RTA* -- Coordination policies: None, Traffic Rules, Reservation-K -- Proximity and range sensors per robot -- Deadlock detection and recovery -- Heatmap visualization -- Per-robot statistics: distance, energy, tasks completed, idle ticks -- Results export and database logging (PostgreSQL via Supabase) +- Config file loading and saving in JSON format. +- Template maps and random map generation. +- Drag-and-drop entity placement in the simulation editor. +- Navigation algorithms: Greedy, Bug, and RTA*. +- Coordination policies: None, Traffic Rules, and Reservation-K. +- Proximity and range sensors per robot. +- Deadlock detection and recovery. +- Heatmap visualization. +- Per-robot statistics, including distance, energy, tasks completed, and idle ticks. +- Results export and optional PostgreSQL logging. + +## Built With + +| Category | Technology | Version | Notes | +| --- | --- | --- | --- | +| Language | Java | 21 | Primary application language | +| UI | JavaFX | 21 | Desktop UI framework | +| Build | Maven | N/A | Build, test, and packaging | +| Database | PostgreSQL | N/A | Optional run logging | +| DB Migrations | Flyway | 10.17.0 | Schema migration management | +| Connection Pooling | HikariCP | 5.1.0 | Shared JDBC connection pool | +| JSON | Jackson Databind | 2.16.1 | Config and export serialization | +| Logging Backend | Logback Classic | 1.5.16 | Logging implementation | +| Unit Testing | JUnit Jupiter | 5.10.1 | Test framework | +| UI Testing | TestFX | 4.0.18 | JavaFX UI tests | +| Coverage | JaCoCo | 0.8.12 | Coverage reporting | +| Mutation Testing | PIT | 1.22.0 | Mutation testing profile | + +## Navigation and Coordination + +### Navigation Algorithms + +- **Greedy**: moves toward the target using Manhattan distance, with deterministic tie-breaking, sensor-aware preference, and backtracking when necessary. +- **Bug**: uses a Bug2-style approach that alternates between direct goal-seeking and left-hand boundary following around obstacles. +- **RTA\***: uses Real-Time A* with a per-robot learned heuristic table while keeping transient sensor penalties separate from learned costs. + +### Coordination Policies + +- **None**: robots follow their move intentions without additional coordination filtering. +- **Traffic Rules**: marked intersection tiles are treated as exclusive regions, with entry prioritized by wait time and load status. +- **Reservation-K**: a robot must reserve the next `k` tiles on its path before moving, reducing head-on and path-overlap conflicts. + +## Architecture Overview + +The codebase is organized into a few clear subsystems: + +- **Application and UI**: JavaFX screens, controllers, styling, and navigation utilities. +- **Simulation Core**: engine tick loop, dispatcher, coordination policies, and collision handling. +- **Robot Domain**: robot state, algorithms, and sensor models. +- **Map Domain**: tiles, vectors, map entities, and environment objects. +- **Tasking**: task generation, lifecycle, and assignment support. +- **Persistence and Logging**: database access, Flyway migrations, DAO layer, and run/event logging. +- **Configuration and I/O**: JSON config loading, saving, and export DTOs. + +## Quick Start + +### Prerequisites + +- Java 21 +- Maven + +### Clone and Run + +1. Clone the repository: + + ```bash + git clone https://github.com/dan-moraru/OpenRobotics.git + cd OpenRobotics/open-robotics + ``` + +2. Start the application: + + ```bash + mvn clean javafx:run + ``` + +From the repository root, you can alternatively use the helper script: + +```bash +./build.sh +``` + +### Optional Database Setup -## Dev Install +Database-backed logging is optional. If no database configuration is available, the application still starts, but remote logging features are unavailable. -1. Make sure you have `Java SDK 21` or higher -2. Clone the [repo](https://github.com/dan-moraru/OpenRobotics) -3. Set up the database config file (see Database Setup below) -4. Open a terminal and run `cd open-robotics` -5. Run `mvn clean javafx:run` or use the IDE's integrated Maven tool window +For local development, copy the provided example file: -### Database Setup +```bash +cp src/main/resources/application.config.example src/main/resources/application.config +``` -The app connects to a remote PostgreSQL database hosted on Supabase for logging simulation results. Create the file `open-robotics/application.config` with the following content (fill in your credentials): +Then edit `src/main/resources/application.config` with your credentials: +```properties +db.url=jdbc:postgresql://:5432/postgres?sslmode=require +db.user= +db.password= ``` -DB_URL=jdbc:postgresql://:5432/postgres?sslmode=require -DB_USER= -DB_PASSWORD= + +Environment variables `DB_URL`, `DB_USER`, and `DB_PASSWORD` are also supported and take precedence over file values. + +### Sample Configurations + +The repository includes several sample JSON configurations under `configs/` inside the `open-robotics` module. Examples include: + +- `warehouse-config.json` +- `warehouse_2.json` +- `test_scenario.json` + +### Standalone Packaging + +To build the standalone packaged JAR: + +```bash +mvn clean package -Pfat-jar -DskipTests ``` -This file is gitignored and must be created locally. Without it, the app will still run but database logging will be unavailable. +This packaging profile bundles JavaFX dependencies for Windows, macOS, and Linux. The standalone shaded artifact is generated under: + +```text +target/open-robotics--standalone.jar +``` -## Dev Usage +## Development Commands -Make sure you are in the `open-robotics` directory before running these commands: +Run these commands from the `open-robotics` directory: -- Run program: `mvn clean javafx:run` +- Run the app: `mvn clean javafx:run` +- Compile only: `mvn compile` - Run all tests: `mvn test` +- Run verification and coverage: `mvn clean verify` - Run mutation tests: `mvn test -Pmutation` -- Build program to JAR: `mvn package -DskipTests` -- Compile only: `mvn compile` +- Build packaged artifacts: `mvn package -DskipTests` +- Build the standalone packaged JAR: `mvn package -Pfat-jar -DskipTests` - Clean old artifacts: `mvn clean` - Show dependencies: `mvn dependency:tree` -You can also run any of these through the IDE's integrated Maven tool window. +## Testing and CI + +The project currently includes **64 test classes** under `src/test/java`, covering hundreds of unit, UI, and integration test cases. + +- **Unit and UI tests** run through Maven and include TestFX-based controller and UI coverage. +- **Coverage reporting** is generated via JaCoCo during `mvn clean verify`. +- **Mutation testing** is available through the `mutation` Maven profile. + +The repository also includes a GitHub Actions CI workflow that: + +- Runs on pushes and pull requests targeting `main` and `dev`. +- Uses Temurin JDK 21. +- Executes `mvn clean verify` from the `open-robotics` module. +- Uploads Surefire and JaCoCo artifacts. + +## Documentation + +Additional project documents are available in [`docs/`](docs): + +- [Project Pitch](docs/Project%20Pitch%20-%20OpenRobotics.pdf) +- [Specification Document](docs/Specification%20Document%20-%20OpenRobotics.pdf) +- [Design Document](docs/Design%20Document%20-%20OpenRobotics.pdf) ## Project Structure -``` +```text open-robotics/ ├── src/main/java/com/openrobotics/ -│ ├── MainApp.java # Entry point +│ ├── MainApp.java # JavaFX application entry point +│ ├── Launcher.java # Plain main class for packaged JAR launch │ ├── controllers/ # FXML screen controllers -│ ├── db/ # DAO layer, models, record builders -│ ├── io/ # Config file loading and saving +│ ├── db/ # DAO layer, models, record builders, database access +│ ├── io/ # Config file loading, saving, and export DTOs │ ├── logging/ # Logger and event types -│ ├── map/ # Map, Tile, Vector2D, entities -│ ├── robot/ # Robot, navigation, sensors -│ ├── simulationcore/ # Engine, dispatcher, policies -│ ├── task/ # Task, TaskGenerator, TaskStatus -│ └── util/ # ScreenNavigator, IconLoader, etc. -├── src/main/resources/com/openrobotics/ -│ ├── css/theme.css # Global stylesheet -│ ├── fxml/ # Screen and dialog FXML files -│ └── img/ # Logo and entity icons -├── src/main/resources/db/migration/ # Flyway SQL migration scripts +│ ├── map/ # Map, tile, vector, and entity types +│ ├── robot/ # Robot model, navigation, and sensors +│ ├── simulationcore/ # Engine, dispatcher, policies, and collision handling +│ ├── task/ # Task generation and task lifecycle +│ └── util/ # Screen navigation, icon loading, viewport tips, and helpers +├── src/main/resources/ +│ ├── application.config.example # Example local database config +│ ├── com/openrobotics/fxml/ # FXML screen and dialog layouts +│ ├── com/openrobotics/css/ # Global stylesheet +│ ├── com/openrobotics/img/ # Logo and simulation assets +│ └── db/migration/ # Flyway SQL migrations +├── src/test/java/com/openrobotics/ # Unit, integration, and UI tests +├── configs/ # Saved JSON simulation configs └── pom.xml ``` @@ -88,3 +232,7 @@ open-robotics/ - Muhammad Sohail, 261142698 - Murad Novruzov, 261164063 - Behnam Yosufi, 261125449 + +## License + +Distributed under the MIT License. See [`LICENSE`](LICENSE) for details. diff --git a/open-robotics/configs/..valid.json b/open-robotics/configs/..valid.json index 2c5928f7..ca7b53f7 100644 --- a/open-robotics/configs/..valid.json +++ b/open-robotics/configs/..valid.json @@ -1,6 +1,6 @@ { "config" : { - "runId" : "641949f5-3dbe-43c8-ad7b-cfeb1b222f18", + "runId" : "ebc72ee6-96dc-4bb3-a878-6f8e7df4cf02", "runName" : "default_run", "tickMs" : 100, "maxTicks" : 5000, @@ -15,14 +15,14 @@ "manualTaskAssignment" : false }, "map" : { - "mapId" : "5b307bf9-e784-4863-8250-7367bf3d6560", + "mapId" : "4ef3d4b7-a31b-4aab-8064-b16b7de126b6", "width" : 8, "height" : 8, "tiles" : [ ] }, "entities" : { "robots" : [ { - "id" : "cef66328-ce04-4d1c-8217-57ab1a8c4f93", + "id" : "bb6e0a02-c19d-4200-9c79-8c2f0feb95e3", "name" : "R1", "position" : { "x" : 1, diff --git a/open-robotics/configs/observalble_deadlock.json b/open-robotics/configs/observalble_deadlock.json index e03f0c9d..1f08674e 100644 --- a/open-robotics/configs/observalble_deadlock.json +++ b/open-robotics/configs/observalble_deadlock.json @@ -10,7 +10,9 @@ "chargePerTick" : 5.0, "energyPerMove" : 1.0, "loadingTicks" : 1, - "unloadingTicks" : 1 + "unloadingTicks" : 1, + "maxTasks" : 10, + "manualTaskAssignment" : false }, "map" : { "mapId" : "e8b76c63-4505-499e-8070-4eb32d17b96e", @@ -93,7 +95,8 @@ }, "type" : null, "boxCount" : 1, - "validDropoffIds" : null + "validDropoffIds" : null, + "manualDropoffAssignment" : false }, { "id" : "2222aaaa-2222-4222-a222-222222222222", "name" : "Rack-02", @@ -103,7 +106,8 @@ }, "type" : null, "boxCount" : 1, - "validDropoffIds" : null + "validDropoffIds" : null, + "manualDropoffAssignment" : false }, { "id" : "3333aaaa-3333-4333-a333-333333333333", "name" : "Rack-03", @@ -113,7 +117,8 @@ }, "type" : null, "boxCount" : 1, - "validDropoffIds" : null + "validDropoffIds" : null, + "manualDropoffAssignment" : false } ], "obstacles" : [ { "id" : "f0000000-0000-4000-a000-000000000001", diff --git a/open-robotics/configs/testeroooo.json b/open-robotics/configs/testeroooo.json new file mode 100644 index 00000000..73ace8f4 --- /dev/null +++ b/open-robotics/configs/testeroooo.json @@ -0,0 +1,75 @@ +{ + "config" : { + "runId" : "1baa8958-adfc-406b-a838-e9322dc8b957", + "runName" : "experiment_1", + "tickMs" : 100, + "maxTicks" : 30000, + "seed" : 42, + "batteryCapacity" : 100.0, + "lowBatteryThreshold" : 20.0, + "chargePerTick" : 5.0, + "energyPerMove" : 1.0, + "loadingTicks" : 1, + "unloadingTicks" : 1, + "maxTasks" : 10, + "manualTaskAssignment" : false + }, + "map" : { + "mapId" : "46ffd701-e156-43bd-9a05-a334d5601a0b", + "width" : 16, + "height" : 10, + "tiles" : [ ] + }, + "entities" : { + "robots" : [ { + "id" : "e50d74d5-cf03-4479-8693-4c533be3edd2", + "name" : "robot_1", + "position" : { + "x" : 4, + "y" : 3 + }, + "type" : null, + "battery" : 100.0, + "state" : "IDLE", + "navigationStrategy" : "GREEDY", + "sensorStrategy" : "PROXIMITY", + "stuckTicks" : 0 + } ], + "stations" : [ { + "id" : "ad36503c-eb3c-46cd-a25f-a2cf3bfe423c", + "name" : "charger_2", + "position" : { + "x" : 2, + "y" : 2 + }, + "type" : "CHARGING" + }, { + "id" : "f3fd75bf-15bb-4e62-9f9f-3613c8923b49", + "name" : "station_3", + "position" : { + "x" : 10, + "y" : 3 + }, + "type" : "DELIVERY" + } ], + "racks" : [ { + "id" : "f344d174-c871-4100-9e6b-1e6cd96a2e7a", + "name" : "shelf_4", + "position" : { + "x" : 7, + "y" : 4 + }, + "type" : null, + "boxCount" : 1, + "validDropoffIds" : null, + "manualDropoffAssignment" : false + } ], + "obstacles" : [ ] + }, + "tasks" : [ ], + "simulation" : { + "tick" : 0, + "isRunning" : false, + "speedMultiplier" : 1.0 + } +} \ No newline at end of file diff --git a/open-robotics/pom.xml b/open-robotics/pom.xml index 501237ff..39d220d8 100644 --- a/open-robotics/pom.xml +++ b/open-robotics/pom.xml @@ -128,6 +128,7 @@ + OpenRobotics-App org.apache.maven.plugins @@ -193,10 +194,10 @@ shade true - fat + standalone - com.openrobotics.MainApp + com.openrobotics.Launcher @@ -207,6 +208,7 @@ META-INF/*.SF META-INF/*.DSA META-INF/*.RSA + module-info.class @@ -253,6 +255,24 @@ fat-jar + + org.openjfx + javafx-base + ${javafx.version} + win + + + org.openjfx + javafx-base + ${javafx.version} + linux + + + org.openjfx + javafx-base + ${javafx.version} + mac + org.openjfx javafx-graphics @@ -307,6 +327,24 @@ ${javafx.version} mac + + org.openjfx + javafx-swing + ${javafx.version} + win + + + org.openjfx + javafx-swing + ${javafx.version} + linux + + + org.openjfx + javafx-swing + ${javafx.version} + mac + diff --git a/open-robotics/src/main/java/com/openrobotics/AppState.java b/open-robotics/src/main/java/com/openrobotics/AppState.java index 8ed9d816..4d7c730c 100644 --- a/open-robotics/src/main/java/com/openrobotics/AppState.java +++ b/open-robotics/src/main/java/com/openrobotics/AppState.java @@ -2,15 +2,15 @@ import com.openrobotics.simulationcore.SimulationEngine; -/** - * Lightweight application-level state holder shared across screens. - * Holds the active {@link SimulationEngine} instance and the path of the - * last loaded config file so the simulation can be restarted from scratch. - */ +/** Lightweight application-level state holder; shares the active engine and last-loaded config path across screens. */ public final class AppState { private static SimulationEngine engine; private static String configPath; + private static String editorBaselinePath; + private static String simulationConsoleText; + private static String simulationLogText; + private static long simulationLogCursor; private static int canvasWidthTiles = 15; private static int canvasHeightTiles = 15; private static int simulationTick = 0; @@ -25,10 +25,31 @@ private AppState() {} public static void setConfigPath(String path) { configPath = path; } public static boolean hasConfigPath() { return configPath != null && !configPath.isBlank(); } + public static String getEditorBaselinePath() { return editorBaselinePath; } + public static void setEditorBaselinePath(String path) { editorBaselinePath = path; } + public static boolean hasEditorBaselinePath() { return editorBaselinePath != null && !editorBaselinePath.isBlank(); } + + public static String getSimulationConsoleText() { return simulationConsoleText; } + public static void setSimulationConsoleText(String text){ simulationConsoleText = text; } + public static boolean hasSimulationConsoleText() { return simulationConsoleText != null && !simulationConsoleText.isBlank(); } + + public static String getSimulationLogText() { return simulationLogText; } + public static void setSimulationLogText(String text) { simulationLogText = text; } + public static boolean hasSimulationLogText() { return simulationLogText != null && !simulationLogText.isBlank(); } + + public static long getSimulationLogCursor() { return simulationLogCursor; } + public static void setSimulationLogCursor(long cursor) { simulationLogCursor = Math.max(0, cursor); } + public static int getCanvasWidthTiles() { return canvasWidthTiles; } public static int getCanvasHeightTiles() { return canvasHeightTiles; } public static void setCanvasDimensions(int w, int h) { canvasWidthTiles = Math.max(1, w); canvasHeightTiles = Math.max(1, h); } - /** Legacy single-axis accessor — returns the larger of width/height. */ + + /** + * Returns the larger of the canvas width and height as a single tile dimension. + * Legacy accessor — prefer {@link #getCanvasWidthTiles()} and {@link #getCanvasHeightTiles()} for new code. + * + * @return the larger of {@code canvasWidthTiles} and {@code canvasHeightTiles} + */ public static int getCanvasTiles() { return Math.max(canvasWidthTiles, canvasHeightTiles); } public static int getSimulationTick() { return simulationTick; } @@ -37,6 +58,10 @@ private AppState() {} public static void clear() { engine = null; configPath = null; + editorBaselinePath = null; + simulationConsoleText = null; + simulationLogText = null; + simulationLogCursor = 0; simulationTick = 0; } } diff --git a/open-robotics/src/main/java/com/openrobotics/Launcher.java b/open-robotics/src/main/java/com/openrobotics/Launcher.java new file mode 100644 index 00000000..4aa7fd75 --- /dev/null +++ b/open-robotics/src/main/java/com/openrobotics/Launcher.java @@ -0,0 +1,12 @@ +package com.openrobotics; + +/** + * Non-Application entry point for the JAR. + * JavaFX refuses to start from the classpath when the main class extends Application, + * so this plain class delegates to MainApp.main() to bypass that check. + */ +public class Launcher { + public static void main(String[] args) { + MainApp.main(args); + } +} diff --git a/open-robotics/src/main/java/com/openrobotics/MainApp.java b/open-robotics/src/main/java/com/openrobotics/MainApp.java index 15a19fc6..2bbad251 100644 --- a/open-robotics/src/main/java/com/openrobotics/MainApp.java +++ b/open-robotics/src/main/java/com/openrobotics/MainApp.java @@ -11,19 +11,12 @@ import java.io.IOException; import java.sql.SQLException; -/** - * Application entry point. - * - *

Initialises the primary {@link Stage}, registers it with - * {@link ScreenNavigator}, and navigates to the Welcome screen. - * All subsequent screen transitions are handled by {@link ScreenNavigator}. - */ +/** Application entry point; initialises the primary stage, connects to the database, and navigates to the welcome screen. */ public class MainApp extends Application { - private SimulationController controller; + private SimulationController controller; @Override public void init() { - // Database initialization try { Database.init(); System.out.println("Database initialized successfully."); @@ -46,7 +39,6 @@ public void start(Stage primaryStage) throws IOException { @Override public void stop() { - // Clean up resources try { Database.shutdown(); System.out.println("Database connection closed successfully."); @@ -54,7 +46,7 @@ public void stop() { System.err.println("Database shutdown failure: " + e.getMessage()); } - // Shutdown sim controller threads + // shut down the simulation controller if it is the active screen Object controller = ScreenNavigator.getCurrentController(); if (controller instanceof SimulationController simController) { @@ -64,6 +56,11 @@ public void stop() { System.out.println("Shutdown complete."); } + /** + * Launches the JavaFX application. + * + * @param args command-line arguments forwarded to {@link javafx.application.Application#launch}. + */ public static void main(String[] args) { launch(args); } diff --git a/open-robotics/src/main/java/com/openrobotics/SimulationDevLauncher.java b/open-robotics/src/main/java/com/openrobotics/SimulationDevLauncher.java index 4163060b..7efdf6d3 100644 --- a/open-robotics/src/main/java/com/openrobotics/SimulationDevLauncher.java +++ b/open-robotics/src/main/java/com/openrobotics/SimulationDevLauncher.java @@ -4,11 +4,7 @@ import javafx.application.Application; import javafx.stage.Stage; -/** - * Dev launcher that opens the Simulation screen directly, - * bypassing Welcome and Setup. Run this class from the IDE - * while the screens are not yet fully stitched together. - */ +/** Dev launcher that opens the simulation screen directly, bypassing the welcome and setup screens. */ public class SimulationDevLauncher extends Application { @Override @@ -23,6 +19,11 @@ public void start(Stage primaryStage) { ScreenNavigator.goToSimulation(); } + /** + * Launches the dev simulation screen. + * + * @param args command-line arguments forwarded to {@link javafx.application.Application#launch}. + */ public static void main(String[] args) { launch(args); } diff --git a/open-robotics/src/main/java/com/openrobotics/common/Direction.java b/open-robotics/src/main/java/com/openrobotics/common/Direction.java index 8848ebc6..c8ba96de 100644 --- a/open-robotics/src/main/java/com/openrobotics/common/Direction.java +++ b/open-robotics/src/main/java/com/openrobotics/common/Direction.java @@ -1,6 +1,6 @@ package com.openrobotics.common; -/** movement directions for the grid */ +/** Cardinal movement directions for the warehouse grid. */ public enum Direction { UP(0, -1), DOWN(0, 1), diff --git a/open-robotics/src/main/java/com/openrobotics/config/ApplicationConfig.java b/open-robotics/src/main/java/com/openrobotics/config/ApplicationConfig.java index e5036ca2..88143216 100644 --- a/open-robotics/src/main/java/com/openrobotics/config/ApplicationConfig.java +++ b/open-robotics/src/main/java/com/openrobotics/config/ApplicationConfig.java @@ -7,7 +7,7 @@ import java.nio.charset.StandardCharsets; import java.util.Properties; -/** loads database configuration from application.config; env vars DB_URL, DB_USER, DB_PASSWORD override file values */ +/** Loads database configuration from application.config; env vars DB_URL, DB_USER, and DB_PASSWORD override file values. */ public class ApplicationConfig { private static final String CONFIG_RESOURCE = "application.config"; @@ -17,10 +17,10 @@ public class ApplicationConfig { private final String dbPassword; /** - * loads config from application.config on the classpath, then overrides with env vars if set. + * Loads config from application.config on the classpath, then overrides with env vars if set. * * @throws IOException if the config file cannot be read - * @throws IllegalStateException if any required db config value is missing after loading + * @throws IllegalStateException if any required database config value is missing after loading */ public ApplicationConfig() throws IOException { Properties props = loadFromClasspath(); diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/ExitConfirmController.java b/open-robotics/src/main/java/com/openrobotics/controllers/ExitConfirmController.java index e4ffff81..7f757de4 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/ExitConfirmController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/ExitConfirmController.java @@ -5,58 +5,34 @@ import javafx.stage.Stage; /** - * Controller for {@code ExitConfirmDialog.fxml}. - * - *

Implements both {@link ScreenNavigator.DialogController} (to receive the - * owning stage) and {@link ScreenNavigator.ExitConfirmResultHolder} (so the - * navigator can read back whether the user confirmed the exit). + * Controller for ExitConfirmDialog.fxml; implements both {@link ScreenNavigator.DialogController} + * and {@link ScreenNavigator.ExitConfirmResultHolder}. */ public class ExitConfirmController implements ScreenNavigator.DialogController, ScreenNavigator.ExitConfirmResultHolder { + // Dialog State private Stage dialogStage; private boolean confirmed = false; - // ------------------------------------------------------------------ // - // DialogController - // ------------------------------------------------------------------ // - @Override public void setDialogStage(Stage stage) { this.dialogStage = stage; } - // ------------------------------------------------------------------ // - // ExitConfirmResultHolder - // ------------------------------------------------------------------ // - @Override public boolean isConfirmed() { return confirmed; } - // ------------------------------------------------------------------ // - // FXML initialisation - // ------------------------------------------------------------------ // - - @FXML - private void initialize() { - // Nothing to initialise – labels are static in the FXML. - } - - // ------------------------------------------------------------------ // - // Event Handlers - // ------------------------------------------------------------------ // - - /** User pressed "Yes – Exit": set confirmed flag and close. */ + // Dialog Actions @FXML private void onYes() { confirmed = true; close(); } - /** User pressed "No – Keep Working": leave confirmed = false and close. */ @FXML private void onNo() { confirmed = false; @@ -73,12 +49,8 @@ private void onReturn() { onNo(); } - // ------------------------------------------------------------------ // - // Helper - // ------------------------------------------------------------------ // - + // Dialog Helpers private void close() { if (dialogStage != null) dialogStage.close(); } } - diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/ExportResultsController.java b/open-robotics/src/main/java/com/openrobotics/controllers/ExportResultsController.java index 3f92f385..7bef6c5a 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/ExportResultsController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/ExportResultsController.java @@ -20,18 +20,21 @@ import java.util.prefs.Preferences; /** - * Controller for {@code ExportResultsDialog.fxml}. - * Serializes post-simulation results to a JSON file chosen by the user. + * Controller for ExportResultsDialog.fxml; serializes post-simulation results to a user-chosen + * JSON file. */ public class ExportResultsController implements ScreenNavigator.DialogController { + // Form Fields @FXML private TextField fileNameField; @FXML private ComboBox directoryCombo; @FXML private Label selectedDirLabel; + // Dialog State private Stage dialogStage; private File selectedDirectory; + // Defaults private static final String PREFS_KEY = "recentExportDirs"; private static final int MAX_RECENT = 8; private static final String DEFAULT_DIR = @@ -42,6 +45,7 @@ public void setDialogStage(Stage stage) { this.dialogStage = stage; } + // Initialization @FXML private void initialize() { directoryCombo.valueProperty().addListener((obs, o, n) -> { @@ -60,6 +64,7 @@ private void initialize() { java.time.LocalDate.now().toString() + ".json"); } + // Directory Selection @FXML private void onResetDirectory() { selectedDirectory = new File(DEFAULT_DIR); @@ -85,6 +90,7 @@ private void onBrowse() { } } + // Export Actions @FXML private void onExport() { if (selectedDirectory == null) { @@ -145,7 +151,8 @@ private void onCancel() { close(); } - /** Builds the results DTO from the current engine state. */ + // Export DTO + // Build the export DTO from the current engine state. private ResultsExportDTO buildDTO(SimulationEngine engine) { ResultsExportDTO dto = new ResultsExportDTO(); dto.runName = AppState.getConfigPath() != null ? AppState.getConfigPath() : "unknown"; @@ -188,10 +195,12 @@ private ResultsExportDTO buildDTO(SimulationEngine engine) { return dto; } + // Dialog Helpers private void close() { if (dialogStage != null) dialogStage.close(); } + // Recent Directories private List loadRecentDirs() { Preferences prefs = Preferences.userNodeForPackage(ExportResultsController.class); List dirs = new ArrayList<>(); @@ -211,4 +220,4 @@ private void saveRecentDir(String dir) { prefs.put(PREFS_KEY + i, current.get(i)); } } -} \ No newline at end of file +} diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/LoadConfigController.java b/open-robotics/src/main/java/com/openrobotics/controllers/LoadConfigController.java index 09c5d1ca..66bc261e 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/LoadConfigController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/LoadConfigController.java @@ -13,37 +13,31 @@ import java.util.prefs.Preferences; /** - * Controller for {@code LoadConfigDialog.fxml}. - * - *

Implements {@link ScreenNavigator.DialogController} so the navigator - * can inject the owning dialog stage for self-close behaviour. + * Controller for LoadConfigDialog.fxml; lets the user pick a JSON config file from recent history + * or the filesystem. */ public class LoadConfigController implements ScreenNavigator.DialogController { + // Form Fields @FXML private ComboBox configFileCombo; @FXML private Label selectedPathLabel; + // Dialog State private Stage dialogStage; private File selectedFile; - /** Key used to persist recent paths in Java Preferences. */ + // Defaults + // Persist recent config paths in Java preferences. private static final String PREFS_KEY = "recentConfigPaths"; private static final int MAX_RECENT = 8; private static final String BROWSE_SENTINEL = "Browse..."; - // ------------------------------------------------------------------ // - // DialogController - // ------------------------------------------------------------------ // - @Override public void setDialogStage(Stage stage) { this.dialogStage = stage; } - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // - + // Initialization @FXML private void initialize() { configFileCombo.valueProperty().addListener((obs, o, n) -> { @@ -62,15 +56,12 @@ private void initialize() { configFileCombo.getItems().add(BROWSE_SENTINEL); } + // File Selection private void ensureBrowseSentinelLast() { configFileCombo.getItems().remove(BROWSE_SENTINEL); configFileCombo.getItems().add(BROWSE_SENTINEL); } - // ------------------------------------------------------------------ // - // Event Handlers - // ------------------------------------------------------------------ // - @FXML private void onResetToDefault() { selectedFile = null; @@ -100,6 +91,7 @@ private void onBrowse() { } } + // Dialog Actions @FXML private void onLoad() { if (selectedFile != null) { @@ -114,24 +106,23 @@ private void onCancel() { close(); } - // ------------------------------------------------------------------ // - // Result accessor (for callers that stored the FXMLLoader) - // ------------------------------------------------------------------ // - - /** Returns the file chosen by the user, or {@code null} if cancelled. */ + /** + * Returns the file chosen by the user, or {@code null} if the dialog was cancelled. + * Callers retrieve this after the dialog closes via the FXMLLoader from + * {@link ScreenNavigator#openDialog}. + * + * @return the selected file, or {@code null} if cancelled. + */ public File getSelectedFile() { return selectedFile; } - // ------------------------------------------------------------------ // - // Helpers - // ------------------------------------------------------------------ // - + // Dialog Helpers private void close() { if (dialogStage != null) dialogStage.close(); } - @SuppressWarnings("unchecked") + // Recent Paths private List loadRecentPaths() { Preferences prefs = Preferences.userNodeForPackage(LoadConfigController.class); List paths = new ArrayList<>(); @@ -156,4 +147,3 @@ private void saveRecentPath(String path) { } } } - diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/ObjectDescController.java b/open-robotics/src/main/java/com/openrobotics/controllers/ObjectDescController.java index f5d20ff9..60328710 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/ObjectDescController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/ObjectDescController.java @@ -7,40 +7,34 @@ import javafx.stage.Stage; /** - * Controller for {@code ObjectDescDialog.fxml}. - * - *

Shows a description of a placeable simulation object and lets the user - * add it to the viewport (§4.2 – Specialty Screen 3). + * Controller for ObjectDescDialog.fxml; shows a description of a placeable object and lets the + * user add it to the viewport (§4.2). */ public class ObjectDescController implements ScreenNavigator.DialogController { + // Form Fields @FXML private Label objectIconLabel; @FXML private Label objectNameLabel; @FXML private Label objectTypeLabel; @FXML private Label objectDescLabel; @FXML private VBox propsOverview; + // Dialog State private Stage dialogStage; private String objectType; private boolean addRequested = false; - // ------------------------------------------------------------------ // - // DialogController - // ------------------------------------------------------------------ // - @Override public void setDialogStage(Stage stage) { this.dialogStage = stage; } - // ------------------------------------------------------------------ // - // Data injection (called by SimulationController before showing dialog) - // ------------------------------------------------------------------ // - + // Dialog Content /** - * Populates all labels with data for the given object type. + * Populates all labels for the given object type. * - * @param type one of: ROBOT, CHARGER, STATION, DOCK, WALL, INTERSECTION + * @param type one of {@code ROBOT}, {@code CHARGER}, {@code STATION}, {@code DOCK}, + * {@code WALL}, or {@code INTERSECTION}. */ public void setObjectType(String type) { this.objectType = type; @@ -115,19 +109,7 @@ public void setObjectType(String type) { } } - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // - - @FXML - private void initialize() { - // Content is set via setObjectType(); nothing to do here. - } - - // ------------------------------------------------------------------ // - // Event Handlers - // ------------------------------------------------------------------ // - + // Dialog Actions @FXML private void onAdd() { addRequested = true; @@ -139,18 +121,15 @@ private void onClose() { close(); } - // ------------------------------------------------------------------ // - // Result accessor - // ------------------------------------------------------------------ // - - /** Returns {@code true} if the user pressed "Add to Viewport". */ + /** + * Returns whether the user pressed {@code Add to Viewport}. + * + * @return {@code true} if the user requested that the object be added. + */ public boolean isAddRequested() { return addRequested; } public String getObjectType() { return objectType; } - // ------------------------------------------------------------------ // - // Helpers - // ------------------------------------------------------------------ // - + // Dialog Helpers private void close() { if (dialogStage != null) dialogStage.close(); } diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/ResultsController.java b/open-robotics/src/main/java/com/openrobotics/controllers/ResultsController.java index 523cc487..1321b694 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/ResultsController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/ResultsController.java @@ -25,24 +25,22 @@ import java.util.List; /** - * Controller for {@code ResultsScreen.fxml}. - * - *

Displays the post-simulation results dashboard with real statistics - * pulled from the active {@link SimulationEngine} in {@link AppState}. + * Controller for ResultsScreen.fxml; displays the post-simulation results dashboard using data + * from {@link AppState#getEngine()}. */ public class ResultsController { - // ── TOP BAR ───────────────────────────────────────────────────────── + // Top Bar @FXML private Label ramLabel; @FXML private Label simTicksLabel; @FXML private Label simStatusLabel; @FXML private Label runNameLabel; - // ── CHARTS ────────────────────────────────────────────────────────── + // Charts @FXML private StackPane chartContainer1; @FXML private StackPane chartContainer2; - // ── TABLE ─────────────────────────────────────────────────────────── + // Robot Table @FXML private TableView robotStatsTable; @FXML private TableColumn colRobotId; @FXML private TableColumn colNavAlgo; @@ -56,7 +54,7 @@ public class ResultsController { @FXML private Label tableTitle; @FXML private Label tableInfoLabel; - // ── SUMMARY STATS ──────────────────────────────────────────────────── + // Summary Stats @FXML private Label statTotalTicks; @FXML private Label statTotalTasks; @FXML private Label statCompletedTasks; @@ -78,24 +76,21 @@ public class ResultsController { @FXML private Label statMostIdle; @FXML private Label statTotalDistance; - // ── MASK VIEWPORT ─────────────────────────────────────────────────── + // Heatmap @FXML private StackPane heatmapContainer; @FXML private Canvas heatmapCanvas; @FXML private Label heatmapPlaceholder; @FXML private Label maskTitleLabel; @FXML private Label maskSubtitleLabel; - // ── MASK SETTINGS ──────────────────────────────────────────────────── + // Heatmap Settings @FXML private CheckBox viewObjectsCheck; @FXML private CheckBox maskOpt1Check; - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // - + // Initialization @FXML private void initialize() { - // Bind mask canvas size and redraw on container resize + // Bind the heatmap canvas to the container and redraw on resize. if (heatmapCanvas != null && heatmapContainer != null) { heatmapCanvas.widthProperty().bind(heatmapContainer.widthProperty()); heatmapCanvas.heightProperty().bind(heatmapContainer.heightProperty()); @@ -104,10 +99,8 @@ private void initialize() { heatmapCanvas.heightProperty().addListener(redrawListener); } - // Update RAM display updateRamLabel(); - // Load real data from engine SimulationEngine engine = AppState.getEngine(); if (engine != null && engine.getRobots() != null) { populateTable(engine); @@ -118,6 +111,7 @@ private void initialize() { } } + // Top Bar private void updateRamLabel() { if (ramLabel != null) { long usedKb = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024; @@ -125,15 +119,11 @@ private void updateRamLabel() { } } - // ------------------------------------------------------------------ // - // Table population - // ------------------------------------------------------------------ // - + // Robot Table private void populateTable(SimulationEngine engine) { Robot[] robots = engine.getRobots(); if (robots == null || robots.length == 0) return; - // Configure columns with cell value factories colRobotId.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue().getName())); colNavAlgo.setCellValueFactory(cd -> @@ -164,15 +154,12 @@ private void populateTable(SimulationEngine engine) { } } - // ------------------------------------------------------------------ // - // Chart population - // ------------------------------------------------------------------ // - + // Charts private void populateCharts(SimulationEngine engine) { Robot[] robots = engine.getRobots(); if (robots == null || robots.length == 0) return; - // Chart 1: Tasks completed per robot (bar chart) + // Tasks completed per robot. if (chartContainer1 != null) { chartContainer1.getChildren().clear(); CategoryAxis xAxis1 = new CategoryAxis(); @@ -195,7 +182,7 @@ private void populateCharts(SimulationEngine engine) { chartContainer1.getChildren().add(tasksChart); } - // Chart 2: Energy consumed per robot (bar chart) + // Energy consumed per robot. if (chartContainer2 != null) { chartContainer2.getChildren().clear(); CategoryAxis xAxis2 = new CategoryAxis(); @@ -219,10 +206,7 @@ private void populateCharts(SimulationEngine engine) { } } - // ------------------------------------------------------------------ // - // Summary statistics - // ------------------------------------------------------------------ // - + // Summary private void populateSummary(SimulationEngine engine) { Robot[] robots = engine.getRobots(); if (robots == null || robots.length == 0) return; @@ -230,7 +214,6 @@ private void populateSummary(SimulationEngine engine) { int totalTicks = engine.getTickCounter(); int robotCount = robots.length; - // Count tasks from dispatcher int totalTasks = 0; int completedTasks = 0; int pendingTasks = 0; @@ -242,7 +225,6 @@ private void populateSummary(SimulationEngine engine) { totalTasks = engine.getDispatcher().getTotalTasksAdded(); } - // Accumulate per-robot stats int sumTasks = 0, sumDist = 0, sumIdle = 0, sumStuck = 0, sumMoving = 0, sumCharging = 0; float sumEnergy = 0, sumBattery = 0; Robot bestRobot = robots[0], worstRobot = robots[0]; @@ -267,7 +249,7 @@ private void populateSummary(SimulationEngine engine) { completedTasks = sumTasks; totalTasks = Math.max(totalTasks, completedTasks + pendingTasks); - // Top bar + // Top bar. if (simTicksLabel != null) simTicksLabel.setText("Ticks: " + totalTicks); if (simStatusLabel != null) { simStatusLabel.setText(engine.getIsRunning() ? "Status: Running" : "Status: Stopped"); @@ -277,7 +259,7 @@ private void populateSummary(SimulationEngine engine) { ? AppState.getConfigPath() : ""); } - // Simulation summary column + // Simulation summary column. setText(statTotalTicks, "Total Ticks: " + totalTicks); setText(statTotalTasks, "Total Tasks: " + totalTasks); setText(statCompletedTasks, "Completed: " + completedTasks); @@ -293,7 +275,7 @@ private void populateSummary(SimulationEngine engine) { setText(statCompletionRate, "Completion: –"); } - // Robot averages column + // Robot averages column. setText(statAvgTasks, String.format("Avg Tasks/Robot: %.1f", (double) sumTasks / robotCount)); setText(statAvgDistance, String.format("Avg Distance: %.1f tiles", (double) sumDist / robotCount)); setText(statAvgEnergy, String.format("Avg Energy Used: %.1f", sumEnergy / robotCount)); @@ -303,7 +285,7 @@ private void populateSummary(SimulationEngine engine) { } setText(statAvgBattery, String.format("Avg Battery: %.0f%%", sumBattery / robotCount)); - // Highlights column + // Highlights column. setText(statBestRobot, "Most Tasks: " + bestRobot.getName() + " (" + bestRobot.getTasksCompleted() + ")"); setText(statWorstRobot, "Fewest Tasks: " + worstRobot.getName() + " (" + worstRobot.getTasksCompleted() + ")"); setText(statMostDistance, "Most Distance: " + mostDistRobot.getName() @@ -319,17 +301,14 @@ private void setText(Label label, String text) { if (label != null) label.setText(text); } - // ------------------------------------------------------------------ // - // Heatmap canvas - // ------------------------------------------------------------------ // - + // Heatmap Rendering private void drawMaskCanvas() { if (heatmapCanvas == null) return; var gc = heatmapCanvas.getGraphicsContext2D(); double w = heatmapCanvas.getWidth(); double h = heatmapCanvas.getHeight(); - // Background — slightly lighter than the viewport bg so unvisited tiles stand out + // Background: lighter than the viewport so unvisited tiles stand out. gc.setFill(javafx.scene.paint.Color.web("#E8E4DB")); gc.fillRect(0, 0, w, h); @@ -342,7 +321,6 @@ private void drawMaskCanvas() { double offsetX = (w - mapW * tileSize) / 2; double offsetY = (h - mapH * tileSize) / 2; - // Compute max visit count for normalization int maxVisits = 0; for (int y = 0; y < mapH; y++) { for (int x = 0; x < mapW; x++) { @@ -356,12 +334,10 @@ private void drawMaskCanvas() { } } - // Three-band heatmap: low (top 33%), medium (66%), high (100%) - // Visits that equal maxVisits get the "high" color regardless of threshold rounding. + // Use low, medium, and high traffic bands based on the maximum visit count. int lowThreshold = maxVisits > 0 ? Math.max(1, (int) Math.ceil(maxVisits * 0.33)) : 0; int mediumThreshold = maxVisits > 0 ? Math.max(1, (int) Math.ceil(maxVisits * 0.66)) : 0; - // Draw heatmap tiles for (int y = 0; y < mapH; y++) { for (int x = 0; x < mapW; x++) { Tile tile = engine.getMap().getTile(x, y); @@ -369,16 +345,16 @@ private void drawMaskCanvas() { int visits = tile.getVisitCount(); javafx.scene.paint.Color color; if (visits == 0) { - // Unvisited, very light warm sand so it is clearly different from the canvas bg + // Unvisited tiles. color = javafx.scene.paint.Color.web("#F4F1EA"); } else if (visits >= mediumThreshold) { - // High traffic, red + // High traffic. color = javafx.scene.paint.Color.web("#C0392B"); } else if (visits >= lowThreshold) { - // Medium traffic, amber + // Medium traffic. color = javafx.scene.paint.Color.web("#e89003"); } else { - // Low traffic, yellow + // Low traffic. color = javafx.scene.paint.Color.web("#fffaa2"); } gc.setFill(color); @@ -387,12 +363,11 @@ private void drawMaskCanvas() { } } - // Draw entities overlay if enabled if (viewObjectsCheck != null && viewObjectsCheck.isSelected()) { drawEntitiesOverlay(gc, engine, tileSize, offsetX, offsetY); } - // Map boundary + // Map boundary. gc.setStroke(javafx.scene.paint.Color.web("#5D5B54")); gc.setLineWidth(1.5); gc.strokeRect(offsetX, offsetY, mapW * tileSize, mapH * tileSize); @@ -401,7 +376,7 @@ private void drawMaskCanvas() { private void drawEntitiesOverlay(GraphicsContext gc, SimulationEngine engine, double tileSize, double offsetX, double offsetY) { if (engine.getMap() == null) return; - // Draw obstacles (dark filled rectangles) + // Obstacles. gc.setFill(javafx.scene.paint.Color.web("#2A2926")); for (MapEntity entity : engine.getMap().getEntities()) { if (entity instanceof Obstacle) { @@ -411,7 +386,7 @@ private void drawEntitiesOverlay(GraphicsContext gc, SimulationEngine engine, do } } - // Draw charging stations (yellow/amber rectangles) + // Charging stations. gc.setFill(javafx.scene.paint.Color.web("#8C7B38")); for (MapEntity entity : engine.getMap().getEntities()) { if (entity instanceof ChargingStation) { @@ -421,7 +396,7 @@ private void drawEntitiesOverlay(GraphicsContext gc, SimulationEngine engine, do } } - // Draw delivery stations (blue-ish rectangles) + // Delivery stations. gc.setFill(javafx.scene.paint.Color.web("#3A5A8C")); for (MapEntity entity : engine.getMap().getEntities()) { if (entity instanceof DeliveryStation) { @@ -431,7 +406,7 @@ private void drawEntitiesOverlay(GraphicsContext gc, SimulationEngine engine, do } } - // Draw racks (medium grey rectangles) + // Racks. gc.setFill(javafx.scene.paint.Color.web("#6E6B65")); for (MapEntity entity : engine.getMap().getEntities()) { if (entity instanceof Rack) { @@ -441,15 +416,14 @@ private void drawEntitiesOverlay(GraphicsContext gc, SimulationEngine engine, do } } - // Draw robots as small colored dots at their final positions + // Robots at their final positions, colored by state. Robot[] robots = engine.getRobots(); if (robots != null) { for (Robot robot : robots) { double px = offsetX + robot.getPosition().getX() * tileSize + tileSize / 2; double py = offsetY + robot.getPosition().getY() * tileSize + tileSize / 2; - double radius = Math.max(3, tileSize / 4); // Circle + double radius = Math.max(3, tileSize / 4); - // Color by robot state switch (robot.getState()) { case MOVING -> gc.setFill(javafx.scene.paint.Color.web("#599068")); case CHARGING -> gc.setFill(javafx.scene.paint.Color.web("#8C7B38")); @@ -461,16 +435,9 @@ private void drawEntitiesOverlay(GraphicsContext gc, SimulationEngine engine, do } } - // ------------------------------------------------------------------ // - // Tab strip - // ------------------------------------------------------------------ // - + // Navigation @FXML private void onTabEditor() { ScreenNavigator.goToSimulation(); } - @FXML private void onTabResults() { /* already here */ } - - // ------------------------------------------------------------------ // - // Mask viewport - // ------------------------------------------------------------------ // + @FXML private void onTabResults() { } @FXML private void onMaskZoomIn() { } @FXML private void onMaskZoomOut() { } @@ -480,10 +447,6 @@ private void onToggleViewObjects() { drawMaskCanvas(); } - // ------------------------------------------------------------------ // - // Bottom actions - // ------------------------------------------------------------------ // - @FXML private void onNewExperiment() { ScreenNavigator.goToSetup(); } @@ -495,10 +458,6 @@ private void onExport() { ScreenNavigator.openDialog(ScreenNavigator.DIALOG_EXPORT_RESULTS, "Export Results"); } - // ------------------------------------------------------------------ // - // Menu - // ------------------------------------------------------------------ // - @FXML private void onMenuWelcome() { ScreenNavigator.goToWelcome(); } @FXML diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/SaveConfigController.java b/open-robotics/src/main/java/com/openrobotics/controllers/SaveConfigController.java index 5860710c..699be509 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/SaveConfigController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/SaveConfigController.java @@ -19,52 +19,68 @@ import java.util.prefs.Preferences; /** - * Controller for {@code SaveConfigDialog.fxml}. - * - *

Implements {@link ScreenNavigator.DialogController} so the navigator - * can inject the owning dialog stage for self-close behaviour. + * Controller for SaveConfigDialog.fxml; lets the user choose a directory and file name, then + * saves the current config. */ public class SaveConfigController implements ScreenNavigator.DialogController { + // Form Fields @FXML private TextField fileNameField; @FXML private ComboBox directoryCombo; @FXML private Label selectedDirLabel; + // Dialog State private Stage dialogStage; private File selectedDirectory; private String resultFilePath; // set in onSave(), read by caller after dialog closes + // Defaults private static final String PREFS_KEY = "recentSaveDirs"; + private static final String CONFIG_SUFFIX = ".json"; private static final int MAX_RECENT = 8; private static final String DEFAULT_DIR = System.getProperty("user.home") + File.separator + ".open-robotics" + File.separator + "configs"; private static final String BROWSE_SENTINEL = "Browse..."; private static final String CONFIG_DIR = "./configs"; - // ------------------------------------------------------------------ // - // DialogController - // ------------------------------------------------------------------ // - @Override public void setDialogStage(Stage stage) { this.dialogStage = stage; } - /** Returns the full path (directory + file name) set when the user clicked Save, or null if not saved yet. */ + // Result Access + /** + * Returns the full path set when the user clicked {@code Save}, or {@code null} if the dialog + * has not saved a file yet. + * + * @return the saved config path, or {@code null} if no file has been saved yet. + */ public String getResultFilePath() { return resultFilePath; } - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // + /** + * Sets the default file name shown in the dialog, stripping any ".json" suffix so the user + * only edits the base name. + * + * @param name the default base file name + */ + public void setDefaultFileName(String name) { + if (name == null || name.isEmpty()) { + return; + } + if (name.toLowerCase().endsWith(CONFIG_SUFFIX)) { + name = name.substring(0, name.length() - CONFIG_SUFFIX.length()); + } + fileNameField.setText(name); + } + // Initialization @FXML private void initialize() { directoryCombo.valueProperty().addListener((obs, o, n) -> { if (n == null) return; if (BROWSE_SENTINEL.equals(n)) { - // Defer so the popup closes before opening the native chooser, - // then restore the prior selection (or clear it) before browsing. + // Let the combo close before opening the native chooser. javafx.application.Platform.runLater(() -> { directoryCombo.getSelectionModel().select(o); onBrowse(); @@ -85,13 +101,10 @@ private void initialize() { } fileNameField.setText("experiment_" + - java.time.LocalDate.now().toString() + ".json"); + java.time.LocalDate.now().toString()); } - // ------------------------------------------------------------------ // - // Event Handlers - // ------------------------------------------------------------------ // - + // Directory Selection @FXML private void onResetDirectory() { selectedDirectory = new File(DEFAULT_DIR); @@ -124,6 +137,7 @@ private void ensureBrowseSentinelLast() { directoryCombo.getItems().add(BROWSE_SENTINEL); } + // Save Actions @FXML private void onSave() { if (selectedDirectory == null) { @@ -139,6 +153,10 @@ private void onSave() { selectedDirLabel.setText("File name contains invalid characters."); return; } + // Auto-append the config suffix when the user omits it. + if (!fileName.toLowerCase().endsWith(CONFIG_SUFFIX)) { + fileName += CONFIG_SUFFIX; + } if (!selectedDirectory.exists()) { selectedDirLabel.setText("Selected directory does not exist."); return; @@ -163,7 +181,7 @@ private void onSave() { saveRecentDir(selectedDirectory.getAbsolutePath()); resultFilePath = fullPath; - // Automatically save a copy to the configs directory for the list + // Mirror the saved config into ./configs so it appears in the load list. File configsDir = new File(CONFIG_DIR); if (!configsDir.exists()) { configsDir.mkdirs(); @@ -186,24 +204,18 @@ private void onCancel() { close(); } - // ------------------------------------------------------------------ // - // Result accessors - // ------------------------------------------------------------------ // - public File getSelectedDirectory() { return selectedDirectory; } public String getFileName() { String value = fileNameField == null ? null : fileNameField.getText(); return value == null ? "" : value.trim(); } - // ------------------------------------------------------------------ // - // Helpers - // ------------------------------------------------------------------ // - + // Dialog Helpers private void close() { if (dialogStage != null) dialogStage.close(); } + // Recent Directories private List loadRecentDirs() { Preferences prefs = Preferences.userNodeForPackage(SaveConfigController.class); List dirs = new ArrayList<>(); @@ -228,4 +240,3 @@ private void saveRecentDir(String dir) { } } } - diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java b/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java index ed84c7cb..33451d1a 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/SetupController.java @@ -40,63 +40,54 @@ /** * Controller for {@code SetupScreen.fxml}. - * - *

Manages all simulation configuration inputs (§4.1.2): - *

    - *
  • Map selection / random map generation
  • - *
  • Coordination policy + reservation k
  • - *
  • Workload parameters
  • - *
  • Simulation tick settings
  • - *
  • Load / Save configuration file
  • - *
  • Edit Warehouse
  • - *
+ * Manages simulation configuration input, preview rendering, config-file loading, and transition + * into the editor flow (§4.1.2). */ public class SetupController { - // ── MAP ───────────────────────────────────────────────────────────── + // Map Settings @FXML private ComboBox mapCombo; @FXML private HBox mapSeedRow; @FXML private TextField randomSeedField; - // ── COORDINATION POLICY ───────────────────────────────────────────── + + // Coordination Policy @FXML private ComboBox policyCombo; @FXML private Spinner reservationKSpinner; - // ── WORKLOAD ──────────────────────────────────────────────────────── + // Task Settings @FXML private RadioButton autoTaskRadio; @FXML private RadioButton manualTaskRadio; @FXML private VBox maxTasksGroup; @FXML private TextField maxTasksField; @FXML private TextField workloadSeedField; - // ── ROBOT PHYSICS ──────────────────────────────────────────────────── + // Robot Settings @FXML private TextField batteryCapacityField; @FXML private TextField lowBatteryField; @FXML private TextField chargePerTickField; @FXML private TextField energyPerMoveField; - // ── SIMULATION ────────────────────────────────────────────────────── + // Run Settings @FXML private TextField maxTicksField; - // ── RUN META ──────────────────────────────────────────────────────── @FXML private TextField runNameField; - // ── CANVAS ────────────────────────────────────────────────────────── + // Preview @FXML private Spinner canvasWidthSpinner; @FXML private Spinner canvasHeightSpinner; @FXML private Canvas previewCanvas; @FXML private StackPane viewportPreviewStack; @FXML private Label canvasWarnLabel; - // ── STATUS ────────────────────────────────────────────────────────── + // Status @FXML private Label statusLabel; - // Config file list + // Config Files @FXML private ListView configListView; - // ------------------------------------------------------------------ // - // Default values (spec §5.1.3.4) - // ------------------------------------------------------------------ // - + // Defaults + private static final Color INTERSECTION_FILL_COLOR = Color.web("#8C7B38", 0.25); + private static final Color INTERSECTION_STROKE_COLOR = Color.web("#6A4828"); private static final String DEFAULT_POLICY = "NONE"; private static final int DEFAULT_RESERVATION_K = 3; private static final int DEFAULT_MAX_TASKS = 10; @@ -111,16 +102,10 @@ public class SetupController { private static final float DEFAULT_CHARGE_PER_TICK = 5.0f; private static final float DEFAULT_ENERGY_PER_MOVE = 1.0f; private final String CONFIG_DIRECTORY_PATH = "./configs"; - // private static final int DEFAULT_CANVAS_TILES = 15; - - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // + // Initialization @FXML private void initialize() { - - // Map dropdown mapCombo.setItems(FXCollections.observableArrayList( "empty", "baseline_small", "narrow_aisles", "many_intersections", "random_map")); mapCombo.getSelectionModel().selectFirst(); @@ -134,15 +119,12 @@ private void initialize() { boolean isRandom = "random_map".equals(n); mapSeedRow.setVisible(isRandom); if (n != null) { - // User explicitly chose a template, drop any previously loaded config file - // so the preview and NEXT button both use the template, not the old engine. + // Switching back to a template invalidates any loaded config-backed engine. if (AppState.hasEngine()) { AppState.clear(); configListView.getSelectionModel().clearSelection(); } - // Re-enable canvas size spinners now that a template map is active - canvasWidthSpinner.setDisable(false); - canvasHeightSpinner.setDisable(false); + setConfigParamsDisabled(false); autoSetCanvasForMap(n); } refreshPreview(); @@ -152,12 +134,10 @@ private void initialize() { reservationKSpinner.setValueFactory( new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 20, DEFAULT_RESERVATION_K)); - // Coordination policy policyCombo.setItems(FXCollections.observableArrayList( "NONE", "TRAFFIC_RULES", "RESERVATION_K")); policyCombo.getSelectionModel().select(DEFAULT_POLICY); - // Canvas size spinners canvasWidthSpinner.setValueFactory( new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 5000, AppState.getCanvasWidthTiles())); canvasHeightSpinner.setValueFactory( @@ -173,7 +153,6 @@ private void initialize() { checkCanvasConstraint(); }); - // Task assignment mode toggle ToggleGroup taskModeGroup = new ToggleGroup(); autoTaskRadio.setToggleGroup(taskModeGroup); manualTaskRadio.setToggleGroup(taskModeGroup); @@ -184,24 +163,21 @@ private void initialize() { maxTasksGroup.setVisible(!isManual); }); - // Default text fields maxTasksField.setText(String.valueOf(DEFAULT_MAX_TASKS)); workloadSeedField.setText(String.valueOf(DEFAULT_WORKLOAD_SEED)); maxTicksField.setText(String.valueOf(DEFAULT_MAX_TICKS)); runNameField.setText(DEFAULT_RUN_NAME); - // Robot physics fields setTextIfPresent(batteryCapacityField, String.valueOf((int) DEFAULT_BATTERY_CAPACITY)); setTextIfPresent(lowBatteryField, String.valueOf((int) DEFAULT_LOW_BATTERY)); setTextIfPresent(chargePerTickField, String.valueOf((int) DEFAULT_CHARGE_PER_TICK)); setTextIfPresent(energyPerMoveField, String.valueOf((int) DEFAULT_ENERGY_PER_MOVE)); - // Disable reservation k spinner unless policy is RESERVATION_K reservationKSpinner.setDisable(true); policyCombo.valueProperty().addListener((obs, oldVal, newVal) -> reservationKSpinner.setDisable(!"RESERVATION_K".equals(newVal))); - // Resize canvas to fill its parent without creating a layout min-width constraint + // Keep the preview canvas synced to the available stack size without forcing layout width. viewportPreviewStack.widthProperty().addListener((obs, o, n) -> { previewCanvas.setWidth(n.doubleValue()); refreshPreview(); @@ -211,39 +187,42 @@ private void initialize() { refreshPreview(); }); - // Get config lists onRefreshFileList(); - // Listener to load config from list configListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { File selectedFile = new File(CONFIG_DIRECTORY_PATH, newValue); boolean success = loadConfigFile(selectedFile); if (success) { - // Unselect the dropdown map since custom file is active + // Config-backed maps own their dimensions, so clear the template selection. mapCombo.getSelectionModel().clearSelection(); - - // Disable canvas size spinners, config file dimensions are fixed - canvasWidthSpinner.setDisable(true); - canvasHeightSpinner.setDisable(true); - - // Redraw the canvas + setConfigParamsDisabled(true); refreshPreview(); checkCanvasConstraint(); } } }); - // Apply canvas sizing for the initial map selection (listener fires only on changes) + // Apply input restrictions (prevent negative signs and letters) + makeIntegerOnly(maxTasksField); + makeIntegerOnly(maxTicksField); + makeIntegerOnly(workloadSeedField); + makeIntegerOnly(randomSeedField); + makeFloatOnly(batteryCapacityField); + makeFloatOnly(lowBatteryField); + makeFloatOnly(chargePerTickField); + makeFloatOnly(energyPerMoveField); + makeSpinnerIntegerOnly(reservationKSpinner); + String initialMap = mapCombo.getValue(); if (initialMap != null) autoSetCanvasForMap(initialMap); } - // ------------------------------------------------------------------ // - // Reset Handlers - // ------------------------------------------------------------------ // - + // Reset Actions @FXML private void onResetMap() { + AppState.clear(); + configListView.getSelectionModel().clearSelection(); + setConfigParamsDisabled(false); mapCombo.getSelectionModel().selectFirst(); refreshPreview(); checkCanvasConstraint(); @@ -266,10 +245,7 @@ private void initialize() { @FXML private void onResetChargePerTick() { setTextIfPresent(chargePerTickField, String.valueOf((int) DEFAULT_CHARGE_PER_TICK)); } @FXML private void onResetEnergyPerMove() { setTextIfPresent(energyPerMoveField, String.valueOf((int) DEFAULT_ENERGY_PER_MOVE)); } - // ------------------------------------------------------------------ // - // Preview Canvas - // ------------------------------------------------------------------ // - + // Preview Rendering private void refreshPreview() { double vw = previewCanvas.getWidth(); double vh = previewCanvas.getHeight(); @@ -279,7 +255,6 @@ private void refreshPreview() { com.openrobotics.map.Map previewMap = null; - // If a config file has been loaded, render that map instead of the combo selection if (AppState.hasEngine() && AppState.getEngine().getMap() != null) { previewMap = AppState.getEngine().getMap(); } else { @@ -290,16 +265,12 @@ private void refreshPreview() { int cw = canvasWidthSpinner.getValue() != null ? canvasWidthSpinner.getValue() : AppState.getCanvasWidthTiles(); int ch = canvasHeightSpinner.getValue() != null ? canvasHeightSpinner.getValue() : AppState.getCanvasHeightTiles(); - // For config file maps the dimensions are fixed by the file — use them directly. - // For template maps the user controls the canvas size via the spinners, so keep cw/ch - // from the spinners above and let the entity centering logic place content inside it. + // Loaded config previews use fixed map dimensions instead of the editable canvas spinners. boolean isConfigFile = AppState.hasEngine() && AppState.getEngine().getMap() == previewMap; if (isConfigFile && previewMap != null) { cw = previewMap.getWidth(); ch = previewMap.getHeight(); - // Disable canvas size spinners, config file dimensions are fixed - canvasWidthSpinner.setDisable(true); - canvasHeightSpinner.setDisable(true); + setConfigParamsDisabled(true); } double padding = 16; @@ -325,6 +296,7 @@ private void refreshPreview() { int tileOffY = (ch - contentH) / 2 - minY; drawPreviewEntities(gc, previewMap, bx, by, tileSize, tileOffX, tileOffY); + drawTrafficRuleIntersections(gc, previewMap, bx, by, tileSize, tileOffX, tileOffY); } } @@ -333,11 +305,10 @@ private void drawPreviewEntities(GraphicsContext gc, com.openrobotics.map.Map ma int tileOffX, int tileOffY) { double pad = Math.max(1.0, tileSize * 0.06); - // Show robots only when previewing a loaded config file, not for template maps. + // Template maps preview layout only; robot markers are shown for loaded configs. boolean showRobots = AppState.hasEngine() && AppState.getEngine().getMap() == map; for (MapEntity entity : map.getEntities()) { - // Skip robots entirely for template maps if (entity instanceof Robot && !showRobots) continue; double sx = ox + (tileOffX + entity.getPosition().getX()) * tileSize; @@ -348,7 +319,7 @@ private void drawPreviewEntities(GraphicsContext gc, com.openrobotics.map.Map ma if (wallLike) gc.drawImage(icon, sx, sy, tileSize, tileSize); else gc.drawImage(icon, sx+pad, sy+pad, tileSize-2*pad, tileSize-2*pad); } else if (entity instanceof Robot) { - // Fallback robot rendering: dark rounded rect + small green dot + // Fallback robot marker when the preview icon is unavailable. gc.setFill(Color.web("#4D4B45")); gc.fillRoundRect(sx+pad, sy+pad, tileSize-2*pad, tileSize-2*pad, 4, 4); double r = Math.max(2.0, tileSize * 0.15); @@ -366,6 +337,31 @@ private void drawPreviewEntities(GraphicsContext gc, com.openrobotics.map.Map ma } } + private void drawTrafficRuleIntersections(GraphicsContext gc, com.openrobotics.map.Map map, double ox, double oy, double tileSize, int tileOffX, int tileOffY) { + if (!AppState.hasEngine() || !AppState.getEngine().usesTrafficRulesPolicy()) return; + double markerInset = Math.max(3.0, tileSize * 0.22); + + Image intersectionIcon = IconLoader.getIcon("INTERSECTION"); + + gc.setStroke(INTERSECTION_STROKE_COLOR); + gc.setLineWidth(Math.max(1.5, tileSize * 0.08)); + + for (Vector2D intersection : AppState.getEngine().getTrafficRuleIntersections()) { + double sx = ox + (tileOffX + intersection.getX()) * tileSize; + double sy = oy + (tileOffY + intersection.getY()) * tileSize; + + if (intersectionIcon != null && !intersectionIcon.isError()) { + gc.drawImage(intersectionIcon, sx, sy, tileSize, tileSize); + } else { + double centerInset = Math.max(2.0, tileSize * 0.38); + gc.setFill(INTERSECTION_FILL_COLOR); + gc.fillOval(sx + markerInset, sy + markerInset, tileSize - 2 * markerInset, tileSize - 2 * markerInset); + gc.strokeLine(sx + centerInset, sy + tileSize / 2.0, sx + tileSize - centerInset, sy + tileSize / 2.0); + gc.strokeLine(sx + tileSize / 2.0, sy + centerInset, sx + tileSize / 2.0, sy + tileSize - centerInset); + } + } + } + private Image resolvePreviewIcon(MapEntity entity) { List candidates = new ArrayList<>(); if (entity instanceof Robot) candidates.add("ROBOT"); @@ -381,10 +377,7 @@ private Image resolvePreviewIcon(MapEntity entity) { return null; } - // ------------------------------------------------------------------ // - // Built-in Map Builders - // ------------------------------------------------------------------ // - + // Built-in Maps private com.openrobotics.map.Map buildBuiltinMap(String name) { return switch (name) { case "empty" -> null; @@ -395,25 +388,7 @@ private com.openrobotics.map.Map buildBuiltinMap(String name) { }; } - /** - * FIX #2 – baseline_small (16 × 10). - * - * Layout: - *
-     *  y\x  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
-     *   0   W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W
-     *   1   W  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *   2   W  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *   3   W  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *   4   C  .  .  .  .  .  .  .  .  .  .  .  .  .  .  D  ← main aisle
-     *   5   W  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *   6   W  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *   7   W  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *   8   W  .  .  .  .  .  .  .  .  .  .  .  .  .  .  W  ← lower aisle
-     *   9   W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W
-     * 
- * Vertical aisles at cols 4, 7, 10. Charger/delivery on side-wall openings. - */ + // Builds the 16x10 baseline warehouse template. private com.openrobotics.map.Map buildBaselineSmall() { final int W = 16, H = 10; com.openrobotics.map.Map m = new com.openrobotics.map.Map(W, H); @@ -442,22 +417,7 @@ private com.openrobotics.map.Map buildBaselineSmall() { return m; } - /** - * FIX #2 – narrow_aisles (22 × 14). - * - * Six 2-wide shelving units with tall rack blocks and only 1-tile vertical aisles. - *
-     *  cols: 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
-     *  y=0 : W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W
-     *  y=1 : W  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *  ...5: W  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *  y=6 : C  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  D  ← main aisle
-     *  y=7 : W  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  W  ← lower aisle
-     *  y=8 : W  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *  ...12: W .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  R  R  .  .  W
-     *  y=13: W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W  W
-     * 
- */ + // Builds the 22x14 narrow-aisles warehouse template. private com.openrobotics.map.Map buildNarrowAisles() { final int W = 22, H = 14; com.openrobotics.map.Map m = new com.openrobotics.map.Map(W, H); @@ -486,33 +446,7 @@ private com.openrobotics.map.Map buildNarrowAisles() { return m; } - /** - * FIX #2 – many_intersections (26 × 18). - * - * Seven 2-wide shelving units × four short rack blocks, creating a dense - * grid of crossing aisles (14 intersection points per horizontal aisle). - *
-     *  cols: 0 1 2 3 | 5 6 | 8 9 |11 12|14 15|17 18|20 21| 23 24 25
-     *  y=0 : W  W  W  W  W  ...wall...  W
-     *  y=1 : W  . [R  R] . [R  R] ...
-     *  y=2 : W  . [R  R] . [R  R] ...
-     *  y=3 : W  .  .  .  .  .  ... (aisle)
-     *  y=4 : W  . [R  R] . [R  R] ...
-     *  y=5 : W  . [R  R] . [R  R] ...
-     *  y=6 : W  .  .  .  .  .  ... (aisle)
-     *  y=7 : W  .  .  .  .  .  ... (wider aisle)
-     *  y=8 : C  .  .  .  .  .  ...  D  ← main aisle
-     *  y=9 : W  .  .  .  .  .  ... (wider aisle)
-     *  y=10: W  . [R  R] . [R  R] ...
-     *  y=11: W  . [R  R] . [R  R] ...
-     *  y=12: W  .  .  .  .  .  ... (aisle)
-     *  y=13: W  . [R  R] . [R  R] ...
-     *  y=14: W  . [R  R] . [R  R] ...
-     *  y=15: W  .  .  .  .  .  ... (aisle)
-     *  y=16: W  .  .  .  .  .  ...
-     *  y=17: W  W  W  W  W  ...wall...  W
-     * 
- */ + // Builds the 26x18 dense-intersection warehouse template. private com.openrobotics.map.Map buildManyIntersections() { final int W = 26, H = 18; com.openrobotics.map.Map m = new com.openrobotics.map.Map(W, H); @@ -541,10 +475,6 @@ private com.openrobotics.map.Map buildManyIntersections() { return m; } - // ------------------------------------------------------------------ // - // Canvas Constraint + Auto-sizing - // ------------------------------------------------------------------ // - private boolean isBuiltinMap(String name) { return "baseline_small".equals(name) || "narrow_aisles".equals(name) || "many_intersections".equals(name); } @@ -567,6 +497,7 @@ private int[] computeEntityBounds(com.openrobotics.map.Map map) { return new int[]{minX, minY, maxX, maxY}; } + // Canvas Constraints private void autoSetCanvasForMap(String mapName) { if ("empty".equals(mapName)) { // empty map — keep current canvas size @@ -596,7 +527,7 @@ private void autoSetCanvasForMap(String mapName) { } private void checkCanvasConstraint() { - // If a config file has been loaded, use the engine's map dimensions + // Config-backed maps use fixed dimensions from the loaded engine. if (AppState.hasEngine() && AppState.getEngine().getMap() != null) { com.openrobotics.map.Map map = AppState.getEngine().getMap(); enforceSpinnerMin(1, 1); @@ -642,25 +573,18 @@ private void enforceSpinnerMin(int minW, int minH) { if (hf.getValue() < minH) hf.setValue(minH); } - // ------------------------------------------------------------------ // - // Blocked preview click - // ------------------------------------------------------------------ // - @FXML private void onPreviewBlocked(MouseEvent event) { statusLabel.setText("Map preview not interactive during setup."); event.consume(); } - // ------------------------------------------------------------------ // - // Config File Actions - // ------------------------------------------------------------------ // - private void debugStatus(String message) { statusLabel.setText(message); System.out.println("[SetupController] " + message); } + // Config Loading private File getFallbackTestConfig() { File fromWorkingDir = new File(System.getProperty("user.dir"), "test_scenario.json"); if (fromWorkingDir.isFile()) return fromWorkingDir; @@ -750,12 +674,13 @@ private boolean goToSimulationScreen() { @FXML private void onLoadConfig() { if (promptAndLoadConfig()) { + mapCombo.getSelectionModel().clearSelection(); + setConfigParamsDisabled(true); refreshPreview(); checkCanvasConstraint(); } } - // Get config file list @FXML private void onRefreshFileList() { if (configListView == null) return; @@ -763,7 +688,6 @@ private void onRefreshFileList() { configListView.getItems().clear(); File configDir = new File(CONFIG_DIRECTORY_PATH); - // Create directory if it doesn't exist if (!configDir.exists()) { configDir.mkdirs(); } @@ -782,10 +706,7 @@ private void onRefreshFileList() { } } - // ------------------------------------------------------------------ // - // Start Simulation - // ------------------------------------------------------------------ // - + // Start Simulation @FXML private void onStartSimulation() { debugStatus("Start Simulation clicked."); @@ -793,7 +714,6 @@ private void onStartSimulation() { if (!AppState.hasEngine()) { if (AppState.hasConfigPath()) { - // Rebuild engine from previously loaded config path debugStatus("Rebuilding engine from saved config path."); try { SimulationEngine engine = new SimulationEngine(AppState.getConfigPath()); @@ -808,15 +728,13 @@ private void onStartSimulation() { return; } } else { - // Build engine from current setup screen values SimulationEngine engine = buildEngineFromSetup(); if (engine == null) return; AppState.setEngine(engine); - // persist to a temp JSON so SimulationController.onRestart() can reload it persistEngineToTempFile(engine); } } else if (!AppState.hasConfigPath()) { - // Engine exists but was built from UI values, rebuild so latest field values apply. + // Rebuild UI-started runs so the latest field edits are applied. SimulationEngine engine = buildEngineFromSetup(); if (engine == null) return; AppState.setEngine(engine); @@ -825,11 +743,8 @@ private void onStartSimulation() { goToSimulationScreen(); } - /** - * Saves the given engine to a temp JSON file and stores the path in AppState. - * This allows SimulationController.onRestart() to reload the engine even when the user - * started from a template map (no pre-existing config file). - */ + // Engine Construction + // Persists a temp config so restart can rebuild runs started from setup values. private void persistEngineToTempFile(SimulationEngine engine) { try { File tmp = File.createTempFile("openrobotics_run_", ".json"); @@ -843,9 +758,7 @@ private void persistEngineToTempFile(SimulationEngine engine) { } } - /** - * Constructs a SimulationEngine entirely from the current UI field values. - */ + // Builds a SimulationEngine from the current setup-screen values. private SimulationEngine buildEngineFromSetup() { int canvasW = canvasWidthSpinner.getValue() != null ? canvasWidthSpinner.getValue() : 16; int canvasH = canvasHeightSpinner.getValue() != null ? canvasHeightSpinner.getValue() : 10; @@ -870,7 +783,7 @@ private SimulationEngine buildEngineFromSetup() { } else { String mapName = mapCombo.getValue(); if (mapName == null || mapName.isBlank()) { - // Shouldn't reach here if validate() passed, but guard just in case + // Guard against a cleared map selection before engine construction. statusLabel.setText("\u26a0 Please select a map."); return null; } @@ -884,15 +797,7 @@ private SimulationEngine buildEngineFromSetup() { CoordinationPolicy policy = buildCoordinationPolicy(); - // Robots are not auto-spawned here — they must be placed in the editor - // or loaded from a JSON config file (handled via SimulationEngine(configPath)). - Dispatcher dispatcher = new Dispatcher(); - // Generate tasks based on the map's racks and delivery stations. - // Tasks are generated here so the engine has the expected workload from the start. - if (maxTasks > 0 && map != null) { - generateFixedTasks(map, seed, maxTasks, dispatcher); - } String runName = runNameField.getText().trim(); if (runName.isEmpty()) runName = DEFAULT_RUN_NAME; @@ -916,9 +821,8 @@ private SimulationEngine buildEngineFromSetup() { return engine; } - /** - * Constructs a builtin map scaled/centred to fit the canvas dimensions. - */ + // Map and Task Generation + // Copies a builtin map into the current canvas with a one-tile margin. private com.openrobotics.map.Map buildBuiltinMapInCanvas(String mapName, int canvasW, int canvasH) { com.openrobotics.map.Map src = buildBuiltinMap(mapName); com.openrobotics.map.Map map = new com.openrobotics.map.Map(canvasW, canvasH); @@ -946,9 +850,7 @@ private com.openrobotics.map.Map buildBuiltinMapInCanvas(String mapName, int can return map; } - /** - * Generates a random warehouse map of the given dimensions. - */ + // Generates a random warehouse layout for the current canvas size. private com.openrobotics.map.Map buildRandomMap(int width, int height, long seed) { com.openrobotics.map.Map map = new com.openrobotics.map.Map(width, height); java.util.Random rng = new java.util.Random(seed); @@ -1020,15 +922,7 @@ private Vector2D findSpawnTile(Vector2D center, com.openrobotics.map.Map map, Se return center; // fallback } - /** - * Generates a fixed number of tasks based on the map's racks and delivery stations. - * Used by tests to verify task generation logic. - * - * @param map the map containing racks and delivery stations - * @param seed random seed for reproducibility - * @param count number of tasks to generate - * @param dispatcher the dispatcher to add tasks to - */ + // Seeds the dispatcher with the initial automatically generated tasks. private void generateFixedTasks(com.openrobotics.map.Map map, long seed, int count, Dispatcher dispatcher) { List tasks = TaskGenerator.generateAutomaticTasks(map, count, seed); for (Task task : tasks) { @@ -1050,6 +944,7 @@ private CoordinationPolicy buildCoordinationPolicy() { }; } + // Parsing Helpers private int parseIntSafe(String text, int fallback) { try { return Integer.parseInt(text.trim()); } catch (NumberFormatException e) { return fallback; } } @@ -1066,35 +961,176 @@ private float parseFloatSafe(String text, float fallback) { try { return Float.parseFloat(text.trim()); } catch (NumberFormatException e) { return fallback; } } - /** Basic validation – returns {@code true} if all required fields are filled. */ + // Validation + + // Validates the minimum required inputs before starting the simulation. private boolean validate() { - // If a config file was loaded from the list, the engine and path are already set - // skip the map combo check entirely since it was intentionally cleared on file selection. + // Map Check if (!AppState.hasEngine() && mapCombo.getValue() == null) { - statusLabel.setText("\u26a0 Please select a map or load a config file."); + statusLabel.setText("⚠ Please select a map or load a config file."); return false; } + + if ("RESERVATION_K".equals(policyCombo.getValue())) { + if (!checkSpinnerBound(reservationKSpinner, "Reservation K", 1, 50)) return false; + } + + // Integer Bounds + if (!checkIntBound(maxTasksField, "Max Tasks", 1, 500)) return false; + if (!checkIntBound(maxTicksField, "Max Ticks", 1, 1000000)) return false; + + // Float Bounds + if (batteryCapacityField != null && batteryCapacityField.getScene() != null) { + if (!checkFloatBound(batteryCapacityField, "Battery Capacity", 1.0f, 1000.0f)) return false; + if (!checkFloatBound(lowBatteryField, "Low Battery", 0.0f, 1000.0f)) return false; + if (!checkFloatBound(chargePerTickField, "Charge Per Tick", 0.0f, 100.0f)) return false; + if (!checkFloatBound(energyPerMoveField, "Energy Per Move", 0.0f, 100.0f)) return false; + + float cap = Float.parseFloat(batteryCapacityField.getText().trim()); + float low = Float.parseFloat(lowBatteryField.getText().trim()); + if (low >= cap) { + statusLabel.setText("⚠ Low battery threshold must be lower than total capacity."); + return false; + } + } + + statusLabel.setText(""); // Clear errors + return true; + } + + private boolean checkIntBound(TextField field, String fieldName, int min, int max) { + if (field == null) return true; try { - int ticks = Integer.parseInt(maxTicksField.getText().trim()); - if (ticks <= 0) throw new NumberFormatException(); + int value = Integer.parseInt(field.getText().trim()); + if (value < min || value > max) { + statusLabel.setText("⚠ " + fieldName + " must be between " + min + " and " + max + "."); + return false; + } + return true; } catch (NumberFormatException e) { - statusLabel.setText("\u26a0 Max ticks must be a positive integer."); + statusLabel.setText("⚠ " + fieldName + " requires a valid number."); return false; } - statusLabel.setText(""); - return true; } - // ------------------------------------------------------------------ // - // Tab strip - // ------------------------------------------------------------------ // + private boolean checkFloatBound(TextField field, String fieldName, float min, float max) { + if (field == null) return true; + try { + float value = Float.parseFloat(field.getText().trim()); + if (value < min || value > max) { + statusLabel.setText("⚠ " + fieldName + " must be between " + min + " and " + max + "."); + return false; + } + return true; + } catch (NumberFormatException e) { + statusLabel.setText("⚠ " + fieldName + " requires a valid number."); + return false; + } + } - @FXML - private void onTabEditor() { /* already on setup/editor screen */ } + private boolean checkSpinnerBound(Spinner spinner, String fieldName, int min, int max) { + if (spinner == null || spinner.isDisabled()) return true; + try { + // Check the editor text because Spinner.getValue() might not have changed yet + String text = spinner.getEditor().getText().trim(); + if (text.isEmpty()) { + statusLabel.setText("⚠ " + fieldName + " cannot be empty."); + return false; + } - // ------------------------------------------------------------------ // - // Menu items - // ------------------------------------------------------------------ // + int value = Integer.parseInt(text); + if (value < min || value > max) { + statusLabel.setText("⚠ " + fieldName + " must be between " + min + " and " + max + "."); + return false; + } + + // Force the spinner to adopt the typed value internally + spinner.getValueFactory().setValue(value); + return true; + } catch (NumberFormatException e) { + statusLabel.setText("⚠ " + fieldName + " requires a valid number."); + return false; + } + } + + /** Restricts a TextField to only accept positive integers */ + private void makeIntegerOnly(TextField field) { + if (field == null) return; + field.setTextFormatter(new TextFormatter<>(change -> { + // Allows empty string (while deleting) or digits only + if (change.getControlNewText().matches("\\d*")) { + return change; + } + return null; + })); + } + + /** Restricts a TextField to accept positive floats (decimals) */ + private void makeFloatOnly(TextField field) { + if (field == null) return; + field.setTextFormatter(new TextFormatter<>(change -> { + // Allows empty, digits, or digits with a single decimal point + if (change.getControlNewText().matches("\\d*(\\.\\d*)?")) { + return change; + } + return null; + })); + } + + /** Restricts an editable Spinner to only accept positive integers */ + private void makeSpinnerIntegerOnly(Spinner spinner) { + if (spinner == null || !spinner.isEditable()) return; + TextField editor = spinner.getEditor(); + editor.setTextFormatter(new TextFormatter<>(change -> { + if (change.getControlNewText().matches("\\d*")) { + return change; + } + return null; // Reject the keystroke + })); + } + + /** + * Disables or re-enables all user-editable parameters on the setup screen. + * Called with {@code true} whenever a config file is loaded and with {@code false} when switching back to a template map. + * The reservationK spinner has its own separate policy-driven disable logic + */ + private void setConfigParamsDisabled(boolean disabled) { + + // Coordination policy + policyCombo.setDisable(disabled); + if (!disabled) { + // Re-evaluate the policy-driven disable for reservationK + reservationKSpinner.setDisable(!"RESERVATION_K".equals(policyCombo.getValue())); + } else { + reservationKSpinner.setDisable(true); + } + + // Task settings + if (autoTaskRadio != null) autoTaskRadio.setDisable(disabled); + if (manualTaskRadio != null) manualTaskRadio.setDisable(disabled); + maxTasksField.setDisable(disabled); + workloadSeedField.setDisable(disabled); + + // Robot physics + setDisableIfPresent(batteryCapacityField, disabled); + setDisableIfPresent(lowBatteryField, disabled); + setDisableIfPresent(chargePerTickField, disabled); + setDisableIfPresent(energyPerMoveField, disabled); + + // Run settings + maxTicksField.setDisable(disabled); + runNameField.setDisable(disabled); + + // Canvas size + canvasWidthSpinner.setDisable(disabled); + canvasHeightSpinner.setDisable(disabled); + } + + private void setDisableIfPresent(TextField field, boolean disabled) { + if (field != null) field.setDisable(disabled); + } + + // Navigation @FXML private void onMenuWelcome() { ScreenNavigator.goToWelcome(); } @@ -1108,10 +1144,6 @@ private void onMenuGithub() { } } - // ------------------------------------------------------------------ // - // Global Exit - // ------------------------------------------------------------------ // - @FXML private void onExit() { if (ScreenNavigator.confirmExit()) { diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java b/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java index 850aa031..a2cc3d2b 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/SimulationController.java @@ -6,13 +6,10 @@ import com.openrobotics.db.model.MapRecord; import com.openrobotics.db.model.SimLogRecord; import com.openrobotics.db.model.SimulationRunRecord; -import com.openrobotics.db.model.WorkloadTaskRecord; import com.openrobotics.db.recordbuilders.MapRecordBuilder; import com.openrobotics.db.recordbuilders.SimulationRunRecordBuilder; -import com.openrobotics.db.recordbuilders.WorkloadTaskRecordBuilder; import com.openrobotics.logging.Logger; import com.openrobotics.logging.eventtypes.SimulationRunEvent; -import com.openrobotics.logging.eventtypes.TaskEvent; import com.openrobotics.map.MapEntity; import com.openrobotics.map.Vector2D; import com.openrobotics.map.entities.environment.Obstacle; @@ -58,39 +55,39 @@ /** * Controller for {@code SimulationScreen.fxml}. * - *

Manages the simulation viewport and all UI controls (§4.1.3 A + B): + *

Manages the simulation viewport and related UI controls, including: *

    - *
  • 2-D warehouse canvas rendering
  • - *
  • Drag-and-drop of object tiles from sidebar onto the canvas
  • - *
  • Drag existing objects within the canvas to reposition them
  • - *
  • Outliner: Add Object / Outliner tabs
  • - *
  • Object properties panel
  • - *
  • Console output
  • - *
  • Playback controls: play / pause / stop / restart / speed
  • - *
  • Live statistics: tick, tasks done, collisions, status
  • - *
  • Viewport navigation (right-click pan, scroll zoom)
  • + *
  • 2D warehouse canvas rendering.
  • + *
  • Drag-and-drop placement of object tiles from the sidebar onto the canvas.
  • + *
  • Dragging existing objects within the canvas to reposition them.
  • + *
  • The Add Object and Outliner tabs.
  • + *
  • The object properties panel.
  • + *
  • Console output.
  • + *
  • Playback controls for play, pause, stop, restart, and speed changes.
  • + *
  • Live statistics, including tick, completed tasks, collisions, and status.
  • + *
  • Viewport navigation with right-click panning and scroll-wheel zoom.
  • *
*/ public class SimulationController implements ScreenNavigator.Cleanable { - // ── TOPBAR LIVE STATS ──────────────────────────────────────────────── + // Topbar Live Stats @FXML private Label objsLabel; @FXML private Label ramLabel; @FXML private Label canvasSizeLabel; - // ── LIVE STATS (not yet in FXML – guarded with null checks) ────────── + // Simulation Stats @FXML private Label tickLabel; @FXML private Label tasksDoneLabel; @FXML private Label collisionsLabel; @FXML private Label simStatusLabel; - // ── OUTLINER ──────────────────────────────────────────────────────── + // Outliner @FXML private TabPane outlinerTabPane; @FXML private ListView outlinerListView; @FXML private TextField outlinerSearchField; @FXML private Button intersectionObjectTile; - // ── VIEWPORT ──────────────────────────────────────────────────────── + // Viewport @FXML private StackPane viewportStack; @FXML private Canvas warehouseCanvas; @FXML private Label viewportModeLabel; @@ -106,27 +103,28 @@ public class SimulationController implements ScreenNavigator.Cleanable { @FXML private Label tickDisplayLabel; @FXML private VBox shortcutOverlay; + // Split Pane State // Saved divider positions so collapse/expand is smooth private double savedSidebarDivider = 0.17; private double savedConsoleDivider = 0.83; - // ── PROPERTIES PANEL ──────────────────────────────────────────────── + // Properties Panel @FXML private VBox propertiesPanel; @FXML private Label propDescLabel; @FXML private TextField strPropField; - // ── CONSOLE ───────────────────────────────────────────────────────── + // Console @FXML private TextArea consoleArea; @FXML private Button clearConsoleButton; - // - DATABASE LOGGING + // Simulation Logs @FXML private TextArea logArea; - // ── TAB STRIP ───────────────────────────────────────────────────────── + // Tab Strip @FXML private Button editorTabBtn; @FXML private Button resultsTabBtn; - // ── PLAYBACK ──────────────────────────────────────────────────────── + // Playback @FXML private Button speed1Btn; @FXML private Button speed2Btn; @FXML private Button speed3Btn; @@ -135,32 +133,31 @@ public class SimulationController implements ScreenNavigator.Cleanable { @FXML private Button pauseBtn; @FXML private ProgressBar simProgressBar; - // ── Viewport navigation state ───────────────────────────────────────── + // Viewport State private double lastMouseX; private double lastMouseY; - private double viewportMouseX = -1; // last known mouse X over viewport (-1 = unset) + private double viewportMouseX = -1; // Last known mouse X over the viewport (-1 means unset). private double viewportMouseY = -1; private double viewOffsetX = 0; private double viewOffsetY = 0; private double zoom = 1.0; - // ── Canvas logical size (tiles) – read from AppState on init ────────── + // Canvas State private int canvasWidthTiles; private int canvasHeightTiles; - // Tile offset applied to all entity/object rendering so they are centred in the border + // Center loaded entities inside the canvas border. private int entityOffsetTileX = 0; private int entityOffsetTileY = 0; - private boolean viewportCentered = false; // ensures we only auto-centre once + private boolean viewportCentered = false; // Auto-center only once after layout settles. - // ── Simulation state ────────────────────────────────────────────────── + // Simulation State private boolean running = false; private boolean paused = false; private boolean simulationFailed = false; - // Snapshot of engine state at the moment play was first pressed (tick 0 baseline). - // Restart always reloads from this, not from AppState.getConfigPath(). + // Restart reloads the latest tick-0 editor snapshot rather than the original config path. private String initialSnapshotPath = null; - // ── Engine binding ──────────────────────────────────────────────────── + // Engine Binding private SimulationEngine engine; private Timeline simLoop; private double speedFactor = 1.0; @@ -183,44 +180,46 @@ public class SimulationController implements ScreenNavigator.Cleanable { private static final Color OBJECT_SELECTION_COLOR = Color.web("#C2BEAE"); private static final Color INTERSECTION_FILL_COLOR = Color.web("#8C7B38", 0.25); private static final Color INTERSECTION_STROKE_COLOR = Color.web("#6A4828"); - // ── Object rendering ────────────────────────────────────────────────── - // Uses IconLoader utility for icon caching - - // ── Canvas object state ─────────────────────────────────────────────── + // Canvas Object State private final List outlinerBacking = new ArrayList<>(); private MapEntity selectedEntity = null; private Rack pickingRack = null; private int pickingSlot = -1; private Vector2D selectedIntersection = null; private MapEntity draggingOnCanvas = null; - // Position of draggingOnCanvas at the moment the drag started — used to update tasks on release + // Track the drag origin so related task endpoints can be updated on release. private com.openrobotics.map.Vector2D dragStartPosition = null; + // Intersections are engine-managed coordinates, not MapEntity instances. + private Vector2D dragStartIntersection = null; + private Vector2D dragIntersectionOrigin = null; + // Track copied entities and intersections separately. + private MapEntity clipboardEntity = null; + private Vector2D clipboardIntersection = null; private int nextObjId = 1; private Timeline tipRotationLoop; - - // ── Drag-over tile highlight ────────────────────────────────────────── private int dragHighlightTileX = -1; private int dragHighlightTileY = -1; - // Animation state + // Animation private boolean animating = false; private double animationProgress = 0.0; private Timeline animTimeline = null; private Map prevRobotPositions = new HashMap<>(); private static final int ANIMATION_FRAMES = 5; - // ── Undo / Redo ───────────────────────────────────────────────────── + // Undo and Redo private final java.util.Deque undoStack = new java.util.ArrayDeque<>(); private final java.util.Deque redoStack = new java.util.ArrayDeque<>(); - // Used for updating simulation logs + // Log Polling private Timeline logPollingTimeline; private final ExecutorService executor = Executors.newSingleThreadExecutor(); private long lastSeenLogId = 0; + private boolean restoredOutputState = false; - /** Sealed interface for reversible editor actions. */ - private sealed interface EditorAction permits AddAction, DeleteAction, MoveAction, RenameAction, AlgorithmChangeAction, BatteryChangeAction, SensorChangeAction { + // Editor Actions + private sealed interface EditorAction permits AddAction, DeleteAction, MoveAction, RenameAction, AlgorithmChangeAction, BatteryChangeAction, SensorChangeAction, IntersectionToggleAction, IntersectionMoveAction { void undo(SimulationController ctrl); void redo(SimulationController ctrl); String description(); @@ -274,8 +273,44 @@ private record SensorChangeAction(Robot robot, String oldSensor, String newSenso public void redo(SimulationController ctrl) { robot.setSensor(newSensorStrat); ctrl.drawViewport(); } public String description() { return "Change " + robot.getName() + " sensor " + oldSensor + " → " + newSensor; } } + // Undo/redo for intersection add/delete — toggle is its own inverse + private record IntersectionToggleAction(Vector2D position, boolean wasAdded) implements EditorAction { + public void undo(SimulationController ctrl) { + if (ctrl.engine == null) return; + ctrl.engine.toggleTrafficRuleIntersection((int) position.getX(), (int) position.getY()); + ctrl.drawViewport(); + ctrl.populateOutliner(); + } + public void redo(SimulationController ctrl) { + if (ctrl.engine == null) return; + ctrl.engine.toggleTrafficRuleIntersection((int) position.getX(), (int) position.getY()); + ctrl.drawViewport(); + ctrl.populateOutliner(); + } + public String description() { return (wasAdded ? "Add" : "Delete") + " INTERSECTION at " + position; } + } + + // Undo/redo for intersection drag — stores original and final positions + private record IntersectionMoveAction(Vector2D from, Vector2D to) implements EditorAction { + public void undo(SimulationController ctrl) { + if (ctrl.engine == null) return; + ctrl.engine.toggleTrafficRuleIntersection((int) to.getX(), (int) to.getY()); + ctrl.engine.toggleTrafficRuleIntersection((int) from.getX(), (int) from.getY()); + ctrl.selectedIntersection = from; + ctrl.drawViewport(); + } + public void redo(SimulationController ctrl) { + if (ctrl.engine == null) return; + ctrl.engine.toggleTrafficRuleIntersection((int) from.getX(), (int) from.getY()); + ctrl.engine.toggleTrafficRuleIntersection((int) to.getX(), (int) to.getY()); + ctrl.selectedIntersection = to; + ctrl.drawViewport(); + } + public String description() { return "Move INTERSECTION from " + from + " to " + to; } + } private void pushAction(EditorAction action) { + // Any new edit becomes the latest undo point and invalidates redo history. undoStack.push(action); redoStack.clear(); saveEditorBaseline(); @@ -285,6 +320,7 @@ private void undoAction() { if (undoStack.isEmpty()) { log("Nothing to undo."); return; } EditorAction action = undoStack.pop(); action.undo(this); + // Keep redo symmetrical with the action that was just unwound. redoStack.push(action); log("Undo: " + action.description()); saveEditorBaseline(); @@ -294,17 +330,16 @@ private void redoAction() { if (redoStack.isEmpty()) { log("Nothing to redo."); return; } EditorAction action = redoStack.pop(); action.redo(this); + // A redone action becomes the newest entry in undo history again. undoStack.push(action); log("Redo: " + action.description()); saveEditorBaseline(); } - /** Returns true if editor mutations (add/move/delete/rename/property changes) are blocked. */ private boolean isEditorLocked() { return running || localTick > 0 || simulationFailed; } - /** Logs an error and returns true if the editor is locked. Use as a guard at the top of mutating methods. */ private boolean guardEditor(String actionName) { if (isEditorLocked()) { log("\u26a0 Cannot " + actionName + " while the simulation is running, has advanced past tick 0, or has failed. Please reset first."); @@ -313,9 +348,7 @@ private boolean guardEditor(String actionName) { return false; } - /** Saves the current editor state as the baseline for reset. Reuses a single temp file. - * Only snapshots when the simulation has not yet started (tick 0, not running), so that - * mid-run editor actions do not overwrite the clean baseline with a non-zero tick state. */ + // Keep a reusable tick-0 snapshot so play and restart reflect the latest editor state. private void saveEditorBaseline() { if (engine == null) return; if (running || engine.getTickCounter() > 0) return; // never overwrite with mid-run state @@ -326,19 +359,16 @@ private void saveEditorBaseline() { initialSnapshotPath = snap.getAbsolutePath(); } engine.configSaving(initialSnapshotPath); + AppState.setEditorBaselinePath(initialSnapshotPath); } catch (Exception ex) { log("\u26a0 Could not snapshot initial state: " + ex.getMessage()); } } - // ------------------------------------------------------------------ // - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // - + // Initialization @FXML private void initialize() { - // Set up periodic log polling (every 5 seconds) to fetch new logs from the database and update the logArea. + // Poll fresh simulation logs once per second while the screen is active. logPollingTimeline = new Timeline( new KeyFrame(Duration.seconds(1), event -> fetchLogsAsync()) ); @@ -349,15 +379,12 @@ private void initialize() { intersectionObjectTile.setVisible(false); } - // Read canvas size from shared state (set in Setup screen) + // Resume the canvas dimensions chosen on the setup screen. canvasWidthTiles = AppState.getCanvasWidthTiles(); canvasHeightTiles = AppState.getCanvasHeightTiles(); + restoredOutputState = restorePersistedOutputState(); - // Track viewport stack size with listeners (not bind) — same pattern as SetupController. - // Using bind() makes the Canvas report its pixel size as its preferred size to the - // layout engine, which then locks the SplitPane divider and prevents free dragging. - // With managed="false" on the Canvas and listener-driven setWidth/setHeight, the - // layout engine ignores the Canvas when computing preferred sizes, so dividers stay free. + // Use listeners instead of property binding so the canvas does not lock SplitPane layout. viewportStack.widthProperty().addListener((obs, oldW, newW) -> { warehouseCanvas.setWidth(newW.doubleValue()); if (!viewportCentered && newW.doubleValue() > 0 && warehouseCanvas.getHeight() > 0) { @@ -375,7 +402,6 @@ private void initialize() { drawViewport(); }); - // Viewport mouse interactions viewportStack.setOnMousePressed(this::onViewportMousePressed); viewportStack.setOnMouseDragged(this::onViewportMouseDragged); viewportStack.setOnMouseReleased(this::onViewportMouseReleased); @@ -393,7 +419,6 @@ private void initialize() { drawViewport(); }); - // Drop target: accept objects dragged from the Add Object sidebar viewportStack.setOnDragOver(this::onCanvasDragOver); viewportStack.setOnDragDropped(this::onCanvasDragDropped); viewportStack.setOnDragExited(e -> { @@ -403,7 +428,6 @@ private void initialize() { drawViewport(); }); - // Keyboard shortcuts: Delete, Cmd+C, Cmd+V, Ctrl+Z, Ctrl+Y viewportStack.setFocusTraversable(true); viewportStack.setOnKeyPressed(e -> { switch (e.getCode()) { @@ -417,13 +441,17 @@ private void initialize() { e.consume(); }); - // Outliner search filter if (outlinerSearchField != null) { outlinerSearchField.textProperty().addListener((obs, o, n) -> filterOutliner(n)); } - // Bind engine from shared AppState + if (outlinerListView != null) { + outlinerListView.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> onOutlinerSelect()); + } + + // Rebuild the engine lazily when the controller is opened from a saved config path. engine = AppState.getEngine(); + initialSnapshotPath = AppState.getEditorBaselinePath(); if (engine == null && AppState.hasConfigPath()) { engine = new SimulationEngine(AppState.getConfigPath()); if (engine == null || engine.getMap() == null) { @@ -439,17 +467,14 @@ private void initialize() { AppState.setEngine(engine); } - // Restore simulation tick from AppState (persists across tab switches) localTick = AppState.getSimulationTick(); - - // Update RAM display updateRamLabel(); if (engine != null && engine.getMap() != null) { com.openrobotics.map.Map loadedMap = engine.getMap(); - // Saving map to database. try { + // Persist the loaded map once so downstream screens can inspect it from the DB. MapRecordBuilder recordBuilder = new MapRecordBuilder(loadedMap); MapRecord record = recordBuilder.build(); MapDao.insert(record); @@ -457,46 +482,37 @@ private void initialize() { System.out.println("Error saving map to database: " + e.getMessage()); } - // Compute canvas to tightly fit the loaded entities, then centre them inside. - // The user's configured size (from SetupScreen) sets a minimum — the canvas - // never shrinks below that, but entities are always centred within whatever size results. - if (!loadedMap.getEntities().isEmpty()) { - int maxTileX = 0, maxTileY = 0; - for (MapEntity entity : loadedMap.getEntities()) { - maxTileX = Math.max(maxTileX, (int) entity.getPosition().getX()); - maxTileY = Math.max(maxTileY, (int) entity.getPosition().getY()); - } - int entitySpanX = maxTileX + 1; // number of tiles the map occupies - int entitySpanY = maxTileY + 1; - // Default canvas = entity span + 2 tiles of padding (1 each side) - canvasWidthTiles = Math.max(AppState.getCanvasWidthTiles(), entitySpanX + 2); - canvasHeightTiles = Math.max(AppState.getCanvasHeightTiles(), entitySpanY + 2); - // Offset so entities are centred inside the border - entityOffsetTileX = (canvasWidthTiles - entitySpanX) / 2; - entityOffsetTileY = (canvasHeightTiles - entitySpanY) / 2; - } - if (canvasSizeLabel != null) + // buildBuiltinMapInCanvas and loaded configs already sit at their correct absolute tile positions within the map. Adding a centering offset on top + // Use map dimensions directly and set offsets to zero. + canvasWidthTiles = loadedMap.getWidth(); + canvasHeightTiles = loadedMap.getHeight(); + entityOffsetTileX = 0; + entityOffsetTileY = 0; + if (canvasSizeLabel != null) { canvasSizeLabel.setText("Canvas Size: " + canvasWidthTiles + "×" + canvasHeightTiles + " Tiles"); + } refreshIntersectionObjectTileVisibility(); populateOutliner(); if (viewportStatusLabel != null) { viewportStatusLabel.setText("Loaded " + loadedMap.getEntities().size() + " objects"); } - log("Loaded simulation with " + loadedMap.getEntities().size() + " entities. Press \u25b6 to start."); + if (!restoredOutputState) { + log("Loaded simulation with " + loadedMap.getEntities().size() + " entities. Press \u25b6 to start."); + } drawViewport(); } else { refreshIntersectionObjectTileVisibility(); if (viewportStatusLabel != null) { viewportStatusLabel.setText("No config loaded"); } - log("No configuration loaded. Go to Setup \u2192 Load Config first."); + if (!restoredOutputState) { + log("No configuration loaded. Go to Setup \u2192 Load Config first."); + } } - // Pre-load icons and initialize tips IconLoader.preloadAllIcons(); - // Keep "Loaded N objects" after a successful load; otherwise show the default selection tip - // (replaces the transient "No config loaded" empty-state message). + // Preserve the load-state message; only show the default tip in the empty state. if (engine == null || engine.getMap() == null) { updateSelectionLabel(); } @@ -506,30 +522,25 @@ private void initialize() { if (viewportModeLabel != null) viewportModeLabel.setText("Right-click to pan, left-click to select"); if (tipLabel != null) tipLabel.setText("TIP: " + ViewportTips.nextTip()); - // Reset button colors to default CSS style (beige) if (playBtn != null) playBtn.setStyle(""); if (pauseBtn != null) pauseBtn.setStyle(""); - // Set initial sidebar position — not locked, user can drag freely if (mainSplitPane != null && mainSplitPane.getWidth() > 0) { mainSplitPane.setDividerPosition(0, 230.0 / mainSplitPane.getWidth()); } - log("Simulation screen ready. Drag an object from the panel into the viewport."); + if (!restoredOutputState) { + log("Simulation screen ready. Drag an object from the panel into the viewport."); + } } - // ------------------------------------------------------------------ // - // Viewport rendering - // ------------------------------------------------------------------ // - - /** Centers the viewport so the canvas border is centred in the visible area. */ + // Viewport Rendering private void centerViewportOnCanvas() { double tileSize = 32.0 * zoom; viewOffsetX = (warehouseCanvas.getWidth() - canvasWidthTiles * tileSize) / 2.0; viewOffsetY = (warehouseCanvas.getHeight() - canvasHeightTiles * tileSize) / 2.0; } - /** Draws the warehouse grid and all placed objects on the canvas. */ private void drawViewport() { GraphicsContext gc = warehouseCanvas.getGraphicsContext2D(); double w = warehouseCanvas.getWidth(); @@ -583,7 +594,6 @@ private void drawViewport() { } } - /** Renders every entity from the engine onto the canvas. */ private void drawEntities(GraphicsContext gc) { if (engine == null || engine.getMap() == null) return; double tileSize = 32 * zoom; @@ -593,6 +603,7 @@ private void drawEntities(GraphicsContext gc) { double ex = entity.getPosition().getX(); double ey = entity.getPosition().getY(); + // Interpolate robot sprites between ticks so movement appears continuous. if (animating && entity instanceof Robot && prevRobotPositions.containsKey(entity.getId())) { com.openrobotics.map.Vector2D prev = prevRobotPositions.get(entity.getId()); ex = prev.getX() + (ex - prev.getX()) * animationProgress; @@ -662,6 +673,14 @@ private void drawEntities(GraphicsContext gc) { gc.fillText(lbl, sx + pad + 1, sy + tileSize - pad - 2, maxLabelWidth); } + // Green outline for the currently selected entity + if (entity == selectedEntity) { + gc.setStroke(Color.web("#1a743f")); + gc.setLineWidth(Math.max(2.5, tileSize * 0.09)); + double inset = Math.max(1.5, tileSize * 0.04); + gc.strokeRect(sx + inset, sy + inset, tileSize - 2 * inset, tileSize - 2 * inset); + } + if (pickingRack != null && entity instanceof DeliveryStation) { gc.setStroke(OBJECT_SELECTION_COLOR); gc.setLineWidth(Math.max(2.0, tileSize * 0.1)); @@ -699,21 +718,17 @@ private void drawTrafficRuleIntersections(GraphicsContext gc) { // Selection highlight — always drawn regardless of icon if (intersection.equals(selectedIntersection)) { - gc.setStroke(OBJECT_SELECTION_COLOR); - gc.setLineWidth(Math.max(2.0, tileSize * 0.1)); - gc.strokeOval( - sx + Math.max(1.0, markerInset - 2.0), - sy + Math.max(1.0, markerInset - 2.0), - tileSize - 2 * Math.max(1.0, markerInset - 2.0), - tileSize - 2 * Math.max(1.0, markerInset - 2.0) - ); + gc.setStroke(Color.web("#1a743f")); + gc.setLineWidth(Math.max(2.5, tileSize * 0.09)); + double inset = Math.max(1.5, tileSize * 0.04); + gc.strokeRect(sx + inset, sy + inset, tileSize - 2 * inset, tileSize - 2 * inset); gc.setStroke(INTERSECTION_STROKE_COLOR); gc.setLineWidth(Math.max(1.5, tileSize * 0.08)); } } } - /** Resolves the best-matching icon for an engine-loaded map entity. */ + // Match engine-loaded entities against the closest available sidebar icon. private javafx.scene.image.Image resolveEntityIcon(MapEntity entity) { List candidates = new ArrayList<>(); @@ -771,7 +786,6 @@ private boolean isWallLikeEntity(MapEntity entity) { return name.contains("wall") || name.contains("obstacle"); } - /** Returns the fill colour for each object type. */ private String objectBodyColor(String type) { return switch (type) { case "ROBOT" -> "#4D4B45"; @@ -791,10 +805,8 @@ private void refreshIntersectionObjectTileVisibility() { } } - // ------------------------------------------------------------------ // - // Map-bounds helpers (clamp positions to the engine map, not the canvas) - // ------------------------------------------------------------------ // - + // Map Bounds + // Clamp editor placement against the actual map bounds, not just the visible canvas border. private int maxMapTileX() { if (engine != null && engine.getMap() != null) return engine.getMap().getWidth() - 1; return Math.max(0, canvasWidthTiles - entityOffsetTileX - 1); @@ -849,6 +861,7 @@ private boolean canPlaceEntityAt(MapEntity candidate, int x, int y, MapEntity ig return false; } + // Build the tile occupancy as if the candidate were already placed there. List occupants = new ArrayList<>(); for (MapEntity entity : engine.getMap().getEntities()) { if (entity == ignoreEntity) { @@ -863,14 +876,7 @@ private boolean canPlaceEntityAt(MapEntity candidate, int x, int y, MapEntity ig return isValidTileOccupancy(occupants); } - // ------------------------------------------------------------------ // - // Drag FROM sidebar tile → canvas (JavaFX DnD API) - // ------------------------------------------------------------------ // - - /** - * Fired when the user starts dragging an object tile button. - * Puts the object type string into the dragboard so the canvas can receive it. - */ + // Sidebar Drag and Drop @FXML private void onObjectTileDragDetected(MouseEvent e) { Button source = (Button) e.getSource(); @@ -898,10 +904,24 @@ private void onObjectTileDragDetected(MouseEvent e) { content.putString(type); db.setContent(content); - if (source.getGraphic() != null) { + // Snapshot only the icon, not the full labeled button. + javafx.scene.Node graphic = source.getGraphic(); + javafx.scene.image.ImageView iconView = null; + if (graphic instanceof javafx.scene.layout.HBox hbox) { + for (javafx.scene.Node child : hbox.getChildren()) { + if (child instanceof javafx.scene.image.ImageView iv) { + iconView = iv; + break; + } + } + } else if (graphic instanceof javafx.scene.image.ImageView iv) { + iconView = iv; + } + javafx.scene.Node snapTarget = iconView != null ? iconView : graphic; + if (snapTarget != null) { javafx.scene.SnapshotParameters params = new javafx.scene.SnapshotParameters(); params.setFill(javafx.scene.paint.Color.TRANSPARENT); - javafx.scene.image.WritableImage snap = source.getGraphic().snapshot(params, null); + javafx.scene.image.WritableImage snap = snapTarget.snapshot(params, null); db.setDragView(snap, snap.getWidth() / 2, snap.getHeight() / 2); } @@ -911,7 +931,6 @@ private void onObjectTileDragDetected(MouseEvent e) { e.consume(); } - /** Accept the drag as long as the dragboard carries an object-type string. */ private void onCanvasDragOver(DragEvent e) { if (running) { e.consume(); return; } if (e.getDragboard().hasString()) { @@ -919,7 +938,7 @@ private void onCanvasDragOver(DragEvent e) { double tileSize = 32 * zoom; int tx = clampTileX((int) Math.floor((e.getX() - viewOffsetX) / tileSize) - entityOffsetTileX); int ty = clampTileY((int) Math.floor((e.getY() - viewOffsetY) / tileSize) - entityOffsetTileY); - // Redraw only when the highlighted tile changes (avoids thrashing) + // Redraw only when the highlighted tile changes. if (tx != dragHighlightTileX || ty != dragHighlightTileY) { dragHighlightTileX = tx; dragHighlightTileY = ty; @@ -934,7 +953,6 @@ private void onCanvasDragOver(DragEvent e) { e.consume(); } - /** Creates a new entity at the tile where the user dropped. */ private void onCanvasDragDropped(DragEvent e) { if (guardEditor("add objects")) { e.setDropCompleted(false); e.consume(); return; } Dragboard db = e.getDragboard(); @@ -956,6 +974,7 @@ private void onCanvasDragDropped(DragEvent e) { if (engine != null && engine.getMap() != null) { if ("INTERSECTION".equalsIgnoreCase(type)) { + // Intersection markers have their own placement rules and toggle semantics. dropCompleted = handleIntersectionDrop(tx, ty); if (dropCompleted && viewportStatusLabel != null) viewportStatusLabel.setText(""); e.setDropCompleted(dropCompleted); @@ -1009,6 +1028,7 @@ private boolean handleIntersectionDrop(int tx, int ty) { } if (!alreadyMarked) { + // Existing markers can be removed in place, but new ones cannot sit on active robot/station tiles. boolean hasBlockingOccupant = engine.getMap().getEntitiesAt(position).stream() .anyMatch(entity -> entity instanceof Robot || entity instanceof ChargingStation @@ -1027,6 +1047,7 @@ private boolean handleIntersectionDrop(int tx, int ty) { drawViewport(); log((alreadyMarked ? "Removed" : "Added") + " INTERSECTION at tile (" + tx + ", " + ty + ")."); + pushAction(new IntersectionToggleAction(new Vector2D(tx, ty), !alreadyMarked)); return true; } @@ -1036,6 +1057,7 @@ private MapEntity createEntityFromType(String type, int x, int y, String name) { String upperType = type.toUpperCase(); if ("ROBOT".equals(upperType)) { Robot robot = new Robot(id, name, pos); + // Fresh robots start with the same defaults used by setup-generated configs. robot.setNav(new GreedyNavigationStrategy(42L)); robot.setSensor(new ProximitySensor()); return robot; @@ -1051,10 +1073,7 @@ private MapEntity createEntityFromType(String type, int x, int y, String name) { return new MapEntity(id, name, pos); } - // ------------------------------------------------------------------ // - // Drag existing object WITHIN canvas (mouse events) - // ------------------------------------------------------------------ // - + // Viewport Interaction private void onViewportMousePressed(MouseEvent e) { viewportStack.requestFocus(); lastMouseX = e.getX(); @@ -1062,6 +1081,7 @@ private void onViewportMousePressed(MouseEvent e) { viewportMouseX = e.getX(); viewportMouseY = e.getY(); + // While assigning rack dropoffs, the next primary click is interpreted as a station picker. if (pickingRack != null && e.getButton() == MouseButton.PRIMARY) { MapEntity hit = entityAtScreenPos(e.getX(), e.getY()); if (hit instanceof DeliveryStation ds) { @@ -1080,19 +1100,21 @@ private void onViewportMousePressed(MouseEvent e) { } } else { cancelDropoffPicking(); - log("Dropoff assignment cancelled."); + log("Dropoff assignment cancelled. Please select a drop off station"); } return; } if (e.getButton() == MouseButton.PRIMARY) { + // Intersections get first priority so a marker can be dragged even when a tile also contains an entity. Vector2D intersectionHit = intersectionAtScreenPos(e.getX(), e.getY()); if (intersectionHit != null) { selectIntersection(intersectionHit); draggingOnCanvas = null; dragStartPosition = null; + dragStartIntersection = intersectionHit; + dragIntersectionOrigin = intersectionHit; } else { - // Left click: select. If on an entity, also arm for drag. MapEntity entityHit = entityAtScreenPos(e.getX(), e.getY()); if (entityHit != null) { selectEntity(entityHit); @@ -1104,16 +1126,15 @@ private void onViewportMousePressed(MouseEvent e) { clearSelection(); draggingOnCanvas = null; dragStartPosition = null; + dragStartIntersection = null; } } } else if (e.getButton() == MouseButton.SECONDARY) { - // Right click: pan mode draggingOnCanvas = null; dragStartPosition = null; - dragStartPosition = null; + dragStartIntersection = null; } - // Update RAM display updateRamLabel(); } @@ -1127,7 +1148,7 @@ private void onViewportMouseDragged(MouseEvent e) { dragStartPosition = null; log("\u26a0 Cannot move objects while the simulation is running or has advanced past tick 0. Reset first."); } else { - // Left-drag: move selected entity – snap to nearest tile, clamped to map bounds + // Update the entity live while dragging so the viewport previews the final placement. double tileSize = 32 * zoom; int newTX = clampTileX((int) Math.floor((e.getX() - viewOffsetX) / tileSize) - entityOffsetTileX); int newTY = clampTileY((int) Math.floor((e.getY() - viewOffsetY) / tileSize) - entityOffsetTileY); @@ -1136,14 +1157,35 @@ private void onViewportMouseDragged(MouseEvent e) { if (viewportStatusLabel != null) viewportStatusLabel.setText( "dragging(" + draggingOnCanvas.getName() - + ") → (" + newTX + ", " + newTY + ")"); + + ") → (" + newTX + ", " + newTY + ")"); } else if (viewportStatusLabel != null) { viewportStatusLabel.setText("blocked at (" + newTX + ", " + newTY + ")"); } drawViewport(); } + } else if (dragStartIntersection != null && e.getButton() == MouseButton.PRIMARY) { + if (isEditorLocked()) { + dragStartIntersection = null; + } else { + double tileSize = 32 * zoom; + int newTX = clampTileX((int) Math.floor((e.getX() - viewOffsetX) / tileSize) - entityOffsetTileX); + int newTY = clampTileY((int) Math.floor((e.getY() - viewOffsetY) / tileSize) - entityOffsetTileY); + Vector2D newPos = new Vector2D(newTX, newTY); + if (!newPos.equals(dragStartIntersection)) { + // Moving an intersection is modeled as toggling off the old tile and on the new tile. + engine.toggleTrafficRuleIntersection( + (int) dragStartIntersection.getX(), + (int) dragStartIntersection.getY()); + engine.toggleTrafficRuleIntersection(newTX, newTY); + selectedIntersection = newPos; + dragStartIntersection = newPos; + if (viewportStatusLabel != null) + viewportStatusLabel.setText( + "dragging(INTERSECTION) → (" + newTX + ", " + newTY + ")"); + drawViewport(); + } + } } else if (e.getButton() == MouseButton.SECONDARY) { - // Right-drag: pan the viewport viewOffsetX += dx; viewOffsetY += dy; drawViewport(); @@ -1152,7 +1194,6 @@ private void onViewportMouseDragged(MouseEvent e) { lastMouseX = e.getX(); lastMouseY = e.getY(); - // Update RAM display updateRamLabel(); } @@ -1160,30 +1201,37 @@ private void onViewportMouseReleased(MouseEvent e) { if (draggingOnCanvas != null) { com.openrobotics.map.Vector2D endPos = draggingOnCanvas.getPosition(); if (dragStartPosition != null && !dragStartPosition.equals(endPos)) { + // Only create undo history when the entity actually changed tiles. pushAction(new MoveAction(draggingOnCanvas, dragStartPosition, endPos)); - // Sync tasks to reflect the entity's new position syncTaskPositions(dragStartPosition, endPos); } log("Moved " + draggingOnCanvas.getName() - + " to tile (" + (int)endPos.getX() + ", " + (int)endPos.getY() + ")."); + + " to tile (" + (int)endPos.getX() + ", " + (int)endPos.getY() + ")."); draggingOnCanvas = null; dragStartPosition = null; + dragStartIntersection = null; if (viewportStatusLabel != null) viewportStatusLabel.setText(""); if (viewportModeLabel != null) viewportModeLabel.setText("right-click to pan, left-click to select"); - // Persist editor changes so the play-button snapshot and restart are always fresh persistEditorChanges(); } - // Update RAM display + if (dragStartIntersection != null) { + if (dragIntersectionOrigin != null && !dragIntersectionOrigin.equals(dragStartIntersection)) { + pushAction(new IntersectionMoveAction(dragIntersectionOrigin, dragStartIntersection)); + } + log("Moved INTERSECTION to tile (" + + (int)dragStartIntersection.getX() + ", " + + (int)dragStartIntersection.getY() + ")."); + dragStartIntersection = null; + dragIntersectionOrigin = null; + if (viewportStatusLabel != null) viewportStatusLabel.setText(""); + } + updateRamLabel(); } - // ------------------------------------------------------------------ // - // Hit-testing - // ------------------------------------------------------------------ // - - /** Returns the topmost engine entity whose rendered area contains (sx, sy), or null. */ + // Hit Testing private MapEntity entityAtScreenPos(double sx, double sy) { if (engine == null || engine.getMap() == null) return null; double tileSize = 32 * zoom; @@ -1202,30 +1250,25 @@ private Vector2D intersectionAtScreenPos(double sx, double sy) { if (engine == null || !engine.usesTrafficRulesPolicy()) return null; double tileSize = 32 * zoom; - double markerInset = Math.max(3.0, tileSize * 0.22); - double radius = (tileSize - 2 * markerInset) / 2.0; for (Vector2D intersection : engine.getTrafficRuleIntersections()) { - double centerX = viewOffsetX + (intersection.getX() + entityOffsetTileX) * tileSize + tileSize / 2.0; - double centerY = viewOffsetY + (intersection.getY() + entityOffsetTileY) * tileSize + tileSize / 2.0; - double dx = sx - centerX; - double dy = sy - centerY; - if ((dx * dx) + (dy * dy) <= radius * radius) { + double px = viewOffsetX + (intersection.getX() + entityOffsetTileX) * tileSize; + double py = viewOffsetY + (intersection.getY() + entityOffsetTileY) * tileSize; + if (sx >= px && sx < px + tileSize && sy >= py && sy < py + tileSize) { return intersection; } } return null; } - // ------------------------------------------------------------------ // - // Selection & Properties panel - // ------------------------------------------------------------------ // - + // Selection and Properties private void deleteSelected() { if (selectedIntersection != null) { + Vector2D deletedIntersection = selectedIntersection; if (engine != null && engine.toggleTrafficRuleIntersection( - selectedIntersection.getX(), selectedIntersection.getY())) { - log("Deleted INTERSECTION at tile (" + selectedIntersection.getX() + ", " + selectedIntersection.getY() + ")."); + deletedIntersection.getX(), deletedIntersection.getY())) { + log("Deleted INTERSECTION at tile (" + deletedIntersection.getX() + ", " + deletedIntersection.getY() + ")."); + pushAction(new IntersectionToggleAction(deletedIntersection, false)); } selectedIntersection = null; populateOutliner(); @@ -1246,14 +1289,36 @@ private void deleteSelected() { } private void copySelected() { + if (selectedIntersection != null) { + // Intersections are positional markers — copy just arms the paste target + clipboardIntersection = selectedIntersection; + clipboardEntity = null; + log("Copied INTERSECTION at tile (" + selectedIntersection.getX() + ", " + selectedIntersection.getY() + ")."); + return; + } if (selectedEntity == null) return; clipboardEntity = selectedEntity; + clipboardIntersection = null; log("Copied " + clipboardEntity.getName() + "."); } private void pasteClipboard() { + if (clipboardIntersection != null && engine != null) { + if (guardEditor("paste")) return; + // Offset pasted markers by one tile so repeated paste stays visible. + int newX = clampTileX((int) clipboardIntersection.getX() + 1); + int newY = clampTileY((int) clipboardIntersection.getY() + 1); + engine.toggleTrafficRuleIntersection(newX, newY); + selectedIntersection = new Vector2D(newX, newY); + populateOutliner(); + drawViewport(); + log("Pasted INTERSECTION at tile (" + newX + ", " + newY + ")."); + pushAction(new IntersectionToggleAction(new Vector2D(newX, newY), true)); + return; + } if (clipboardEntity == null || engine == null || engine.getMap() == null) return; if (guardEditor("paste")) return; + // Offset copied entities by one tile, matching the intersection paste behavior. int newX = clampTileX((int)clipboardEntity.getPosition().getX() + 1); int newY = clampTileY((int)clipboardEntity.getPosition().getY() + 1); MapEntity copy = createEntityFromType( @@ -1274,8 +1339,6 @@ private void pasteClipboard() { } } - private MapEntity clipboardEntity = null; - private void clearSelection() { selectedEntity = null; selectedIntersection = null; @@ -1293,6 +1356,7 @@ private void selectEntity(MapEntity entity) { if (entity != null) { if (engine != null && engine.getMap() != null) { int idx = engine.getMap().getEntities().indexOf(entity); + // Mirror viewport selection into the outliner when the entity is currently visible there. if (outlinerListView != null && idx >= 0) outlinerListView.getSelectionModel().select(idx); } @@ -1343,8 +1407,7 @@ private void showPropertiesFor(MapEntity entity) { HBox nameBox = new HBox(8); TextField nameField = new TextField(entity.getName()); nameField.setStyle("-fx-font-size: 11;"); - // Commit rename whenever the text changes (covers programmatic setText in tests and - // direct keyboard editing) as well as on focus-lost for undo-history bookkeeping. + // Commit rename on text updates so tests and direct editing behave the same way. nameField.textProperty().addListener((obs, oldText, newText) -> { if (newText == null || newText.isBlank() || newText.equals(entity.getName())) return; if (guardEditor("rename")) { nameField.setText(entity.getName()); return; } @@ -1369,7 +1432,7 @@ private void showPropertiesFor(MapEntity entity) { xSpinner.setPrefWidth(60); ySpinner.setPrefWidth(60); xSpinner.valueProperty().addListener((obs, oldVal, newVal) -> { - if (newVal == null || oldVal == null) return; + if (newVal == null || oldVal == null || newVal == (int) entity.getPosition().getX()) return; if (guardEditor("move")) { xSpinner.getValueFactory().setValue(oldVal); return; } int targetX = newVal; int targetY = (int) entity.getPosition().getY(); @@ -1386,7 +1449,7 @@ private void showPropertiesFor(MapEntity entity) { } }); ySpinner.valueProperty().addListener((obs, oldVal, newVal) -> { - if (newVal == null || oldVal == null) return; + if (newVal == null || oldVal == null || newVal == (int) entity.getPosition().getY()) return; if (guardEditor("move")) { ySpinner.getValueFactory().setValue(oldVal); return; } int targetX = (int) entity.getPosition().getX(); int targetY = newVal; @@ -1410,13 +1473,12 @@ private void showPropertiesFor(MapEntity entity) { propertiesPanel.getChildren().add(posBox); if (entity instanceof Robot robot) { - // Navigation algorithm dropdown + // Robot properties expose the live navigation/sensor configuration used by the engine. HBox algoBox = new HBox(8); algoBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT); ComboBox algoCombo = new ComboBox<>( FXCollections.observableArrayList("GREEDY", "BUG", "RTA_STAR")); algoCombo.setPrefWidth(120); - // Determine current algorithm from the robot's nav strategy String detectedAlgo = robot.getNav() != null ? robot.getNav().toString() : "GREEDY"; final String currentAlgo = algoCombo.getItems().contains(detectedAlgo) ? detectedAlgo : "GREEDY"; algoCombo.setValue(currentAlgo); @@ -1435,7 +1497,6 @@ private void showPropertiesFor(MapEntity entity) { algoBox.getChildren().addAll(new Label("Algorithm:"), algoCombo); propertiesPanel.getChildren().add(algoBox); - // Sensor dropdown HBox sensorBox = new HBox(8); sensorBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT); ComboBox sensorCombo = new ComboBox<>( @@ -1458,29 +1519,49 @@ private void showPropertiesFor(MapEntity entity) { sensorBox.getChildren().addAll(new Label("Sensor:"), sensorCombo); propertiesPanel.getChildren().add(sensorBox); - // Battery level spinner HBox batteryBox = new HBox(8); batteryBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT); float maxBattery = robot.getConfig().batteryCapacity; + Spinner batterySpinner = new Spinner<>( - new SpinnerValueFactory.DoubleSpinnerValueFactory(0, maxBattery, robot.getBattery(), 1.0)); + new SpinnerValueFactory.DoubleSpinnerValueFactory(0.0, (double) maxBattery, (double) robot.getBattery(), 1.0)); batterySpinner.setPrefWidth(90); batterySpinner.setEditable(true); + + // Restrict input to positive digits and a single decimal point + batterySpinner.getEditor().setTextFormatter(new TextFormatter<>(change -> { + String newText = change.getControlNewText(); + if (newText.matches("\\d*(\\.\\d*)?")) { + return change; + } + return null; // Reject the change + })); + + // Force spinner to commit text value when the user clicks away + batterySpinner.getEditor().focusedProperty().addListener((obs, wasFocused, isFocused) -> { + if (!isFocused) { + batterySpinner.increment(0); + } + }); + batterySpinner.valueProperty().addListener((obs, oldVal, newVal) -> { - if (newVal != null && oldVal != null && !newVal.equals(oldVal)) { - if (guardEditor("change battery")) { - batterySpinner.getValueFactory().setValue(oldVal); - return; - } - robot.setBattery(newVal.floatValue()); - drawViewport(); - pushAction(new BatteryChangeAction(robot, oldVal.floatValue(), newVal.floatValue())); + if (newVal == null || oldVal == null || newVal.floatValue() == robot.getBattery()) return; + if (guardEditor("change battery")) { + // Revert spinner if editor is locked + Platform.runLater(() -> batterySpinner.getValueFactory().setValue(oldVal)); + return; } + float newBat = newVal.floatValue(); + float oldBat = oldVal.floatValue(); + + robot.setBattery(newBat); + drawViewport(); + pushAction(new BatteryChangeAction(robot, oldBat, newBat)); }); + batteryBox.getChildren().addAll(new Label("Battery:"), batterySpinner); propertiesPanel.getChildren().add(batteryBox); - // Read-only state display Label stateLabel = new Label("State: " + robot.getState()); stateLabel.setStyle("-fx-text-fill: #666;"); propertiesPanel.getChildren().add(stateLabel); @@ -1488,37 +1569,60 @@ private void showPropertiesFor(MapEntity entity) { Label stationProps = new Label("Station configuration"); propertiesPanel.getChildren().add(stationProps); } else if (entity instanceof Rack rack) { + // Rack-specific controls only matter when the run uses manual task assignment. boolean globalManual = engine != null && engine.isManualTaskAssignment(); if (globalManual) { - // ── Number of boxes (manual mode only) ── HBox boxCountBox = new HBox(8); boxCountBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + Spinner boxCountSpinner = new Spinner<>( - new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 99, rack.getBoxCount())); + new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 99, rack.getBoxCount())); boxCountSpinner.setPrefWidth(80); boxCountSpinner.setEditable(true); + + // Restrict input to positive integers only (no decimals, no negatives) + boxCountSpinner.getEditor().setTextFormatter(new TextFormatter<>(change -> { + if (change.getControlNewText().matches("\\d*")) { + return change; + } + return null; // Reject non-digit characters + })); + + // Force spinner to commit text value when the user clicks away + boxCountSpinner.getEditor().focusedProperty().addListener((obs, wasFocused, isFocused) -> { + if (!isFocused) { + boxCountSpinner.increment(0); + } + }); + boxCountSpinner.valueProperty().addListener((obs, oldV, newV) -> { - if (newV == null || oldV == null || newV.equals(oldV)) return; + if (newV == null || oldV == null || newV.equals(rack.getBoxCount())) return; + if (guardEditor("change box count")) { - boxCountSpinner.getValueFactory().setValue(oldV); + // Revert spinner if editor is locked + javafx.application.Platform.runLater(() -> boxCountSpinner.getValueFactory().setValue(oldV)); return; } + rack.setBoxCount(newV); persistEditorChanges(); }); - boxCountBox.getChildren().addAll(new Label("Number of boxes:"), boxCountSpinner); + + boxCountBox.getChildren().addAll(new Label("Number of tasks:"), boxCountSpinner); propertiesPanel.getChildren().add(boxCountBox); - // ── Manual dropoff assignment checkbox (manual mode only) ── HBox manualBox = new HBox(8); manualBox.setAlignment(javafx.geometry.Pos.CENTER_LEFT); CheckBox manualCheck = new CheckBox("Manual dropoff assignment"); manualCheck.setSelected(rack.isManualDropoffAssignment()); + VBox dropoffArrayBox = new VBox(4); dropoffArrayBox.setVisible(rack.isManualDropoffAssignment()); dropoffArrayBox.setManaged(rack.isManualDropoffAssignment()); + manualCheck.selectedProperty().addListener((obs, oldV, newV) -> { + if (newV == rack.isManualDropoffAssignment()) return; if (guardEditor("toggle manual assignment")) { manualCheck.setSelected(oldV); return; @@ -1529,10 +1633,10 @@ private void showPropertiesFor(MapEntity entity) { renderRackDropoffArray(rack, dropoffArrayBox); persistEditorChanges(); }); + manualBox.getChildren().add(manualCheck); propertiesPanel.getChildren().add(manualBox); - // ── Dropoff array (shown only when manual checkbox is on) ── renderRackDropoffArray(rack, dropoffArrayBox); propertiesPanel.getChildren().add(dropoffArrayBox); } @@ -1542,14 +1646,13 @@ private void showPropertiesFor(MapEntity entity) { private void renderRackDropoffArray(Rack rack, VBox container) { container.getChildren().clear(); - // Header HBox header = new HBox(6); header.setAlignment(javafx.geometry.Pos.CENTER_LEFT); Label headerLabel = new Label("Valid Dropoff Points"); headerLabel.setStyle("-fx-font-weight: bold;"); Tooltip headerTip = new Tooltip( - "Boxes are distributed across the valid dropoff points using round-robin. " + - "Null slots are ignored. At least one non-null slot is required to play."); + "Boxes are distributed across the valid dropoff points using round-robin. " + + "Null slots are ignored. At least one non-null slot is required to play."); Tooltip.install(headerLabel, headerTip); Button addBtn = new Button("+"); addBtn.setOnAction(ev -> { @@ -1561,7 +1664,7 @@ private void renderRackDropoffArray(Rack rack, VBox container) { header.getChildren().addAll(headerLabel, addBtn); container.getChildren().add(header); - // Build station lookup + // Resolve stored UUIDs back to live delivery stations for display. java.util.Map stationById = new java.util.HashMap<>(); if (engine != null && engine.getMap() != null) { for (MapEntity e : engine.getMap().getEntities()) { @@ -1586,7 +1689,7 @@ private void renderRackDropoffArray(Rack rack, VBox container) { DeliveryStation ds = stationById.get(id); display = ds == null ? "(deleted station)" : ds.getName() + " (" + (int)ds.getPosition().getX() - + ", " + (int)ds.getPosition().getY() + ")"; + + ", " + (int)ds.getPosition().getY() + ")"; } Label displayLabel = new Label(display); if (id == null || stationById.get(id) == null) { @@ -1613,8 +1716,9 @@ private void beginDropoffPicking(Rack rack, int slotIndex) { this.pickingRack = rack; this.pickingSlot = slotIndex; if (tipLabel != null) { + // Reuse the main viewport click handler for the actual station assignment. tipLabel.setText("TIP: Click a delivery station to assign it to slot #" - + slotIndex + ". Click elsewhere to cancel."); + + slotIndex + ". Click elsewhere to cancel."); } viewportStack.setCursor(javafx.scene.Cursor.CROSSHAIR); drawViewport(); @@ -1665,7 +1769,6 @@ private void clearPropertiesPanel() { new Label("Select an object.")); } - /** Updates the selection text below the viewport with a tip. */ private void updateSelectionLabel() { if (viewportStatusLabel != null) { String tip = ViewportTips.getRandomSelectionTip(); @@ -1673,7 +1776,6 @@ private void updateSelectionLabel() { } } - /** Starts the tip rotation timer, cycling a new tip every 5 seconds. */ private void startTipRotation() { tipRotationLoop = new Timeline(new KeyFrame(Duration.seconds(5), e -> { if (tipLabel != null) { @@ -1684,14 +1786,7 @@ private void startTipRotation() { tipRotationLoop.play(); } - // ------------------------------------------------------------------ // - // Add Object / Outliner - // ------------------------------------------------------------------ // - - /** - * Handles a click on one of the Add-Object tiles (§4.1.3 B, zone 1). - * A single click logs the selection; a double-click opens the Object Description dialog. - */ + // Add Object and Outliner @FXML private void onAddObjectClick(MouseEvent e) { Button source = (Button) e.getSource(); @@ -1710,7 +1805,6 @@ private void onAddObjectClick(MouseEvent e) { } } - /** Handles selection in the Outliner list. */ @FXML private void onOutlinerSelect(MouseEvent e) { onOutlinerSelect(); @@ -1739,20 +1833,20 @@ private void filterOutliner(@SuppressWarnings("unused") String query) { populateOutliner(); } - /** Rebuilds the outliner list from the current engine map. - * Compares new entries against current list to avoid unnecessary layout invalidation. */ private void populateOutliner() { if (outlinerListView == null || engine == null || engine.getMap() == null || engine.getMap().getEntities() == null) return; String filter = (outlinerSearchField != null && outlinerSearchField.getText() != null) ? outlinerSearchField.getText().toLowerCase() : ""; List newItems = new ArrayList<>(); List newBacking = new ArrayList<>(); + // Keep entity rows first so editor selections stay predictable. for (MapEntity e : engine.getMap().getEntities()) { - String icon = (e instanceof Robot) ? "\ud83e\udd16 " // 🤖 robot - : (e instanceof Rack) ? "\ud83d\udce6 " // 📦 rack/shelf - : (e instanceof Station) ? "\u26a1 " // ⚡ station - : (e instanceof Obstacle) ? "\ud83e\uddf1 " // 🧱 wall/obstacle - : "\u25ab "; // ▫ generic entity + String icon = (e instanceof Robot) ? "\ud83e\udd16 " // robot + : (e instanceof Rack) ? "\ud83d\udce6 " // rack/shelf + : (e instanceof ChargingStation) ? "\u26a1 " // charging station + : (e instanceof DeliveryStation) ? "\uD83C\uDFC1 " // delivery station + : (e instanceof Obstacle) ? "\ud83e\uddf1 " // wall/obstacle + : "\u25ab "; // generic entity String entry = icon + e.getName() + " " + e.getPosition(); if (filter.isEmpty() || entry.toLowerCase().contains(filter)) { newItems.add(entry); @@ -1772,7 +1866,7 @@ private void populateOutliner() { } } - // Only update the ListView if content actually changed to avoid layout thrashing + // Skip redundant ListView updates to avoid unnecessary layout work. if (!newItems.equals(outlinerListView.getItems())) { outlinerListView.getItems().setAll(newItems); outlinerBacking.clear(); @@ -1785,19 +1879,7 @@ private void populateOutliner() { } } - // ------------------------------------------------------------------ // - // Properties panel reset - // ------------------------------------------------------------------ // - - @FXML - private void onResetStringProp() { - if (strPropField != null) strPropField.setText("Hello"); - } - - // ------------------------------------------------------------------ // - // Playback Controls (§4.1.3 B, zone 5) - // ------------------------------------------------------------------ // - + // Playback Controls @FXML private void onPlay() { if (engine == null) { @@ -1810,7 +1892,7 @@ private void onPlay() { return; } - logPollingTimeline.play(); // start regular polling + logPollingTimeline.play(); if (running) { if (paused) { @@ -1819,22 +1901,17 @@ private void onPlay() { simStatusLabel.setText("RUNNING"); simStatusLabel.setStyle("-fx-text-fill: #2E9E5B; -fx-font-weight: bold;"); } - playBtn.setStyle("-fx-background-color: #2E9E5B;"); - pauseBtn.setStyle("-fx-background-color: #FFB3B3;"); + playBtn.setStyle("-fx-background-color: #1a743f;"); + pauseBtn.setStyle("-fx-background-color: #c9605a;"); startLoop(); log("Simulation resumed."); - // Update RAM display updateRamLabel(); } } else { - // Ensure a baseline snapshot exists before starting (editor actions save it continuously, - // but if no edits were made this is the first snapshot). + // Ensure a tick-0 baseline exists even if the editor has not been modified yet. if (initialSnapshotPath == null && engine != null) { saveEditorBaseline(); } - - - running = true; paused = false; if (simStatusLabel != null) { @@ -1842,10 +1919,10 @@ private void onPlay() { simStatusLabel.setStyle("-fx-text-fill: #2E9E5B; -fx-font-weight: bold;"); } - playBtn.setStyle("-fx-background-color: #2E9E5B;"); - pauseBtn.setStyle("-fx-background-color: #FFB3B3;"); + playBtn.setStyle("-fx-background-color: #1a743f;"); + pauseBtn.setStyle("-fx-background-color: #c9605a;"); - // ── Validate manual-mode racks before generating tasks ── + // Manual mode requires every participating rack to have at least one valid dropoff. if (engine.getMap() != null) { java.util.Set stationIds = new java.util.HashSet<>(); for (MapEntity me : engine.getMap().getEntities()) { @@ -1855,7 +1932,7 @@ private void onPlay() { for (MapEntity me : engine.getMap().getEntities()) { if (me instanceof Rack r && r.isManualDropoffAssignment()) { boolean hasValid = r.getValidDropoffIds().stream() - .anyMatch(uid -> uid != null && stationIds.contains(uid)); + .anyMatch(uid -> uid != null && stationIds.contains(uid)); if (!hasValid) offenders.add(r.getName()); } } @@ -1873,18 +1950,16 @@ private void onPlay() { alert.setTitle("Cannot start simulation"); alert.setHeaderText("Manual-mode racks have no valid dropoff points"); alert.setContentText( - "The following racks use Manual Dropoff Assignment but their pool is empty or all-null:\n\n \u2022 " - + String.join("\n \u2022 ", offenders) - + "\n\nAssign at least one delivery station per rack, or disable Manual Dropoff Assignment."); + "The following racks use Manual Dropoff Assignment but their pool is empty or all-null:\n\n \u2022 " + + String.join("\n \u2022 ", offenders) + + "\n\nAssign at least one delivery station per rack, or disable Manual Dropoff Assignment."); alert.showAndWait(); log("\u26a0 Play aborted: " + offenders.size() + " rack(s) have an empty manual dropoff pool."); return; } } - // Generate tasks if the dispatcher is empty. - // Automatic mode: exactly engine.getMaxTasks() tasks, random rack+station pairs. - // Manual mode: one task per rack box using each rack's configured dropoff pool. + // Seed the dispatcher on first play if setup did not pre-populate tasks. if (engine.getDispatcher().getAllQueuedTasks().isEmpty() && engine.getMap() != null) { List generated; if (engine.isManualTaskAssignment()) { @@ -1893,40 +1968,35 @@ private void onPlay() { } else { generated = com.openrobotics.task.TaskGenerator.generateAutomaticTasks( engine.getMap(), engine.getMaxTasks(), engine.getSeed()); + + // No task assignments could be generated due to invalid map configuration + if (generated.isEmpty()) { + log("\u26a0 No tasks could be generated."); + engine.setSimulationError(SimulationError.INVALID_MAP_CONFIGURATION); + handleSimulationFailure(); + return; + } } if (!generated.isEmpty()) { engine.getDispatcher().addTasks(generated); log("Auto-generated " + generated.size() + " tasks from map racks and delivery stations."); - } else { - log("\u26a0 No tasks could be generated. Ensure the map has at least one rack and one delivery station."); } } - // Logging simulation run start event + // Logging simulation start event SimulationRunRecordBuilder simRunRecordBuilder = new SimulationRunRecordBuilder(engine); SimulationRunRecord record = simRunRecordBuilder.buildSimulationStartRecord(); Logger.logSimulationRunEvent(SimulationRunEvent.RUN_STARTED, record); - // Logging task creation events for existing tasks in dispatcher at simulation start - List existingTasks = engine.getDispatcher().getAllQueuedTasks(); - - for (Task task : existingTasks) { - WorkloadTaskRecordBuilder taskRecordBuilder = new WorkloadTaskRecordBuilder(engine.getRunId(), task); - WorkloadTaskRecord taskRecord = taskRecordBuilder.buildTaskCreationRecord(engine.getTickCounter()); - long artificialId = Logger.logTaskEvent(TaskEvent.TASK_CREATED, taskRecord); - task.setArtificialId(artificialId); // setting artificial ID for tracking in logs - } - startLoop(); log("Simulation started."); - // Update RAM display updateRamLabel(); } } @FXML private void onPause() { - logPollingTimeline.stop(); // stop regular polling for sim logs + logPollingTimeline.stop(); if (running && !paused) { paused = true; @@ -1935,10 +2005,9 @@ private void onPause() { simStatusLabel.setText("PAUSED"); simStatusLabel.setStyle("-fx-text-fill: #E0B200; -fx-font-weight: bold;"); } - playBtn.setStyle("-fx-background-color: #90EE90;"); - pauseBtn.setStyle("-fx-background-color: #C23B42;"); + playBtn.setStyle("-fx-background-color: #599068;"); + pauseBtn.setStyle("-fx-background-color: #C0392B;"); log("Simulation paused."); - // Update RAM display updateRamLabel(); } } @@ -1956,14 +2025,12 @@ private void onStop() { if (playBtn != null) playBtn.setStyle(""); if (pauseBtn != null) pauseBtn.setStyle(""); log("Simulation stopped."); - // Update RAM display updateRamLabel(); } @FXML private void onRestart() { - // If already at tick 0, not running, and there is nothing to reload, nothing to reset. - String earlyReloadPath = initialSnapshotPath != null ? initialSnapshotPath : AppState.getConfigPath(); + String earlyReloadPath = resolveReloadPath(); if (localTick == 0 && !running && !simulationFailed && earlyReloadPath == null) { log("Already at tick 0. Nothing to reset."); return; @@ -1973,6 +2040,7 @@ private void onRestart() { animationProgress = 0.0; prevRobotPositions.clear(); selectedEntity = null; + // A restart returns to the saved baseline, so editor history from the previous run no longer applies. undoStack.clear(); redoStack.clear(); onStop(); @@ -1980,9 +2048,9 @@ private void onRestart() { simulationFailed = false; lastSeenLogId = 0; AppState.setSimulationTick(0); - // Reload from the editor baseline snapshot (continuously updated on every editor action). - // This restores the most recent editor state, not the original config file. - String reloadPath = initialSnapshotPath != null ? initialSnapshotPath : AppState.getConfigPath(); + AppState.setSimulationLogCursor(0); + // Reload the latest editor baseline rather than the original config file. + String reloadPath = resolveReloadPath(); if (reloadPath != null) { SimulationEngine reloaded = new SimulationEngine(reloadPath); if (reloaded == null || reloaded.getMap() == null || reloaded.getInitError() != null) { @@ -2000,7 +2068,6 @@ private void onRestart() { AppState.setEngine(engine); } - // If no engine is loaded, restart still resets the UI state safely. if (engine == null) { if (tickDisplayLabel != null) tickDisplayLabel.setText("TICK 0"); if (simProgressBar != null) simProgressBar.setProgress(0); @@ -2012,7 +2079,6 @@ private void onRestart() { return; } - // Resetting some internal engine state for new simulation run engine.reset(); if (tickDisplayLabel != null) tickDisplayLabel.setText("TICK 0"); @@ -2022,16 +2088,16 @@ private void onRestart() { simStatusLabel.setStyle("-fx-text-fill: #2E9E5B; -fx-font-weight: bold;"); } log("Simulation reset."); - // Update RAM display updateRamLabel(); populateOutliner(); drawViewport(); - // Reset simulation logs text area if (logArea != null) { System.out.println("[SimulationController] Clearing log area on simulation reset."); logArea.clear(); } + AppState.setSimulationLogText(""); + AppState.setSimulationLogCursor(0); } @FXML @@ -2046,11 +2112,10 @@ private void onNextFrame() { } doTick(); log("Step \u2192 TICK " + localTick); - // Update RAM display updateRamLabel(); - Logger.flushRobotEvents(); // ensure all robot events are flushed after stepping - fetchLogsAsync(); // fetch logs after stepping to get latest events + Logger.flushRobotEvents(); + fetchLogsAsync(); } @FXML private void onSpeed1() { setSpeed(1); log("Speed set to ×1."); } @@ -2058,15 +2123,12 @@ private void onNextFrame() { @FXML private void onSpeed3() { setSpeed(3); log("Speed set to ×3."); } @FXML private void onSpeed10() { setSpeed(10); log("Speed set to ×10."); } + // Simulation Loop private void setSpeed(int factor) { speedFactor = factor; if (running && !paused) startLoop(); } - // ------------------------------------------------------------------ // - // Simulation loop helpers - // ------------------------------------------------------------------ // - private void startLoop() { if (simLoop != null) simLoop.stop(); simLoop = new Timeline(new KeyFrame( @@ -2078,7 +2140,7 @@ private void startLoop() { } private void stopLoop() { - logPollingTimeline.stop(); // stop regular polling for sim logs + logPollingTimeline.stop(); if (simLoop != null) { simLoop.stop(); @@ -2086,15 +2148,17 @@ private void stopLoop() { } System.out.println("[SimulationController] Flushing logs and fetching remaining logs on stop..."); - Logger.flushRobotEvents(); // ensure all robot events are flushed when stopping - fetchLogsSync(); // fetch any remaining logs synchronously on stop + Logger.flushRobotEvents(); + fetchLogsSync(); } - /** Stops all timelines and unbinds canvas properties. Called by ScreenNavigator before replacing this screen. */ @Override public void cleanup() { shutdown(); stopLoop(); + if (AppState.hasEngine()) { + persistOutputState(); + } if (tipRotationLoop != null) { tipRotationLoop.stop(); tipRotationLoop = null; } if (animTimeline != null) { animTimeline.stop(); animTimeline = null; } warehouseCanvas.widthProperty().unbind(); @@ -2107,6 +2171,7 @@ private void doTick() { if (animating) return; if (engine.getRobots() != null) { + // Capture pre-tick robot positions so the renderer can tween to the new state. prevRobotPositions.clear(); for (Robot robot : engine.getRobots()) { prevRobotPositions.put(robot.getId(), new com.openrobotics.map.Vector2D( @@ -2115,18 +2180,15 @@ private void doTick() { } if (!engine.tick()) { - // Sandbox mode: no robots means an empty map used for layout/stepping only — - // treat the tick as a no-op success so the step counter still advances. - if (engine.getSimulationError() == SimulationError.NO_ROBOTS_SPAWNED) { - // fall through to the localTick++ / display-update block below - } else if (engine.getSimulationError() != SimulationError.NONE) { + if (engine.getSimulationError() != SimulationError.NONE) { handleSimulationFailure(); - return; } else { handleSimulationComplete(); - return; } + + return; } + localTick++; AppState.setSimulationTick(localTick); @@ -2144,6 +2206,7 @@ private void doTick() { } private void handleSimulationComplete() { + drawViewport(); // drawing the final viewport stopLoop(); running = false; paused = false; @@ -2159,10 +2222,8 @@ private void handleSimulationComplete() { log("Simulation complete at TICK " + localTick + "."); } - /** - * Handles simulation failure (e.g. all robots died) - */ private void handleSimulationFailure() { + drawViewport(); // drawing the final viewport stopLoop(); running = false; paused = false; @@ -2176,7 +2237,7 @@ private void handleSimulationFailure() { } if (playBtn != null) playBtn.setStyle(""); if (pauseBtn != null) pauseBtn.setStyle(""); - log("Simulation failed: " + engine.getSimulationErrorMessage()); // logging simulation error message + log("Simulation failed: " + engine.getSimulationErrorMessage()); log("Simulation failed at TICK " + localTick + "."); } @@ -2186,6 +2247,7 @@ private void startAnimation() { animationProgress = 0.0; animTimeline = new Timeline(); final int totalFrames = ANIMATION_FRAMES; + // Spread each logical tick across a short frame sequence for smoother robot movement. for (int i = 0; i < totalFrames; i++) { final int frame = i; animTimeline.getKeyFrames().add(new KeyFrame( @@ -2205,10 +2267,7 @@ private void startAnimation() { animTimeline.play(); } - // ------------------------------------------------------------------ // - // Viewport zoom buttons - // ------------------------------------------------------------------ // - + // Viewport Controls @FXML private void onZoomIn() { double cx = warehouseCanvas.getWidth() / 2; double cy = warehouseCanvas.getHeight() / 2; @@ -2243,10 +2302,10 @@ private void onReturnToOrigin() { private void onToggleSidebar() { if (sidebarPanel == null || mainSplitPane == null) return; if (toggleSidebarItem.isSelected()) { - // Restore: reposition divider after the current layout pass + // Restore the saved divider position after the current layout pass. Platform.runLater(() -> mainSplitPane.setDividerPosition(0, savedSidebarDivider)); } else { - // Collapse: save position, then push divider to 0 + // Save the divider before collapsing the sidebar completely. if (mainSplitPane.getDividerPositions().length > 0) { savedSidebarDivider = mainSplitPane.getDividerPositions()[0]; } @@ -2258,10 +2317,10 @@ private void onToggleSidebar() { private void onToggleConsole() { if (consoleShell == null || viewportConsoleSplit == null) return; if (toggleConsoleItem.isSelected()) { - // Restore: reposition divider after the current layout pass + // Restore the saved divider position after the current layout pass. Platform.runLater(() -> viewportConsoleSplit.setDividerPosition(0, savedConsoleDivider)); } else { - // Collapse: save position, then push divider to 1.0 + // Save the divider before collapsing the console completely. if (viewportConsoleSplit.getDividerPositions().length > 0) { savedConsoleDivider = viewportConsoleSplit.getDividerPositions()[0]; } @@ -2269,18 +2328,24 @@ private void onToggleConsole() { } } - // ------------------------------------------------------------------ // - // Console - // ------------------------------------------------------------------ // - + // Console and Configuration @FXML private void onClearConsole() { if (consoleArea != null) consoleArea.clear(); + AppState.setSimulationConsoleText(""); } @FXML private void onSaveConfig() { - ScreenNavigator.openDialog(ScreenNavigator.DIALOG_SAVE_CONFIG, "Save Configuration"); + ScreenNavigator.openDialog(ScreenNavigator.DIALOG_SAVE_CONFIG, "Save Configuration", ctrl -> { + if (ctrl instanceof SaveConfigController scc) { + SimulationEngine engine = AppState.getEngine(); + String runName = engine != null ? engine.getRunName() : null; + if (runName != null && !runName.isEmpty()) { + scc.setDefaultFileName(runName); + } + } + }); } @FXML @@ -2292,8 +2357,7 @@ private void onLoadConfig() { java.io.File file = lcc.getSelectedFile(); if (file == null) return; - // Full reset: stop the loop, clear transient editor state, drop undo/redo, - // then rebuild the engine from the chosen file. + // Clear transient editor state before rebuilding the engine from the chosen file. stopLoop(); running = false; paused = false; @@ -2302,6 +2366,7 @@ private void onLoadConfig() { selectedEntity = null; undoStack.clear(); redoStack.clear(); + clearPersistedOutputState(); SimulationEngine loaded = new SimulationEngine(file.getAbsolutePath()); if (loaded == null || loaded.getMap() == null || loaded.getInitError() != null) { @@ -2329,12 +2394,18 @@ private void onLoadConfig() { private void log(String message) { System.out.println("[SimulationController] " + message); - if (consoleArea != null) consoleArea.appendText(message + "\n"); + if (consoleArea != null) { + consoleArea.appendText(message + "\n"); + AppState.setSimulationConsoleText(consoleArea.getText()); + } else { + String existing = AppState.getSimulationConsoleText(); + AppState.setSimulationConsoleText((existing == null ? "" : existing) + message + "\n"); + } } + // Database Logging /** - * Asynchronously fetches simulation logs from the database using a background thread to - * avoid blocking/glitching the UI + * Fetches simulation logs on a background thread to avoid blocking the UI. */ public void fetchLogsAsync() { if (engine == null) return; @@ -2354,9 +2425,10 @@ protected Object call() { }; task.setOnSucceeded(e -> { + // onSucceeded runs on the FX thread, so it is safe to append directly to the log area here. List logs = (List) task.getValue(); long start = System.currentTimeMillis(); - updateLogsArea(logs); // safe: runs on UI thread + updateLogsArea(logs); System.out.println("[SimulationController] Updated sim log area in " + (System.currentTimeMillis() - start) + " ms."); }); @@ -2364,11 +2436,11 @@ protected Object call() { task.getException().printStackTrace(); }); - executor.submit(task); // runs the task on a background thread + executor.submit(task); } /** - * Synchronously fetches simulation logs from the database and updates the logs area. + * Fetches simulation logs synchronously and updates the log area. */ public void fetchLogsSync() { if (engine == null) return; @@ -2385,10 +2457,7 @@ public void fetchLogsSync() { } } - /** - * Updates the logs area with the provided list of SimLogRecords. - * @param logs the list of SimLogRecords to display, or null if an error occurred during fetching - */ + // Append only the newly fetched log rows and advance the database cursor. private void updateLogsArea(List logs) { if (logs == null || logs.isEmpty()) return; @@ -2399,13 +2468,58 @@ private void updateLogsArea(List logs) { lastSeenLogId = log.getId(); } - logArea.appendText(sb.toString()); + if (logArea != null) { + logArea.appendText(sb.toString()); + AppState.setSimulationLogText(logArea.getText()); + } else { + String existing = AppState.getSimulationLogText(); + AppState.setSimulationLogText((existing == null ? "" : existing) + sb); + } + AppState.setSimulationLogCursor(lastSeenLogId); + } + + private boolean restorePersistedOutputState() { + lastSeenLogId = AppState.getSimulationLogCursor(); + localTick = AppState.getSimulationTick(); + + boolean restored = false; + + if (tickDisplayLabel != null) tickDisplayLabel.setText("TICK " + localTick); + if (consoleArea != null && AppState.getSimulationConsoleText() != null) { + consoleArea.setText(AppState.getSimulationConsoleText()); + restored = restored || !consoleArea.getText().isBlank(); + } + if (logArea != null && AppState.getSimulationLogText() != null) { + logArea.setText(AppState.getSimulationLogText()); + restored = restored || !logArea.getText().isBlank(); + } + return restored; + } + + private void persistOutputState() { + if (consoleArea != null) { + AppState.setSimulationConsoleText(consoleArea.getText()); + } + if (logArea != null) { + AppState.setSimulationLogText(logArea.getText()); + } + AppState.setSimulationLogCursor(lastSeenLogId); } - // ------------------------------------------------------------------ // - // Navigation & Menu - // ------------------------------------------------------------------ // + private void clearPersistedOutputState() { + if (consoleArea != null) { + consoleArea.clear(); + } + if (logArea != null) { + logArea.clear(); + } + AppState.setSimulationConsoleText(""); + AppState.setSimulationLogText(""); + AppState.setSimulationLogCursor(0); + lastSeenLogId = 0; + } + // Navigation @FXML private void onTabEditor() { if (editorTabBtn != null) { editorTabBtn.getStyleClass().setAll("tab-btn-active"); } if (resultsTabBtn != null) { resultsTabBtn.getStyleClass().setAll("tab-btn"); } @@ -2446,10 +2560,6 @@ private void onExit() { if (ScreenNavigator.confirmExit()) javafx.application.Platform.exit(); } - /** - * Toggles the shortcut reference overlay on/off. - * The overlay sits inside viewportStack so it floats above the canvas. - */ @FXML private void onToggleShortcutOverlay() { boolean nowVisible = !shortcutOverlay.isVisible(); @@ -2457,11 +2567,8 @@ private void onToggleShortcutOverlay() { shortcutOverlay.setManaged(nowVisible); } - /** - * Updates any pending tasks whose pickup or dropoff location matches {@code oldPos} - * to use {@code newPos} instead. Called whenever an entity is moved in the editor - * so that tasks remain aligned with the actual rack/station positions on the map. - */ + // Editor Persistence + // Keep queued tasks aligned when their source entity moves in the editor. private void syncTaskPositions(Vector2D oldPos, Vector2D newPos) { if (engine == null || engine.getDispatcher() == null) return; int updated = 0; @@ -2479,32 +2586,41 @@ private void syncTaskPositions(Vector2D oldPos, Vector2D newPos) { } if (updated > 0) { log("Updated " + updated + " task(s) to reflect entity move from " + oldPos + " → " + newPos + "."); - populateOutliner(); // refresh task list in the outliner + populateOutliner(); } } - /** - * Saves the current engine state to a temp file and updates - * so that pressing Play (or Restart) always uses the latest editor layout, including - * any rack/entity moves made before the simulation has started. - */ + // Save the latest pre-run editor state so play and restart reload the current layout. private void persistEditorChanges() { - if (engine == null || running) return; // never overwrite a running sim + if (engine == null || running) return; try { - // Reuse the existing snapshot file if one already exists, otherwise create fresh - String savePath = initialSnapshotPath != null ? initialSnapshotPath : AppState.getConfigPath(); + // Reuse the existing snapshot file when possible. + String savePath = resolveReloadPath(); if (savePath == null) { java.io.File tmp = java.io.File.createTempFile("openrobotics_editor_", ".json"); tmp.deleteOnExit(); savePath = tmp.getAbsolutePath(); initialSnapshotPath = savePath; } + // Keep restart/play anchored to the latest editable layout rather than the original import. engine.configSaving(savePath); + initialSnapshotPath = savePath; + AppState.setEditorBaselinePath(savePath); } catch (Exception ex) { log("\u26a0 Could not auto-save editor changes: " + ex.getMessage()); } } + private String resolveReloadPath() { + if (initialSnapshotPath != null && !initialSnapshotPath.isBlank()) { + return initialSnapshotPath; + } + if (AppState.hasEditorBaselinePath()) { + return AppState.getEditorBaselinePath(); + } + return AppState.getConfigPath(); + } + private void updateRamLabel() { if (ramLabel != null) { long usedKb = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024; @@ -2512,18 +2628,19 @@ private void updateRamLabel() { } } + // Shutdown /** - * Performs necessary cleanup when the application is closing + * Performs controller shutdown cleanup before the application closes. */ public void shutdown() { - // stop log polling timeline + // Stop polling first so no new background work is queued during shutdown. if (logPollingTimeline != null) { logPollingTimeline.stop(); } - // shutdown the executor to stop any ongoing log fetching tasks + // Interrupt any in-flight log fetches after polling is stopped. executor.shutdownNow(); - Logger.flushRobotEvents(); // ensure all logs are flushed before shutdown + Logger.flushRobotEvents(); } -} +} \ No newline at end of file diff --git a/open-robotics/src/main/java/com/openrobotics/controllers/WelcomeController.java b/open-robotics/src/main/java/com/openrobotics/controllers/WelcomeController.java index ad60483e..d6e52fa5 100644 --- a/open-robotics/src/main/java/com/openrobotics/controllers/WelcomeController.java +++ b/open-robotics/src/main/java/com/openrobotics/controllers/WelcomeController.java @@ -5,39 +5,26 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; import java.awt.Desktop; import java.util.logging.Logger; -/** - * Controller for {@code WelcomeScreen.fxml}. - * - *

The welcome screen (§4.1.1) shows the application logo, branding, and - * a changelog panel. Pressing START SETUP navigates to the Setup screen. - */ +/** Controller for WelcomeScreen.fxml; shows branding and changelog, and routes {@code START SETUP} to the setup screen. */ public class WelcomeController { + // Logging private static final Logger LOGGER = Logger.getLogger(WelcomeController.class.getName()); + // View State @FXML - private StackPane rootPane; - - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // + private StackPane rootPane; // Injected by FXML but not referenced directly. + // Initialization @FXML private void initialize() { LOGGER.fine("Default JavaFX font: " + javafx.scene.text.Font.getDefault()); } - // ------------------------------------------------------------------ // - // Event Handlers - // ------------------------------------------------------------------ // - - /** - * START SETUP button navigates to the Setup screen (§2.2.6). - */ + // Navigation @FXML private void onStartSetup(ActionEvent event) { ScreenNavigator.goToSetup(); diff --git a/open-robotics/src/main/java/com/openrobotics/db/Database.java b/open-robotics/src/main/java/com/openrobotics/db/Database.java index 82e1c16b..2810b841 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/Database.java +++ b/open-robotics/src/main/java/com/openrobotics/db/Database.java @@ -18,8 +18,9 @@ import java.util.List; /** - * central database access point; runs Flyway migrations at startup and provides a shared HikariCP connection pool. - * call {@link #init()} once at startup before any DAO calls. + * Central database access point; runs {@link Flyway} migrations at startup and provides a shared + * {@link HikariDataSource} connection pool. + * Call {@link #init()} once at startup before any DAO calls. */ public final class Database { @@ -32,11 +33,12 @@ public final class Database { private Database() {} /** - * loads config, runs Flyway migrations, and creates the connection pool. - * safe to call multiple times; subsequent calls are no-ops after the first successful init. + * Loads database config, runs Flyway migrations, and creates the shared connection pool. + * Safe to call multiple times; subsequent calls are no-ops after the first successful initialization. * - * @throws IOException on config read error - * @throws SQLException on connection or migration error + * @throws IOException if the database config cannot be read + * @throws IllegalStateException if the database was already shut down in this process + * @throws FlywayException if Flyway migration fails and cannot be skipped */ public static synchronized void init() throws IOException { if (shutdown) { @@ -125,8 +127,10 @@ private static boolean isUuidColumn(PreparedStatement ps, String table, String c } /** - * Returns the shared DataSource. {@link #init()} must have been called first. - * @throws IllegalStateException if the database has not been initialized yet + * Returns the shared {@link DataSource}. {@link #init()} must have been called first. + * + * @return the shared data source + * @throws IllegalStateException if the database has not been initialized or has already been shut down */ public static DataSource getDataSource() { if (dataSource == null) { @@ -139,15 +143,21 @@ public static DataSource getDataSource() { } /** - * Returns a connection from the pool. Caller must close it (e.g. try-with-resources). - * @throws IllegalStateException if the database has not been initialized yet + * Returns a connection from the shared pool. Callers must close it, for example with try-with-resources. + * + * @return a pooled database connection + * @throws SQLException if the pool cannot provide a connection + * @throws IllegalStateException if the database has not been initialized or has already been shut down */ public static Connection getConnection() throws SQLException { return getDataSource().getConnection(); } /** - * Indicates whether the DB schema has UUID robot identifier columns expected by newer DAO tests. + * Indicates whether the active schema includes the UUID robot identifier columns expected by newer DAO tests. + * + * @return {@code true} if the UUID robot schema is active + * @throws IllegalStateException if the database has not been initialized or has already been shut down */ public static boolean isUuidRobotSchemaReady() { if (dataSource == null) { @@ -159,9 +169,7 @@ public static boolean isUuidRobotSchemaReady() { return uuidRobotSchemaReady; } - /** - * Closes the shared Hikari pool and marks the DB layer as shut down. - */ + /** Closes the shared HikariCP pool and marks the database layer as shut down. */ public static synchronized void shutdown() { HikariDataSource ds = dataSource; dataSource = null; diff --git a/open-robotics/src/main/java/com/openrobotics/db/dao/MapDao.java b/open-robotics/src/main/java/com/openrobotics/db/dao/MapDao.java index c65dd105..4ffd4a0d 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/dao/MapDao.java +++ b/open-robotics/src/main/java/com/openrobotics/db/dao/MapDao.java @@ -10,14 +10,15 @@ import java.util.Optional; import java.util.UUID; -/** DAO for the maps table */ +/** DAO for the {@code maps} table. */ public final class MapDao { private MapDao() {} /** - * inserts a map record; uses {@code r.getId()} if set, otherwise generates a new UUID. + * Inserts a map record; uses {@code r.getId()} if set, otherwise generates a new UUID. * + * @param r the map record to insert * @return the inserted map's ID * @throws SQLException on database error */ @@ -43,8 +44,10 @@ INSERT INTO maps (id, name, width, height, tile_data, is_preset, random_seed, cr } /** - * finds a map record by ID. + * Finds a map record by ID. * + * @param id the map UUID to look up + * @return an {@link Optional} containing the record, or empty if not found * @throws SQLException on database error */ public static Optional findById(UUID id) throws SQLException { @@ -59,8 +62,9 @@ public static Optional findById(UUID id) throws SQLException { } /** - * returns all map records ordered by created_at descending. + * Returns all map records ordered by {@code created_at} descending. * + * @return list of all map records * @throws SQLException on database error */ public static List findAll() throws SQLException { @@ -77,9 +81,10 @@ public static List findAll() throws SQLException { } /** - * deletes a map record by ID. + * Deletes a map record by ID. * - * @return true if a row was deleted + * @param id the UUID of the map record to delete + * @return {@code true} if a row was deleted * @throws SQLException on database error */ public static boolean deleteById(UUID id) throws SQLException { diff --git a/open-robotics/src/main/java/com/openrobotics/db/dao/RobotRunStatsDao.java b/open-robotics/src/main/java/com/openrobotics/db/dao/RobotRunStatsDao.java index dfaf1b8e..377f6b63 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/dao/RobotRunStatsDao.java +++ b/open-robotics/src/main/java/com/openrobotics/db/dao/RobotRunStatsDao.java @@ -9,14 +9,15 @@ import java.util.Optional; import java.util.UUID; -/** DAO for the robot_run_stats table */ +/** DAO for the {@code robot_run_stats} table. */ public final class RobotRunStatsDao { private RobotRunStatsDao() {} /** - * inserts or updates robot stats for a (run_id, robot_id) pair. + * Inserts or updates robot stats for a ({@code run_id}, {@code robot_id}) pair. * + * @param r the robot run stats record to upsert * @return the row ID of the upserted record * @throws SQLException on database error */ @@ -61,8 +62,10 @@ ON CONFLICT (run_id, robot_id) DO UPDATE SET } /** - * finds all robot stats records for a given run. + * Finds all robot stats records for a given run, ordered by {@code robot_id}. * + * @param runId the run UUID to query + * @return list of robot run stats records for the run * @throws SQLException on database error */ public static List findByRunId(UUID runId) throws SQLException { @@ -81,8 +84,11 @@ public static List findByRunId(UUID runId) throws SQLExcept } /** - * finds a robot stats record by run ID and robot ID. + * Finds a robot stats record by run ID and robot ID. * + * @param runId the run UUID to query + * @param robotId the robot UUID to query + * @return an {@link Optional} containing the record, or empty if not found * @throws SQLException on database error */ public static Optional findByRunIdAndRobotId(UUID runId, UUID robotId) throws SQLException { @@ -98,9 +104,10 @@ public static Optional findByRunIdAndRobotId(UUID runId, UU } /** - * deletes all robot stats records for a given run. + * Deletes all robot stats records for a given run. * - * @return number of deleted rows + * @param runId the run UUID whose robot stats should be deleted + * @return the number of deleted rows * @throws SQLException on database error */ public static int deleteByRunId(UUID runId) throws SQLException { diff --git a/open-robotics/src/main/java/com/openrobotics/db/dao/RunResultDao.java b/open-robotics/src/main/java/com/openrobotics/db/dao/RunResultDao.java index f2dcd174..e71a57f2 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/dao/RunResultDao.java +++ b/open-robotics/src/main/java/com/openrobotics/db/dao/RunResultDao.java @@ -8,14 +8,15 @@ import java.util.Optional; import java.util.UUID; -/** DAO for the run_results table */ +/** DAO for the {@code run_results} table. */ public final class RunResultDao { private RunResultDao() {} /** - * inserts a run result record. + * Inserts a run result record. * + * @param r the run result record to insert * @throws SQLException on database error */ public static void insert(RunResultRecord r) throws SQLException { @@ -45,8 +46,10 @@ public static void insert(RunResultRecord r) throws SQLException { } /** - * finds a run result record by run ID. + * Finds a run result record by run ID. * + * @param runId the run UUID to look up + * @return an {@link Optional} containing the record, or empty if not found * @throws SQLException on database error */ public static Optional findByRunId(UUID runId) throws SQLException { @@ -61,9 +64,10 @@ public static Optional findByRunId(UUID runId) throws SQLExcept } /** - * deletes a run result record by run ID. + * Deletes a run result record by run ID. * - * @return true if a row was deleted + * @param runId the run UUID whose result record should be deleted + * @return {@code true} if a row was deleted * @throws SQLException on database error */ public static boolean deleteByRunId(UUID runId) throws SQLException { diff --git a/open-robotics/src/main/java/com/openrobotics/db/dao/SimLogDao.java b/open-robotics/src/main/java/com/openrobotics/db/dao/SimLogDao.java index 7dbaa13a..d85e7e78 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/dao/SimLogDao.java +++ b/open-robotics/src/main/java/com/openrobotics/db/dao/SimLogDao.java @@ -9,14 +9,16 @@ import java.util.List; import java.util.UUID; -/** DAO for the sim_logs table */ +/** DAO for the {@code sim_logs} table. */ public final class SimLogDao { private SimLogDao() {} /** - * inserts a single log record and returns its generated ID. + * Inserts a single log record and returns its generated ID. * + * @param r the sim log record to insert + * @return the generated row ID * @throws SQLException on database error */ public static long insert(SimLogRecord r) throws SQLException { @@ -44,8 +46,9 @@ INSERT INTO sim_logs (run_id, tick, robot_id, event_type, x, y, details) } /** - * inserts a batch of log records in a single transaction for better throughput. + * Inserts a batch of log records in a single transaction for better throughput. * + * @param records the list of sim log records to insert; no-op if empty * @throws SQLException on database error */ public static void insertBatch(List records) throws SQLException { @@ -81,8 +84,10 @@ INSERT INTO sim_logs (run_id, tick, robot_id, event_type, x, y, details) } /** - * finds all log records for a run, ordered by tick then ID. + * Finds all log records for a run, ordered by tick then ID. * + * @param runId the run UUID to query + * @return list of sim log records for the run * @throws SQLException on database error */ public static List findByRunId(UUID runId) throws SQLException { @@ -91,9 +96,11 @@ public static List findByRunId(UUID runId) throws SQLException { } /** - * finds all log records for a run greater than the given log id, ordered by tick then ID. + * Finds all log records for a run with an ID greater than {@code lastSeenId}, ordered by tick then ID. * - * @param lastSeenId the last log ID that was seen by the caller; only logs with a greater ID will be returned + * @param runId the run UUID to query + * @param lastSeenId the last log ID seen by the caller; only records with a greater ID are returned + * @return list of sim log records newer than {@code lastSeenId} * @throws SQLException on database error */ public static List findLatestLogs(UUID runId, long lastSeenId) throws SQLException { @@ -114,8 +121,11 @@ public static List findLatestLogs(UUID runId, long lastSeenId) thr } /** - * finds all log records for a run at a specific tick, ordered by ID. + * Finds all log records for a run at a specific tick, ordered by ID. * + * @param runId the run UUID to query + * @param tick the tick number to filter by + * @return list of sim log records at the given tick * @throws SQLException on database error */ public static List findByRunIdAndTick(UUID runId, int tick) throws SQLException { @@ -135,9 +145,10 @@ public static List findByRunIdAndTick(UUID runId, int tick) throws } /** - * deletes all log records for a run. + * Deletes all log records for a run. * - * @return number of deleted rows + * @param runId the run UUID whose log records should be deleted + * @return the number of deleted rows * @throws SQLException on database error */ public static int deleteByRunId(UUID runId) throws SQLException { diff --git a/open-robotics/src/main/java/com/openrobotics/db/dao/SimulationRunDao.java b/open-robotics/src/main/java/com/openrobotics/db/dao/SimulationRunDao.java index d3127433..e4b08846 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/dao/SimulationRunDao.java +++ b/open-robotics/src/main/java/com/openrobotics/db/dao/SimulationRunDao.java @@ -10,14 +10,15 @@ import java.util.Optional; import java.util.UUID; -/** DAO for the simulation_runs table */ +/** DAO for the {@code simulation_runs} table. */ public final class SimulationRunDao { private SimulationRunDao() {} /** - * inserts a simulation run record; uses {@code r.getId()} if set, otherwise generates a new UUID. + * Inserts a simulation run record; uses {@code r.getId()} if set, otherwise generates a new UUID. * + * @param r the simulation run record to insert * @return the inserted run's ID * @throws SQLException on database error */ @@ -48,8 +49,10 @@ public static UUID insert(SimulationRunRecord r) throws SQLException { } /** - * finds a simulation run record by ID. + * Finds a simulation run record by ID. * + * @param id the run UUID to look up + * @return an {@link Optional} containing the record, or empty if not found * @throws SQLException on database error */ public static Optional findById(UUID id) throws SQLException { @@ -64,8 +67,10 @@ public static Optional findById(UUID id) throws SQLExceptio } /** - * finds all simulation run records for a given map, ordered by started_at descending. + * Finds all simulation run records for a given map, ordered by {@code started_at} descending. * + * @param mapId the map UUID to filter by + * @return list of simulation run records for the map * @throws SQLException on database error */ public static List findByMapId(UUID mapId) throws SQLException { @@ -84,8 +89,11 @@ public static List findByMapId(UUID mapId) throws SQLExcept } /** - * updates the status and finished_at timestamp of a simulation run. + * Updates the {@code status} and {@code finished_at} timestamp of a simulation run. * + * @param id the run UUID to update + * @param status the new status string + * @param finishedAt the completion timestamp; may be {@code null} * @throws SQLException on database error */ public static void updateStatus(UUID id, String status, Timestamp finishedAt) throws SQLException { @@ -100,9 +108,10 @@ public static void updateStatus(UUID id, String status, Timestamp finishedAt) th } /** - * deletes a simulation run record by ID. + * Deletes a simulation run record by ID. * - * @return true if a row was deleted + * @param id the run UUID to delete + * @return {@code true} if a row was deleted * @throws SQLException on database error */ public static boolean deleteById(UUID id) throws SQLException { diff --git a/open-robotics/src/main/java/com/openrobotics/db/dao/WorkloadTaskDao.java b/open-robotics/src/main/java/com/openrobotics/db/dao/WorkloadTaskDao.java index 84b02af1..228d4def 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/dao/WorkloadTaskDao.java +++ b/open-robotics/src/main/java/com/openrobotics/db/dao/WorkloadTaskDao.java @@ -9,14 +9,16 @@ import java.util.List; import java.util.UUID; -/** DAO for the run_workload_tasks table */ +/** DAO for the {@code run_workload_tasks} table. */ public final class WorkloadTaskDao { private WorkloadTaskDao() {} /** - * inserts a workload task record and returns its generated ID. + * Inserts a workload task record and returns its generated ID. * + * @param r the workload task record to insert + * @return the generated row ID * @throws SQLException on database error */ public static long insert(WorkloadTaskRecord r) throws SQLException { @@ -52,8 +54,10 @@ public static long insert(WorkloadTaskRecord r) throws SQLException { } /** - * finds all workload task records for a given run, ordered by ID. + * Finds all workload task records for a given run, ordered by ID. * + * @param runId the run UUID to query + * @return list of workload task records for the run * @throws SQLException on database error */ public static List findByRunId(UUID runId) throws SQLException { @@ -72,8 +76,11 @@ public static List findByRunId(UUID runId) throws SQLExcepti } /** - * updates status and completed_tick for a task. + * Updates the {@code status} and {@code completed_tick} for a task. * + * @param id the row ID of the task to update + * @param status the new status string + * @param completedTick the tick at which the task was completed; may be {@code null} * @throws SQLException on database error */ public static void markCompleted(long id, String status, Integer completedTick) throws SQLException { @@ -88,8 +95,11 @@ public static void markCompleted(long id, String status, Integer completedTick) } /** - * assigns a task to a robot; also sets status to IN_PROGRESS. + * Assigns a task to a robot and sets its status to {@code IN_PROGRESS}. * + * @param id the row ID of the task to assign + * @param robotId the UUID of the robot being assigned + * @param assignedTick the tick at which the assignment occurs * @throws SQLException on database error */ public static void assignToRobot(long id, UUID robotId, int assignedTick) throws SQLException { @@ -104,9 +114,10 @@ public static void assignToRobot(long id, UUID robotId, int assignedTick) throws } /** - * deletes all workload task records for a given run. + * Deletes all workload task records for a given run. * - * @return number of deleted rows + * @param runId the run UUID whose workload task records should be deleted + * @return the number of deleted rows * @throws SQLException on database error */ public static int deleteByRunId(UUID runId) throws SQLException { diff --git a/open-robotics/src/main/java/com/openrobotics/db/model/MapRecord.java b/open-robotics/src/main/java/com/openrobotics/db/model/MapRecord.java index aaf25270..11404664 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/model/MapRecord.java +++ b/open-robotics/src/main/java/com/openrobotics/db/model/MapRecord.java @@ -3,7 +3,7 @@ import java.sql.Timestamp; import java.util.UUID; -/** database record for the maps table */ +/** Database record for the {@code maps} table. */ public class MapRecord { private UUID id; @@ -24,6 +24,12 @@ public MapRecord() {} public void setName(String name) { this.name = name; } public int getWidth() { return width; } + /** + * Sets the map width. + * + * @param width the map width in tiles; must be positive + * @throws IllegalArgumentException if {@code width} is zero or negative + */ public void setWidth(int width) { if (width <= 0) { throw new IllegalArgumentException("width must be positive"); @@ -32,6 +38,12 @@ public void setWidth(int width) { } public int getHeight() { return height; } + /** + * Sets the map height. + * + * @param height the map height in tiles; must be positive + * @throws IllegalArgumentException if {@code height} is zero or negative + */ public void setHeight(int height) { if (height <= 0) { throw new IllegalArgumentException("height must be positive"); diff --git a/open-robotics/src/main/java/com/openrobotics/db/model/RobotRunStatsRecord.java b/open-robotics/src/main/java/com/openrobotics/db/model/RobotRunStatsRecord.java index d282e57f..29db34eb 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/model/RobotRunStatsRecord.java +++ b/open-robotics/src/main/java/com/openrobotics/db/model/RobotRunStatsRecord.java @@ -3,7 +3,7 @@ import java.math.BigDecimal; import java.util.UUID; -/** database record for the robot_run_stats table */ +/** Database record for the {@code robot_run_stats} table. */ public class RobotRunStatsRecord { private Long id; diff --git a/open-robotics/src/main/java/com/openrobotics/db/model/RunResultRecord.java b/open-robotics/src/main/java/com/openrobotics/db/model/RunResultRecord.java index ccc6d59d..e8b547b9 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/model/RunResultRecord.java +++ b/open-robotics/src/main/java/com/openrobotics/db/model/RunResultRecord.java @@ -3,7 +3,7 @@ import java.math.BigDecimal; import java.util.UUID; -/** database record for the run_results table */ +/** Database record for the {@code run_results} table. */ public class RunResultRecord { private UUID runId; diff --git a/open-robotics/src/main/java/com/openrobotics/db/model/SimLogRecord.java b/open-robotics/src/main/java/com/openrobotics/db/model/SimLogRecord.java index ee0db072..8959a120 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/model/SimLogRecord.java +++ b/open-robotics/src/main/java/com/openrobotics/db/model/SimLogRecord.java @@ -2,7 +2,7 @@ import java.util.UUID; -/** database record for the sim_logs table */ +/** Database record for the {@code sim_logs} table. */ public class SimLogRecord { private Long id; diff --git a/open-robotics/src/main/java/com/openrobotics/db/model/SimulationRunRecord.java b/open-robotics/src/main/java/com/openrobotics/db/model/SimulationRunRecord.java index d9ed03c6..bbb4d5e7 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/model/SimulationRunRecord.java +++ b/open-robotics/src/main/java/com/openrobotics/db/model/SimulationRunRecord.java @@ -4,7 +4,7 @@ import java.util.Objects; import java.util.UUID; -/** database record for the simulation_runs table */ +/** Database record for the {@code simulation_runs} table. */ public class SimulationRunRecord { private UUID id; @@ -28,6 +28,12 @@ public SimulationRunRecord() {} public void setMapId(UUID mapId) { this.mapId = mapId; } public Integer getRobotCount() { return robotCount; } + /** + * Sets the robot count for this run. + * + * @param robotCount the number of robots; must be non-negative if non-{@code null} + * @throws IllegalArgumentException if {@code robotCount} is negative + */ public void setRobotCount(Integer robotCount) { if (robotCount != null && robotCount < 0) { throw new IllegalArgumentException("robotCount must be non-negative"); diff --git a/open-robotics/src/main/java/com/openrobotics/db/model/WorkloadTaskRecord.java b/open-robotics/src/main/java/com/openrobotics/db/model/WorkloadTaskRecord.java index 9638f02a..e7b1902b 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/model/WorkloadTaskRecord.java +++ b/open-robotics/src/main/java/com/openrobotics/db/model/WorkloadTaskRecord.java @@ -2,7 +2,7 @@ import java.util.UUID; -/** database record for the run_workload_tasks table */ +/** Database record for the {@code run_workload_tasks} table. */ public class WorkloadTaskRecord { private Long id; diff --git a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/MapRecordBuilder.java b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/MapRecordBuilder.java index 5ad776f6..6cb21ac6 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/MapRecordBuilder.java +++ b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/MapRecordBuilder.java @@ -3,10 +3,15 @@ import com.openrobotics.db.model.MapRecord; import com.openrobotics.map.Map; -/** builds MapRecord instances from Map objects for database persistence */ +/** Builds {@link MapRecord} instances from {@link Map} objects for database persistence. */ public class MapRecordBuilder { private MapRecord record; + /** + * Creates a builder pre-filled from the given map. + * + * @param map the warehouse map to build a record from + */ public MapRecordBuilder(Map map) { this.record = new MapRecord(); record.setId(map.getMapid()); @@ -14,13 +19,17 @@ public MapRecordBuilder(Map map) { record.setWidth(map.getWidth()); record.setHeight(map.getHeight()); - // placeholder to satisfy not-null constraint; TODO: serialize map entities to JSON + // placeholder to satisfy not-null constraint record.setTileData("{}"); - // TODO: differentiate preset maps from user-created maps and set accordingly record.setPreset(false); } + /** + * Returns the built {@link MapRecord}. + * + * @return the populated map record + */ public MapRecord build() { return record; } diff --git a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimLogRecordBuilder.java b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimLogRecordBuilder.java index 7da9d074..3af7f56b 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimLogRecordBuilder.java +++ b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimLogRecordBuilder.java @@ -5,11 +5,19 @@ import java.util.UUID; -/** builds SimLogRecord instances for each robot event type */ +/** Builds {@link SimLogRecord} instances for each robot event type. */ public class SimLogRecordBuilder { private SimLogRecord record; - /** initializes a SimLogRecord with the run ID, tick, robot ID, and position */ + /** + * Creates a builder pre-filled with the run ID, tick, robot ID, and tile position. + * + * @param simulationRunId the UUID of the simulation run + * @param tick the simulation tick at which the event occurred + * @param robotId the UUID of the robot involved in the event + * @param x the tile x-coordinate of the event + * @param y the tile y-coordinate of the event + */ public SimLogRecordBuilder(UUID simulationRunId, int tick, UUID robotId, int x, int y) { this.record = new SimLogRecord(); record.setRunId(simulationRunId); @@ -19,16 +27,21 @@ public SimLogRecordBuilder(UUID simulationRunId, int tick, UUID robotId, int x, record.setY(y); } - /** builds a MOVE_EXECUTED log record */ + /** + * Builds a {@code MOVE_EXECUTED} log record. + * + * @return the populated sim log record + */ public SimLogRecord buildMoveExecutionRecord() { record.setEventType(RobotEvent.MOVE_EXECUTED.toString()); return record; } /** - * builds a COLLISION log record. + * Builds a {@code COLLISION} log record. * - * @param details collision details JSON + * @param details collision details as a JSON string + * @return the populated sim log record */ public SimLogRecord buildCollisionRecord(String details) { record.setEventType(RobotEvent.COLLISION.toString()); @@ -37,9 +50,10 @@ public SimLogRecord buildCollisionRecord(String details) { } /** - * builds a NEAR_MISS log record. + * Builds a {@code NEAR_MISS} log record. * - * @param details near miss details JSON + * @param details near miss details as a JSON string + * @return the populated sim log record */ public SimLogRecord buildNearMissRecord(String details) { record.setEventType(RobotEvent.NEAR_MISS.toString()); @@ -47,16 +61,21 @@ public SimLogRecord buildNearMissRecord(String details) { return record; } - /** builds a BATTERY_DEATH log record */ + /** + * Builds a {@code BATTERY_DEATH} log record. + * + * @return the populated sim log record + */ public SimLogRecord buildBatteryDeathRecord() { record.setEventType(RobotEvent.BATTERY_DEATH.toString()); return record; } /** - * builds a CHARGE_START log record. + * Builds a {@code CHARGE_START} log record. * * @param details JSON with structure: {'batteryLevel': int} + * @return the populated sim log record */ public SimLogRecord buildChargeStartRecord(String details) { record.setDetails(details); @@ -65,9 +84,10 @@ public SimLogRecord buildChargeStartRecord(String details) { } /** - * builds a CHARGE_END log record. + * Builds a {@code CHARGE_END} log record. * * @param details JSON with structure: {'batteryLevel': int} + * @return the populated sim log record */ public SimLogRecord buildChargeEndRecord(String details) { record.setDetails(details); @@ -76,9 +96,10 @@ public SimLogRecord buildChargeEndRecord(String details) { } /** - * builds a DEADLOCK_DETECTED log record. + * Builds a {@code DEADLOCK_DETECTED} log record. * * @param details JSON with structure: {'stuckTicks': int} + * @return the populated sim log record */ public SimLogRecord buildDeadlockDetectionRecord(String details) { record.setDetails(details); @@ -87,9 +108,10 @@ public SimLogRecord buildDeadlockDetectionRecord(String details) { } /** - * builds a DEADLOCK_RESOLVED log record. + * Builds a {@code DEADLOCK_RESOLVED} log record. * * @param details JSON with structure: {'resolutionMethod': String} + * @return the populated sim log record */ public SimLogRecord buildDeadlockResolutionRecord(String details) { record.setDetails(details); diff --git a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimulationRunRecordBuilder.java b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimulationRunRecordBuilder.java index 2c346799..5a72f998 100644 --- a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimulationRunRecordBuilder.java +++ b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/SimulationRunRecordBuilder.java @@ -6,11 +6,15 @@ import java.sql.Timestamp; import java.time.Instant; -/** builds SimulationRunRecord instances for simulation lifecycle events */ +/** Builds {@link SimulationRunRecord} instances for simulation lifecycle events. */ public class SimulationRunRecordBuilder { private SimulationRunRecord record; - /** initializes a SimulationRunRecord from the engine's run ID, map, robot count, and coordination policy */ + /** + * Creates a builder pre-filled from the engine's run ID, map, robot count, and coordination policy. + * + * @param engine the simulation engine to extract run metadata from + */ public SimulationRunRecordBuilder(SimulationEngine engine) { this.record = new SimulationRunRecord(); record.setId(engine.getRunId()); @@ -20,14 +24,22 @@ public SimulationRunRecordBuilder(SimulationEngine engine) { } // TODO: set all simulation run fields appropriately (probably do this in the constructor) - /** builds the simulation start record; sets startedAt to now and status to RUNNING */ + /** + * Builds the simulation start record; sets {@code startedAt} to now and status to {@code RUNNING}. + * + * @return the populated simulation run record + */ public SimulationRunRecord buildSimulationStartRecord() { record.setStartedAt(Timestamp.from(Instant.now())); record.setStatus("RUNNING"); return record; } - /** builds the simulation complete record; sets finishedAt to now and status to COMPLETED */ + /** + * Builds the simulation complete record; sets {@code finishedAt} to now and status to {@code COMPLETED}. + * + * @return the populated simulation run record + */ public SimulationRunRecord buildSimulationCompleteRecord() { record.setFinishedAt(Timestamp.from(Instant.now())); record.setStatus("COMPLETED"); diff --git a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/WorkloadTaskRecordBuilder.java b/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/WorkloadTaskRecordBuilder.java deleted file mode 100644 index b1010312..00000000 --- a/open-robotics/src/main/java/com/openrobotics/db/recordbuilders/WorkloadTaskRecordBuilder.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.openrobotics.db.recordbuilders; - -import com.openrobotics.db.model.WorkloadTaskRecord; -import com.openrobotics.task.Task; - -import java.util.UUID; - -/** builds WorkloadTaskRecord instances for task lifecycle events */ -public class WorkloadTaskRecordBuilder { - private WorkloadTaskRecord record; - - /** initializes a WorkloadTaskRecord from the given task, pre-filling type, priority, locations, and status */ - public WorkloadTaskRecordBuilder(UUID simulationRunId, Task task) { - this.record = new WorkloadTaskRecord(); - record.setId(task.getArtificialId()); - record.setRunId(simulationRunId); - record.setTaskType(task.getClass().getSimpleName()); - record.setPriority(task.getPriority()); - record.setPickupX(task.getPickupLocation().getX()); - record.setPickupY(task.getPickupLocation().getY()); - record.setDropoffX(task.getDropoffLocation().getX()); - record.setDropoffY(task.getDropoffLocation().getY()); - record.setStatus(task.getStatus().name()); - } - - /** builds the task creation record; sets createdTick */ - public WorkloadTaskRecord buildTaskCreationRecord(int creationTick) { - record.setCreatedTick(creationTick); - return record; - } - - /** builds the task assignment record; sets assignedTick and assignedRobotId */ - public WorkloadTaskRecord buildTaskAssignmentRecord(int assignedTick, UUID assignedRobotId) { - record.setAssignedTick(assignedTick); - record.setAssignedRobotId(assignedRobotId); - return record; - } - - /** builds the task completion record; sets completedTick and status to COMPLETED */ - public WorkloadTaskRecord buildTaskCompletionRecord(int completedTick) { - record.setCompletedTick(completedTick); - record.setStatus("COMPLETED"); - return record; - } -} diff --git a/open-robotics/src/main/java/com/openrobotics/io/ConfigLoader.java b/open-robotics/src/main/java/com/openrobotics/io/ConfigLoader.java index c90539eb..4a0c18bd 100644 --- a/open-robotics/src/main/java/com/openrobotics/io/ConfigLoader.java +++ b/open-robotics/src/main/java/com/openrobotics/io/ConfigLoader.java @@ -5,46 +5,41 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -/** loads and saves simulation config files as JSON; path and class validation guard against path traversal and unsafe deserialization */ +/** + * Loads and saves simulation config JSON files while canonicalizing file paths and restricting + * deserialization targets. + */ public class ConfigLoader { private static final ObjectMapper mapper = new ObjectMapper(); - private static final String BASE_DIR_PROPERTY = "openrobotics.config.baseDir"; private ConfigLoader() {} - private static List allowedBaseDirs() throws IOException { - List bases = new ArrayList<>(); - String configuredBase = System.getProperty(BASE_DIR_PROPERTY); - if (configuredBase != null && !configuredBase.isBlank()) { - bases.add(new File(configuredBase).getCanonicalFile().toPath()); - } else { - bases.add(new File(System.getProperty("user.dir", ".")).getCanonicalFile().toPath()); - } - bases.add(new File(System.getProperty("java.io.tmpdir", ".")).getCanonicalFile().toPath()); - bases.add(new File(System.getProperty("user.home", ".") + File.separator - + ".open-robotics" + File.separator + "configs").getCanonicalFile().toPath()); - return bases; - } - + /** + * Resolves a config path to a canonical file. + * + * @param path the user-supplied file path + * @return the canonical file for {@code path} + * @throws IOException if canonicalization fails + * @throws IllegalArgumentException if {@code path} is {@code null} or blank + */ private static File validatePath(String path) throws IOException { if (path == null || path.isBlank()) { throw new IllegalArgumentException("path must not be null or blank"); } - File file = new File(path).getCanonicalFile(); - Path filePath = file.toPath(); - for (Path base : allowedBaseDirs()) { - if (filePath.startsWith(base)) { - return file; - } - } - throw new SecurityException("path is outside allowed config directories: " + file); + // Resolves the file and removes any relative navigation + return new File(path).getCanonicalFile(); } + /** + * Validates that a deserialization target class is in an allowed OpenRobotics package. + * + * @param the deserialization target type + * @param clazz the target class + * @throws IllegalArgumentException if {@code clazz} is {@code null} + * @throws SecurityException if {@code clazz} is outside the allowed packages + */ private static void validateTargetClass(Class clazz) { if (clazz == null) { throw new IllegalArgumentException("clazz must not be null"); @@ -58,10 +53,15 @@ private static void validateTargetClass(Class clazz) { } /** - * loads and deserializes a JSON file at {@code path} into an instance of {@code clazz}. - * path must resolve under an allowed base directory; clazz must be in an openrobotics DTO/model package. + * Loads a JSON file at {@code path} into an instance of {@code clazz}. * - * @throws SecurityException if path or clazz fails validation + * @param the deserialized type + * @param path the JSON file path + * @param clazz the target class + * @return the deserialized object + * @throws IOException if the file cannot be read or deserialized + * @throws IllegalArgumentException if {@code path} is blank or {@code clazz} is {@code null} + * @throws SecurityException if {@code clazz} is outside the allowed packages */ public static T load(String path, Class clazz) throws IOException { validateTargetClass(clazz); @@ -71,10 +71,12 @@ public static T load(String path, Class clazz) throws IOException { } /** - * serializes {@code obj} as pretty-printed JSON and writes it to {@code path}. - * path must resolve under an allowed base directory. + * Serializes {@code obj} as pretty-printed JSON and writes it to {@code path}. * - * @throws SecurityException if path fails validation + * @param path the output file path + * @param obj the object to serialize + * @throws IOException if the file cannot be written + * @throws IllegalArgumentException if {@code path} is blank or {@code obj} is {@code null} */ public static void save(String path, Object obj) throws IOException { if (obj == null) { diff --git a/open-robotics/src/main/java/com/openrobotics/io/ResultsExportDTO.java b/open-robotics/src/main/java/com/openrobotics/io/ResultsExportDTO.java index 1e5f212e..d4690fb1 100644 --- a/open-robotics/src/main/java/com/openrobotics/io/ResultsExportDTO.java +++ b/open-robotics/src/main/java/com/openrobotics/io/ResultsExportDTO.java @@ -2,7 +2,7 @@ import java.util.List; -/** data transfer object for serializing post-simulation results to JSON */ +/** Data transfer object for serializing post-simulation results to JSON. */ public class ResultsExportDTO { public String runName; @@ -25,4 +25,4 @@ public static class RobotResultDTO { public double battery; public String finalState; } -} \ No newline at end of file +} diff --git a/open-robotics/src/main/java/com/openrobotics/io/SimulationConfigDTO.java b/open-robotics/src/main/java/com/openrobotics/io/SimulationConfigDTO.java index fa88124f..5002385c 100644 --- a/open-robotics/src/main/java/com/openrobotics/io/SimulationConfigDTO.java +++ b/open-robotics/src/main/java/com/openrobotics/io/SimulationConfigDTO.java @@ -6,7 +6,7 @@ import java.util.List; import java.util.UUID; -/** JSON schema for simulation config files; sections cover config, map, entities, tasks, coordination, and sim state */ +/** JSON schema for simulation config files; sections cover config, map, entities, tasks, coordination, and simulation state. */ public class SimulationConfigDTO { public ConfigSection config; public MapSection map; @@ -29,7 +29,7 @@ public static class ConfigSection { public int loadingTicks = 1; public int unloadingTicks = 1; public int maxTasks = 10; - /** true = tasks come from rack box counts + dropoff pools; false = randomly generated up to maxTasks */ + /** Whether tasks come from rack box counts and dropoff pools instead of random generation up to {@code maxTasks}. */ public boolean manualTaskAssignment = false; } @@ -105,4 +105,4 @@ public static class TaskDTO { public int priority; public TaskStatus status; } -} \ No newline at end of file +} diff --git a/open-robotics/src/main/java/com/openrobotics/logging/Logger.java b/open-robotics/src/main/java/com/openrobotics/logging/Logger.java index 5929c888..7802ee69 100644 --- a/open-robotics/src/main/java/com/openrobotics/logging/Logger.java +++ b/open-robotics/src/main/java/com/openrobotics/logging/Logger.java @@ -2,20 +2,19 @@ import com.openrobotics.db.dao.SimLogDao; import com.openrobotics.db.dao.SimulationRunDao; -import com.openrobotics.db.dao.WorkloadTaskDao; import com.openrobotics.db.model.SimLogRecord; import com.openrobotics.db.model.SimulationRunRecord; -import com.openrobotics.db.model.WorkloadTaskRecord; import com.openrobotics.logging.eventtypes.RobotEvent; import com.openrobotics.logging.eventtypes.SimulationRunEvent; -import com.openrobotics.logging.eventtypes.TaskEvent; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -/** central logger; robot events are buffered in a background queue, task and simulation events are written synchronously */ +/** + * Central simulation logger; robot events are buffered in a background queue, while simulation events are written synchronously. + */ public class Logger { private static LoggerMode mode = LoggerMode.DB; private static final BlockingQueue robotEventQueue = new LinkedBlockingQueue<>(); @@ -48,46 +47,21 @@ public class Logger { private Logger() { } - /** sets the logging mode; NO_OP suppresses all logging calls */ + /** + * Sets the logging mode. {@link LoggerMode#NO_OP} suppresses all logging calls. + * + * @param newMode the new logging mode + */ public static void setMode(LoggerMode newMode) { mode = newMode; } /** - * logs a task lifecycle event to the run_workload_task table. + * Logs a simulation run lifecycle event to the {@code simulation_runs} table. * - * @return the artificial id of the inserted record (TASK_CREATED only), or -1 for other event types + * @param eventType the simulation run event type to persist + * @param record the simulation run record containing the event payload */ - public static long logTaskEvent(TaskEvent eventType, WorkloadTaskRecord record) { - if (mode == LoggerMode.NO_OP) { - return -1; - } - if (mode == null) { - System.err.println("Failed to log task event of type: " + eventType); - return -1; - } - - try { - switch (eventType) { - case TASK_CREATED: - return WorkloadTaskDao.insert(record); - case TASK_ASSIGNED: - WorkloadTaskDao.assignToRobot(record.getId(), record.getAssignedRobotId(), record.getAssignedTick()); - return -1; - case TASK_COMPLETED: - WorkloadTaskDao.markCompleted(record.getId(), record.getStatus(), record.getCompletedTick()); - return -1; - default: - throw new IllegalStateException("Unsupported event type: " + eventType); - } - } catch (Exception e) { - System.err.println("Failed to log task event of type: " + eventType); - System.err.println("Exception message: " + e.getMessage()); - return -1; - } - } - - /** logs a simulation run lifecycle event to the simulation_runs table */ public static void logSimulationRunEvent(SimulationRunEvent eventType, SimulationRunRecord record) { if (mode == LoggerMode.NO_OP) { return; @@ -114,8 +88,11 @@ public static void logSimulationRunEvent(SimulationRunEvent eventType, Simulatio } /** - * enqueues a robot event for async batch write to the sim_logs table. - * the background worker flushes the queue every 5 seconds in batches of up to 100. + * Enqueues a robot event for asynchronous batch write to the {@code sim_logs} table. + * The background worker flushes the queue about once per second. + * + * @param eventType the robot event type associated with the log record + * @param record the simulation log record to enqueue */ public static void logRobotEvent(RobotEvent eventType, SimLogRecord record) { if (mode == LoggerMode.NO_OP) { @@ -125,10 +102,7 @@ public static void logRobotEvent(RobotEvent eventType, SimLogRecord record) { robotEventQueue.offer(record); } - /** - * flushes all pending robot events in the queue immediately; - * can be called at the end of a simulation run to ensure all events are persisted before shutdown. - */ + /** Flushes all pending robot events immediately. */ public static void flushRobotEvents() { List batch = new ArrayList<>(); robotEventQueue.drainTo(batch); diff --git a/open-robotics/src/main/java/com/openrobotics/logging/LoggerMode.java b/open-robotics/src/main/java/com/openrobotics/logging/LoggerMode.java index 58b35b1c..e7b932bb 100644 --- a/open-robotics/src/main/java/com/openrobotics/logging/LoggerMode.java +++ b/open-robotics/src/main/java/com/openrobotics/logging/LoggerMode.java @@ -1,6 +1,6 @@ package com.openrobotics.logging; -/** logging mode; DB writes events to the database, NO_OP suppresses all logging calls */ +/** Logging mode; {@code DB} writes events to the database, and {@code NO_OP} suppresses all logging calls. */ public enum LoggerMode { DB, NO_OP diff --git a/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/RobotEvent.java b/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/RobotEvent.java index b0f2c105..fcfd831e 100644 --- a/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/RobotEvent.java +++ b/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/RobotEvent.java @@ -1,6 +1,6 @@ package com.openrobotics.logging.eventtypes; -/** event types emitted by robots during simulation; used to categorize sim_logs entries */ +/** Event types emitted by robots during simulation; used to categorize {@code sim_logs} entries. */ public enum RobotEvent { MOVE_INTENT, // NOTE: Is this needed? Can't think of a use case for knowing these events MOVE_EXECUTED, diff --git a/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/SimulationRunEvent.java b/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/SimulationRunEvent.java index 823a6698..3b6071a0 100644 --- a/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/SimulationRunEvent.java +++ b/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/SimulationRunEvent.java @@ -1,6 +1,6 @@ package com.openrobotics.logging.eventtypes; -/** lifecycle events for a simulation run; used to categorize simulation_runs table updates */ +/** Lifecycle events for a simulation run; used to categorize {@code simulation_runs} table updates. */ public enum SimulationRunEvent { RUN_STARTED, RUN_COMPLETED, diff --git a/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/TaskEvent.java b/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/TaskEvent.java deleted file mode 100644 index 4d729ea4..00000000 --- a/open-robotics/src/main/java/com/openrobotics/logging/eventtypes/TaskEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.openrobotics.logging.eventtypes; - -/** lifecycle events for a workload task; used to categorize run_workload_task table updates */ -public enum TaskEvent { - TASK_CREATED, - TASK_ASSIGNED, - TASK_COMPLETED, -} diff --git a/open-robotics/src/main/java/com/openrobotics/map/Map.java b/open-robotics/src/main/java/com/openrobotics/map/Map.java index 7b1852f4..0032dab4 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/Map.java +++ b/open-robotics/src/main/java/com/openrobotics/map/Map.java @@ -13,7 +13,7 @@ import com.openrobotics.robot.Robot; import com.openrobotics.robot.RobotState; -/** warehouse grid; owns all tiles and entities, and provides spatial queries (uml 3.3.3) */ +/** Warehouse grid; owns all tiles and entities and provides spatial queries. */ public class Map { private UUID mapid; // unique identifier for database storage private final int width; // number of columns (x-axis) @@ -21,11 +21,25 @@ public class Map { private final Tile[][] grid; // grid[height][width] -> grid[row][col] -> grid[y][x] private final List entities; // all objects placed on the map + /** + * Creates a map with the given dimensions and assigns a specific database ID. + * + * @param mapId the UUID to assign to this map for database storage + * @param width the number of columns + * @param height the number of rows + */ public Map(UUID mapId, int width, int height) { this(width, height); this.mapid = mapId; } + /** + * Creates a map with the given dimensions and a randomly generated ID. + * + * @param width the number of columns + * @param height the number of rows + * @throws IllegalArgumentException if width or height is less than 1 + */ public Map(int width, int height) { this.mapid = UUID.randomUUID(); if (width < 1 || height < 1) { @@ -50,21 +64,38 @@ public Map(int width, int height) { public int getWidth() { return width; } public int getHeight() { return height; } - /** returns the tile at grid position (x, y), or null if out of bounds */ - // internally we flip to grid[y][x] because the array is row-major + /** + * Returns the tile at grid position (x, y), or {@code null} if out of bounds. + * + * @param x the column index + * @param y the row index + * @return the tile at (x, y), or {@code null} if the position is out of bounds + */ public Tile getTile(int x, int y) { + // internally we flip to grid[y][x] because the array is row-major if (x < 0 || x >= width || y < 0 || y >= height) { return null; } return grid[y][x]; } - /** true if (x, y) is in bounds and the tile is not currently occupied or blocked by a dead robot */ + /** + * Returns true if (x, y) is in bounds and the tile is not occupied or blocked by a dead robot. + * + * @param x the column index + * @param y the row index + * @return {@code true} if the position is a valid move target + */ public boolean isValidMove(int x, int y) { Tile tile = getTile(x, y); return tile != null && !tile.isOccupied() && !hasDeadRobotAt(tile.getPosition()); } + /** + * Adds an entity to the map and updates station-overlap flags if it is a station. + * + * @param entity the entity to add + */ public void addEntity(MapEntity entity) { entities.add(entity); if (entity instanceof DeliveryStation || entity instanceof ChargingStation) { @@ -72,6 +103,12 @@ public void addEntity(MapEntity entity) { } } + /** + * Removes an entity from the map and updates station-overlap flags if it was a station. + * + * @param entity the entity to remove + * @return {@code true} if the entity was present and has been removed + */ public boolean removeEntity(MapEntity entity) { boolean removed = entities.remove(entity); if (removed && (entity instanceof DeliveryStation || entity instanceof ChargingStation)) { @@ -80,7 +117,12 @@ public boolean removeEntity(MapEntity entity) { return removed; } - // get all entities at a given position + /** + * Returns all entities whose position equals {@code pos}. + * + * @param pos the grid position to query + * @return a list of entities at the given position; empty if none + */ public List getEntitiesAt(Vector2D pos) { List result = new ArrayList<>(); for (MapEntity entity : entities) { @@ -91,21 +133,42 @@ public List getEntitiesAt(Vector2D pos) { return result; } + /** + * Returns an unmodifiable view of all entities on the map. + * + * @return an unmodifiable list of all map entities + */ public List getEntities() { return Collections.unmodifiableList(entities); } - /** semantic alias for getNeighbors(); prefer this name in task/dispatcher contexts */ + /** + * Returns the traversable cardinal neighbours of {@code pos}; semantic alias for + * {@link #getNeighbors(Vector2D)}, preferred in task and dispatcher contexts. + * + * @param pos the grid position to query + * @return a list of traversable neighbouring positions + */ public List getTraversableAdjacentTiles(Vector2D pos) { return getNeighbors(pos); } - /** true if pos has at least one traversable cardinal neighbor */ + /** + * Returns true if {@code pos} has at least one traversable cardinal neighbour. + * + * @param pos the grid position to check + * @return {@code true} if at least one neighbour of {@code pos} is traversable + */ public boolean hasTraversableAdjacentTile(Vector2D pos) { return !getTraversableAdjacentTiles(pos).isEmpty(); } - /** true if a Rack entity occupies the given position */ + /** + * Returns true if a {@link Rack} entity occupies the given position. + * + * @param pos the grid position to check + * @return {@code true} if a rack is present at {@code pos} + */ public boolean isRackAt(Vector2D pos) { for (MapEntity entity : entities) { if (entity instanceof Rack && entity.getPosition().equals(pos)) { @@ -115,14 +178,20 @@ public boolean isRackAt(Vector2D pos) { return false; } - /** true if pos is in bounds and holds no Rack, Obstacle, or dead robot; live robot conflicts are handled by CollisionManager */ + /** + * Returns true if {@code pos} is in bounds and holds no rack, obstacle, or dead robot. + * Live robot conflicts are handled separately by the collision manager. + * + * @param pos the grid position to check + * @return {@code true} if a robot may move to this position + */ public boolean isTraversable(Vector2D pos) { Tile tile = getTile(pos.getX(), pos.getY()); if (tile == null) return false; for (MapEntity entity : entities) { if (entity.getPosition().equals(pos)) { - // Racks are solid. Robots interact with them from the side. + // racks are solid; robots interact with them from the side if (entity instanceof Rack || entity instanceof Obstacle || (entity instanceof Robot robot && robot.getState() == RobotState.BATTERY_DEAD)) { @@ -133,7 +202,12 @@ public boolean isTraversable(Vector2D pos) { return true; } - /** traversable cardinal neighbors of pos in Direction-enum order for determinism */ + /** + * Returns the traversable cardinal neighbours of {@code pos} in Direction-enum order for determinism. + * + * @param pos the grid position to query + * @return a list of traversable neighbouring positions + */ public List getNeighbors(Vector2D pos) { List neighbors = new ArrayList<>(); for (Direction dir : Direction.values()) { @@ -145,12 +219,18 @@ public List getNeighbors(Vector2D pos) { return neighbors; } - /** nearest ChargingStation to {@code from} by manhattan distance; null if none exist */ + /** + * Returns the position of the nearest {@link ChargingStation} to {@code from} by Manhattan + * distance, or {@code null} if no charging stations exist on the map. + * + * @param from the position to measure from + * @return the position of the nearest charging station, or {@code null} if none exist + */ public Vector2D findNearestChargingStation(Vector2D from) { Vector2D nearest = null; int bestDist = Integer.MAX_VALUE; for (MapEntity entity : entities) { - // only chargingstation counts, not generic stations + // only ChargingStation counts, not generic stations if (entity instanceof ChargingStation) { int dist = from.manhattanDistance(entity.getPosition()); if (dist < bestDist) { diff --git a/open-robotics/src/main/java/com/openrobotics/map/MapEntity.java b/open-robotics/src/main/java/com/openrobotics/map/MapEntity.java index caac76e1..37666e56 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/MapEntity.java +++ b/open-robotics/src/main/java/com/openrobotics/map/MapEntity.java @@ -2,16 +2,29 @@ import java.util.UUID; -/** base class for all physical objects placed on the warehouse map (uml 3.3.4) */ +/** Base class for all physical objects placed on the warehouse map. */ public class MapEntity { private final UUID id; // unique id for logging in db - private String name; // user facing label + private String name; // user-facing label private Vector2D position; + /** + * Creates a new entity with a randomly generated ID. + * + * @param name the user-facing label for this entity + * @param position the initial grid position + */ public MapEntity(String name, Vector2D position) { this(UUID.randomUUID(), name, position); } + /** + * Creates an entity with an explicit ID; used when loading from the database. + * + * @param id the entity's unique identifier + * @param name the user-facing label for this entity + * @param position the initial grid position + */ public MapEntity(UUID id, String name, Vector2D position) { this.id = id; this.name = name; @@ -25,6 +38,10 @@ public MapEntity(UUID id, String name, Vector2D position) { public void setName(String name) { this.name = name; } public void setPosition(Vector2D position) { this.position = position; } - /** per-tick update hook; Robot overrides this to execute state-dependent behavior each tick */ + /** + * Per-tick update hook; Robot overrides this to execute state-dependent behaviour each tick. + * + * @param map the current warehouse map, available to subclasses for spatial queries + */ public void update(Map map) {} } diff --git a/open-robotics/src/main/java/com/openrobotics/map/Tile.java b/open-robotics/src/main/java/com/openrobotics/map/Tile.java index 0fec2f0f..e30c015f 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/Tile.java +++ b/open-robotics/src/main/java/com/openrobotics/map/Tile.java @@ -1,6 +1,6 @@ package com.openrobotics.map; -/** single grid cell in the warehouse map; tracks occupancy and robot-overlap station status */ +/** Single grid cell in the warehouse map; tracks occupancy and station-overlap flags. */ public class Tile { private final int x; private final int y; @@ -23,6 +23,12 @@ public Tile(int x, int y) { public boolean isOccupied() { return isOccupied; } public boolean isDeliveryStation() { return isDeliveryStation; } public boolean isChargingStation() { return isChargingStation; } + + /** + * Returns true if robots may overlap on this tile without triggering a collision. + * + * @return {@code true} if this tile is a delivery station or a charging station + */ public boolean allowsRobotOverlap() { return isDeliveryStation || isChargingStation; } public void setOccupied(boolean occupied) { this.isOccupied = occupied; } @@ -33,9 +39,12 @@ public Tile(int x, int y) { public void incrementVisitCount() { visitCount++; } public void resetVisitCount() { visitCount = 0; } - /** current position as a new Vector2D; allocates a new object on every call */ + /** + * Returns the current position as a new {@code Vector2D}; allocates a new object on every call. + * + * @return a new {@code Vector2D} at (x, y) + */ public Vector2D getPosition() { return new Vector2D(x, y); } - } diff --git a/open-robotics/src/main/java/com/openrobotics/map/Vector2D.java b/open-robotics/src/main/java/com/openrobotics/map/Vector2D.java index 0cc4a857..41276a63 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/Vector2D.java +++ b/open-robotics/src/main/java/com/openrobotics/map/Vector2D.java @@ -2,7 +2,7 @@ import java.util.Objects; -/** immutable 2d integer position; used for all grid coordinates throughout the system */ +/** Immutable 2-D integer position used for all grid coordinates throughout the system. */ public class Vector2D { private final int x; private final int y; @@ -15,14 +15,23 @@ public Vector2D(int x, int y) { public int getX() { return x; } public int getY() { return y; } - /** returns a new vector offset by (dx, dy); this instance is unchanged */ + /** + * Returns a new vector offset by (dx, dy); this instance is unchanged. + * + * @param dx the x delta to add + * @param dy the y delta to add + * @return a new {@code Vector2D} at (x + dx, y + dy) + * @throws ArithmeticException if the addition overflows int + */ public Vector2D add(int dx, int dy) { return new Vector2D(Math.addExact(this.x, dx), Math.addExact(this.y, dy)); } /** - * manhattan distance to {@code other}. + * Returns the Manhattan distance to {@code other}. * + * @param other the target position + * @return the non-negative integer distance * @throws ArithmeticException if the result overflows int */ public int manhattanDistance(Vector2D other) { diff --git a/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Obstacle.java b/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Obstacle.java index 608e4968..4c61440e 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Obstacle.java +++ b/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Obstacle.java @@ -5,7 +5,7 @@ import java.util.UUID; -/** impassable map entity; stops sensor rays and blocks traversal (uml 3.3.4) */ +/** Impassable map entity; stops sensor rays and blocks traversal. */ public class Obstacle extends MapEntity { public Obstacle(String name, Vector2D position) { super(name, position); diff --git a/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Rack.java b/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Rack.java index 7bfc19eb..b0612a95 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Rack.java +++ b/open-robotics/src/main/java/com/openrobotics/map/entities/environment/Rack.java @@ -7,31 +7,56 @@ import java.util.List; import java.util.UUID; -/** storage rack; robots pick up boxes from an adjacent tile, never from the rack tile itself */ +/** Storage rack; robots pick up boxes from an adjacent tile, never from the rack tile itself. */ public class Rack extends MapEntity { private int boxCount; private List validDropoffIds; private boolean manualDropoffAssignment = false; + /** + * Creates a new rack with a randomly generated ID and an initial box count of 1. + * + * @param name the user-facing label + * @param position the initial grid position + */ public Rack(String name, Vector2D position) { super(name, position); - this.boxCount = 1; + this.boxCount = 0; this.validDropoffIds = new ArrayList<>(); } + /** + * Creates a rack with an explicit ID; used when loading from the database. + * + * @param id the entity's unique identifier + * @param name the user-facing label + * @param position the initial grid position + */ public Rack(UUID id, String name, Vector2D position) { super(id, name, position); - this.boxCount = 1; + this.boxCount = 0; this.validDropoffIds = new ArrayList<>(); } public int getBoxCount() { return boxCount; } + + /** + * Sets the box count, clamping to a minimum of 1. + * + * @param boxCount the desired count; values below 1 are treated as 1 + */ public void setBoxCount(int boxCount) { // a rack always has at least 1 box; 0 would mean nothing to pick up - this.boxCount = Math.max(1, boxCount); + this.boxCount = Math.max(0, boxCount); } public List getValidDropoffIds() { return validDropoffIds; } + + /** + * Sets the list of valid dropoff station IDs for this rack; a {@code null} argument is treated as empty. + * + * @param validDropoffIds the list of delivery station UUIDs, or {@code null} to clear + */ public void setValidDropoffIds(List validDropoffIds) { this.validDropoffIds = validDropoffIds != null ? validDropoffIds : new ArrayList<>(); } diff --git a/open-robotics/src/main/java/com/openrobotics/map/entities/station/ChargingStation.java b/open-robotics/src/main/java/com/openrobotics/map/entities/station/ChargingStation.java index e64e5214..5ab105ad 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/entities/station/ChargingStation.java +++ b/open-robotics/src/main/java/com/openrobotics/map/entities/station/ChargingStation.java @@ -4,7 +4,7 @@ import java.util.UUID; -/** charging station; robots travel here to recharge when battery is low */ +/** Charging station; robots travel here to recharge when their battery is low. */ public class ChargingStation extends Station { public ChargingStation(String name, Vector2D position) { super(name, position); diff --git a/open-robotics/src/main/java/com/openrobotics/map/entities/station/DeliveryStation.java b/open-robotics/src/main/java/com/openrobotics/map/entities/station/DeliveryStation.java index 58c55c9b..96ced1f7 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/entities/station/DeliveryStation.java +++ b/open-robotics/src/main/java/com/openrobotics/map/entities/station/DeliveryStation.java @@ -4,7 +4,7 @@ import java.util.UUID; -/** delivery station; robots drop off boxes here to complete a task */ +/** Delivery station; robots drop off boxes here to complete a task. */ public class DeliveryStation extends Station { public DeliveryStation(String name, Vector2D position) { super(name, position); diff --git a/open-robotics/src/main/java/com/openrobotics/map/entities/station/Station.java b/open-robotics/src/main/java/com/openrobotics/map/entities/station/Station.java index e74a2edc..46dcfc2c 100644 --- a/open-robotics/src/main/java/com/openrobotics/map/entities/station/Station.java +++ b/open-robotics/src/main/java/com/openrobotics/map/entities/station/Station.java @@ -5,7 +5,7 @@ import java.util.UUID; -/** abstract base for station-type map entities (charging and delivery) */ +/** Abstract base for station-type map entities (charging and delivery). */ public abstract class Station extends MapEntity { private boolean isBusy; diff --git a/open-robotics/src/main/java/com/openrobotics/robot/AlgorithmType.java b/open-robotics/src/main/java/com/openrobotics/robot/AlgorithmType.java index fccf29af..fbacb003 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/AlgorithmType.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/AlgorithmType.java @@ -1,10 +1,15 @@ package com.openrobotics.robot; -/** algorithm type enum; maps config strategy name strings to enum values */ +/** Algorithm type enum; maps config strategy name strings to enum values. */ public enum AlgorithmType { GREEDY, BUG, RTA_STAR, RANDOM, NONE; - /** returns the AlgorithmType for {@code name}, case-insensitive; returns NONE if unrecognized */ + /** + * Returns the {@code AlgorithmType} for {@code name}, case-insensitive. + * + * @param name the config string (e.g. {@code "GREEDY"}, {@code "BUG"}, {@code "RTA_STAR"}, {@code "RANDOM"}) + * @return the matching type, or {@code NONE} if the name is {@code null} or unrecognized + */ public static AlgorithmType fromConfigString(String name) { if (name == null) return NONE; String upper = name.toUpperCase().trim(); diff --git a/open-robotics/src/main/java/com/openrobotics/robot/Robot.java b/open-robotics/src/main/java/com/openrobotics/robot/Robot.java index 03bf13f8..3f092974 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/Robot.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/Robot.java @@ -4,12 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.openrobotics.AppState; import com.openrobotics.db.model.SimLogRecord; -import com.openrobotics.db.model.WorkloadTaskRecord; import com.openrobotics.db.recordbuilders.SimLogRecordBuilder; import com.openrobotics.logging.Logger; import com.openrobotics.logging.eventtypes.RobotEvent; -import com.openrobotics.logging.eventtypes.TaskEvent; -import com.openrobotics.db.recordbuilders.WorkloadTaskRecordBuilder; import com.openrobotics.map.Map; import com.openrobotics.map.MapEntity; import com.openrobotics.map.Tile; @@ -27,7 +24,7 @@ import static com.openrobotics.robot.RobotState.IDLE; -/** robot entity; extends MapEntity with state machine, navigation, sensor, and lifetime stats (uml 3.3.4) */ +/** Robot entity; extends MapEntity with a state machine, navigation, sensor, and lifetime stats. */ public class Robot extends MapEntity { private float battery; private NavigationStrategy nav; @@ -56,12 +53,25 @@ public class Robot extends MapEntity { private RobotConfig config; + /** + * Creates a new robot with a randomly generated ID and default configuration. + * + * @param name the user-facing label + * @param position the initial grid position + */ public Robot(String name, Vector2D position) { super(name, position); this.config = RobotConfig.defaults(); initMovementFields(); } + /** + * Creates a robot with an explicit ID and default configuration; used when loading from the database. + * + * @param id the entity's unique identifier + * @param name the user-facing label + * @param position the initial grid position + */ public Robot(UUID id, String name, Vector2D position) { super(id, name, position); this.config = RobotConfig.defaults(); @@ -115,6 +125,12 @@ private void initMovementFields() { public float getTotalEnergyConsumed() { return totalEnergyConsumed; } public RobotConfig getConfig() { return config; } + + /** + * Sets the robot's configuration; falls back to defaults if {@code config} is {@code null}. + * + * @param config the new configuration, or {@code null} to restore defaults + */ public void setConfig(RobotConfig config) { this.config = config != null ? config : RobotConfig.defaults(); } @@ -123,18 +139,29 @@ public void setConfig(RobotConfig config) { public void setNav(NavigationStrategy nav) { this.nav = nav; } public void setSensor(SensorStrategy sensor) { this.sensor = sensor; } public void setState(RobotState state) { this.state = state; } + + /** + * Sets the robot's current task; resets the deadlock reroute budget if the task changes. + * + * @param currentTask the new task to assign, or {@code null} to clear the current task + */ public void setCurrentTask(Task currentTask) { - // Each task gets one reroute budget, so changing tasks resets the reroute state. + // each task gets one reroute budget, so changing tasks resets the reroute state if (!sameTask(this.currentTask, currentTask)) { clearDeadlockRerouteState(); } this.currentTask = currentTask; } + public void setStuckTicks(int stuckTicks) { this.stuckTicks = stuckTicks; } - /** initiates a reroute attempt for a deadlocked robot; marks the blocked tile and resets nav state; returns true if the attempt was valid */ + /** + * Initiates a reroute attempt for a deadlocked robot; marks the blocked tile and resets nav state. + * + * @return {@code true} if the reroute attempt was valid and has been started + */ public boolean startDeadlockRerouteAttempt() { - // A reroute only makes sense if the robot actually tried to enter a different tile. + // a reroute only makes sense if the robot actually tried to enter a different tile if (!canStartDeadlockRerouteAttempt()) { return false; } @@ -147,9 +174,12 @@ public boolean startDeadlockRerouteAttempt() { return true; } - /** resets the robot to idle and clears all task and nav state; called when the engine gives up on the current task due to deadlock */ + /** + * Resets the robot to idle and clears all task and nav state; called when the engine gives up + * on the current task due to deadlock. + */ public void recoverFromDeadlock() { - // Recovery returns the robot to a clean idle state for the next assignment attempt. + // recovery returns the robot to a clean idle state for the next assignment attempt if (nav != null) { nav.reset(this); } @@ -164,7 +194,11 @@ public void recoverFromDeadlock() { state = IDLE; } - /** navigation target for this tick; charger overrides task target when battery is low */ + /** + * Returns the navigation target for this tick; charger target overrides task target when battery is low. + * + * @return the target position, or {@code null} if the robot has no task and no charger target + */ public Vector2D getTarget() { if (chargerTarget != null) return chargerTarget; if (currentTask == null) return null; @@ -172,9 +206,15 @@ public Vector2D getTarget() { return currentTask.getDropoffLocation(); } - /** returns the robot's move intention for this tick; runs sensor scan and saves position before delegating to nav strategy */ + /** + * Returns the robot's move intention for this tick; runs the sensor scan and saves position + * before delegating to the navigation strategy. + * + * @param map the current warehouse map + * @return the intended move for this tick + */ public MoveIntention getNextMove(Map map) { - // Update data sensor first + // update sensor first if (this.sensor != null) { this.lastScan = this.sensor.scan(this, map); } @@ -234,8 +274,6 @@ public MoveIntention getNextMove(Map map) { } else { System.err.println("[Robot] No charging station found for robot " + getName() + " at " + getPosition() + " with battery=" + battery); - state = IDLE; - return rememberRequestedMove(new MoveIntention(fromTile, fromTile, this)); } } } else { @@ -250,22 +288,39 @@ public MoveIntention getNextMove(Map map) { return rememberRequestedMove(new MoveIntention(fromTile, fromTile, this)); } - /** true if the robot is idle with no current task; used by the Dispatcher to find available robots */ + /** + * Returns true if the robot is idle with no current task; used by the Dispatcher to find available robots. + * + * @return {@code true} if the robot's state is IDLE and it has no current task + */ public boolean isAvailable() { return state == IDLE && currentTask == null; } - // battery decreases per move, floors at 0 to prevent negative values + /** + * Decreases battery by {@code amount}, flooring at 0 to prevent negative values. + * + * @param amount the amount of energy to consume + */ public void consumeEnergy(float amount) { this.battery = Math.max(0, this.battery - amount); } - // robot seeks charging station when below threshold (design doc 3.4.3) + /** + * Returns true if the robot's battery is below the given threshold (design doc 3.4.3). + * + * @param threshold the battery level below which the robot should seek a charger + * @return {@code true} if {@code battery < threshold} + */ public boolean needsCharging(float threshold) { return this.battery < threshold; } - // returns the enum state name — keeps old api working for tests/ui + /** + * Returns the robot's current state as a string; keeps the original string-based API for tests and UI. + * + * @return the name of the current {@link RobotState} + */ public String getStatus() { return state.name(); } @@ -279,7 +334,6 @@ public void update(Map map) { battery = Math.min(config.batteryCapacity, battery + config.chargePerTick); // cap at capacity if (battery >= config.batteryCapacity) { // fully charged — resume task or go idle - state = (currentTask != null) ? RobotState.MOVING : RobotState.IDLE; // Logging charging end event @@ -314,12 +368,6 @@ public void update(Map map) { if (currentTask != null) { currentTask.setStatus(TaskStatus.COMPLETED); tasksCompleted++; - - // Logging task completion event - int currentTick = AppState.getEngine().getTickCounter(); - WorkloadTaskRecordBuilder taskCompletionRecordBuilder = new WorkloadTaskRecordBuilder(AppState.getEngine().getRunId(), currentTask); - WorkloadTaskRecord record = taskCompletionRecordBuilder.buildTaskCompletionRecord(currentTick); - Logger.logTaskEvent(TaskEvent.TASK_COMPLETED, record); } setCurrentTask(null); hasPickedUp = false; @@ -330,7 +378,7 @@ public void update(Map map) { case MOVING: totalMovingTicks++; - // check if robots battery has died + // check if robot's battery has died if (battery <= 0) { state = RobotState.BATTERY_DEAD; @@ -355,7 +403,7 @@ public void update(Map map) { Logger.logRobotEvent(RobotEvent.MOVE_EXECUTED, moveRecord); if (rerouteAvoidTile != null) { - // The first successful move after rerouting means the temporary avoid hint is no longer needed. + // the first successful move after rerouting means the temporary avoid hint is no longer needed rerouteAvoidTile = null; // Logging robot deadlock resolution event via rerouting @@ -366,8 +414,8 @@ public void update(Map map) { String json = mapper.writeValueAsString(details); SimLogRecordBuilder recordBuilder = new SimLogRecordBuilder(AppState.getEngine().getRunId(), AppState.getEngine().getTickCounter(), getId(), getPosition().getX(), getPosition().getY()); - SimLogRecord record = recordBuilder.buildDeadlockResolutionRecord(json); - Logger.logRobotEvent(RobotEvent.DEADLOCK_RESOLVED, record); + SimLogRecord deadlockRecord = recordBuilder.buildDeadlockResolutionRecord(json); + Logger.logRobotEvent(RobotEvent.DEADLOCK_RESOLVED, deadlockRecord); } catch (JsonProcessingException e) { System.out.println("Error serializing deadlock resolution details for logging: " + e.getMessage()); } @@ -401,9 +449,8 @@ public void update(Map map) { } } - // returns true if the robot has arrived at target. - // racks are solid — arrival means standing adjacent (distance 1), not on top. - // all other targets require being on the same tile. + // racks are solid — arrival means standing adjacent (distance 1), not on top; + // all other targets require being on the same tile private boolean isAtTarget(Map map, Vector2D target) { if (target == null || map == null) return false; return map.isRackAt(target) @@ -418,20 +465,20 @@ public String toString() { } private MoveIntention rememberRequestedMove(MoveIntention intention) { - // Deadlock rerouting needs to know which tile the robot originally wanted before - // coordination or collision handling changed the outcome of the tick. + // deadlock rerouting needs to know which tile the robot originally wanted before + // coordination or collision handling changed the outcome of the tick if (intention == null || intention.getFromTile() == null || intention.getToTile() == null) { - // Missing tile data means there is no meaningful move request to remember. + // missing tile data means there is no meaningful move request to remember lastRequestedNextTile = null; return intention; } if (intention.getFromTile().getX() == intention.getToTile().getX() && intention.getFromTile().getY() == intention.getToTile().getY()) { - // Waiting in place does not create an alternate tile for reroute recovery to avoid. + // waiting in place does not create an alternate tile for reroute recovery to avoid lastRequestedNextTile = null; } else { - // Store the raw requested destination so the first deadlock recovery can avoid it once. + // store the raw requested destination so the first deadlock recovery can avoid it once lastRequestedNextTile = intention.getToTile().getPosition(); } return intention; @@ -443,7 +490,12 @@ private void clearDeadlockRerouteState() { lastRequestedNextTile = null; } - /** true if this robot qualifies to begin a deadlock reroute attempt */ + /** + * Returns true if this robot qualifies to begin a deadlock reroute attempt. + * + * @return {@code true} if the robot has a task, has not already rerouted, has a nav strategy, + * has a pending tile request, and is not already standing on its target + */ public boolean canStartDeadlockRerouteAttempt() { if (currentTask == null || rerouteAttemptedForCurrentTask || nav == null || lastRequestedNextTile == null) { return false; @@ -452,10 +504,10 @@ public boolean canStartDeadlockRerouteAttempt() { Vector2D target = getTarget(); if (target == null) return false; - // Do not reroute if the robot is already standing on the target tile. - // Rack-adjacency arrival is detected by Robot.update() which transitions to LOADING; + // do not reroute if the robot is already standing on the target tile; + // rack-adjacency arrival is detected by Robot.update() which transitions to LOADING; // recoverDeadlockedRobots only runs on MOVING robots, so that case is already excluded - // before this method is ever called > no need to pass a null map here. + // before this method is ever called — no need to pass a map here if (getPosition().equals(target)) { return false; } diff --git a/open-robotics/src/main/java/com/openrobotics/robot/RobotConfig.java b/open-robotics/src/main/java/com/openrobotics/robot/RobotConfig.java index ec932be7..60f5f353 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/RobotConfig.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/RobotConfig.java @@ -1,6 +1,6 @@ package com.openrobotics.robot; -/** immutable value object holding robot physics constants; passed to each Robot at creation time */ +/** Immutable value object holding robot physics constants; passed to each Robot at creation time. */ public class RobotConfig { public final float batteryCapacity; public final float lowBatteryThreshold; @@ -9,6 +9,16 @@ public class RobotConfig { public final int loadingTicks; public final int unloadingTicks; + /** + * Creates a configuration with explicit physics values. + * + * @param batteryCapacity the maximum battery level + * @param lowBatteryThreshold the battery level below which the robot seeks a charger + * @param chargePerTick the battery units restored each tick while charging + * @param energyPerMove the battery units consumed per tile moved + * @param loadingTicks the number of ticks spent picking up a box + * @param unloadingTicks the number of ticks spent dropping off a box + */ public RobotConfig(float batteryCapacity, float lowBatteryThreshold, float chargePerTick, float energyPerMove, int loadingTicks, int unloadingTicks) { @@ -20,7 +30,11 @@ public RobotConfig(float batteryCapacity, float lowBatteryThreshold, this.unloadingTicks = unloadingTicks; } - /** default config values matching previous hardcoded constants */ + /** + * Returns the default configuration values matching the simulator's original hardcoded constants. + * + * @return a {@code RobotConfig} with standard physics values + */ public static RobotConfig defaults() { return new RobotConfig(100.0f, 20.0f, 5.0f, 1.0f, 1, 1); } diff --git a/open-robotics/src/main/java/com/openrobotics/robot/RobotState.java b/open-robotics/src/main/java/com/openrobotics/robot/RobotState.java index 9575de8e..1bd262b5 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/RobotState.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/RobotState.java @@ -1,11 +1,11 @@ package com.openrobotics.robot; -/** robot state machine states (design doc 3.4.3) */ +/** Robot state machine states (design doc 3.4.3). */ public enum RobotState { - IDLE, // no task, waiting for assignment + IDLE, // no task, waiting for assignment BATTERY_DEAD, // battery depleted, unable to move - MOVING, // navigating toward pickup or dropoff location - LOADING, // robot is at a rack picking up a box - UNLOADING, // robot is at delivery station, dropping off box - CHARGING, // replenishing battery + MOVING, // navigating toward pickup or dropoff location + LOADING, // robot is at a rack picking up a box + UNLOADING, // robot is at delivery station, dropping off box + CHARGING, // replenishing battery } diff --git a/open-robotics/src/main/java/com/openrobotics/robot/SensorType.java b/open-robotics/src/main/java/com/openrobotics/robot/SensorType.java index a80b37d4..a2e68219 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/SensorType.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/SensorType.java @@ -1,10 +1,15 @@ package com.openrobotics.robot; -/** sensor type enum; maps config sensor name strings to enum values */ +/** Sensor type enum; maps config sensor name strings to enum values. */ public enum SensorType { PROXIMITY, RANGE, NONE; - /** returns the SensorType for {@code name}, case-insensitive; returns NONE if unrecognized */ + /** + * Returns the {@code SensorType} for {@code name}, case-insensitive. + * + * @param name the config string (e.g. {@code "PROXIMITY"}, {@code "RANGE"}) + * @return the matching type, or {@code NONE} if the name is {@code null} or unrecognized + */ public static SensorType fromConfigString(String name) { if (name == null) return NONE; String upper = name.toUpperCase().trim(); diff --git a/open-robotics/src/main/java/com/openrobotics/robot/navigation/BugNavigationStrategy.java b/open-robotics/src/main/java/com/openrobotics/robot/navigation/BugNavigationStrategy.java index 3b2f2409..864497ec 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/navigation/BugNavigationStrategy.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/navigation/BugNavigationStrategy.java @@ -12,12 +12,17 @@ import java.util.*; -/** bug2 navigation; alternates between greedy goal-seeking and left-hand boundary following when an obstacle is encountered */ +/** Bug2 navigation; alternates between greedy goal-seeking and left-hand boundary following when an obstacle is encountered. */ public class BugNavigationStrategy implements NavigationStrategy { private final long baseSeed; // per-robot navigation state keyed by robot id (same pattern as greedy) private final java.util.Map navStates = new HashMap<>(); + /** + * Creates a Bug2 navigation strategy with the given base seed. + * + * @param baseSeed the seed used for deterministic tie-breaking per robot + */ public BugNavigationStrategy(long baseSeed) { this.baseSeed = baseSeed; } diff --git a/open-robotics/src/main/java/com/openrobotics/robot/navigation/GreedyNavigationStrategy.java b/open-robotics/src/main/java/com/openrobotics/robot/navigation/GreedyNavigationStrategy.java index 6a737083..53c0c42b 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/navigation/GreedyNavigationStrategy.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/navigation/GreedyNavigationStrategy.java @@ -11,12 +11,17 @@ import java.util.*; -/** greedy navigation; moves toward target by manhattan distance with seeded tie-breaking and backtracking for dead ends */ +/** Greedy navigation; moves toward the target by Manhattan distance with seeded tie-breaking and backtracking for dead ends. */ public class GreedyNavigationStrategy implements NavigationStrategy { private final long baseSeed; // per-robot navigation state keyed by robot id private final java.util.Map navStates = new HashMap<>(); + /** + * Creates a greedy navigation strategy with the given base seed. + * + * @param baseSeed the seed used for deterministic tie-breaking per robot + */ public GreedyNavigationStrategy(long baseSeed) { this.baseSeed = baseSeed; } diff --git a/open-robotics/src/main/java/com/openrobotics/robot/navigation/NavigationStrategy.java b/open-robotics/src/main/java/com/openrobotics/robot/navigation/NavigationStrategy.java index 3b224fd0..aa170eb1 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/navigation/NavigationStrategy.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/navigation/NavigationStrategy.java @@ -6,16 +6,33 @@ import com.openrobotics.robot.Robot; import com.openrobotics.simulationcore.MoveIntention; -/** strategy pattern interface for robot navigation (uml 3.3.4 / 3.4.5) */ +/** Strategy pattern interface for robot navigation. */ public interface NavigationStrategy { - /** returns the robot's intended move for the current tick */ + + /** + * Returns the robot's intended move for the current tick. + * + * @param robot the robot requesting the move + * @param map the current warehouse map + * @return the move intention for this tick + */ MoveIntention getNextMove(Robot robot, Map map); - /** called during deadlock recovery so the strategy can discard any saved search state for this robot */ + /** + * Called during deadlock recovery so the strategy can discard any saved search state for this robot. + * + * @param robot the robot whose state should be cleared + */ default void reset(Robot robot) {} - // Factory: create a navigation strategy for the given algorithm type. - // Unknown or NONE falls back to GREEDY so robots are always moveable. + /** + * Creates a navigation strategy for the given algorithm type; unknown or {@code NONE} falls back + * to GREEDY so robots are always moveable. + * + * @param algo the algorithm type, or {@code null} to use GREEDY + * @param seed the base seed for deterministic tie-breaking + * @return a new {@link NavigationStrategy} instance for the given type + */ static NavigationStrategy create(AlgorithmType algo, long seed) { if (algo == null) return new GreedyNavigationStrategy(seed); return switch (algo) { diff --git a/open-robotics/src/main/java/com/openrobotics/robot/navigation/RandomNavigation.java b/open-robotics/src/main/java/com/openrobotics/robot/navigation/RandomNavigation.java index 3bcafc5f..326cf754 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/navigation/RandomNavigation.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/navigation/RandomNavigation.java @@ -8,10 +8,17 @@ import java.util.Random; -/** random navigation strategy; used in tests only, not wired in production */ +/** Random navigation strategy; used in tests only, not wired in production. */ public class RandomNavigation implements NavigationStrategy { - /** shuffles all four cardinal directions with fisher-yates, skips any that land on rerouteAvoidTile, and returns the first valid move; falls back to WAIT if all are blocked */ + /** + * Shuffles all four cardinal directions with Fisher-Yates, skips any that land on + * {@code rerouteAvoidTile}, and returns the first valid move; falls back to WAIT if all are blocked. + * + * @param robot the robot requesting the move + * @param map the current warehouse map + * @return the move intention for this tick + */ @Override public MoveIntention getNextMove(Robot robot, Map map) { Vector2D position = robot.getPosition(); @@ -19,7 +26,7 @@ public MoveIntention getNextMove(Robot robot, Map map) { Vector2D target = robot.getTarget(); Vector2D rerouteAvoidTile = robot.getRerouteAvoidTile(); - // Try each direction at most once in random order, then fall back to WAIT. + // try each direction at most once in random order, then fall back to WAIT Direction[] directions = Direction.values(); // unseeded — intentionally non-deterministic; this strategy is test-only Random random = new Random(); diff --git a/open-robotics/src/main/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategy.java b/open-robotics/src/main/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategy.java index ad5bf7cd..13e9e081 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategy.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategy.java @@ -11,12 +11,17 @@ import java.util.*; -/** real-time A* navigation; learns heuristic values during exploration and penalizes sensor-detected obstacles */ +/** Real-time A* navigation; learns heuristic values during exploration and penalises sensor-detected obstacles. */ public class RtaStarNavigationStrategy implements NavigationStrategy { private final long baseSeed; // per-robot navigation state keyed by robot id (same pattern as greedy/bug) private final java.util.Map navStates = new HashMap<>(); + /** + * Creates an RTA* navigation strategy with the given base seed. + * + * @param baseSeed the seed used for deterministic tie-breaking per robot + */ public RtaStarNavigationStrategy(long baseSeed) { this.baseSeed = baseSeed; } diff --git a/open-robotics/src/main/java/com/openrobotics/robot/sensors/ProximitySensor.java b/open-robotics/src/main/java/com/openrobotics/robot/sensors/ProximitySensor.java index 9bc789b8..2f37a203 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/sensors/ProximitySensor.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/sensors/ProximitySensor.java @@ -7,7 +7,7 @@ import java.util.ArrayList; import java.util.List; -/** proximity sensor; scans all 8 tiles immediately surrounding the robot (range = 1) */ +/** Proximity sensor; scans all 8 tiles immediately surrounding the robot (range = 1). */ public class ProximitySensor implements SensorStrategy { private final int range = 1; diff --git a/open-robotics/src/main/java/com/openrobotics/robot/sensors/RangeSensor.java b/open-robotics/src/main/java/com/openrobotics/robot/sensors/RangeSensor.java index e5633c2a..da1983a5 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/sensors/RangeSensor.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/sensors/RangeSensor.java @@ -11,7 +11,7 @@ import java.util.List; import java.util.Set; -/** range sensor; casts 15 rays in a 90-degree arc ahead of the robot, stopping each ray at the first Obstacle */ +/** Range sensor; casts 15 rays in a 90-degree arc ahead of the robot, stopping each ray at the first {@link Obstacle}. */ public class RangeSensor implements SensorStrategy { private final double maxRange = 5.0; private final int rayCount = 15; // Number of rays (90 degrees / 15 = 1 ray every 6 deg) diff --git a/open-robotics/src/main/java/com/openrobotics/robot/sensors/Sensor.java b/open-robotics/src/main/java/com/openrobotics/robot/sensors/Sensor.java index f72a4189..47d35bda 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/sensors/Sensor.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/sensors/Sensor.java @@ -7,10 +7,15 @@ import java.util.List; import java.util.Set; -/** result of a single sensor scan; exposes detected entities and spatial query methods */ +/** Result of a single sensor scan; exposes detected entities and spatial query methods. */ public class Sensor { private final List detectedEntities; + /** + * Creates a scan result from the given list of detected entities. + * + * @param entities the entities detected during this scan + */ public Sensor(List entities) { this.detectedEntities = entities; } @@ -19,7 +24,12 @@ public List getDetectedEntities() { return detectedEntities; } - /** true if an Obstacle entity was detected at the given position */ + /** + * Returns true if an {@link Obstacle} entity was detected at the given position. + * + * @param pos the position to check + * @return {@code true} if an obstacle was detected at {@code pos} + */ public boolean isObstacleDetectedAt(Vector2D pos) { for (MapEntity e : detectedEntities) { if (e instanceof Obstacle && e.getPosition().equals(pos)) { @@ -29,7 +39,11 @@ public boolean isObstacleDetectedAt(Vector2D pos) { return false; } - /** positions of all detected Obstacle entities; convenience for navigation strategies */ + /** + * Returns the positions of all detected {@link Obstacle} entities; convenience for navigation strategies. + * + * @return a set of positions where obstacles were detected + */ public Set getObstaclePositions() { Set positions = new HashSet<>(); for (MapEntity e : detectedEntities) { @@ -39,9 +53,14 @@ public Set getObstaclePositions() { } /** - * steps from {@code from} toward {@code toward} that are free of detected obstacles. - * tiles beyond the sensor's scan range are treated as unknown (not clear) — a PROXIMITY robot - * always returns maxDist, while a RANGE robot returns the real value (1–5) + * Returns the number of steps from {@code from} toward {@code toward} that are free of detected obstacles. + * Tiles beyond the sensor's scan range are treated as unknown (not confirmed clear) — a PROXIMITY + * robot always returns {@code maxDist}, while a RANGE robot returns the real value (1–5). + * + * @param from the robot's current position + * @param toward the candidate next tile + * @param maxDist the maximum distance to check (scan range cap) + * @return the number of obstacle-free steps, up to {@code maxDist} */ public int knownClearanceToward(Vector2D from, Vector2D toward, int maxDist) { if (from != null && from.equals(toward)) { @@ -56,4 +75,4 @@ public int knownClearanceToward(Vector2D from, Vector2D toward, int maxDist) { } return maxDist; } -} \ No newline at end of file +} diff --git a/open-robotics/src/main/java/com/openrobotics/robot/sensors/SensorStrategy.java b/open-robotics/src/main/java/com/openrobotics/robot/sensors/SensorStrategy.java index 61df66f3..70710430 100644 --- a/open-robotics/src/main/java/com/openrobotics/robot/sensors/SensorStrategy.java +++ b/open-robotics/src/main/java/com/openrobotics/robot/sensors/SensorStrategy.java @@ -4,13 +4,25 @@ import com.openrobotics.robot.Robot; import com.openrobotics.robot.SensorType; -/** strategy interface for sensor scanning; implementations determine scan range and detection logic */ +/** Strategy interface for sensor scanning; implementations determine scan range and detection logic. */ public interface SensorStrategy { - /** scans the environment around the robot and returns all detected entities */ + + /** + * Scans the environment around the robot and returns all detected entities. + * + * @param robot the scanning robot + * @param map the current warehouse map + * @return a {@link Sensor} containing all detected entities + */ Sensor scan(Robot robot, Map map); - // Factory: create a sensor strategy for the given sensor type. - // Unknown or NONE falls back to PROXIMITY so robots always have a sensor. + /** + * Creates a sensor strategy for the given sensor type; unknown or {@code NONE} falls back to + * PROXIMITY so robots always have a sensor. + * + * @param type the sensor type, or {@code null} to use PROXIMITY + * @return a new {@link SensorStrategy} instance for the given type + */ static SensorStrategy create(SensorType type) { if (type == null) return new ProximitySensor(); return switch (type) { @@ -18,4 +30,4 @@ static SensorStrategy create(SensorType type) { case PROXIMITY, NONE -> new ProximitySensor(); }; } -} \ No newline at end of file +} diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/CollisionManager.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/CollisionManager.java index 88e4bdef..4e24bb18 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/CollisionManager.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/CollisionManager.java @@ -14,13 +14,20 @@ import java.util.*; -/** detects and resolves same-target and swap conflicts among move intentions each tick */ +/** Detects and resolves same-target and swap conflicts among move intentions each tick. */ public class CollisionManager { // legal checks: // - no null intention/from/to // - robot id must be present // - one tile per tick max by Manhattan distance // and distance 0 means wait/non move, which is fine + /** + * Returns {@code true} if the intention is structurally valid for this tick. + * A legal intention has non-null robot, from-tile, and to-tile, and moves at most one Manhattan step. + * + * @param intention the move intention to validate + * @return {@code true} if the intention is legal; {@code false} otherwise + */ public boolean isLegalIntention(MoveIntention intention) { if (intention == null || intention.getRobot() == null || @@ -55,10 +62,24 @@ private boolean allowsOverlap(Tile tile) { return tile.allowsRobotOverlap(); } + /** + * Resolves conflicts without map context; same-target and swap conflicts are still detected. + * + * @param intentions the raw move intentions for this tick + * @return the approved subset of intentions with conflicts removed + */ public MoveIntention[] resolveConflicts(MoveIntention[] intentions) { return resolveConflicts(null, intentions); } + /** + * Resolves same-target, starting-tile occupancy, and swap conflicts among all submitted intentions. + * Robots blocked by a dead robot's tile are also held. Returns only the approved intentions. + * + * @param map the current warehouse map, used to detect dead-robot tile occupancy; may be {@code null} + * @param intentions the raw move intentions for this tick + * @return the approved subset of intentions with all detected conflicts removed + */ public MoveIntention[] resolveConflicts(com.openrobotics.map.Map map, MoveIntention[] intentions) { if (intentions == null || intentions.length == 0) { return new MoveIntention[0]; diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/CoordinationPolicy.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/CoordinationPolicy.java index bcd6c139..7fa619da 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/CoordinationPolicy.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/CoordinationPolicy.java @@ -6,18 +6,33 @@ import java.util.ArrayList; import java.util.List; -/** coordination policy interface; implementations filter or delay move intentions each tick */ +/** Coordination policy interface; implementations filter or delay move intentions each tick. */ public interface CoordinationPolicy { CoordinationPolicy NO_OP = (map, intentions) -> copyNonNull(intentions); - // Applies policy rules to this tick's intentions - // Policy can force robots to wait by returning a WAIT intention + /** + * Applies policy rules to this tick's intentions. + * Implementations may force robots to wait by substituting a stay-in-place intention. + * + * @param map the current warehouse map + * @param intentions the raw move intentions for this tick + * @return the filtered or modified intentions after policy is applied + */ MoveIntention[] apply(Map map, MoveIntention[] intentions); - // Coordination-state cleanup hook used by both reroute attempts and full fallback recovery. + /** + * Releases any per-robot coordination state held by this policy. + * Called on both reroute attempts and full fallback deadlock recovery. + * + * @param robot the robot whose state should be cleared + */ default void clearRobotCoordinationState(Robot robot) {} - // Default policy: return a copy with null intentions removed + /** + * Returns the no-op policy; passes all non-null intentions through unchanged. + * + * @return the shared no-op {@link CoordinationPolicy} instance + */ static CoordinationPolicy noOp() { return NO_OP; } diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/Dispatcher.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/Dispatcher.java index 868c9580..6bf5522e 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/Dispatcher.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/Dispatcher.java @@ -1,10 +1,6 @@ package com.openrobotics.simulationcore; import com.openrobotics.AppState; -import com.openrobotics.db.model.WorkloadTaskRecord; -import com.openrobotics.logging.Logger; -import com.openrobotics.logging.eventtypes.TaskEvent; -import com.openrobotics.db.recordbuilders.WorkloadTaskRecordBuilder; import com.openrobotics.robot.Robot; import com.openrobotics.robot.RobotState; import com.openrobotics.task.Task; @@ -12,7 +8,7 @@ import java.util.*; -/** manages a priority queue of tasks and assigns them to available robots each tick */ +/** Manages a priority queue of tasks and assigns them to available robots each tick. */ public class Dispatcher { private final List lifetimeTasks; // stores all tasks ever added to this dispatcher private final PriorityQueue taskQueue; @@ -24,7 +20,11 @@ public Dispatcher() { this.totalTasksAdded = 0; } - /** adds a task to the pending queue if it is not null and not already present */ + /** + * Adds a task to the pending queue and the lifetime task list. + * + * @param task the task to enqueue; must not be {@code null} or already present + */ public void addTask(Task task) { enqueueTask(task, true); lifetimeTasks.add(task); @@ -44,7 +44,11 @@ private void enqueueTask(Task task, boolean countTowardsTotal) { } } - /** adds tasks to the queue in bulk */ + /** + * Adds tasks to the queue in bulk. + * + * @param tasks the list of tasks to enqueue; must not be {@code null} and must contain no {@code null} entries + */ public void addTasks(List tasks) { if (tasks == null) { throw new IllegalArgumentException("Tasks list cannot be null"); @@ -60,9 +64,10 @@ public void addTasks(List tasks) { } /** - * assigns at most one pending task per available robot; logs each assignment. + * Assigns at most one pending task per available robot; logs each assignment. * - * @return number of assignments made this call + * @param robots the array of robots to assign tasks to; must not be {@code null} + * @return the number of assignments made during this call */ public int assignTasks(Robot[] robots) { if (robots == null) { @@ -71,7 +76,6 @@ public int assignTasks(Robot[] robots) { return 0; // there are no tasks available, no assignments are made } - int currentTick = AppState.getEngine().getTickCounter(); // getting the current simulation tick from global app state int assignmentCount = 0; for (Robot robot : robots) { @@ -95,10 +99,6 @@ public int assignTasks(Robot[] robots) { taskQueue.poll(); assignmentCount++; - - WorkloadTaskRecordBuilder recordBuilder = new WorkloadTaskRecordBuilder(AppState.getEngine().getRunId(), task); - WorkloadTaskRecord record = recordBuilder.buildTaskAssignmentRecord(currentTick, robot.getId()); - Logger.logTaskEvent(TaskEvent.TASK_ASSIGNED, record); } catch (RuntimeException ex) { System.err.println("[Dispatcher] Failed to assign task " + task.getId() + " to robot " + robot.getId() + ": " + ex.getMessage()); @@ -109,7 +109,11 @@ public int assignTasks(Robot[] robots) { return assignmentCount; } - /** requeues a task without incrementing the total count; used for deadlock recovery */ + /** + * Requeues a task without incrementing the total count; used for deadlock recovery. + * + * @param task the task to requeue; must not be {@code null} or already present in the queue + */ public void requeueTask(Task task) { enqueueTask(task, false); } @@ -122,13 +126,22 @@ public int getPendingTaskCount() { return taskQueue.size(); } + /** + * Returns all currently queued (pending) tasks sorted by natural task ordering. + * + * @return sorted list of pending tasks + */ public List getAllQueuedTasks() { List tasks = new ArrayList<>(taskQueue); tasks.sort(Task::compareTo); return tasks; } - /** returns all tasks ever added to this dispatcher, including completed ones */ + /** + * Returns all tasks ever added to this dispatcher, including completed and in-progress ones. + * + * @return a snapshot copy of the lifetime task list + */ public List getLifetimeTasks() { return new ArrayList<>(lifetimeTasks); } @@ -136,6 +149,7 @@ public List getLifetimeTasks() { /** * Returns the total number of tasks ever added to this dispatcher. * Used to distinguish "no tasks were configured" from "all tasks completed". + * * @return the total number of tasks added since construction */ public int getTotalTasksAdded() { diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/MoveIntention.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/MoveIntention.java index 99a90943..41d90078 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/MoveIntention.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/MoveIntention.java @@ -5,12 +5,19 @@ import java.util.UUID; -/** represents a pending move request from a robot for one tick */ +/** Represents a pending move request from a robot for one tick. */ public class MoveIntention { private final Robot robot; private final Tile from; private final Tile to; + /** + * Creates a move intention for the given robot. + * + * @param from the tile the robot is currently occupying + * @param to the tile the robot intends to move to (may equal {@code from} for a wait) + * @param robot the robot submitting this intention + */ public MoveIntention(Tile from, Tile to, Robot robot) { this.from = from; this.to = to; diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/ReservationKPolicy.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/ReservationKPolicy.java index 93d495ec..a24e3597 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/ReservationKPolicy.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/ReservationKPolicy.java @@ -8,8 +8,8 @@ import java.util.*; /** - * reservation-k coordination policy; a robot must hold locks for the next k tiles on its path before it can move. - * as the robot progresses, tiles it already reached are released. + * Reservation-K coordination policy; a robot must hold locks for the next {@code k} tiles on its path + * before it can move. As the robot progresses, tiles it has already reached are released. */ public class ReservationKPolicy implements CoordinationPolicy { private final int k; @@ -20,6 +20,12 @@ public class ReservationKPolicy implements CoordinationPolicy { // Tracks the ordered reservation window for each robot. private final java.util.Map> robotReservations = new HashMap<>(); + /** + * Creates a Reservation-K policy with the given look-ahead window size. + * + * @param k the number of tiles ahead a robot must lock before moving; must be {@code >= 1} + * @throws IllegalArgumentException if {@code k < 1} + */ public ReservationKPolicy(int k) { if (k < 1) { throw new IllegalArgumentException("k must be >= 1"); diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationEngine.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationEngine.java index 94bdee36..0bc29f4c 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationEngine.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationEngine.java @@ -24,7 +24,7 @@ import java.io.IOException; import java.util.*; -/** tick-based simulation engine; coordinates robot movement, task dispatch, collision resolution, and deadlock recovery */ +/** Tick-based simulation engine; coordinates robot movement, task dispatch, collision resolution, and deadlock recovery. */ public class SimulationEngine { private UUID runId; private static final int DEADLOCK_RECOVERY_THRESHOLD = 5; @@ -48,20 +48,46 @@ public class SimulationEngine { private boolean initialized; private String initError; - /** constructs an engine with default run name, tickMs=100, maxTicks=5000, seed=42 */ + /** + * Creates an engine with default run name, {@code tickMs=100}, {@code maxTicks=5000}, {@code seed=42}. + * + * @param map the warehouse map + * @param robots the array of robots to simulate + * @param dispatcher the task dispatcher + * @param coordinationPolicy the coordination policy; {@code null} is treated as no-op + */ public SimulationEngine(Map map, Robot[] robots, Dispatcher dispatcher, CoordinationPolicy coordinationPolicy) { this(map, robots, dispatcher, coordinationPolicy, "default_run", 100, 5000, 42); } - /** constructs an engine with explicit run config; delegates to the 9-param constructor with maxTasks=10 */ + /** + * Creates an engine with an explicit run config; delegates to the 9-param constructor with {@code maxTasks=10}. + * + * @param map the warehouse map + * @param robots the array of robots to simulate + * @param dispatcher the task dispatcher + * @param coordinationPolicy the coordination policy; {@code null} is treated as no-op + * @param runName name used to identify this run in logs and database records + * @param tickMs milliseconds per simulation tick + * @param maxTicks maximum number of ticks before the simulation halts + * @param seed RNG seed for reproducible task generation and navigation + */ public SimulationEngine(Map map, Robot[] robots, Dispatcher dispatcher, CoordinationPolicy coordinationPolicy, String runName, int tickMs, int maxTicks, long seed) { this(map, robots, dispatcher, coordinationPolicy, runName, tickMs, maxTicks, seed, 10); } /** - * primary constructor; all other constructors delegate here. + * Primary constructor; all other programmatic constructors delegate here. * + * @param map the warehouse map + * @param robots the array of robots to simulate + * @param dispatcher the task dispatcher + * @param coordinationPolicy the coordination policy; {@code null} is treated as no-op + * @param runName name used to identify this run in logs and database records + * @param tickMs milliseconds per simulation tick + * @param maxTicks maximum number of ticks before the simulation halts + * @param seed RNG seed for reproducible task generation and navigation * @param maxTasks absolute cap on total tasks generated (applies to both static and dynamic workload modes) */ public SimulationEngine(Map map, Robot[] robots, Dispatcher dispatcher, CoordinationPolicy coordinationPolicy, @@ -84,7 +110,12 @@ public SimulationEngine(Map map, Robot[] robots, Dispatcher dispatcher, Coordina this.initialized = map != null && robots != null && dispatcher != null; } - /** constructs an engine by loading state from a JSON config file; use {@link #getInitError()} to check for load failures */ + /** + * Creates an engine by loading state from a JSON config file. + * Use {@link #getInitError()} to check whether loading succeeded. + * + * @param configFilePath path to the JSON config file; {@code null} produces an uninitialised engine + */ public SimulationEngine(String configFilePath) { if (configFilePath != null) { configInitialization(configFilePath); @@ -282,7 +313,7 @@ private void addObstaclesToMap(List entityDtos } /** - * saves the current simulation state to a JSON config file. + * Saves the current simulation state to a JSON config file. * * @param path destination file path * @throws IOException if the map is not initialized or the file cannot be written @@ -435,10 +466,11 @@ private SimulationConfigDTO.MapEntityDTO mapToEntityDTO(MapEntity entity) { } /** - * advances the simulation by one tick: dispatches tasks, collects intentions, applies coordination policy, + * Advances the simulation by one tick: dispatches tasks, collects intentions, applies coordination policy, * resolves conflicts, commits moves, runs robot state machines, and handles deadlock recovery. * - * @return true if the simulation is still running; false when workload is complete, tick limit reached, or all robots dead + * @return {@code true} if the simulation is still running; {@code false} when the workload is complete, + * the tick limit is reached, or all robots are dead */ public boolean tick() { if (!initialized || robots == null || dispatcher == null || collisionManager == null || map == null) { @@ -452,14 +484,27 @@ public boolean tick() { return false; } + // guard against ticking when no tasks were ever added to the dispatcher + if (dispatcher.getTotalTasksAdded() == 0) { + this.running = false; + + // differentiate between no tasks because of manual assignment (user didn't add any) vs. automatic generation (generation failed) + if (manualTaskAssignment) { + simulationError = SimulationError.NO_TASKS_ASSIGNED; + } else { + simulationError = SimulationError.NO_TASKS_GENERATED; + } + + return false; + } + if (tickCounter >= maxTicks) { this.running = false; return false; } - // "No configured tasks" is treated as sandbox mode: ticks still run. - // Only short-circuit when a workload was actually configured and is now finished. - if (dispatcher.getTotalTasksAdded() > 0 && workloadComplete()) { + // Check if workload complete before ticking + if (workloadComplete()) { this.running = false; SimulationRunRecordBuilder recordBuilder = new SimulationRunRecordBuilder(this); @@ -598,7 +643,6 @@ private void recoverDeadlockedRobots() { } // Skipping recovery for robots with depleted batteries - // TODO: Consider a method for handling dead robots if (robot.getState() == RobotState.BATTERY_DEAD) { continue; // Let battery recovery handle this robot, don't interfere with task recovery } @@ -689,7 +733,11 @@ private TrafficRulesPolicy buildTrafficRulesPolicy(Set intersectionPos public RobotConfig getRobotConfig() { return robotConfig; } - /** Replaces the robot physics config and propagates it to all loaded robots. */ + /** + * Replaces the robot physics config and propagates it to all loaded robots. + * + * @param config the new robot config to apply + */ public void setRobotConfig(RobotConfig config) { this.robotConfig = config; if (robots != null) { @@ -701,6 +749,10 @@ public UUID getRunId() { return runId; } + public String getRunName() { + return runName; + } + public Map getMap() { return map; } @@ -717,9 +769,11 @@ public void setManualTaskAssignment(boolean manualTaskAssignment) { } /** - * A run is finished when the clock has started, every robot is idle, and - * the dispatcher has no more queued tasks. tickCounter==0 is never - * finished so Play on a fresh engine is always allowed. + * Returns {@code true} when the clock has started, every robot is idle, and the dispatcher has no + * more queued tasks. A {@code tickCounter} of {@code 0} is never considered finished, so Play on a + * fresh engine is always allowed. + * + * @return {@code true} if the run is finished; {@code false} otherwise */ public boolean isFinished() { if (tickCounter <= 0) return false; @@ -750,13 +804,19 @@ public Robot[] getRobots() { public long getSeed() { return seed; } - /** returns the fully-qualified class name of the active coordination policy, or null if none is set */ + /** + * Returns the fully-qualified class name of the active coordination policy. + * + * @return the class name of the active policy, or {@code null} if none is set + */ public String getCoordinationPolicy() { return coordinationPolicy != null ? coordinationPolicy.getClass().getName() : null; } /** * Returns whether the active coordination policy is traffic-rules based. + * + * @return {@code true} if the active policy is a {@link TrafficRulesPolicy} */ public boolean usesTrafficRulesPolicy() { return coordinationPolicy instanceof TrafficRulesPolicy; @@ -764,6 +824,8 @@ public boolean usesTrafficRulesPolicy() { /** * Returns a copy of the configured traffic-rules intersection positions. + * + * @return unmodifiable set of intersection positions; empty if the active policy is not traffic-rules based */ public Set getTrafficRuleIntersections() { if (!(coordinationPolicy instanceof TrafficRulesPolicy policy)) { @@ -781,6 +843,10 @@ public Set getTrafficRuleIntersections() { /** * Returns whether the provided tile is configured as a traffic-rules intersection. + * + * @param x the tile x-coordinate + * @param y the tile y-coordinate + * @return {@code true} if the tile at ({@code x}, {@code y}) is a registered intersection */ public boolean hasTrafficRuleIntersection(int x, int y) { return getTrafficRuleIntersections().contains(new Vector2D(x, y)); @@ -789,6 +855,8 @@ public boolean hasTrafficRuleIntersection(int x, int y) { /** * Toggles the provided tile in the traffic-rules intersection set. * + * @param x the tile x-coordinate to toggle + * @param y the tile y-coordinate to toggle * @return {@code true} when the active policy supports traffic-rule intersections * and the coordinate exists on the map; {@code false} otherwise */ @@ -811,7 +879,11 @@ public Dispatcher getDispatcher() { return dispatcher; } - /** returns the initialization error message, or null if config loading succeeded */ + /** + * Returns the initialization error message set during config loading. + * + * @return the error message, or {@code null} if config loading succeeded + */ public String getInitError() { return initError; } @@ -822,7 +894,8 @@ public SimulationError getSimulationError() { /** * Returns a user-friendly error message based on the current simulation error state. - * @return a user-friendly error message if a simulation error is present, or null if no error has occurred. + * + * @return a user-friendly error message if a simulation error is present, or {@code null} if no error has occurred */ public String getSimulationErrorMessage() { if (simulationError == null) { @@ -830,14 +903,15 @@ public String getSimulationErrorMessage() { return "An unknown error has occurred in the simulation."; } - switch (simulationError) { - case ALL_ROBOTS_DEAD: - return "All robots have depleted their batteries. Simulation cannot continue.\nConsider adding more charging stations to the map."; - case NO_ROBOTS_SPAWNED: - return "No robots were spawned in the simulation. Simulation cannot run.\nPlease add robots to the simulation"; - default: - return "An unknown error has occurred in the simulation."; - } + return simulationError.getMessage(); + } + + /** + * Sets the current simulation error state + * @param error the SimulationError to set for the simulation + */ + public void setSimulationError(SimulationError error) { + this.simulationError = error; } // generates a new runId; called on reset so logging for each run is isolated @@ -847,6 +921,7 @@ private void updateRunId() { /** * Adds an entity to the simulation map at runtime. + * * @param entity the entity to add */ public void addEntity(MapEntity entity) { @@ -860,8 +935,9 @@ public void addEntity(MapEntity entity) { /** * Removes an entity from the simulation map at runtime. + * * @param entity the entity to remove - * @return true if the entity was removed + * @return {@code true} if the entity was present and removed; {@code false} otherwise */ public boolean removeEntity(MapEntity entity) { if (map != null && entity != null) { diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationError.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationError.java index d8fb5b87..7e825012 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationError.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/SimulationError.java @@ -1,7 +1,21 @@ package com.openrobotics.simulationcore; +/** Terminal error states that halt the simulation. */ public enum SimulationError { - NONE, - ALL_ROBOTS_DEAD, - NO_ROBOTS_SPAWNED, + NONE("No errors detected. Simulation is running smoothly."), + ALL_ROBOTS_DEAD("All robots have depleted their batteries. Simulation cannot continue.\nConsider adding more charging stations to the map."), + NO_ROBOTS_SPAWNED("No robots were spawned in the simulation. Simulation cannot run.\nPlease add robots to the simulation"), + INVALID_MAP_CONFIGURATION("The map configuration is invalid. Simulation cannot run.\nEnsure the map has at least one rack and one delivery station."), + NO_TASKS_ASSIGNED("No tasks were added to the simulation. Simulation cannot run.\nPlease manually add tasks to the simulation by configuring racks on the map."), + NO_TASKS_GENERATED("No tasks were generated for the simulation. Simulation cannot continue.\nThere was likely an error with automatic task generation."); + + private final String message; + + SimulationError(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } } diff --git a/open-robotics/src/main/java/com/openrobotics/simulationcore/TrafficRulesPolicy.java b/open-robotics/src/main/java/com/openrobotics/simulationcore/TrafficRulesPolicy.java index 5660e97e..074fa97c 100644 --- a/open-robotics/src/main/java/com/openrobotics/simulationcore/TrafficRulesPolicy.java +++ b/open-robotics/src/main/java/com/openrobotics/simulationcore/TrafficRulesPolicy.java @@ -3,12 +3,13 @@ import com.openrobotics.map.Map; import com.openrobotics.map.Tile; import com.openrobotics.robot.Robot; +import com.openrobotics.robot.RobotState; import java.util.*; /** - * traffic-rules coordination policy; grants exclusive access to marked intersection tiles one robot at a time, - * using a priority queue ordered by wait time and load status + * Traffic-rules coordination policy; grants exclusive access to marked intersection tiles one robot at a time, + * using a priority queue ordered by wait time and load status. */ public class TrafficRulesPolicy implements CoordinationPolicy { private final Set intersections; @@ -20,6 +21,11 @@ public class TrafficRulesPolicy implements CoordinationPolicy { // The robot currently inside the intersection area, if any. private UUID activeIntersectionRobotId; + /** + * Creates a traffic-rules policy with the given set of intersection tiles. + * + * @param intersectionTiles the tiles that require exclusive access; {@code null} is treated as empty + */ public TrafficRulesPolicy(Set intersectionTiles) { this.intersections = (intersectionTiles == null) ? new HashSet<>() @@ -55,6 +61,12 @@ public MoveIntention[] apply(Map map, MoveIntention[] intentions) { continue; } + if (robot.getState() == RobotState.BATTERY_DEAD) { + clearRobotCoordinationState(robot); + result.add(intention); + continue; + } + Tile from = intention.getFromTile(); Tile to = intention.getToTile(); boolean actualMove = isActualMove(intention); @@ -156,6 +168,11 @@ private void releaseIntersectionIfOwnerExited(java.util.Map robotsB return; } + if (owner.getState() == RobotState.BATTERY_DEAD) { + activeIntersectionRobotId = null; + return; + } + if (owner.getPosition() == null) { activeIntersectionRobotId = null; return; @@ -214,10 +231,32 @@ private MoveIntention[] sortByRobotId(MoveIntention[] intentions) { return intentions; } + /** + * Returns a copy of the registered intersection tiles. + * + * @return a new set containing the current intersection tiles + */ public Set getIntersectionTiles() { return new HashSet<>(intersections); } + @Override + public void clearRobotCoordinationState(Robot robot) { + if (robot == null) { + return; + } + + UUID robotId = robot.getId(); + if (robotId == null) { + return; + } + + intersectionQueue.remove(robotId); + if (robotId.equals(activeIntersectionRobotId)) { + activeIntersectionRobotId = null; + } + } + private boolean hasTiles(MoveIntention intention) { return intention.getFromTile() != null && intention.getToTile() != null; } diff --git a/open-robotics/src/main/java/com/openrobotics/task/Task.java b/open-robotics/src/main/java/com/openrobotics/task/Task.java index 81b6254d..7eb38220 100644 --- a/open-robotics/src/main/java/com/openrobotics/task/Task.java +++ b/open-robotics/src/main/java/com/openrobotics/task/Task.java @@ -4,7 +4,7 @@ import java.util.Objects; -/** a task assigned to a robot; pairs a rack pickup location with a delivery station dropoff */ +/** A task assigned to a robot; pairs a rack pickup location with a delivery station dropoff. */ public class Task implements Comparable { private final long id; // unique id private long artificialId; // temporary fix for disconnect between database and application task ids @@ -13,6 +13,15 @@ public class Task implements Comparable { private int priority; // higher = more urgent private TaskStatus status; // PENDING, IN_PROGRESS, COMPLETED, FAILED + /** + * Creates a new task with the given id, locations, and priority. + * Status is initialised to {@link TaskStatus#PENDING}. + * + * @param id unique task identifier + * @param pickupLocation the rack tile the robot must visit first + * @param dropoffLocation the delivery station tile the robot must deliver to + * @param priority higher values are more urgent + */ public Task(long id, Vector2D pickupLocation, Vector2D dropoffLocation, int priority) { this.id = id; this.pickupLocation = pickupLocation; @@ -28,7 +37,12 @@ public Task(long id, Vector2D pickupLocation, Vector2D dropoffLocation, int prio public int getPriority() { return priority; } public TaskStatus getStatus() { return status; } - /** avoid mutating priority while this task is in a sorted collection; it can invalidate ordering assumptions */ + /** + * Mutates the task priority; avoid calling while this task is in a sorted collection + * as it can invalidate ordering assumptions. + * + * @param priority the new priority value + */ @Deprecated public void setPriority(int priority) { this.priority = priority; } public void setStatus(TaskStatus status) { this.status = status; } @@ -56,7 +70,7 @@ public int hashCode() { return Objects.hash(id); } - /** higher priority first; ties broken by ascending task id */ + // higher priority first; ties broken by ascending task id @Override public int compareTo(Task task) { int byPriority = Integer.compare(task.priority, this.priority); diff --git a/open-robotics/src/main/java/com/openrobotics/task/TaskGenerator.java b/open-robotics/src/main/java/com/openrobotics/task/TaskGenerator.java index cd6c2c2e..543ad9c1 100644 --- a/open-robotics/src/main/java/com/openrobotics/task/TaskGenerator.java +++ b/open-robotics/src/main/java/com/openrobotics/task/TaskGenerator.java @@ -8,21 +8,30 @@ import java.util.*; -/** generates Task objects by pairing rack pickup positions with delivery station dropoff positions */ +/** Generates {@link Task} objects by pairing rack pickup positions with delivery station dropoff positions. */ public class TaskGenerator { private final Map map; private final Random random; private int nextTaskId = 1; + /** + * Creates a TaskGenerator for the given map. + * + * @param map the warehouse map to generate tasks for + * @param seed RNG seed for reproducible task generation + */ public TaskGenerator(Map map, long seed) { this.map = map; this.random = new Random(seed); } /** - * Generates tasks by pairing rack positions (pickups) with valid delivery - * station positions (dropoffs). Respects per-rack boxCount and validDropoffIds. - * If a rack has no validDropoffIds set, all delivery stations are valid dropoffs. + * Generates tasks by pairing rack positions (pickups) with valid delivery station positions (dropoffs). + * Respects per-rack {@code boxCount} and {@code validDropoffIds}. + * If a rack has no {@code validDropoffIds} set, all delivery stations are valid dropoffs. + * + * @param maxCount the maximum number of tasks to generate + * @return list of generated tasks, at most {@code maxCount} entries */ public List generateTasks(int maxCount) { List tasks = new ArrayList<>(); @@ -96,6 +105,11 @@ public List generateTasks(int maxCount) { /** * Generates exactly {@code count} tasks by randomly sampling rack pickups and delivery station * dropoffs, ignoring per-rack box counts and dropoff pools. Used in Automatic task assignment mode. + * + * @param map the warehouse map to sample from + * @param count the exact number of tasks to generate + * @param seed RNG seed for reproducible output + * @return list of exactly {@code count} tasks, or an empty list if the map has no racks or stations */ public static List generateAutomaticTasks(Map map, int count, long seed) { List pickups = new ArrayList<>(); @@ -121,7 +135,14 @@ else if (e instanceof DeliveryStation ds) return tasks; } - /** convenience factory; constructs a TaskGenerator with the given seed and generates up to {@code count} tasks */ + /** + * Convenience factory; constructs a {@link TaskGenerator} with the given seed and generates up to {@code count} tasks. + * + * @param map the warehouse map to generate tasks for + * @param count the maximum number of tasks to generate + * @param seed RNG seed for reproducible output + * @return list of generated tasks, at most {@code count} entries + */ public static List generateRandomTasks(Map map, int count, long seed) { return new TaskGenerator(map, seed).generateTasks(count); } diff --git a/open-robotics/src/main/java/com/openrobotics/task/TaskStatus.java b/open-robotics/src/main/java/com/openrobotics/task/TaskStatus.java index 2323d9a4..ca365f54 100644 --- a/open-robotics/src/main/java/com/openrobotics/task/TaskStatus.java +++ b/open-robotics/src/main/java/com/openrobotics/task/TaskStatus.java @@ -1,6 +1,6 @@ package com.openrobotics.task; -/** task lifecycle states */ +/** Task lifecycle states. */ public enum TaskStatus { PENDING, IN_PROGRESS, diff --git a/open-robotics/src/main/java/com/openrobotics/task/UnloadBox.java b/open-robotics/src/main/java/com/openrobotics/task/UnloadBox.java index ccaea676..59b6c489 100644 --- a/open-robotics/src/main/java/com/openrobotics/task/UnloadBox.java +++ b/open-robotics/src/main/java/com/openrobotics/task/UnloadBox.java @@ -2,7 +2,7 @@ import com.openrobotics.map.Vector2D; -/** specific task type for unloading a box (uml 3.3.4) */ +/** Specific task type for unloading a box. */ public class UnloadBox extends Task { public UnloadBox(int id, Vector2D pickupLocation, Vector2D dropoffLocation, int priority) { super(id, pickupLocation, dropoffLocation, priority); diff --git a/open-robotics/src/main/java/com/openrobotics/util/IconLoader.java b/open-robotics/src/main/java/com/openrobotics/util/IconLoader.java index e1260038..d2ba5c26 100644 --- a/open-robotics/src/main/java/com/openrobotics/util/IconLoader.java +++ b/open-robotics/src/main/java/com/openrobotics/util/IconLoader.java @@ -1,31 +1,25 @@ package com.openrobotics.util; import javafx.scene.image.Image; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -/** - * Utility class for loading and caching object icons. - * Provides a unified interface for retrieving icons for different entity types. - * Icons are cached after first load to avoid repeated file I/O. - */ +/** Loads and caches entity icons from resources; all calls happen on the JavaFX thread. */ public class IconLoader { - private static final Map> imageCache = new ConcurrentHashMap<>(); + private static final Map> imageCache = new HashMap<>(); - // Standard object types with icon assets private static final List SUPPORTED_TYPES = List.of( "ROBOT", "CHARGER", "STATION", "DOCK", "WALL", "SHELF", "RACK", "OBSTACLE", "DELIVERY", "INTERSECTION" ); /** - * Gets or loads an icon for the specified object type. - * Icons are cached; subsequent calls return from cache. + * Returns the cached icon for the given type, loading it on first access. * - * @param type the object type (e.g., "ROBOT", "CHARGER") - * @return the icon image, or null if not found + * @param type the entity type string (e.g. {@code "ROBOT"}, {@code "RACK"}); case-insensitive + * @return the icon {@link Image}, or {@code null} if the resource was not found */ public static Image getIcon(String type) { if (type == null) return null; @@ -36,20 +30,14 @@ public static Image getIcon(String type) { .orElse(null); } - /** - * Pre-loads all standard icons at startup. - * Call this during application initialization to avoid load delays. - */ + /** Pre-loads all standard icons to avoid first-use delays. */ public static void preloadAllIcons() { for (String type : SUPPORTED_TYPES) { getIcon(type); } } - /** - * Clears the icon cache. - * Useful for testing or if icons change at runtime. - */ + /** Clears the icon cache; used in tests. */ public static void clearCache() { imageCache.clear(); } diff --git a/open-robotics/src/main/java/com/openrobotics/util/ScreenNavigator.java b/open-robotics/src/main/java/com/openrobotics/util/ScreenNavigator.java index 41783cd7..8a5c7cec 100644 --- a/open-robotics/src/main/java/com/openrobotics/util/ScreenNavigator.java +++ b/open-robotics/src/main/java/com/openrobotics/util/ScreenNavigator.java @@ -14,13 +14,7 @@ import java.util.ResourceBundle; import java.util.function.Consumer; -/** - * Utility class that centralises all screen/dialog transitions. - * - *

Every screen is loaded from its FXML resource. The primary {@link Stage} - * is held as a static reference so any controller can call - * {@code ScreenNavigator.loadScreen(...)} without passing the stage around. - */ +/** Centralises all screen and dialog transitions; holds the primary stage so controllers do not need to pass it around. */ public final class ScreenNavigator { // FXML resource paths (relative to the resources root) @@ -35,19 +29,13 @@ public final class ScreenNavigator { public static final String DIALOG_EXIT_CONFIRM = "/com/openrobotics/fxml/ExitConfirmDialog.fxml"; public static final String DIALOG_EXPORT_RESULTS = "/com/openrobotics/fxml/ExportResultsDialog.fxml"; - /** The application's primary stage – set once in {@link com.openrobotics.MainApp}. */ + // set once in MainApp.start() private static Stage primaryStage; - - /** Holds the controller of the currently displayed screen so it can be cleaned up on navigation. */ + // used to call cleanup() before replacing the screen private static Object currentController; private ScreenNavigator() {} - // ------------------------------------------------------------------ // - // Initialisation - // ------------------------------------------------------------------ // - - /** Called once from {@link com.openrobotics.MainApp#start(Stage)}. */ public static void setPrimaryStage(Stage stage) { primaryStage = stage; } @@ -60,18 +48,13 @@ public static Object getCurrentController() { return currentController; } - // ------------------------------------------------------------------ // - // Full-screen navigation - // ------------------------------------------------------------------ // - /** - * Loads and displays the given FXML as the primary scene (full screen). + * Loads and displays the given FXML as the primary scene. * - * @param fxmlPath one of the path constants defined in this class + * @param fxmlPath one of the path constants in this class */ public static void loadScreen(String fxmlPath) { try { - // Clean up the previous screen's controller before replacing it if (currentController instanceof Cleanable c) { c.cleanup(); } @@ -100,23 +83,22 @@ public static void loadScreen(String fxmlPath) { } } - // ------------------------------------------------------------------ // - // Modal dialogs - // ------------------------------------------------------------------ // - /** * Opens the given FXML as a blocking modal dialog. * - * @param fxmlPath one of the {@code DIALOG_*} constants - * @return the {@link FXMLLoader} after the dialog is closed - * (lets callers retrieve the controller for result data) + * @param fxmlPath the path to the dialog FXML + * @return the loader after the dialog closes, so callers can retrieve controller data */ public static FXMLLoader openDialog(String fxmlPath) { return openDialog(fxmlPath, "", null); } /** - * Opens a modal dialog with a custom title. + * Opens the given FXML as a blocking modal dialog with the given title. + * + * @param fxmlPath the path to the dialog FXML + * @param title the window title to display + * @return the loader after the dialog closes, so callers can retrieve controller data */ public static FXMLLoader openDialog(String fxmlPath, String title) { return openDialog(fxmlPath, title, null); @@ -124,6 +106,11 @@ public static FXMLLoader openDialog(String fxmlPath, String title) { /** * Opens a modal dialog with a custom title and optional controller initializer. + * + * @param fxmlPath the path to the dialog FXML + * @param title the window title; ignored if blank + * @param controllerInitializer optional callback to configure the controller before the dialog opens + * @return the loader after the dialog closes, so callers can retrieve controller data */ public static FXMLLoader openDialog(String fxmlPath, String title, Consumer controllerInitializer) { try { @@ -163,20 +150,16 @@ public static FXMLLoader openDialog(String fxmlPath, String title, Consumer deck = new ArrayList<>(); private static int deckIndex = 0; private static final Object lock = new Object(); @@ -67,6 +67,8 @@ public class ViewportTips { /** * Returns the next tip in shuffled sequence. * Reshuffles the deck once all tips have been shown. + * + * @return the next tip string */ public static String nextTip() { synchronized (lock) { @@ -86,13 +88,19 @@ public static String nextTip() { /** * Returns a single random tip without advancing the deck. * Use for one-off display (e.g. on first load). + * + * @return a randomly selected tip string */ public static String getRandomSelectionTip() { if (selectionTips.isEmpty()) return "Left-click to select"; return selectionTips.get(ThreadLocalRandom.current().nextInt(selectionTips.size())); } - /** Adds a custom tip to the pool. */ + /** + * Adds a custom tip to the pool. + * + * @param tip the tip text to add; ignored if {@code null} or blank + */ public static void addTip(String tip) { if (tip != null && !tip.isBlank()) { synchronized (lock) { @@ -103,7 +111,11 @@ public static void addTip(String tip) { } } - /** Replaces all tips with a custom set. */ + /** + * Replaces all tips with a custom set. + * + * @param tips the new tip list; {@code null} entries and blank strings are ignored + */ public static void setTips(List tips) { List filtered = new ArrayList<>(); if (tips != null) { @@ -121,7 +133,11 @@ public static void setTips(List tips) { } } - /** Returns an unmodifiable view of all registered tips. */ + /** + * Returns an unmodifiable view of all registered tips. + * + * @return unmodifiable list of all registered tip strings + */ public static List getAllTips() { return Collections.unmodifiableList(selectionTips); } diff --git a/open-robotics/src/main/resources/com/openrobotics/css/base/buttons.css b/open-robotics/src/main/resources/com/openrobotics/css/base/buttons.css index c0d8dcb3..ff1e9ce7 100644 --- a/open-robotics/src/main/resources/com/openrobotics/css/base/buttons.css +++ b/open-robotics/src/main/resources/com/openrobotics/css/base/buttons.css @@ -19,17 +19,17 @@ -fx-background-color: #4D4B45; } -/* EDITOR tab button */ +/* Active tab button — padding must match .tab-btn exactly to prevent layout shift */ .button.tab-btn-active { - -fx-background-color: #8D8A7F; + -fx-background-color: #B8B4A6; -fx-text-fill: #2A2926; -fx-font-size: 11px; -fx-font-weight: bold; - -fx-padding: 6 20 7 20; - -fx-background-radius: 6 6 0 0; - -fx-border-color: #8D8A7F #8D8A7F transparent #8D8A7F; - -fx-border-width: 1 1 0 1; - -fx-border-radius: 6 6 0 0; + -fx-padding: 5 16 5 16; + -fx-background-radius: 4; + -fx-border-color: #5D5B54; + -fx-border-width: 1; + -fx-border-radius: 4; -fx-cursor: hand; } @@ -151,6 +151,18 @@ -fx-cursor: hand; } +.zoom-btn { + -fx-background-color: #C2BEAE; + -fx-text-fill: #1a1a18; + -fx-font-size: 14px; + -fx-padding: 3 8 3 8; + -fx-background-radius: 3px; + -fx-border-color: #8D8A7F; + -fx-border-width: 1px; + -fx-border-radius: 3px; + -fx-cursor: hand; +} + .playback-btn:hover { -fx-background-color: #8D8A7F; -fx-text-fill: #C2BEAE; diff --git a/open-robotics/src/main/resources/com/openrobotics/css/base/layout.css b/open-robotics/src/main/resources/com/openrobotics/css/base/layout.css index b5a907bf..68f25f34 100644 --- a/open-robotics/src/main/resources/com/openrobotics/css/base/layout.css +++ b/open-robotics/src/main/resources/com/openrobotics/css/base/layout.css @@ -40,12 +40,16 @@ -fx-text-fill: #2A2926; -fx-font-size: 11px; -fx-font-weight: bold; - -fx-padding: 6 16 5 16; - -fx-background-radius: 4 4 0 0; + -fx-padding: 5 16 5 16; + -fx-background-radius: 4; -fx-cursor: hand; - -fx-border-color: #2A2926; + -fx-border-color: #5D5B54; -fx-border-width: 1; - -fx-border-radius: 4 4 0 0; + -fx-border-radius: 4; +} + +.tab-btn-active:hover { + -fx-background-color: #D8D5CC; } .tab-btn { @@ -53,10 +57,12 @@ -fx-text-fill: #C2BEAE; -fx-font-size: 11px; -fx-font-weight: bold; - -fx-padding: 4 14 4 14; - -fx-background-radius: 0; + -fx-padding: 5 16 5 16; + -fx-background-radius: 4; -fx-cursor: hand; - -fx-border-color: transparent; + -fx-border-color: #5D5B54; + -fx-border-width: 1; + -fx-border-radius: 4; } .tab-btn:hover { diff --git a/open-robotics/src/main/resources/com/openrobotics/fxml/ResultsScreen.fxml b/open-robotics/src/main/resources/com/openrobotics/fxml/ResultsScreen.fxml index 1b3e6ab3..fc1619c1 100644 --- a/open-robotics/src/main/resources/com/openrobotics/fxml/ResultsScreen.fxml +++ b/open-robotics/src/main/resources/com/openrobotics/fxml/ResultsScreen.fxml @@ -54,8 +54,9 @@ + - + + - - - - - - - -