diff --git a/.gitignore b/.gitignore index 6fa7db9..5c91afc 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ build/ # Other *.swp *.pdf + +# JAR built pom file +dependency-reduced-pom.xml \ No newline at end of file diff --git a/main/pom.xml b/main/pom.xml index 56ce74f..f2e1f63 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -29,6 +29,18 @@ gson 2.10.1 + + + + org.openjfx + javafx-controls + 17.0.2 + + + org.openjfx + javafx-fxml + 17.0.2 + @@ -42,6 +54,15 @@ sd.Entry + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + sd.dashboard.DashboardUI + + org.apache.maven.plugins maven-shade-plugin diff --git a/main/src/main/java/sd/ExitNodeProcess.java b/main/src/main/java/sd/ExitNodeProcess.java index c2a4f2d..ec3dd07 100644 --- a/main/src/main/java/sd/ExitNodeProcess.java +++ b/main/src/main/java/sd/ExitNodeProcess.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; import sd.config.SimulationConfig; import sd.coordinator.SocketClient; +import sd.dashboard.StatsUpdatePayload; import sd.model.Message; import sd.model.MessageType; import sd.model.Vehicle; @@ -38,6 +39,9 @@ public class ExitNodeProcess { /** Flag para controlar a execução do processo (volatile para visibilidade entre threads) */ private volatile boolean running; + /** Simulation start time (milliseconds) to calculate relative times */ + private long simulationStartMillis; + /** Counter de veículos que completaram a rota */ private int totalVehiclesReceived; @@ -161,9 +165,10 @@ public class ExitNodeProcess { int port = config.getExitPort(); serverSocket = new ServerSocket(port); running = true; + simulationStartMillis = System.currentTimeMillis(); System.out.println("Exit node started on port " + port); - System.out.println("Waiting for vehicles...\n"); + System.out.println("Waiting for vehicles...\\n"); while (running) { try { @@ -186,28 +191,54 @@ public class ExitNodeProcess { * @param clientSocket Socket da ligação estabelecida com a interseção */ private void handleIncomingConnection(Socket clientSocket) { + String clientAddress = clientSocket.getInetAddress().getHostAddress(); + System.out.println("New connection accepted from " + clientAddress); + try (SocketConnection connection = new SocketConnection(clientSocket)) { - System.out.println("New connection accepted from " + - clientSocket.getInetAddress().getHostAddress()); - while (running && connection.isConnected()) { try { + System.out.println("[Exit] Waiting for message from " + clientAddress); MessageProtocol message = connection.receiveMessage(); + System.out.println("[Exit] Received message type: " + message.getType() + + " from " + message.getSourceNode()); - if (message.getType() == MessageType.VEHICLE_TRANSFER) { - Vehicle vehicle = (Vehicle) message.getPayload(); + if (message.getType() == MessageType.SIMULATION_START) { + // Coordinator sends start time - use it instead of our local start + simulationStartMillis = ((Number) message.getPayload()).longValue(); + System.out.println("[Exit] Simulation start time synchronized"); + } else if (message.getType() == MessageType.VEHICLE_TRANSFER) { + Object payload = message.getPayload(); + System.out.println("[Exit] Payload type: " + payload.getClass().getName()); + + // Handle Gson LinkedHashMap + Vehicle vehicle; + if (payload instanceof com.google.gson.internal.LinkedTreeMap || + payload instanceof java.util.LinkedHashMap) { + String json = new com.google.gson.Gson().toJson(payload); + vehicle = new com.google.gson.Gson().fromJson(json, Vehicle.class); + } else { + vehicle = (Vehicle) payload; + } + processExitingVehicle(vehicle); } } catch (ClassNotFoundException e) { - System.err.println("Unknown message type received: " + e.getMessage()); + System.err.println("[Exit] Unknown message type: " + e.getMessage()); + e.printStackTrace(); + } catch (Exception e) { + System.err.println("[Exit] Error processing message: " + e.getMessage()); + e.printStackTrace(); } } + System.out.println("[Exit] Connection closed from " + clientAddress); + } catch (IOException e) { if (running) { - System.err.println("Connection error: " + e.getMessage()); + System.err.println("[Exit] Connection error from " + clientAddress + ": " + e.getMessage()); + e.printStackTrace(); } } } @@ -226,10 +257,14 @@ public class ExitNodeProcess { private synchronized void processExitingVehicle(Vehicle vehicle) { totalVehiclesReceived++; - double systemTime = vehicle.getTotalTravelTime(getCurrentTime()); + // Calculate relative simulation time (seconds since simulation start) + double currentSimTime = (System.currentTimeMillis() - simulationStartMillis) / 1000.0; + // System time = time vehicle spent in system (current time - entry time) + double systemTime = currentSimTime - vehicle.getEntryTime(); double waitTime = vehicle.getTotalWaitingTime(); double crossingTime = vehicle.getTotalCrossingTime(); + // Store times in seconds, will be converted to ms when sending to dashboard totalSystemTime += systemTime; totalWaitingTime += waitTime; totalCrossingTime += crossingTime; @@ -238,12 +273,11 @@ public class ExitNodeProcess { vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1); vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime); - System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs)%n", - vehicle.getId(), vehicle.getType(), systemTime, waitTime); + System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs, crossing=%.2fs)%n", + vehicle.getId(), vehicle.getType(), systemTime, waitTime, crossingTime); - if (totalVehiclesReceived % 10 == 0) { - sendStatsToDashboard(); - } + // Send stats after every vehicle to ensure dashboard updates quickly + sendStatsToDashboard(); } /** @@ -273,32 +307,42 @@ public class ExitNodeProcess { } try { - Map stats = new HashMap<>(); - stats.put("totalVehicles", totalVehiclesReceived); - stats.put("avgSystemTime", totalVehiclesReceived > 0 ? totalSystemTime / totalVehiclesReceived : 0.0); - stats.put("avgWaitingTime", totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0); - stats.put("avgCrossingTime", totalVehiclesReceived > 0 ? totalCrossingTime / totalVehiclesReceived : 0.0); + // Create stats payload + StatsUpdatePayload payload = new StatsUpdatePayload(); + + // Set global stats - convert seconds to milliseconds + payload.setTotalVehiclesCompleted(totalVehiclesReceived); + payload.setTotalSystemTime((long)(totalSystemTime * 1000.0)); // s -> ms + payload.setTotalWaitingTime((long)(totalWaitingTime * 1000.0)); // s -> ms + + // Set vehicle type stats + Map typeCounts = new HashMap<>(); + Map typeWaitTimes = new HashMap<>(); - Map typeCounts = new HashMap<>(); - Map typeAvgWait = new HashMap<>(); for (VehicleType type : VehicleType.values()) { - int count = vehicleTypeCount.get(type); - typeCounts.put(type.name(), count); - if (count > 0) { - typeAvgWait.put(type.name(), vehicleTypeWaitTime.get(type) / count); - } + typeCounts.put(type, vehicleTypeCount.get(type)); + typeWaitTimes.put(type, (long)(vehicleTypeWaitTime.get(type) * 1000.0)); // s -> ms } - stats.put("vehicleTypeCounts", typeCounts); - stats.put("vehicleTypeAvgWait", typeAvgWait); - Message message = new Message(MessageType.STATS_UPDATE, "ExitNode", "Dashboard", stats); + payload.setVehicleTypeCounts(typeCounts); + payload.setVehicleTypeWaitTimes(typeWaitTimes); + + // Send message + Message message = new Message( + MessageType.STATS_UPDATE, + "ExitNode", + "Dashboard", + payload + ); + dashboardClient.send(message); + double avgWait = totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0; System.out.printf("[Exit] Sent stats to dashboard (total=%d, avg_wait=%.2fs)%n", - totalVehiclesReceived, totalWaitingTime / totalVehiclesReceived); + totalVehiclesReceived, avgWait); - } catch (SerializationException | IOException e) { - System.err.println("Failed to send stats to dashboard: " + e.getMessage()); + } catch (Exception e) { + System.err.println("[Exit] Failed to send stats to dashboard: " + e.getMessage()); } } diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index 57c658f..0c57807 100644 --- a/main/src/main/java/sd/IntersectionProcess.java +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -12,13 +12,17 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import sd.config.SimulationConfig; +import sd.coordinator.SocketClient; +import sd.dashboard.StatsUpdatePayload; import sd.engine.TrafficLightThread; import sd.model.Intersection; +import sd.model.Message; import sd.model.MessageType; import sd.model.TrafficLight; import sd.model.Vehicle; import sd.protocol.MessageProtocol; import sd.protocol.SocketConnection; +import sd.serialization.SerializationException; /** * Main class for an Intersection Process in the distributed traffic simulation. @@ -59,6 +63,12 @@ public class IntersectionProcess { */ private volatile String currentGreenDirection; + private SocketClient dashboardClient; + private long simulationStartMillis; + private volatile int totalArrivals = 0; + private volatile int totalDepartures = 0; + private long lastStatsUpdateTime; + /** * Constructs a new IntersectionProcess. * @@ -82,6 +92,35 @@ public class IntersectionProcess { System.out.println("=".repeat(60)); } + // Main entry point for running an intersection process + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Usage: java IntersectionProcess [configFile]"); + System.err.println("Example: java IntersectionProcess Cr1"); + System.exit(1); + } + + String intersectionId = args[0]; + String configFile = args.length > 1 ? args[1] : "src/main/resources/simulation.properties"; + + try { + IntersectionProcess process = new IntersectionProcess(intersectionId, configFile); + process.initialize(); + process.start(); + + // Add shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nShutdown signal received..."); + process.shutdown(); + })); + + } catch (IOException e) { + System.err.println("Failed to start intersection process: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + public void initialize() { System.out.println("\n[" + intersectionId + "] Initializing intersection..."); @@ -89,9 +128,36 @@ public class IntersectionProcess { configureRouting(); + connectToDashboard(); + System.out.println("[" + intersectionId + "] Initialization complete."); } + /** + * Establishes connection to the dashboard server for statistics reporting. + */ + private void connectToDashboard() { + try { + String dashboardHost = config.getDashboardHost(); + int dashboardPort = config.getDashboardPort(); + + System.out.println("[" + intersectionId + "] Connecting to dashboard at " + + dashboardHost + ":" + dashboardPort + "..."); + + dashboardClient = new SocketClient(intersectionId, dashboardHost, dashboardPort); + dashboardClient.connect(); + + System.out.println("[" + intersectionId + "] Connected to dashboard."); + lastStatsUpdateTime = System.currentTimeMillis(); + + } catch (IOException e) { + System.err.println("[" + intersectionId + "] Failed to connect to dashboard: " + + e.getMessage()); + System.err.println("[" + intersectionId + "] Will continue without dashboard reporting."); + dashboardClient = null; + } + } + /** * Creates traffic lights for this intersection based on its physical * connections. @@ -222,17 +288,22 @@ public class IntersectionProcess { // Get or create connection to next destination SocketConnection connection = getOrCreateConnection(nextDestination); - // Create and send message - MessageProtocol message = new VehicleTransferMessage( + // Create and send message using Message class + MessageProtocol message = new Message( + MessageType.VEHICLE_TRANSFER, intersectionId, nextDestination, - vehicle); + vehicle, + System.currentTimeMillis()); connection.sendMessage(message); System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() + " to " + nextDestination); + // Record departure for statistics + recordVehicleDeparture(); + // Note: vehicle route is advanced when it arrives at the next intersection } catch (IOException | InterruptedException e) { @@ -374,14 +445,42 @@ public class IntersectionProcess { try { MessageProtocol message = connection.receiveMessage(); - if (message.getType() == MessageType.VEHICLE_TRANSFER) { - Vehicle vehicle = (Vehicle) message.getPayload(); + // Handle simulation start time synchronization + if (message.getType() == MessageType.SIMULATION_START) { + simulationStartMillis = ((Number) message.getPayload()).longValue(); + System.out.println("[" + intersectionId + "] Simulation start time synchronized"); + continue; + } + + // Accept both VEHICLE_TRANSFER and VEHICLE_SPAWN (from coordinator) + if (message.getType() == MessageType.VEHICLE_TRANSFER || + message.getType() == MessageType.VEHICLE_SPAWN) { + // Cast payload to Vehicle - handle Gson deserialization + Vehicle vehicle; + Object payload = message.getPayload(); + if (payload instanceof Vehicle) { + vehicle = (Vehicle) payload; + } else if (payload instanceof java.util.Map) { + // Gson deserialized as LinkedHashMap - re-serialize and deserialize as Vehicle + com.google.gson.Gson gson = new com.google.gson.Gson(); + String json = gson.toJson(payload); + vehicle = gson.fromJson(json, Vehicle.class); + } else { + System.err.println("[" + intersectionId + "] Unknown payload type: " + payload.getClass()); + continue; + } System.out.println("[" + intersectionId + "] Received vehicle: " + vehicle.getId() + " from " + message.getSourceNode()); + // Advance vehicle to next destination in its route + vehicle.advanceRoute(); + // Add vehicle to appropriate queue intersection.receiveVehicle(vehicle); + + // Record arrival for statistics + recordVehicleArrival(); } } catch (java.net.SocketTimeoutException e) { @@ -394,6 +493,13 @@ public class IntersectionProcess { System.err.println("[" + intersectionId + "] Unknown message type received: " + e.getMessage()); break; // Invalid message, close connection + } catch (IOException e) { + if (running) { + System.err.println("[" + intersectionId + "] Failed to deserialize message: " + + e.getMessage()); + e.printStackTrace(); // For debugging - maybe change//remove later + } + break; // Connection error, close connection } } @@ -459,6 +565,11 @@ public class IntersectionProcess { outgoingConnections.clear(); } + // 5. Close dashboard connection + if (dashboardClient != null) { + dashboardClient.close(); + } + System.out.println("[" + intersectionId + "] Shutdown complete."); System.out.println("============================================================\n"); } @@ -473,6 +584,73 @@ public class IntersectionProcess { return intersection; } + /** + * Records that a vehicle has arrived at this intersection. + */ + public void recordVehicleArrival() { + totalArrivals++; + checkAndSendStats(); + } + + /** + * Records that a vehicle has departed from this intersection. + */ + public void recordVehicleDeparture() { + totalDepartures++; + checkAndSendStats(); + } + + /** + * Checks if it's time to send statistics to the dashboard and sends them if needed. + */ + private void checkAndSendStats() { + long now = System.currentTimeMillis(); + long elapsed = now - lastStatsUpdateTime; + + // Send stats every 5 seconds + if (elapsed >= 5000) { + sendStatsToDashboard(); + lastStatsUpdateTime = now; + } + } + + /** + * Sends current statistics to the dashboard server. + */ + private void sendStatsToDashboard() { + if (dashboardClient == null || !dashboardClient.isConnected()) { + return; + } + + try { + // Calculate current queue size + int currentQueueSize = intersection.getTrafficLights().stream() + .mapToInt(TrafficLight::getQueueSize) + .sum(); + + StatsUpdatePayload payload = new StatsUpdatePayload() + .setIntersectionArrivals(totalArrivals) + .setIntersectionDepartures(totalDepartures) + .setIntersectionQueueSize(currentQueueSize); + + // Send StatsUpdatePayload directly as the message payload + sd.model.Message message = new sd.model.Message( + MessageType.STATS_UPDATE, + intersectionId, + "Dashboard", + payload + ); + + dashboardClient.send(message); + + System.out.printf("[%s] Sent stats to dashboard (arrivals=%d, departures=%d, queue=%d)%n", + intersectionId, totalArrivals, totalDepartures, currentQueueSize); + + } catch (SerializationException | IOException e) { + System.err.println("[" + intersectionId + "] Failed to send stats to dashboard: " + e.getMessage()); + } + } + // --- Inner class for Vehicle Transfer Messages --- /** diff --git a/main/src/main/java/sd/coordinator/CoordinatorProcess.java b/main/src/main/java/sd/coordinator/CoordinatorProcess.java index 9d25ee0..163dc5a 100644 --- a/main/src/main/java/sd/coordinator/CoordinatorProcess.java +++ b/main/src/main/java/sd/coordinator/CoordinatorProcess.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.Map; import sd.config.SimulationConfig; +import sd.dashboard.StatsUpdatePayload; import sd.model.Message; import sd.model.MessageType; import sd.model.Vehicle; @@ -24,6 +25,7 @@ public class CoordinatorProcess { private final SimulationConfig config; private final VehicleGenerator vehicleGenerator; private final Map intersectionClients; + private SocketClient dashboardClient; private double currentTime; private int vehicleCounter; private boolean running; @@ -75,6 +77,9 @@ public class CoordinatorProcess { } public void initialize() { + // Connect to dashboard first + connectToDashboard(); + System.out.println("Connecting to intersection processes..."); String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"}; @@ -108,6 +113,9 @@ public class CoordinatorProcess { System.out.println("Duration: " + duration + " seconds"); System.out.println(); + // Send simulation start time to all processes for synchronization + sendSimulationStartTime(); + nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); final double TIME_STEP = 0.1; @@ -132,6 +140,9 @@ public class CoordinatorProcess { System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n", currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute()); + // Send generation count to dashboard + sendGenerationStatsToDashboard(); + if (vehicle.getRoute().isEmpty()) { System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!"); return; @@ -201,4 +212,77 @@ public class CoordinatorProcess { System.out.println("\nStop signal received..."); running = false; } + + private void connectToDashboard() { + try { + String host = config.getDashboardHost(); + int port = config.getDashboardPort(); + + System.out.println("Connecting to dashboard at " + host + ":" + port); + dashboardClient = new SocketClient("Dashboard", host, port); + dashboardClient.connect(); + System.out.println("Successfully connected to dashboard\n"); + } catch (IOException e) { + System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage()); + System.err.println("Coordinator will continue without dashboard connection\n"); + } + } + + private void sendGenerationStatsToDashboard() { + if (dashboardClient == null || !dashboardClient.isConnected()) { + return; + } + + try { + // Create stats payload with vehicle generation count + StatsUpdatePayload payload = new StatsUpdatePayload(); + payload.setTotalVehiclesGenerated(vehicleCounter); + + Message message = new Message( + MessageType.STATS_UPDATE, + "COORDINATOR", + "Dashboard", + payload + ); + + dashboardClient.send(message); + } catch (Exception e) { //This is fine - can add IOException if need be + // Don't crash if dashboard update fails + System.err.println("Failed to send stats to dashboard: " + e.getMessage()); + } + } + + private void sendSimulationStartTime() { + long startTimeMillis = System.currentTimeMillis(); + + // Send to all intersections + for (Map.Entry entry : intersectionClients.entrySet()) { + try { + Message message = new Message( + MessageType.SIMULATION_START, + "COORDINATOR", + entry.getKey(), + startTimeMillis + ); + entry.getValue().send(message); + } catch (Exception e) { // Same thing here + System.err.println("Failed to send start time to " + entry.getKey() + ": " + e.getMessage()); + } + } + + // Send to dashboard + if (dashboardClient != null && dashboardClient.isConnected()) { + try { + Message message = new Message( + MessageType.SIMULATION_START, + "COORDINATOR", + "Dashboard", + startTimeMillis + ); + dashboardClient.send(message); + } catch (Exception e) { // And here + // Don't crash + } + } + } } diff --git a/main/src/main/java/sd/dashboard/DashboardClientHandler.java b/main/src/main/java/sd/dashboard/DashboardClientHandler.java index 11ccb17..68a52db 100644 --- a/main/src/main/java/sd/dashboard/DashboardClientHandler.java +++ b/main/src/main/java/sd/dashboard/DashboardClientHandler.java @@ -2,6 +2,7 @@ package sd.dashboard; import java.io.IOException; import java.net.Socket; +import java.util.Map; import sd.model.MessageType; import sd.protocol.MessageProtocol; @@ -71,12 +72,22 @@ public class DashboardClientHandler implements Runnable { System.out.println("[Handler] Received STATS_UPDATE from: " + senderId); - if (payload instanceof StatsUpdatePayload stats) { - updateStatistics(senderId, stats); + // Handle both direct StatsUpdatePayload and Gson-deserialized Map + StatsUpdatePayload stats; + if (payload instanceof StatsUpdatePayload) { + stats = (StatsUpdatePayload) payload; + } else if (payload instanceof java.util.Map) { + // Gson deserialized as LinkedHashMap - re-serialize and deserialize properly + com.google.gson.Gson gson = new com.google.gson.Gson(); + String json = gson.toJson(payload); + stats = gson.fromJson(json, StatsUpdatePayload.class); } else { System.err.println("[Handler] Unknown payload type: " + (payload != null ? payload.getClass().getName() : "null")); + return; } + + updateStatistics(senderId, stats); } private void updateStatistics(String senderId, StatsUpdatePayload stats) { @@ -88,14 +99,30 @@ public class DashboardClientHandler implements Runnable { statistics.updateVehiclesCompleted(stats.getTotalVehiclesCompleted()); } + // Exit Node sends cumulative totals, so we SET rather than ADD if (stats.getTotalSystemTime() >= 0) { - statistics.addSystemTime(stats.getTotalSystemTime()); + statistics.setTotalSystemTime(stats.getTotalSystemTime()); } if (stats.getTotalWaitingTime() >= 0) { - statistics.addWaitingTime(stats.getTotalWaitingTime()); + statistics.setTotalWaitingTime(stats.getTotalWaitingTime()); } + // Process vehicle type statistics (from Exit Node) + if (stats.getVehicleTypeCounts() != null && !stats.getVehicleTypeCounts().isEmpty()) { + Map counts = stats.getVehicleTypeCounts(); + Map waitTimes = stats.getVehicleTypeWaitTimes(); + + for (var entry : counts.entrySet()) { + sd.model.VehicleType type = entry.getKey(); + int count = entry.getValue(); + long waitTime = (waitTimes != null && waitTimes.containsKey(type)) + ? waitTimes.get(type) : 0L; + statistics.updateVehicleTypeStats(type, count, waitTime); + } + } + + // Process intersection statistics (from Intersection processes) if (senderId.startsWith("Cr") || senderId.startsWith("E")) { statistics.updateIntersectionStats( senderId, diff --git a/main/src/main/java/sd/dashboard/DashboardServer.java b/main/src/main/java/sd/dashboard/DashboardServer.java index 9299d0a..cca71b0 100644 --- a/main/src/main/java/sd/dashboard/DashboardServer.java +++ b/main/src/main/java/sd/dashboard/DashboardServer.java @@ -22,34 +22,51 @@ public class DashboardServer { private ServerSocket serverSocket; public static void main(String[] args) { - System.out.println("=".repeat(60)); - System.out.println("DASHBOARD SERVER - DISTRIBUTED TRAFFIC SIMULATION"); - System.out.println("=".repeat(60)); + // Check if GUI mode is requested + boolean useGUI = false; + String configFile = "src/main/resources/simulation.properties"; - try { - // Load configuration - String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties"; - System.out.println("Loading configuration from: " + configFile); + for (int i = 0; i < args.length; i++) { + if (args[i].equals("--gui") || args[i].equals("-g")) { + useGUI = true; + } else { + configFile = args[i]; + } + } + + if (useGUI) { + // Launch JavaFX UI + System.out.println("Launching Dashboard with JavaFX GUI..."); + DashboardUI.main(args); + } else { + // Traditional terminal mode + System.out.println("=".repeat(60)); + System.out.println("DASHBOARD SERVER - DISTRIBUTED TRAFFIC SIMULATION"); + System.out.println("=".repeat(60)); - SimulationConfig config = new SimulationConfig(configFile); - DashboardServer server = new DashboardServer(config); - - // Start the server - System.out.println("\n" + "=".repeat(60)); - server.start(); - - // Keep running until interrupted - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - System.out.println("\n\nShutdown signal received..."); - server.stop(); - })); - - // Display statistics periodically - server.displayLoop(); - - } catch (IOException e) { - System.err.println("Failed to start Dashboard Server: " + e.getMessage()); - System.exit(1); + try { + System.out.println("Loading configuration from: " + configFile); + + SimulationConfig config = new SimulationConfig(configFile); + DashboardServer server = new DashboardServer(config); + + // Start the server + System.out.println("\n" + "=".repeat(60)); + server.start(); + + // Keep running until interrupted + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\n\nShutdown signal received..."); + server.stop(); + })); + + // Display statistics periodically + server.displayLoop(); + + } catch (IOException e) { + System.err.println("Failed to start Dashboard Server: " + e.getMessage()); + System.exit(1); + } } } diff --git a/main/src/main/java/sd/dashboard/DashboardStatistics.java b/main/src/main/java/sd/dashboard/DashboardStatistics.java index 56f227c..da6f097 100644 --- a/main/src/main/java/sd/dashboard/DashboardStatistics.java +++ b/main/src/main/java/sd/dashboard/DashboardStatistics.java @@ -68,11 +68,21 @@ public class DashboardStatistics { updateTimestamp(); } + public void setTotalSystemTime(long timeMs) { + totalSystemTime.set(timeMs); + updateTimestamp(); + } + public void addWaitingTime(long timeMs) { totalWaitingTime.addAndGet(timeMs); updateTimestamp(); } + public void setTotalWaitingTime(long timeMs) { + totalWaitingTime.set(timeMs); + updateTimestamp(); + } + public void updateVehicleTypeStats(VehicleType type, int count, long waitTimeMs) { vehicleTypeCount.get(type).set(count); vehicleTypeWaitTime.get(type).set(waitTimeMs); diff --git a/main/src/main/java/sd/dashboard/DashboardUI.java b/main/src/main/java/sd/dashboard/DashboardUI.java new file mode 100644 index 0000000..df544d7 --- /dev/null +++ b/main/src/main/java/sd/dashboard/DashboardUI.java @@ -0,0 +1,378 @@ +package sd.dashboard; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TitledPane; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.stage.Stage; +import sd.config.SimulationConfig; +import sd.model.VehicleType; + +/** + * JavaFX-based Dashboard UI for displaying real-time simulation statistics. + * Provides a graphical interface with auto-updating statistics panels. + */ +public class DashboardUI extends Application { + + private DashboardServer server; + private DashboardStatistics statistics; + + // Global Statistics Labels + private Label lblVehiclesGenerated; + private Label lblVehiclesCompleted; + private Label lblVehiclesInTransit; + private Label lblAvgSystemTime; + private Label lblAvgWaitingTime; + private Label lblLastUpdate; + + // Vehicle Type Table + private TableView vehicleTypeTable; + + // Intersection Table + private TableView intersectionTable; + + // Update scheduler + private ScheduledExecutorService updateScheduler; + + @Override + public void start(Stage primaryStage) { + try { + // Initialize server + String configFile = getParameters().getRaw().isEmpty() + ? "src/main/resources/simulation.properties" + : getParameters().getRaw().get(0); + + SimulationConfig config = new SimulationConfig(configFile); + server = new DashboardServer(config); + statistics = server.getStatistics(); + + // Start the dashboard server + server.start(); + + // Build UI + BorderPane root = new BorderPane(); + root.setStyle("-fx-background-color: #f5f5f5;"); + + // Header + VBox header = createHeader(); + root.setTop(header); + + // Main content + VBox mainContent = createMainContent(); + root.setCenter(mainContent); + + // Footer + HBox footer = createFooter(); + root.setBottom(footer); + + // Create scene + Scene scene = new Scene(root, 1200, 800); + primaryStage.setTitle("Traffic Simulation Dashboard - Real-time Statistics"); + primaryStage.setScene(scene); + primaryStage.show(); + + // Start periodic updates + startPeriodicUpdates(); + + // Handle window close + primaryStage.setOnCloseRequest(event -> { + shutdown(); + }); + + } catch (IOException e) { + showErrorAlert("Failed to start Dashboard Server", e.getMessage()); + Platform.exit(); + } + } + + private VBox createHeader() { + VBox header = new VBox(10); + header.setPadding(new Insets(20)); + header.setStyle("-fx-background-color: linear-gradient(to right, #2c3e50, #3498db);"); + + Label title = new Label("DISTRIBUTED TRAFFIC SIMULATION DASHBOARD"); + title.setFont(Font.font("Arial", FontWeight.BOLD, 28)); + title.setTextFill(Color.WHITE); + + Label subtitle = new Label("Real-time Statistics and Monitoring"); + subtitle.setFont(Font.font("Arial", FontWeight.NORMAL, 16)); + subtitle.setTextFill(Color.web("#ecf0f1")); + + header.getChildren().addAll(title, subtitle); + header.setAlignment(Pos.CENTER); + + return header; + } + + private VBox createMainContent() { + VBox mainContent = new VBox(15); + mainContent.setPadding(new Insets(20)); + + // Global Statistics Panel + TitledPane globalStatsPane = createGlobalStatisticsPanel(); + + // Vehicle Type Statistics Panel + TitledPane vehicleTypePane = createVehicleTypePanel(); + + // Intersection Statistics Panel + TitledPane intersectionPane = createIntersectionPanel(); + + mainContent.getChildren().addAll(globalStatsPane, vehicleTypePane, intersectionPane); + + return mainContent; + } + + private TitledPane createGlobalStatisticsPanel() { + GridPane grid = new GridPane(); + grid.setPadding(new Insets(15)); + grid.setHgap(20); + grid.setVgap(15); + grid.setStyle("-fx-background-color: white; -fx-border-radius: 5;"); + + // Initialize labels + lblVehiclesGenerated = createStatLabel("0"); + lblVehiclesCompleted = createStatLabel("0"); + lblVehiclesInTransit = createStatLabel("0"); + lblAvgSystemTime = createStatLabel("0.00 ms"); + lblAvgWaitingTime = createStatLabel("0.00 ms"); + + // Add labels with descriptions + addStatRow(grid, 0, "Total Vehicles Generated:", lblVehiclesGenerated); + addStatRow(grid, 1, "Total Vehicles Completed:", lblVehiclesCompleted); + addStatRow(grid, 2, "Vehicles In Transit:", lblVehiclesInTransit); + addStatRow(grid, 3, "Average System Time:", lblAvgSystemTime); + addStatRow(grid, 4, "Average Waiting Time:", lblAvgWaitingTime); + + TitledPane pane = new TitledPane("Global Statistics", grid); + pane.setCollapsible(false); + pane.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + return pane; + } + + private TitledPane createVehicleTypePanel() { + vehicleTypeTable = new TableView<>(); + vehicleTypeTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + vehicleTypeTable.setPrefHeight(200); + + TableColumn typeCol = new TableColumn<>("Vehicle Type"); + typeCol.setCellValueFactory(new PropertyValueFactory<>("vehicleType")); + typeCol.setPrefWidth(200); + + TableColumn countCol = new TableColumn<>("Count"); + countCol.setCellValueFactory(new PropertyValueFactory<>("count")); + countCol.setPrefWidth(150); + + TableColumn avgWaitCol = new TableColumn<>("Avg Wait Time"); + avgWaitCol.setCellValueFactory(new PropertyValueFactory<>("avgWaitTime")); + avgWaitCol.setPrefWidth(150); + + vehicleTypeTable.getColumns().addAll(typeCol, countCol, avgWaitCol); + + TitledPane pane = new TitledPane("Vehicle Type Statistics", vehicleTypeTable); + pane.setCollapsible(false); + pane.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + return pane; + } + + private TitledPane createIntersectionPanel() { + intersectionTable = new TableView<>(); + intersectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + intersectionTable.setPrefHeight(250); + + TableColumn idCol = new TableColumn<>("Intersection ID"); + idCol.setCellValueFactory(new PropertyValueFactory<>("intersectionId")); + idCol.setPrefWidth(200); + + TableColumn arrivalsCol = new TableColumn<>("Total Arrivals"); + arrivalsCol.setCellValueFactory(new PropertyValueFactory<>("arrivals")); + arrivalsCol.setPrefWidth(150); + + TableColumn departuresCol = new TableColumn<>("Total Departures"); + departuresCol.setCellValueFactory(new PropertyValueFactory<>("departures")); + departuresCol.setPrefWidth(150); + + TableColumn queueCol = new TableColumn<>("Current Queue"); + queueCol.setCellValueFactory(new PropertyValueFactory<>("queueSize")); + queueCol.setPrefWidth(150); + + intersectionTable.getColumns().addAll(idCol, arrivalsCol, departuresCol, queueCol); + + TitledPane pane = new TitledPane("Intersection Statistics", intersectionTable); + pane.setCollapsible(false); + pane.setFont(Font.font("Arial", FontWeight.BOLD, 16)); + + return pane; + } + + private HBox createFooter() { + HBox footer = new HBox(10); + footer.setPadding(new Insets(10, 20, 10, 20)); + footer.setStyle("-fx-background-color: #34495e;"); + footer.setAlignment(Pos.CENTER_LEFT); + + Label statusLabel = new Label("Status:"); + statusLabel.setTextFill(Color.WHITE); + statusLabel.setFont(Font.font("Arial", FontWeight.BOLD, 12)); + + Circle statusIndicator = new Circle(6); + statusIndicator.setFill(Color.LIME); + + Label statusText = new Label("Connected and Receiving Data"); + statusText.setTextFill(Color.WHITE); + statusText.setFont(Font.font("Arial", 12)); + + lblLastUpdate = new Label("Last Update: --:--:--"); + lblLastUpdate.setTextFill(Color.web("#ecf0f1")); + lblLastUpdate.setFont(Font.font("Arial", 12)); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + footer.getChildren().addAll(statusLabel, statusIndicator, statusText, spacer, lblLastUpdate); + + return footer; + } + + private Label createStatLabel(String initialValue) { + Label label = new Label(initialValue); + label.setFont(Font.font("Arial", FontWeight.BOLD, 20)); + label.setTextFill(Color.web("#2980b9")); + return label; + } + + private void addStatRow(GridPane grid, int row, String description, Label valueLabel) { + Label descLabel = new Label(description); + descLabel.setFont(Font.font("Arial", FontWeight.NORMAL, 14)); + descLabel.setTextFill(Color.web("#34495e")); + + grid.add(descLabel, 0, row); + grid.add(valueLabel, 1, row); + } + + private void startPeriodicUpdates() { + updateScheduler = Executors.newSingleThreadScheduledExecutor(); + updateScheduler.scheduleAtFixedRate(() -> { + Platform.runLater(this::updateUI); + }, 0, 5, TimeUnit.SECONDS); + } + + private void updateUI() { + // Update global statistics + lblVehiclesGenerated.setText(String.valueOf(statistics.getTotalVehiclesGenerated())); + lblVehiclesCompleted.setText(String.valueOf(statistics.getTotalVehiclesCompleted())); + lblVehiclesInTransit.setText(String.valueOf( + statistics.getTotalVehiclesGenerated() - statistics.getTotalVehiclesCompleted())); + lblAvgSystemTime.setText(String.format("%.2f ms", statistics.getAverageSystemTime())); + lblAvgWaitingTime.setText(String.format("%.2f ms", statistics.getAverageWaitingTime())); + lblLastUpdate.setText(String.format("Last Update: %tT", statistics.getLastUpdateTime())); + + // Update vehicle type table + vehicleTypeTable.getItems().clear(); + for (VehicleType type : VehicleType.values()) { + int count = statistics.getVehicleTypeCount(type); + double avgWait = statistics.getAverageWaitingTimeByType(type); + vehicleTypeTable.getItems().add(new VehicleTypeRow( + type.toString(), count, String.format("%.2f ms", avgWait))); + } + + // Update intersection table + intersectionTable.getItems().clear(); + Map intersectionStats = + statistics.getAllIntersectionStats(); + for (DashboardStatistics.IntersectionStats stats : intersectionStats.values()) { + intersectionTable.getItems().add(new IntersectionRow( + stats.getIntersectionId(), + stats.getTotalArrivals(), + stats.getTotalDepartures(), + stats.getCurrentQueueSize() + )); + } + } + + private void shutdown() { + System.out.println("Shutting down Dashboard UI..."); + + if (updateScheduler != null && !updateScheduler.isShutdown()) { + updateScheduler.shutdownNow(); + } + + if (server != null) { + server.stop(); + } + + Platform.exit(); + } + + private void showErrorAlert(String title, String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + public static void main(String[] args) { + launch(args); + } + + // Inner classes for TableView data models + public static class VehicleTypeRow { + private final String vehicleType; + private final int count; + private final String avgWaitTime; + + public VehicleTypeRow(String vehicleType, int count, String avgWaitTime) { + this.vehicleType = vehicleType; + this.count = count; + this.avgWaitTime = avgWaitTime; + } + + public String getVehicleType() { return vehicleType; } + public int getCount() { return count; } + public String getAvgWaitTime() { return avgWaitTime; } + } + + public static class IntersectionRow { + private final String intersectionId; + private final int arrivals; + private final int departures; + private final int queueSize; + + public IntersectionRow(String intersectionId, int arrivals, int departures, int queueSize) { + this.intersectionId = intersectionId; + this.arrivals = arrivals; + this.departures = departures; + this.queueSize = queueSize; + } + + public String getIntersectionId() { return intersectionId; } + public int getArrivals() { return arrivals; } + public int getDepartures() { return departures; } + public int getQueueSize() { return queueSize; } + } +} diff --git a/main/src/main/java/sd/model/Message.java b/main/src/main/java/sd/model/Message.java index 0217070..87e1200 100644 --- a/main/src/main/java/sd/model/Message.java +++ b/main/src/main/java/sd/model/Message.java @@ -1,14 +1,15 @@ package sd.model; -import java.io.Serializable; import java.util.UUID; +import sd.protocol.MessageProtocol; + /** * Represents a message exchanged between processes in the distributed simulation. * Each message has a unique ID, a type, a sender, a destination, and a payload. - * This class implements {@link Serializable} to allow transmission over the network. + * This class implements {@link MessageProtocol} which extends Serializable for network transmission. */ -public class Message implements Serializable { +public class Message implements MessageProtocol { private static final long serialVersionUID = 1L; @@ -132,6 +133,17 @@ public class Message implements Serializable { return (T) payload; } + // Impl MessageProtocol interface + @Override + public String getSourceNode() { + return senderId; + } + + @Override + public String getDestinationNode() { + return destinationId; + } + @Override public String toString() { return String.format("Message[id=%s, type=%s, from=%s, to=%s, timestamp=%d]", diff --git a/main/src/main/java/sd/model/MessageType.java b/main/src/main/java/sd/model/MessageType.java index 76cb067..a9f4794 100644 --- a/main/src/main/java/sd/model/MessageType.java +++ b/main/src/main/java/sd/model/MessageType.java @@ -19,6 +19,12 @@ public enum MessageType { */ STATS_UPDATE, + /** + * Message to synchronize simulation start time across all processes. + * Payload: Start timestamp (long milliseconds) + */ + SIMULATION_START, + /** * Message to synchronize traffic light states between processes. * Payload: TrafficLight state and timing information diff --git a/main/src/main/java/sd/model/TrafficLight.java b/main/src/main/java/sd/model/TrafficLight.java index 1007c03..7cfd393 100644 --- a/main/src/main/java/sd/model/TrafficLight.java +++ b/main/src/main/java/sd/model/TrafficLight.java @@ -1,6 +1,8 @@ package sd.model; +import java.util.HashMap; import java.util.LinkedList; +import java.util.Map; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; @@ -93,6 +95,12 @@ public class TrafficLight { * been dequeued (processed) by this light. */ private int totalVehiclesProcessed; + + /** + * Track when vehicles arrive at this light for wait time calculation. + * Maps vehicle ID to arrival timestamp (milliseconds). + */ + private final Map vehicleArrivalTimes; /** * Constructs a new TrafficLight. @@ -115,6 +123,7 @@ public class TrafficLight { this.greenTime = greenTime; this.redTime = redTime; + this.vehicleArrivalTimes = new HashMap<>(); this.totalVehiclesProcessed = 0; } @@ -128,6 +137,7 @@ public class TrafficLight { lock.lock(); // Acquire the lock try { queue.offer(vehicle); // Add vehicle to queue + vehicleArrivalTimes.put(vehicle.getId(), System.currentTimeMillis()); vehicleAdded.signalAll(); // Signal (for concurrent models) } finally { lock.unlock(); // Always release the lock @@ -152,6 +162,13 @@ public class TrafficLight { Vehicle vehicle = queue.poll(); // Remove vehicle from queue if (vehicle != null) { totalVehiclesProcessed++; + + // Calculate wait time (time spent in queue) + Long arrivalTime = vehicleArrivalTimes.remove(vehicle.getId()); + if (arrivalTime != null) { + double waitTimeSeconds = (System.currentTimeMillis() - arrivalTime) / 1000.0; + vehicle.addWaitingTime(waitTimeSeconds); + } } return vehicle; } diff --git a/main/src/main/java/sd/protocol/SocketConnection.java b/main/src/main/java/sd/protocol/SocketConnection.java index 446281c..12feecb 100644 --- a/main/src/main/java/sd/protocol/SocketConnection.java +++ b/main/src/main/java/sd/protocol/SocketConnection.java @@ -172,8 +172,8 @@ public class SocketConnection implements Closeable { byte[] data = new byte[length]; dataIn.readFully(data); - // Deserialize do JSON - return serializer.deserialize(data, MessageProtocol.class); + // Deserialize do JSON - use concrete Message class, not interface + return serializer.deserialize(data, sd.model.Message.class); } catch (SerializationException e) { throw new IOException("Failed to deserialize message", e); diff --git a/main/start.sh b/main/start.sh new file mode 100755 index 0000000..d710bd7 --- /dev/null +++ b/main/start.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Distributed Traffic Simulation Startup Script + +# kill java +echo "-> Cleaning up existing processes..." +pkill -9 java 2>/dev/null +sleep 2 + +# build +echo "-> Building project..." +cd "$(dirname "$0")" +mvn package -DskipTests -q +if [ $? -ne 0 ]; then + echo "XXX Build failed! XXX" + exit 1 +fi +echo "-> Build complete" +echo "" + +# start gui +echo "-> Starting JavaFX Dashboard..." +mvn javafx:run & +DASHBOARD_PID=$! +sleep 3 + +# acho que é assim idk +echo "-> Starting 5 Intersection processes..." +for id in Cr1 Cr2 Cr3 Cr4 Cr5; do + java -cp target/classes:target/main-1.0-SNAPSHOT.jar sd.IntersectionProcess $id > /tmp/$(echo $id | tr '[:upper:]' '[:lower:]').log 2>&1 & + echo "[SUCCESS] Started $id" +done +sleep 2 + +# exit +echo "-> Starting Exit Node..." +java -cp target/classes:target/main-1.0-SNAPSHOT.jar sd.ExitNodeProcess > /tmp/exit.log 2>&1 & +sleep 1 + +# coordinator +echo "-> Starting Coordinator..." +java -cp target/classes:target/main-1.0-SNAPSHOT.jar sd.coordinator.CoordinatorProcess > /tmp/coordinator.log 2>&1 & +sleep 1 + +echo "" +echo "-> All processes started!" +echo "" +echo "-> System Status:" +ps aux | grep "java.*sd\." | grep -v grep | wc -l | xargs -I {} echo " {} Java processes running" +echo "" +echo " IMPORTANT: Keep the JavaFX Dashboard window OPEN for 60+ seconds" +echo " to see live updates! The simulation runs for 60 seconds." +echo "" +echo "-> Logs available at:" +echo " Dashboard: Check JavaFX window (live updates)" +echo " Intersections: /tmp/cr*.log" +echo " Exit Node: /tmp/exit.log" +echo " Coordinator: /tmp/coordinator.log" +echo "" +echo "-> To stop all processes: pkill -9 java" +echo ""