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 @@
+
-
+
diff --git a/open-robotics/src/main/resources/com/openrobotics/fxml/SetupScreen.fxml b/open-robotics/src/main/resources/com/openrobotics/fxml/SetupScreen.fxml
index 74f99e80..2dc9579e 100644
--- a/open-robotics/src/main/resources/com/openrobotics/fxml/SetupScreen.fxml
+++ b/open-robotics/src/main/resources/com/openrobotics/fxml/SetupScreen.fxml
@@ -1,45 +1,40 @@
-
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
-
+
@@ -48,137 +43,121 @@
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
@@ -186,12 +165,12 @@
-
-
-
-
+
+
+
+
-
+
@@ -201,20 +180,18 @@
-
-
-
+
-
-
-
-
+
+
+
+
-
+
@@ -224,13 +201,12 @@
-
-
-
-
-
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/open-robotics/src/main/resources/com/openrobotics/fxml/SimulationScreen.fxml b/open-robotics/src/main/resources/com/openrobotics/fxml/SimulationScreen.fxml
index 4ce8ee5e..0212f9f1 100644
--- a/open-robotics/src/main/resources/com/openrobotics/fxml/SimulationScreen.fxml
+++ b/open-robotics/src/main/resources/com/openrobotics/fxml/SimulationScreen.fxml
@@ -1,37 +1,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -50,62 +22,56 @@
-
+
-
-
+
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -119,106 +85,95 @@
-
+
-
-
-
+
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
@@ -234,9 +189,8 @@
-
-
+
+
@@ -246,7 +200,7 @@
-
+
@@ -254,13 +208,13 @@
-
+
-
+
@@ -272,113 +226,101 @@
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
+
@@ -386,23 +328,20 @@
-
+
-
-
-
-
-
+
+
+
+
+
-
+
@@ -410,11 +349,10 @@
-
+
-
+
diff --git a/open-robotics/src/test/java/com/openrobotics/AppStateTest.java b/open-robotics/src/test/java/com/openrobotics/AppStateTest.java
index 8556e189..01d75127 100644
--- a/open-robotics/src/test/java/com/openrobotics/AppStateTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/AppStateTest.java
@@ -14,16 +14,29 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Unit tests for global {@link AppState} storage semantics.
+ *
+ * This suite verifies clear/reset behavior, canvas-dimension clamping rules, and consistency of
+ * engine/config-path presence flags with assigned values.
+ */
public class AppStateTest {
+ /**
+ * Restores shared static app state after each test to avoid cross-test leakage.
+ */
@AfterEach
void resetSharedState() {
AppState.clear();
AppState.setCanvasDimensions(30, 30);
}
+ /**
+ * Verifies {@link AppState#clear()} removes engine/config/output paths while preserving previously set
+ * canvas dimensions.
+ */
@Test
- void clear_resets_engine_and_config_path_but_keeps_canvas_dimensions() {
+ void clear_resets_engine_and_paths_but_keeps_canvas_dimensions() {
SimulationEngine engine = new SimulationEngine(
new Map(1, 1),
new Robot[0],
@@ -33,18 +46,33 @@ void clear_resets_engine_and_config_path_but_keeps_canvas_dimensions() {
AppState.setEngine(engine);
AppState.setConfigPath("sample.json");
+ AppState.setEditorBaselinePath("baseline.json");
+ AppState.setSimulationConsoleText("console");
+ AppState.setSimulationLogText("log");
+ AppState.setSimulationLogCursor(42);
AppState.setCanvasDimensions(12, 8);
AppState.clear();
assertNull(AppState.getEngine());
assertNull(AppState.getConfigPath());
+ assertNull(AppState.getEditorBaselinePath());
+ assertNull(AppState.getSimulationConsoleText());
+ assertNull(AppState.getSimulationLogText());
+ assertEquals(0, AppState.getSimulationLogCursor());
assertFalse(AppState.hasEngine());
assertFalse(AppState.hasConfigPath());
+ assertFalse(AppState.hasEditorBaselinePath());
+ assertFalse(AppState.hasSimulationConsoleText());
+ assertFalse(AppState.hasSimulationLogText());
assertEquals(12, AppState.getCanvasWidthTiles());
assertEquals(8, AppState.getCanvasHeightTiles());
}
+ /**
+ * Verifies canvas dimensions are clamped to minimum 1 and aggregate tile accessor returns the
+ * larger dimension.
+ */
@Test
void setCanvasDimensions_clamps_values_and_getCanvasTiles_returns_max_dimension() {
AppState.setCanvasDimensions(-5, 0);
@@ -60,8 +88,11 @@ void setCanvasDimensions_clamps_values_and_getCanvasTiles_returns_max_dimension(
assertEquals(14, AppState.getCanvasTiles());
}
+ /**
+ * Verifies engine/config assignment updates both direct getters and presence flags.
+ */
@Test
- void engine_and_config_path_flags_follow_assigned_values() {
+ void engine_and_path_flags_follow_assigned_values() {
SimulationEngine engine = new SimulationEngine(
new Map(1, 1),
new Robot[0],
@@ -71,10 +102,21 @@ void engine_and_config_path_flags_follow_assigned_values() {
AppState.setEngine(engine);
AppState.setConfigPath("config.json");
+ AppState.setEditorBaselinePath("baseline.json");
+ AppState.setSimulationConsoleText("console");
+ AppState.setSimulationLogText("log");
+ AppState.setSimulationLogCursor(7);
assertSame(engine, AppState.getEngine());
assertEquals("config.json", AppState.getConfigPath());
+ assertEquals("baseline.json", AppState.getEditorBaselinePath());
+ assertEquals("console", AppState.getSimulationConsoleText());
+ assertEquals("log", AppState.getSimulationLogText());
+ assertEquals(7, AppState.getSimulationLogCursor());
assertTrue(AppState.hasEngine());
assertTrue(AppState.hasConfigPath());
+ assertTrue(AppState.hasEditorBaselinePath());
+ assertTrue(AppState.hasSimulationConsoleText());
+ assertTrue(AppState.hasSimulationLogText());
}
}
diff --git a/open-robotics/src/test/java/com/openrobotics/MainAppTest.java b/open-robotics/src/test/java/com/openrobotics/MainAppTest.java
index 63a9ee49..12f07f09 100644
--- a/open-robotics/src/test/java/com/openrobotics/MainAppTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/MainAppTest.java
@@ -12,10 +12,22 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX startup test for {@link MainApp}.
+ *
+ * This suite verifies application bootstrap wiring for the primary stage, including title,
+ * minimum/initial dimensions, and initial welcome-screen loading.
+ */
public class MainAppTest extends ApplicationTest {
+ /** Primary stage provided by TestFX and initialized by {@link MainApp#start(Stage)}. */
private Stage primaryStage;
+ /**
+ * Launches the real application entrypoint on the TestFX stage.
+ *
+ * @param stage JavaFX stage supplied by TestFX
+ */
@Override
public void start(Stage stage) {
primaryStage = stage;
@@ -26,6 +38,9 @@ public void start(Stage stage) {
}
}
+ /**
+ * Verifies application start configures stage metadata and loads the welcome root node.
+ */
@Test
void start_configures_primary_stage_and_loads_welcome_screen() {
WaitForAsyncUtils.waitForFxEvents();
diff --git a/open-robotics/src/test/java/com/openrobotics/common/LoggerTest.java b/open-robotics/src/test/java/com/openrobotics/common/LoggerTest.java
deleted file mode 100644
index 14d52310..00000000
--- a/open-robotics/src/test/java/com/openrobotics/common/LoggerTest.java
+++ /dev/null
@@ -1,176 +0,0 @@
-package com.openrobotics.common;
-
-import com.openrobotics.map.Vector2D;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * Unit tests for {@link Logger}.
- *
- * Verifies that the event list starts empty, that log entries are appended
- * in order, that each entry contains the expected field values, and that
- * {@code getEvents()} returns the live list (not a snapshot).
- */
-//public class LoggerTest {
-//
-// private Logger logger;
-//
-// /**
-// * Creates a fresh Logger before every test.
-// */
-// @BeforeEach
-// public void setUp() {
-// logger = new Logger();
-// }
-//
-// /**
-// * A new Logger should have no events.
-// */
-// @Test
-// public void testInitiallyEmpty() {
-// assertTrue(logger.getEvents().isEmpty(), "New Logger must have no events");
-// }
-//
-// /**
-// * After one logEvent() call the list must contain exactly one entry.
-// */
-// @Test
-// public void testSingleEventAdded() {
-// logger.logEvent(1, 0, "MOVE", new Vector2D(3, 4), "moved right");
-// assertEquals(1, logger.getEvents().size());
-// }
-//
-// /**
-// * The logged entry must contain the tick number.
-// */
-// @Test
-// public void testEntryContainsTick() {
-// logger.logEvent(7, 0, "MOVE", new Vector2D(0, 0), "");
-// assertTrue(logger.getEvents().get(0).contains("7"),
-// "Log entry must contain the tick value");
-// }
-//
-// /**
-// * The logged entry must contain the robot id.
-// */
-// @Test
-// public void testEntryContainsRobotId() {
-// logger.logEvent(1, 42, "TASK", new Vector2D(0, 0), "");
-// assertTrue(logger.getEvents().get(0).contains("42"),
-// "Log entry must contain the robot id");
-// }
-//
-// /**
-// * The logged entry must contain the event type.
-// */
-// @Test
-// public void testEntryContainsEventType() {
-// logger.logEvent(1, 0, "CHARGING", new Vector2D(0, 0), "");
-// assertTrue(logger.getEvents().get(0).contains("CHARGING"),
-// "Log entry must contain the event type");
-// }
-//
-// /**
-// * The logged entry must include the position string.
-// */
-// @Test
-// public void testEntryContainsPosition() {
-// logger.logEvent(1, 0, "IDLE", new Vector2D(5, 9), "details");
-// String entry = logger.getEvents().get(0);
-// assertTrue(entry.contains("5") && entry.contains("9"),
-// "Log entry must contain the position coordinates");
-// }
-//
-// /**
-// * The logged entry must include the details string.
-// */
-// @Test
-// public void testEntryContainsDetails() {
-// logger.logEvent(1, 0, "EVENT", new Vector2D(0, 0), "task_completed");
-// assertTrue(logger.getEvents().get(0).contains("task_completed"),
-// "Log entry must contain the details string");
-// }
-//
-// /**
-// * The log entry format should match the current key=value layout exactly.
-// */
-// @Test
-// public void testExactEntryFormat() {
-// logger.logEvent(12, 7, "MOVE", new Vector2D(3, 4), "ok");
-//
-// assertEquals("tick=12 robot=7 event=MOVE pos=(3, 4) details=ok", logger.getEvents().get(0));
-// }
-//
-// /**
-// * Multiple logEvent() calls should result in the correct number of entries.
-// */
-// @Test
-// public void testMultipleEventsCount() {
-// logger.logEvent(1, 0, "A", new Vector2D(0, 0), "");
-// logger.logEvent(2, 1, "B", new Vector2D(1, 1), "");
-// logger.logEvent(3, 2, "C", new Vector2D(2, 2), "");
-// assertEquals(3, logger.getEvents().size());
-// }
-//
-// /**
-// * Events must be stored in the order they were logged.
-// */
-// @Test
-// public void testEventOrder() {
-// logger.logEvent(1, 0, "FIRST", new Vector2D(0, 0), "");
-// logger.logEvent(2, 0, "SECOND", new Vector2D(0, 0), "");
-// List events = logger.getEvents();
-// assertTrue(events.get(0).contains("FIRST"), "First logged event must be at index 0");
-// assertTrue(events.get(1).contains("SECOND"), "Second logged event must be at index 1");
-// }
-//
-// /**
-// * After obtaining a reference via getEvents(), a subsequent logEvent() must
-// * be visible through the same reference.
-// */
-// @Test
-// public void testGetEventsIsLiveList() {
-// List events = logger.getEvents();
-// logger.logEvent(5, 3, "LIVE", new Vector2D(0, 0), "");
-// assertEquals(1, events.size(),
-// "getEvents() should return the live list, not a snapshot");
-// }
-//
-// /**
-// * Mutating the returned list should affect the logger because it exposes the backing list.
-// */
-// @Test
-// public void testGetEventsReturnsMutableBackingList() {
-// List events = logger.getEvents();
-//
-// events.add("manually-added");
-//
-// assertEquals(1, logger.getEvents().size());
-// assertEquals("manually-added", logger.getEvents().get(0));
-// }
-//
-// /**
-// * Null field values are currently accepted and rendered as the string "null".
-// */
-// @Test
-// public void testNullFieldsAreRenderedAsNullStrings() {
-// logger.logEvent(1, 2, null, null, null);
-//
-// assertEquals("tick=1 robot=2 event=null pos=null details=null", logger.getEvents().get(0));
-// }
-//
-// /**
-// * Negative numeric values are stored verbatim in the log entry.
-// */
-// @Test
-// public void testNegativeTickAndRobotIdAreStoredVerbatim() {
-// logger.logEvent(-5, -9, "EVENT", new Vector2D(0, 0), "details");
-//
-// assertTrue(logger.getEvents().get(0).contains("tick=-5"));
-// assertTrue(logger.getEvents().get(0).contains("robot=-9"));
-// }
-//}
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/ExitConfirmControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/ExitConfirmControllerTest.java
index c060c276..19ed98eb 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/ExitConfirmControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/ExitConfirmControllerTest.java
@@ -13,11 +13,17 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Unit tests for {@link ExitConfirmController}.
+ */
public class ExitConfirmControllerTest extends ApplicationTest {
private ExitConfirmController controller;
private Stage dialogStage;
+ /**
+ * Start the application and set the dialog stage.
+ */
@Override
public void start(Stage stage) throws Exception {
dialogStage = stage;
@@ -31,6 +37,9 @@ public void start(Stage stage) throws Exception {
stage.show();
}
+ /**
+ * Test that the yes button confirms and closes the dialog.
+ */
@Test
void yes_confirms_and_closes_dialog() {
clickOn(lookup((Button b) -> "QUIT".equals(b.getText())).queryAs(Button.class));
@@ -40,6 +49,9 @@ void yes_confirms_and_closes_dialog() {
assertFalse(dialogStage.isShowing());
}
+ /**
+ * Test that the no button keeps the unconfirmed state and closes the dialog.
+ */
@Test
void no_keeps_unconfirmed_and_closes_dialog() {
clickOn(lookup((Button b) -> "RETURN".equals(b.getText())).queryAs(Button.class));
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/ExportResultsControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/ExportResultsControllerTest.java
index 40a17635..4af97cf7 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/ExportResultsControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/ExportResultsControllerTest.java
@@ -50,11 +50,21 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * UI-focused tests for {@link ExportResultsController}.
+ *
+ * This suite verifies dialog initialization, validation paths, export success and failure states,
+ * DTO construction behavior, and preference persistence for recent export directories. The tests run
+ * with TestFX and use an in-memory {@link PreferencesFactory} to keep state isolated.
+ */
class ExportResultsControllerTest extends ApplicationTest {
private static final String PREFS_KEY = "recentExportDirs";
private static final int MAX_RECENT = 8;
+ /**
+ * Forces JVM preferences calls to use the in-memory implementation for deterministic tests.
+ */
static {
System.setProperty("java.util.prefs.PreferencesFactory",
InMemoryPreferencesFactory.class.getName());
@@ -66,6 +76,9 @@ class ExportResultsControllerTest extends ApplicationTest {
private ExportResultsController controller;
private Stage dialogStage;
+ /**
+ * Boots the dialog under test on the JavaFX stage.
+ */
@Override
public void start(Stage stage) throws Exception {
dialogStage = stage;
@@ -74,6 +87,9 @@ public void start(Stage stage) throws Exception {
loadDialog();
}
+ /**
+ * Resets global state and closes the dialog after each test.
+ */
@AfterEach
void tearDown() throws Exception {
AppState.clear();
@@ -350,6 +366,9 @@ void cancel_closes_dialog_without_exporting() throws Exception {
);
}
+ /**
+ * Loads the export dialog FXML and wires the controller to the current stage.
+ */
private void loadDialog() throws IOException {
FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(
getClass().getResource("/com/openrobotics/fxml/ExportResultsDialog.fxml")));
@@ -360,6 +379,9 @@ private void loadDialog() throws IOException {
dialogStage.show();
}
+ /**
+ * Reloads the dialog on the FX thread so tests can re-evaluate initialization behavior.
+ */
private void reloadDialog() {
interact(() -> {
try {
@@ -371,6 +393,9 @@ private void reloadDialog() {
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Selects the provided directory in the combo box, adding it first if needed.
+ */
private void chooseDirectory(Path directory) {
String path = directory.toString();
interact(() -> {
@@ -383,15 +408,30 @@ private void chooseDirectory(Path directory) {
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Updates the export file name field on the FX thread.
+ */
private void setFileName(String fileName) {
interact(() -> fileNameField().setText(fileName));
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Invokes a no-argument private controller method via reflection.
+ */
private Object invokePrivate(String methodName) throws Exception {
return invokePrivate(methodName, new Class>[0]);
}
+ /**
+ * Invokes a private controller method via reflection, ensuring execution on the FX thread.
+ *
+ * @param methodName the controller method name
+ * @param parameterTypes reflected parameter types
+ * @param args invocation arguments
+ * @return method result, or {@code null} for void methods
+ * @throws Exception when the invoked method throws an exception
+ */
private Object invokePrivate(String methodName, Class>[] parameterTypes, Object... args) throws Exception {
Method method = ExportResultsController.class.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
@@ -427,19 +467,31 @@ private Object invokePrivate(String methodName, Class>[] parameterTypes, Objec
return result[0];
}
+ /**
+ * Returns the file name text field from the dialog scene graph.
+ */
private TextField fileNameField() {
return lookup("#fileNameField").queryAs(TextField.class);
}
+ /**
+ * Returns the recent-directory combo box from the dialog scene graph.
+ */
@SuppressWarnings("unchecked")
private ComboBox directoryCombo() {
return lookup("#directoryCombo").queryAs(ComboBox.class);
}
+ /**
+ * Returns the label used by the controller to display status and selected directory text.
+ */
private Label selectedDirLabel() {
return lookup("#selectedDirLabel").queryAs(Label.class);
}
+ /**
+ * Clears stored recent directory entries used by the dialog.
+ */
private void clearPrefs() throws Exception {
Preferences prefs = prefs();
for (int i = 0; i < MAX_RECENT; i++) {
@@ -448,6 +500,9 @@ private void clearPrefs() throws Exception {
prefs.flush();
}
+ /**
+ * Seeds recent directory preference entries in the order provided.
+ */
private void writeRecentDirs(String... dirs) throws Exception {
clearPrefs();
Preferences prefs = prefs();
@@ -457,10 +512,16 @@ private void writeRecentDirs(String... dirs) throws Exception {
prefs.flush();
}
+ /**
+ * Returns the preferences node used by {@link ExportResultsController}.
+ */
private Preferences prefs() {
return Preferences.userNodeForPackage(ExportResultsController.class);
}
+ /**
+ * Creates a dispatcher pre-populated with pending tasks for export metric tests.
+ */
private Dispatcher dispatcherWithPendingTasks(int count, int firstId) {
Dispatcher dispatcher = new Dispatcher();
for (int i = 0; i < count; i++) {
@@ -469,10 +530,16 @@ private Dispatcher dispatcherWithPendingTasks(int count, int firstId) {
return dispatcher;
}
+ /**
+ * Creates a simulation engine test double that reports supplied metrics and collaborators.
+ */
private SimulationEngine reportingEngine(int ticks, Robot[] robots, Dispatcher dispatcher) {
return new ReportingEngine(ticks, robots, dispatcher);
}
+ /**
+ * SimulationEngine test double exposing fixed tick count, robots, and dispatcher.
+ */
private static class ReportingEngine extends SimulationEngine {
private final int ticks;
private final Robot[] robots;
@@ -501,6 +568,9 @@ public Dispatcher getDispatcher() {
}
}
+ /**
+ * Dispatcher test double with fixed queued tasks and total task count.
+ */
private static class ReportingDispatcher extends Dispatcher {
private final List tasks;
private final int totalTasksAdded;
@@ -521,6 +591,9 @@ public int getTotalTasksAdded() {
}
}
+ /**
+ * Robot test double returning deterministic reporting fields for export serialization.
+ */
private static class ReportingRobot extends Robot {
private final NavigationStrategy nav;
private final int tasksCompleted;
@@ -586,6 +659,9 @@ public RobotState getState() {
}
}
+ /**
+ * Marker navigation strategy used to verify algorithm name rendering in exported JSON.
+ */
private static class AuditNavigationStrategy implements NavigationStrategy {
@Override
public MoveIntention getNextMove(Robot robot, Map map) {
@@ -593,21 +669,29 @@ public MoveIntention getNextMove(Robot robot, Map map) {
}
}
+ /**
+ * Preferences factory backed by in-memory preference roots for isolated tests.
+ */
public static class InMemoryPreferencesFactory implements PreferencesFactory {
private static final MemoryPreferences USER_ROOT = new MemoryPreferences(null, "");
private static final MemoryPreferences SYSTEM_ROOT = new MemoryPreferences(null, "");
+ // Returns the user root preferences.
@Override
public Preferences userRoot() {
return USER_ROOT;
}
+ // Returns the system root preferences.
@Override
public Preferences systemRoot() {
return SYSTEM_ROOT;
}
}
+ /**
+ * Minimal in-memory {@link Preferences} implementation for test-only persistence behavior.
+ */
private static class MemoryPreferences extends AbstractPreferences {
private final java.util.Map values = new ConcurrentHashMap<>();
private final java.util.Map children = new ConcurrentHashMap<>();
@@ -616,47 +700,56 @@ private static class MemoryPreferences extends AbstractPreferences {
super(parent, name);
}
+ // Puts a value into the preferences.
@Override
protected void putSpi(String key, String value) {
values.put(key, value);
}
+ // Gets a value from the preferences.
@Override
protected String getSpi(String key) {
return values.get(key);
}
+ // Removes a value from the preferences.
@Override
protected void removeSpi(String key) {
values.remove(key);
}
+ // Removes a node from the preferences.
@Override
protected void removeNodeSpi() throws BackingStoreException {
values.clear();
children.clear();
}
+ // Returns the keys from the preferences.
@Override
protected String[] keysSpi() throws BackingStoreException {
return values.keySet().toArray(String[]::new);
}
+ // Returns the children names from the preferences.
@Override
protected String[] childrenNamesSpi() throws BackingStoreException {
return children.keySet().toArray(String[]::new);
}
+ // Returns the child preferences.
@Override
protected AbstractPreferences childSpi(String name) {
return children.computeIfAbsent(name, childName -> new MemoryPreferences(this, childName));
}
+ // Syncs the preferences.
@Override
protected void syncSpi() throws BackingStoreException {
// In-memory test preferences are always current.
}
+ // Flushes the preferences.
@Override
protected void flushSpi() throws BackingStoreException {
// Nothing to flush for the in-memory implementation.
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/LoadConfigControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/LoadConfigControllerTest.java
index 71420542..1ec1d842 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/LoadConfigControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/LoadConfigControllerTest.java
@@ -23,11 +23,20 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * UI tests for {@link LoadConfigController}.
+ *
+ * This suite verifies combo-box selection behavior, reset/cancel actions, and successful load
+ * flow using the JavaFX dialog defined in {@code LoadConfigDialog.fxml}.
+ */
public class LoadConfigControllerTest extends ApplicationTest {
private LoadConfigController controller;
private Stage dialogStage;
+ /**
+ * Clears persisted recent configuration entries used by the load dialog.
+ */
private void clearPrefs() throws Exception {
Preferences prefs = Preferences.userNodeForPackage(LoadConfigController.class);
for (int i = 0; i < 8; i++) {
@@ -36,6 +45,9 @@ private void clearPrefs() throws Exception {
prefs.flush();
}
+ /**
+ * Loads the load-config dialog on the JavaFX stage before each test.
+ */
@Override
public void start(Stage stage) throws Exception {
clearPrefs();
@@ -56,6 +68,9 @@ public void start(Stage stage) throws Exception {
}
}
+ /**
+ * Test that selecting a combo value updates the selected file and label.
+ */
@Test
void selecting_combo_value_updates_selected_file_and_label() {
String path = new File("sample-config.json").getAbsolutePath();
@@ -71,6 +86,9 @@ void selecting_combo_value_updates_selected_file_and_label() {
assertEquals(path, controller.getSelectedFile().getAbsolutePath());
}
+ /**
+ * Test that resetting to default clears the selection.
+ */
@Test
void reset_to_default_clears_selection() {
String path = new File("sample-config.json").getAbsolutePath();
@@ -89,6 +107,9 @@ void reset_to_default_clears_selection() {
lookup("#selectedPathLabel").queryAs(Label.class).getText());
}
+ /**
+ * Test that canceling clears the selection and closes the dialog.
+ */
@Test
void cancel_clears_selection_and_closes_dialog() {
clickOn(lookup((Button b) -> "CANCEL".equals(b.getText())).queryAs(Button.class));
@@ -98,6 +119,9 @@ void cancel_clears_selection_and_closes_dialog() {
assertFalse(dialogStage.isShowing());
}
+ /**
+ * Test that loading closes the dialog when a file is selected.
+ */
@Test
void load_closes_dialog_when_file_selected() {
String path = new File("sample-config.json").getAbsolutePath();
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/ObjectDescControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/ObjectDescControllerTest.java
index 6b4333e7..393f45f5 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/ObjectDescControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/ObjectDescControllerTest.java
@@ -15,6 +15,16 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX controller tests for {@link ObjectDescController}.
+ *
+ * This suite validates that the controller maps known object types to the expected
+ * UI copy (title, subtype, descriptive text, and properties list), falls back safely for unknown
+ * object types, and correctly drives dialog outcomes for add/close actions.
+ *
+ *
The controller under test is instantiated directly and its FXML-injected fields are provided
+ * via reflection to keep tests focused on controller behavior rather than FXML loading concerns.
+ */
public class ObjectDescControllerTest extends ApplicationTest {
private ObjectDescController controller;
@@ -24,6 +34,15 @@ public class ObjectDescControllerTest extends ApplicationTest {
private Label objectDescLabel;
private VBox propsOverview;
+ /**
+ * Initializes a minimal JavaFX stage and wires a fresh controller instance.
+ *
+ *
This method simulates FXML injection by placing test labels/containers into the
+ * controller's private fields, assigns the dialog stage, and shows the stage so dialog-closing
+ * behavior can be asserted in action tests.
+ *
+ * @param stage the JavaFX stage provided by TestFX
+ */
@Override
public void start(Stage stage) {
dialogStage = stage;
@@ -44,6 +63,12 @@ public void start(Stage stage) {
stage.show();
}
+ /**
+ * Verifies that selecting {@code ROBOT} populates all robot-specific UI content.
+ *
+ *
Asserts title/subtype labels, descriptive body text, and that the expected
+ * number of robot property rows are rendered.
+ */
@Test
void set_object_type_populates_robot_content() {
interact(() -> controller.setObjectType("ROBOT"));
@@ -55,6 +80,12 @@ void set_object_type_populates_robot_content() {
assertEquals(3, propsOverview.getChildren().size());
}
+ /**
+ * Verifies unknown object-type handling uses the generic fallback presentation.
+ *
+ *
The controller should preserve the raw object type value while clearing subtype text,
+ * showing a default "no description" message, and leaving the properties container empty.
+ */
@Test
void unknown_object_type_falls_back_to_generic_description() {
interact(() -> controller.setObjectType("MYSTERY"));
@@ -67,6 +98,12 @@ void unknown_object_type_falls_back_to_generic_description() {
assertTrue(propsOverview.getChildren().isEmpty());
}
+ /**
+ * Verifies that selecting {@code INTERSECTION} populates intersection-specific UI content.
+ *
+ *
Asserts expected labels and description fragment, and validates that the properties
+ * overview contains exactly the rows defined for this object type.
+ */
@Test
void set_object_type_populates_intersection_content() {
interact(() -> controller.setObjectType("INTERSECTION"));
@@ -78,6 +115,12 @@ void set_object_type_populates_intersection_content() {
assertEquals(2, propsOverview.getChildren().size());
}
+ /**
+ * Verifies that invoking the add action sets the add-requested flag and closes the dialog.
+ *
+ *
This confirms state mutation ({@link ObjectDescController#isAddRequested()}) and
+ * user-visible stage behavior occur together when the add handler executes.
+ */
@Test
void add_sets_flag_and_closes_dialog() {
interact(() -> controller.setObjectType("WALL"));
@@ -88,6 +131,12 @@ void add_sets_flag_and_closes_dialog() {
assertFalse(dialogStage.isShowing());
}
+ /**
+ * Verifies that invoking the close action does not request add and closes the dialog.
+ *
+ *
This guards against accidental add-state mutation when a user dismisses
+ * the dialog without confirming.
+ */
@Test
void close_leaves_add_flag_false() {
interact(() -> controller.setObjectType("WALL"));
@@ -98,6 +147,13 @@ void close_leaves_add_flag_false() {
assertFalse(dialogStage.isShowing());
}
+ /**
+ * Injects a value into a private controller field to emulate FXML wiring.
+ *
+ * @param fieldName private field name declared in {@link ObjectDescController}
+ * @param value object instance to assign to that field
+ * @throws RuntimeException if reflection access or assignment fails
+ */
private void inject(String fieldName, Object value) {
try {
Field field = ObjectDescController.class.getDeclaredField(fieldName);
@@ -108,6 +164,15 @@ private void inject(String fieldName, Object value) {
}
}
+ /**
+ * Invokes a no-arg private controller method on the JavaFX thread.
+ *
+ *
This is used for action handlers (for example, {@code onAdd} and {@code onClose})
+ * that are intentionally non-public but still need direct behavioral verification.
+ *
+ * @param methodName private method name to invoke
+ * @throws RuntimeException if reflection lookup/invocation fails
+ */
private void invokePrivate(String methodName) {
interact(() -> {
try {
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/ResultsControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/ResultsControllerTest.java
index 709c3bcd..69a5ee08 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/ResultsControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/ResultsControllerTest.java
@@ -27,8 +27,26 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX integration-style tests for the Results screen/controller flow.
+ *
+ *
This suite seeds {@link AppState} with a small deterministic simulation, opens
+ * {@code ResultsScreen.fxml}, and verifies that table/stat labels, charts, and navigation actions
+ * reflect the seeded engine state.
+ */
public class ResultsControllerTest extends ApplicationTest {
+ /**
+ * Seeds global {@link AppState} with a minimal but complete simulation model used by all tests.
+ *
+ * The seeded state includes:
+ *
+ * an 8x8 map,
+ * one robot with a concrete navigation strategy and sensor,
+ * one queued task in the dispatcher, and
+ * a known config path for run-name assertions.
+ *
+ */
private void seedAppState() {
Map map = new Map(8, 8);
Robot robot = new Robot("R1", new Vector2D(1, 1));
@@ -45,6 +63,12 @@ private void seedAppState() {
AppState.setConfigPath("results-test.json");
}
+ /**
+ * Loads the results screen onto the JavaFX stage after seeding application state.
+ *
+ * @param stage the stage provided by TestFX
+ * @throws Exception if FXML loading fails
+ */
@Override
public void start(Stage stage) throws Exception {
seedAppState();
@@ -56,6 +80,12 @@ public void start(Stage stage) throws Exception {
stage.show();
}
+ /**
+ * Verifies that initial table and summary labels are populated from the seeded engine.
+ *
+ * Confirms robot stats row count, tick label, run-name source, and top-level task metrics
+ * shown in both summary and table info labels.
+ */
@Test
void results_screen_populates_table_and_summary_from_engine() {
WaitForAsyncUtils.waitForFxEvents();
@@ -68,6 +98,12 @@ void results_screen_populates_table_and_summary_from_engine() {
assertTrue(lookup("#tableInfoLabel").queryAs(Label.class).getText().contains("1"));
}
+ /**
+ * Verifies that both chart containers are populated when at least one robot exists.
+ *
+ * Asserts non-empty chart containers and validates that the rendered chart nodes are
+ * {@link BarChart} instances.
+ */
@Test
void results_screen_creates_charts_when_robots_exist() {
WaitForAsyncUtils.waitForFxEvents();
@@ -81,6 +117,12 @@ void results_screen_creates_charts_when_robots_exist() {
assertTrue(chart2.getChildren().get(0) instanceof BarChart);
}
+ /**
+ * Verifies that clicking {@code NEW SIMULATION} navigates back to setup.
+ *
+ * Navigation success is asserted using presence of a setup-screen control
+ * ({@code #mapCombo}).
+ */
@Test
void new_simulation_button_navigates_to_setup_screen() {
clickOn("NEW SIMULATION");
@@ -89,6 +131,11 @@ void new_simulation_button_navigates_to_setup_screen() {
assertTrue(lookup("#mapCombo").tryQuery().isPresent());
}
+ /**
+ * Verifies that clicking {@code RETURN TO EDITOR} navigates to the simulation/editor screen.
+ *
+ * Navigation success is asserted by checking the editor tick label content.
+ */
@Test
void return_to_editor_button_navigates_to_simulation_screen() {
clickOn("RETURN TO EDITOR");
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/SaveConfigControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/SaveConfigControllerTest.java
index 8b524d62..2686785f 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/SaveConfigControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/SaveConfigControllerTest.java
@@ -23,11 +23,20 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX UI tests for {@link SaveConfigController}.
+ *
+ * This suite validates default initialization, directory-reset behavior, save-form validation
+ * errors, successful save confirmation flow, and cancel semantics for the save-config dialog.
+ */
public class SaveConfigControllerTest extends ApplicationTest {
private SaveConfigController controller;
private Stage dialogStage;
+ /**
+ * Clears persisted recent save-directory entries to keep tests deterministic.
+ */
private void clearPrefs() throws Exception {
Preferences prefs = Preferences.userNodeForPackage(SaveConfigController.class);
for (int i = 0; i < 8; i++) {
@@ -36,6 +45,12 @@ private void clearPrefs() throws Exception {
prefs.flush();
}
+ /**
+ * Loads the save-config dialog FXML and attaches it to the TestFX stage.
+ *
+ * @param stage the JavaFX stage provided by TestFX
+ * @throws Exception if preferences reset or FXML loading fails
+ */
@Override
public void start(Stage stage) throws Exception {
clearPrefs();
@@ -50,12 +65,19 @@ public void start(Stage stage) throws Exception {
stage.show();
}
+ /**
+ * Verifies the filename input is pre-populated with a JSON-suffixed default value.
+ */
@Test
void initialize_sets_default_filename() {
TextField fileNameField = lookup("#fileNameField").queryAs(TextField.class);
- assertTrue(fileNameField.getText().endsWith(".json"));
+ assertTrue(fileNameField.getText().startsWith("experiment_"));
+ assertFalse(fileNameField.getText().endsWith(".json"));
}
+ /**
+ * Verifies reset-directory action selects the default OpenRobotics directory.
+ */
@Test
void reset_directory_sets_default_directory() {
clickOn("↺");
@@ -64,6 +86,9 @@ void reset_directory_sets_default_directory() {
assertTrue(lookup("#selectedDirLabel").queryAs(Label.class).getText().contains(".open-robotics"));
}
+ /**
+ * Verifies save action is rejected when no directory is selected.
+ */
@Test
void save_without_directory_shows_error() {
clickOn(lookup((Button b) -> "SAVE".equals(b.getText())).queryAs(Button.class));
@@ -73,6 +98,11 @@ void save_without_directory_shows_error() {
lookup("#selectedDirLabel").queryAs(Label.class).getText());
}
+ /**
+ * Verifies invalid filename characters are rejected during save validation.
+ *
+ * @throws Exception if temporary directory setup/cleanup fails
+ */
@Test
void save_with_invalid_filename_shows_error() throws Exception {
Path dir = Files.createTempDirectory("save-config-test");
@@ -96,6 +126,9 @@ void save_with_invalid_filename_shows_error() throws Exception {
}
}
+ /**
+ * Verifies save validation rejects directories that do not exist.
+ */
@Test
void save_with_nonexistent_directory_shows_error() {
File missing = new File(System.getProperty("java.io.tmpdir"), "definitely-missing-openrobotics-save-dir");
@@ -113,6 +146,11 @@ void save_with_nonexistent_directory_shows_error() {
lookup("#selectedDirLabel").queryAs(Label.class).getText());
}
+ /**
+ * Verifies save validation rejects a selected path when it is a file, not a directory.
+ *
+ * @throws Exception if temporary file setup/cleanup fails
+ */
@Test
void save_with_file_path_instead_of_directory_shows_error() throws Exception {
Path file = Files.createTempFile("save-config-test", ".json");
@@ -134,6 +172,11 @@ void save_with_file_path_instead_of_directory_shows_error() throws Exception {
}
}
+ /**
+ * Verifies successful save validation stores selected values and closes the dialog.
+ *
+ * @throws Exception if temporary directory setup/cleanup fails
+ */
@Test
void successful_save_validation_closes_dialog_and_preserves_selection() throws Exception {
Path dir = Files.createTempDirectory("save-config-test");
@@ -161,6 +204,9 @@ void successful_save_validation_closes_dialog_and_preserves_selection() throws E
}
}
+ /**
+ * Verifies cancel clears selected output directory and closes the dialog.
+ */
@Test
void cancel_clears_selected_directory_and_closes_dialog() {
clickOn(lookup((Button b) -> "CANCEL".equals(b.getText())).queryAs(Button.class));
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/SetupControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/SetupControllerTest.java
index 6916cbb0..a9e320ea 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/SetupControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/SetupControllerTest.java
@@ -61,6 +61,14 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX controller tests for {@link SetupController}.
+ *
+ * This suite validates setup-screen defaults, field-reset handlers, validation logic, map/task
+ * builders, config load/persist behavior, start-simulation flow, and navigation actions. Tests
+ * primarily invoke private controller logic via reflection to assert internal behavior while
+ * running on a real JavaFX scene.
+ */
public class SetupControllerTest extends ApplicationTest {
@TempDir
@@ -69,6 +77,12 @@ public class SetupControllerTest extends ApplicationTest {
private Stage stage;
private SetupController controller;
+ /**
+ * Loads the setup screen into the TestFX stage.
+ *
+ * @param stage JavaFX stage supplied by TestFX
+ * @throws Exception if FXML loading fails
+ */
@Override
public void start(Stage stage) throws Exception {
this.stage = stage;
@@ -76,6 +90,9 @@ public void start(Stage stage) throws Exception {
loadSetupScene();
}
+ /**
+ * Resets shared app state and reloads a clean setup scene before each test.
+ */
@BeforeEach
void resetSetupScene() {
AppState.clear();
@@ -83,12 +100,18 @@ void resetSetupScene() {
loadSetupSceneOnFx();
}
+ /**
+ * Clears global state after each test to avoid cross-test coupling.
+ */
@AfterEach
void clearSharedState() {
AppState.clear();
AppState.setCanvasDimensions(30, 30);
}
+ /**
+ * Verifies initial control defaults, option lists, and visibility flags on first render.
+ */
@Test
void initialize_sets_all_visible_defaults_and_available_options() {
assertAll(
@@ -110,6 +133,9 @@ void initialize_sets_all_visible_defaults_and_available_options() {
);
}
+ /**
+ * Verifies random-map selection reveals seed controls and random-seed reset clears input.
+ */
@Test
void random_map_selection_shows_seed_row_and_reset_clears_seed() {
TextField seedField = textField("randomSeedField");
@@ -129,6 +155,9 @@ void random_map_selection_shows_seed_row_and_reset_clears_seed() {
assertEquals("", seedField.getText());
}
+ /**
+ * Verifies built-in map presets auto-size canvas controls and warning visibility toggles.
+ */
@Test
void builtin_map_selection_auto_sizes_canvas_and_warns_at_minimum() {
interact(() -> mapCombo().setValue("baseline_small"));
@@ -150,6 +179,9 @@ void builtin_map_selection_auto_sizes_canvas_and_warns_at_minimum() {
assertFalse(field("canvasWarnLabel", Label.class).isVisible());
}
+ /**
+ * Verifies reservation-K spinner enablement is tied strictly to RESERVATION_K policy.
+ */
@Test
void reservation_spinner_enables_only_for_reservation_k_policy() {
interact(() -> policyCombo().setValue("RESERVATION_K"));
@@ -165,6 +197,10 @@ void reservation_spinner_enables_only_for_reservation_k_policy() {
assertTrue(reservationKSpinner().isDisabled());
}
+ /**
+ * Verifies all reset handlers restore default values for map, workload, canvas, and robot
+ * physics fields.
+ */
@Test
void reset_handlers_restore_defaults_for_visible_and_optional_fields() {
installRobotPhysicsFields("150", "35", "9", "3");
@@ -213,6 +249,9 @@ void reset_handlers_restore_defaults_for_visible_and_optional_fields() {
);
}
+ /**
+ * Verifies validation requires map selection only when no engine is already loaded.
+ */
@Test
void validate_rejects_missing_map_only_when_no_engine_is_loaded() {
interact(() -> mapCombo().getSelectionModel().clearSelection());
@@ -227,25 +266,49 @@ void validate_rejects_missing_map_only_when_no_engine_is_loaded() {
assertEquals("", statusLabel().getText());
}
+ /**
+ * Verifies max-ticks validation rejects invalid/non-positive values and accepts valid input.
+ */
@Test
void validate_rejects_bad_max_tick_values_and_accepts_positive_integer() {
+ // Select a map so the validation doesn't fail on the Map check early
+ interact(() -> {
+ ComboBox maps = lookup("#mapCombo").queryComboBox();
+ maps.getSelectionModel().selectFirst();
+ });
+
TextField maxTicks = textField("maxTicksField");
- interact(() -> maxTicks.setText("abc"));
+ // Test non-numeric characters
+ interact(() -> {
+ maxTicks.clear();
+ maxTicks.setText("abc");
+ });
+ assertEquals("", maxTicks.getText());
assertFalse((boolean) invokePrivate("validate", new Class>[0]));
- assertEquals("⚠ Max ticks must be a positive integer.", statusLabel().getText());
+ assertEquals("\u26a0 Max Ticks requires a valid number.", statusLabel().getText());
+ // Test zero
interact(() -> maxTicks.setText("0"));
assertFalse((boolean) invokePrivate("validate", new Class>[0]));
+ assertEquals("\u26a0 Max Ticks must be between 1 and 1000000.", statusLabel().getText());
- interact(() -> maxTicks.setText("-5"));
+ // Test negative
+ interact(() -> {
+ maxTicks.clear();
+ maxTicks.setText("-5");
+ });
assertFalse((boolean) invokePrivate("validate", new Class>[0]));
- interact(() -> maxTicks.setText("1"));
+ // Test valid positive integer
+ interact(() -> maxTicks.setText("30000"));
assertTrue((boolean) invokePrivate("validate", new Class>[0]));
assertEquals("", statusLabel().getText());
}
+ /**
+ * Verifies coordination policy builder maps combo selection to expected implementations.
+ */
@Test
void buildCoordinationPolicy_returns_expected_policy_implementations() {
assertSame(CoordinationPolicy.noOp(), invokePrivate("buildCoordinationPolicy", new Class>[0]));
@@ -264,6 +327,9 @@ void buildCoordinationPolicy_returns_expected_policy_implementations() {
assertSame(CoordinationPolicy.noOp(), invokePrivate("buildCoordinationPolicy", new Class>[0]));
}
+ /**
+ * Verifies parse helpers trim values and return fallback defaults on malformed input.
+ */
@Test
void parse_helpers_trim_values_and_return_fallbacks_for_bad_input() {
assertEquals(12, invokePrivate("parseIntSafe", new Class>[]{String.class, int.class}, " 12 ", 3));
@@ -274,6 +340,9 @@ void parse_helpers_trim_values_and_return_fallbacks_for_bad_input() {
assertEquals(9.0f, (float) invokePrivate("parseOptionalFloatField", new Class>[]{TextField.class, float.class}, new TextField("9"), 2.0f), 0.001f);
}
+ /**
+ * Verifies built-in map builders return expected dimensions, entities, and computed bounds.
+ */
@Test
void builtin_map_builders_return_expected_shapes_and_bounds() {
assertNull(invokePrivate("buildBuiltinMap", new Class>[]{String.class}, "empty"));
@@ -300,6 +369,9 @@ void builtin_map_builders_return_expected_shapes_and_bounds() {
assertEquals(List.of(0, 0, 15, 9), List.of(bounds[0], bounds[1], bounds[2], bounds[3]));
}
+ /**
+ * Verifies embedding a built-in map into a larger canvas preserves entity types and placement.
+ */
@Test
void builtin_map_in_canvas_preserves_entity_types_and_offsets_into_canvas() {
Map empty = (Map) invokePrivate("buildBuiltinMapInCanvas", new Class>[]{String.class, int.class, int.class}, "empty", 8, 6);
@@ -316,6 +388,10 @@ void builtin_map_in_canvas_preserves_entity_types_and_offsets_into_canvas() {
assertTrue(countEntities(baseline, Obstacle.class) > 0);
}
+ /**
+ * Verifies random-map generation is deterministic for a fixed seed and keeps core entities in
+ * expected regions.
+ */
@Test
void random_map_builder_is_seeded_and_keeps_core_entities_in_expected_regions() {
Map first = (Map) invokePrivate("buildRandomMap", new Class>[]{int.class, int.class, long.class}, 16, 10, 123L);
@@ -332,6 +408,10 @@ void random_map_builder_is_seeded_and_keeps_core_entities_in_expected_regions()
assertEquals(15, onlyEntity(first, DeliveryStation.class).getPosition().getX());
}
+ /**
+ * Verifies spawn-tile finder selects nearest traversable/unoccupied tile and falls back to
+ * center when all tiles are occupied.
+ */
@Test
void findSpawnTile_returns_nearest_available_traversable_tile_or_center_fallback() {
Map map = new Map(3, 3);
@@ -367,6 +447,9 @@ void findSpawnTile_returns_nearest_available_traversable_tile_or_center_fallback
assertEquals(new Vector2D(1, 1), fallback);
}
+ /**
+ * Verifies fixed task generation uses racks and delivery stations when both are available.
+ */
@Test
void generateFixedTasks_uses_racks_and_delivery_stations_when_available() {
Map map = new Map(4, 3);
@@ -383,6 +466,10 @@ void generateFixedTasks_uses_racks_and_delivery_stations_when_available() {
}
}
+ /**
+ * Verifies fixed-task generation falls back to automatic generation when rack pickups are
+ * unavailable.
+ */
@Test
void generateFixedTasks_falls_back_to_automatic_task_generation() {
Map map = new Map(3, 3);
@@ -396,6 +483,9 @@ void generateFixedTasks_falls_back_to_automatic_task_generation() {
assertTrue(dispatcher.getAllQueuedTasks().stream().noneMatch(task -> task.getPickupLocation().equals(new Vector2D(2, 2))));
}
+ /**
+ * Verifies fixed-task generation skips floor pickups when racks are completely unreachable.
+ */
@Test
void generateFixedTasks_does_not_create_floor_pickups_for_unreachable_racks() {
Map map = new Map(3, 3);
@@ -412,6 +502,9 @@ void generateFixedTasks_does_not_create_floor_pickups_for_unreachable_racks() {
assertEquals(0, dispatcher.getPendingTaskCount());
}
+ /**
+ * Verifies engine builder consumes field values and propagates robot config/custom policy.
+ */
@Test
void buildEngineFromSetup_uses_field_values_defaults_and_robot_config() {
installRobotPhysicsFields("150.5", "35.5", "9.5", "3.5");
@@ -433,13 +526,16 @@ void buildEngineFromSetup_uses_field_values_defaults_and_robot_config() {
assertEquals(77, engine.getMaxTicks());
assertEquals(123L, engine.getSeed());
assertTrue(engine.getCoordinationPolicy().contains(ReservationKPolicy.class.getName()));
- assertEquals(4, engine.getDispatcher().getTotalTasksAdded());
+ assertEquals(0, engine.getDispatcher().getTotalTasksAdded());
assertEquals(150.5f, config.batteryCapacity, 0.001f);
assertEquals(35.5f, config.lowBatteryThreshold, 0.001f);
assertEquals(9.5f, config.chargePerTick, 0.001f);
assertEquals(3.5f, config.energyPerMove, 0.001f);
}
+ /**
+ * Verifies engine builder falls back to defaults for invalid numeric fields and blank run name.
+ */
@Test
void buildEngineFromSetup_falls_back_for_invalid_numeric_fields_and_blank_run_name() {
installRobotPhysicsFields("bad", "bad", "bad", "bad");
@@ -465,6 +561,9 @@ void buildEngineFromSetup_falls_back_for_invalid_numeric_fields_and_blank_run_na
assertEquals(1.0f, config.energyPerMove, 0.001f);
}
+ /**
+ * Verifies random-map engine construction honors explicit map seed for deterministic topology.
+ */
@Test
void buildEngineFromSetup_random_map_honors_explicit_map_seed() {
interact(() -> {
@@ -481,11 +580,14 @@ void buildEngineFromSetup_random_map_honors_explicit_map_seed() {
assertEquals(entitySignature(first.getMap()), entitySignature(second.getMap()));
assertEquals(111L, first.getSeed());
- assertEquals(3, first.getDispatcher().getTotalTasksAdded());
+ assertEquals(0, first.getDispatcher().getTotalTasksAdded());
assertEquals(1, countEntities(first.getMap(), ChargingStation.class));
assertEquals(1, countEntities(first.getMap(), DeliveryStation.class));
}
+ /**
+ * Verifies engine builder returns null and reports status when map selection is missing.
+ */
@Test
void buildEngineFromSetup_returns_null_and_status_when_map_selection_is_missing() {
interact(() -> mapCombo().getSelectionModel().clearSelection());
@@ -494,6 +596,11 @@ void buildEngineFromSetup_returns_null_and_status_when_map_selection_is_missing(
assertEquals("⚠ Please select a map.", statusLabel().getText());
}
+ /**
+ * Verifies config loader rejects null, missing, and malformed files with user-facing status.
+ *
+ * @throws Exception if temp-file setup fails
+ */
@Test
void loadConfigFile_rejects_null_missing_and_malformed_files() throws Exception {
assertFalse((boolean) invokePrivate("loadConfigFile", new Class>[]{File.class}, (Object) null));
@@ -510,6 +617,11 @@ void loadConfigFile_rejects_null_missing_and_malformed_files() throws Exception
assertFalse(AppState.hasEngine());
}
+ /**
+ * Verifies config loader accepts a valid saved config and updates {@link AppState}.
+ *
+ * @throws Exception if config save helper fails
+ */
@Test
void loadConfigFile_accepts_valid_saved_config_and_updates_app_state() throws Exception {
File config = saveEngineConfig(tempDir.resolve("valid-config.json"), emptyEngine());
@@ -523,6 +635,12 @@ void loadConfigFile_accepts_valid_saved_config_and_updates_app_state() throws Ex
assertEquals(3, AppState.getEngine().getMap().getHeight());
}
+ /**
+ * Verifies selecting a config file from the list loads it, clears map selection, and disables
+ * manual canvas-size controls.
+ *
+ * @throws Exception if file setup fails
+ */
@Test
void selecting_config_list_item_loads_config_clears_map_selection_and_disables_canvas_size() throws Exception {
File configDir = new File("configs");
@@ -545,6 +663,11 @@ void selecting_config_list_item_loads_config_clears_map_selection_and_disables_c
}
}
+ /**
+ * Verifies refresh-file-list includes only JSON configs and ignores non-JSON files.
+ *
+ * @throws Exception if temporary file operations fail
+ */
@Test
void refresh_file_list_includes_json_files_and_ignores_non_json_files() throws Exception {
File configDir = new File("configs");
@@ -566,6 +689,9 @@ void refresh_file_list_includes_json_files_and_ignores_non_json_files() throws E
}
}
+ /**
+ * Verifies temporary config persistence stores restart path and reports success status.
+ */
@Test
void persistEngineToTempFile_stores_restart_config_path_and_reports_success() {
SimulationEngine engine = emptyEngine();
@@ -577,6 +703,9 @@ void persistEngineToTempFile_stores_restart_config_path_and_reports_success() {
assertTrue(statusLabel().getText().contains("✔ Config persisted for restart"));
}
+ /**
+ * Verifies persistence failure reports error and clears stale restart config path.
+ */
@Test
void persistEngineToTempFile_reports_error_and_clears_path_when_save_fails() {
AppState.setConfigPath("existing.json");
@@ -587,6 +716,10 @@ void persistEngineToTempFile_reports_error_and_clears_path_when_save_fails() {
assertTrue(statusLabel().getText().contains("⚠ Could not persist temp config for restart: cannot save"));
}
+ /**
+ * Verifies start-simulation builds engine from setup fields, persists config, and navigates to
+ * simulation screen.
+ */
@Test
void startSimulation_builds_engine_from_setup_values_persists_config_and_navigates() {
interact(() -> {
@@ -599,11 +732,15 @@ void startSimulation_builds_engine_from_setup_values_persists_config_and_navigat
assertTrue(AppState.hasEngine());
assertTrue(AppState.hasConfigPath());
- assertEquals(2, AppState.getEngine().getDispatcher().getTotalTasksAdded());
+ assertEquals(0, AppState.getEngine().getDispatcher().getTotalTasksAdded());
assertEquals(200, AppState.getEngine().getMaxTicks());
assertNotNull(lookup("#warehouseCanvas").queryAs(Canvas.class));
}
+ /**
+ * Verifies start-simulation rebuilds UI-driven engine when an engine exists but no config path
+ * is available.
+ */
@Test
void startSimulation_rebuilds_existing_ui_built_engine_when_no_config_path_exists() {
AppState.setEngine(emptyEngine());
@@ -617,10 +754,15 @@ void startSimulation_rebuilds_existing_ui_built_engine_when_no_config_path_exist
assertTrue(AppState.hasEngine());
assertTrue(AppState.hasConfigPath());
assertEquals(18, AppState.getEngine().getMap().getWidth());
- assertEquals(1, AppState.getEngine().getDispatcher().getTotalTasksAdded());
+ assertEquals(0, AppState.getEngine().getDispatcher().getTotalTasksAdded());
assertNotNull(lookup("#warehouseCanvas").queryAs(Canvas.class));
}
+ /**
+ * Verifies start-simulation surfaces reload errors when saved config path contains bad JSON.
+ *
+ * @throws Exception if malformed file setup fails
+ */
@Test
void startSimulation_reports_config_reload_failure_when_saved_path_is_bad() throws Exception {
Path malformed = tempDir.resolve("bad-start.json");
@@ -634,6 +776,9 @@ void startSimulation_reports_config_reload_failure_when_saved_path_is_bad() thro
assertNotNull(lookup("#mapCombo").queryAs(ComboBox.class));
}
+ /**
+ * Verifies preview-blocked handler reports message and consumes incoming mouse event.
+ */
@Test
void preview_blocked_sets_message_and_consumes_event() {
MouseEvent event = mouseClick();
@@ -644,6 +789,9 @@ void preview_blocked_sets_message_and_consumes_event() {
assertTrue(event.isConsumed());
}
+ /**
+ * Verifies welcome menu action navigates back to the welcome screen.
+ */
@Test
void menu_welcome_navigates_to_welcome_screen() {
invokeOnFx("onMenuWelcome", new Class>[0]);
@@ -651,6 +799,9 @@ void menu_welcome_navigates_to_welcome_screen() {
assertNotNull(lookup("#rootPane").query());
}
+ /**
+ * Reloads setup scene on FX thread for deterministic per-test controller state.
+ */
private void loadSetupSceneOnFx() {
interact(() -> {
try {
@@ -662,6 +813,11 @@ private void loadSetupSceneOnFx() {
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Loads setup FXML and binds controller to the shared test stage.
+ *
+ * @throws Exception if FXML loading fails
+ */
private void loadSetupScene() throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/openrobotics/fxml/SetupScreen.fxml"));
Parent root = loader.load();
@@ -670,6 +826,9 @@ private void loadSetupScene() throws Exception {
stage.show();
}
+ /**
+ * Injects robot-physics text fields used by build/reset logic.
+ */
private void installRobotPhysicsFields(String battery, String lowBattery, String charge, String energy) {
interact(() -> {
setField("batteryCapacityField", new TextField(battery));
@@ -680,19 +839,34 @@ private void installRobotPhysicsFields(String battery, String lowBattery, String
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Persists a simulation engine to a config file path for load/reload tests.
+ *
+ * @return saved config file
+ * @throws IOException if serialization fails
+ */
private File saveEngineConfig(Path path, SimulationEngine engine) throws IOException {
engine.configSaving(path.toAbsolutePath().toString());
return path.toFile();
}
+ /**
+ * Creates a minimal empty engine used as a baseline for state and persistence tests.
+ */
private SimulationEngine emptyEngine() {
return new SimulationEngine(new Map(3, 3), new Robot[0], new Dispatcher(), CoordinationPolicy.noOp());
}
+ /**
+ * Counts entities of a specific type present in the map.
+ */
private long countEntities(Map map, Class extends MapEntity> type) {
return map.getEntities().stream().filter(type::isInstance).count();
}
+ /**
+ * Returns the single entity of the requested type and asserts uniqueness.
+ */
private T onlyEntity(Map map, Class type) {
List matches = map.getEntities().stream()
.filter(type::isInstance)
@@ -702,6 +876,9 @@ private T onlyEntity(Map map, Class type) {
return matches.get(0);
}
+ /**
+ * Produces a deterministic string signature for map entities to compare map topology.
+ */
private String entitySignature(Map map) {
return map.getEntities().stream()
.map(entity -> entity.getClass().getSimpleName()
@@ -711,6 +888,9 @@ private String entitySignature(Map map) {
.collect(Collectors.joining("|"));
}
+ /**
+ * Creates a primary-button click event used for preview-blocking handler tests.
+ */
private MouseEvent mouseClick() {
return new MouseEvent(
MouseEvent.MOUSE_CLICKED,
@@ -734,41 +914,68 @@ private MouseEvent mouseClick() {
);
}
+ /**
+ * Returns the setup map-selection combo box.
+ */
@SuppressWarnings("unchecked")
private ComboBox mapCombo() {
return (ComboBox) field("mapCombo", ComboBox.class);
}
+ /**
+ * Returns the coordination-policy combo box.
+ */
@SuppressWarnings("unchecked")
private ComboBox policyCombo() {
return (ComboBox) field("policyCombo", ComboBox.class);
}
+ /**
+ * Returns the reservation-K spinner.
+ */
private Spinner reservationKSpinner() {
return integerSpinner("reservationKSpinner");
}
+ /**
+ * Returns the canvas-width spinner.
+ */
private Spinner canvasWidthSpinner() {
return integerSpinner("canvasWidthSpinner");
}
+ /**
+ * Returns the canvas-height spinner.
+ */
private Spinner canvasHeightSpinner() {
return integerSpinner("canvasHeightSpinner");
}
+ /**
+ * Returns a spinner field by name, cast as integer spinner.
+ */
@SuppressWarnings("unchecked")
private Spinner integerSpinner(String fieldName) {
return (Spinner) field(fieldName, Spinner.class);
}
+ /**
+ * Returns a reflected text field by controller field name.
+ */
private TextField textField(String fieldName) {
return field(fieldName, TextField.class);
}
+ /**
+ * Returns setup status label used for user-facing validation/load messages.
+ */
private Label statusLabel() {
return field("statusLabel", Label.class);
}
+ /**
+ * Invokes a private controller method on FX thread and returns the result.
+ */
private Object invokeOnFx(String name, Class>[] parameterTypes, Object... args) {
AtomicReference result = new AtomicReference<>();
interact(() -> result.set(invokePrivate(name, parameterTypes, args)));
@@ -776,6 +983,9 @@ private Object invokeOnFx(String name, Class>[] parameterTypes, Object... args
return result.get();
}
+ /**
+ * Invokes a private controller method via reflection, marshalling to FX thread when needed.
+ */
private Object invokePrivate(String name, Class>[] parameterTypes, Object... args) {
if (!Platform.isFxApplicationThread()) {
AtomicReference result = new AtomicReference<>();
@@ -798,6 +1008,9 @@ private Object invokePrivate(String name, Class>[] parameterTypes, Object... a
}
}
+ /**
+ * Reads a private controller field and casts it to expected type.
+ */
private T field(String name, Class type) {
try {
Field field = SetupController.class.getDeclaredField(name);
@@ -808,6 +1021,9 @@ private T field(String name, Class type) {
}
}
+ /**
+ * Sets a private controller field by reflection.
+ */
private void setField(String name, Object value) {
try {
Field field = SetupController.class.getDeclaredField(name);
@@ -818,6 +1034,9 @@ private void setField(String name, Object value) {
}
}
+ /**
+ * SimulationEngine test double that always fails when saving config.
+ */
private static class UnsavableEngine extends SimulationEngine {
UnsavableEngine() {
super(new Map(2, 2), new Robot[0], new Dispatcher(), CoordinationPolicy.noOp());
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/SimulationControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/SimulationControllerTest.java
index ee422019..b2f12d5b 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/SimulationControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/SimulationControllerTest.java
@@ -19,13 +19,7 @@
import com.openrobotics.task.Task;
import com.openrobotics.util.ScreenNavigator;
import javafx.event.Event;
-import com.openrobotics.map.Map;
-import com.openrobotics.robot.Robot;
-import com.openrobotics.simulationcore.CoordinationPolicy;
-import com.openrobotics.simulationcore.Dispatcher;
import com.openrobotics.simulationcore.ReservationKPolicy;
-import com.openrobotics.util.ScreenNavigator;
-import com.openrobotics.simulationcore.SimulationEngine;
import com.openrobotics.simulationcore.TrafficRulesPolicy;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
@@ -75,6 +69,15 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX integration-style tests for {@link SimulationController}.
+ *
+ * This suite exercises simulation-screen behavior across loading, editing, playback controls,
+ * outliner/property synchronization, configuration persistence/reload, screen navigation, and
+ * policy-dependent UI visibility. Tests intentionally interact with real FXML scene wiring while
+ * using reflection helpers for private controller paths that are difficult to trigger only through
+ * public UI events.
+ */
public class SimulationControllerTest extends ApplicationTest {
@TempDir
@@ -83,10 +86,16 @@ public class SimulationControllerTest extends ApplicationTest {
private Stage stage;
private SimulationController controller;
+ /**
+ * Creates a minimal engine using the supplied coordination policy.
+ */
private SimulationEngine buildEngine(CoordinationPolicy policy) {
return new SimulationEngine(new Map(6, 6), new Robot[]{}, new Dispatcher(), policy);
}
+ /**
+ * Reloads simulation screen from a specific engine through {@link AppState}.
+ */
private void reloadSimulationWithEngine(SimulationEngine engine) {
interact(() -> {
AppState.clear();
@@ -97,6 +106,12 @@ private void reloadSimulationWithEngine(SimulationEngine engine) {
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Initializes the simulation scene on the TestFX stage.
+ *
+ * @param stage JavaFX stage from TestFX
+ * @throws Exception if FXML loading fails
+ */
@Override
public void start(Stage stage) throws Exception {
this.stage = stage;
@@ -107,6 +122,11 @@ public void start(Stage stage) throws Exception {
loadSceneFromCurrentAppState();
}
+ /**
+ * Resets UI/controller runtime state before each test.
+ *
+ * @throws TimeoutException if console clear does not complete in time
+ */
@BeforeEach
void resetState() throws TimeoutException {
Logger.setMode(LoggerMode.NO_OP);
@@ -115,6 +135,9 @@ void resetState() throws TimeoutException {
assertConsoleEventuallyEmpty();
}
+ /**
+ * Cleans controller resources and shared app/logging state after each test.
+ */
@AfterEach
void cleanUpController() {
if (controller != null) {
@@ -126,6 +149,7 @@ void cleanUpController() {
Logger.setMode(LoggerMode.DB);
}
+ /** Verifies safe empty-state labels/progress when no engine is loaded. */
@Test
void initial_screen_without_engine_shows_safe_empty_state() {
assertEquals("TICK 0", tickLabel().getText());
@@ -139,6 +163,7 @@ void initial_screen_without_engine_shows_safe_empty_state() {
assertTrue(consoleText().isEmpty(), "BeforeEach should leave console empty for each test");
}
+ /** Verifies malformed config reload reports failure without crashing the screen. */
@Test
void loading_invalid_config_path_reports_failure_without_crashing() throws Exception {
Path malformed = tempDir.resolve("bad-config.json");
@@ -151,6 +176,7 @@ void loading_invalid_config_path_reports_failure_without_crashing() throws Excep
assertNull(AppState.getEngine());
}
+ /** Verifies loading a populated engine refreshes canvas, outliner, stats, and console logs. */
@Test
void loading_engine_populates_canvas_outliner_stats_and_console() {
EngineFixture fixture = loadDiverseEngine();
@@ -159,7 +185,7 @@ void loading_engine_populates_canvas_outliner_stats_and_console() {
assertAll(
() -> assertSame(fixture.engine, AppState.getEngine()),
- () -> assertEquals("Canvas Size: 6×5 Tiles", field("canvasSizeLabel", Label.class).getText()),
+ () -> assertEquals("Canvas Size: 5×5 Tiles", field("canvasSizeLabel", Label.class).getText()),
() -> assertEquals("Loaded 5 objects", field("viewportStatusLabel", Label.class).getText()),
() -> assertEquals("Objects: 6", field("objsLabel", Label.class).getText()),
() -> assertEquals(6, outliner.getItems().size()),
@@ -171,6 +197,7 @@ void loading_engine_populates_canvas_outliner_stats_and_console() {
);
}
+ /** Verifies outliner filtering updates visible rows and derived object count label. */
@Test
void outliner_filter_updates_entities_tasks_and_object_count() {
loadDiverseEngine();
@@ -195,6 +222,7 @@ void outliner_filter_updates_entities_tasks_and_object_count() {
assertEquals("Objects: 6", field("objsLabel", Label.class).getText());
}
+ /** Verifies selecting an outliner entity populates properties panel with entity details. */
@Test
void selecting_outliner_entity_populates_properties_panel() {
EngineFixture fixture = loadDiverseEngine();
@@ -212,6 +240,7 @@ void selecting_outliner_entity_populates_properties_panel() {
assertTrue(panelText.contains("Battery:"));
}
+ /** Verifies renaming through properties panel updates model and outliner text. */
@Test
void property_name_edit_updates_entity_outliner_and_viewport() {
EngineFixture fixture = loadDiverseEngine();
@@ -228,6 +257,7 @@ void property_name_edit_updates_entity_outliner_and_viewport() {
assertTrue(outliner().getItems().stream().anyMatch(item -> item.contains("renamed_rack")));
}
+ /** Verifies entity factory type mapping and placement rules for common tile occupancy cases. */
@Test
void factory_creates_expected_entity_types_and_occupancy_rules() {
loadDiverseEngine();
@@ -257,6 +287,7 @@ void factory_creates_expected_entity_types_and_occupancy_rules() {
);
}
+ /** Verifies copy/paste/delete operations mutate engine entities and emit console feedback. */
@Test
void copy_paste_and_delete_selected_entity_update_engine_and_console() {
EngineFixture fixture = loadDiverseEngine();
@@ -281,6 +312,7 @@ void copy_paste_and_delete_selected_entity_update_engine_and_console() {
assertEquals(initialCount, fixture.engine.getMap().getEntities().size());
}
+ /** Verifies task pickup/dropoff coordinates are synchronized when referenced entities move. */
@Test
void sync_task_positions_updates_pickup_and_dropoff_references() {
EngineFixture fixture = loadDiverseEngine();
@@ -304,19 +336,21 @@ void sync_task_positions_updates_pickup_and_dropoff_references() {
assertTrue(outliner().getItems().stream().anyMatch(item -> item.contains("(2, 1) → (4, 1)")));
}
+ /** Verifies single-step frame fails to advance simulation tick/progress/status and logs. */
@Test
- void next_frame_with_engine_advances_tick_status_progress_and_console() {
+ void next_frame_with_engine_does_not_advance_tick_status_progress_and_console() {
loadScreenWith(emptyEngine(), null, 30, 30);
Label simStatus = installOptionalStatusLabel();
invokeOnFx("onNextFrame", new Class>[0]);
- assertEquals("STEPPING", simStatus.getText());
- assertEquals("TICK 1", tickLabel().getText());
- assertEquals(0.001, progressBar().getProgress(), 0.0001);
- assertTrue(consoleText().contains("Step → TICK 1"));
+ assertEquals("FAILURE", simStatus.getText());
+ assertEquals("TICK 0", tickLabel().getText());
+ assertEquals(0.0, progressBar().getProgress(), 0.0001);
+ assertTrue(consoleText().contains("Step → TICK 0"));
}
+ /** Verifies completion path updates status/progress without incrementing tick counter. */
@Test
void completing_tick_updates_complete_state_without_incrementing_tick() {
loadScreenWith(new CompletingEngine(), null, 30, 30);
@@ -332,41 +366,22 @@ void completing_tick_updates_complete_state_without_incrementing_tick() {
assertTrue(consoleText().contains("Step → TICK 0"));
}
+ /** Verifies play/pause/replay/stop flow updates status, button styles, and timeline logs. */
@Test
void play_pause_resume_and_stop_flow_updates_labels_buttons_and_logs() {
loadScreenWith(emptyEngine(), null, 30, 30);
Label simStatus = installOptionalStatusLabel();
fireButton("playBtn");
+ WaitForAsyncUtils.waitForFxEvents();
- // Add a bit of delay
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- assertEquals("RUNNING", simStatus.getText());
- assertTrue(consoleText().contains("Simulation started."));
+ assertEquals("FAILURE", simStatus.getText());
+ assertTrue(consoleText().contains("\u26a0 No tasks could be generated."));
+ assertTrue(consoleText().contains("Simulation failed: The map configuration is invalid. Simulation cannot run."));
+ assertTrue(consoleText().contains("Simulation failed at TICK 0."));
assertNotNull(field("initialSnapshotPath", String.class));
- assertTrue(field("playBtn", Button.class).getStyle().contains("#2E9E5B"));
-
- fireButton("pauseBtn");
-
- assertEquals("PAUSED", simStatus.getText());
- assertTrue(consoleText().contains("Simulation paused."));
- assertTrue(field("pauseBtn", Button.class).getStyle().contains("#C23B42"));
-
- fireButton("playBtn");
-
- // Add a bit of delay
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- assertTrue(consoleText().contains("\u26a0 Simulation already complete. Press Stop to reset before playing again."));
+ assertEquals("", field("playBtn", Button.class).getStyle());
+ assertEquals("", field("pauseBtn", Button.class).getStyle());
invokeOnFx("onStop", new Class>[0]);
@@ -376,17 +391,24 @@ void play_pause_resume_and_stop_flow_updates_labels_buttons_and_logs() {
assertEquals("", field("pauseBtn", Button.class).getStyle());
}
+ /** Verifies play still starts when baseline snapshot save fails, while logging failure context. */
@Test
void play_logs_snapshot_failure_but_still_starts_when_engine_cannot_save_baseline() {
loadScreenWith(new UnsavableEngine(), null, 30, 30);
- installOptionalStatusLabel();
+ Label simStatus = installOptionalStatusLabel();
fireButton("playBtn");
+ WaitForAsyncUtils.waitForFxEvents();
invokeOnFx("onStop", new Class>[0]);
- assertTrue(consoleText().contains("Simulation started."));
+ assertEquals("STOPPED", simStatus.getText());
+ assertTrue(consoleText().contains("\u26a0 Could not snapshot initial state: snapshot failed"));
+ assertTrue(consoleText().contains("\u26a0 No tasks could be generated."));
+ assertTrue(consoleText().contains("Simulation failed: The map configuration is invalid. Simulation cannot run."));
+ assertFalse(consoleText().contains("Simulation started."));
}
+ /** Verifies speed controls update internal speed factor and append matching log entries. */
@Test
void speed_buttons_update_speed_factor_and_log_each_choice() {
fireButton("speed2Btn");
@@ -402,21 +424,23 @@ void speed_buttons_update_speed_factor_and_log_each_choice() {
assertTrue(consoleText().contains("Speed set to ×1."));
}
+ /** Verifies zoom-in/out/reset controls update viewport zoom and reset log messaging. */
@Test
void zoom_buttons_and_origin_reset_update_zoom_and_console() {
double initialZoom = field("zoom", Double.class);
- fireButtonByText("⊕");
+ fireButtonByText("+");
assertTrue(field("zoom", Double.class) > initialZoom);
- fireButtonByText("⊖");
+ fireButtonByText("-");
assertEquals(initialZoom, field("zoom", Double.class), 0.0001);
- fireButtonByText("⌖");
+ fireButtonByText("⊙");
assertEquals(1.0, field("zoom", Double.class), 0.0001);
assertTrue(consoleText().contains("Viewport reset to origin."));
}
+ /** Verifies sidebar/console toggle menu items collapse and restore split-pane dividers. */
@Test
void sidebar_and_console_toggles_collapse_and_restore_split_panes() {
CheckMenuItem sidebarItem = field("toggleSidebarItem", CheckMenuItem.class);
@@ -447,6 +471,7 @@ void sidebar_and_console_toggles_collapse_and_restore_split_panes() {
assertEquals(0.83, consoleSplit.getDividerPositions()[0], 0.08);
}
+ /** Verifies restart with invalid engine resets run state and rotates run identifier. */
@Test
void restart_with_engine_resets_tick_progress_status_and_updates_run_id() {
SimulationEngine engine = emptyEngine();
@@ -455,7 +480,7 @@ void restart_with_engine_resets_tick_progress_status_and_updates_run_id() {
UUID originalRunId = engine.getRunId();
invokeOnFx("onNextFrame", new Class>[0]);
- assertEquals("TICK 1", tickLabel().getText());
+ assertEquals("TICK 0", tickLabel().getText());
fireButtonByText("↺");
@@ -466,6 +491,7 @@ void restart_with_engine_resets_tick_progress_status_and_updates_run_id() {
assertTrue(consoleText().contains("Simulation reset."));
}
+ /** Verifies restart surfaces error state when saved config path reload is invalid. */
@Test
void restart_with_bad_config_path_reports_error_state() throws Exception {
Path malformed = tempDir.resolve("bad-restart.json");
@@ -484,6 +510,56 @@ void restart_with_bad_config_path_reports_error_state() throws Exception {
assertTrue(consoleText().contains("Simulation reset failed"));
}
+ /** Verifies reset after Results → Editor reloads the saved editor baseline instead of the original config. */
+ @Test
+ void restart_after_results_round_trip_uses_editor_baseline_snapshot() throws Exception {
+ Path originalConfig = tempDir.resolve("original-empty.json");
+ Path baselineConfig = tempDir.resolve("editor-baseline.json");
+
+ SimulationEngine originalEngine = new SimulationEngine(
+ new Map(4, 4),
+ new Robot[]{},
+ new Dispatcher(),
+ CoordinationPolicy.noOp()
+ );
+ originalEngine.configSaving(originalConfig.toString());
+
+ Map editedMap = new Map(4, 4);
+ Robot placedRobot = new Robot("placed_bot", new Vector2D(2, 1));
+ editedMap.addEntity(placedRobot);
+ SimulationEngine editedEngine = new SimulationEngine(
+ editedMap,
+ new Robot[]{ placedRobot },
+ new Dispatcher(),
+ CoordinationPolicy.noOp()
+ );
+ editedEngine.configSaving(baselineConfig.toString());
+
+ loadScreenWith(editedEngine, originalConfig.toString(), baselineConfig.toString(), 30, 30);
+
+ assertEquals(baselineConfig.toString(), field("initialSnapshotPath", String.class));
+
+ fireButtonByText("RESULTS");
+ WaitForAsyncUtils.waitForFxEvents();
+ assertNotNull(lookup("#robotStatsTable").queryAs(TableView.class));
+
+ fireButtonByText("RETURN TO EDITOR");
+ WaitForAsyncUtils.waitForFxEvents();
+
+ interact(this::syncControllerFromNavigator);
+ WaitForAsyncUtils.waitForFxEvents();
+
+ assertEquals(baselineConfig.toString(), field("initialSnapshotPath", String.class));
+
+ fireButtonByText("↺");
+
+ assertEquals(1, AppState.getEngine().getMap().getEntities().size());
+ assertTrue(AppState.getEngine().getMap().getEntities().stream()
+ .anyMatch(entity -> "placed_bot".equals(entity.getName())));
+ assertEquals("TICK 0", tickLabel().getText());
+ }
+
+ /** Verifies results navigation and back-navigation route to expected real screens. */
@Test
void results_tab_and_back_button_navigate_through_real_screens() {
fireButtonByText("RESULTS");
@@ -500,6 +576,7 @@ void results_tab_and_back_button_navigate_through_real_screens() {
assertNotNull(lookup("#mapCombo").query());
}
+ /** Verifies editor tab activation updates tab style state without navigation side effects. */
@Test
void editor_tab_keeps_editor_active_without_navigation() {
Button editor = field("editorTabBtn", Button.class);
@@ -511,6 +588,7 @@ void editor_tab_keeps_editor_active_without_navigation() {
assertEquals(List.of("tab-btn"), results.getStyleClass());
}
+ /** Verifies single-click object tile selection logs selected type without opening dialogs. */
@Test
void add_object_single_click_logs_selected_type_without_opening_dialog() {
Button robotTile = objectTile("ROBOT");
@@ -521,20 +599,7 @@ void add_object_single_click_logs_selected_type_without_opening_dialog() {
assertTrue(consoleText().contains("Selected object type: ROBOT."));
}
- /*@Test
- // TODO: update
- void reset_string_property_and_query_logs_handle_optional_fields() {
- TextField optionalStringField = new TextField("custom");
- installPrivateField("strPropField", optionalStringField);
-
- invokeOnFx("onResetStringProp", new Class>[0]);
- interact(controller::queryLogs);
- WaitForAsyncUtils.waitForFxEvents();
-
- assertEquals("Hello", optionalStringField.getText());
- assertEquals("hello testing", field("databaseArea", TextArea.class).getText());
- }*/
-
+ /** Verifies cleanup stops animation/tip loops while preserving active AppState engine. */
@Test
void cleanup_stops_tip_and_animation_timelines() {
EngineFixture fixture = loadDiverseEngine();
@@ -552,17 +617,34 @@ void cleanup_stops_tip_and_animation_timelines() {
assertSame(fixture.engine, AppState.getEngine());
}
+ /** Clears console through UI button to mirror user-driven behavior. */
private void clearConsoleThroughUi() {
Button clear = field("clearConsoleButton", Button.class);
interact(clear::fire);
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Waits for console to become empty to reduce flakiness from async logging.
+ *
+ * @throws TimeoutException if console is not emptied within timeout
+ */
private void assertConsoleEventuallyEmpty() throws TimeoutException {
WaitForAsyncUtils.waitFor(5, TimeUnit.SECONDS, () -> consoleText().isEmpty());
}
+ /**
+ * Loads/reloads screen using provided engine/config and canvas dimensions.
+ */
private void loadScreenWith(SimulationEngine engine, String configPath, int canvasWidth, int canvasHeight) {
+ loadScreenWith(engine, configPath, null, canvasWidth, canvasHeight);
+ }
+
+ /**
+ * Loads/reloads screen using provided engine/config/baseline and canvas dimensions.
+ */
+ private void loadScreenWith(SimulationEngine engine, String configPath, String editorBaselinePath,
+ int canvasWidth, int canvasHeight) {
interact(() -> {
try {
AppState.clear();
@@ -573,6 +655,9 @@ private void loadScreenWith(SimulationEngine engine, String configPath, int canv
if (configPath != null) {
AppState.setConfigPath(configPath);
}
+ if (editorBaselinePath != null) {
+ AppState.setEditorBaselinePath(editorBaselinePath);
+ }
loadSceneFromCurrentAppState();
} catch (Exception ex) {
throw new RuntimeException(ex);
@@ -581,6 +666,11 @@ private void loadScreenWith(SimulationEngine engine, String configPath, int canv
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Loads simulation FXML and binds the controller to the shared stage.
+ *
+ * @throws IOException if FXML cannot be loaded
+ */
private void loadSceneFromCurrentAppState() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/com/openrobotics/fxml/SimulationScreen.fxml"));
Parent root = loader.load();
@@ -589,12 +679,19 @@ private void loadSceneFromCurrentAppState() throws IOException {
stage.show();
}
+ /** Refreshes the cached controller after navigation recreates the simulation screen. */
+ private void syncControllerFromNavigator() {
+ controller = assertInstanceOf(SimulationController.class, ScreenNavigator.getCurrentController());
+ }
+
+ /** Builds and loads a representative fixture containing multiple entity types and one task. */
private EngineFixture loadDiverseEngine() {
EngineFixture fixture = diverseFixture();
loadScreenWith(fixture.engine, null, 6, 5);
return fixture;
}
+ /** Creates a deterministic mixed-entity engine fixture for outliner/properties behavior tests. */
private EngineFixture diverseFixture() {
Map map = new Map(5, 5);
Robot robot = new Robot("robot_1", new Vector2D(0, 0));
@@ -618,6 +715,7 @@ private EngineFixture diverseFixture() {
return new EngineFixture(engine, robot, rack, charger, station, obstacle, task);
}
+ /** Creates a minimal engine suitable for transport/control-flow tests. */
private SimulationEngine emptyEngine() {
Map map = new Map(3, 3);
Robot robot = new Robot("idle_bot", new Vector2D(0, 0));
@@ -627,6 +725,7 @@ private SimulationEngine emptyEngine() {
return new SimulationEngine(map, new Robot[]{robot}, new Dispatcher(), CoordinationPolicy.noOp());
}
+ /** Creates an entity via controller factory path for a given user-facing type token. */
private MapEntity createdEntity(String type, int x, int y) {
return (MapEntity) invokePrivate(
"createEntityFromType",
@@ -638,6 +737,7 @@ private MapEntity createdEntity(String type, int x, int y) {
);
}
+ /** Proxies private placement rule check to validate occupancy constraints in tests. */
private boolean canPlace(MapEntity entity, int x, int y, MapEntity ignore) {
return (boolean) invokePrivate(
"canPlaceEntityAt",
@@ -649,18 +749,21 @@ private boolean canPlace(MapEntity entity, int x, int y, MapEntity ignore) {
);
}
+ /** Fires a button by controller field name. */
private void fireButton(String fieldName) {
Button button = field(fieldName, Button.class);
interact(button::fire);
WaitForAsyncUtils.waitForFxEvents();
}
+ /** Fires the first visible button matching display text. */
private void fireButtonByText(String text) {
Button button = buttonWithText(text);
interact(button::fire);
WaitForAsyncUtils.waitForFxEvents();
}
+ /** Finds a button by displayed text in current scene graph. */
private Button buttonWithText(String text) {
return descendants(stage.getScene().getRoot()).stream()
.filter(Button.class::isInstance)
@@ -670,6 +773,7 @@ private Button buttonWithText(String text) {
.orElseThrow(() -> new AssertionError("No button with text: " + text));
}
+ /** Finds an object palette tile button by userData marker. */
private Button objectTile(String userData) {
return descendants(stage.getScene().getRoot()).stream()
.filter(Button.class::isInstance)
@@ -679,6 +783,7 @@ private Button objectTile(String userData) {
.orElseThrow(() -> new AssertionError("No object tile with userData: " + userData));
}
+ /** Creates a synthetic primary-click event with specified click count. */
private MouseEvent clickEvent(int clickCount) {
return new MouseEvent(
MouseEvent.MOUSE_CLICKED,
@@ -702,6 +807,7 @@ private MouseEvent clickEvent(int clickCount) {
);
}
+ /** Returns a flattened list of node descendants including the root node itself. */
private List descendants(Node node) {
List result = new ArrayList<>();
result.add(node);
@@ -713,6 +819,7 @@ private List descendants(Node node) {
return result;
}
+ /** Aggregates labeled text from properties panel for content assertions. */
private String propertiesPanelText() {
Parent panel = field("propertiesPanel", Parent.class);
return descendants(panel).stream()
@@ -722,6 +829,7 @@ private String propertiesPanelText() {
.collect(Collectors.joining("\n"));
}
+ /** Finds a TextField in properties panel with exact current value. */
private TextField textFieldWithValue(String value) {
Parent panel = field("propertiesPanel", Parent.class);
return descendants(panel).stream()
@@ -732,38 +840,46 @@ private TextField textFieldWithValue(String value) {
.orElseThrow(() -> new AssertionError("No TextField with value: " + value));
}
+ /** Returns outliner list view from private controller field. */
@SuppressWarnings("unchecked")
private ListView outliner() {
return (ListView) field("outlinerListView", ListView.class);
}
+ /** Returns console text area reference. */
private TextArea console() {
return field("consoleArea", TextArea.class);
}
+ /** Returns current console contents. */
private String consoleText() {
return console().getText();
}
+ /** Returns tick display label. */
private Label tickLabel() {
return field("tickDisplayLabel", Label.class);
}
+ /** Returns simulation progress bar. */
private ProgressBar progressBar() {
return field("simProgressBar", ProgressBar.class);
}
+ /** Installs optional sim-status label used by some controller branches. */
private Label installOptionalStatusLabel() {
Label label = new Label();
installPrivateField("simStatusLabel", label);
return label;
}
+ /** Installs a private field value on FX thread. */
private void installPrivateField(String name, Object value) {
interact(() -> setField(name, value));
WaitForAsyncUtils.waitForFxEvents();
}
+ /** Invokes a private controller method on FX thread and returns its result. */
private Object invokeOnFx(String name, Class>[] parameterTypes, Object... args) {
AtomicReference result = new AtomicReference<>();
interact(() -> result.set(invokePrivate(name, parameterTypes, args)));
@@ -771,6 +887,7 @@ private Object invokeOnFx(String name, Class>[] parameterTypes, Object... args
return result.get();
}
+ /** Invokes a private controller method via reflection. */
private Object invokePrivate(String name, Class>[] parameterTypes, Object... args) {
try {
Method method = SimulationController.class.getDeclaredMethod(name, parameterTypes);
@@ -787,6 +904,7 @@ private Object invokePrivate(String name, Class>[] parameterTypes, Object... a
}
}
+ /** Reads a private controller field and casts it to target type. */
private T field(String name, Class type) {
try {
Field field = SimulationController.class.getDeclaredField(name);
@@ -797,6 +915,7 @@ private T field(String name, Class type) {
}
}
+ /** Writes a private controller field through reflection. */
private void setField(String name, Object value) {
try {
Field field = SimulationController.class.getDeclaredField(name);
@@ -807,6 +926,7 @@ private void setField(String name, Object value) {
}
}
+ /** Engine test double whose tick method reports immediate completion. */
private static class CompletingEngine extends SimulationEngine {
CompletingEngine() {
super(new Map(2, 2), new Robot[]{new Robot("bot", new Vector2D(0, 0))}, new Dispatcher(), CoordinationPolicy.noOp());
@@ -818,6 +938,7 @@ public boolean tick() {
}
}
+ /** Engine test double that throws on snapshot/config save attempts. */
private static class UnsavableEngine extends SimulationEngine {
UnsavableEngine() {
super(new Map(2, 2), new Robot[]{new Robot("bot", new Vector2D(0, 0))}, new Dispatcher(), CoordinationPolicy.noOp());
@@ -829,6 +950,7 @@ public void configSaving(String path) throws IOException {
}
}
+ /** Container for a reusable diverse engine fixture and key entity references. */
private static class EngineFixture {
final SimulationEngine engine;
final Robot robot;
@@ -857,6 +979,7 @@ private static class EngineFixture {
}
}
+ /** Verifies intersection tile is available for traffic-rules policy engines. */
@Test
void intersection_tile_visible_for_traffic_rules_engine() {
reloadSimulationWithEngine(buildEngine(new TrafficRulesPolicy(new java.util.HashSet<>())));
@@ -866,6 +989,7 @@ void intersection_tile_visible_for_traffic_rules_engine() {
assertTrue(intersectionButton.isManaged());
}
+ /** Verifies intersection tile is hidden for reservation policy engines. */
@Test
void intersection_tile_hidden_for_reservation_policy_engine() {
reloadSimulationWithEngine(buildEngine(new ReservationKPolicy(3)));
@@ -875,6 +999,7 @@ void intersection_tile_hidden_for_reservation_policy_engine() {
assertFalse(intersectionButton.isManaged());
}
+ /** Verifies intersection tile is hidden for NO_OP policy engines. */
@Test
void intersection_tile_hidden_for_no_op_engine() {
reloadSimulationWithEngine(buildEngine(CoordinationPolicy.noOp()));
diff --git a/open-robotics/src/test/java/com/openrobotics/controllers/WelcomeControllerTest.java b/open-robotics/src/test/java/com/openrobotics/controllers/WelcomeControllerTest.java
index d1be117c..721dc9cf 100644
--- a/open-robotics/src/test/java/com/openrobotics/controllers/WelcomeControllerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/controllers/WelcomeControllerTest.java
@@ -13,8 +13,20 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
+/**
+ * JavaFX navigation test for {@link WelcomeController}.
+ *
+ * This test suite verifies that the welcome screen loads correctly and that the primary
+ * call-to-action button routes users to the setup screen.
+ */
public class WelcomeControllerTest extends ApplicationTest {
+ /**
+ * Loads the welcome screen FXML on the TestFX stage.
+ *
+ * @param stage JavaFX stage supplied by TestFX
+ * @throws Exception if FXML loading fails
+ */
@Override
public void start(Stage stage) throws Exception {
ScreenNavigator.setPrimaryStage(stage);
@@ -25,6 +37,11 @@ public void start(Stage stage) throws Exception {
stage.show();
}
+ /**
+ * Verifies clicking the "START SETUP" action navigates to the setup screen.
+ *
+ * Navigation success is asserted by querying a setup-specific control ({@code #mapCombo}).
+ */
@Test
void start_setup_navigates_to_setup_screen() {
Button startButton = lookup((Button b) ->
diff --git a/open-robotics/src/test/java/com/openrobotics/db/DatabaseLifecycleTest.java b/open-robotics/src/test/java/com/openrobotics/db/DatabaseLifecycleTest.java
index 40ea1a8b..e9455476 100644
--- a/open-robotics/src/test/java/com/openrobotics/db/DatabaseLifecycleTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/db/DatabaseLifecycleTest.java
@@ -10,12 +10,24 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Reflection-backed lifecycle tests for {@link Database}'s static state machine.
+ *
+ * This suite validates failure and guard behavior around initialization, shutdown, and access
+ * APIs without requiring a real DB connection. It snapshots/restores static internals before and
+ * after each test to keep behavior deterministic and avoid cross-test pollution.
+ */
public class DatabaseLifecycleTest {
private HikariDataSource originalDataSource;
private boolean originalShutdown;
private boolean originalUuidRobotSchemaReady;
+ /**
+ * Captures current static {@link Database} internals so each test can safely mutate them.
+ *
+ * @throws Exception if reflective field access fails
+ */
@BeforeEach
void captureState() throws Exception {
originalDataSource = (HikariDataSource) getField("dataSource");
@@ -23,6 +35,11 @@ void captureState() throws Exception {
originalUuidRobotSchemaReady = (boolean) getField("uuidRobotSchemaReady");
}
+ /**
+ * Restores static {@link Database} internals to their pre-test values.
+ *
+ * @throws Exception if reflective field write fails
+ */
@AfterEach
void restoreState() throws Exception {
setField("dataSource", originalDataSource);
@@ -30,6 +47,11 @@ void restoreState() throws Exception {
setField("uuidRobotSchemaReady", originalUuidRobotSchemaReady);
}
+ /**
+ * Verifies {@link Database#getDataSource()} rejects access before successful initialization.
+ *
+ * @throws Exception if reflective state setup fails
+ */
@Test
void getDataSource_throws_when_database_has_not_been_initialized() throws Exception {
setField("dataSource", null);
@@ -40,6 +62,11 @@ void getDataSource_throws_when_database_has_not_been_initialized() throws Except
assertTrue(ex.getMessage().contains("Database not initialized"));
}
+ /**
+ * Verifies UUID-schema readiness accessor also rejects access before initialization.
+ *
+ * @throws Exception if reflective state setup fails
+ */
@Test
void isUuidRobotSchemaReady_throws_when_database_has_not_been_initialized() throws Exception {
setField("dataSource", null);
@@ -50,6 +77,11 @@ void isUuidRobotSchemaReady_throws_when_database_has_not_been_initialized() thro
assertTrue(ex.getMessage().contains("Database not initialized"));
}
+ /**
+ * Verifies {@link Database#init()} fails fast once shutdown flag is set.
+ *
+ * @throws Exception if reflective state setup fails
+ */
@Test
void init_fails_fast_when_database_has_already_been_shut_down() throws Exception {
setField("dataSource", null);
@@ -60,6 +92,11 @@ void init_fails_fast_when_database_has_already_been_shut_down() throws Exception
assertTrue(ex.getMessage().contains("shut down"));
}
+ /**
+ * Verifies {@link Database#shutdown()} closes active datasource and blocks later access.
+ *
+ * @throws Exception if reflective state setup fails
+ */
@Test
void shutdown_closes_current_data_source_and_blocks_further_access() throws Exception {
HikariDataSource dataSource = new HikariDataSource();
@@ -72,12 +109,26 @@ void shutdown_closes_current_data_source_and_blocks_further_access() throws Exce
assertThrows(IllegalStateException.class, Database::getDataSource);
}
+ /**
+ * Reads a private static field from {@link Database}.
+ *
+ * @param fieldName declared field name on {@link Database}
+ * @return current field value
+ * @throws Exception if reflection lookup/read fails
+ */
private Object getField(String fieldName) throws Exception {
Field field = Database.class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(null);
}
+ /**
+ * Writes a private static field on {@link Database}.
+ *
+ * @param fieldName declared field name on {@link Database}
+ * @param value value to assign
+ * @throws Exception if reflection lookup/write fails
+ */
private void setField(String fieldName, Object value) throws Exception {
Field field = Database.class.getDeclaredField(fieldName);
field.setAccessible(true);
diff --git a/open-robotics/src/test/java/com/openrobotics/db/dao/DaoConstraintFailureTest.java b/open-robotics/src/test/java/com/openrobotics/db/dao/DaoConstraintFailureTest.java
index 548e5a4b..ffb50179 100644
--- a/open-robotics/src/test/java/com/openrobotics/db/dao/DaoConstraintFailureTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/db/dao/DaoConstraintFailureTest.java
@@ -20,8 +20,23 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
+/**
+ * Integration tests that assert DAO writes fail when database constraints are violated.
+ *
+ * These tests intentionally submit invalid inputs to verify that JDBC operations surface
+ * {@link SQLException} for JSONB validation failures, foreign-key violations, and uniqueness
+ * conflicts. The suite is guarded to run only when the shared DB schema is migrated to the UUID
+ * robot-column shape expected by current DAOs.
+ */
public class DaoConstraintFailureTest {
+ /**
+ * Initializes database access once for the suite and skips tests when schema prerequisites are
+ * not met in the shared environment.
+ *
+ * @throws IOException if database config initialization fails
+ * @throws SQLException if DB bootstrap/migration connectivity fails
+ */
@BeforeAll
static void initDatabase() throws IOException, SQLException {
Database.init();
@@ -31,6 +46,9 @@ static void initDatabase() throws IOException, SQLException {
);
}
+ /**
+ * Verifies map insertion rejects malformed JSON content in the JSONB tile-data column.
+ */
@Test
void mapDao_insert_rejects_invalid_jsonb_tile_data() {
MapRecord record = new MapRecord();
@@ -43,6 +61,9 @@ void mapDao_insert_rejects_invalid_jsonb_tile_data() {
assertThrows(SQLException.class, () -> MapDao.insert(record));
}
+ /**
+ * Verifies simulation-run insert fails when referencing a non-existent map ID.
+ */
@Test
void simulationRunDao_insert_rejects_unknown_map_foreign_key() {
SimulationRunRecord record = new SimulationRunRecord();
@@ -55,6 +76,14 @@ void simulationRunDao_insert_rejects_unknown_map_foreign_key() {
assertThrows(SQLException.class, () -> SimulationRunDao.insert(record));
}
+ /**
+ * Verifies map deletion fails while a simulation run still references that map.
+ *
+ * The test creates a valid map/run pair, asserts delete failure, then performs explicit
+ * cleanup in dependency order.
+ *
+ * @throws Exception if setup or cleanup DAO operations fail unexpectedly
+ */
@Test
void mapDao_deleteById_rejects_removal_when_runs_still_reference_map() throws Exception {
UUID mapId = MapDao.insert(validMap("Referenced Map"));
@@ -71,6 +100,11 @@ void mapDao_deleteById_rejects_removal_when_runs_still_reference_map() throws Ex
}
}
+ /**
+ * Verifies run-results table enforces one result row per run (unique run reference).
+ *
+ * @throws Exception if setup or cleanup DAO operations fail unexpectedly
+ */
@Test
void runResultDao_insert_rejects_duplicate_row_for_same_run() throws Exception {
UUID mapId = MapDao.insert(validMap("Duplicate Result Map"));
@@ -95,6 +129,9 @@ void runResultDao_insert_rejects_duplicate_row_for_same_run() throws Exception {
}
}
+ /**
+ * Verifies task/log/stats DAOs reject inserts/upserts that reference unknown run IDs.
+ */
@Test
void workloadTaskAndLogAndStats_inserts_reject_unknown_run_foreign_keys() {
UUID unknownRunId = UUID.randomUUID();
@@ -120,6 +157,9 @@ void workloadTaskAndLogAndStats_inserts_reject_unknown_run_foreign_keys() {
assertThrows(SQLException.class, () -> RobotRunStatsDao.upsert(stats));
}
+ /**
+ * Builds a minimal valid map record used by tests that need a persisted map parent row.
+ */
private MapRecord validMap(String name) {
MapRecord record = new MapRecord();
record.setName(name);
@@ -130,6 +170,9 @@ private MapRecord validMap(String name) {
return record;
}
+ /**
+ * Builds a minimal valid simulation-run record referencing an existing map.
+ */
private SimulationRunRecord validRun(UUID mapId) {
SimulationRunRecord record = new SimulationRunRecord();
record.setMapId(mapId);
diff --git a/open-robotics/src/test/java/com/openrobotics/db/dao/DaoIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/db/dao/DaoIntegrationTest.java
index e48ecf6a..52f6ff44 100644
--- a/open-robotics/src/test/java/com/openrobotics/db/dao/DaoIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/db/dao/DaoIntegrationTest.java
@@ -17,16 +17,43 @@
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+/**
+ * Ordered integration tests for DAO CRUD/query behavior against a real database.
+ *
+ * This suite validates positive-path persistence and retrieval semantics across map, run,
+ * result, task, log, and robot-stats DAOs. Tests intentionally share seeded IDs and use explicit
+ * ordering so later assertions can build on earlier inserts while still cleaning up all created
+ * rows in {@link #cleanup()}.
+ */
public class DaoIntegrationTest {
+ /**
+ * Canonical map ID created by early MapDao tests and reused by downstream DAO tests.
+ */
private static UUID mapId;
+ /**
+ * Canonical simulation run ID created by SimulationRunDao tests and reused downstream.
+ */
private static UUID runId;
+ /**
+ * Stable robot ID used in log/stats fixtures for deterministic ordering and joins.
+ */
private static final UUID ROBOT_ID_0 = UUID.fromString("00000000-0000-0000-0000-000000000001");
+ /**
+ * Second stable robot ID used in multi-robot ordering/upsert scenarios.
+ */
private static final UUID ROBOT_ID_1 = UUID.fromString("00000000-0000-0000-0000-000000000002");
/** Extra map IDs created in individual tests; cleaned up in @AfterAll. */
private static final List extraMapIds = new ArrayList<>();
+ /**
+ * Initializes database access once for the suite and skips execution if the shared schema is
+ * not yet migrated to UUID robot columns required by the current DAO model layer.
+ *
+ * @throws IOException if DB configuration/bootstrap fails
+ * @throws SQLException if DB connectivity or migration checks fail
+ */
@BeforeAll
static void initDatabase() throws IOException, SQLException {
Database.init();
@@ -36,6 +63,14 @@ static void initDatabase() throws IOException, SQLException {
);
}
+ /**
+ * Best-effort cleanup of all rows created by this suite, in dependency-safe order.
+ *
+ * Child rows (stats/logs/tasks/results/runs) are deleted before map rows to satisfy foreign
+ * keys. Cleanup covers both primary shared IDs and additional IDs created in individual tests.
+ *
+ * @throws SQLException if cleanup queries fail
+ */
@AfterAll
static void cleanup() throws SQLException {
if (mapId != null) {
@@ -64,6 +99,9 @@ static void cleanup() throws SQLException {
// HELPER METHODS
+ /**
+ * Creates a minimal valid {@link MapRecord} fixture with deterministic defaults.
+ */
private static MapRecord mapRec(String name) {
MapRecord m = new MapRecord();
m.setName(name);
@@ -74,6 +112,9 @@ private static MapRecord mapRec(String name) {
return m;
}
+ /**
+ * Creates a minimal valid {@link SimulationRunRecord} fixture linked to a map.
+ */
private static SimulationRunRecord simRun(UUID forMapId) {
SimulationRunRecord r = new SimulationRunRecord();
r.setMapId(forMapId);
@@ -84,10 +125,16 @@ private static SimulationRunRecord simRun(UUID forMapId) {
return r;
}
+ /**
+ * Inserts a minimal simulation run for convenience in downstream tests.
+ */
private static UUID insertRun(UUID forMapId) throws SQLException {
return SimulationRunDao.insert(simRun(forMapId));
}
+ /**
+ * Creates a minimal workload task fixture for a run.
+ */
private static WorkloadTaskRecord task(UUID forRunId, String type) {
WorkloadTaskRecord t = new WorkloadTaskRecord();
t.setRunId(forRunId);
@@ -96,6 +143,9 @@ private static WorkloadTaskRecord task(UUID forRunId, String type) {
return t;
}
+ /**
+ * Creates a minimal simulation log fixture.
+ */
private static SimLogRecord log(UUID forRunId, int tick, UUID robotId, String event) {
SimLogRecord l = new SimLogRecord();
l.setRunId(forRunId);
@@ -105,6 +155,9 @@ private static SimLogRecord log(UUID forRunId, int tick, UUID robotId, String ev
return l;
}
+ /**
+ * Creates a mostly-populated robot-stats fixture used by upsert/query tests.
+ */
private static RobotRunStatsRecord stats(UUID forRunId, UUID robotId) {
RobotRunStatsRecord s = new RobotRunStatsRecord();
s.setRunId(forRunId);
@@ -123,7 +176,11 @@ private static RobotRunStatsRecord stats(UUID forRunId, UUID robotId) {
// ── MapDao ────────────────────────────────────────────────────────────────
- /** insert() with null id auto-generates UUID; inserted fields round-trip correctly. */
+ /** insert() with null id auto-generates UUID; inserted fields round-trip correctly.
+ * Test that insert() with null id auto-generates UUID and inserted fields round-trip correctly.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(1)
void mapDao_insert_autoGeneratesId_andFieldsRoundTrip() throws SQLException {
@@ -144,7 +201,10 @@ void mapDao_insert_autoGeneratesId_andFieldsRoundTrip() throws SQLException {
assertNotNull(found.get().getCreatedAt());
}
- /** insert() with an explicit id uses it; null randomSeed preserved; COALESCE fills createdAt. */
+ /** insert() with an explicit id uses it; null randomSeed preserved; COALESCE fills createdAt.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(2)
void mapDao_insert_usesProvidedId_andNullRandomSeedPreserved() throws SQLException {
@@ -168,14 +228,20 @@ void mapDao_insert_usesProvidedId_andNullRandomSeedPreserved() throws SQLExcepti
MapDao.deleteById(customId);
}
- /** findById() returns empty for an unknown UUID. */
+ /** findById() returns empty for an unknown UUID.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(3)
void mapDao_findById_notFound() throws SQLException {
assertTrue(MapDao.findById(UUID.randomUUID()).isEmpty());
}
- /** findAll() returns all rows ordered by created_at DESC. */
+ /** findAll() returns all rows ordered by created_at DESC.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(4)
void mapDao_findAll_containsInsertedMaps_orderedByCreatedAtDesc() throws SQLException {
@@ -200,7 +266,10 @@ void mapDao_findAll_containsInsertedMaps_orderedByCreatedAtDesc() throws SQLExce
assertTrue(all.stream().anyMatch(r -> r.getId().equals(mapId)));
}
- /** deleteById() returns true for existing row, false for missing. */
+ /** deleteById() returns true for existing row, false for missing.
+ *
+ * @throws SQLException if the delete fails
+ */
@Test
@Order(5)
void mapDao_deleteById_trueWhenFound_falseWhenMissing() throws SQLException {
@@ -212,7 +281,10 @@ void mapDao_deleteById_trueWhenFound_falseWhenMissing() throws SQLException {
// ── SimulationRunDao ──────────────────────────────────────────────────────
- /** insert() with null id auto-generates UUID; basic fields persist. */
+ /** insert() with null id auto-generates UUID; basic fields persist.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(6)
void simulationRunDao_insert_autoGeneratesId_andFieldsRoundTrip() throws SQLException {
@@ -233,7 +305,10 @@ void simulationRunDao_insert_autoGeneratesId_andFieldsRoundTrip() throws SQLExce
assertEquals(mapId, found.get().getMapId());
}
- /** insert() with explicit id uses it; all null JSONB fields stored as NULL. */
+ /** insert() with explicit id uses it; all null JSONB fields stored as NULL.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(7)
void simulationRunDao_insert_usesProvidedId_andNullJsonFieldsPreserved() throws SQLException {
@@ -258,7 +333,10 @@ void simulationRunDao_insert_usesProvidedId_andNullJsonFieldsPreserved() throws
assertNull(found.get().getFinishedAt());
}
- /** insert() with non-null JSONB fields and workloadSeed round-trips all values. */
+ /** insert() with non-null JSONB fields and workloadSeed round-trips all values.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(8)
void simulationRunDao_insert_withAllJsonFields_roundTrip() throws SQLException {
@@ -277,14 +355,20 @@ void simulationRunDao_insert_withAllJsonFields_roundTrip() throws SQLException {
assertTrue(f.getSimSettings().contains("\"speed\""));
}
- /** findById() returns empty for an unknown UUID. */
+ /** findById() returns empty for an unknown UUID.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(9)
void simulationRunDao_findById_notFound() throws SQLException {
assertTrue(SimulationRunDao.findById(UUID.randomUUID()).isEmpty());
}
- /** findByMapId() filters by map and orders by started_at DESC. */
+ /** findByMapId() filters by map and orders by started_at DESC.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(10)
void simulationRunDao_findByMapId_filteredAndOrderedByStartedAtDesc() throws SQLException {
@@ -312,14 +396,20 @@ void simulationRunDao_findByMapId_filteredAndOrderedByStartedAtDesc() throws SQL
assertTrue(idxNewer < idxOlder, "newer run must appear first");
}
- /** findByMapId() returns empty for an unknown map UUID. */
+ /** findByMapId() returns empty for an unknown map UUID.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(11)
void simulationRunDao_findByMapId_emptyForUnknownMap() throws SQLException {
assertTrue(SimulationRunDao.findByMapId(UUID.randomUUID()).isEmpty());
}
- /** updateStatus() sets status and finishedAt on existing run. */
+ /** updateStatus() sets status and finishedAt on existing run.
+ *
+ * @throws SQLException if the update fails
+ */
@Test
@Order(12)
void simulationRunDao_updateStatus_setsStatusAndFinishedAt() throws SQLException {
@@ -330,7 +420,10 @@ void simulationRunDao_updateStatus_setsStatusAndFinishedAt() throws SQLException
assertNotNull(f.getFinishedAt());
}
- /** updateStatus() on a non-existent UUID is a no-op for other runs. */
+ /** updateStatus() on a non-existent UUID is a no-op for other runs.
+ *
+ * @throws SQLException if the update fails
+ */
@Test
@Order(13)
void simulationRunDao_updateStatus_noEffectOnUnrelatedRun() throws SQLException {
@@ -340,7 +433,10 @@ void simulationRunDao_updateStatus_noEffectOnUnrelatedRun() throws SQLException
assertEquals("RUNNING", SimulationRunDao.findById(localRunId).orElseThrow().getStatus());
}
- /** deleteById() returns true for existing row and false for missing. */
+ /** deleteById() returns true for existing row and false for missing.
+ *
+ * @throws SQLException if the delete fails
+ */
@Test
@Order(14)
void simulationRunDao_deleteById_trueWhenFound_falseWhenMissing() throws SQLException {
@@ -352,7 +448,10 @@ void simulationRunDao_deleteById_trueWhenFound_falseWhenMissing() throws SQLExce
// ── RunResultDao ──────────────────────────────────────────────────────────
- /** insert() with all non-null metrics and findByRunId() retrieve correctly. */
+ /** insert() with all non-null metrics and findByRunId() retrieve correctly.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(15)
void runResultDao_insert_andFind_withAllMetrics() throws SQLException {
@@ -378,7 +477,10 @@ void runResultDao_insert_andFind_withAllMetrics() throws SQLException {
assertNull(f.getExtraMetrics());
}
- /** insert() with all null optional metrics preserves NULLs. */
+ /** insert() with all null optional metrics preserves NULLs.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(16)
void runResultDao_insert_andFind_withNullMetrics() throws SQLException {
@@ -409,7 +511,10 @@ void runResultDao_insert_andFind_withNullMetrics() throws SQLException {
assertNull(f.getExtraMetrics());
}
- /** insert() with non-null extraMetrics JSONB stores and retrieves the JSON. */
+ /** insert() with non-null extraMetrics JSONB stores and retrieves the JSON.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(17)
void runResultDao_insert_andFind_extraMetricsJsonb() throws SQLException {
@@ -427,14 +532,20 @@ void runResultDao_insert_andFind_extraMetricsJsonb() throws SQLException {
assertTrue(stored.contains("1") && stored.contains("2") && stored.contains("3"));
}
- /** findByRunId() returns empty for an unknown UUID. */
+ /** findByRunId() returns empty for an unknown UUID.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(18)
void runResultDao_findByRunId_notFound() throws SQLException {
assertTrue(RunResultDao.findByRunId(UUID.randomUUID()).isEmpty());
}
- /** deleteByRunId() returns true for existing row and false for missing. */
+ /** deleteByRunId() returns true for existing row and false for missing.
+ *
+ * @throws SQLException if the delete fails
+ */
@Test
@Order(19)
void runResultDao_deleteByRunId_trueWhenFound_falseWhenMissing() throws SQLException {
@@ -452,7 +563,10 @@ void runResultDao_deleteByRunId_trueWhenFound_falseWhenMissing() throws SQLExcep
// ── WorkloadTaskDao ───────────────────────────────────────────────────────
- /** insert() with all fields (including non-null JSONB details) round-trips correctly. */
+ /** insert() with all fields (including non-null JSONB details) round-trips correctly.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(20)
void workloadTaskDao_insert_andFind_withFullFields_includesJsonbDetails() throws SQLException {
@@ -481,7 +595,10 @@ void workloadTaskDao_insert_andFind_withFullFields_includesJsonbDetails() throws
assertTrue(f.getDetails().contains("\"note\""));
}
- /** insert() with all null optional fields stores NULLs in the DB. */
+ /** insert() with all null optional fields stores NULLs in the DB.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(21)
void workloadTaskDao_insert_andFind_withNullOptionals() throws SQLException {
@@ -515,7 +632,10 @@ void workloadTaskDao_insert_andFind_withNullOptionals() throws SQLException {
assertNull(f.getDetails());
}
- /** findByRunId() returns tasks ordered by id ascending. */
+ /** findByRunId() returns tasks ordered by id ascending.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(22)
void workloadTaskDao_findByRunId_orderedById() throws SQLException {
@@ -531,14 +651,20 @@ void workloadTaskDao_findByRunId_orderedById() throws SQLException {
assertEquals(id3, tasks.get(2).getId());
}
- /** findByRunId() returns empty for an unknown run UUID. */
+ /** findByRunId() returns empty for an unknown run UUID.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(23)
void workloadTaskDao_findByRunId_emptyForUnknownRun() throws SQLException {
assertTrue(WorkloadTaskDao.findByRunId(UUID.randomUUID()).isEmpty());
}
- /** assignToRobot() sets assigned_robot_id, assigned_tick, and status to IN_PROGRESS. */
+ /** assignToRobot() sets assigned_robot_id, assigned_tick, and status to IN_PROGRESS.
+ *
+ * @throws SQLException if the assign fails
+ */
@Test
@Order(24)
void workloadTaskDao_assignToRobot_setsRobotIdTickAndStatus() throws SQLException {
@@ -552,7 +678,10 @@ void workloadTaskDao_assignToRobot_setsRobotIdTickAndStatus() throws SQLExceptio
assertEquals(15, f.getAssignedTick());
}
- /** updateStatus() sets status and non-null completedTick. */
+ /** updateStatus() sets status and non-null completedTick.
+ *
+ * @throws SQLException if the update fails
+ */
@Test
@Order(25)
void workloadTaskDao_updateStatus_withNonNullCompletedTick() throws SQLException {
@@ -565,7 +694,10 @@ void workloadTaskDao_updateStatus_withNonNullCompletedTick() throws SQLException
assertEquals(123, f.getCompletedTick());
}
- /** updateStatus() with null completedTick stores NULL in the DB. */
+ /** updateStatus() with null completedTick stores NULL in the DB.
+ *
+ * @throws SQLException if the update fails
+ */
@Test
@Order(26)
void workloadTaskDao_updateStatus_withNullCompletedTick() throws SQLException {
@@ -578,7 +710,10 @@ void workloadTaskDao_updateStatus_withNullCompletedTick() throws SQLException {
assertNull(f.getCompletedTick());
}
- /** updateStatus() and assignToRobot() on a non-existent id are no-ops. */
+ /** updateStatus() and assignToRobot() on a non-existent id are no-ops.
+ *
+ * @throws SQLException if the update or assign fails
+ */
@Test
@Order(27)
void workloadTaskDao_updateAndAssign_nonExistingId_noEffect() throws SQLException {
@@ -592,7 +727,10 @@ void workloadTaskDao_updateAndAssign_nonExistingId_noEffect() throws SQLExceptio
assertNull(f.getAssignedRobotId());
}
- /** deleteByRunId() removes all tasks and returns count; 0 for missing run. */
+ /** deleteByRunId() removes all tasks and returns count; 0 for missing run.
+ *
+ * @throws SQLException if the delete fails
+ */
@Test
@Order(28)
void workloadTaskDao_deleteByRunId_existingAndMissing() throws SQLException {
@@ -608,7 +746,10 @@ void workloadTaskDao_deleteByRunId_existingAndMissing() throws SQLException {
// ── SimLogDao ─────────────────────────────────────────────────────────────
- /** insert() with x/y coordinates and null details stores correctly. */
+ /** insert() with x/y coordinates and null details stores correctly.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(29)
void simLogDao_insert_andFind_withCoordinatesAndNullDetails() throws SQLException {
@@ -628,7 +769,10 @@ void simLogDao_insert_andFind_withCoordinatesAndNullDetails() throws SQLExceptio
assertNull(f.getDetails());
}
- /** insert() with null x, y, and details stores NULLs. */
+ /** insert() with null x, y, and details stores NULLs.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(30)
void simLogDao_insert_andFind_withNullCoordinatesAndNullDetails() throws SQLException {
@@ -645,7 +789,10 @@ void simLogDao_insert_andFind_withNullCoordinatesAndNullDetails() throws SQLExce
assertNull(f.getDetails());
}
- /** insert() with non-null details JSONB covers the non-null jsonb() branch. */
+ /** insert() with non-null details JSONB covers the non-null jsonb() branch.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(31)
void simLogDao_insert_withNonNullDetails_jsonbBranchCovered() throws SQLException {
@@ -659,7 +806,10 @@ void simLogDao_insert_withNonNullDetails_jsonbBranchCovered() throws SQLExceptio
assertTrue(f.getDetails().contains("\"impactForce\""));
}
- /** insertBatch() with an empty list is a no-op. */
+ /** insertBatch() with an empty list is a no-op.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(32)
void simLogDao_insertBatch_emptyList_isNoOp() throws SQLException {
@@ -668,7 +818,10 @@ void simLogDao_insertBatch_emptyList_isNoOp() throws SQLException {
assertEquals(sizeBefore, SimLogDao.findByRunId(runId).size());
}
- /** insertBatch() with a non-empty list persists all records. */
+ /** insertBatch() with a non-empty list persists all records.
+ *
+ * @throws SQLException if the insert fails
+ */
@Test
@Order(33)
void simLogDao_insertBatch_nonEmpty_insertsAll() throws SQLException {
@@ -682,7 +835,10 @@ void simLogDao_insertBatch_nonEmpty_insertsAll() throws SQLException {
assertEquals(2, tick2.size());
}
- /** findByRunId() orders results by tick ASC then id ASC. */
+ /** findByRunId() orders results by tick ASC then id ASC.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(34)
void simLogDao_findByRunId_orderedByTickThenId() throws SQLException {
@@ -701,7 +857,10 @@ void simLogDao_findByRunId_orderedByTickThenId() throws SQLException {
assertEquals(id3, logs.get(2).getId());
}
- /** findByRunIdAndTick() returns matching records; empty for unknown run. */
+ /** findByRunIdAndTick() returns matching records; empty for unknown run.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(35)
void simLogDao_findByRunIdAndTick_resultsAndEmpty() throws SQLException {
@@ -712,7 +871,10 @@ void simLogDao_findByRunIdAndTick_resultsAndEmpty() throws SQLException {
assertTrue(SimLogDao.findByRunIdAndTick(UUID.randomUUID(), 42).isEmpty());
}
- /** deleteByRunId() removes all logs for a run; returns 0 for missing run. */
+ /** deleteByRunId() removes all logs for a run; returns 0 for missing run.
+ *
+ * @throws SQLException if the delete fails
+ */
@Test
@Order(36)
void simLogDao_deleteByRunId_existingAndMissing() throws SQLException {
@@ -727,7 +889,10 @@ void simLogDao_deleteByRunId_existingAndMissing() throws SQLException {
// ── RobotRunStatsDao ──────────────────────────────────────────────────────
- /** upsert() on same (run_id, robot_id) always returns same id; last write wins. */
+ /** upsert() on same (run_id, robot_id) always returns same id; last write wins.
+ *
+ * @throws SQLException if the upsert fails
+ */
@Test
@Order(37)
void robotRunStatsDao_upsert_idStableAcrossMultipleUpdates() throws SQLException {
@@ -745,7 +910,10 @@ void robotRunStatsDao_upsert_idStableAcrossMultipleUpdates() throws SQLException
assertEquals(3, RobotRunStatsDao.findByRunIdAndRobotId(localRunId, ROBOT_ID_0).orElseThrow().getTasksCompleted());
}
- /** upsert() with all null numeric metrics stores NULLs. */
+ /** upsert() with all null numeric metrics stores NULLs.
+ *
+ * @throws SQLException if the upsert fails
+ */
@Test
@Order(38)
void robotRunStatsDao_upsert_withNullMetrics() throws SQLException {
@@ -775,7 +943,10 @@ void robotRunStatsDao_upsert_withNullMetrics() throws SQLException {
assertNull(f.getDeadlocks());
}
- /** findByRunId() returns all robots ordered by robot_id; empty for unknown run. */
+ /** findByRunId() returns all robots ordered by robot_id; empty for unknown run.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(39)
void robotRunStatsDao_findByRunId_orderedByRobotId_andEmptyForUnknown() throws SQLException {
@@ -796,7 +967,10 @@ void robotRunStatsDao_findByRunId_orderedByRobotId_andEmptyForUnknown() throws S
assertTrue(RobotRunStatsDao.findByRunId(UUID.randomUUID()).isEmpty());
}
- /** findByRunIdAndRobotId() returns empty for unknown run or non-existent robot. */
+ /** findByRunIdAndRobotId() returns empty for unknown run or non-existent robot.
+ *
+ * @throws SQLException if the find fails
+ */
@Test
@Order(40)
void robotRunStatsDao_findByRunIdAndRobotId_notFound() throws SQLException {
@@ -804,7 +978,10 @@ void robotRunStatsDao_findByRunIdAndRobotId_notFound() throws SQLException {
assertTrue(RobotRunStatsDao.findByRunIdAndRobotId(runId, UUID.randomUUID()).isEmpty());
}
- /** deleteByRunId() removes all stats for a run; returns 0 for missing run. */
+ /** deleteByRunId() removes all stats for a run; returns 0 for missing run.
+ *
+ * @throws SQLException if the delete fails
+ */
@Test
@Order(41)
void robotRunStatsDao_deleteByRunId_existingAndMissing() throws SQLException {
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/SimulationIntegrationTestSupport.java b/open-robotics/src/test/java/com/openrobotics/integration/SimulationIntegrationTestSupport.java
index 39c8e334..a11e0e56 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/SimulationIntegrationTestSupport.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/SimulationIntegrationTestSupport.java
@@ -10,30 +10,56 @@
import static org.junit.jupiter.api.Assertions.fail;
+/**
+ * Shared helper base for simulation integration tests.
+ *
+ * Provides concise fixture utilities for common position/robot setup and deterministic tick
+ * advancement patterns used across integration suites.
+ */
public abstract class SimulationIntegrationTestSupport {
+ /**
+ * Creates a position vector with integer tile coordinates.
+ */
protected Vector2D pos(int x, int y) {
return new Vector2D(x, y);
}
+ /**
+ * Creates a robot with greedy navigation and deterministic seed.
+ */
protected Robot greedyRobot(String name, int x, int y, long seed) {
Robot robot = new Robot(name, pos(x, y));
robot.setNav(new GreedyNavigationStrategy(seed));
return robot;
}
+ /**
+ * Creates an ID-stable robot with greedy navigation and deterministic seed.
+ */
protected Robot greedyRobot(UUID id, String name, int x, int y, long seed) {
Robot robot = new Robot(id, name, pos(x, y));
robot.setNav(new GreedyNavigationStrategy(seed));
return robot;
}
+ /**
+ * Advances simulation by a fixed number of ticks.
+ */
protected void runTicks(SimulationEngine engine, int count) {
for (int i = 0; i < count; i++) {
engine.tick();
}
}
+ /**
+ * Advances simulation until condition passes or tick budget is exhausted.
+ *
+ * @param engine engine to advance
+ * @param condition success predicate checked before each tick
+ * @param maxTicks maximum number of ticks to execute
+ * @param failureMessage assertion message used if condition never becomes true
+ */
protected void runTicksUntil(SimulationEngine engine, BooleanSupplier condition, int maxTicks, String failureMessage) {
for (int i = 0; i < maxTicks; i++) {
if (condition.getAsBoolean()) {
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripReservationKIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripReservationKIntegrationTest.java
index 3fb2e814..a3482385 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripReservationKIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripReservationKIntegrationTest.java
@@ -24,11 +24,30 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+/**
+ * Integration test for config round-trip fidelity with Reservation-K coordination.
+ *
+ * This test verifies a save → load → save sequence preserves simulation-level settings,
+ * coordination policy type/parameter, robot strategy metadata, and basic workload presence when
+ * serialized back into DTO form.
+ */
public class ConfigRoundTripReservationKIntegrationTest extends SimulationIntegrationTestSupport {
@TempDir
Path tempDir;
+ /**
+ * Verifies Reservation-K configuration survives a double serialization cycle.
+ *
+ * Scenario:
+ *
+ * Build an in-memory engine with Reservation-K policy, one robot, and one task.
+ * Save config to disk, reload engine from that file, then save again.
+ * Load final JSON into DTO and assert key fields remained stable.
+ *
+ *
+ * @throws Exception if save/load or reflective policy access fails
+ */
@Test
void saveLoadSaveRoundTripPreservesReservationKConfiguration() throws Exception {
Map map = new Map(5, 3);
@@ -92,6 +111,13 @@ void saveLoadSaveRoundTripPreservesReservationKConfiguration() throws Exception
assertEquals(1, loadedEngine.getDispatcher().getPendingTaskCount());
}
+ /**
+ * Reads the engine's private coordination policy field for runtime type assertions.
+ *
+ * @param engine loaded simulation engine instance
+ * @return current coordination policy object
+ * @throws ReflectiveOperationException if reflective lookup/access fails
+ */
private Object getCoordinationPolicy(SimulationEngine engine) throws ReflectiveOperationException {
Field field = SimulationEngine.class.getDeclaredField("coordinationPolicy");
field.setAccessible(true);
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripTrafficRulesIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripTrafficRulesIntegrationTest.java
index 3d34c37a..c48d92ba 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripTrafficRulesIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/config/ConfigRoundTripTrafficRulesIntegrationTest.java
@@ -32,11 +32,33 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Integration test for full config round-trip fidelity under Traffic Rules coordination.
+ *
+ * The scenario builds a heterogeneous simulation (multiple robots, stations, rack, obstacle,
+ * tasks, and occupied tiles), performs save → load → save, then asserts that both DTO-level data
+ * and runtime engine reconstructions preserve expected metadata and strategy types.
+ */
public class ConfigRoundTripTrafficRulesIntegrationTest extends SimulationIntegrationTestSupport {
@TempDir
Path tempDir;
+ /**
+ * Verifies save/load/save round-trip preserves map/entity/task metadata and Traffic Rules
+ * coordination intersections.
+ *
+ * Assertions cover:
+ *
+ * top-level config values (run name, tick/max ticks, seed),
+ * map dimensions and occupied tile flags,
+ * entity counts and robot strategy/sensor/state metadata,
+ * coordination type and intersection count, and
+ * reconstructed runtime engine object types and pending workload.
+ *
+ *
+ * @throws Exception if serialization/deserialization fails
+ */
@Test
void saveLoadSaveRoundTripPreservesEntitiesMetadataAndTrafficRules() throws Exception {
Map map = new Map(6, 4);
@@ -141,6 +163,9 @@ void saveLoadSaveRoundTripPreservesEntitiesMetadataAndTrafficRules() throws Exce
assertEquals(1, countEntitiesOfType(loadedEngine.getMap().getEntities(), Obstacle.class));
}
+ /**
+ * Returns whether a tile list contains an occupied tile at the given coordinates.
+ */
private boolean hasOccupiedTile(List tiles, int x, int y) {
if (tiles == null) {
return false;
@@ -148,6 +173,9 @@ private boolean hasOccupiedTile(List tiles, int x,
return tiles.stream().anyMatch(tile -> tile.x == x && tile.y == y && tile.isOccupied);
}
+ /**
+ * Finds a DTO robot by name, failing if no matching entry exists.
+ */
private SimulationConfigDTO.RobotDTO findRobot(List robots, String name) {
return robots.stream()
.filter(robot -> name.equals(robot.name))
@@ -155,6 +183,9 @@ private SimulationConfigDTO.RobotDTO findRobot(List entities, Class> type) {
return (int) entities.stream().filter(type::isInstance).count();
}
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/config/SimulationConfigNegativeIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/config/SimulationConfigNegativeIntegrationTest.java
index 2efc9aa7..9cf29ced 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/config/SimulationConfigNegativeIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/config/SimulationConfigNegativeIntegrationTest.java
@@ -16,11 +16,23 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Negative integration tests for configuration loading and simulation initialization.
+ *
+ * This suite validates failure behavior when configuration files are malformed, incomplete, or
+ * semantically invalid. Assertions focus on defensive initialization outcomes (captured init error,
+ * unusable engine state) and expected exceptions from loader APIs.
+ */
public class SimulationConfigNegativeIntegrationTest {
@TempDir
Path tempDir;
+ /**
+ * Verifies malformed JSON produces init error state and prevents engine ticking.
+ *
+ * @throws Exception if test file setup fails
+ */
@Test
void malformed_json_file_sets_init_error_and_engine_cannot_tick() throws Exception {
Path path = tempDir.resolve("malformed.json");
@@ -33,6 +45,12 @@ void malformed_json_file_sets_init_error_and_engine_cannot_tick() throws Excepti
assertThrows(IllegalStateException.class, engine::tick);
}
+ /**
+ * Verifies missing required top-level config section causes initialization failure and blocks
+ * simulation advancement.
+ *
+ * @throws Exception if test file setup fails
+ */
@Test
void missing_required_config_section_sets_init_error_and_engine_cannot_tick() throws Exception {
Path path = tempDir.resolve("missing-config.json");
@@ -53,6 +71,11 @@ void missing_required_config_section_sets_init_error_and_engine_cannot_tick() th
assertThrows(IllegalStateException.class, engine::tick);
}
+ /**
+ * Verifies invalid robot enum/state values in config surface an initialization error.
+ *
+ * @throws Exception if test file setup fails
+ */
@Test
void invalid_robot_state_in_config_sets_init_error() throws Exception {
Path path = tempDir.resolve("invalid-state.json");
@@ -89,6 +112,9 @@ void invalid_robot_state_in_config_sets_init_error() throws Exception {
assertTrue(engine.getInitError().contains("IllegalArgumentException"));
}
+ /**
+ * Verifies {@link ConfigLoader#load(String, Class)} throws for non-existent file paths.
+ */
@Test
void configLoader_load_throws_for_missing_file() {
Path missing = tempDir.resolve("missing-file.json");
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/db/SimulationPersistenceIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/db/SimulationPersistenceIntegrationTest.java
index 89b8c74e..1fcd7d10 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/db/SimulationPersistenceIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/db/SimulationPersistenceIntegrationTest.java
@@ -36,13 +36,26 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Integration tests for end-to-end simulation persistence into DAO-backed tables.
+ *
+ * This suite validates that completed simulation state can be materialized across map/run/task/
+ * log/stats/result DAOs and that deleting a run removes dependent persisted records.
+ */
public class SimulationPersistenceIntegrationTest extends SimulationIntegrationTestSupport {
+ /**
+ * Tracks map row created by the current test for dependency-safe cleanup.
+ */
private UUID createdMapId;
+ /**
+ * Tracks run row created by the current test for dependency-safe cleanup.
+ */
private UUID createdRunId;
/**
- * Seed AppState with a dummy SimulationEngine to satisfy logging code
+ * Seeds {@link AppState} with a minimal engine so code paths expecting an active engine (for
+ * example logging helpers) remain valid during integration tests.
*/
private void seedAppState() {
SimulationEngine dummyEngine = new SimulationEngine(
@@ -55,12 +68,21 @@ private void seedAppState() {
AppState.setEngine(dummyEngine);
}
+ /**
+ * Prepares shared state before each test and disables runtime logger side effects.
+ */
@BeforeEach
public void setUp() {
seedAppState();
Logger.setMode(LoggerMode.NO_OP); // disable logging during tests
}
+ /**
+ * Initializes database access and skips suite when UUID robot schema is not available.
+ *
+ * @throws IOException if DB bootstrap fails
+ * @throws SQLException if DB connectivity/migration checks fail
+ */
@BeforeAll
static void initDatabase() throws IOException, SQLException {
Database.init();
@@ -70,6 +92,11 @@ static void initDatabase() throws IOException, SQLException {
);
}
+ /**
+ * Cleans persisted rows created by tests in child-first dependency order.
+ *
+ * @throws SQLException if DAO cleanup operations fail
+ */
@AfterEach
void cleanup() throws SQLException {
if (createdRunId != null) {
@@ -87,6 +114,12 @@ void cleanup() throws SQLException {
}
}
+ /**
+ * Verifies a completed simulation snapshot can be persisted and queried consistently across all
+ * result-oriented DAOs.
+ *
+ * @throws Exception if simulation setup or DAO operations fail
+ */
@Test
void completed_simulation_state_persists_cleanly_across_all_result_daos() throws Exception {
Map map = new Map(5, 1);
@@ -147,6 +180,11 @@ void completed_simulation_state_persists_cleanly_across_all_result_daos() throws
assertEquals(0, result.getCollisions());
}
+ /**
+ * Verifies deleting a run removes associated persisted results, tasks, logs, and stats.
+ *
+ * @throws Exception if DAO setup or deletion fails
+ */
@Test
void deleting_a_simulation_run_cascades_persisted_results_logs_tasks_and_stats() throws Exception {
createdMapId = MapDao.insert(mapRecord("Cascade Map", new Map(2, 1)));
@@ -168,6 +206,9 @@ void deleting_a_simulation_run_cascades_persisted_results_logs_tasks_and_stats()
assertTrue(RobotRunStatsDao.findByRunId(deletedRunId).isEmpty());
}
+ /**
+ * Creates a minimal map record fixture from a runtime map instance.
+ */
private MapRecord mapRecord(String name, Map map) {
MapRecord record = new MapRecord();
record.setName(name);
@@ -178,6 +219,9 @@ private MapRecord mapRecord(String name, Map map) {
return record;
}
+ /**
+ * Creates a simulation-run record fixture linked to a map and optional runtime engine.
+ */
private SimulationRunRecord runRecord(UUID mapId, SimulationEngine engine) {
SimulationRunRecord record = new SimulationRunRecord();
record.setMapId(mapId);
@@ -191,6 +235,9 @@ private SimulationRunRecord runRecord(UUID mapId, SimulationEngine engine) {
return record;
}
+ /**
+ * Creates a persisted-task fixture from runtime task and optional robot/engine context.
+ */
private WorkloadTaskRecord workloadTaskRecord(UUID runId, Task task, Robot robot, SimulationEngine engine) {
WorkloadTaskRecord record = new WorkloadTaskRecord();
record.setRunId(runId);
@@ -209,6 +256,9 @@ private WorkloadTaskRecord workloadTaskRecord(UUID runId, Task task, Robot robot
return record;
}
+ /**
+ * Creates a simulation-log fixture row.
+ */
private SimLogRecord simLogRecord(UUID runId, int tick, UUID robotId, String eventType, Integer x, Integer y, String details) {
SimLogRecord record = new SimLogRecord();
record.setRunId(runId);
@@ -221,6 +271,9 @@ private SimLogRecord simLogRecord(UUID runId, int tick, UUID robotId, String eve
return record;
}
+ /**
+ * Creates robot-run stats fixture from runtime robot metrics.
+ */
private RobotRunStatsRecord robotStatsRecord(UUID runId, Robot robot) {
RobotRunStatsRecord record = new RobotRunStatsRecord();
record.setRunId(runId);
@@ -237,6 +290,9 @@ private RobotRunStatsRecord robotStatsRecord(UUID runId, Robot robot) {
return record;
}
+ /**
+ * Creates a run-result fixture from runtime simulation/robot metrics.
+ */
private RunResultRecord runResultRecord(UUID runId, SimulationEngine engine, Robot robot) {
RunResultRecord record = new RunResultRecord();
record.setRunId(runId);
@@ -254,6 +310,9 @@ private RunResultRecord runResultRecord(UUID runId, SimulationEngine engine, Rob
return record;
}
+ /**
+ * Creates a minimal run-result fixture with explicit scalar values.
+ */
private RunResultRecord runResultRecord(UUID runId, int completionTicks, double totalEnergy, double tasksPerMinute) {
RunResultRecord record = new RunResultRecord();
record.setRunId(runId);
@@ -263,6 +322,9 @@ private RunResultRecord runResultRecord(UUID runId, int completionTicks, double
return record;
}
+ /**
+ * Creates a deterministic robot fixture used for stats persistence tests.
+ */
private Robot statsRobot(UUID robotId) {
Robot robot = greedyRobot(robotId, "Persisted", 0, 0, 1L);
robot.setCurrentTask(null);
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/LongRunningSimulationRegressionTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/LongRunningSimulationRegressionTest.java
index b4dd327a..fde9613b 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/LongRunningSimulationRegressionTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/simulation/LongRunningSimulationRegressionTest.java
@@ -20,8 +20,22 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Regression-style integration tests for longer simulation runs.
+ *
+ * This suite targets stability properties that can regress over extended tick sequences:
+ * multi-robot task completion, tile exclusivity between robots, bounded movement, and successful
+ * completion of battery-constrained workloads that require charging behavior.
+ */
public class LongRunningSimulationRegressionTest extends SimulationIntegrationTestSupport {
+ /**
+ * Verifies two robots can complete a multi-task queue over many ticks without ever occupying
+ * the same map tile at the same time.
+ *
+ * The test also asserts task completion outcomes, dispatcher depletion, and final robot
+ * availability/state consistency.
+ */
@Test
void multiple_robots_complete_a_longer_queue_without_ever_sharing_a_tile() {
Map map = new Map(6, 3);
@@ -65,6 +79,12 @@ void multiple_robots_complete_a_longer_queue_without_ever_sharing_a_tile() {
assertTrue(robotB.isAvailable());
}
+ /**
+ * Verifies a low-battery robot can finish a longer sequence by charging as needed.
+ *
+ * Asserts end-state task completion, positive charging activity, valid battery level,
+ * and that the engine no longer advances ticks once workload is fully complete.
+ */
@Test
void long_running_sequence_with_charging_eventually_completes_all_tasks() {
Map map = new Map(7, 2);
@@ -106,6 +126,9 @@ void long_running_sequence_with_charging_eventually_completes_all_tasks() {
assertEquals(completedTick, engine.getTickCounter());
}
+ /**
+ * Asserts all provided robots remain on valid map tiles and on distinct positions.
+ */
private void assertDistinctAndInBounds(Map map, Robot... robots) {
Set positions = new HashSet<>();
for (Robot robot : robots) {
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/ReservationKIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/ReservationKIntegrationTest.java
index 875e0057..9a46d8d0 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/ReservationKIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/simulation/ReservationKIntegrationTest.java
@@ -1,6 +1,9 @@
package com.openrobotics.integration.simulation;
+import com.openrobotics.AppState;
import com.openrobotics.integration.SimulationIntegrationTestSupport;
+import com.openrobotics.logging.Logger;
+import com.openrobotics.logging.LoggerMode;
import com.openrobotics.map.Map;
import com.openrobotics.robot.Robot;
import com.openrobotics.robot.RobotState;
@@ -14,8 +17,21 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+/**
+ * Integration test for temporal spacing behavior under {@link ReservationKPolicy}.
+ *
+ * This scenario validates that a trailing robot yields to a lead robot in a one-tile corridor
+ * when the lead robot has reserved a forward window of tiles.
+ */
public class ReservationKIntegrationTest extends SimulationIntegrationTestSupport {
+ /**
+ * Verifies Reservation-K enforces a wait for the trailing robot until the lead robot advances
+ * beyond the reserved horizon.
+ *
+ * The test uses deterministic UUID ordering because policy internals process robots in UUID
+ * order; random IDs would make movement ordering and assertions flaky.
+ */
@Test
void reservationKMakesTrailingRobotWaitForLeadRobotsWindow() {
Map map = new Map(5, 1);
@@ -38,13 +54,20 @@ void reservationKMakesTrailingRobotWaitForLeadRobotsWindow() {
map.addEntity(leadRobot);
map.addEntity(trailingRobot);
+ Dispatcher dispatcher = new Dispatcher();
+ dispatcher.addTask(trailingTask); // adding task so ticking doesn't fail
+
SimulationEngine engine = new SimulationEngine(
map,
new Robot[]{leadRobot, trailingRobot},
- new Dispatcher(),
+ dispatcher,
new ReservationKPolicy(2)
);
+ // Setting global state for logging
+ AppState.setEngine(engine);
+ Logger.setMode(LoggerMode.NO_OP);
+
engine.tick();
assertEquals(pos(2, 0), leadRobot.getPosition());
assertEquals(pos(0, 0), trailingRobot.getPosition());
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationChargingIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationChargingIntegrationTest.java
index 462e75db..17569224 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationChargingIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationChargingIntegrationTest.java
@@ -8,7 +8,6 @@
import com.openrobotics.map.entities.station.ChargingStation;
import com.openrobotics.robot.Robot;
import com.openrobotics.robot.RobotState;
-import com.openrobotics.simulationcore.CollisionManager;
import com.openrobotics.simulationcore.CoordinationPolicy;
import com.openrobotics.simulationcore.Dispatcher;
import com.openrobotics.simulationcore.SimulationEngine;
@@ -23,7 +22,8 @@
public class SimulationChargingIntegrationTest extends SimulationIntegrationTestSupport {
/**
- * Seed AppState with a dummy SimulationEngine to satisfy logging code
+ * Seeds {@link AppState} with a minimal engine so logging-dependent code paths remain valid
+ * during isolated simulation integration tests.
*/
private void seedAppState() {
SimulationEngine dummyEngine = new SimulationEngine(
@@ -36,12 +36,23 @@ private void seedAppState() {
AppState.setEngine(dummyEngine);
}
+ /**
+ * Initializes per-test state and disables logger side effects for deterministic assertions.
+ */
@BeforeEach
public void setUp() {
seedAppState();
Logger.setMode(LoggerMode.NO_OP); // disable logging during tests
}
+ /**
+ * Verifies a low-battery robot first diverts to charge, fully recharges, then resumes
+ * progress on its current task.
+ *
+ * The test asserts intermediate charging behavior (position/state/battery deltas), full
+ * recharge transition back to moving state, and eventual departure from the charging tile while
+ * preserving task assignment.
+ */
@Test
void lowBatteryRobotDivertsToChargerBeforeResumingTask() {
Map map = new Map(6, 2);
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationDispatcherIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationDispatcherIntegrationTest.java
index fa44b7a6..deb6e9e7 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationDispatcherIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationDispatcherIntegrationTest.java
@@ -13,8 +13,21 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
+/**
+ * Integration test for dispatcher-driven task assignment across simulation ticks.
+ *
+ * This suite verifies priority ordering and wave-based assignment behavior when multiple robots
+ * consume queued tasks over time.
+ */
public class SimulationDispatcherIntegrationTest extends SimulationIntegrationTestSupport {
+ /**
+ * Verifies dispatcher assigns highest-priority tasks first to available robots, then feeds
+ * remaining lower-priority work in a later wave after completions.
+ *
+ * The assertions track task status transitions, pending-queue counts, and robot completion
+ * counters to ensure assignment and progression remain consistent across ticks.
+ */
@Test
void dispatcherFeedsHighestPriorityTasksAcrossMultipleCompletionWaves() {
Map map = new Map(6, 2);
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationIdleLoopIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationIdleLoopIntegrationTest.java
deleted file mode 100644
index 484b8958..00000000
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationIdleLoopIntegrationTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.openrobotics.integration.simulation;
-
-import com.openrobotics.integration.SimulationIntegrationTestSupport;
-import com.openrobotics.map.Map;
-import com.openrobotics.robot.Robot;
-import com.openrobotics.robot.RobotState;
-import com.openrobotics.simulationcore.CoordinationPolicy;
-import com.openrobotics.simulationcore.Dispatcher;
-import com.openrobotics.simulationcore.SimulationEngine;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class SimulationIdleLoopIntegrationTest extends SimulationIntegrationTestSupport {
-
- @Test
- void noConfiguredTasksKeepsSimulationTickingAndRobotIdle() {
- Map map = new Map(3, 3);
- Robot robot = greedyRobot("IdleBot", 1, 1, 99L);
- map.addEntity(robot);
-
- Dispatcher dispatcher = new Dispatcher();
- SimulationEngine engine = new SimulationEngine(
- map,
- new Robot[]{robot},
- dispatcher,
- CoordinationPolicy.noOp()
- );
-
- runTicks(engine, 3);
-
- assertEquals(pos(1, 1), robot.getPosition());
- assertEquals(RobotState.IDLE, robot.getState());
- assertNull(robot.getCurrentTask());
- assertTrue(robot.isAvailable());
- assertEquals(3, robot.getTotalIdleTicks());
- assertEquals(0, dispatcher.getTotalTasksAdded());
- assertEquals(3, engine.getTickCounter());
- }
-}
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationTaskFlowIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationTaskFlowIntegrationTest.java
index aa9f7aed..1908d23d 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationTaskFlowIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/simulation/SimulationTaskFlowIntegrationTest.java
@@ -19,10 +19,17 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Integration test for core single-robot task execution flow.
+ *
+ * This suite verifies that a task progresses through dispatcher assignment, robot pickup/dropoff
+ * movement, completion bookkeeping, and stable post-completion engine behavior.
+ */
public class SimulationTaskFlowIntegrationTest extends SimulationIntegrationTestSupport {
/**
- * Seed AppState with a dummy SimulationEngine to satisfy logging code
+ * Seeds {@link AppState} with a minimal engine so logging-dependent code paths are satisfied
+ * during integration tests.
*/
private void seedAppState() {
SimulationEngine dummyEngine = new SimulationEngine(
@@ -35,12 +42,21 @@ private void seedAppState() {
AppState.setEngine(dummyEngine);
}
+ /**
+ * Initializes per-test state and disables logger side effects for deterministic assertions.
+ */
@BeforeEach
public void setUp() {
seedAppState();
Logger.setMode(LoggerMode.NO_OP); // disable logging during tests
}
+ /**
+ * Verifies a single robot completes one task end-to-end and then remains stable in idle state.
+ *
+ * Asserts include task status, robot position/state/metrics, dispatcher depletion, and that
+ * an extra tick after completion does not advance simulation time.
+ */
@Test
void singleRobotTaskCompletesThroughFullEnginePipeline() {
Map map = new Map(5, 1);
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/simulation/TrafficRulesIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/simulation/TrafficRulesIntegrationTest.java
index 6c6294db..d61ae5c8 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/simulation/TrafficRulesIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/simulation/TrafficRulesIntegrationTest.java
@@ -22,10 +22,17 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
+/**
+ * Integration test for intersection exclusivity under {@link TrafficRulesPolicy}.
+ *
+ * This suite verifies that only one robot occupies/advances through constrained intersection
+ * tiles at a time, while trailing robots wait and resume once the intersection clears.
+ */
public class TrafficRulesIntegrationTest extends SimulationIntegrationTestSupport {
/**
- * Seed AppState with a dummy SimulationEngine to satisfy logging code
+ * Seeds {@link AppState} with a minimal engine so logging-dependent paths remain valid during
+ * simulation integration tests.
*/
private void seedAppState() {
SimulationEngine dummyEngine = new SimulationEngine(
@@ -38,12 +45,21 @@ private void seedAppState() {
AppState.setEngine(dummyEngine);
}
+ /**
+ * Initializes per-test state and disables logger side effects for deterministic assertions.
+ */
@BeforeEach
public void setUp() {
seedAppState();
Logger.setMode(LoggerMode.NO_OP); // disable logging during tests
}
+ /**
+ * Verifies traffic-rules policy enforces exclusive traversal through configured intersections.
+ *
+ * The test advances several ticks and checks that robot A clears the intersection before
+ * robot B is allowed to enter, while task ownership/state transitions remain coherent.
+ */
@Test
void trafficRulesKeepsIntersectionExclusiveAcrossTicks() {
Map map = new Map(4, 4);
@@ -81,10 +97,13 @@ void trafficRulesKeepsIntersectionExclusiveAcrossTicks() {
map.getTile(2, 1)
));
+ Dispatcher dispatcher = new Dispatcher();
+ dispatcher.addTask(taskA); // adding task so ticking doesn't fail
+
SimulationEngine engine = new SimulationEngine(
map,
new Robot[]{robotA, robotB},
- new Dispatcher(),
+ dispatcher,
policy
);
diff --git a/open-robotics/src/test/java/com/openrobotics/integration/ui/NavigationFlowIntegrationTest.java b/open-robotics/src/test/java/com/openrobotics/integration/ui/NavigationFlowIntegrationTest.java
index 6b81ed7c..8d55d67f 100644
--- a/open-robotics/src/test/java/com/openrobotics/integration/ui/NavigationFlowIntegrationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/integration/ui/NavigationFlowIntegrationTest.java
@@ -30,8 +30,21 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * End-to-end UI navigation integration tests across welcome, setup, simulation, and results
+ * screens.
+ *
+ * This suite seeds deterministic application state, launches from the welcome view, and asserts
+ * that primary navigation actions preserve expected screen content and simulation summary data.
+ */
public class NavigationFlowIntegrationTest extends ApplicationTest {
+ /**
+ * Boots the UI flow at the welcome screen after seeding AppState.
+ *
+ * @param stage JavaFX stage provided by TestFX
+ * @throws Exception if FXML loading fails
+ */
@Override
public void start(Stage stage) throws Exception {
seedAppState();
@@ -43,6 +56,14 @@ public void start(Stage stage) throws Exception {
stage.show();
}
+ /**
+ * Verifies the main onboarding path from Welcome → Setup → Simulation.
+ *
+ * Asserts setup defaults and confirms simulation screen initialization output after starting
+ * from setup.
+ *
+ * @throws TimeoutException if simulation screen does not appear in time
+ */
@Test
void welcomeToSetupToSimulationFlowWorks() throws TimeoutException {
fireButton("#welcomeStartSetupButton");
@@ -58,6 +79,14 @@ void welcomeToSetupToSimulationFlowWorks() throws TimeoutException {
assertEquals("TICK 0", lookup("#tickDisplayLabel").queryAs(Label.class).getText());
}
+ /**
+ * Verifies navigation from Simulation → Results → back to Simulation editor.
+ *
+ * Asserts results-table content before returning, then checks simulation tick label after
+ * navigating back.
+ *
+ * @throws TimeoutException if target screens do not appear in time
+ */
@Test
void simulationResultsAndBackToEditorFlowWorks() throws TimeoutException {
fireButton("#welcomeStartSetupButton");
@@ -77,6 +106,9 @@ void simulationResultsAndBackToEditorFlowWorks() throws TimeoutException {
assertEquals("TICK 0", lookup("#tickDisplayLabel").queryAs(Label.class).getText());
}
+ /**
+ * Seeds a minimal deterministic simulation state used by navigation-flow assertions.
+ */
private void seedAppState() {
Map map = new Map(5, 3);
Robot robot = new Robot("UiBot", new Vector2D(0, 0));
@@ -104,11 +136,21 @@ private void fireButton(String query) {
WaitForAsyncUtils.waitForFxEvents();
}
+ /**
+ * Waits until simulation screen controls are present.
+ *
+ * @throws TimeoutException if simulation screen is not visible within timeout
+ */
private void waitForSimulationScreen() throws TimeoutException {
WaitForAsyncUtils.waitFor(15, TimeUnit.SECONDS,
() -> lookup("#consoleArea").tryQuery().isPresent());
}
+ /**
+ * Waits until results screen controls are present.
+ *
+ * @throws TimeoutException if results screen is not visible within timeout
+ */
private void waitForResultsScreen() throws TimeoutException {
WaitForAsyncUtils.waitFor(15, TimeUnit.SECONDS,
() -> lookup("#robotStatsTable").tryQuery().isPresent());
diff --git a/open-robotics/src/test/java/com/openrobotics/logging/LoggerTest.java b/open-robotics/src/test/java/com/openrobotics/logging/LoggerTest.java
index 82cdad19..6b0cfe9a 100644
--- a/open-robotics/src/test/java/com/openrobotics/logging/LoggerTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/logging/LoggerTest.java
@@ -5,7 +5,6 @@
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 org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -26,21 +25,38 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Unit tests for {@link Logger}.
+ *
+ * This consolidated suite validates logger mode-gating semantics, task/simulation event
+ * exception handling behavior, robot-event queueing and flushing paths, and private/static internal
+ * state interactions used by the logger's asynchronous batch strategy.
+ */
class LoggerTest {
+ /**
+ * Resets logger mode and queue to a deterministic baseline before each test.
+ */
@BeforeEach
void setUp() throws Exception {
Logger.setMode(LoggerMode.NO_OP);
robotQueue().clear();
}
+ /**
+ * Restores logger mode and queue after each test to prevent cross-test interference.
+ */
@AfterEach
void tearDown() throws Exception {
Logger.setMode(LoggerMode.NO_OP);
robotQueue().clear();
}
+ /**
+ * Verifies the private constructor can be accessed reflectively for branch coverage.
+ */
@Test
void constructor_is_private_but_reflection_can_instantiate_for_coverage() throws Exception {
Constructor constructor = Logger.class.getDeclaredConstructor();
@@ -51,6 +67,9 @@ void constructor_is_private_but_reflection_can_instantiate_for_coverage() throws
assertNotNull(constructor.newInstance());
}
+ /**
+ * Verifies {@link Logger#setMode(LoggerMode)} updates the static mode field.
+ */
@Test
void setMode_updates_static_logger_mode() throws Exception {
Logger.setMode(LoggerMode.DB);
@@ -60,22 +79,9 @@ void setMode_updates_static_logger_mode() throws Exception {
assertSame(LoggerMode.NO_OP, currentMode());
}
- @Test
- void noOpMode_ignores_task_events_and_returns_minus_one_without_touching_record() {
- WorkloadTaskRecord record = workloadRecord();
-
- long result = Logger.logTaskEvent(TaskEvent.TASK_CREATED, record);
-
- assertEquals(-1, result);
- }
-
- @Test
- void noOpMode_ignores_null_task_event_and_null_record_safely() {
- long result = assertDoesNotThrow(() -> Logger.logTaskEvent(null, null));
-
- assertEquals(-1, result);
- }
-
+ /**
+ * Verifies NO_OP mode suppresses simulation-run logging across normal and null inputs.
+ */
@Test
void noOpMode_ignores_simulation_run_events_including_nulls() {
assertAll(
@@ -84,6 +90,9 @@ void noOpMode_ignores_simulation_run_events_including_nulls() {
);
}
+ /**
+ * Verifies NO_OP mode suppresses robot-event queueing entirely.
+ */
@Test
void noOpMode_ignores_robot_events_and_does_not_enqueue_records() throws Exception {
SimLogRecord record = simLogRecord();
@@ -94,62 +103,43 @@ void noOpMode_ignores_robot_events_and_does_not_enqueue_records() throws Excepti
assertTrue(robotQueue().isEmpty());
}
+ /**
+ * Verifies DB mode simulation-run logging catches null event type failures.
+ */
@Test
- void dbMode_taskEventWithNullEvent_isCaughtAndReported() {
+ void dbMode_simulationRunEventWithNullEvent_isCaughtAndReported() {
Logger.setMode(LoggerMode.DB);
- CapturedErr captured = captureErr(() -> {
- long result = Logger.logTaskEvent(null, workloadRecord());
- assertEquals(-1, result);
- });
+ CapturedErr captured = captureErr(() ->
+ Logger.logSimulationRunEvent(null, simulationRecord()));
assertAll(
- () -> assertTrue(captured.text().contains("Failed to log task event of type: null")),
+ () -> assertTrue(captured.text().contains("Failed to log simulation run event of type: null")),
() -> assertTrue(captured.text().contains("Exception message:"))
);
}
+ /**
+ * Verifies RUN_STARTED path catches null record failures without propagating exceptions.
+ */
@Test
- void dbMode_taskAssignedWithMissingId_isCaughtBeforeDatabaseCall() {
+ void dbMode_simulationRunEventRunStartedWithNullRecord_isCaught() {
Logger.setMode(LoggerMode.DB);
- WorkloadTaskRecord record = workloadRecord();
- record.setId(null);
-
- CapturedErr captured = captureErr(() -> {
- long result = Logger.logTaskEvent(TaskEvent.TASK_ASSIGNED, record);
- assertEquals(-1, result);
- });
-
- assertTrue(captured.text().contains("Failed to log task event of type: TASK_ASSIGNED"));
+ assertDoesNotThrow(() -> Logger.logSimulationRunEvent(SimulationRunEvent.RUN_STARTED, null));
}
+ /**
+ * Verifies RUN_COMPLETED path catches null record failures without propagating exceptions.
+ */
@Test
- void dbMode_taskCompletedWithMissingId_isCaughtBeforeDatabaseCall() {
+ void dbMode_simulationRunEventRunCompletedWithNullRecord_isCaught() {
Logger.setMode(LoggerMode.DB);
- WorkloadTaskRecord record = workloadRecord();
- record.setId(null);
-
- CapturedErr captured = captureErr(() -> {
- long result = Logger.logTaskEvent(TaskEvent.TASK_COMPLETED, record);
- assertEquals(-1, result);
- });
-
- assertTrue(captured.text().contains("Failed to log task event of type: TASK_COMPLETED"));
- }
-
- @Test
- void dbMode_simulationRunEventWithNullEvent_isCaughtAndReported() {
- Logger.setMode(LoggerMode.DB);
-
- CapturedErr captured = captureErr(() ->
- Logger.logSimulationRunEvent(null, simulationRecord()));
-
- assertAll(
- () -> assertTrue(captured.text().contains("Failed to log simulation run event of type: null")),
- () -> assertTrue(captured.text().contains("Exception message:"))
- );
+ assertDoesNotThrow(() -> Logger.logSimulationRunEvent(SimulationRunEvent.RUN_COMPLETED, null));
}
+ /**
+ * Verifies RUN_FAILED branch remains non-throwing (currently no-op branch).
+ */
@Test
void dbMode_runFailedBranch_isCurrentlyNoOpAndDoesNotThrow() {
Logger.setMode(LoggerMode.DB);
@@ -158,6 +148,9 @@ void dbMode_runFailedBranch_isCurrentlyNoOpAndDoesNotThrow() {
Logger.logSimulationRunEvent(SimulationRunEvent.RUN_FAILED, simulationRecord()));
}
+ /**
+ * Verifies DB mode robot events are enqueued for async batch flushing.
+ */
@Test
void dbMode_robotEvents_areQueuedForBackgroundBatchFlush() throws Exception {
Logger.setMode(LoggerMode.DB);
@@ -176,6 +169,9 @@ void dbMode_robotEvents_areQueuedForBackgroundBatchFlush() throws Exception {
);
}
+ /**
+ * Verifies current queueing behavior ignores robot event type (including null values).
+ */
@Test
void dbMode_robotEventTypeIsNotUsedByCurrentQueueingImplementation() throws Exception {
Logger.setMode(LoggerMode.DB);
@@ -189,6 +185,9 @@ void dbMode_robotEventTypeIsNotUsedByCurrentQueueingImplementation() throws Exce
);
}
+ /**
+ * Verifies queue rejects null robot records in DB mode.
+ */
@Test
void dbMode_robotEventWithNullRecordThrowsBecauseQueueRejectsNulls() {
Logger.setMode(LoggerMode.DB);
@@ -197,30 +196,77 @@ void dbMode_robotEventWithNullRecordThrowsBecauseQueueRejectsNulls() {
Logger.logRobotEvent(RobotEvent.MOVE_EXECUTED, null));
}
+ /**
+ * Verifies explicit flush handles empty queue without side effects.
+ */
@Test
- void settingNullModeMakesTaskLoggingFailSafelyButRobotLoggingStillQueues() throws Exception {
- Logger.setMode(null);
+ void flushRobotEvents_withEmptyQueue_doesNotThrow() throws Exception {
+ Logger.setMode(LoggerMode.DB);
+ robotQueue().clear();
+
+ assertDoesNotThrow(Logger::flushRobotEvents);
+ assertTrue(robotQueue().isEmpty());
+ }
+
+ /**
+ * Verifies explicit flush drains pending queue entries even if DB write fails.
+ */
+ @Test
+ void flushRobotEvents_withPendingEntries_drainsQueue() throws Exception {
+ Logger.setMode(LoggerMode.DB);
SimLogRecord record = simLogRecord();
- long[] result = new long[1];
- CapturedErr captured = captureErr(() ->
- result[0] = Logger.logTaskEvent(TaskEvent.TASK_ASSIGNED, workloadRecord()));
- Logger.logRobotEvent(RobotEvent.NEAR_MISS, record);
+ Logger.logRobotEvent(RobotEvent.CHARGE_START, record);
+ assertEquals(1, robotQueue().size());
- assertAll(
- () -> assertEquals(-1, result[0]),
- () -> assertTrue(captured.text().contains("Failed to log task event of type: TASK_ASSIGNED")),
- () -> assertEquals(1, robotQueue().size()),
- () -> assertSame(record, robotQueue().peek())
- );
+ assertDoesNotThrow(Logger::flushRobotEvents);
+ assertTrue(robotQueue().isEmpty());
+ }
+
+ /**
+ * Verifies background worker catches batch-flush exceptions and emits stderr message.
+ */
+ @Test
+ void backgroundWorker_nonEmptyBatch_exceptionIsCaughtAndLogged() {
+ Logger.setMode(LoggerMode.DB);
+ ByteArrayOutputStream errCapture = new ByteArrayOutputStream();
+ PrintStream originalErr = System.err;
+ System.setErr(new PrintStream(errCapture));
+ try {
+ Logger.logRobotEvent(RobotEvent.MOVE_EXECUTED, simLogRecord());
+
+ long deadline = System.currentTimeMillis() + 3000;
+ while (System.currentTimeMillis() < deadline) {
+ if (errCapture.toString().contains("Failed to flush robot event batch:")) {
+ break;
+ }
+ Thread.sleep(50);
+ }
+
+ assertTrue(
+ errCapture.toString().contains("Failed to flush robot event batch:"),
+ "Expected worker catch block to log batch flush failure"
+ );
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ fail("Interrupted while waiting for background logger worker");
+ } finally {
+ System.setErr(originalErr);
+ }
}
+ /**
+ * Reads logger static mode field via reflection.
+ */
private LoggerMode currentMode() throws Exception {
Field mode = Logger.class.getDeclaredField("mode");
mode.setAccessible(true);
return (LoggerMode) mode.get(null);
}
+ /**
+ * Reads logger robot-event queue via reflection.
+ */
@SuppressWarnings("unchecked")
private BlockingQueue robotQueue() throws Exception {
Field queue = Logger.class.getDeclaredField("robotEventQueue");
@@ -228,6 +274,9 @@ private BlockingQueue robotQueue() throws Exception {
return (BlockingQueue) queue.get(null);
}
+ /**
+ * Builds a representative workload-task record fixture.
+ */
private WorkloadTaskRecord workloadRecord() {
WorkloadTaskRecord record = new WorkloadTaskRecord();
record.setId(42L);
@@ -247,6 +296,9 @@ private WorkloadTaskRecord workloadRecord() {
return record;
}
+ /**
+ * Builds a representative simulation-run record fixture.
+ */
private SimulationRunRecord simulationRecord() {
SimulationRunRecord record = new SimulationRunRecord();
record.setId(UUID.randomUUID());
@@ -263,6 +315,9 @@ private SimulationRunRecord simulationRecord() {
return record;
}
+ /**
+ * Builds a representative simulation-log record fixture.
+ */
private SimLogRecord simLogRecord() {
SimLogRecord record = new SimLogRecord();
record.setId(7L);
@@ -276,6 +331,9 @@ private SimLogRecord simLogRecord() {
return record;
}
+ /**
+ * Captures stderr output produced while executing the provided action.
+ */
private CapturedErr captureErr(Runnable action) {
PrintStream originalErr = System.err;
ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -288,5 +346,8 @@ private CapturedErr captureErr(Runnable action) {
return new CapturedErr(output.toString());
}
+ /**
+ * Simple stderr capture payload.
+ */
private record CapturedErr(String text) { }
}
diff --git a/open-robotics/src/test/java/com/openrobotics/map/entities/environment/RackTest.java b/open-robotics/src/test/java/com/openrobotics/map/entities/environment/RackTest.java
index a6b1c61a..77c49794 100644
--- a/open-robotics/src/test/java/com/openrobotics/map/entities/environment/RackTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/map/entities/environment/RackTest.java
@@ -8,8 +8,17 @@
import static org.junit.jupiter.api.Assertions.*;
+/**
+ * Unit tests for {@link Rack} state-management behavior.
+ *
+ * This suite verifies defaults and invariants around manual dropoff assignment configuration,
+ * persistence of valid dropoff identifiers, and minimum constraints on rack box count.
+ */
class RackTest {
+ /**
+ * Verifies newly constructed racks default to random (non-manual) dropoff assignment mode.
+ */
@Test
void newRackDefaultsToRandomAssignment() {
Rack rack = new Rack("r1", new Vector2D(0, 0));
@@ -17,6 +26,9 @@ void newRackDefaultsToRandomAssignment() {
"New racks must default to random (non-manual) mode");
}
+ /**
+ * Verifies manual assignment flag can be toggled on/off and retains assigned value.
+ */
@Test
void manualAssignmentFlagIsPersistent() {
Rack rack = new Rack("r1", new Vector2D(0, 0));
@@ -26,6 +38,9 @@ void manualAssignmentFlagIsPersistent() {
assertFalse(rack.isManualDropoffAssignment());
}
+ /**
+ * Verifies disabling manual mode does not clear previously configured valid dropoff IDs.
+ */
@Test
void togglingManualOffPreservesValidDropoffIds() {
Rack rack = new Rack("r1", new Vector2D(0, 0));
@@ -37,12 +52,15 @@ void togglingManualOffPreservesValidDropoffIds() {
"Toggling manual mode must not clear the existing pool");
}
+ /**
+ * Verifies box count setter clamps invalid low values to minimum supported count of zero.
+ */
@Test
- void boxCountSetterEnforcesMinimumOfOne() {
+ void boxCountSetterEnforcesMinimumOfZero() {
Rack rack = new Rack("r1", new Vector2D(0, 0));
rack.setBoxCount(0);
- assertEquals(1, rack.getBoxCount());
+ assertEquals(0, rack.getBoxCount());
rack.setBoxCount(-5);
- assertEquals(1, rack.getBoxCount());
+ assertEquals(0, rack.getBoxCount());
}
}
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/AlgorithmTypeTest.java b/open-robotics/src/test/java/com/openrobotics/robot/AlgorithmTypeTest.java
index 23412030..630b3312 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/AlgorithmTypeTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/AlgorithmTypeTest.java
@@ -15,8 +15,6 @@
*/
public class AlgorithmTypeTest {
- // ── Canonical names ───────────────────────────────────────────────────────
-
/**
* "GREEDY" maps to {@link AlgorithmType#GREEDY}.
*/
@@ -49,8 +47,6 @@ public void testRandomCanonical() {
assertEquals(AlgorithmType.RANDOM, AlgorithmType.fromConfigString("RANDOM"));
}
- // ── Class-name aliases ────────────────────────────────────────────────────
-
/**
* "GREEDYNAVIGATIONSTRATEGY" maps to {@link AlgorithmType#GREEDY}.
*/
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/RobotAdvancedTest.java b/open-robotics/src/test/java/com/openrobotics/robot/RobotAdvancedTest.java
index 67205e26..4f25dcdd 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/RobotAdvancedTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/RobotAdvancedTest.java
@@ -6,6 +6,7 @@
import com.openrobotics.map.Map;
import com.openrobotics.map.Vector2D;
import com.openrobotics.map.entities.station.ChargingStation;
+import com.openrobotics.robot.navigation.GreedyNavigationStrategy;
import com.openrobotics.robot.sensors.Sensor;
import com.openrobotics.robot.sensors.SensorStrategy;
import com.openrobotics.simulationcore.CoordinationPolicy;
@@ -276,10 +277,7 @@ public void testMovingConsumesBatteryWhenActuallyMoved() {
// Simulate a previous position different from the current one
// by calling getNextMove() which saves previousPosition, then
// manually update position to simulate movement.
- // We use the public API: set previousPosition via getNextMove on a
- // minimal map, then move the robot.
- // Arrange: call getNextMove to save previousPosition as (5,5)
@SuppressWarnings("unused")
MoveIntention intention = robot.getNextMove(map);
// Now move the robot to (6,5)
@@ -474,17 +472,20 @@ public void testLowBatteryOnChargingStationTransitionsToCharging() {
}
/**
- * If no charging station exists, a low-battery robot gives up moving and becomes IDLE.
+ * If no charging station exists, a low-battery robot continues moving towards its task,
+ * consuming energy as normal (and risking death if battery depletes).
*/
@Test
- public void testLowBatteryWithoutChargingStationBecomesIdle() {
+ public void testLowBatteryWithoutChargingStationContinuesMoving() {
robot.setBattery(10.0f);
robot.setCurrentTask(new Task(1, new Vector2D(8, 8), new Vector2D(9, 9), 1));
robot.setState(RobotState.MOVING);
MoveIntention intention = robot.getNextMove(map);
- assertEquals(RobotState.IDLE, robot.getState());
+ assertEquals(RobotState.MOVING, robot.getState());
+ assertEquals(10.0f, robot.getBattery(), 0.001f,
+ "Low-battery robot without charging station should not consume energy when getNextMove() is called with a null navigation strategy");
assertSame(intention.getFromTile(), intention.getToTile());
}
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/RobotTest.java b/open-robotics/src/test/java/com/openrobotics/robot/RobotTest.java
index 0bb5af38..23b7b746 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/RobotTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/RobotTest.java
@@ -4,14 +4,27 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
+/**
+ * Core unit tests for baseline {@link Robot} behavior.
+ *
+ * This suite validates constructor defaults, inherited entity identity/position fields, battery
+ * consumption invariants, charging-threshold checks, availability rules, and explicit state/status
+ * transitions.
+ */
public class RobotTest {
+ /**
+ * Verifies a newly constructed robot reports the default IDLE status label.
+ */
@Test
public void testRobotInitialStatus() {
Robot robot = new Robot("BOT-001", new Vector2D(0, 0));
assertEquals("IDLE", robot.getStatus(), "New robots should start in Idle state.");
}
+ /**
+ * Verifies robot instances expose inherited {@code MapEntity}-level fields correctly.
+ */
@Test
public void testRobotInheritsMapEntity() {
Robot robot = new Robot("BOT-002", new Vector2D(5, 10));
@@ -21,6 +34,9 @@ public void testRobotInheritsMapEntity() {
assertNotNull(robot.getId());
}
+ /**
+ * Verifies battery value decreases by the consumed energy amount.
+ */
@Test
public void testRobotBattery() {
Robot robot = new Robot("BOT-003", new Vector2D(0, 0));
@@ -29,6 +45,9 @@ public void testRobotBattery() {
assertEquals(74.5f, robot.getBattery(), 0.01f);
}
+ /**
+ * Verifies battery consumption is clamped at zero (never negative).
+ */
@Test
public void testRobotBatteryCannotGoNegative() {
Robot robot = new Robot("BOT-004", new Vector2D(0, 0));
@@ -36,6 +55,9 @@ public void testRobotBatteryCannotGoNegative() {
assertEquals(0.0f, robot.getBattery());
}
+ /**
+ * Verifies charging-threshold check flips once battery drops below threshold.
+ */
@Test
public void testRobotNeedsCharging() {
Robot robot = new Robot("BOT-005", new Vector2D(0, 0));
@@ -44,6 +66,9 @@ public void testRobotNeedsCharging() {
assertTrue(robot.needsCharging(20.0f));
}
+ /**
+ * Verifies availability reflects idle/no-task baseline and becomes false while moving.
+ */
@Test
public void testRobotAvailability() {
Robot robot = new Robot("BOT-006", new Vector2D(0, 0));
@@ -53,6 +78,9 @@ public void testRobotAvailability() {
assertFalse(robot.isAvailable());
}
+ /**
+ * Verifies explicit state changes are reflected by both enum state and string status accessors.
+ */
@Test
public void testRobotStateTransitions() {
Robot robot = new Robot("BOT-007", new Vector2D(0, 0));
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/navigation/BugNavigationStrategyTest.java b/open-robotics/src/test/java/com/openrobotics/robot/navigation/BugNavigationStrategyTest.java
index 19197962..44a06ab0 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/navigation/BugNavigationStrategyTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/navigation/BugNavigationStrategyTest.java
@@ -22,50 +22,79 @@
*/
public class BugNavigationStrategyTest {
+ /** Shared map fixture reset before each test. */
private Map map;
+ /** Strategy under test with deterministic seed. */
private BugNavigationStrategy strategy;
+ /**
+ * Robot test double exposing controllable target and scan values for deterministic strategy
+ * assertions.
+ */
private static class TestRobot extends Robot {
private Vector2D target;
private Sensor scan;
+ /**
+ * Creates a test robot with auto-generated UUID.
+ */
private TestRobot(String name, Vector2D position) {
super(name, position);
}
+ /**
+ * Creates a test robot with explicit UUID for deterministic ordering scenarios.
+ */
private TestRobot(UUID id, String name, Vector2D position) {
super(id, name, position);
}
+ /**
+ * Injects a synthetic target used by strategy target lookups.
+ */
public void setTargetForTest(Vector2D target) {
this.target = target;
}
+ /**
+ * Injects synthetic sensor scan output used by tie-breaking and obstacle filtering.
+ */
public void setScanForTest(Sensor scan) {
this.scan = scan;
}
+ /** Returns injected test target. */
@Override
public Vector2D getTarget() {
return target;
}
+ /** Returns injected test scan. */
@Override
public Sensor getLastScan() {
return scan;
}
}
+ /**
+ * Initializes a fresh map and strategy instance for each test case.
+ */
@BeforeEach
public void setUp() {
map = new Map(10, 10);
strategy = new BugNavigationStrategy(42L);
}
+ /**
+ * Creates a deterministic test robot with default name and auto-generated UUID.
+ */
private TestRobot makeRobot(int x, int y) {
return new TestRobot("BugBot", new Vector2D(x, y));
}
+ /**
+ * Creates a deterministic test robot with explicit UUID.
+ */
private TestRobot makeRobot(UUID id, int x, int y) {
return new TestRobot(id, "BugBot", new Vector2D(x, y));
}
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/navigation/GreedyNavigationStrategyTest.java b/open-robotics/src/test/java/com/openrobotics/robot/navigation/GreedyNavigationStrategyTest.java
index dbbc3117..c30cebce 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/navigation/GreedyNavigationStrategyTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/navigation/GreedyNavigationStrategyTest.java
@@ -21,42 +21,64 @@
*/
public class GreedyNavigationStrategyTest {
+ /** Shared map fixture recreated before each test. */
private Map map;
+ /** Strategy under test with deterministic seed for repeatable tie-break behavior. */
private GreedyNavigationStrategy strategy;
+ /**
+ * Robot test double exposing controllable target/scan values for deterministic assertions.
+ */
private static class TestRobot extends Robot {
private Vector2D target;
private Sensor scan;
+ /**
+ * Creates a test robot with configurable starting position.
+ */
private TestRobot(String name, Vector2D position) {
super(name, position);
}
+ /**
+ * Injects a synthetic target used by strategy calls.
+ */
public void setTargetForTest(Vector2D target) {
this.target = target;
}
+ /**
+ * Injects synthetic sensor scan output used by obstacle/tie-break logic.
+ */
public void setScanForTest(Sensor scan) {
this.scan = scan;
}
+ /** Returns injected test target. */
@Override
public Vector2D getTarget() {
return target;
}
+ /** Returns injected test sensor scan. */
@Override
public Sensor getLastScan() {
return scan;
}
}
+ /**
+ * Initializes fresh map and strategy fixtures before each test.
+ */
@BeforeEach
public void setUp() {
map = new Map(10, 10);
strategy = new GreedyNavigationStrategy(42L);
}
+ /**
+ * Creates a deterministic test robot with default name.
+ */
private TestRobot makeRobot(int x, int y) {
return new TestRobot("GreedyBot", new Vector2D(x, y));
}
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/navigation/RandomNavigationTest.java b/open-robotics/src/test/java/com/openrobotics/robot/navigation/RandomNavigationTest.java
index 933fff22..c2101ac2 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/navigation/RandomNavigationTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/navigation/RandomNavigationTest.java
@@ -19,15 +19,23 @@
*/
public class RandomNavigationTest {
+ /** Shared map fixture reinitialized before each test. */
private Map map;
+ /** Strategy under test. */
private RandomNavigation strategy;
+ /**
+ * Initializes a fresh open map and navigation strategy for each test case.
+ */
@BeforeEach
public void setUp() {
map = new Map(10, 10);
strategy = new RandomNavigation();
}
+ /**
+ * Creates a robot fixture at a specific tile.
+ */
private Robot makeRobot(int x, int y) {
return new Robot("RandomBot", new Vector2D(x, y));
}
diff --git a/open-robotics/src/test/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategyTest.java b/open-robotics/src/test/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategyTest.java
index e0a95d1d..02e844a5 100644
--- a/open-robotics/src/test/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategyTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/robot/navigation/RtaStarNavigationStrategyTest.java
@@ -21,42 +21,64 @@
*/
public class RtaStarNavigationStrategyTest {
+ /** Shared map fixture reset before each test. */
private Map map;
+ /** Strategy under test with deterministic seed. */
private RtaStarNavigationStrategy strategy;
+ /**
+ * Robot test double exposing controllable target/scan values for deterministic assertions.
+ */
private static class TestRobot extends Robot {
private Vector2D target;
private Sensor scan;
+ /**
+ * Creates a test robot with configurable start position.
+ */
private TestRobot(String name, Vector2D position) {
super(name, position);
}
+ /**
+ * Injects synthetic target used by strategy lookups.
+ */
public void setTargetForTest(Vector2D target) {
this.target = target;
}
+ /**
+ * Injects synthetic scan data used by sensor-aware filtering/tie-breaking.
+ */
public void setScanForTest(Sensor scan) {
this.scan = scan;
}
+ /** Returns injected test target. */
@Override
public Vector2D getTarget() {
return target;
}
+ /** Returns injected test scan. */
@Override
public Sensor getLastScan() {
return scan;
}
}
+ /**
+ * Initializes fresh map and strategy fixtures before each test.
+ */
@BeforeEach
public void setUp() {
map = new Map(10, 10);
strategy = new RtaStarNavigationStrategy(42L);
}
+ /**
+ * Creates a deterministic test robot fixture.
+ */
private TestRobot makeRobot(int x, int y) {
return new TestRobot("RtaBot", new Vector2D(x, y));
}
diff --git a/open-robotics/src/test/java/com/openrobotics/simulationcore/ReservationKPolicyTest.java b/open-robotics/src/test/java/com/openrobotics/simulationcore/ReservationKPolicyTest.java
index b042ac21..f5d869df 100644
--- a/open-robotics/src/test/java/com/openrobotics/simulationcore/ReservationKPolicyTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/simulationcore/ReservationKPolicyTest.java
@@ -1,5 +1,8 @@
package com.openrobotics.simulationcore;
+import com.openrobotics.AppState;
+import com.openrobotics.logging.Logger;
+import com.openrobotics.logging.LoggerMode;
import com.openrobotics.map.Map;
import com.openrobotics.map.Tile;
import com.openrobotics.map.Vector2D;
@@ -23,6 +26,7 @@
*/
public class ReservationKPolicyTest {
+ /** Fresh policy fixture recreated before each test. */
private ReservationKPolicy policy;
/**
@@ -33,26 +37,43 @@ public void setUp() {
policy = new ReservationKPolicy(1);
}
- // Helpers
+ /**
+ * Creates a position vector with integer tile coordinates.
+ */
+ protected Vector2D pos(int x, int y) {
+ return new Vector2D(x, y);
+ }
+ /**
+ * Creates a robot fixture at origin with a generated UUID.
+ */
private static Robot makeRobot(String name) {
return new Robot(name, new Vector2D(0, 0));
}
+ /**
+ * Creates a move intention using standalone tiles (not map-owned tiles).
+ */
private static MoveIntention move(Robot robot, int fromX, int fromY, int toX, int toY) {
return new MoveIntention(new Tile(fromX, fromY), new Tile(toX, toY), robot);
}
+ /**
+ * Creates a stay intention where source and destination are the same tile.
+ */
private static MoveIntention stay(Robot robot, int x, int y) {
Tile t = new Tile(x, y);
return new MoveIntention(t, t, robot);
}
+ /**
+ * Creates a generic test map used by policy-only tests.
+ */
private static Map testMap() {
return new Map(20, 20);
}
- /**
+ /**
* Creates a robot with a target task and moving state.
* @param name the name of the robot
* @param id the id of the robot
@@ -411,6 +432,10 @@ void nullMapStillApprovesIndependentImmediateMoves() {
assertMove(result, robotB, 2, 0);
}
+ /**
+ * k-window reservations may block moves into farther tiles even when immediate destinations do
+ * not conflict directly.
+ */
@Test
void blocksMovesIntoFartherTilesReservedByTheBfsWindow() {
Map map = new Map(5, 1);
@@ -552,13 +577,21 @@ void simulationEngineAppliesReservationPolicyBeforeCollisionResolution() {
map.addEntity(robotA);
map.addEntity(robotB);
+ // Creating dispatcher and loading it with a dummy task
+ Dispatcher dispatcher = new Dispatcher();
+ Task dummyTask = new Task(1, pos(0, 1), pos(3, 1), 1);
+ dispatcher.addTask(dummyTask);
+
SimulationEngine engine = new SimulationEngine(
map,
new Robot[] { robotA, robotB },
- new Dispatcher(),
+ dispatcher,
new ReservationKPolicy(3)
);
+ AppState.setEngine(engine);
+ Logger.setMode(LoggerMode.NO_OP);
+
engine.tick();
// Without ReservationK being applied in the engine, robot B would move to 3,0.
diff --git a/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineConfigLoadTest.java b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineConfigLoadTest.java
index fe6104c2..b2d43abb 100644
--- a/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineConfigLoadTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineConfigLoadTest.java
@@ -2,9 +2,6 @@
import com.openrobotics.map.Map;
import com.openrobotics.robot.Robot;
-import com.openrobotics.simulationcore.Dispatcher;
-import com.openrobotics.simulationcore.SimulationEngine;
-import com.openrobotics.task.TaskStatus;
import org.junit.jupiter.api.Test;
import java.io.IOException;
@@ -17,10 +14,22 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Integration-style tests for {@link SimulationEngine} JSON load/save behavior.
+ *
+ * This suite verifies successful config loading from test resources, backward-compatible loading
+ * when optional coordination sections are absent, and save-time omission of coordination metadata
+ * when the engine runs with no-op policy semantics.
+ */
class SimulationEngineConfigLoadTest {
+ /**
+ * Verifies a repository-backed scenario JSON can be copied to a temp file and loaded into a
+ * fully initialized engine instance.
+ *
+ * @throws IOException if test resource copy or file operations fail
+ */
@Test
void loadsJsonScenarioFromRepositoryRoot() throws IOException {
Path configPath = Files.createTempFile("test_scenario", ".json");
@@ -38,6 +47,12 @@ void loadsJsonScenarioFromRepositoryRoot() throws IOException {
assertEquals(20, engine.getMap().getEntities().size(), "Config load should materialize all scenario entities.");
}
+ /**
+ * Verifies configs without a {@code coordination} section still load successfully and default
+ * to no-op coordination behavior.
+ *
+ * @throws IOException if temporary config file operations fail
+ */
@Test
void loadsConfigWithoutCoordinationSectionUsingNoOpPolicy() throws IOException {
Path configPath = Files.createTempFile("simulation-no-coordination", ".json");
@@ -76,6 +91,11 @@ void loadsConfigWithoutCoordinationSectionUsingNoOpPolicy() throws IOException {
assertDoesNotThrow(engine::tick, "Engine should tick with the no-op coordination policy.");
}
+ /**
+ * Verifies saving an engine with no coordination policy omits the coordination JSON block.
+ *
+ * @throws IOException if save/read file operations fail
+ */
@Test
void savingWithNoOpPolicyOmitsCoordinationSection() throws IOException {
SimulationEngine engine = new SimulationEngine(
@@ -93,6 +113,11 @@ void savingWithNoOpPolicyOmitsCoordinationSection() throws IOException {
"Saved JSON should omit the coordination section when using the no-op policy.");
}
+ /**
+ * Finds a robot by name in a possibly sparse robot array.
+ *
+ * @return matching robot or {@code null} if no match exists
+ */
private Robot findRobotByName(Robot[] robots, String name) {
for (Robot robot : robots) {
if (robot != null && name.equals(robot.getName())) {
diff --git a/open-robotics/src/test/java/com/openrobotics/SimulationEngineDeadlockRecoveryTest.java b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineDeadlockRecoveryTest.java
similarity index 80%
rename from open-robotics/src/test/java/com/openrobotics/SimulationEngineDeadlockRecoveryTest.java
rename to open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineDeadlockRecoveryTest.java
index 50381b55..e8708a2b 100644
--- a/open-robotics/src/test/java/com/openrobotics/SimulationEngineDeadlockRecoveryTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineDeadlockRecoveryTest.java
@@ -1,5 +1,6 @@
-package com.openrobotics;
+package com.openrobotics.simulationcore;
+import com.openrobotics.AppState;
import com.openrobotics.logging.Logger;
import com.openrobotics.logging.LoggerMode;
import com.openrobotics.map.Map;
@@ -8,10 +9,6 @@
import com.openrobotics.robot.Robot;
import com.openrobotics.robot.RobotState;
import com.openrobotics.robot.navigation.NavigationStrategy;
-import com.openrobotics.simulationcore.CoordinationPolicy;
-import com.openrobotics.simulationcore.Dispatcher;
-import com.openrobotics.simulationcore.MoveIntention;
-import com.openrobotics.simulationcore.SimulationEngine;
import com.openrobotics.task.Task;
import com.openrobotics.task.TaskStatus;
import org.junit.jupiter.api.BeforeEach;
@@ -24,10 +21,17 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Integration-style tests for deadlock recovery behavior in {@link SimulationEngine}.
+ *
+ * This suite validates first-attempt reroute behavior, second-attempt reset/requeue fallback,
+ * immediate fallback when reroute avoid tile equals task target, and reroute-budget reset when a
+ * robot receives a new task.
+ */
class SimulationEngineDeadlockRecoveryTest {
/**
- * Seed AppState with a dummy SimulationEngine to satisfy logging code
+ * Seeds {@link AppState} with a minimal engine so logging-dependent paths remain valid.
*/
private void seedAppState() {
SimulationEngine dummyEngine = new SimulationEngine(
@@ -40,12 +44,19 @@ private void seedAppState() {
AppState.setEngine(dummyEngine);
}
+ /**
+ * Initializes per-test state and disables logger side effects for deterministic assertions.
+ */
@BeforeEach
public void setUp() {
seedAppState();
Logger.setMode(LoggerMode.NO_OP); // disable logging during tests
}
+ /**
+ * Verifies first deadlock triggers a reroute attempt while preserving task ownership and moving
+ * state, and that strategy/policy reset hooks are invoked once.
+ */
@Test
void firstDeadlockAttemptsARerouteWithoutDroppingTheTask() {
Map map = new Map(3, 3);
@@ -83,6 +94,9 @@ void firstDeadlockAttemptsARerouteWithoutDroppingTheTask() {
assertTrue(recoveredBot.hasRerouteAttemptedForCurrentTask());
}
+ /**
+ * Verifies second deadlock (after failed reroute) falls back to reset + task requeue.
+ */
@Test
void secondDeadlockFallsBackToResetAndRequeue() {
Map map = new Map(3, 3);
@@ -114,6 +128,9 @@ void secondDeadlockFallsBackToResetAndRequeue() {
assertEquals(2, nav.resetCalls);
}
+ /**
+ * Verifies immediate fallback path when the computed reroute-avoid tile equals task target.
+ */
@Test
void fallingBackImmediatelyWhenTheAvoidTileIsTheTargetRequeuesTheTask() {
Map map = new Map(3, 3);
@@ -143,6 +160,9 @@ void fallingBackImmediatelyWhenTheAvoidTileIsTheTargetRequeuesTheTask() {
assertEquals(1, nav.resetCalls);
}
+ /**
+ * Verifies assigning a different task clears reroute-attempt state for the robot.
+ */
@Test
void assigningANewTaskResetsTheRerouteBudget() {
Map map = new Map(3, 3);
@@ -166,6 +186,10 @@ void assigningANewTaskResetsTheRerouteBudget() {
assertNull(robot.getLastRequestedNextTile());
}
+ /**
+ * Navigation test double that succeeds on reroute by choosing a different tile once avoid state
+ * is set.
+ */
private static class RerouteSuccessStrategy implements NavigationStrategy {
private int resetCalls;
@@ -176,12 +200,18 @@ public MoveIntention getNextMove(Robot robot, Map map) {
return new MoveIntention(fromTile, map.getTile(next.getX(), next.getY()), robot);
}
+ /**
+ * Counts strategy reset invocations triggered by engine deadlock handling.
+ */
@Override
public void reset(Robot robot) {
resetCalls++;
}
}
+ /**
+ * Navigation test double that fails reroute by waiting after avoid tile is set.
+ */
private static class RerouteFailureStrategy implements NavigationStrategy {
private int resetCalls;
@@ -194,12 +224,18 @@ public MoveIntention getNextMove(Robot robot, Map map) {
return new MoveIntention(fromTile, fromTile, robot);
}
+ /**
+ * Counts strategy reset invocations triggered by engine deadlock handling.
+ */
@Override
public void reset(Robot robot) {
resetCalls++;
}
}
+ /**
+ * Navigation test double that always targets the same tile (used to force avoid==target path).
+ */
private static class TargetTileStrategy implements NavigationStrategy {
private int resetCalls;
@@ -209,12 +245,18 @@ public MoveIntention getNextMove(Robot robot, Map map) {
return new MoveIntention(fromTile, map.getTile(2, 1), robot);
}
+ /**
+ * Counts strategy reset invocations triggered by engine deadlock handling.
+ */
@Override
public void reset(Robot robot) {
resetCalls++;
}
}
+ /**
+ * Coordination policy test double that passes intentions through and tracks state-clear hooks.
+ */
private static class HookAwarePolicy implements CoordinationPolicy {
private int clearCalls;
@@ -223,12 +265,18 @@ public MoveIntention[] apply(Map map, MoveIntention[] intentions) {
return intentions;
}
+ /**
+ * Counts engine calls that clear per-robot coordination state.
+ */
@Override
public void clearRobotCoordinationState(Robot robot) {
clearCalls++;
}
}
+ /**
+ * Local non-null assertion helper used to avoid additional static imports.
+ */
private static void assertNotNull(Object value) {
assertTrue(value != null);
}
diff --git a/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineFinishedTest.java b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineFinishedTest.java
index 18d599a4..2992fc33 100644
--- a/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineFinishedTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineFinishedTest.java
@@ -12,26 +12,49 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
-/** Tests for {@link SimulationEngine#isFinished()}. */
+/**
+ * Unit tests for {@link SimulationEngine#isFinished()} completion semantics.
+ *
+ * This suite verifies tick-gating behavior, queued-task influence, and robot-state influence on
+ * the engine's finished predicate.
+ */
public class SimulationEngineFinishedTest {
+ /**
+ * Sets the private tick counter via reflection to create deterministic completion states.
+ *
+ * @param engine engine instance under test
+ * @param tick tick value to inject
+ * @throws Exception if reflective field access fails
+ */
private static void setTick(SimulationEngine engine, int tick) throws Exception {
Field f = SimulationEngine.class.getDeclaredField("tickCounter");
f.setAccessible(true);
f.setInt(engine, tick);
}
+ /**
+ * Creates a minimal engine fixture with a fresh map and no-op coordination policy.
+ */
private static SimulationEngine newEngine(Robot[] robots, Dispatcher dispatcher) {
Map map = new Map(5, 5);
return new SimulationEngine(map, robots, dispatcher, CoordinationPolicy.noOp());
}
+ /**
+ * Verifies a newly created engine at tick zero is never considered finished.
+ */
@Test
public void notFinishedAtTickZero() {
SimulationEngine engine = newEngine(new Robot[0], new Dispatcher());
assertFalse(engine.isFinished(), "fresh engine at tick 0 must not be finished");
}
+ /**
+ * Verifies queued tasks keep the engine unfinished even after tick advancement.
+ *
+ * @throws Exception if reflective tick injection fails
+ */
@Test
public void notFinishedWhenTasksStillQueued() throws Exception {
Dispatcher dispatcher = new Dispatcher();
@@ -41,6 +64,11 @@ public void notFinishedWhenTasksStillQueued() throws Exception {
assertFalse(engine.isFinished());
}
+ /**
+ * Verifies engine is finished when ticks advanced, no tasks remain, and all robots are idle.
+ *
+ * @throws Exception if reflective tick injection fails
+ */
@Test
public void finishedWhenTickAdvancedAllIdleNoTasks() throws Exception {
Robot r = new Robot("r1", new Vector2D(0, 0));
@@ -50,6 +78,11 @@ public void finishedWhenTickAdvancedAllIdleNoTasks() throws Exception {
assertTrue(engine.isFinished());
}
+ /**
+ * Verifies any non-idle robot keeps the engine unfinished despite tick advancement.
+ *
+ * @throws Exception if reflective tick injection fails
+ */
@Test
public void notFinishedWhenARobotIsNonIdle() throws Exception {
Robot r = new Robot("r1", new Vector2D(0, 0));
diff --git a/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineTest.java b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineTest.java
index a9661b16..b2b1d98a 100644
--- a/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/simulationcore/SimulationEngineTest.java
@@ -44,34 +44,57 @@
*/
public class SimulationEngineTest {
+ /** Default test-map width used by setup fixtures. */
private static final int MAP_W = 10;
+ /** Default test-map height used by setup fixtures. */
private static final int MAP_H = 10;
+ /** Shared map fixture recreated before each test. */
private Map map;
+ /** Shared dispatcher fixture recreated before each test. */
private Dispatcher dispatcher;
+ /** Baseline engine fixture created in {@link #setUp()}. */
private SimulationEngine engine;
+ /**
+ * Robot test double with scripted next-move output and update-call counting.
+ */
private static class ScriptedRobot extends Robot {
private MoveIntention nextMove;
private int updateCalls;
+ /**
+ * Creates a scripted robot fixture at a fixed position.
+ */
private ScriptedRobot(String name, Vector2D position) {
super(name, position);
}
+ /**
+ * Injects the next move that {@link #getNextMove(Map)} should return.
+ */
public void setNextMove(MoveIntention nextMove) {
this.nextMove = nextMove;
}
+ /**
+ * Returns how many times {@link #update(Map)} has been invoked.
+ */
public int getUpdateCalls() {
return updateCalls;
}
+ /**
+ * Returns the scripted move intention for the current tick.
+ */
@Override
public MoveIntention getNextMove(Map map) {
return nextMove;
}
+ /**
+ * Increments update-call counter to validate per-tick robot updates.
+ */
@Override
public void update(Map map) {
updateCalls++;
@@ -209,20 +232,21 @@ public void testTickIncrementsCounter() {
}
/**
- * With no configured tasks, the engine treats this as sandbox mode:
- * {@code tick()} still advances time and leaves {@code running = false}.
- * At least one robot is required for tick() to proceed past the no-robots guard.
+ * When no tasks have ever been added to the dispatcher, {@code tick()} exits
+ * early without advancing the counter, sets {@code running} to {@code false},
+ * and records a {@link SimulationError#NO_TASKS_GENERATED} error.
+ * At least one robot is required to reach the no-tasks guard.
*/
@Test
- public void testTickStopsWhenWorkloadComplete() {
+ public void testTickStopsWhenNoTasksAdded() {
Robot r = makeRobot("R1", 5, 5);
SimulationEngine eng = buildEngine(new Robot[]{ r });
- int before = eng.getTickCounter();
eng.tick();
- assertEquals(before + 1, eng.getTickCounter(),
- "tick() should increment in no-workload sandbox mode");
+ assertEquals(0, eng.getTickCounter(),
+ "tick() should not increment when no tasks have been added");
assertFalse(eng.getIsRunning());
+ assertEquals(SimulationError.NO_TASKS_GENERATED, eng.getSimulationError());
}
/**
@@ -254,6 +278,8 @@ public void testTickBlocksMoveIntoDeadRobotTile() {
*/
@Test
public void testTickBlocksSameDirectionFollowThrough() {
+ dispatcher.addTask(makeTask(1, 0, 0, 9, 9));
+
ScriptedRobot leader = new ScriptedRobot("Leader", new Vector2D(1, 0));
leader.setState(RobotState.MOVING);
leader.setNextMove(new MoveIntention(map.getTile(1, 0), map.getTile(2, 0), leader));
@@ -279,6 +305,8 @@ public void testTickBlocksSameDirectionFollowThrough() {
*/
@Test
public void testTickAllowsOverlapOnDeliveryStation() {
+ dispatcher.addTask(makeTask(1, 0, 0, 9, 9));
+
map.addEntity(new DeliveryStation("Delivery", new Vector2D(1, 0)));
ScriptedRobot staying = new ScriptedRobot("Staying", new Vector2D(1, 0));
@@ -306,6 +334,8 @@ public void testTickAllowsOverlapOnDeliveryStation() {
*/
@Test
public void testTickAllowsOverlapOnChargingStation() {
+ dispatcher.addTask(makeTask(1, 0, 0, 9, 9));
+
map.addEntity(new ChargingStation("Charging", new Vector2D(1, 0)));
ScriptedRobot staying = new ScriptedRobot("Staying", new Vector2D(1, 0));
@@ -521,6 +551,8 @@ public void testEngineHaltsNaturallyAfterSingleTaskCompletion() {
*/
@Test
public void testTickDoesNotCommitIllegalMoveIntentions() {
+ dispatcher.addTask(makeTask(1, 0, 0, 9, 9));
+
ScriptedRobot robot = new ScriptedRobot("Scripted", new Vector2D(0, 0));
robot.setNextMove(new MoveIntention(map.getTile(0, 0), map.getTile(2, 0), robot));
@@ -980,6 +1012,9 @@ public void testConfigSavingWithTrafficRulesPolicyPersistsIntersections() throws
}
}
+ /**
+ * Toggling a traffic-rule intersection on persists marker coordinates in saved config.
+ */
@Test
public void testToggleTrafficRuleIntersectionPersistsToSavedConfig() throws IOException {
SimulationEngine eng = new SimulationEngine(
@@ -1005,6 +1040,9 @@ public void testToggleTrafficRuleIntersectionPersistsToSavedConfig() throws IOEx
}
}
+ /**
+ * Toggling the same traffic-rule intersection twice removes the persisted marker.
+ */
@Test
public void testToggleTrafficRuleIntersectionTwiceRemovesMarker() throws IOException {
SimulationEngine eng = new SimulationEngine(
@@ -1029,6 +1067,10 @@ public void testToggleTrafficRuleIntersectionTwiceRemovesMarker() throws IOExcep
}
}
+ /**
+ * Intersection toggling is ignored for non-traffic-rules policies and writes no coordination
+ * marker data.
+ */
@Test
public void testToggleTrafficRuleIntersectionIgnoredForNonTrafficRulesPolicy() throws IOException {
SimulationEngine eng = buildEngine(new Robot[]{});
@@ -1342,7 +1384,8 @@ private SimulationEngine buildEngine(Robot[] robots) {
/**
* Creates a base configuration DTO.
- * @return
+ *
+ * @return fully initialized DTO with default sections for config-load tests
*/
private SimulationConfigDTO baseConfigDto() {
SimulationConfigDTO dto = new SimulationConfigDTO();
@@ -1437,6 +1480,15 @@ private SimulationConfigDTO.MapEntityDTO stationDto(UUID id, String name, int x,
return entity;
}
+ /**
+ * Creates a rack DTO.
+ *
+ * @param id rack ID
+ * @param name rack name
+ * @param x x coordinate
+ * @param y y coordinate
+ * @return rack DTO instance
+ */
private SimulationConfigDTO.RackDTO rackDto(UUID id, String name, int x, int y) {
SimulationConfigDTO.RackDTO entity = new SimulationConfigDTO.RackDTO();
entity.id = id;
diff --git a/open-robotics/src/test/java/com/openrobotics/simulationcore/TrafficRulesPolicyTest.java b/open-robotics/src/test/java/com/openrobotics/simulationcore/TrafficRulesPolicyTest.java
index 67d1fc78..10832740 100644
--- a/open-robotics/src/test/java/com/openrobotics/simulationcore/TrafficRulesPolicyTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/simulationcore/TrafficRulesPolicyTest.java
@@ -28,20 +28,31 @@
public class TrafficRulesPolicyTest {
// Helpers
-
+ /**
+ * Creates a robot fixture at origin with generated UUID.
+ */
private static Robot makeRobot(String name) {
return new Robot(name, new Vector2D(0, 0));
}
+ /**
+ * Creates a move intention using standalone tiles (not map-owned tile instances).
+ */
private static MoveIntention move(Robot robot, int fromX, int fromY, int toX, int toY) {
return new MoveIntention(new Tile(fromX, fromY), new Tile(toX, toY), robot);
}
+ /**
+ * Creates a stay intention where source and destination are identical.
+ */
private static MoveIntention stay(Robot robot, int x, int y) {
Tile t = new Tile(x, y);
return new MoveIntention(t, t, robot);
}
+ /**
+ * Builds an intersection tile set from flat coordinate pairs.
+ */
private static Set intersectionSet(int... coords) {
Set set = new HashSet<>();
for (int i = 0; i < coords.length - 1; i += 2) {
@@ -50,6 +61,9 @@ private static Set intersectionSet(int... coords) {
return set;
}
+ /**
+ * Creates a generic map fixture used by policy-only tests.
+ */
private static Map testMap() {
return new Map(20, 20);
}
@@ -332,6 +346,76 @@ public void testIntersectionOwnershipReleasedAfterOwnerExits() {
assertEquals(5, released[0].getToTile().getY());
}
+ /**
+ * A dead robot inside an intersection must not keep ownership on later ticks.
+ */
+ @Test
+ void releasesIntersectionOwnershipWhenOwnerDiesInsideIntersection() {
+ Map map = new Map(3, 3);
+ TrafficRulesPolicy policy = new TrafficRulesPolicy(Set.of(map.getTile(1, 1), map.getTile(1, 2)));
+
+ Robot owner = unloadedRobot("owner",
+ UUID.fromString("00000000-0000-0000-0000-000000000031"),
+ new Vector2D(1, 1),
+ new Vector2D(2, 1),
+ 2);
+ Robot challenger = unloadedRobot("challenger",
+ UUID.fromString("00000000-0000-0000-0000-000000000032"),
+ new Vector2D(0, 2),
+ new Vector2D(2, 2),
+ 1);
+
+ MoveIntention[] blocked = policy.apply(map, new MoveIntention[] {
+ stay(map, owner, 1, 1),
+ move(map, challenger, 0, 2, 1, 2)
+ });
+ assertMove(blocked, challenger, 0, 2);
+
+ owner.setState(RobotState.BATTERY_DEAD);
+
+ MoveIntention[] released = policy.apply(map, new MoveIntention[] {
+ stay(map, owner, 1, 1),
+ move(map, challenger, 0, 2, 1, 2)
+ });
+ assertMove(released, challenger, 1, 2);
+ }
+
+ /**
+ * Clearing coordination state should drop the active owner immediately.
+ */
+ @Test
+ void clearRobotCoordinationStateReleasesActiveIntersectionOwner() {
+ Map map = new Map(3, 3);
+ TrafficRulesPolicy policy = new TrafficRulesPolicy(Set.of(map.getTile(1, 1), map.getTile(1, 2)));
+
+ Robot owner = unloadedRobot("owner",
+ UUID.fromString("00000000-0000-0000-0000-000000000033"),
+ new Vector2D(1, 1),
+ new Vector2D(2, 1),
+ 2);
+ Robot challenger = unloadedRobot("challenger",
+ UUID.fromString("00000000-0000-0000-0000-000000000034"),
+ new Vector2D(0, 2),
+ new Vector2D(2, 2),
+ 1);
+
+ MoveIntention[] blocked = policy.apply(map, new MoveIntention[] {
+ stay(map, owner, 1, 1),
+ move(map, challenger, 0, 2, 1, 2)
+ });
+ assertMove(blocked, challenger, 0, 2);
+
+ policy.clearRobotCoordinationState(owner);
+
+ MoveIntention[] released = policy.apply(map, new MoveIntention[] {
+ move(map, challenger, 0, 2, 1, 2)
+ });
+ assertMove(released, challenger, 1, 2);
+ }
+
+ /**
+ * Longer stop time gets intersection priority when multiple robots contend for the same tile.
+ */
@Test
void givesIntersectionPriorityToRobotWithLongerStopTime() {
Map map = new Map(3, 3);
@@ -357,6 +441,9 @@ void givesIntersectionPriorityToRobotWithLongerStopTime() {
assertMove(result, longWait, 1, 1);
}
+ /**
+ * When stop time ties, loaded robots (carrying payload) receive intersection priority.
+ */
@Test
void givesIntersectionPriorityToLoadedRobotWhenStopTimeIsTied() {
Map map = new Map(3, 3);
@@ -382,6 +469,9 @@ void givesIntersectionPriorityToLoadedRobotWhenStopTimeIsTied() {
assertMove(result, loaded, 1, 1);
}
+ /**
+ * Once a robot acquires an intersection, it remains exclusive until that robot exits.
+ */
@Test
void keepsIntersectionExclusiveUntilTheActiveRobotLeaves() {
Map map = new Map(3, 3);
@@ -423,6 +513,9 @@ void keepsIntersectionExclusiveUntilTheActiveRobotLeaves() {
assertMove(tickThree, queuedRobot, 1, 1);
}
+ /**
+ * Creates an unloaded moving robot whose task target is also used as pickup target.
+ */
private static Robot unloadedRobot(String name, UUID id, Vector2D start, Vector2D pickupTarget, int stuckTicks) {
Robot robot = new Robot(id, name, start);
robot.setCurrentTask(new Task(id.hashCode(), pickupTarget, pickupTarget, 1));
@@ -431,6 +524,9 @@ private static Robot unloadedRobot(String name, UUID id, Vector2D start, Vector2
return robot;
}
+ /**
+ * Creates a loaded moving robot with dropoff target and configured stuck-tick history.
+ */
private static Robot loadedRobot(String name, UUID id, Vector2D start, Vector2D dropoffTarget, int stuckTicks) {
Robot robot = new Robot(id, name, start);
robot.setCurrentTask(new Task(id.hashCode(), start, dropoffTarget, 1));
@@ -440,15 +536,24 @@ private static Robot loadedRobot(String name, UUID id, Vector2D start, Vector2D
return robot;
}
+ /**
+ * Creates a map-backed move intention using map tile references.
+ */
private static MoveIntention move(Map map, Robot robot, int fromX, int fromY, int toX, int toY) {
return new MoveIntention(map.getTile(fromX, fromY), map.getTile(toX, toY), robot);
}
+ /**
+ * Creates a map-backed stay intention for the given tile.
+ */
private static MoveIntention stay(Map map, Robot robot, int x, int y) {
Tile tile = map.getTile(x, y);
return new MoveIntention(tile, tile, robot);
}
+ /**
+ * Asserts that a robot's resulting move intention matches expected destination coordinates.
+ */
private static void assertMove(MoveIntention[] intentions, Robot robot, int expectedX, int expectedY) {
for (MoveIntention intention : intentions) {
if (intention.getRobot().getId().equals(robot.getId())) {
diff --git a/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorManualModeTest.java b/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorManualModeTest.java
index 6430407f..abf6a98f 100644
--- a/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorManualModeTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorManualModeTest.java
@@ -8,12 +8,24 @@
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
+/**
+ * Unit tests for {@link TaskGenerator} behavior when rack manual dropoff mode is enabled/disabled.
+ *
+ * This suite verifies that random mode ignores rack-specific dropoff restrictions, while manual
+ * mode enforces filtered round-robin assignment over resolvable, non-null dropoff station IDs.
+ */
class TaskGeneratorManualModeTest {
+ /**
+ * Creates a deterministic empty map fixture with stable dimensions for task-generation tests.
+ */
private Map buildMap() {
return new Map(UUID.randomUUID(), 10, 10);
}
+ /**
+ * Verifies random mode ignores {@code validDropoffIds} and samples from all stations.
+ */
@Test
void randomModeIgnoresValidDropoffIds() {
// In random (non-manual) mode, validDropoffIds is ignored — all stations are valid.
@@ -34,6 +46,9 @@ void randomModeIgnoresValidDropoffIds() {
assertTrue(usedB, "Random mode must draw from all stations, not just validDropoffIds");
}
+ /**
+ * Verifies manual mode performs ordered round-robin over the non-null, resolvable station pool.
+ */
@Test
void manualModeUsesRoundRobinOverNonNullPool() {
Map m = buildMap();
@@ -60,6 +75,9 @@ void manualModeUsesRoundRobinOverNonNullPool() {
"Round-robin must cycle through non-null pool in order");
}
+ /**
+ * Verifies manual mode drops unresolvable station IDs and uses only mapped stations.
+ */
@Test
void manualModeFiltersUnresolvableIds() {
Map m = buildMap();
diff --git a/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorTest.java b/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorTest.java
index a3ba8c5f..b197a567 100644
--- a/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/task/TaskGeneratorTest.java
@@ -10,8 +10,17 @@
import static org.junit.jupiter.api.Assertions.*;
+/**
+ * Unit tests for random task generation from map entities.
+ *
+ * This suite verifies pickup/dropoff source selection, empty-result guardrails, max-count
+ * clamping, and valid-dropoff filtering when racks are configured for manual assignment.
+ */
class TaskGeneratorTest {
+ /**
+ * Verifies generated tasks originate from rack position and target available delivery stations.
+ */
@Test
void generatesTasksFromRacksToDeliveryStations() {
Map map = new Map(10, 10);
@@ -32,6 +41,9 @@ void generatesTasksFromRacksToDeliveryStations() {
}
}
+ /**
+ * Verifies generator returns no tasks when the map lacks delivery stations.
+ */
@Test
void returnsEmptyWhenNoDeliveryStations() {
Map map = new Map(10, 10);
@@ -42,6 +54,9 @@ void returnsEmptyWhenNoDeliveryStations() {
assertTrue(tasks.isEmpty());
}
+ /**
+ * Verifies generated task count is capped by the requested maximum.
+ */
@Test
void respectsMaxCount() {
Map map = new Map(10, 10);
@@ -55,6 +70,9 @@ void respectsMaxCount() {
assertEquals(3, tasks.size());
}
+ /**
+ * Verifies manual mode restricts dropoffs to rack {@code validDropoffIds}.
+ */
@Test
void respectsValidDropoffIds() {
com.openrobotics.map.Map map = new com.openrobotics.map.Map(10, 10);
diff --git a/open-robotics/src/test/java/com/openrobotics/util/IconLoaderTest.java b/open-robotics/src/test/java/com/openrobotics/util/IconLoaderTest.java
index 73b0e878..6d4b245e 100644
--- a/open-robotics/src/test/java/com/openrobotics/util/IconLoaderTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/util/IconLoaderTest.java
@@ -9,13 +9,26 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertNotSame;
+/**
+ * Unit tests for {@link IconLoader} icon lookup and cache behavior.
+ *
+ * This suite verifies cache hits for equivalent type keys, cache invalidation after clear, and
+ * null/unknown-type handling contract.
+ */
public class IconLoaderTest {
+ /**
+ * Clears icon cache after each test to keep cache-state assertions isolated.
+ */
@AfterEach
void clearCache() {
IconLoader.clearCache();
}
+ /**
+ * Verifies repeated lookups for the same supported type return the cached {@link Image}
+ * instance, regardless of input case.
+ */
@Test
void getIcon_returns_cached_instance_for_same_supported_type() {
Image first = IconLoader.getIcon("ROBOT");
@@ -25,6 +38,9 @@ void getIcon_returns_cached_instance_for_same_supported_type() {
assertSame(first, second);
}
+ /**
+ * Verifies clearing cache forces a new icon instance to be loaded on next request.
+ */
@Test
void clearCache_forces_a_fresh_load() {
Image first = IconLoader.getIcon("CHARGER");
@@ -36,6 +52,9 @@ void clearCache_forces_a_fresh_load() {
assertNotSame(first, second);
}
+ /**
+ * Verifies null and unknown icon types return {@code null} without throwing.
+ */
@Test
void getIcon_returns_null_for_null_or_unknown_type() {
assertNull(IconLoader.getIcon(null));
diff --git a/open-robotics/src/test/java/com/openrobotics/util/ScreenNavigatorTest.java b/open-robotics/src/test/java/com/openrobotics/util/ScreenNavigatorTest.java
index 612a2351..0e96b4e3 100644
--- a/open-robotics/src/test/java/com/openrobotics/util/ScreenNavigatorTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/util/ScreenNavigatorTest.java
@@ -11,8 +11,19 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * JavaFX tests for {@link ScreenNavigator} screen/dialog loading behavior.
+ *
+ * This suite verifies expected failure modes for missing stage/FXML resources and confirms
+ * successful root replacement during normal screen navigation.
+ */
public class ScreenNavigatorTest extends ApplicationTest {
+ /**
+ * Initializes the primary stage used by {@link ScreenNavigator} for each TestFX run.
+ *
+ * @param stage JavaFX stage provided by TestFX
+ */
@Override
public void start(Stage stage) {
ScreenNavigator.setPrimaryStage(stage);
@@ -20,16 +31,21 @@ public void start(Stage stage) {
stage.show();
}
+ /**
+ * Verifies screen loading fails (wrapped by TestFX) when primary stage is unset.
+ */
@Test
void loadScreen_throws_wrapped_exception_when_primary_stage_is_missing() {
interact(() -> ScreenNavigator.setPrimaryStage(null));
RuntimeException ex = assertThrows(RuntimeException.class, () -> interact(ScreenNavigator::goToWelcome));
- // TestFX runs callables on the FX thread; failures may be wrapped (e.g. ExecutionException).
assertNotNull(findCauseOfType(ex, IllegalStateException.class));
}
+ /**
+ * Verifies loading an unknown FXML path throws a wrapped argument/resource error.
+ */
@Test
void loadScreen_throws_wrapped_exception_for_unknown_fxml_path() {
RuntimeException ex = assertThrows(
@@ -40,7 +56,11 @@ void loadScreen_throws_wrapped_exception_for_unknown_fxml_path() {
assertNotNull(findCauseOfType(ex, IllegalArgumentException.class));
}
- // Follows {@code getCause()} — TestFX {@code interact} may wrap FX-thread failures.
+ /**
+ * Walks the exception cause chain and returns the first cause assignable to {@code type}.
+ *
+ * Useful because TestFX {@code interact} often wraps FX-thread exceptions.
+ */
private static T findCauseOfType(Throwable ex, Class type) {
for (Throwable t = ex; t != null; t = t.getCause()) {
if (type.isInstance(t)) {
@@ -50,6 +70,9 @@ private static T findCauseOfType(Throwable ex, Class ty
return null;
}
+ /**
+ * Verifies dialog loading reports a clear error when FXML resource path is invalid.
+ */
@Test
void openDialog_throws_for_unknown_fxml_path() {
IllegalArgumentException ex = assertThrows(
@@ -60,6 +83,9 @@ void openDialog_throws_for_unknown_fxml_path() {
assertTrue(ex.getMessage().contains("FXML resource not found"));
}
+ /**
+ * Verifies navigation calls swap the scene root to the requested destination screen.
+ */
@Test
void loadScreen_replaces_scene_root_with_requested_screen() {
interact(ScreenNavigator::goToWelcome);
diff --git a/open-robotics/src/test/java/com/openrobotics/util/ViewportTipsTest.java b/open-robotics/src/test/java/com/openrobotics/util/ViewportTipsTest.java
index 84121ee3..f5127cab 100644
--- a/open-robotics/src/test/java/com/openrobotics/util/ViewportTipsTest.java
+++ b/open-robotics/src/test/java/com/openrobotics/util/ViewportTipsTest.java
@@ -14,20 +14,36 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Unit tests for {@link ViewportTips} tip-deck management behavior.
+ *
+ * This suite validates non-repeating draw behavior within a cycle, blank-input filtering for
+ * mutation APIs, and immutability/normalization guarantees of the exposed tip list.
+ */
public class ViewportTipsTest {
+ /** Snapshot of original shared tips list restored after each test. */
private List originalTips;
+ /**
+ * Captures current global tips before each test to avoid leaking state across tests.
+ */
@BeforeEach
void captureOriginalTips() {
originalTips = new ArrayList<>(ViewportTips.getAllTips());
}
+ /**
+ * Restores original global tips after each test.
+ */
@AfterEach
void restoreOriginalTips() {
ViewportTips.setTips(originalTips);
}
+ /**
+ * Verifies deck draws consume all available tips before repeats occur.
+ */
@Test
void nextTip_uses_all_tips_before_any_repeat_when_deck_is_small() {
ViewportTips.setTips(List.of("tip-a", "tip-b"));
@@ -41,6 +57,9 @@ void nextTip_uses_all_tips_before_any_repeat_when_deck_is_small() {
assertTrue(Set.of("tip-a", "tip-b").contains(third));
}
+ /**
+ * Verifies {@link ViewportTips#addTip(String)} ignores blank values and appends valid tips.
+ */
@Test
void addTip_ignores_blank_values_and_adds_valid_tip() {
ViewportTips.setTips(List.of("base"));
@@ -51,6 +70,10 @@ void addTip_ignores_blank_values_and_adds_valid_tip() {
assertEquals(List.of("base", "added"), ViewportTips.getAllTips());
}
+ /**
+ * Verifies {@link ViewportTips#setTips(List)} removes null/blank entries and publishes an
+ * unmodifiable list view.
+ */
@Test
void setTips_filters_null_and_blank_entries_and_result_is_unmodifiable() {
ViewportTips.setTips(Arrays.asList("keep", "", " ", null, "also-keep"));