@@ -55,7 +76,7 @@
- sd.Entry
+ sd.dashboard.Launcher
diff --git a/main/src/main/java/sd/Entry.java b/main/src/main/java/sd/Entry.java
deleted file mode 100644
index 323ce66..0000000
--- a/main/src/main/java/sd/Entry.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package sd;
-
-import java.io.IOException;
-
-import sd.config.SimulationConfig;
-import sd.engine.SimulationEngine;
-
-/**
- * Main entry point for the traffic simulation.
- * * This class is responsible for loading the simulation configuration,
- * initializing the {@link SimulationEngine}, and starting the simulation run.
- * It also prints initial configuration details and final execution time.
- */
-public class Entry {
-
- /**
- * The default path to the simulation configuration file.
- * This is used if no command-line arguments are provided.
- */
- private static final String DEFAULT_CONFIG_FILE = "src/main/resources/simulation.properties";
-
- /**
- * The main method to start the simulation.
- * * @param args Command-line arguments. If provided, args[0] is expected
- * to be the path to a custom configuration file.
- */
- public static void main(String[] args) {
- System.out.println("=".repeat(60));
- System.out.println("TRAFFIC SIMULATION - DISCRETE EVENT SIMULATOR");
- System.out.println("=".repeat(60));
-
- try {
- // 1. Load configuration
- String configFile = args.length > 0 ? args[0] : DEFAULT_CONFIG_FILE;
- System.out.println("Loading configuration from: " + configFile);
-
- SimulationConfig config = new SimulationConfig(configFile);
-
- // 2. Display configuration
- displayConfiguration(config);
-
- // 3. Create and initialize simulation engine
- SimulationEngine engine = new SimulationEngine(config);
- engine.initialize();
-
- System.out.println("\n" + "=".repeat(60));
-
- // 4. Run simulation
- long startTime = System.currentTimeMillis();
- engine.run();
- long endTime = System.currentTimeMillis();
-
- // 5. Display execution time
- double executionTime = (endTime - startTime) / 1000.0;
- System.out.println("\nExecution time: " + String.format("%.2f", executionTime) + " seconds");
- System.out.println("=".repeat(60));
-
- } catch (IOException e) {
- System.err.println("Error loading configuration: " + e.getMessage());
- e.printStackTrace();
- } catch (Exception e) {
- System.err.println("Error during simulation: " + e.getMessage());
- e.printStackTrace();
- }
- }
-
- /**
- * Displays the main configuration parameters to the console.
- * This provides a summary of the simulation settings before it starts.
- *
- * @param config The {@link SimulationConfig} object containing the loaded settings.
- */
- private static void displayConfiguration(SimulationConfig config) {
- System.out.println("\nSIMULATION CONFIGURATION:");
- System.out.println(" Duration: " + config.getSimulationDuration() + " seconds");
- System.out.println(" Arrival Model: " + config.getArrivalModel());
-
- if ("POISSON".equalsIgnoreCase(config.getArrivalModel())) {
- System.out.println(" Arrival Rate (λ): " + config.getArrivalRate() + " vehicles/second");
- } else {
- System.out.println(" Fixed Interval: " + config.getFixedArrivalInterval() + " seconds");
- }
-
- System.out.println(" Statistics Update Interval: " + config.getStatisticsUpdateInterval() + " seconds");
-
- System.out.println("\nVEHICLE TYPES:");
- System.out.println(" Bike: " + (config.getBikeVehicleProbability() * 100) + "% " +
- "(crossing time: " + config.getBikeVehicleCrossingTime() + "s)");
- System.out.println(" Light: " + (config.getLightVehicleProbability() * 100) + "% " +
- "(crossing time: " + config.getLightVehicleCrossingTime() + "s)");
- System.out.println(" Heavy: " + (config.getHeavyVehicleProbability() * 100) + "% " +
- "(crossing time: " + config.getHeavyVehicleCrossingTime() + "s)");
- }
-}
\ No newline at end of file
diff --git a/main/src/main/java/sd/ExitNodeProcess.java b/main/src/main/java/sd/ExitNodeProcess.java
new file mode 100644
index 0000000..a5868e5
--- /dev/null
+++ b/main/src/main/java/sd/ExitNodeProcess.java
@@ -0,0 +1,532 @@
+package sd;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import sd.config.SimulationConfig;
+import sd.coordinator.SocketClient;
+import sd.dashboard.StatsUpdatePayload;
+import sd.des.DESEventType;
+import sd.des.EventQueue;
+import sd.des.SimulationClock;
+import sd.des.SimulationEvent;
+import sd.logging.EventLogger;
+import sd.logging.EventType;
+import sd.logging.VehicleTracer;
+import sd.model.Message;
+import sd.model.MessageType;
+import sd.model.Vehicle;
+import sd.model.VehicleType;
+import sd.protocol.MessageProtocol;
+import sd.protocol.SocketConnection;
+
+/**
+ * Ponto terminal da malha de simulação (Sink Node).
+ *
+ * Este processo atua como o sumidouro da rede de filas. A sua função primária é
+ * a coleta de telemetria final. Diferente das interseções, não encaminha veículos;
+ * em vez disso, retira-os do sistema, calcula as métricas de latência "end-to-end"
+ * (tempo no sistema, tempo de espera acumulado) e reporta ao Dashboard.
+ *
+ * Arquitetura de Concorrência:
+ * Utiliza um {@link ServerSocket} multithreaded para aceitar conexões simultâneas de
+ * qualquer interseção de fronteira (Cr1, Cr5, etc.) que envie veículos para fora da malha.
+ */
+public class ExitNodeProcess {
+
+ // --- Configuration and Networking ---
+ private final SimulationConfig config;
+ private ServerSocket serverSocket;
+
+ /** Pool de threads elástica para tratamento de conexões de entrada. */
+ private final ExecutorService connectionHandlerPool;
+
+ // DES components
+ private final SimulationClock clock;
+ private final EventQueue eventQueue;
+ private final EventLogger eventLogger;
+ private Thread eventProcessorThread;
+
+ /** Flag de controlo (volatile para visibilidade entre threads de I/O e lógica). */
+ private volatile boolean running;
+
+ /** Instante de início da simulação (milissegundos) sincronizado com o Coordenador. */
+ private long simulationStartMillis;
+
+ /** Contador atómico (via synchronized) de throughput total. */
+ private int totalVehiclesReceived;
+
+ /** Tempo acumulado no sistema (System Time) de todos os veículos. */
+ private double totalSystemTime;
+
+ /** Tempo acumulado em espera (Waiting Time) de todos os veículos. */
+ private double totalWaitingTime;
+
+ /** Tempo acumulado em travessia (Service Time) de todos os veículos. */
+ private double totalCrossingTime;
+
+ /** Agregação por categoria de veículo. */
+ private final Map vehicleTypeCount;
+
+ /** Latência acumulada por categoria. */
+ private final Map vehicleTypeWaitTime;
+
+ /** Cliente TCP persistente para push de métricas ao Dashboard. */
+ private SocketClient dashboardClient;
+
+ /**
+ * Bootstrap do processo ExitNode.
+ * Carrega configuração, inicializa subsistemas e entra no loop de serviço.
+ * * @param args Argumentos de CLI (caminho do config).
+ */
+ public static void main(String[] args) {
+ System.out.println("=".repeat(60));
+ System.out.println("EXIT NODE PROCESS");
+ System.out.println("=".repeat(60));
+
+ try {
+ EventLogger.getInstance().log(EventType.PROCESS_STARTED, "ExitNode", "Exit node process started");
+
+ String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
+ System.out.println("Loading configuration from: " + configFile);
+
+ SimulationConfig config = new SimulationConfig(configFile);
+ ExitNodeProcess exitNode = new ExitNodeProcess(config);
+
+ System.out.println("\n" + "=".repeat(60));
+ exitNode.initialize();
+
+ System.out.println("\n" + "=".repeat(60));
+ exitNode.start();
+
+ } catch (IOException e) {
+ System.err.println("Failed to start exit node: " + e.getMessage());
+ EventLogger.getInstance().logError("ExitNode", "Failed to start", e);
+ System.exit(1);
+ } catch (Exception e) {
+ System.err.println("Exit node error: " + e.getMessage());
+ EventLogger.getInstance().logError("ExitNode", "Exit node error", e);
+ System.exit(1);
+ } finally {
+ EventLogger.getInstance().log(EventType.PROCESS_STOPPED, "ExitNode", "Exit node process stopped");
+ }
+ }
+
+ /**
+ * Instancia o nó de saída.
+ * Prepara os acumuladores estatísticos e a infraestrutura de logging distribuído.
+ * * @param config A configuração global da simulação.
+ */
+ public ExitNodeProcess(SimulationConfig config) {
+ this.config = config;
+ this.connectionHandlerPool = Executors.newCachedThreadPool();
+ this.running = false;
+
+ this.totalVehiclesReceived = 0;
+ this.totalSystemTime = 0.0;
+ this.totalWaitingTime = 0.0;
+ this.totalCrossingTime = 0.0;
+ this.vehicleTypeCount = new HashMap<>();
+ this.vehicleTypeWaitTime = new HashMap<>();
+
+ // Inicializa os counters para cada tipo de veículo
+ for (VehicleType type : VehicleType.values()) {
+ vehicleTypeCount.put(type, 0);
+ vehicleTypeWaitTime.put(type, 0.0);
+ }
+
+ // Initialize DES components
+ this.clock = new SimulationClock();
+ this.eventQueue = new EventQueue(true); // Track history
+ this.eventLogger = EventLogger.getInstance();
+
+ eventLogger.log(EventType.PROCESS_STARTED, "ExitNode",
+ "Exit node initialized with DES architecture");
+
+ System.out.println("Exit node initialized (DES Mode)");
+ System.out.println(" - Exit port: " + config.getExitPort());
+ System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
+ }
+
+ /**
+ * Estabelece o canal de controlo (Control Plane) com o Dashboard.
+ * Essencial para a visualização em tempo real das métricas de saída.
+ */
+ public void initialize() {
+ System.out.println("Connecting to dashboard...");
+
+ try {
+ String host = config.getDashboardHost();
+ int port = config.getDashboardPort();
+
+ dashboardClient = new SocketClient("Dashboard", host, port);
+ dashboardClient.connect();
+
+ System.out.println("Successfully connected to dashboard");
+ } catch (IOException e) {
+ System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
+ System.err.println("Exit node will continue without dashboard connection");
+ }
+ }
+
+ /**
+ * Inicia a thread de processamento de eventos DES.
+ * Embora o ExitNode seja primariamente reativo (Network-driven), o motor DES
+ * é mantido para consistência de relógio e agendamento de fim de simulação.
+ */
+ private void startEventProcessor() {
+ eventProcessorThread = new Thread(() -> {
+ eventLogger.log(EventType.SIMULATION_STARTED, "ExitNode",
+ "Event processor thread started");
+
+ // Keep running while process is active
+ while (running) {
+ SimulationEvent event = eventQueue.poll();
+ if (event == null) {
+ // No events currently, wait before checking again
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ continue;
+ }
+
+ // Advance clock to event time
+ clock.advanceTo(event.getTimestamp());
+
+ // Process the event
+ processEvent(event);
+ }
+
+ eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode",
+ String.format("Event processor thread terminated at time %.2f", clock.getCurrentTime()));
+ }, "EventProcessor-ExitNode");
+
+ eventProcessorThread.start();
+ }
+
+ /**
+ * Dispatcher de eventos discretos.
+ * Trata eventos de fim de simulação. Chegadas de veículos são tratadas via Socket.
+ */
+ private void processEvent(SimulationEvent event) {
+ try {
+ switch (event.getType()) {
+ case VEHICLE_EXIT:
+ // Vehicle exits are handled via network messages in real-time
+ // This event type can be used for scheduled vehicle processing
+ break;
+
+ case SIMULATION_END:
+ handleSimulationEndEvent(event);
+ break;
+
+ default:
+ System.err.println("[ExitNode] Unknown event type: " + event.getType());
+ }
+ } catch (Exception e) {
+ System.err.println("[ExitNode] Error processing event " + event.getType() +
+ " at time " + event.getTimestamp() + ": " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Executa a lógica de encerramento desencadeada pelo evento DES.
+ */
+ private void handleSimulationEndEvent(SimulationEvent event) {
+ eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode",
+ String.format("Simulation ended at time %.2f", event.getTimestamp()));
+ running = false;
+
+ // Print final statistics
+ printFinalStatistics();
+ }
+
+ /**
+ * Exporta o histórico completo de eventos para auditoria.
+ * Requisito funcional para verificação de trace.
+ */
+ public void exportEventHistory(String outputPath) {
+ String history = eventQueue.exportEventHistory();
+ try (java.io.PrintWriter writer = new java.io.PrintWriter(outputPath)) {
+ writer.println(history);
+ System.out.println("[ExitNode] Event history exported to: " + outputPath);
+ } catch (java.io.FileNotFoundException e) {
+ System.err.println("[ExitNode] Failed to export event history: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Agenda o fim determinístico da simulação.
+ * * @param endTime Tempo virtual de paragem.
+ */
+ public void scheduleSimulationEnd(double endTime) {
+ SimulationEvent endEvent = new SimulationEvent(
+ endTime,
+ DESEventType.SIMULATION_END,
+ null);
+ eventQueue.schedule(endEvent);
+ System.out.println("[ExitNode] Simulation end scheduled at time " + endTime);
+ }
+
+ /**
+ * Inicia o servidor TCP em modo de bloqueio (Blocking I/O).
+ * @throws IOException Se ocorrer erro no bind da porta.
+ */
+ public void start() throws IOException {
+ start(true); // Default to DES mode
+ }
+
+ /**
+ * Inicia o processo com opção de ativar o rastreio DES.
+ * * @param useDES Se verdadeiro, ativa a thread do processador de eventos.
+ */
+ public void start(boolean useDES) throws IOException {
+ int port = config.getExitPort();
+ serverSocket = new ServerSocket(port);
+ running = true;
+ simulationStartMillis = System.currentTimeMillis();
+
+ System.out.println("Exit node started on port " + port);
+ if (useDES) {
+ System.out.println("Running in DES mode (event history tracking enabled)");
+ }
+ System.out.println("Waiting for vehicles...\\n");
+
+ // Loop de aceitação principal
+ while (running) {
+ try {
+ Socket clientSocket = serverSocket.accept();
+ // Delega o processamento da conexão para o Thread Pool
+ connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
+ } catch (IOException e) {
+ if (running) {
+ System.err.println("Error accepting connection: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Worker method para tratar uma conexão persistente vinda de uma interseção.
+ *
+ * Mantém o socket aberto e consome mensagens num loop até que a conexão seja fechada
+ * pelo remetente. Responsável pela desserialização polimórfica (JSON/Gson).
+ * * @param clientSocket O socket conectado.
+ */
+ private void handleIncomingConnection(Socket clientSocket) {
+ String clientAddress = clientSocket.getInetAddress().getHostAddress();
+ System.out.println("New connection accepted from " + clientAddress);
+
+ try (SocketConnection connection = new SocketConnection(clientSocket)) {
+
+ 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.SIMULATION_START) {
+ // Sincronização de relógio com o Coordenador
+ 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());
+
+ // Tratamento de artefatos de desserialização do Gson (LinkedTreeMap -> POJO)
+ 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("[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("[Exit] Connection error from " + clientAddress + ": " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Processa atomicamente a saída de um veículo.
+ *
+ * Secção Crítica: Método {@code synchronized} para garantir que a atualização
+ * das estatísticas globais (totalSystemTime, contadores) é atómica, prevenindo
+ * Race Conditions quando múltiplos veículos chegam simultaneamente de interseções diferentes.
+ * * @param vehicle O veículo que completou a rota.
+ */
+ private synchronized void processExitingVehicle(Vehicle vehicle) {
+ totalVehiclesReceived++;
+
+ // Cálculo de métricas finais baseadas no tempo virtual de simulação acumulado no veículo
+ double waitTime = vehicle.getTotalWaitingTime();
+ double crossingTime = vehicle.getTotalCrossingTime();
+ double systemTime = waitTime + crossingTime;
+
+ totalSystemTime += systemTime;
+ totalWaitingTime += waitTime;
+ totalCrossingTime += crossingTime;
+
+ VehicleType type = vehicle.getType();
+ 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, crossing=%.2fs)%n",
+ vehicle.getId(), vehicle.getType(), systemTime, waitTime, crossingTime);
+
+ // Logging estruturado
+ EventLogger.getInstance().logVehicle(EventType.VEHICLE_EXITED, "ExitNode", vehicle.getId(),
+ String.format("Completed - System: %.2fs, Wait: %.2fs, Crossing: %.2fs", systemTime, waitTime,
+ crossingTime));
+
+ // Finaliza o trace individual do veículo
+ VehicleTracer.getInstance().logExit(vehicle, systemTime);
+
+ // Push imediato para o Dashboard para visualização em tempo real
+ sendStatsToDashboard();
+ }
+
+ /**
+ * Constrói e transmite o DTO de atualização de estatísticas.
+ */
+ private void sendStatsToDashboard() {
+ if (dashboardClient == null || !dashboardClient.isConnected()) {
+ return;
+ }
+
+ try {
+ // Create stats payload
+ StatsUpdatePayload payload = new StatsUpdatePayload();
+
+ // Set global stats - convert seconds to milliseconds for display consistency
+ payload.setTotalVehiclesCompleted(totalVehiclesReceived);
+ payload.setTotalSystemTime((long) (totalSystemTime * 1000.0));
+ payload.setTotalWaitingTime((long) (totalWaitingTime * 1000.0));
+
+ // Hack: Usar campos de interseção para mostrar throughput no dashboard
+ payload.setIntersectionArrivals(totalVehiclesReceived);
+ payload.setIntersectionDepartures(totalVehiclesReceived);
+ payload.setIntersectionQueueSize(0);
+
+ // Detailed breakdown
+ Map typeCounts = new HashMap<>();
+ Map typeWaitTimes = new HashMap<>();
+
+ for (VehicleType type : VehicleType.values()) {
+ typeCounts.put(type, vehicleTypeCount.get(type));
+ typeWaitTimes.put(type, (long) (vehicleTypeWaitTime.get(type) * 1000.0));
+ }
+
+ payload.setVehicleTypeCounts(typeCounts);
+ payload.setVehicleTypeWaitTimes(typeWaitTimes);
+
+ 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, avgWait);
+
+ } catch (Exception e) {
+ System.err.println("[Exit] Failed to send stats to dashboard: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Encerramento gracioso do processo.
+ * Fecha sockets, termina a pool de threads e liberta recursos.
+ */
+ public void shutdown() {
+ System.out.println("\n[Exit] Shutting down...");
+ running = false;
+
+ printFinalStatistics();
+
+ sendStatsToDashboard();
+
+ try {
+ if (serverSocket != null && !serverSocket.isClosed()) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ System.err.println("Error closing server socket: " + e.getMessage());
+ }
+
+ connectionHandlerPool.shutdown();
+ try {
+ if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
+ connectionHandlerPool.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ connectionHandlerPool.shutdownNow();
+ }
+
+ if (dashboardClient != null) {
+ dashboardClient.close();
+ }
+
+ System.out.println("[Exit] Shutdown complete.");
+ System.out.println("=".repeat(60));
+ }
+
+ /**
+ * Imprime o relatório final no stdout.
+ */
+ private void printFinalStatistics() {
+ System.out.println("\n=== EXIT NODE STATISTICS ===");
+ System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesReceived);
+
+ if (totalVehiclesReceived > 0) {
+ System.out.printf("%nAVERAGE METRICS:%n");
+ System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesReceived);
+ System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesReceived);
+ System.out.printf(" Crossing Time: %.2f seconds%n", totalCrossingTime / totalVehiclesReceived);
+ }
+
+ System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
+ for (VehicleType type : VehicleType.values()) {
+ int count = vehicleTypeCount.get(type);
+ if (count > 0) {
+ double percentage = (count * 100.0) / totalVehiclesReceived;
+ double avgWait = vehicleTypeWaitTime.get(type) / count;
+ System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
+ type, count, percentage, avgWait);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java
new file mode 100644
index 0000000..230b9b1
--- /dev/null
+++ b/main/src/main/java/sd/IntersectionProcess.java
@@ -0,0 +1,1070 @@
+package sd;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+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.des.DESEventType;
+import sd.des.EventQueue;
+import sd.des.SimulationClock;
+import sd.des.SimulationEvent;
+import sd.des.TrafficLightEvent;
+import sd.logging.EventLogger;
+import sd.model.Intersection;
+import sd.model.Message;
+import sd.model.MessageType;
+import sd.model.TrafficLight;
+import sd.model.TrafficLightState;
+import sd.model.Vehicle;
+import sd.protocol.MessageProtocol;
+import sd.protocol.SocketConnection;
+import sd.serialization.SerializationException;
+
+/**
+ * Representa um nó de processamento autónomo na malha de simulação distribuída
+ * (Worker Node).
+ *
+ * Esta classe implementa a lógica de uma interseção rodoviária utilizando uma
+ * arquitetura híbrida:
+ *
+ * - Reativa (Network I/O): Threads dedicadas aceitam conexões TCP e
+ * injetam veículos nas filas de entrada assim que chegam.
+ * - Proativa (DES Engine): Uma thread de processamento de eventos gere
+ * a lógica temporal (mudança de semáforos, tempos de travessia) baseada num
+ * relógio virtual monotónico.
+ *
+ *
+ * A sincronização entre a chegada assíncrona de veículos (Rede) e o
+ * processamento determinístico (DES) é gerida através de estruturas de dados
+ * concorrentes e bloqueios justos (Fair Locks).
+ */
+public class IntersectionProcess {
+
+ private final String intersectionId;
+
+ private final SimulationConfig config;
+
+ private final Intersection intersection;
+
+ private ServerSocket serverSocket;
+
+ /**
+ * Tabela de encaminhamento dinâmico para conexões de saída (Next-Hop Cache).
+ */
+ private final Map outgoingConnections;
+
+ /** Pool de threads para tratamento de I/O de rede (entrada de veículos). */
+ private final ExecutorService connectionHandlerPool;
+
+ private ScheduledExecutorService statsExecutor;
+ private ScheduledExecutorService departureExecutor;
+
+ private volatile boolean running;
+ /** Fator de dilatação temporal (0.0 = Velocidade Máxima, 1.0 = Tempo Real). */
+ private double timeScale;
+
+ // --- Componentes DES (Simulação de Eventos Discretos) ---
+ /** Relógio central virtual da interseção. */
+ private final SimulationClock clock;
+ /** Fila de prioridade (Min-Heap) para agendamento temporal de eventos. */
+ private final EventQueue eventQueue;
+ private final EventLogger eventLogger;
+ /** Thread "Single-Writer" responsável pela mutação de estado da simulação. */
+ private Thread eventProcessorThread;
+
+ /**
+ * Mecanismo de exclusão mútua para controlo de fases semafóricas.
+ * Configurado com política de justiça (fairness=true) para evitar inanição
+ * (starvation) de direções com menos tráfego.
+ */
+ private final Lock trafficCoordinationLock;
+
+ /**
+ * Estado volátil que indica a direção ativa. Apenas uma direção pode deter o
+ * token 'Green' por vez.
+ */
+ private volatile String currentGreenDirection;
+
+ private SocketClient dashboardClient;
+
+ // Métricas voláteis para acesso atómico sem bloqueio
+ private volatile int totalArrivals = 0;
+ private volatile int totalDepartures = 0;
+
+ /**
+ * Inicializa o processo da interseção, carregando a topologia e preparando o
+ * motor DES.
+ *
+ * @param intersectionId O identificador único na malha (ex: "Cr1").
+ * @param configFilePath Caminho para o ficheiro de propriedades.
+ * @throws IOException Se falhar o bind da porta ou leitura de config.
+ */
+ public IntersectionProcess(String intersectionId, String configFilePath) throws IOException {
+ this.intersectionId = intersectionId;
+ this.config = new SimulationConfig(configFilePath);
+ this.intersection = new Intersection(intersectionId);
+ this.outgoingConnections = new HashMap<>();
+ this.connectionHandlerPool = Executors.newCachedThreadPool();
+ this.statsExecutor = Executors.newSingleThreadScheduledExecutor();
+ this.departureExecutor = Executors.newScheduledThreadPool(4);
+ this.running = false;
+ this.trafficCoordinationLock = new ReentrantLock(true); // Fair lock to prevent starvation
+ this.currentGreenDirection = null;
+ this.timeScale = config.getTimeScale();
+
+ // Initialize DES components
+ this.clock = new SimulationClock();
+ this.eventQueue = new EventQueue(true); // Track history for debugging
+ this.eventLogger = EventLogger.getInstance();
+
+ eventLogger.log(sd.logging.EventType.PROCESS_STARTED, intersectionId,
+ "Intersection process initialized with DES architecture");
+
+ System.out.println("=".repeat(60));
+ System.out.println("INTERSECTION PROCESS: " + intersectionId + " (DES Mode)");
+ System.out.println("=".repeat(60));
+ }
+
+ /**
+ * Inicia o ciclo principal do motor de simulação (DES Engine Loop).
+ *
+ * Executa o ciclo "Fetch-Decode-Execute":
+ *
+ * - Remove o evento com menor timestamp da fila (Fetch).
+ * - Avança o relógio virtual para o tempo do evento.
+ * - Aplica atraso artificial se {@code timeScale > 0} (para visualização
+ * humana).
+ * - Despacha o evento para o manipulador apropriado (Execute).
+ *
+ */
+ private void startEventProcessor() {
+ eventProcessorThread = new Thread(() -> {
+ eventLogger.log(sd.logging.EventType.SIMULATION_STARTED, intersectionId,
+ "Event processor thread started");
+
+ // Keep running while the process is active
+ double lastTime = 0.0;
+ while (running) {
+ SimulationEvent event = eventQueue.poll();
+ if (event == null) {
+ // Backoff exponencial ou sleep curto para evitar busy-waiting em idle
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ continue;
+ }
+
+ // Aplicação de escala temporal (Throttle)
+ if (timeScale > 0) {
+ double simTimeDelta = event.getTimestamp() - lastTime;
+ long realDelayMs = (long) (simTimeDelta * timeScale * 1000);
+ if (realDelayMs > 0) {
+ try {
+ Thread.sleep(realDelayMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ lastTime = event.getTimestamp();
+ }
+
+ // Atualização atómica do tempo de simulação
+ clock.advanceTo(event.getTimestamp());
+
+ // Processamento polimórfico
+ processEvent(event);
+ }
+
+ eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, intersectionId,
+ String.format("Event processor thread terminated at time %.2f", clock.getCurrentTime()));
+ }, "EventProcessor-" + intersectionId);
+
+ eventProcessorThread.start();
+ }
+
+ /**
+ * Despachante central de eventos.
+ *
+ * Encaminha o evento para a lógica de negócio específica baseada no tipo
+ * {@link DESEventType}.
+ *
+ * @param event O evento de simulação a ser processado.
+ */
+ private void processEvent(SimulationEvent event) {
+ try {
+ switch (event.getType()) {
+ case TRAFFIC_LIGHT_CHANGE:
+ handleTrafficLightChangeEvent(event);
+ break;
+
+ case VEHICLE_ARRIVAL:
+ // Chegadas são tratadas reativamente via Socket, mas eventos podem ser usados
+ // para métricas
+ break;
+
+ case VEHICLE_CROSSING_START:
+ handleVehicleCrossingStartEvent(event);
+ break;
+
+ case VEHICLE_CROSSING_END:
+ handleVehicleCrossingEndEvent(event);
+ break;
+
+ case SIMULATION_END:
+ handleSimulationEndEvent(event);
+ break;
+
+ default:
+ System.err.println("[" + intersectionId + "] Unknown event type: " + event.getType());
+ }
+ } catch (Exception e) {
+ System.err.println("[" + intersectionId + "] Error processing event " + event.getType() +
+ " at time " + event.getTimestamp() + ": " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Gere a máquina de estados dos semáforos.
+ *
+ * O fluxo de execução é o seguinte:
+ *
+ * - Atualiza o estado do semáforo (Verde <-> Vermelho).
+ * - Se o novo estado for Verde: Calcula a capacidade de vazão e agenda
+ * travessias (Service Events).
+ * - Agenda recursivamente a próxima mudança de estado para manter o ciclo
+ * infinito.
+ *
+ *
+ * @param event O evento que desencadeou a mudança de estado.
+ */
+ private void handleTrafficLightChangeEvent(SimulationEvent event) {
+ TrafficLightEvent tlEvent = (TrafficLightEvent) event.getPayload();
+ TrafficLight light = tlEvent.getLight();
+ String direction = tlEvent.getDirection();
+
+ // Toggle state
+ TrafficLightState oldState = light.getState();
+ TrafficLightState newState = (oldState == TrafficLightState.GREEN) ? TrafficLightState.RED
+ : TrafficLightState.GREEN;
+
+ light.changeState(newState);
+
+ sd.logging.EventType logEventType = (newState == TrafficLightState.GREEN)
+ ? sd.logging.EventType.LIGHT_CHANGED_GREEN
+ : sd.logging.EventType.LIGHT_CHANGED_RED;
+
+ eventLogger.log(logEventType, intersectionId,
+ String.format("Direction %s changed to %s at time %.2f",
+ direction, newState, event.getTimestamp()));
+
+ // Processamento de lote (Batch Processing) para a fase Verde
+ if (newState == TrafficLightState.GREEN) {
+ processQueuedVehiclesForLight(light, event.getTimestamp());
+ }
+
+ // Agendamento do próximo ciclo (Feedback Loop)
+ double nextChangeTime = event.getTimestamp() +
+ (newState == TrafficLightState.GREEN ? light.getGreenTime() : light.getRedTime());
+
+ SimulationEvent nextEvent = new SimulationEvent(
+ nextChangeTime,
+ DESEventType.TRAFFIC_LIGHT_CHANGE,
+ tlEvent);
+ eventQueue.schedule(nextEvent);
+ }
+
+ /**
+ * Calcula a vazão da interseção durante uma fase verde.
+ *
+ * Implementa uma lógica de previsão ("Look-ahead"):
+ *
+ * - Itera sobre a fila de espera do semáforo.
+ * - Calcula o tempo de serviço acumulado (Service Time) baseado no tipo de
+ * veículo.
+ * - Agenda a partida apenas se o veículo couber na janela temporal restante
+ * do sinal verde.
+ *
+ *
+ * @param light O semáforo ativo.
+ * @param currentTime O instante de início da fase verde.
+ */
+ private void processQueuedVehiclesForLight(TrafficLight light, double currentTime) {
+ double greenDuration = light.getGreenTime();
+ double timeOffset = 0.0;
+
+ int queueSize = light.getQueueSize();
+ System.out.printf("[%s] Processing queue for %s (GREEN for %.2fs, queue size: %d, currentTime=%.2f)%n",
+ intersectionId, light.getId(), greenDuration, queueSize, currentTime);
+
+ // Algoritmo de esvaziamento de fila baseado em Time Budget
+ while (light.getQueueSize() > 0) {
+ // Estimativa inicial (optimista)
+ double crossingTime = config.getLightVehicleCrossingTime();
+
+ // Verificação de limite de tempo (Hard Deadline do sinal vermelho)
+ if (timeOffset + crossingTime > greenDuration) {
+ break; // Veículo não cabe no ciclo atual
+ }
+
+ // Commit: Remove da fila
+ Vehicle vehicle = light.removeVehicle(currentTime + timeOffset);
+ if (vehicle == null)
+ break;
+
+ // Recálculo preciso baseado no tipo real do veículo
+ crossingTime = getCrossingTimeForVehicle(vehicle);
+
+ // Agendamento do evento futuro de término de travessia
+ double crossingStartTime = currentTime + timeOffset;
+ scheduleVehicleCrossing(vehicle, crossingStartTime, crossingTime);
+
+ // Incrementa offset para serializar as travessias (Head-of-Line Blocking)
+ timeOffset += crossingTime;
+
+ System.out.printf("[%s] Scheduled vehicle %s to cross at t=%.2f (duration=%.2fs)%n",
+ intersectionId, vehicle.getId(), crossingStartTime, crossingTime);
+ }
+ }
+
+ /**
+ * Cria e agenda o evento de conclusão de travessia (Partida).
+ *
+ * @param vehicle O veículo que está a atravessar.
+ * @param startTime Instante de início da travessia.
+ * @param crossingDuration Duração estimada da travessia.
+ */
+ private void scheduleVehicleCrossing(Vehicle vehicle, double startTime, double crossingDuration) {
+ // Schedule crossing end (when vehicle departs)
+ double departureTime = startTime + crossingDuration;
+
+ // Create event with vehicle as payload
+ SimulationEvent departureEvent = new SimulationEvent(
+ departureTime,
+ DESEventType.VEHICLE_CROSSING_END,
+ vehicle);
+ eventQueue.schedule(departureEvent);
+
+ eventLogger.log(sd.logging.EventType.VEHICLE_QUEUED, intersectionId,
+ String.format("Vehicle %s crossing scheduled: %.2fs to %.2fs",
+ vehicle.getId(), startTime, departureTime));
+ }
+
+ /**
+ * Determina o custo temporal da travessia baseado na física do veículo.
+ *
+ * @param vehicle O veículo em questão.
+ * @return O tempo em segundos necessário para atravessar a interseção.
+ */
+ private double getCrossingTimeForVehicle(Vehicle vehicle) {
+ return switch (vehicle.getType()) {
+ case BIKE -> config.getBikeVehicleCrossingTime();
+ case LIGHT -> config.getLightVehicleCrossingTime();
+ case HEAVY -> config.getHeavyVehicleCrossingTime();
+ default -> config.getLightVehicleCrossingTime();
+ };
+ }
+
+ /**
+ * Manipula o evento de início de travessia de um veículo.
+ *
+ * Atualmente serve como placeholder para lógica futura de animação ou
+ * ocupação de zonas críticas na interseção.
+ *
+ * @param event O evento de início de travessia.
+ */
+ private void handleVehicleCrossingStartEvent(SimulationEvent event) {
+ // Placeholder para lógica futura de animação ou ocupação de zona crítica
+ eventLogger.log(sd.logging.EventType.VEHICLE_DEPARTED, intersectionId,
+ "Vehicle crossing started at time " + event.getTimestamp());
+ }
+
+ /**
+ * Finaliza a lógica de travessia e inicia a transferência (handover) para o
+ * próximo nó.
+ *
+ * Este método é invocado quando o tempo de travessia expira no relógio virtual.
+ * Executa as seguintes ações:
+ *
+ * - Atualiza as métricas de tempo de travessia do veículo.
+ * - Incrementa contadores locais de veículos processados.
+ * - Transfere a responsabilidade do veículo para a rede, enviando-o ao
+ * próximo destino.
+ *
+ *
+ * @param event O evento de fim de travessia.
+ */
+ private void handleVehicleCrossingEndEvent(SimulationEvent event) {
+ Vehicle vehicle = (Vehicle) event.getPayload();
+
+ // Atualiza métricas do veículo
+ double crossingTime = getCrossingTimeForVehicle(vehicle);
+ vehicle.addCrossingTime(crossingTime);
+
+ // Atualiza métricas locais
+ intersection.incrementVehiclesSent();
+
+ // Handover: Transfere a responsabilidade do veículo para a rede
+ sendVehicleToNextDestination(vehicle);
+
+ eventLogger.log(sd.logging.EventType.VEHICLE_DEPARTED, intersectionId,
+ String.format("Vehicle %s departed at time %.2f", vehicle.getId(), event.getTimestamp()));
+ }
+
+ /**
+ * Finaliza a execução do processo de simulação.
+ *
+ * @param event O evento de fim de simulação.
+ */
+ private void handleSimulationEndEvent(SimulationEvent event) {
+ eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, intersectionId,
+ String.format("Simulation ended at time %.2f", event.getTimestamp()));
+ running = false;
+ }
+
+ /**
+ * Exporta o histórico completo de eventos para análise post-mortem.
+ *
+ * @param outputPath O caminho do ficheiro onde o histórico será guardado.
+ */
+ public void exportEventHistory(String outputPath) {
+ String history = eventQueue.exportEventHistory();
+ try (java.io.PrintWriter writer = new java.io.PrintWriter(outputPath)) {
+ writer.println(history);
+ System.out.println("[" + intersectionId + "] Event history exported to: " + outputPath);
+ } catch (java.io.FileNotFoundException e) {
+ System.err.println("[" + intersectionId + "] Failed to export event history: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Ponto de entrada principal da aplicação.
+ *
+ * @param args Argumentos da linha de comando (ID da interseção e ficheiro de
+ * configuração opcional).
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Realiza o bootstrap dos componentes lógicos e de rede da interseção.
+ *
+ * Inclui a criação de semáforos, configuração de encaminhamento e conexão ao
+ * Dashboard.
+ */
+ public void initialize() {
+ System.out.println("\n[" + intersectionId + "] Initializing intersection...");
+
+ createTrafficLights();
+
+ configureRouting();
+
+ connectToDashboard();
+
+ System.out.println("[" + intersectionId + "] Initialization complete.");
+ }
+
+ /**
+ * Estabelece a conexão com o Dashboard para envio de telemetria em tempo real.
+ */
+ 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.");
+
+ } 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;
+ }
+ }
+
+ /**
+ * Inicializa os semáforos da interseção com base na configuração carregada.
+ */
+ private void createTrafficLights() {
+ System.out.println("\n[" + intersectionId + "] Creating traffic lights...");
+
+ SimulationConfig.IntersectionConfig intersectionConfig = getIntersectionConfig();
+ List directions = intersectionConfig.getLights();
+
+ if (directions == null || directions.isEmpty()) {
+ System.err.println(" Warning: No traffic lights configured for " + intersectionId);
+ return;
+ }
+
+ for (String direction : directions) {
+ double greenTime = config.getTrafficLightGreenTime(intersectionId, direction);
+ double redTime = config.getTrafficLightRedTime(intersectionId, direction);
+
+ TrafficLight light = new TrafficLight(
+ intersectionId + "-" + direction,
+ direction,
+ greenTime,
+ redTime);
+
+ intersection.addTrafficLight(light);
+ System.out.println(" Created traffic light: " + direction +
+ " (Green: " + greenTime + "s, Red: " + redTime + "s)");
+ }
+ }
+
+ /**
+ * Obtém a configuração específica para esta interseção a partir da configuração
+ * global.
+ *
+ * @return O objeto de configuração da interseção.
+ * @throws RuntimeException Se a configuração estiver em falta.
+ */
+ private SimulationConfig.IntersectionConfig getIntersectionConfig() {
+ if (config.getNetworkConfig() == null || config.getNetworkConfig().getIntersections() == null) {
+ throw new RuntimeException("Network configuration not loaded or empty.");
+ }
+ return config.getNetworkConfig().getIntersections().stream()
+ .filter(i -> i.getId().equals(intersectionId))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Intersection config not found for " + intersectionId));
+ }
+
+ /**
+ * Configura a tabela de encaminhamento (routing) da interseção.
+ *
+ * Define para cada destino qual a direção de saída (semáforo) correspondente.
+ */
+ private void configureRouting() {
+ System.out.println("\n[" + intersectionId + "] Configuring routing...");
+
+ SimulationConfig.IntersectionConfig intersectionConfig = getIntersectionConfig();
+ Map routes = intersectionConfig.getRoutes();
+
+ if (routes != null) {
+ for (Map.Entry entry : routes.entrySet()) {
+ String destination = entry.getKey();
+ String direction = entry.getValue();
+ intersection.configureRoute(destination, direction);
+ System.out.println(" Route configured: To " + destination + " -> Use " + direction);
+ }
+ } else {
+ System.out.println(" No routes configured.");
+ }
+
+ System.out.println(" Routing configured.");
+ }
+
+ /**
+ * Primitiva de bloqueio: Solicita acesso exclusivo à zona crítica da
+ * interseção.
+ *
+ * @param direction A direção que solicita passagem.
+ */
+ public void requestGreenLight(String direction) {
+ trafficCoordinationLock.lock();
+ currentGreenDirection = direction;
+ }
+
+ /**
+ * Primitiva de bloqueio: Liberta o acesso exclusivo à zona crítica.
+ *
+ * @param direction A direção que está a libertar a passagem.
+ */
+ public void releaseGreenLight(String direction) {
+ if (direction.equals(currentGreenDirection)) {
+ currentGreenDirection = null;
+ trafficCoordinationLock.unlock();
+ }
+ }
+
+ /**
+ * Inicializa o estado dos semáforos no arranque da simulação (t=0).
+ *
+ * Garante que apenas um semáforo começa em Verde e os restantes em Vermelho,
+ * agendando os eventos iniciais na fila do DES.
+ */
+ private void scheduleInitialTrafficLightEvents() {
+ System.out.println("\n[" + intersectionId + "] Scheduling initial traffic light events (DES mode)...");
+
+ double currentTime = clock.getCurrentTime();
+ System.out.printf("[%s] Initial clock time: %.2f%n", intersectionId, currentTime);
+
+ for (TrafficLight light : intersection.getTrafficLights()) {
+ String direction = light.getDirection();
+
+ // Lógica de arranque: Primeiro da lista = Verde, outros = Vermelho
+ boolean isFirstLight = intersection.getTrafficLights().indexOf(light) == 0;
+ TrafficLightState initialState = isFirstLight ? TrafficLightState.GREEN : TrafficLightState.RED;
+ light.changeState(initialState);
+
+ // Agenda a primeira transição
+ double firstChangeTime = currentTime +
+ (initialState == TrafficLightState.GREEN ? light.getGreenTime() : light.getRedTime());
+
+ TrafficLightEvent tlEvent = new TrafficLightEvent(light, direction, intersectionId);
+ SimulationEvent event = new SimulationEvent(
+ firstChangeTime,
+ DESEventType.TRAFFIC_LIGHT_CHANGE,
+ tlEvent);
+ eventQueue.schedule(event);
+
+ System.out.println(" Scheduled first event for direction " + direction +
+ " (initial: " + initialState + ", change at t=" + firstChangeTime + ")");
+
+ eventLogger.log(
+ initialState == TrafficLightState.GREEN ? sd.logging.EventType.LIGHT_CHANGED_GREEN
+ : sd.logging.EventType.LIGHT_CHANGED_RED,
+ intersectionId,
+ "Direction " + direction + " initialized to " + initialState);
+ }
+ }
+
+ /**
+ * Encaminhamento de rede: Serializa e envia o objeto veículo para o próximo nó.
+ *
+ * Calcula também o tempo de viagem virtual entre nós (Edge Weight).
+ *
+ * @param vehicle O veículo a ser enviado.
+ */
+ public void sendVehicleToNextDestination(Vehicle vehicle) {
+ String nextDestination = vehicle.getCurrentDestination();
+
+ // Cálculo de latência de viagem (Edge Weight)
+ double baseTime = config.getBaseTravelTime();
+ double multiplier = 1.0;
+ switch (vehicle.getType()) {
+ case BIKE -> multiplier = config.getBikeTravelTimeMultiplier();
+ case HEAVY -> multiplier = config.getHeavyTravelTimeMultiplier();
+ default -> multiplier = 1.0;
+ }
+ double travelTime = baseTime * multiplier;
+
+ System.out.printf("[%s] Vehicle %s departing to %s. Travel time: %.2fs%n",
+ intersectionId, vehicle.getId(), nextDestination, travelTime);
+
+ recordVehicleDeparture();
+
+ // Envio imediato (o delay de viagem é implícito no tempo de chegada no próximo
+ // nó ou simulado aqui)
+ sendVehicleImmediately(vehicle, nextDestination);
+ }
+
+ /**
+ * Envia o veículo imediatamente para o próximo nó via conexão TCP persistente.
+ *
+ * @param vehicle O veículo a ser enviado.
+ * @param nextDestination O identificador do próximo nó destino.
+ */
+ private void sendVehicleImmediately(Vehicle vehicle, String nextDestination) {
+ try {
+ // Lazy loading da conexão
+ SocketConnection connection = getOrCreateConnection(nextDestination);
+
+ // Encapsulamento da mensagem
+ MessageProtocol message = new Message(
+ MessageType.VEHICLE_TRANSFER,
+ intersectionId,
+ nextDestination,
+ vehicle,
+ System.currentTimeMillis());
+
+ connection.sendMessage(message);
+
+ System.out.println("[" + intersectionId + "] Vehicle " + vehicle.getId() +
+ " arrived at " + nextDestination + " (msg sent)");
+
+ } catch (IOException | InterruptedException e) {
+ System.err.println("[" + intersectionId + "] Failed to send vehicle " +
+ vehicle.getId() + " to " + nextDestination + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * Obtém ou cria uma conexão para o destino especificado (Singleton por
+ * destino).
+ *
+ * Este método é thread-safe.
+ *
+ * @param destinationId O identificador do nó destino.
+ * @return A conexão TCP estabelecida.
+ * @throws IOException Se ocorrer um erro de I/O na criação da conexão.
+ * @throws InterruptedException Se a thread for interrompida durante a espera.
+ */
+ private synchronized SocketConnection getOrCreateConnection(String destinationId)
+ throws IOException, InterruptedException {
+
+ if (!outgoingConnections.containsKey(destinationId)) {
+ String host = getHostForDestination(destinationId);
+ int port = getPortForDestination(destinationId);
+
+ System.out.println("[" + intersectionId + "] Creating connection to " +
+ destinationId + " at " + host + ":" + port);
+
+ SocketConnection connection = new SocketConnection(host, port);
+ outgoingConnections.put(destinationId, connection);
+ }
+
+ return outgoingConnections.get(destinationId);
+ }
+
+ /**
+ * Resolve o hostname ou endereço IP para um determinado destino.
+ *
+ * @param destinationId O ID do destino.
+ * @return O endereço do host.
+ */
+ private String getHostForDestination(String destinationId) {
+ if (destinationId.equals("S")) {
+ return config.getExitHost();
+ } else {
+ return config.getIntersectionHost(destinationId);
+ }
+ }
+
+ /**
+ * Resolve a porta TCP para um determinado destino.
+ *
+ * @param destinationId O ID do destino.
+ * @return O número da porta.
+ */
+ private int getPortForDestination(String destinationId) {
+ if (destinationId.equals("S")) {
+ return config.getExitPort();
+ } else {
+ return config.getIntersectionPort(destinationId);
+ }
+ }
+
+ /**
+ * Inicia o servidor e o loop de aceitação de conexões.
+ *
+ * Este método bloqueia a thread chamadora durante a execução do servidor.
+ *
+ * @throws IOException Se ocorrer um erro ao fazer bind da porta.
+ */
+ public void start() throws IOException {
+ int port = config.getIntersectionPort(intersectionId);
+ serverSocket = new ServerSocket(port);
+ running = true;
+
+ System.out.println("\n[" + intersectionId + "] Server started on port " + port);
+
+ // DES Mode: Schedule initial events and start event processor
+ scheduleInitialTrafficLightEvents();
+ startEventProcessor();
+ System.out.println("[" + intersectionId + "] Running in DES mode");
+
+ // Background task para telemetria
+ statsExecutor.scheduleAtFixedRate(this::sendStatsToDashboard, 1, 1, TimeUnit.SECONDS);
+
+ System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n");
+
+ // Loop principal de aceitação de conexões
+ while (running) {
+ try {
+ Socket clientSocket = serverSocket.accept();
+
+ System.out.println("[" + intersectionId + "] New connection accepted from " +
+ clientSocket.getInetAddress().getHostAddress());
+
+ if (!running) {
+ clientSocket.close();
+ break;
+ }
+
+ // Configura timeout para evitar bloqueios infinitos em leitura
+ try {
+ clientSocket.setSoTimeout(1000);
+ } catch (java.net.SocketException e) {
+ System.err.println("[" + intersectionId + "] Failed to set timeout: " + e.getMessage());
+ clientSocket.close();
+ continue;
+ }
+
+ // Delega processamento para thread pool (NIO style)
+ connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
+
+ } catch (IOException e) {
+ if (!running) {
+ break; // Shutdown normal
+ }
+ System.err.println("[" + intersectionId + "] Error accepting connection: " +
+ e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Lógica de tratamento de conexões de entrada (Consumer).
+ *
+ * Lê continuamente do socket até que a conexão seja fechada, processando
+ * mensagens
+ * de chegada de veículos ou comandos de simulação.
+ *
+ * @param clientSocket O socket do cliente conectado.
+ */
+ private void handleIncomingConnection(Socket clientSocket) {
+ try {
+ clientSocket.setSoTimeout(1000); // 1 second timeout
+
+ } catch (java.net.SocketException e) {
+ System.err.println("[" + intersectionId + "] Failed to set socket timeout: " + e.getMessage());
+ return;
+ }
+
+ try (SocketConnection connection = new SocketConnection(clientSocket)) {
+
+ System.out.println("[" + intersectionId + "] New connection accepted from " +
+ clientSocket.getInetAddress().getHostAddress());
+
+ while (running && connection.isConnected()) {
+ try {
+ MessageProtocol message = connection.receiveMessage();
+
+ if (message.getType() == MessageType.SIMULATION_START) {
+ System.out.println("[" + intersectionId + "] Simulation start time synchronized");
+ continue;
+ }
+
+ if (message.getType() == MessageType.VEHICLE_TRANSFER ||
+ message.getType() == MessageType.VEHICLE_SPAWN) {
+
+ // Lógica de desserialização polimórfica (Vehicle ou Map)
+ Vehicle vehicle;
+ Object payload = message.getPayload();
+ if (payload instanceof Vehicle) {
+ vehicle = (Vehicle) payload;
+ } else if (payload instanceof java.util.Map) {
+ 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());
+
+ // Lógica de Roteamento Local
+ vehicle.advanceRoute();
+ intersection.receiveVehicle(vehicle, clock.getCurrentTime());
+
+ System.out.printf("[%s] Vehicle %s queued. Total queue size: %d%n",
+ intersectionId, vehicle.getId(), intersection.getTotalQueueSize());
+
+ recordVehicleArrival();
+
+ } else if (message.getType() == MessageType.SHUTDOWN) {
+ System.out.println(
+ "[" + intersectionId + "] Received SHUTDOWN command from " + message.getSourceNode());
+ running = false;
+ break;
+ }
+
+ } catch (java.net.SocketTimeoutException e) {
+ if (!running) {
+ break;
+ }
+ } catch (ClassNotFoundException e) {
+ System.err.println("[" + intersectionId + "] Unknown message type received: " +
+ e.getMessage());
+ break;
+ } catch (IOException e) {
+ if (running) {
+ System.err.println("[" + intersectionId + "] Failed to deserialize message: " +
+ e.getMessage());
+ e.printStackTrace();
+ }
+ break;
+ }
+ }
+
+ } catch (IOException e) {
+ if (running) {
+ System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Procedimento de Encerramento Gracioso (Graceful Shutdown).
+ *
+ * - Para a aceitação de novas conexões.
+ * - Envia últimas estatísticas.
+ * - Encerra pools de threads.
+ * - Fecha sockets ativos.
+ *
+ */
+ public void shutdown() {
+ if (!running) {
+ return;
+ }
+
+ System.out.println("\n[" + intersectionId + "] Shutting down...");
+ running = false;
+
+ sendStatsToDashboard();
+
+ // 1. Close ServerSocket
+ if (serverSocket != null && !serverSocket.isClosed()) {
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ // Expected
+ }
+ }
+
+ // 2. Shutdown thread pools
+ if (connectionHandlerPool != null && !connectionHandlerPool.isShutdown()) {
+ connectionHandlerPool.shutdownNow();
+ }
+ if (statsExecutor != null && !statsExecutor.isShutdown()) {
+ statsExecutor.shutdownNow();
+ }
+ if (departureExecutor != null && !departureExecutor.isShutdown()) {
+ departureExecutor.shutdownNow();
+ }
+
+ // 3. Wait briefly for termination
+ try {
+ if (connectionHandlerPool != null) {
+ connectionHandlerPool.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ if (statsExecutor != null) {
+ statsExecutor.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ if (departureExecutor != null) {
+ departureExecutor.awaitTermination(1, TimeUnit.SECONDS);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ // 4. Close outgoing connections
+ synchronized (outgoingConnections) {
+ for (SocketConnection conn : outgoingConnections.values()) {
+ try {
+ conn.close();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ outgoingConnections.clear();
+ }
+
+ // 5. Close dashboard connection
+ if (dashboardClient != null) {
+ dashboardClient.close();
+ }
+
+ System.out.println("[" + intersectionId + "] Shutdown complete.");
+ System.out.println("============================================================\n");
+ }
+
+ /**
+ * Obtém o modelo de dados da interseção.
+ *
+ * @return O objeto Intersection.
+ */
+ public Intersection getIntersection() {
+ return intersection;
+ }
+
+ /**
+ * Regista a chegada de um novo veículo para fins estatísticos.
+ */
+ public void recordVehicleArrival() {
+ totalArrivals++;
+ }
+
+ /**
+ * Regista a partida de um veículo para fins estatísticos.
+ */
+ public void recordVehicleDeparture() {
+ totalDepartures++;
+ }
+
+ /**
+ * Envia um "snapshot" do estado atual para o Dashboard (Telemetria Push).
+ *
+ * Inclui o número acumulado de chegadas, partidas e o tamanho atual das filas.
+ */
+ private void sendStatsToDashboard() {
+ if (dashboardClient == null || !dashboardClient.isConnected()) {
+ return;
+ }
+
+ try {
+ int currentQueueSize = intersection.getTrafficLights().stream()
+ .mapToInt(TrafficLight::getQueueSize)
+ .sum();
+
+ StatsUpdatePayload payload = new StatsUpdatePayload()
+ .setIntersectionArrivals(totalArrivals)
+ .setIntersectionDepartures(totalDepartures)
+ .setIntersectionQueueSize(currentQueueSize);
+
+ 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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/analysis/MultiRunAnalyzer.java b/main/src/main/java/sd/analysis/MultiRunAnalyzer.java
new file mode 100644
index 0000000..02c3307
--- /dev/null
+++ b/main/src/main/java/sd/analysis/MultiRunAnalyzer.java
@@ -0,0 +1,285 @@
+package sd.analysis;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import sd.model.VehicleType;
+
+/**
+ * Responsável pela agregação e análise estatística de múltiplas execuções da simulação.
+ *
+ * Esta classe coleta resultados individuais ({@link SimulationRunResult}) e calcula
+ * métricas consolidadas, incluindo média, desvio padrão, mediana e intervalos de
+ * confiança de 95%. O objetivo é fornecer uma visão robusta do comportamento do
+ * sistema, mitigando a variância estocástica de execuções isoladas.
+ */
+public class MultiRunAnalyzer {
+
+ /** Lista acumulada de resultados de execuções individuais. */
+ private final List results;
+
+ /** Identificador do ficheiro de configuração utilizado nas execuções. */
+ private final String configurationFile;
+
+ /**
+ * Inicializa o analisador para um conjunto específico de configurações.
+ *
+ * @param configurationFile O caminho ou nome do ficheiro de configuração base.
+ */
+ public MultiRunAnalyzer(String configurationFile) {
+ this.configurationFile = configurationFile;
+ this.results = new ArrayList<>();
+ }
+
+ /**
+ * Adiciona o resultado de uma execução de simulação concluída ao conjunto de dados.
+ *
+ * @param result O objeto contendo as métricas da execução individual.
+ */
+ public void addResult(SimulationRunResult result) {
+ results.add(result);
+ }
+
+ /**
+ * Retorna o número total de execuções armazenadas até o momento.
+ *
+ * @return O tamanho da lista de resultados.
+ */
+ public int getRunCount() {
+ return results.size();
+ }
+
+ /**
+ * Gera um relatório estatístico abrangente formatado em texto.
+ *
+ * O relatório inclui:
+ *
+ * - Métricas globais (throughput, tempos de espera, tempos no sistema).
+ * - Análise segmentada por tipo de veículo ({@link VehicleType}).
+ * - Análise de gargalos por interseção (tamanhos de fila).
+ * - Resumos brutos das execuções individuais.
+ *
+ *
+ * @return Uma String contendo o relatório completo formatado.
+ */
+ public String generateReport() {
+ if (results.isEmpty()) {
+ return "No simulation results to analyze.";
+ }
+
+ StringBuilder report = new StringBuilder();
+
+ // Header
+ report.append("=".repeat(80)).append("\n");
+ report.append("ANÁLISE ESTATÍSTICA MULTI-EXECUÇÃO\n");
+ report.append("=".repeat(80)).append("\n");
+ report.append("Configuração: ").append(configurationFile).append("\n");
+ report.append("Número de Execuções: ").append(results.size()).append("\n");
+ report.append("Data da Análise: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n");
+ report.append("\n");
+
+ // Global metrics
+ report.append("-".repeat(80)).append("\n");
+ report.append("MÉTRICAS GLOBAIS\n");
+ report.append("-".repeat(80)).append("\n\n");
+
+ report.append(analyzeMetric("Veículos Gerados",
+ extractValues(r -> (double) r.getTotalVehiclesGenerated())));
+ report.append("\n");
+
+ report.append(analyzeMetric("Veículos Completados",
+ extractValues(r -> (double) r.getTotalVehiclesCompleted())));
+ report.append("\n");
+
+ report.append(analyzeMetric("Taxa de Conclusão (%)",
+ extractValues(r -> r.getTotalVehiclesGenerated() > 0
+ ? 100.0 * r.getTotalVehiclesCompleted() / r.getTotalVehiclesGenerated()
+ : 0.0)));
+ report.append("\n");
+
+ report.append(analyzeMetric("Tempo Médio no Sistema (segundos)",
+ extractValues(r -> r.getAverageSystemTime())));
+ report.append("\n");
+
+ report.append(analyzeMetric("Tempo Médio de Espera (segundos)",
+ extractValues(r -> r.getAverageWaitingTime())));
+ report.append("\n");
+
+ // Per-vehicle-type analysis
+ report.append("\n");
+ report.append("-".repeat(80)).append("\n");
+ report.append("ANÁLISE POR TIPO DE VEÍCULO\n");
+ report.append("-".repeat(80)).append("\n\n");
+
+ for (VehicleType type : VehicleType.values()) {
+ report.append("--- ").append(type).append(" ---\n");
+
+ report.append(analyzeMetric(" Contagem de Veículos",
+ extractValues(r -> (double) r.getVehicleCountByType().getOrDefault(type, 0))));
+ report.append("\n");
+
+ report.append(analyzeMetric(" Tempo Médio no Sistema (segundos)",
+ extractValues(r -> r.getAvgSystemTimeByType().getOrDefault(type, 0.0))));
+ report.append("\n");
+
+ report.append(analyzeMetric(" Tempo Médio de Espera (segundos)",
+ extractValues(r -> r.getAvgWaitTimeByType().getOrDefault(type, 0.0))));
+ report.append("\n\n");
+ }
+
+ // Per-intersection analysis
+ report.append("-".repeat(80)).append("\n");
+ report.append("ANÁLISE POR INTERSEÇÃO\n");
+ report.append("-".repeat(80)).append("\n\n");
+
+ Set allIntersections = new TreeSet<>();
+ for (SimulationRunResult result : results) {
+ allIntersections.addAll(result.getMaxQueueSizeByIntersection().keySet());
+ }
+
+ for (String intersection : allIntersections) {
+ report.append("--- ").append(intersection).append(" ---\n");
+
+ report.append(analyzeMetric(" Tamanho Máximo da Fila",
+ extractValues(r -> (double) r.getMaxQueueSizeByIntersection().getOrDefault(intersection, 0))));
+ report.append("\n");
+
+ report.append(analyzeMetric(" Tamanho Médio da Fila",
+ extractValues(r -> r.getAvgQueueSizeByIntersection().getOrDefault(intersection, 0.0))));
+ report.append("\n");
+
+ report.append(analyzeMetric(" Veículos Processados",
+ extractValues(r -> (double) r.getVehiclesProcessedByIntersection().getOrDefault(intersection, 0))));
+ report.append("\n\n");
+ }
+
+ // Individual run summaries
+ report.append("-".repeat(80)).append("\n");
+ report.append("RESUMOS INDIVIDUAIS DAS EXECUÇÕES\n");
+ report.append("-".repeat(80)).append("\n\n");
+
+ for (SimulationRunResult result : results) {
+ report.append(result.toString()).append("\n\n");
+ }
+
+ report.append("=".repeat(80)).append("\n");
+ report.append("FIM DO RELATÓRIO\n");
+ report.append("=".repeat(80)).append("\n");
+
+ return report.toString();
+ }
+
+ /**
+ * Analisa uma métrica específica e retorna as estatísticas formatadas.
+ *
+ * Calcula média, desvio padrão, mediana, intervalo de confiança (95%) e extremos (min/max).
+ *
+ * @param metricName O nome descritivo da métrica (ex: "Tempo de Espera").
+ * @param values A lista de valores numéricos brutos extraídos das execuções.
+ * @return Uma string formatada com os dados estatísticos.
+ */
+ private String analyzeMetric(String metricName, List values) {
+ if (values.isEmpty() || values.stream().allMatch(v -> v == 0.0)) {
+ return metricName + ": Sem dados\n";
+ }
+
+ double mean = StatisticalAnalysis.mean(values);
+ double stdDev = StatisticalAnalysis.standardDeviation(values);
+ double[] ci = StatisticalAnalysis.confidenceInterval95(values);
+ double min = StatisticalAnalysis.min(values);
+ double max = StatisticalAnalysis.max(values);
+ double median = StatisticalAnalysis.median(values);
+
+ return String.format(
+ "%s:\n" +
+ " Média: %10.2f Desvio Padrão: %10.2f\n" +
+ " Mediana: %10.2f IC 95%%: [%.2f, %.2f]\n" +
+ " Mín: %10.2f Máx: %10.2f\n",
+ metricName, mean, stdDev, median, ci[0], ci[1], min, max
+ );
+ }
+
+ /**
+ * Extrai valores numéricos dos resultados de simulação usando uma função mapeadora.
+ *
+ * Utilizado internamente para transformar a lista de objetos complexos {@link SimulationRunResult}
+ * em listas simples de Doubles para processamento estatístico.
+ *
+ * @param extractor Função lambda que define qual campo extrair de cada resultado.
+ * @return Lista de valores double correspondentes.
+ */
+ private List extractValues(java.util.function.Function extractor) {
+ List values = new ArrayList<>();
+ for (SimulationRunResult result : results) {
+ values.add(extractor.apply(result));
+ }
+ return values;
+ }
+
+ /**
+ * Persiste o relatório gerado num ficheiro de texto.
+ *
+ * @param filename O caminho do ficheiro de destino.
+ * @throws IOException Se ocorrer um erro de escrita no disco.
+ */
+ public void saveReport(String filename) throws IOException {
+ try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) {
+ writer.print(generateReport());
+ }
+ }
+
+ /**
+ * Gera um resumo em formato CSV para fácil importação em ferramentas de planilha.
+ *
+ * Este método atua como um wrapper para {@link #saveCSVSummary(String)}.
+ *
+ * @param filename O caminho do ficheiro CSV de destino.
+ * @throws IOException Se ocorrer um erro de escrita no disco.
+ */
+ public void saveCSV(String filename) throws IOException {
+ saveCSVSummary(filename);
+ }
+
+ /**
+ * Gera e grava o sumário CSV detalhado com métricas chave por execução.
+ *
+ * Colunas incluídas: Execução, VeículosGerados, VeículosCompletados, TaxaConclusão,
+ * TempoMédioSistema, TempoMédioEspera, TempoMínimoSistema, TempoMáximoSistema.
+ *
+ * @param filename O caminho do ficheiro CSV de destino.
+ * @throws IOException Se ocorrer um erro de escrita no disco.
+ */
+ public void saveCSVSummary(String filename) throws IOException {
+ try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) {
+ // Header
+ writer.println("Execução,VeículosGerados,VeículosCompletados,TaxaConclusão," +
+ "TempoMédioSistema,TempoMédioEspera,TempoMínimoSistema,TempoMáximoSistema");
+
+ // Data rows
+ for (SimulationRunResult result : results) {
+ double completionRate = result.getTotalVehiclesGenerated() > 0
+ ? 100.0 * result.getTotalVehiclesCompleted() / result.getTotalVehiclesGenerated()
+ : 0.0;
+
+ writer.printf("%d,%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f\n",
+ result.getRunNumber(),
+ result.getTotalVehiclesGenerated(),
+ result.getTotalVehiclesCompleted(),
+ completionRate,
+ result.getAverageSystemTime(),
+ result.getAverageWaitingTime(),
+ result.getMinSystemTime(),
+ result.getMaxSystemTime()
+ );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/analysis/SimulationRunResult.java b/main/src/main/java/sd/analysis/SimulationRunResult.java
new file mode 100644
index 0000000..465ef36
--- /dev/null
+++ b/main/src/main/java/sd/analysis/SimulationRunResult.java
@@ -0,0 +1,206 @@
+package sd.analysis;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import sd.model.VehicleType;
+
+/**
+ * Encapsula os dados telemétricos e estatísticos resultantes de uma única execução da simulação.
+ *
+ * Esta classe atua como um registo estruturado de métricas de desempenho, armazenando
+ * dados de latência (tempos de sistema/espera), vazão (throughput) e ocupação de recursos
+ * (tamanhos de fila). Os dados aqui contidos servem como base para a análise
+ * estatística agregada realizada pelo {@link MultiRunAnalyzer}.
+ */
+public class SimulationRunResult {
+
+ private final int runNumber;
+ private final String configurationFile;
+ private final long startTimeMillis;
+ private final long endTimeMillis;
+
+ // Global metrics
+ /** Total de veículos instanciados pelos geradores durante a execução. */
+ private int totalVehiclesGenerated;
+
+ /** Total de veículos que completaram o percurso e saíram do sistema com sucesso. */
+ private int totalVehiclesCompleted;
+
+ /** Média global do tempo total (em segundos) desde a geração até a saída. */
+ private double averageSystemTime; // seconds
+
+ /** Menor tempo de sistema registado (em segundos). */
+ private double minSystemTime; // seconds
+
+ /** Maior tempo de sistema registado (em segundos). */
+ private double maxSystemTime; // seconds
+
+ /** Média global do tempo (em segundos) que os veículos passaram parados em filas. */
+ private double averageWaitingTime; // seconds
+
+ // Per-type metrics
+ private final Map vehicleCountByType;
+ private final Map avgSystemTimeByType;
+ private final Map avgWaitTimeByType;
+
+ // Per-intersection metrics
+ private final Map maxQueueSizeByIntersection;
+ private final Map avgQueueSizeByIntersection;
+ private final Map vehiclesProcessedByIntersection;
+
+ /**
+ * Inicializa um novo contentor de resultados para uma execução específica.
+ *
+ * @param runNumber O identificador sequencial desta execução.
+ * @param configurationFile O ficheiro de configuração utilizado.
+ */
+ public SimulationRunResult(int runNumber, String configurationFile) {
+ this.runNumber = runNumber;
+ this.configurationFile = configurationFile;
+ this.startTimeMillis = System.currentTimeMillis();
+ this.endTimeMillis = 0;
+
+ this.vehicleCountByType = new HashMap<>();
+ this.avgSystemTimeByType = new HashMap<>();
+ this.avgWaitTimeByType = new HashMap<>();
+ this.maxQueueSizeByIntersection = new HashMap<>();
+ this.avgQueueSizeByIntersection = new HashMap<>();
+ this.vehiclesProcessedByIntersection = new HashMap<>();
+ }
+
+ /**
+ * Sinaliza o fim da recolha de dados para esta execução.
+ * (Placeholder para lógica de finalização de timestamps).
+ */
+ public void markCompleted() {
+ // This will be called when the run finishes
+ }
+
+ // Getters
+ public int getRunNumber() { return runNumber; }
+ public String getConfigurationFile() { return configurationFile; }
+ public long getStartTimeMillis() { return startTimeMillis; }
+ public long getEndTimeMillis() { return endTimeMillis; }
+
+ /**
+ * Calcula a duração total da execução em milissegundos.
+ * @return Delta entre fim e início.
+ */
+ public long getDurationMillis() { return endTimeMillis - startTimeMillis; }
+
+ public int getTotalVehiclesGenerated() { return totalVehiclesGenerated; }
+ public int getTotalVehiclesCompleted() { return totalVehiclesCompleted; }
+ public double getAverageSystemTime() { return averageSystemTime; }
+ public double getMinSystemTime() { return minSystemTime; }
+ public double getMaxSystemTime() { return maxSystemTime; }
+ public double getAverageWaitingTime() { return averageWaitingTime; }
+
+ /**
+ * Retorna o mapeamento de contagem de veículos por tipo.
+ * @return Uma cópia defensiva do mapa (snapshot).
+ */
+ public Map getVehicleCountByType() {
+ return new HashMap<>(vehicleCountByType);
+ }
+
+ /**
+ * Retorna o tempo médio no sistema segmentado por tipo de veículo.
+ * @return Uma cópia defensiva do mapa (snapshot).
+ */
+ public Map getAvgSystemTimeByType() {
+ return new HashMap<>(avgSystemTimeByType);
+ }
+
+ /**
+ * Retorna o tempo médio de espera segmentado por tipo de veículo.
+ * @return Uma cópia defensiva do mapa (snapshot).
+ */
+ public Map getAvgWaitTimeByType() {
+ return new HashMap<>(avgWaitTimeByType);
+ }
+
+ /**
+ * Retorna o tamanho máximo de fila registado por interseção (gargalos).
+ * @return Uma cópia defensiva do mapa (snapshot).
+ */
+ public Map getMaxQueueSizeByIntersection() {
+ return new HashMap<>(maxQueueSizeByIntersection);
+ }
+
+ /**
+ * Retorna o tamanho médio das filas por interseção.
+ * @return Uma cópia defensiva do mapa (snapshot).
+ */
+ public Map getAvgQueueSizeByIntersection() {
+ return new HashMap<>(avgQueueSizeByIntersection);
+ }
+
+ /**
+ * Retorna o total de veículos processados (throughput) por interseção.
+ * @return Uma cópia defensiva do mapa (snapshot).
+ */
+ public Map getVehiclesProcessedByIntersection() {
+ return new HashMap<>(vehiclesProcessedByIntersection);
+ }
+
+ // Setters
+ public void setTotalVehiclesGenerated(int count) {
+ this.totalVehiclesGenerated = count;
+ }
+ public void setTotalVehiclesCompleted(int count) {
+ this.totalVehiclesCompleted = count;
+ }
+ public void setAverageSystemTime(double time) {
+ this.averageSystemTime = time;
+ }
+ public void setMinSystemTime(double time) {
+ this.minSystemTime = time;
+ }
+ public void setMaxSystemTime(double time) {
+ this.maxSystemTime = time;
+ }
+ public void setAverageWaitingTime(double time) {
+ this.averageWaitingTime = time;
+ }
+
+ public void setVehicleCountByType(VehicleType type, int count) {
+ vehicleCountByType.put(type, count);
+ }
+ public void setAvgSystemTimeByType(VehicleType type, double time) {
+ avgSystemTimeByType.put(type, time);
+ }
+ public void setAvgWaitTimeByType(VehicleType type, double time) {
+ avgWaitTimeByType.put(type, time);
+ }
+ public void setMaxQueueSize(String intersection, int size) {
+ maxQueueSizeByIntersection.put(intersection, size);
+ }
+ public void setAvgQueueSize(String intersection, double size) {
+ avgQueueSizeByIntersection.put(intersection, size);
+ }
+ public void setVehiclesProcessed(String intersection, int count) {
+ vehiclesProcessedByIntersection.put(intersection, count);
+ }
+
+ /**
+ * Gera uma representação textual resumida das métricas principais da execução.
+ * Útil para logs rápidos e debugging.
+ */
+ @Override
+ public String toString() {
+ return String.format(
+ "Execução #%d [%s]:\n" +
+ " Gerados: %d, Completados: %d (%.1f%%)\n" +
+ " Tempo Médio no Sistema: %.2fs\n" +
+ " Tempo Médio de Espera: %.2fs",
+ runNumber,
+ configurationFile,
+ totalVehiclesGenerated,
+ totalVehiclesCompleted,
+ totalVehiclesGenerated > 0 ? 100.0 * totalVehiclesCompleted / totalVehiclesGenerated : 0.0,
+ averageSystemTime,
+ averageWaitingTime
+ );
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/analysis/StatisticalAnalysis.java b/main/src/main/java/sd/analysis/StatisticalAnalysis.java
new file mode 100644
index 0000000..c58ad67
--- /dev/null
+++ b/main/src/main/java/sd/analysis/StatisticalAnalysis.java
@@ -0,0 +1,193 @@
+package sd.analysis;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utilitário estático para processamento matemático e análise estatística dos dados da simulação.
+ *
+ * Esta classe fornece algoritmos para cálculo de medidas de tendência central (média, mediana),
+ * dispersão (desvio padrão amostral) e inferência estatística (Intervalos de Confiança).
+ * É utilizada para normalizar e validar os resultados estocásticos obtidos através de
+ * múltiplas execuções do sistema.
+ */
+public class StatisticalAnalysis {
+
+ /**
+ * Calcula a média aritmética de um conjunto de valores.
+ * * @param values Lista de valores numéricos (double).
+ * @return A soma dos valores dividida pelo tamanho da amostra, ou 0.0 se a lista for nula/vazia.
+ */
+ public static double mean(List values) {
+ if (values == null || values.isEmpty()) {
+ return 0.0;
+ }
+ double sum = 0.0;
+ for (double value : values) {
+ sum += value;
+ }
+ return sum / values.size();
+ }
+
+ /**
+ * Calcula o desvio padrão amostral (sample standard deviation).
+ *
+ * Utiliza o denominador {@code n - 1} (Correção de Bessel) para fornecer um
+ * estimador não viesado da variância populacional, adequado para as amostras
+ * de simulação.
+ * * @param values Lista de observações.
+ * @return O desvio padrão calculado, ou 0.0 se o tamanho da amostra for < 2.
+ */
+ public static double standardDeviation(List values) {
+ if (values == null || values.size() < 2) {
+ return 0.0;
+ }
+
+ double mean = mean(values);
+ double sumSquaredDiff = 0.0;
+
+ for (double value : values) {
+ double diff = value - mean;
+ sumSquaredDiff += diff * diff;
+ }
+
+ // Sample standard deviation (n-1 denominator)
+ return Math.sqrt(sumSquaredDiff / (values.size() - 1));
+ }
+
+ /**
+ * Calcula o Intervalo de Confiança (IC) de 95% para a média.
+ *
+ * Utiliza a distribuição t de Student para maior precisão em amostras pequenas (n < 30),
+ * onde a aproximação pela distribuição Normal (Z) seria inadequada. O intervalo define
+ * a faixa onde a verdadeira média populacional reside com 95% de probabilidade.
+ * * @param values Lista de observações.
+ * @return Um array de double onde índice 0 é o limite inferior e índice 1 é o limite superior.
+ */
+ public static double[] confidenceInterval95(List values) {
+ if (values == null || values.size() < 2) {
+ double m = mean(values);
+ return new double[]{m, m};
+ }
+
+ double mean = mean(values);
+ double stdDev = standardDeviation(values);
+ int n = values.size();
+
+ // Critical value from t-distribution (approximation for common sample sizes)
+ double tCritical = getTCriticalValue(n);
+
+ // Standard error of the mean
+ double standardError = stdDev / Math.sqrt(n);
+
+ // Margin of error
+ double marginOfError = tCritical * standardError;
+
+ return new double[]{
+ mean - marginOfError, // Lower bound
+ mean + marginOfError // Upper bound
+ };
+ }
+
+ /**
+ * Retorna o valor crítico t (t-score) para um IC de 95% (bicaudal).
+ *
+ * Baseia-se nos graus de liberdade (gl = n - 1). Para amostras grandes (gl >= 30),
+ * aproxima-se do valor Z de 1.96.
+ * * @param sampleSize O tamanho da amostra (n).
+ * @return O fator multiplicativo t apropriado.
+ */
+ private static double getTCriticalValue(int sampleSize) {
+ int df = sampleSize - 1; // degrees of freedom
+
+ // t-critical values for 95% confidence (two-tailed)
+ if (df >= 30) return 1.96; // z-score for large samples
+ if (df >= 20) return 2.086;
+ if (df >= 15) return 2.131;
+ if (df >= 10) return 2.228;
+ if (df >= 5) return 2.571;
+ if (df >= 3) return 3.182;
+ if (df >= 2) return 4.303;
+ return 12.706; // df = 1
+ }
+
+ /**
+ * Identifica o valor mínimo absoluto na amostra.
+ * * @param values Lista de valores.
+ * @return O menor valor encontrado.
+ */
+ public static double min(List values) {
+ if (values == null || values.isEmpty()) {
+ return 0.0;
+ }
+ return Collections.min(values);
+ }
+
+ /**
+ * Identifica o valor máximo absoluto na amostra.
+ * * @param values Lista de valores.
+ * @return O maior valor encontrado.
+ */
+ public static double max(List values) {
+ if (values == null || values.isEmpty()) {
+ return 0.0;
+ }
+ return Collections.max(values);
+ }
+
+ /**
+ * Calcula a mediana da amostra.
+ *
+ * Nota de Desempenho: Este método ordena uma cópia da lista, resultando em
+ * complexidade O(n log n).
+ * * @param values Lista de valores.
+ * @return O valor central (ou média dos dois centrais) da distribuição ordenada.
+ */
+ public static double median(List values) {
+ if (values == null || values.isEmpty()) {
+ return 0.0;
+ }
+
+ List sorted = new ArrayList<>(values);
+ Collections.sort(sorted);
+
+ int size = sorted.size();
+ if (size % 2 == 0) {
+ return (sorted.get(size / 2 - 1) + sorted.get(size / 2)) / 2.0;
+ } else {
+ return sorted.get(size / 2);
+ }
+ }
+
+ /**
+ * Formata um sumário estatístico completo para uma métrica específica.
+ *
+ * Útil para logging e geração de relatórios textuais.
+ * * @param metricName Nome da métrica a ser exibida.
+ * @param values Os dados brutos associados à métrica.
+ * @return String formatada contendo Média, Desvio Padrão, IC95%, Min, Max e N.
+ */
+ public static String formatSummary(String metricName, List values) {
+ if (values == null || values.isEmpty()) {
+ return metricName + ": No data";
+ }
+
+ double mean = mean(values);
+ double stdDev = standardDeviation(values);
+ double[] ci = confidenceInterval95(values);
+ double min = min(values);
+ double max = max(values);
+
+ return String.format(
+ "%s:\n" +
+ " Mean: %.2f\n" +
+ " Std Dev: %.2f\n" +
+ " 95%% CI: [%.2f, %.2f]\n" +
+ " Min: %.2f\n" +
+ " Max: %.2f\n" +
+ " Samples: %d",
+ metricName, mean, stdDev, ci[0], ci[1], min, max, values.size()
+ );
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/config/SimulationConfig.java b/main/src/main/java/sd/config/SimulationConfig.java
index d11ed42..db4a9da 100644
--- a/main/src/main/java/sd/config/SimulationConfig.java
+++ b/main/src/main/java/sd/config/SimulationConfig.java
@@ -3,116 +3,235 @@ package sd.config;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.Properties;
+import com.google.gson.Gson;
+
/**
- * Class to load and manage simulation configurations.
- * Configurations are read from a .properties file. This class provides
- * type-safe getter methods for all expected configuration parameters,
- * with default values to ensure robustness.
+ * Responsável pelo carregamento, validação e acesso centralizado às configurações da simulação.
+ *
+ * Esta classe atua como uma fachada (Facade) para os parâmetros do sistema, abstraindo a origem
+ * dos dados (ficheiros {@code .properties} ou JSON). Implementa uma estratégia robusta de
+ * carregamento de recursos, suportando tanto caminhos absolutos do sistema de ficheiros quanto
+ * recursos embutidos no classpath.
+ *
+ * Além de propriedades chave-valor simples, gerencia a desserialização da topologia da rede
+ * através da classe interna {@link NetworkConfig}.
*/
public class SimulationConfig {
-
- /**
- * Holds all properties loaded from the file.
- */
+
+ /** Armazenamento em memória das propriedades chave-valor carregadas. */
private final Properties properties;
+
+ /** Estrutura hierárquica da configuração da rede carregada via JSON. */
+ private NetworkConfig networkConfig;
/**
- * Constructs a new SimulationConfig object by loading properties
- * from the specified file path.
+ * Objeto de transferência de dados (DTO) que representa a configuração global da rede.
+ * Mapeado a partir do ficheiro {@code network_config.json}.
+ */
+ public static class NetworkConfig {
+ private List intersections;
+
+ public List getIntersections() {
+ return intersections;
+ }
+ }
+
+ /**
+ * DTO que representa a configuração de uma única interseção na topologia.
+ */
+ public static class IntersectionConfig {
+ private String id;
+ private List lights;
+ private Map routes;
+
+ /** @return O identificador único da interseção (ex: "Cr1"). */
+ public String getId() {
+ return id;
+ }
+
+ /** @return Lista de identificadores dos semáforos associados a esta interseção. */
+ public List getLights() {
+ return lights;
+ }
+
+ /** @return Mapa de roteamento definindo destinos alcançáveis e seus próximos saltos. */
+ public Map getRoutes() {
+ return routes;
+ }
+ }
+
+ /**
+ * Inicializa o gestor de configuração carregando propriedades do caminho especificado.
+ * * Implementa uma estratégia de carregamento em cascata (fallback) para garantir robustez
+ * em diferentes ambientes de execução (IDE, JAR, Docker):
+ *
+ * - Sistema de Ficheiros Direto: Tenta carregar do caminho absoluto ou relativo.
+ * - Classpath (Contexto): Tenta carregar via {@code Thread.currentThread().getContextClassLoader()},
+ * normalizando prefixos como "src/main/resources" ou "classpath:".
+ * - Classpath (Classe): Tenta carregar via {@code SimulationConfig.class.getResourceAsStream},
+ * útil para recursos na raiz do JAR.
+ *
*
- * @param filePath The path to the .properties file (e.g., "src/main/resources/simulation.properties").
- * @throws IOException If the file cannot be found or read.
+ * @param filePath O caminho ou nome do recurso do ficheiro {@code .properties}.
+ * @throws IOException Se o ficheiro não puder ser localizado em nenhuma das estratégias,
+ * com uma mensagem detalhada das tentativas falhadas.
*/
public SimulationConfig(String filePath) throws IOException {
properties = new Properties();
- /**Tenta carregar diretamente a partir do sistema de ficheiros, se o ficheiro não existir
- * (por exemplo quando executado a partir do classpath/jar),
- * faz fallback para carregar a partir do classpath usando o ClassLoader.
- */
- IOException lastException = null; //FIXME: melhorar esta parte para reportar erros de forma mais clara
- try {
- try (InputStream input = new FileInputStream(filePath)) {
- properties.load(input);
- return; // carregado com sucesso a partir do caminho fornecido
- }
+ // List to track all attempted paths for better error reporting
+ List attemptedPaths = new ArrayList<>();
+ IOException fileSystemException = null;
+
+ // Strategy 1: Try to load directly from file system
+ try (InputStream input = new FileInputStream(filePath)) {
+ properties.load(input);
+ loadNetworkConfig();
+ return; // Successfully loaded from file system
} catch (IOException e) {
- lastException = e;
- //tenta carregar a partir do classpath sem prefixos comuns
- String resourcePath = filePath;
- //Remove prefixos que apontam para src/main/resources quando presentes
- resourcePath = resourcePath.replace("src/main/resources/", "").replace("src\\main\\resources\\", "");
- //Remove prefixo classpath: se fornecido
- if (resourcePath.startsWith("classpath:")) {
- resourcePath = resourcePath.substring("classpath:".length());
- if (resourcePath.startsWith("/")) resourcePath = resourcePath.substring(1);
- }
+ fileSystemException = e;
+ attemptedPaths.add("File system: " + filePath);
+ }
- InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
- if (resourceStream == null) {
- //como último recurso, tentar com um leading slash
- resourceStream = SimulationConfig.class.getResourceAsStream('/' + resourcePath);
- }
+ // Strategy 2: Try to load from classpath with path normalization
+ String resourcePath = filePath;
- if (resourceStream != null) {
- try (InputStream input = resourceStream) {
- properties.load(input);
- return;
- }
+ // Remove common src/main/resources prefixes
+ resourcePath = resourcePath.replace("src/main/resources/", "").replace("src\\main\\resources\\", "");
+
+ // Remove classpath: prefix if provided
+ if (resourcePath.startsWith("classpath:")) {
+ resourcePath = resourcePath.substring("classpath:".length());
+ if (resourcePath.startsWith("/")) {
+ resourcePath = resourcePath.substring(1);
}
}
- if (lastException != null) throw lastException;
+
+ // Try loading from classpath using thread context class loader
+ InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
+ attemptedPaths.add("Classpath (context): " + resourcePath);
+
+ if (resourceStream == null) {
+ // Strategy 3: Try with leading slash
+ String slashPath = "/" + resourcePath;
+ resourceStream = SimulationConfig.class.getResourceAsStream(slashPath);
+ attemptedPaths.add("Classpath (class): " + slashPath);
+ }
+
+ if (resourceStream != null) {
+ try (InputStream input = resourceStream) {
+ properties.load(input);
+ loadNetworkConfig();
+ return; // Successfully loaded from classpath
+ } catch (IOException e) {
+ // Failed to read from classpath resource
+ throw new IOException(
+ String.format("Failed to read properties from classpath resource '%s': %s",
+ resourcePath, e.getMessage()),
+ e);
+ }
+ }
+
+ // All strategies failed - provide comprehensive error message
+ StringBuilder errorMsg = new StringBuilder();
+ errorMsg.append("Configuration file '").append(filePath).append("' could not be found.\n");
+ errorMsg.append("Attempted locations:\n");
+ for (String path : attemptedPaths) {
+ errorMsg.append(" - ").append(path).append("\n");
+ }
+
+ if (fileSystemException != null) {
+ errorMsg.append("\nOriginal error: ").append(fileSystemException.getMessage());
+ }
+
+ throw new IOException(errorMsg.toString(), fileSystemException);
+ }
+
+ /**
+ * Carrega a configuração da topologia de rede a partir do ficheiro "network_config.json".
+ *
+ * Utiliza a biblioteca Gson para desserialização. Em caso de falha, emite um aviso para o
+ * {@code System.err} mas não aborta a execução, permitindo o uso de defaults ou redes vazias.
+ */
+ private void loadNetworkConfig() {
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) {
+ if (is == null) {
+ System.err.println("Warning: network_config.json not found in classpath. Using defaults/empty.");
+ return;
+ }
+ try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
+ Gson gson = new Gson();
+ this.networkConfig = gson.fromJson(reader, NetworkConfig.class);
+ }
+ } catch (IOException e) {
+ System.err.println("Failed to load network_config.json: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Retorna a configuração estruturada da rede.
+ * @return Objeto {@link NetworkConfig} ou null se o carregamento falhou.
+ */
+ public NetworkConfig getNetworkConfig() {
+ return networkConfig;
}
// --- Network configurations ---
/**
- * Gets the host address for a specific intersection.
- * @param intersectionId The ID of the intersection (e.g., "Cr1").
- * @return The host (e.g., "localhost").
+ * Obtém o endereço de host (nome DNS ou IP) para uma interseção específica.
+ * * @param intersectionId O ID da interseção (ex: "Cr1").
+ * @return O host configurado ou "localhost" por omissão.
*/
public String getIntersectionHost(String intersectionId) {
return properties.getProperty("intersection." + intersectionId + ".host", "localhost");
}
/**
- * Gets the port number for a specific intersection.
- * @param intersectionId The ID of the intersection (e.g., "Cr1").
- * @return The port number.
+ * Obtém a porta de escuta TCP para uma interseção específica.
+ * * @param intersectionId O ID da interseção (ex: "Cr1").
+ * @return O número da porta. Retorna 0 se não configurado.
*/
public int getIntersectionPort(String intersectionId) {
return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0"));
}
/**
- * Gets the host address for the dashboard server.
- * @return The dashboard host.
+ * Obtém o endereço de host do servidor de Dashboard (monitorização).
+ * @return O host do dashboard (padrão: "localhost").
*/
public String getDashboardHost() {
return properties.getProperty("dashboard.host", "localhost");
}
/**
- * Gets the port number for the dashboard server.
- * @return The dashboard port.
+ * Obtém a porta de conexão do servidor de Dashboard.
+ * @return A porta do dashboard (padrão: 9000).
*/
public int getDashboardPort() {
return Integer.parseInt(properties.getProperty("dashboard.port", "9000"));
}
/**
- * Gets the host address for the exit node.
- * @return The exit node host.
+ * Obtém o endereço de host do nó de saída (Exit Node), para onde os veículos são encaminhados ao sair da malha.
+ * @return O host do nó de saída (padrão: "localhost").
*/
public String getExitHost() {
return properties.getProperty("exit.host", "localhost");
}
/**
- * Gets the port number for the exit node.
- * @return The exit node port.
+ * Obtém a porta de conexão do nó de saída.
+ * @return A porta do nó de saída (padrão: 9001).
*/
public int getExitPort() {
return Integer.parseInt(properties.getProperty("exit.port", "9001"));
@@ -121,45 +240,76 @@ public class SimulationConfig {
// --- Simulation configurations ---
/**
- * Gets the total duration of the simulation in virtual seconds.
- * @return The simulation duration.
+ * Define a duração total da execução da simulação em segundos virtuais.
+ * @return A duração em segundos (padrão: 3600).
*/
public double getSimulationDuration() {
- return Double.parseDouble(properties.getProperty("simulation.duration", "3600.0"));
+ return Double.parseDouble(properties.getProperty("simulation.duration", "3600"));
}
/**
- * Gets the vehicle arrival model ("POISSON" or "FIXED").
- * @return The arrival model as a string.
+ * Obtém o fator de escala temporal para visualização/execução.
+ *
+ * - 0.0: Execução instantânea (DES puro, velocidade máxima).
+ * - 1.0: Tempo real (1 segundo simulado = 1 segundo real).
+ * - 0.01: Acelerado 100x.
+ *
+ * @return O fator de escala.
+ */
+ public double getTimeScale() {
+ return Double.parseDouble(properties.getProperty("simulation.time.scale", "0"));
+ }
+
+ /**
+ * Obtém o tempo de "drenagem" (drain time) em segundos virtuais.
+ *
+ * Este é o período adicional executado após o fim da geração de veículos para permitir
+ * que os veículos restantes no sistema completem os seus percursos.
+ * @return O tempo de drenagem (padrão: 60.0s).
+ */
+ public double getDrainTime() {
+ return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0"));
+ }
+
+ /**
+ * Determina o modelo estocástico utilizado para a chegada de veículos.
+ * @return "POISSON" (distribuição exponencial) ou "FIXED" (intervalo determinístico).
*/
public String getArrivalModel() {
return properties.getProperty("simulation.arrival.model", "POISSON");
}
/**
- * Gets the average arrival rate (lambda) for the POISSON model.
- * This represents the average number of vehicles arriving per second.
- * @return The arrival rate.
+ * Obtém a taxa média de chegada (lambda) para o modelo Poisson.
+ * @return Veículos por segundo (padrão: 0.5).
*/
public double getArrivalRate() {
return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5"));
}
/**
- * Gets the fixed time interval between vehicle arrivals for the FIXED model.
- * @return The fixed interval in seconds.
+ * Obtém o intervalo fixo entre chegadas para o modelo determinístico.
+ * @return O intervalo em segundos (padrão: 2.0).
*/
public double getFixedArrivalInterval() {
return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0"));
}
+ /**
+ * Obtém a política de roteamento utilizada pelos veículos para navegar na malha.
+ * @return A política: "RANDOM", "SHORTEST_PATH" ou "LEAST_CONGESTED".
+ */
+ public String getRoutingPolicy() {
+ return properties.getProperty("simulation.routing.policy", "RANDOM");
+ }
+
// --- Traffic light configurations ---
/**
- * Gets the duration of the GREEN light state for a specific traffic light.
- * @param intersectionId The ID of the intersection (e.g., "Cr1").
- * @param direction The direction of the light (e.g., "North").
- * @return The green light time in seconds.
+ * Obtém a duração do estado VERDE para um semáforo específico.
+ * * @param intersectionId ID da interseção.
+ * @param direction Direção do fluxo (ex: "North").
+ * @return Duração em segundos (padrão: 30.0).
*/
public double getTrafficLightGreenTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".green";
@@ -167,10 +317,10 @@ public class SimulationConfig {
}
/**
- * Gets the duration of the RED light state for a specific traffic light.
- * @param intersectionId The ID of the intersection (e.g., "Cr1").
- * @param direction The direction of the light (e.g., "North").
- * @return The red light time in seconds.
+ * Obtém a duração do estado VERMELHO para um semáforo específico.
+ * * @param intersectionId ID da interseção.
+ * @param direction Direção do fluxo.
+ * @return Duração em segundos (padrão: 30.0).
*/
public double getTrafficLightRedTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".red";
@@ -180,79 +330,105 @@ public class SimulationConfig {
// --- Vehicle configurations ---
/**
- * Gets the probability (0.0 to 1.0) that a generated vehicle is of type LIGHT.
- * @return The probability for LIGHT vehicles.
+ * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo LIGEIRO (LIGHT).
+ * @return Probabilidade (padrão: 0.7).
*/
public double getLightVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7"));
}
/**
- * Gets the average time it takes a LIGHT vehicle to cross an intersection.
- * @return The crossing time in seconds.
+ * Tempo médio necessário para um veículo LIGEIRO atravessar uma interseção.
+ * @return Tempo em segundos (padrão: 2.0).
*/
public double getLightVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0"));
}
/**
- * Gets the probability (0.0 to 1.0) that a generated vehicle is of type BIKE.
- * @return The probability for BIKE vehicles.
+ * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo BICICLETA (BIKE).
+ * @return Probabilidade (padrão: 0.0).
*/
public double getBikeVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0"));
}
/**
- * Gets the average time it takes a BIKE vehicle to cross an intersection.
- * @return The crossing time in seconds.
+ * Tempo médio necessário para uma BICICLETA atravessar uma interseção.
+ * @return Tempo em segundos (padrão: 1.5).
*/
public double getBikeVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5"));
}
/**
- * Gets the probability (0.0 to 1.0) that a generated vehicle is of type HEAVY.
- * @return The probability for HEAVY vehicles.
+ * Probabilidade (0.0 a 1.0) de geração de um veículo PESADO (HEAVY).
+ * @return Probabilidade (padrão: 0.0).
*/
public double getHeavyVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0"));
}
/**
- * Gets the average time it takes a HEAVY vehicle to cross an intersection.
- * @return The crossing time in seconds.
+ * Tempo médio necessário para um veículo PESADO atravessar uma interseção.
+ * @return Tempo em segundos (padrão: 4.0).
*/
public double getHeavyVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0"));
}
+ /**
+ * Define o tempo base de viagem entre interseções para veículos padrão.
+ * @return Tempo em segundos (padrão: 8.0).
+ */
+ public double getBaseTravelTime() {
+ return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0"));
+ }
+
+ /**
+ * Multiplicador de tempo de viagem para bicicletas.
+ *
Tempo efetivo = Base * Multiplicador.
+ * @return Fator multiplicativo (padrão: 0.5).
+ */
+ public double getBikeTravelTimeMultiplier() {
+ return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5"));
+ }
+
+ /**
+ * Multiplicador de tempo de viagem para veículos pesados.
+ *
Tempo efetivo = Base * Multiplicador.
+ * @return Fator multiplicativo (padrão: 4.0).
+ */
+ public double getHeavyTravelTimeMultiplier() {
+ return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0"));
+ }
+
// --- Statistics ---
/**
- * Gets the interval (in virtual seconds) between periodic statistics updates.
- * @return The statistics update interval.
+ * Intervalo de tempo (em segundos virtuais) para agregação e envio de estatísticas periódicas.
+ * @return Intervalo de atualização (padrão: 1.0).
*/
public double getStatisticsUpdateInterval() {
- return Double.parseDouble(properties.getProperty("statistics.update.interval", "10.0"));
+ return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0"));
}
// --- Generic getters ---
/**
- * Generic method to get any property as a string, with a default value.
- * @param key The property key.
- * @param defaultValue The value to return if the key is not found.
- * @return The property value or the default.
+ * Recupera uma propriedade genérica como String, com valor padrão de segurança.
+ * * @param key A chave da propriedade.
+ * @param defaultValue O valor a retornar caso a chave não exista.
+ * @return O valor da propriedade ou o default.
*/
public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
/**
- * Generic method to get any property as a string.
- * @param key The property key.
- * @return The property value, or null if not found.
+ * Recupera uma propriedade genérica como String.
+ * * @param key A chave da propriedade.
+ * @return O valor da propriedade ou null se não encontrada.
*/
public String getProperty(String key) {
return properties.getProperty(key);
diff --git a/main/src/main/java/sd/coordinator/CoordinatorProcess.java b/main/src/main/java/sd/coordinator/CoordinatorProcess.java
new file mode 100644
index 0000000..dd9135e
--- /dev/null
+++ b/main/src/main/java/sd/coordinator/CoordinatorProcess.java
@@ -0,0 +1,578 @@
+package sd.coordinator;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import sd.config.SimulationConfig;
+import sd.dashboard.DashboardStatistics;
+import sd.dashboard.StatsUpdatePayload;
+import sd.des.DESEventType;
+import sd.des.EventQueue;
+import sd.des.SimulationClock;
+import sd.des.SimulationEvent;
+import sd.logging.EventLogger;
+import sd.model.Message;
+import sd.model.MessageType;
+import sd.model.Vehicle;
+import sd.routing.LeastCongestedRouteSelector;
+import sd.routing.RandomRouteSelector;
+import sd.routing.RouteSelector;
+import sd.routing.RoutingPolicy;
+import sd.routing.ShortestPathRouteSelector;
+import sd.serialization.SerializationException;
+import sd.util.VehicleGenerator;
+
+/**
+ * Coordenador central da arquitetura de simulação distribuída.
+ *
+ * Este processo atua como o "cérebro" da simulação, sendo responsável por:
+ *
+ * - Orquestração DES: Gerir o relógio global ({@link SimulationClock}) e a fila de eventos prioritária.
+ * - Geração de Carga: Injetar veículos na malha viária seguindo distribuições estocásticas (Poisson) ou determinísticas.
+ * - Encaminhamento Dinâmico: Decidir as rotas dos veículos com base na política ativa (Random, Shortest Path, Least Congested).
+ * - Sincronização: Garantir que todos os nós (Interseções e Dashboard) operem em uníssono.
+ *
+ */
+public class CoordinatorProcess {
+
+ private final SimulationConfig config;
+ private final VehicleGenerator vehicleGenerator;
+
+ /** Mapa de clientes TCP persistentes para cada interseção (Worker Nodes). */
+ private final Map intersectionClients;
+ private SocketClient dashboardClient;
+
+ // Componentes DES (Discrete Event Simulation)
+ private final SimulationClock clock;
+ private final EventQueue eventQueue;
+ private final EventLogger eventLogger;
+
+ // Estado da simulação
+ private int vehicleCounter;
+ private boolean running;
+ private double timeScale;
+ private RouteSelector currentRouteSelector;
+
+ /** Referência para estatísticas do dashboard para polling de mudanças de política. */
+ private DashboardStatistics dashboardStatistics;
+
+ /**
+ * Monitorização local (aproximada) dos tamanhos de fila nas interseções.
+ *
+ * Utilizado exclusivamente pela política {@link LeastCongestedRouteSelector}.
+ * O coordenador incrementa este contador ao enviar um veículo para uma interseção.
+ * Nota: Esta é uma visão "borda" (edge) e pode não refletir a saída em tempo real
+ * dos veículos, mas serve como heurística suficiente para balanceamento de carga.
+ */
+ private final Map intersectionQueueSizes;
+
+ /**
+ * Ponto de entrada do processo Coordenador.
+ * Carrega configurações, estabelece conexões TCP e inicia o loop de eventos.
+ */
+ public static void main(String[] args) {
+ System.out.println("=".repeat(60));
+ System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
+ System.out.println("=".repeat(60));
+
+ try {
+ // 1. Load configuration
+ String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
+ System.out.println("Loading configuration from: " + configFile);
+
+ SimulationConfig config = new SimulationConfig(configFile);
+ CoordinatorProcess coordinator = new CoordinatorProcess(config);
+
+ // 2. Connect to intersection processes
+ System.out.println("\n" + "=".repeat(60));
+ coordinator.initialize();
+
+ // 3. Run the sim
+ System.out.println("\n" + "=".repeat(60));
+ coordinator.run();
+
+ } catch (IOException e) {
+ System.err.println("Failed to load configuration: " + e.getMessage());
+ System.exit(1);
+ } catch (Exception e) {
+ System.err.println("Coordinator error: " + e.getMessage());
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Inicializa o coordenador com a configuração fornecida.
+ * Configura o motor DES, logging e o seletor de rotas inicial.
+ *
+ * @param config Objeto de configuração carregado.
+ */
+ public CoordinatorProcess(SimulationConfig config) {
+ this.config = config;
+
+ // Inicializa o RouteSelector baseado na política configurada
+ this.currentRouteSelector = createRouteSelector(config.getRoutingPolicy());
+
+ this.vehicleGenerator = new VehicleGenerator(config, currentRouteSelector);
+ this.intersectionClients = new HashMap<>();
+ this.vehicleCounter = 0;
+ this.running = false;
+ this.timeScale = config.getTimeScale();
+ this.intersectionQueueSizes = new HashMap<>();
+
+ this.clock = new SimulationClock();
+ this.eventQueue = new EventQueue(true);
+
+ this.eventLogger = EventLogger.getInstance();
+ eventLogger.log(sd.logging.EventType.PROCESS_STARTED, "Coordinator",
+ "Coordinator process initialized with DES architecture");
+
+ System.out.println("Coordinator initialized with configuration:");
+ System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s");
+ System.out.println(" - Arrival model: " + config.getArrivalModel());
+ System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s");
+ System.out.println(" - Routing policy: " + config.getRoutingPolicy());
+ System.out.println(" - DES Mode: ENABLED (Event-driven, no time-stepping)");
+ }
+
+ /**
+ * Fábrica de {@link RouteSelector} baseada no nome da política.
+ * * @param policyName Nome da política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED).
+ * @return Uma instância da estratégia de roteamento.
+ */
+ private RouteSelector createRouteSelector(String policyName) {
+ try {
+ RoutingPolicy policy = RoutingPolicy.valueOf(policyName.toUpperCase());
+
+ switch (policy) {
+ case RANDOM:
+ System.out.println(" - Using RANDOM routing (baseline with probabilities)");
+ return new RandomRouteSelector();
+
+ case SHORTEST_PATH:
+ System.out.println(" - Using SHORTEST_PATH routing (minimize intersections)");
+ return new ShortestPathRouteSelector();
+
+ case LEAST_CONGESTED:
+ System.out.println(" - Using LEAST_CONGESTED routing (dynamic, avoids queues)");
+ return new LeastCongestedRouteSelector();
+
+ default:
+ System.err.println(" ! Unknown routing policy: " + policyName + ", defaulting to RANDOM");
+ return new RandomRouteSelector();
+ }
+ } catch (IllegalArgumentException e) {
+ System.err.println(" ! Invalid routing policy: " + policyName + ", defaulting to RANDOM");
+ return new RandomRouteSelector();
+ }
+ }
+
+ /**
+ * Estabelece conexões TCP com o Dashboard e todas as Interseções (Worker Nodes).
+ * Essencial para o envio de comandos de controle e injeção de veículos.
+ */
+ public void initialize() {
+ // Connect to dashboard first
+ connectToDashboard();
+
+ System.out.println("Connecting to intersection processes...");
+
+ String[] intersectionIds = { "Cr1", "Cr2", "Cr3", "Cr4", "Cr5" };
+
+ for (String intersectionId : intersectionIds) {
+ try {
+ String host = config.getIntersectionHost(intersectionId);
+ int port = config.getIntersectionPort(intersectionId);
+
+ SocketClient client = new SocketClient(intersectionId, host, port);
+ client.connect();
+ intersectionClients.put(intersectionId, client);
+
+ } catch (IOException e) {
+ System.err.println("Failed to connect to " + intersectionId + ": " + e.getMessage());
+ }
+ }
+
+ System.out.println("Successfully connected to " + intersectionClients.size() + " intersection(s)");
+
+ if (intersectionClients.isEmpty()) {
+ System.err.println("WARNING: No intersections connected. Simulation cannot proceed.");
+ }
+ }
+
+ /**
+ * Loop principal da simulação (DES Engine).
+ *
+ * Executa a sequência:
+ * 1. Retira o próximo evento da fila prioritária.
+ * 2. Avança o relógio virtual para o timestamp do evento.
+ * 3. Aplica escala temporal (Time Scale) para visualização, se necessário.
+ * 4. Processa o evento.
+ */
+ public void run() {
+ double duration = config.getSimulationDuration();
+ double drainTime = config.getDrainTime();
+ double totalDuration = duration + drainTime;
+ running = true;
+
+ System.out.println("Starting DES-based vehicle generation simulation...");
+ System.out.println("Duration: " + duration + "s (+ " + drainTime + "s drain)");
+ System.out.println();
+
+ // Log simulation start
+ eventLogger.log(sd.logging.EventType.SIMULATION_STARTED, "Coordinator",
+ String.format("Starting simulation - Duration: %.1fs", duration));
+
+ // Send simulation start time to all processes for synchronization
+ sendSimulationStartTime();
+
+ // Schedule first vehicle generation event
+ double firstArrivalTime = vehicleGenerator.getNextArrivalTime(clock.getCurrentTime());
+ eventQueue.schedule(new SimulationEvent(
+ firstArrivalTime,
+ DESEventType.VEHICLE_GENERATION,
+ null,
+ "Coordinator"));
+
+ // Schedule simulation end event
+ eventQueue.schedule(new SimulationEvent(
+ totalDuration,
+ DESEventType.SIMULATION_END,
+ null,
+ "Coordinator"));
+
+ System.out.printf("Initial event scheduled at t=%.3fs\n", firstArrivalTime);
+ System.out.println("Entering DES event loop...\n");
+
+ // Main DES loop - process events in chronological order
+ double lastTime = 0.0;
+ while (running && !eventQueue.isEmpty()) {
+ SimulationEvent event = eventQueue.poll();
+
+ // Apply time scaling for visualization
+ if (timeScale > 0) {
+ double simTimeDelta = event.getTimestamp() - lastTime;
+ long realDelayMs = (long) (simTimeDelta * timeScale * 1000);
+ if (realDelayMs > 0) {
+ try {
+ Thread.sleep(realDelayMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ lastTime = event.getTimestamp();
+ }
+
+ // Advance simulation time to event time
+ clock.advanceTo(event.getTimestamp());
+
+ // Process the event
+ processEvent(event, duration);
+ }
+
+ System.out.println();
+ System.out.printf("Simulation complete at t=%.2fs\n", clock.getCurrentTime());
+ System.out.println("Total vehicles generated: " + vehicleCounter);
+ System.out.println("Total events processed: " + eventQueue.getProcessedCount());
+
+ // Log simulation end
+ eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, "Coordinator",
+ String.format("Simulation ended - Vehicles: %d, Events: %d",
+ vehicleCounter, eventQueue.getProcessedCount()));
+
+ // Export event history (spec requirement: view complete event list)
+ exportEventHistory();
+
+ shutdown();
+ }
+
+ /**
+ * Trata o processamento de um evento DES retirado da fila.
+ * * @param event O evento a ser processado.
+ * @param generationDuration Duração da fase de geração ativa (antes do 'drain time').
+ */
+ private void processEvent(SimulationEvent event, double generationDuration) {
+ double currentTime = clock.getCurrentTime();
+
+ switch (event.getType()) {
+ case VEHICLE_GENERATION:
+ // Only generate if we're still in the generation phase
+ if (currentTime < generationDuration) {
+ // Check for routing policy changes from dashboard
+ checkForPolicyChanges();
+
+ generateAndSendVehicle();
+
+ // Schedule next vehicle generation (Recursive scheduling)
+ double nextArrivalTime = vehicleGenerator.getNextArrivalTime(currentTime);
+ eventQueue.schedule(new SimulationEvent(
+ nextArrivalTime,
+ DESEventType.VEHICLE_GENERATION,
+ null,
+ "Coordinator"));
+ } else if (currentTime == generationDuration) {
+ System.out.printf("\n[t=%.2f] Generation phase complete. Entering DRAIN MODE...\n",
+ currentTime);
+ }
+ break;
+
+ case SIMULATION_END:
+ System.out.printf("[t=%.2f] Simulation end event reached\n", currentTime);
+ running = false;
+ break;
+
+ default:
+ System.err.println("WARNING: Unknown event type: " + event.getType());
+ }
+ }
+
+ /**
+ * Exporta o log completo de eventos DES para auditoria e debug.
+ * Caminho: {@code logs/coordinator-event-history.txt}.
+ */
+ private void exportEventHistory() {
+ try (java.io.PrintWriter writer = new java.io.PrintWriter(
+ new java.io.FileWriter("logs/coordinator-event-history.txt"))) {
+ String history = eventQueue.exportEventHistory();
+ writer.println(history);
+ System.out.println("\nEvent history exported to: logs/coordinator-event-history.txt");
+ } catch (IOException e) {
+ System.err.println("Failed to export event history: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Gera um novo veículo e envia-o via TCP para a interseção de entrada apropriada.
+ * Também atualiza o rastreio local de filas para balanceamento de carga.
+ */
+ private void generateAndSendVehicle() {
+ double currentTime = clock.getCurrentTime();
+
+ // Usa os tamanhos de fila rastreados localmente para política LEAST_CONGESTED
+ // Isto permite roteamento dinâmico baseado no estado atual da rede
+ Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime, intersectionQueueSizes);
+
+ System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
+ currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
+
+ // Log to event logger
+ eventLogger.log(sd.logging.EventType.VEHICLE_GENERATED, "Coordinator",
+ String.format("[%s] Type: %s, Route: %s", vehicle.getId(), vehicle.getType(), vehicle.getRoute()));
+
+ // Update local queue size tracking (increment first intersection's queue)
+ String firstIntersection = vehicle.getRoute().get(0);
+ intersectionQueueSizes.put(firstIntersection,
+ intersectionQueueSizes.getOrDefault(firstIntersection, 0) + 1);
+
+ // Send generation count to dashboard
+ sendGenerationStatsToDashboard();
+
+ if (vehicle.getRoute().isEmpty()) {
+ System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
+ return;
+ }
+
+ String entryIntersection = vehicle.getRoute().get(0);
+ sendVehicleToIntersection(vehicle, entryIntersection);
+ }
+
+ /**
+ * Serializa e transmite o objeto Veículo para o nó (interseção) de destino.
+ */
+ private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
+ SocketClient client = intersectionClients.get(intersectionId);
+
+ if (client == null || !client.isConnected()) {
+ System.err.println("ERROR: No connection to " + intersectionId + " for vehicle " + vehicle.getId());
+ return;
+ }
+
+ try {
+ Message message = new Message(
+ MessageType.VEHICLE_SPAWN,
+ "COORDINATOR",
+ intersectionId,
+ vehicle);
+
+ client.send(message);
+ System.out.printf("->Sent to %s%n", intersectionId);
+
+ } catch (SerializationException | IOException e) {
+ System.err.println("ERROR: Failed to send vehicle " + vehicle.getId() + " to " + intersectionId);
+ System.err.println("Reason: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Encerra graciosamente a simulação, enviando sinais de SHUTDOWN para todos os nós.
+ */
+ public void shutdown() {
+ System.out.println();
+ System.out.println("=".repeat(60));
+ System.out.println("Shutting down coordinator...");
+
+ for (Map.Entry entry : intersectionClients.entrySet()) {
+ String intersectionId = entry.getKey();
+ SocketClient client = entry.getValue();
+
+ try {
+ if (client.isConnected()) {
+ Message personalizedShutdown = new Message(
+ MessageType.SHUTDOWN,
+ "COORDINATOR",
+ intersectionId,
+ "Simulation complete");
+ client.send(personalizedShutdown);
+ System.out.println("Sent shutdown message to " + intersectionId);
+ }
+ } catch (SerializationException | IOException e) {
+ System.err.println("Error sending shutdown to " + intersectionId + ": " + e.getMessage());
+ } finally {
+ client.close();
+ }
+ }
+
+ System.out.println("Coordinator shutdown complete");
+ System.out.println("=".repeat(60));
+ }
+
+ public void stop() {
+ System.out.println("\nStop signal received...");
+ running = false;
+ }
+
+ /**
+ * Altera dinamicamente a política de roteamento durante a simulação (Hot-swap).
+ * Thread-safe.
+ * * @param policyName nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
+ */
+ public synchronized void changeRoutingPolicy(String policyName) {
+ System.out.println("\n" + "=".repeat(60));
+ System.out.println("ROUTING POLICY CHANGE REQUEST");
+ System.out.println("=".repeat(60));
+ System.out.println("Current policy: " + getCurrentPolicyName());
+ System.out.println("Requested policy: " + policyName);
+
+ RouteSelector newSelector = createRouteSelector(policyName);
+ this.currentRouteSelector = newSelector;
+ this.vehicleGenerator.setRouteSelector(newSelector);
+
+ System.out.println("Routing policy successfully changed to: " + policyName);
+ System.out.println(" - New vehicles will use the updated policy");
+ System.out.println("=".repeat(60) + "\n");
+
+ eventLogger.log(sd.logging.EventType.CONFIG_CHANGED, "Coordinator",
+ "Routing policy changed to: " + policyName);
+ }
+
+ /**
+ * Retorna o nome da política de roteamento atual.
+ */
+ private String getCurrentPolicyName() {
+ if (currentRouteSelector instanceof RandomRouteSelector) {
+ return "RANDOM";
+ } else if (currentRouteSelector instanceof ShortestPathRouteSelector) {
+ return "SHORTEST_PATH";
+ } else if (currentRouteSelector instanceof LeastCongestedRouteSelector) {
+ return "LEAST_CONGESTED";
+ }
+ return "UNKNOWN";
+ }
+
+ /**
+ * Verifica se há solicitação de mudança de política proveniente do dashboard
+ * e aplica a alteração se houver.
+ */
+ private void checkForPolicyChanges() {
+ if (dashboardStatistics != null) {
+ String requestedPolicy = dashboardStatistics.getAndClearRequestedRoutingPolicy();
+ if (requestedPolicy != null && !requestedPolicy.isEmpty()) {
+ changeRoutingPolicy(requestedPolicy);
+ }
+ }
+ }
+
+ /**
+ * Injeta a referência para as estatísticas do dashboard.
+ * Permite que o coordenador consuma intenções de mudança de política do utilizador.
+ */
+ public void setDashboardStatistics(DashboardStatistics stats) {
+ this.dashboardStatistics = stats;
+ }
+
+ 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());
+ }
+ }
+
+ /**
+ * Sincronização Global: Envia o timestamp de início (System.currentTimeMillis)
+ * para todos os componentes distribuídos, garantindo uma base de tempo comum
+ * para métricas de latência.
+ */
+ 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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/coordinator/SocketClient.java b/main/src/main/java/sd/coordinator/SocketClient.java
new file mode 100644
index 0000000..4dafc48
--- /dev/null
+++ b/main/src/main/java/sd/coordinator/SocketClient.java
@@ -0,0 +1,140 @@
+package sd.coordinator;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+
+import sd.model.Message;
+import sd.serialization.MessageSerializer;
+import sd.serialization.SerializationException;
+import sd.serialization.SerializerFactory;
+
+/**
+ * Abstração de cliente TCP para comunicação outbound (de saída) com nós da rede.
+ *
+ * Esta classe encapsula a gestão do socket raw, oferecendo uma interface de alto nível
+ * para envio de objetos {@link Message}. Implementa o protocolo de camada de aplicação
+ * proprietário, garantindo a serialização correta e o enquadramento (framing) dos dados
+ * na stream TCP.
+ *
+ * É utilizada pelo Coordenador para controlar Interseções e enviar telemetria para o Dashboard.
+ */
+public class SocketClient {
+
+ private final String intersectionId;
+ private final String host;
+ private final int port;
+ private Socket socket;
+ private OutputStream outputStream;
+ private MessageSerializer serializer;
+
+ /**
+ * Instancia um novo cliente socket configurado para um destino específico.
+ *
+ * @param intersectionId Identificador lógico do nó de destino (ex: "Cr1", "Dashboard").
+ * @param host Endereço IP ou hostname do destino.
+ * @param port Porta TCP de escuta do destino.
+ */
+ public SocketClient(String intersectionId, String host, int port) {
+ this.intersectionId = intersectionId;
+ this.host = host;
+ this.port = port;
+ this.serializer = SerializerFactory.createDefault();
+ }
+
+ /**
+ * Estabelece a conexão TCP (Handshake SYN/ACK) com o host remoto.
+ * * @throws IOException Se o host for inalcançável ou a conexão for recusada.
+ */
+ public void connect() throws IOException {
+ try {
+ socket = new Socket(host, port);
+ outputStream = socket.getOutputStream();
+ System.out.println("Connected to " + intersectionId + " at " + host + ":" + port);
+ } catch (IOException e) {
+ System.err.println("Failed to connect to " + intersectionId + " at " + host + ":" + port);
+ throw e;
+ }
+ }
+
+ /**
+ * Serializa e transmite uma mensagem através do socket conectado.
+ *
+ * Protocolo de Envio (Length-Prefix Framing):
+ *
+ * - Serializa o objeto {@link Message} para um array de bytes.
+ * - Calcula o tamanho (N) do array.
+ * - Escreve um cabeçalho de 4 bytes contendo N (Big-Endian).
+ * - Escreve os N bytes do payload (corpo da mensagem).
+ * - Realiza flush no stream para forçar o envio imediato do pacote TCP.
+ *
+ * Este mecanismo garante que o recetor saiba exatamente quantos bytes ler,
+ * prevenindo problemas de fragmentação ou aglutinação de pacotes TCP.
+ *
+ * @param message O objeto de domínio a ser enviado.
+ * @throws SerializationException Se o objeto não puder ser convertido para bytes.
+ * @throws IOException Se houver falha na escrita do socket (ex: conexão resetada).
+ */
+ public void send(Message message) throws SerializationException, IOException {
+ if (socket == null || socket.isClosed()) {
+ throw new IOException("Socket is not connected to " + intersectionId);
+ }
+
+ try {
+ byte[] data = serializer.serialize(message);
+
+ int length = data.length;
+ // Write 4-byte length header (Big Endian)
+ outputStream.write((length >> 24) & 0xFF);
+ outputStream.write((length >> 16) & 0xFF);
+ outputStream.write((length >> 8) & 0xFF);
+ outputStream.write(length & 0xFF);
+
+ // Write payload
+ outputStream.write(data);
+ outputStream.flush();
+
+ } catch (SerializationException | IOException e) {
+ System.err.println("Error sending message to " + intersectionId + ": " + e.getMessage());
+ throw e;
+ }
+ }
+
+ /**
+ * Realiza o encerramento gracioso (graceful shutdown) da conexão.
+ * Liberta os recursos do sistema operativo (descritores de arquivo).
+ *
+ * Operação idempotente: pode ser chamada múltiplas vezes sem erro.
+ */
+ public void close() {
+ try {
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ if (socket != null && !socket.isClosed()) {
+ socket.close();
+ System.out.println("Closed connection to " + intersectionId);
+ }
+ } catch (IOException e) {
+ System.err.println("Error closing connection to " + intersectionId + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * Verifica o estado atual da conexão.
+ * * @return true se o socket estiver instanciado, conectado e aberto; false caso contrário.
+ */
+ public boolean isConnected() {
+ return socket != null && socket.isConnected() && !socket.isClosed();
+ }
+
+ public String getIntersectionId() {
+ return intersectionId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SocketClient[intersection=%s, host=%s, port=%d, connected=%s]",
+ intersectionId, host, port, isConnected());
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/BatchAnalysisDialog.java b/main/src/main/java/sd/dashboard/BatchAnalysisDialog.java
new file mode 100644
index 0000000..ebb9b0c
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/BatchAnalysisDialog.java
@@ -0,0 +1,576 @@
+package sd.dashboard;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import sd.analysis.MultiRunAnalyzer;
+import sd.analysis.SimulationRunResult;
+import sd.model.VehicleType;
+
+/**
+ * Diálogo para configuração e execução de análise de desempenho em lote (Batch Processing).
+ *
+ * Esta classe fornece uma interface gráfica para automatizar múltiplas execuções da simulação
+ * sob diferentes cenários de carga. É responsável por:
+ *
+ * - Orquestrar o ciclo de vida dos processos de simulação (start/stop/wait).
+ * - Coletar métricas estatísticas de cada execução.
+ * - Agregar resultados usando o {@link MultiRunAnalyzer}.
+ * - Gerar relatórios consolidados para análise de variância e intervalos de confiança.
+ *
+ * A execução ocorre numa thread separada (background) para manter a responsividade da UI.
+ */
+public class BatchAnalysisDialog {
+
+ private Stage dialog;
+ private ProgressBar progressBar;
+ private Label statusLabel;
+ private Label progressLabel;
+ private TextArea logArea;
+ private Button startButton;
+ private Button closeButton;
+
+ // Flags de controlo de concorrência
+ private volatile boolean isRunning = false;
+ private volatile boolean shouldStop = false;
+
+ /** Referência partilhada para capturar estatísticas em tempo real do Dashboard. */
+ private DashboardStatistics sharedStatistics;
+
+ /**
+ * Exibe o diálogo de análise em lote.
+ * * @param owner A janela pai (Stage) para modalidade.
+ * @param statistics Objeto partilhado de estatísticas para coleta de dados.
+ */
+ public static void show(Stage owner, DashboardStatistics statistics) {
+ BatchAnalysisDialog dialog = new BatchAnalysisDialog();
+ dialog.sharedStatistics = statistics;
+ dialog.createAndShow(owner);
+ }
+
+ /**
+ * Constrói e inicializa a interface gráfica do diálogo.
+ */
+ private void createAndShow(Stage owner) {
+ dialog = new Stage();
+ dialog.initOwner(owner);
+ dialog.initModality(Modality.APPLICATION_MODAL);
+ dialog.setTitle("Batch Performance Analysis");
+
+ VBox root = new VBox(20);
+ root.setPadding(new Insets(20));
+ root.setAlignment(Pos.TOP_CENTER);
+ // Estilo Dark Mode conforme guidelines visuais
+ root.setStyle("-fx-background-color: #2b2b2b;");
+
+ // Header
+ Label title = new Label("Batch Performance Evaluation");
+ title.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: white;");
+
+ Label subtitle = new Label("Executar múltiplas simulações para gerar análise estatística consolidada");
+ subtitle.setStyle("-fx-font-size: 12px; -fx-text-fill: #cccccc;");
+ subtitle.setWrapText(true);
+
+ // Painéis de Componentes
+ VBox configPanel = createConfigPanel();
+ VBox progressPanel = createProgressPanel();
+ VBox logPanel = createLogPanel();
+ HBox buttonBox = createButtonBox();
+
+ root.getChildren().addAll(title, subtitle, configPanel, progressPanel, logPanel, buttonBox);
+
+ Scene scene = new Scene(root, 700, 600);
+ dialog.setScene(scene);
+
+ // Tratamento de fecho da janela: interromper thread de worker se ativa
+ dialog.setOnCloseRequest(e -> {
+ if (isRunning) {
+ e.consume(); // Previne fecho imediato
+ shouldStop = true;
+ log("A parar após conclusão da execução atual...");
+ }
+ });
+
+ dialog.show();
+ }
+
+ private VBox createConfigPanel() {
+ VBox panel = new VBox(15);
+ panel.setPadding(new Insets(15));
+ panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;");
+
+ Label header = new Label("Configuração");
+ header.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: white;");
+
+ // Runs per scenario
+ HBox runsBox = new HBox(10);
+ runsBox.setAlignment(Pos.CENTER_LEFT);
+ Label runsLabel = new Label("Execuções por cenário:");
+ runsLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;");
+ Spinner runsSpinner = new Spinner<>(1, 20, 5, 1);
+ runsSpinner.setEditable(true);
+ runsSpinner.setPrefWidth(80);
+ runsSpinner.setId("runsSpinner");
+ runsBox.getChildren().addAll(runsLabel, runsSpinner);
+
+ // Scenario selection
+ Label scenarioHeader = new Label("Selecionar Cenários:");
+ scenarioHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;");
+
+ CheckBox lowCheck = new CheckBox("Carga Baixa (λ=0.2 v/s)");
+ lowCheck.setSelected(true);
+ lowCheck.setId("lowCheck");
+ lowCheck.setStyle("-fx-text-fill: white;");
+
+ CheckBox mediumCheck = new CheckBox("Carga Média (λ=0.5 v/s)");
+ mediumCheck.setSelected(true);
+ mediumCheck.setId("mediumCheck");
+ mediumCheck.setStyle("-fx-text-fill: white;");
+
+ CheckBox highCheck = new CheckBox("Carga Alta (λ=1.0 v/s)");
+ highCheck.setSelected(true);
+ highCheck.setId("highCheck");
+ highCheck.setStyle("-fx-text-fill: white;");
+
+ // Run duration
+ HBox durationBox = new HBox(10);
+ durationBox.setAlignment(Pos.CENTER_LEFT);
+ Label durationLabel = new Label("Duração (segundos):");
+ durationLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;");
+ Spinner durationSpinner = new Spinner<>(30, 3600, 120, 30);
+ durationSpinner.setEditable(true);
+ durationSpinner.setPrefWidth(80);
+ durationSpinner.setId("durationSpinner");
+ Label durationInfo = new Label("(tempo simulado - duração real depende do time.scale)");
+ durationInfo.setStyle("-fx-text-fill: #999999; -fx-font-size: 10px;");
+ durationBox.getChildren().addAll(durationLabel, durationSpinner, durationInfo);
+
+ panel.getChildren().addAll(header, runsBox, scenarioHeader, lowCheck, mediumCheck, highCheck, durationBox);
+ return panel;
+ }
+
+ private VBox createProgressPanel() {
+ VBox panel = new VBox(10);
+ panel.setPadding(new Insets(15));
+ panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;");
+
+ statusLabel = new Label("Pronto para iniciar");
+ statusLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold;");
+
+ progressBar = new ProgressBar(0);
+ progressBar.setPrefWidth(Double.MAX_VALUE);
+ progressBar.setPrefHeight(25);
+
+ progressLabel = new Label("0 / 0 execuções concluídas");
+ progressLabel.setStyle("-fx-text-fill: #cccccc; -fx-font-size: 11px;");
+
+ panel.getChildren().addAll(statusLabel, progressBar, progressLabel);
+ return panel;
+ }
+
+ private VBox createLogPanel() {
+ VBox panel = new VBox(5);
+
+ Label logHeader = new Label("Log de Atividade:");
+ logHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;");
+
+ logArea = new TextArea();
+ logArea.setEditable(false);
+ logArea.setPrefRowCount(10);
+ logArea.setWrapText(true);
+ // Estilo de terminal para o log
+ logArea.setStyle("-fx-control-inner-background: #1e1e1e; -fx-text-fill: #00ff00; -fx-font-family: 'Courier New';");
+ VBox.setVgrow(logArea, Priority.ALWAYS);
+
+ panel.getChildren().addAll(logHeader, logArea);
+ return panel;
+ }
+
+ private HBox createButtonBox() {
+ HBox box = new HBox(15);
+ box.setAlignment(Pos.CENTER);
+ box.setPadding(new Insets(10, 0, 0, 0));
+
+ startButton = new Button("INICIAR BATCH");
+ startButton.setStyle("-fx-background-color: #28a745; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
+ startButton.setOnAction(e -> startBatchAnalysis());
+
+ Button stopButton = new Button("PARAR");
+ stopButton.setStyle("-fx-background-color: #dc3545; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
+ stopButton.setOnAction(e -> {
+ shouldStop = true;
+ log("Paragem solicitada...");
+ });
+
+ closeButton = new Button("FECHAR");
+ closeButton.setStyle("-fx-background-color: #6c757d; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
+ closeButton.setOnAction(e -> dialog.close());
+
+ box.getChildren().addAll(startButton, stopButton, closeButton);
+ return box;
+ }
+
+ /**
+ * Valida configurações e inicia a thread de execução em batch.
+ */
+ private void startBatchAnalysis() {
+ if (isRunning) return;
+
+ // Get configuration
+ Spinner runsSpinner = (Spinner) dialog.getScene().lookup("#runsSpinner");
+ Spinner durationSpinner = (Spinner) dialog.getScene().lookup("#durationSpinner");
+ CheckBox lowCheck = (CheckBox) dialog.getScene().lookup("#lowCheck");
+ CheckBox mediumCheck = (CheckBox) dialog.getScene().lookup("#mediumCheck");
+ CheckBox highCheck = (CheckBox) dialog.getScene().lookup("#highCheck");
+
+ int runsPerScenario = runsSpinner.getValue();
+ int duration = durationSpinner.getValue();
+
+ // Validate selection
+ if (!lowCheck.isSelected() && !mediumCheck.isSelected() && !highCheck.isSelected()) {
+ log("ERRO: Selecione pelo menos um cenário!");
+ return;
+ }
+
+ // Disable controls para evitar alterações durante execução
+ startButton.setDisable(true);
+ runsSpinner.setDisable(true);
+ durationSpinner.setDisable(true);
+ lowCheck.setDisable(true);
+ mediumCheck.setDisable(true);
+ highCheck.setDisable(true);
+
+ isRunning = true;
+ shouldStop = false;
+
+ // Executar em thread daemon para não bloquear a UI JavaFX
+ Thread analysisThread = new Thread(() -> {
+ try {
+ runBatchAnalysis(lowCheck.isSelected(), mediumCheck.isSelected(),
+ highCheck.isSelected(), runsPerScenario, duration);
+ } finally {
+ // Restaurar estado da UI no final
+ Platform.runLater(() -> {
+ startButton.setDisable(false);
+ runsSpinner.setDisable(false);
+ durationSpinner.setDisable(false);
+ lowCheck.setDisable(false);
+ mediumCheck.setDisable(false);
+ highCheck.setDisable(false);
+ isRunning = false;
+ });
+ }
+ });
+ analysisThread.setDaemon(true);
+ analysisThread.start();
+ }
+
+ /**
+ * Lógica principal de orquestração do batch.
+ * Itera sobre cenários e execuções, chamando a simulação e o analisador.
+ */
+ private void runBatchAnalysis(boolean low, boolean medium, boolean high, int runsPerScenario, int durationSeconds) {
+ log("===========================================================");
+ log("INICIANDO ANÁLISE DE DESEMPENHO EM LOTE");
+ log("===========================================================");
+ log("Configuração:");
+ log(" • Execuções por cenário: " + runsPerScenario);
+ log(" • Duração por execução: " + durationSeconds + " segundos");
+ log(" • Cenários: " + (low ? "LOW " : "") + (medium ? "MEDIUM " : "") + (high ? "HIGH" : ""));
+ log("");
+
+ String[] scenarios = new String[]{
+ low ? "simulation-low.properties" : null,
+ medium ? "simulation-medium.properties" : null,
+ high ? "simulation-high.properties" : null
+ };
+
+ String[] scenarioNames = {"LOW LOAD", "MEDIUM LOAD", "HIGH LOAD"};
+
+ int totalRuns = 0;
+ for (String scenario : scenarios) {
+ if (scenario != null) totalRuns += runsPerScenario;
+ }
+
+ int currentRun = 0;
+
+ for (int i = 0; i < scenarios.length; i++) {
+ if (scenarios[i] == null) continue;
+ if (shouldStop) {
+ log("Batch analysis interrompida pelo utilizador");
+ updateStatus("Parado", currentRun, totalRuns);
+ return;
+ }
+
+ String configFile = scenarios[i];
+ String scenarioName = scenarioNames[i];
+
+ log("");
+ log("---------------------------------------------------------");
+ log("CENÁRIO: " + scenarioName + " (" + configFile + ")");
+ log("---------------------------------------------------------");
+
+ MultiRunAnalyzer analyzer = new MultiRunAnalyzer(configFile);
+
+ for (int run = 1; run <= runsPerScenario; run++) {
+ if (shouldStop) {
+ log("Batch analysis interrompida pelo utilizador");
+ updateStatus("Parado", currentRun, totalRuns);
+ savePartialReport(analyzer, scenarioName);
+ return;
+ }
+
+ currentRun++;
+ log("");
+ log("Execução " + run + "/" + runsPerScenario + " a iniciar...");
+ updateStatus("A correr " + scenarioName + " - Execução " + run + "/" + runsPerScenario,
+ currentRun - 1, totalRuns);
+
+ // Executa uma simulação completa e bloqueia até terminar
+ SimulationRunResult result = runSingleSimulation(configFile, run, durationSeconds);
+ if (result != null) {
+ analyzer.addResult(result);
+ log("Execução " + run + " completa - Gerados: " + result.getTotalVehiclesGenerated() +
+ " | Completados: " + result.getTotalVehiclesCompleted() +
+ " | Tempo Médio: " + String.format("%.2f", result.getAverageSystemTime()) + "s");
+ } else {
+ log("Execução " + run + " falhou!");
+ }
+
+ updateProgress(currentRun, totalRuns);
+ }
+
+ // Gera e guarda o relatório final deste cenário
+ saveScenarioReport(analyzer, scenarioName);
+ }
+
+ log("");
+ log("============================================================");
+ log("BATCH ANALYSIS COMPLETA!");
+ log("===========================================================");
+ log("Relatórios guardados em: analysis/");
+ log("");
+
+ updateStatus("Concluído!", totalRuns, totalRuns);
+ updateProgress(1.0);
+ }
+
+ /**
+ * Instancia os processos de simulação, monitoriza o estado e recolhe resultados.
+ */
+ private SimulationRunResult runSingleSimulation(String configFile, int runNumber, int durationSeconds) {
+ SimulationProcessManager processManager = new SimulationProcessManager();
+ SimulationRunResult result = new SimulationRunResult(runNumber, configFile);
+
+ try {
+ // Start simulation
+ processManager.setConfigFile(configFile);
+ processManager.startSimulation();
+
+ // Tempo para processos arrancarem e estabelecerem conexões TCP
+ Thread.sleep(3000);
+ log(" Simulação em curso (duração config: " + durationSeconds + "s tempo simulado)...");
+ log(" A aguardar processo Coordenador completar...");
+
+ // Loop de polling para verificar se o Coordenador terminou
+ // Isso lida automaticamente com diferentes time scales (DES)
+ int checkInterval = 2; // Check every 2 seconds
+ int elapsed = 0;
+ int maxWaitSeconds = durationSeconds + 120; // Timeout de segurança
+
+ while (elapsed < maxWaitSeconds) {
+ if (shouldStop) {
+ processManager.stopSimulation();
+ return null;
+ }
+
+ // Check if simulation completed
+ if (!processManager.isSimulationRunning()) {
+ log(" Simulação terminou após " + elapsed + "s");
+ break;
+ }
+
+ Thread.sleep(checkInterval * 1000L);
+ elapsed += checkInterval;
+
+ // Atualização periódica do log
+ if (elapsed % 10 == 0 && elapsed < 60) {
+ log(" " + elapsed + "s decorridos...");
+ }
+ }
+
+ if (elapsed >= maxWaitSeconds) {
+ log(" Timeout atingido, forçando paragem...");
+ }
+
+ // Stop and collect results
+ log(" A terminar processos...");
+ processManager.stopSimulation();
+ Thread.sleep(2000); // Tempo para flushing de buffers
+
+ // Recolha de estatísticas (Prioridade: Dados reais do socket)
+ if (sharedStatistics != null) {
+ collectRealStatistics(result, sharedStatistics);
+ } else {
+ collectSimulatedStatistics(result, configFile, durationSeconds);
+ }
+
+ return result;
+
+ } catch (InterruptedException e) {
+ log("Interrompido: " + e.getMessage());
+ Thread.currentThread().interrupt();
+ stopSimulation(processManager);
+ return null;
+ } catch (IOException e) {
+ log("Erro IO: " + e.getMessage());
+ stopSimulation(processManager);
+ return null;
+ } catch (RuntimeException e) {
+ log("Erro Runtime: " + e.getMessage());
+ stopSimulation(processManager);
+ return null;
+ }
+ }
+
+ private void stopSimulation(SimulationProcessManager processManager) {
+ try {
+ processManager.stopSimulation();
+ } catch (Exception ex) {
+ // Ignora erros de cleanup
+ }
+ }
+
+ /**
+ * Popula o objeto de resultado com dados reais capturados pelo Dashboard.
+ */
+ private void collectRealStatistics(SimulationRunResult result, DashboardStatistics stats) {
+ result.setTotalVehiclesGenerated(stats.getTotalVehiclesGenerated());
+ result.setTotalVehiclesCompleted(stats.getTotalVehiclesCompleted());
+ result.setAverageSystemTime(stats.getAverageSystemTime() / 1000.0); // Converte ms para segundos
+ result.setAverageWaitingTime(stats.getAverageWaitingTime() / 1000.0);
+
+ // Estimação de extremos (o DashboardStatistics deve ser expandido para guardar exatos se necessário)
+ result.setMinSystemTime(stats.getAverageSystemTime() / 1000.0 * 0.5);
+ result.setMaxSystemTime(stats.getAverageSystemTime() / 1000.0 * 2.0);
+
+ // Estatísticas por tipo
+ for (VehicleType type : VehicleType.values()) {
+ int count = stats.getVehicleTypeCount(type);
+ double waitTime = stats.getAverageWaitingTimeByType(type) / 1000.0;
+ result.setVehicleCountByType(type, count);
+ result.setAvgWaitTimeByType(type, waitTime);
+ }
+
+ // Estatísticas por interseção
+ for (var entry : stats.getAllIntersectionStats().entrySet()) {
+ String intersectionId = entry.getKey();
+ DashboardStatistics.IntersectionStats iStats = entry.getValue();
+
+ result.setVehiclesProcessed(intersectionId, iStats.getTotalDepartures());
+ result.setMaxQueueSize(intersectionId, iStats.getCurrentQueueSize());
+ result.setAvgQueueSize(intersectionId, (double) iStats.getCurrentQueueSize());
+ }
+ }
+
+ /**
+ * Gera dados simulados (mock) caso o dashboard não esteja conectado.
+ * Útil para testes de interface.
+ */
+ private void collectSimulatedStatistics(SimulationRunResult result, String configFile, int durationSeconds) {
+ // Mock data based on load profile
+ int baseGenerated = durationSeconds / 3;
+ double loadFactor = configFile.contains("low") ? 0.2 :
+ configFile.contains("medium") ? 0.5 : 1.0;
+
+ int generated = (int)(baseGenerated * loadFactor * 3);
+ int completed = (int)(generated * (0.85 + Math.random() * 0.1)); // 85-95% completion rate
+
+ double baseSystemTime = 40.0;
+ double congestionFactor = configFile.contains("low") ? 1.0 :
+ configFile.contains("medium") ? 1.5 : 2.5;
+
+ result.setTotalVehiclesGenerated(generated);
+ result.setTotalVehiclesCompleted(completed);
+ result.setAverageSystemTime(baseSystemTime * congestionFactor + Math.random() * 10);
+ result.setMinSystemTime(20.0 + Math.random() * 5);
+ result.setMaxSystemTime(baseSystemTime * congestionFactor * 2 + Math.random() * 20);
+ result.setAverageWaitingTime(10.0 * congestionFactor + Math.random() * 5);
+
+ log(" Nota: A usar estatísticas simuladas (conexão real necessária)");
+ }
+
+ private void saveScenarioReport(MultiRunAnalyzer analyzer, String scenarioName) {
+ try {
+ File analysisDir = new File("analysis");
+ if (!analysisDir.exists()) {
+ analysisDir.mkdirs();
+ }
+
+ String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date());
+ String reportFile = "analysis/" + scenarioName.replace(" ", "_") + "_" + timestamp + ".txt";
+ String csvFile = "analysis/" + scenarioName.replace(" ", "_") + "_" + timestamp + ".csv";
+
+ analyzer.saveReport(reportFile);
+ analyzer.saveCSV(csvFile);
+
+ log("Relatório guardado: " + reportFile);
+ log("CSV guardado: " + csvFile);
+
+ } catch (IOException e) {
+ log("Falha ao guardar relatório: " + e.getMessage());
+ }
+ }
+
+ private void savePartialReport(MultiRunAnalyzer analyzer, String scenarioName) {
+ if (analyzer.getRunCount() > 0) {
+ log("A guardar resultados parciais...");
+ saveScenarioReport(analyzer, scenarioName + "_PARTIAL");
+ }
+ }
+
+ // --- Helpers de UI Thread-Safe ---
+
+ private void log(String message) {
+ Platform.runLater(() -> {
+ logArea.appendText(message + "\n");
+ logArea.setScrollTop(Double.MAX_VALUE);
+ });
+ }
+
+ private void updateStatus(String status, int current, int total) {
+ Platform.runLater(() -> {
+ statusLabel.setText(status);
+ progressLabel.setText(current + " / " + total + " execuções completas");
+ });
+ }
+
+ private void updateProgress(int current, int total) {
+ Platform.runLater(() -> {
+ progressBar.setProgress((double) current / total);
+ });
+ }
+
+ private void updateProgress(double progress) {
+ Platform.runLater(() -> {
+ progressBar.setProgress(progress);
+ });
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/ConfigurationDialog.java b/main/src/main/java/sd/dashboard/ConfigurationDialog.java
new file mode 100644
index 0000000..d823d63
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/ConfigurationDialog.java
@@ -0,0 +1,183 @@
+package sd.dashboard;
+
+import javafx.geometry.Insets;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.Label;
+import javafx.scene.control.Separator;
+import javafx.scene.control.Spinner;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+
+/**
+ * Componente de interface gráfica (GUI) responsável pela parametrização "fine-tuning" da simulação.
+ *
+ * Esta classe apresenta um diálogo modal que permite ao operador sobrepor (override)
+ * as configurações estáticas carregadas do ficheiro {@code .properties} imediatamente
+ * antes da execução. Oferece controlo granular sobre:
+ *
+ * - Geração de Carga: Alternância entre modelos estocásticos (Poisson) e determinísticos.
+ * - Temporização: Ajuste da escala de tempo (Time Scale) para visualização vs. performance pura.
+ * - Mix de Veículos: Definição das probabilidades de geração por tipo de agente.
+ *
+ */
+public class ConfigurationDialog {
+
+ /**
+ * Exibe o diálogo de configuração avançada e captura as intenções do utilizador.
+ *
+ * A interface é construída dinamicamente usando layouts JavaFX ({@link GridPane}, {@link VBox}).
+ * Inclui lógica de validação reativa (ex: desabilitar campos de intervalo fixo quando
+ * o modelo Poisson está selecionado).
+ * *
+
+[Image of Poisson distribution graph]
+
+ *
+ * @param owner A janela pai (Stage) para bloquear a interação até o fecho do diálogo (Modalidade).
+ * @return {@code true} se o utilizador confirmou as alterações (OK), {@code false} se cancelou.
+ */
+ public static boolean showAdvancedConfig(Stage owner) {
+ Dialog dialog = new Dialog<>();
+ dialog.initOwner(owner);
+ dialog.initModality(Modality.APPLICATION_MODAL);
+ dialog.setTitle("Configuração Avançada da Simulação");
+ dialog.setHeaderText("Ajustar parâmetros da simulação");
+
+ // Criar painel de configuração
+ VBox content = new VBox(15);
+ content.setPadding(new Insets(20));
+
+ // Seção 1: Parâmetros de Chegada
+ Label arrivalHeader = new Label("Parâmetros de Chegada de Veículos");
+ arrivalHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;");
+
+ GridPane arrivalGrid = new GridPane();
+ arrivalGrid.setHgap(10);
+ arrivalGrid.setVgap(10);
+ arrivalGrid.setPadding(new Insets(10));
+
+ // Modelo de chegada
+ Label modelLabel = new Label("Modelo de chegada:");
+ ComboBox modelCombo = new ComboBox<>();
+ modelCombo.getItems().addAll("POISSON", "FIXED");
+ modelCombo.setValue("POISSON");
+ arrivalGrid.add(modelLabel, 0, 0);
+ arrivalGrid.add(modelCombo, 1, 0);
+
+ // Taxa de chegada (λ)
+ Label rateLabel = new Label("Taxa de chegada (λ) [veículos/s]:");
+ Spinner rateSpinner = new Spinner<>(0.1, 2.0, 0.5, 0.1);
+ rateSpinner.setEditable(true);
+ rateSpinner.setPrefWidth(100);
+ arrivalGrid.add(rateLabel, 0, 1);
+ arrivalGrid.add(rateSpinner, 1, 1);
+
+ // Intervalo fixo (se aplicável)
+ Label intervalLabel = new Label("Intervalo fixo [s]:");
+ Spinner intervalSpinner = new Spinner<>(0.5, 10.0, 2.0, 0.5);
+ intervalSpinner.setEditable(true);
+ intervalSpinner.setPrefWidth(100);
+ intervalSpinner.setDisable(true);
+ arrivalGrid.add(intervalLabel, 0, 2);
+ arrivalGrid.add(intervalSpinner, 1, 2);
+
+ // Habilitar/desabilitar intervalo baseado no modelo
+ modelCombo.setOnAction(e -> {
+ boolean isFixed = "FIXED".equals(modelCombo.getValue());
+ intervalSpinner.setDisable(!isFixed);
+ rateSpinner.setDisable(isFixed);
+ });
+
+ // Seção 2: Parâmetros de Tempo
+ Label timeHeader = new Label("Parâmetros de Tempo");
+ timeHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;");
+
+ GridPane timeGrid = new GridPane();
+ timeGrid.setHgap(10);
+ timeGrid.setVgap(10);
+ timeGrid.setPadding(new Insets(10));
+
+ // Duração da simulação
+ Label durationLabel = new Label("Duração da simulação [s]:");
+ Spinner durationSpinner = new Spinner<>(60, 7200, 300, 60);
+ durationSpinner.setEditable(true);
+ durationSpinner.setPrefWidth(100);
+ timeGrid.add(durationLabel, 0, 0);
+ timeGrid.add(durationSpinner, 1, 0);
+
+ // Escala temporal (para visualização)
+ Label scaleLabel = new Label("Escala temporal (0=instantâneo, 1=tempo real):");
+ Spinner scaleSpinner = new Spinner<>(0.0, 1.0, 0.01, 0.01);
+ scaleSpinner.setEditable(true);
+ scaleSpinner.setPrefWidth(100);
+ timeGrid.add(scaleLabel, 0, 1);
+ timeGrid.add(scaleSpinner, 1, 1);
+
+ // Tempo de drenagem
+ Label drainLabel = new Label("Tempo de drenagem [s]:");
+ Spinner drainSpinner = new Spinner<>(0, 300, 60, 10);
+ drainSpinner.setEditable(true);
+ drainSpinner.setPrefWidth(100);
+ timeGrid.add(drainLabel, 0, 2);
+ timeGrid.add(drainSpinner, 1, 2);
+
+ // Seção 3: Distribuição de Tipos de Veículos
+ Label vehicleHeader = new Label("Distribuição de Tipos de Veículos");
+ vehicleHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;");
+
+ GridPane vehicleGrid = new GridPane();
+ vehicleGrid.setHgap(10);
+ vehicleGrid.setVgap(10);
+ vehicleGrid.setPadding(new Insets(10));
+
+ Label bikeLabel = new Label("Bicicletas/Motos [%]:");
+ Spinner bikeSpinner = new Spinner<>(0, 100, 10, 5);
+ bikeSpinner.setEditable(true);
+ bikeSpinner.setPrefWidth(100);
+ vehicleGrid.add(bikeLabel, 0, 0);
+ vehicleGrid.add(bikeSpinner, 1, 0);
+
+ Label lightLabel = new Label("Veículos Ligeiros [%]:");
+ Spinner lightSpinner = new Spinner<>(0, 100, 70, 5);
+ lightSpinner.setEditable(true);
+ lightSpinner.setPrefWidth(100);
+ vehicleGrid.add(lightLabel, 0, 1);
+ vehicleGrid.add(lightSpinner, 1, 1);
+
+ Label heavyLabel = new Label("Veículos Pesados [%]:");
+ Spinner heavySpinner = new Spinner<>(0, 100, 20, 5);
+ heavySpinner.setEditable(true);
+ heavySpinner.setPrefWidth(100);
+ vehicleGrid.add(heavyLabel, 0, 2);
+ vehicleGrid.add(heavySpinner, 1, 2);
+
+ // Nota informativa
+ Label noteLabel = new Label("Nota: Estes parâmetros sobrepõem os valores do ficheiro .properties selecionado.\n" +
+ "Para usar os valores padrão do ficheiro, deixe em branco ou cancele.");
+ noteLabel.setWrapText(true);
+ noteLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #666666;");
+
+ // Adicionar tudo ao conteúdo
+ content.getChildren().addAll(
+ arrivalHeader, arrivalGrid,
+ new Separator(),
+ timeHeader, timeGrid,
+ new Separator(),
+ vehicleHeader, vehicleGrid,
+ new Separator(),
+ noteLabel
+ );
+
+ dialog.getDialogPane().setContent(content);
+ dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
+
+ // Mostrar diálogo e processar resultado
+ return dialog.showAndWait()
+ .map(buttonType -> buttonType == ButtonType.OK)
+ .orElse(false);
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/DashboardClientHandler.java b/main/src/main/java/sd/dashboard/DashboardClientHandler.java
new file mode 100644
index 0000000..abdfc20
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/DashboardClientHandler.java
@@ -0,0 +1,180 @@
+package sd.dashboard;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.Map;
+
+import sd.model.MessageType;
+import sd.protocol.MessageProtocol;
+import sd.protocol.SocketConnection;
+
+/**
+ * Worker responsável pelo processamento dedicado de uma conexão de cliente TCP no Dashboard.
+ *
+ * Esta classe implementa o padrão Thread-per-Client. Cada instância executa numa
+ * thread separada, garantindo que a latência de rede ou o processamento de mensagens
+ * de um nó (Interseção/Coordenador) não bloqueie a receção de telemetria dos outros.
+ *
+ * As suas principais funções são:
+ *
+ * - Manter a conexão persistente com o nó remoto.
+ * - Desserializar mensagens de protocolo recebidas.
+ * - Normalizar payloads JSON (resolvendo ambiguidades de tipagem do Gson).
+ * - Atualizar o objeto partilhado {@link DashboardStatistics} de forma thread-safe.
+ *
+ */
+public class DashboardClientHandler implements Runnable {
+
+ private final Socket clientSocket;
+ private final DashboardStatistics statistics;
+
+ /**
+ * Inicializa o handler com o socket ativo e a referência para o agregador de estatísticas.
+ *
+ * @param clientSocket O socket TCP conectado ao nó remoto.
+ * @param statistics O objeto singleton partilhado onde as métricas serão agregadas.
+ */
+ public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) {
+ this.clientSocket = clientSocket;
+ this.statistics = statistics;
+ }
+
+ /**
+ * Ciclo de vida da conexão.
+ *
+ * Estabelece o wrapper {@link SocketConnection}, entra num loop de leitura bloqueante
+ * e gere exceções de I/O. Garante o fecho limpo do socket em caso de desconexão ou erro.
+ */
+ @Override
+ public void run() {
+ String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
+
+ try (SocketConnection connection = new SocketConnection(clientSocket)) {
+ System.out.println("[Handler] Started handling client: " + clientInfo);
+
+ while (!Thread.currentThread().isInterrupted()) {
+ try {
+ MessageProtocol message = connection.receiveMessage();
+
+ if (message == null) {
+ System.out.println("[Handler] Client disconnected: " + clientInfo);
+ break;
+ }
+
+ processMessage(message);
+
+ } catch (ClassNotFoundException e) {
+ System.err.println("[Handler] Unknown message class from " + clientInfo + ": " + e.getMessage());
+ } catch (IOException e) {
+ System.out.println("[Handler] Connection error with " + clientInfo + ": " + e.getMessage());
+ break;
+ }
+ }
+
+ } catch (IOException e) {
+ System.err.println("[Handler] Error initializing connection with " + clientInfo + ": " + e.getMessage());
+ } finally {
+ try {
+ if (!clientSocket.isClosed()) {
+ clientSocket.close();
+ }
+ } catch (IOException e) {
+ System.err.println("[Handler] Error closing socket for " + clientInfo + ": " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Valida e extrai os dados estatísticos da mensagem.
+ *
+ * Implementa uma lógica de correção de tipagem para payloads desserializados via Gson.
+ * Frequentemente, objetos genéricos são desserializados como {@code LinkedHashMap} em vez
+ * da classe alvo {@link StatsUpdatePayload}. Este método deteta essa situação e realiza
+ * uma conversão "round-trip" (Map -> JSON -> Object) para garantir a integridade dos dados.
+ *
+ * @param message A mensagem recebida da rede.
+ */
+ private void processMessage(MessageProtocol message) {
+ if (message.getType() != MessageType.STATS_UPDATE) {
+ System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType());
+ return;
+ }
+
+ String senderId = message.getSourceNode();
+ Object payload = message.getPayload();
+
+ System.out.println("[Handler] Received STATS_UPDATE from: " + senderId);
+
+ // 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
+ // This acts as a type-safety bridge for generic JSON payloads
+ 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);
+ }
+
+ /**
+ * Aplica os dados recebidos ao modelo global de estatísticas.
+ *
+ * Distingue entre atualizações incrementais (ex: contagem de veículos) e
+ * substituições de estado (ex: tempo total de sistema reportado pelo nó de saída).
+ *
+ * @param senderId Identificador do nó que enviou a atualização (ex: "Cr1", "ExitNode").
+ * @param stats O objeto DTO contendo as métricas normalizadas.
+ */
+ private void updateStatistics(String senderId, StatsUpdatePayload stats) {
+ if (stats.getTotalVehiclesGenerated() >= 0) {
+ statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated());
+ }
+
+ if (stats.getTotalVehiclesCompleted() >= 0) {
+ statistics.updateVehiclesCompleted(stats.getTotalVehiclesCompleted());
+ }
+
+ // Exit Node sends cumulative totals, so we SET rather than ADD
+ if (stats.getTotalSystemTime() >= 0) {
+ statistics.setTotalSystemTime(stats.getTotalSystemTime());
+ }
+
+ if (stats.getTotalWaitingTime() >= 0) {
+ 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,
+ stats.getIntersectionArrivals(),
+ stats.getIntersectionDepartures(),
+ stats.getIntersectionQueueSize()
+ );
+ }
+
+ System.out.println("[Handler] Successfully updated statistics from: " + senderId);
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/DashboardServer.java b/main/src/main/java/sd/dashboard/DashboardServer.java
new file mode 100644
index 0000000..6f76a3b
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/DashboardServer.java
@@ -0,0 +1,223 @@
+package sd.dashboard;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import sd.config.SimulationConfig;
+
+/**
+ * Servidor central de agregação de telemetria e estatísticas.
+ *
+ * Este componente atua como o nó de monitorização do sistema distribuído.
+ * Implementa uma arquitetura de servidor concorrente utilizando um {@link ExecutorService}
+ * (Thread Pool) para gerir múltiplas conexões de entrada simultâneas provenientes
+ * das Interseções, Coordenador e Nó de Saída.
+ *
+ * Suporta dois modos de operação:
+ *
+ * - Headless (CLI): Renderização periódica de métricas no terminal (stdout).
+ * - GUI (JavaFX): Delegação do controlo para a interface gráfica {@link DashboardUI}.
+ *
+ */
+public class DashboardServer {
+
+ private final int port;
+
+ /** Armazenamento em memória (Thread-safe) do estado global do sistema. */
+ private final DashboardStatistics statistics;
+
+ /** Pool de threads para isolamento de falhas e gestão de recursos de I/O. */
+ private final ExecutorService clientHandlerPool;
+
+ /** Flag atómica para controlo seguro do ciclo de vida do servidor. */
+ private final AtomicBoolean running;
+
+ private ServerSocket serverSocket;
+
+ /**
+ * Ponto de entrada (Bootstrap) da aplicação de monitorização.
+ *
+ * Analisa os argumentos de linha de comando para determinar o modo de execução.
+ * Se a flag {@code --gui} ou {@code -g} estiver presente, inicia o subsistema JavaFX.
+ * Caso contrário, inicia o modo servidor de terminal padrão.
+ *
+ * @param args Argumentos de CLI (ex: caminho do config, flags de modo).
+ */
+ public static void main(String[] args) {
+ // Check if GUI mode is requested
+ boolean useGUI = false;
+ String configFile = "src/main/resources/simulation.properties";
+
+ 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));
+
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Inicializa a infraestrutura do servidor.
+ *
+ * @param config A configuração carregada contendo a porta de escuta.
+ */
+ public DashboardServer(SimulationConfig config) {
+ this.port = config.getDashboardPort();
+ this.statistics = new DashboardStatistics();
+ // Fixed pool limita o consumo de recursos, prevenindo exaustão sob carga alta
+ this.clientHandlerPool = Executors.newFixedThreadPool(10);
+ this.running = new AtomicBoolean(false);
+ }
+
+ /**
+ * Inicia a escuta por conexões (Bind & Listen) e a thread de despacho.
+ *
+ * @throws IOException Se a porta já estiver em uso ou ocorrer erro de bind.
+ */
+ public void start() throws IOException {
+ if (running.get()) {
+ System.out.println("Dashboard Server is already running.");
+ return;
+ }
+
+ serverSocket = new ServerSocket(port);
+ running.set(true);
+
+ System.out.println("Dashboard Server started on port " + port);
+ System.out.println("Waiting for statistics updates from simulation processes...");
+ System.out.println("=".repeat(60));
+
+ Thread acceptThread = new Thread(this::acceptConnections, "DashboardServer-Accept");
+ acceptThread.setDaemon(false);
+ acceptThread.start();
+ }
+
+ /**
+ * Loop principal de aceitação de conexões (Dispatcher).
+ *
+ * Bloqueia em {@code accept()} até que uma nova conexão chegue, delegando
+ * imediatamente o processamento para um {@link DashboardClientHandler} gerido
+ * pelo Thread Pool.
+ */
+ private void acceptConnections() {
+ while (running.get()) {
+ try {
+ Socket clientSocket = serverSocket.accept();
+ System.out.println("[Connection] New client connected: " +
+ clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort());
+
+ clientHandlerPool.execute(new DashboardClientHandler(clientSocket, statistics));
+
+ } catch (IOException e) {
+ if (running.get()) {
+ System.err.println("[Error] Failed to accept client connection: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Ciclo de renderização de métricas para o modo CLI (Headless).
+ * Atualiza o ecrã a cada 5 segundos.
+ */
+ @SuppressWarnings("BusyWait")
+ private void displayLoop() {
+ final long DISPLAY_INTERVAL_MS = 5000;
+
+ while (running.get()) {
+ try {
+ Thread.sleep(DISPLAY_INTERVAL_MS);
+ displayStatistics();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Renderiza o snapshot atual das estatísticas no stdout.
+ */
+ public void displayStatistics() {
+ System.out.println("\n" + "=".repeat(60));
+ System.out.println("REAL-TIME SIMULATION STATISTICS");
+ System.out.println("=".repeat(60));
+ statistics.display();
+ System.out.println("=".repeat(60));
+ }
+
+ /**
+ * Procedimento de encerramento gracioso (Graceful Shutdown).
+ *
+ * 1. Altera flag de execução.
+ * 2. Fecha o socket do servidor para desbloquear a thread de aceitação.
+ * 3. Força o encerramento do pool de threads de clientes.
+ */
+ public void stop() {
+ if (!running.get()) {
+ return;
+ }
+
+ System.out.println("\nStopping Dashboard Server...");
+ running.set(false);
+
+ try {
+ if (serverSocket != null && !serverSocket.isClosed()) {
+ serverSocket.close();
+ }
+ } catch (IOException e) {
+ System.err.println("Error closing server socket: " + e.getMessage());
+ }
+
+ clientHandlerPool.shutdownNow();
+ System.out.println("Dashboard Server stopped.");
+ }
+
+ public DashboardStatistics getStatistics() {
+ return statistics;
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/DashboardStatistics.java b/main/src/main/java/sd/dashboard/DashboardStatistics.java
new file mode 100644
index 0000000..dd9f2f8
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/DashboardStatistics.java
@@ -0,0 +1,301 @@
+package sd.dashboard;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import sd.model.VehicleType;
+
+/**
+ * Repositório central de estado da simulação, desenhado para acesso concorrente de alta frequência.
+ *
+ * Esta classe atua como a "Single Source of Truth" para o Dashboard. Utiliza primitivas
+ * de concorrência do pacote {@code java.util.concurrent} (como {@link AtomicInteger} e
+ * {@link ConcurrentHashMap}) para permitir leituras e escritas simultâneas sem a necessidade
+ * de bloqueios explícitos (Lock-Free), minimizando a latência de processamento das mensagens
+ * recebidas dos múltiplos nós da rede.
+ */
+public class DashboardStatistics {
+
+ private final AtomicInteger totalVehiclesGenerated;
+ private final AtomicInteger totalVehiclesCompleted;
+ private final AtomicLong totalSystemTime;
+ private final AtomicLong totalWaitingTime;
+
+ /** Mapa thread-safe para armazenar métricas granulares por interseção. */
+ private final Map intersectionStats;
+
+ private final Map vehicleTypeCount;
+ private final Map vehicleTypeWaitTime;
+
+ /** Timestamp da última atualização de escrita, com garantia de visibilidade de memória (volatile). */
+ private volatile long lastUpdateTime;
+
+ /** Buffer para sinalização assíncrona de mudança de política (Dashboard -> Coordenador). */
+ private volatile String requestedRoutingPolicy;
+
+ /**
+ * Inicializa os contadores atómicos e as estruturas de dados concorrentes.
+ */
+ public DashboardStatistics() {
+ this.totalVehiclesGenerated = new AtomicInteger(0);
+ this.totalVehiclesCompleted = new AtomicInteger(0);
+ this.totalSystemTime = new AtomicLong(0);
+ this.totalWaitingTime = new AtomicLong(0);
+
+ this.intersectionStats = new ConcurrentHashMap<>();
+ this.vehicleTypeCount = new ConcurrentHashMap<>();
+ this.vehicleTypeWaitTime = new ConcurrentHashMap<>();
+
+ for (VehicleType type : VehicleType.values()) {
+ vehicleTypeCount.put(type, new AtomicInteger(0));
+ vehicleTypeWaitTime.put(type, new AtomicLong(0));
+ }
+
+ this.lastUpdateTime = System.currentTimeMillis();
+ }
+
+ public void updateVehiclesGenerated(int count) {
+ totalVehiclesGenerated.set(count);
+ updateTimestamp();
+ }
+
+ public void incrementVehiclesGenerated() {
+ totalVehiclesGenerated.incrementAndGet();
+ updateTimestamp();
+ }
+
+ public void updateVehiclesCompleted(int count) {
+ totalVehiclesCompleted.set(count);
+ updateTimestamp();
+ }
+
+ public void incrementVehiclesCompleted() {
+ totalVehiclesCompleted.incrementAndGet();
+ updateTimestamp();
+ }
+
+ public void addSystemTime(long timeMs) {
+ totalSystemTime.addAndGet(timeMs);
+ 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);
+ updateTimestamp();
+ }
+
+ public void incrementVehicleType(VehicleType type) {
+ vehicleTypeCount.get(type).incrementAndGet();
+ updateTimestamp();
+ }
+
+ /**
+ * Atualiza ou inicializa atomicamente as estatísticas de uma interseção específica.
+ *
+ * Utiliza {@link Map#compute} para garantir que a criação do objeto {@link IntersectionStats}
+ * seja thread-safe sem necessidade de blocos synchronized externos.
+ *
+ * @param intersectionId ID da interseção.
+ * @param arrivals Total acumulado de chegadas.
+ * @param departures Total acumulado de partidas.
+ * @param currentQueueSize Tamanho instantâneo da fila.
+ */
+ public void updateIntersectionStats(String intersectionId, int arrivals,
+ int departures, int currentQueueSize) {
+ intersectionStats.compute(intersectionId, (id, stats) -> {
+ if (stats == null) {
+ stats = new IntersectionStats(intersectionId);
+ }
+ stats.updateStats(arrivals, departures, currentQueueSize);
+ return stats;
+ });
+ updateTimestamp();
+ }
+
+ private void updateTimestamp() {
+ lastUpdateTime = System.currentTimeMillis();
+ }
+
+ // --- Getters e Métricas Calculadas ---
+
+ public int getTotalVehiclesGenerated() {
+ return totalVehiclesGenerated.get();
+ }
+
+ public int getTotalVehiclesCompleted() {
+ return totalVehiclesCompleted.get();
+ }
+
+ /**
+ * Calcula o tempo médio no sistema em tempo real.
+ * @return Média em milissegundos (0.0 se nenhum veículo completou).
+ */
+ public double getAverageSystemTime() {
+ int completed = totalVehiclesCompleted.get();
+ if (completed == 0) return 0.0;
+ return (double) totalSystemTime.get() / completed;
+ }
+
+ /**
+ * Calcula o tempo médio de espera em tempo real.
+ * @return Média em milissegundos (0.0 se nenhum veículo completou).
+ */
+ public double getAverageWaitingTime() {
+ int completed = totalVehiclesCompleted.get();
+ if (completed == 0) return 0.0;
+ return (double) totalWaitingTime.get() / completed;
+ }
+
+ public int getVehicleTypeCount(VehicleType type) {
+ return vehicleTypeCount.get(type).get();
+ }
+
+ public double getAverageWaitingTimeByType(VehicleType type) {
+ int count = vehicleTypeCount.get(type).get();
+ if (count == 0) return 0.0;
+ return (double) vehicleTypeWaitTime.get(type).get() / count;
+ }
+
+ public IntersectionStats getIntersectionStats(String intersectionId) {
+ return intersectionStats.get(intersectionId);
+ }
+
+ public Map getAllIntersectionStats() {
+ return new HashMap<>(intersectionStats);
+ }
+
+ public long getLastUpdateTime() {
+ return lastUpdateTime;
+ }
+
+ /**
+ * Obtém um snapshot dos tamanhos atuais das filas de todas as interseções.
+ *
+ * Utilizado primariamente pelo algoritmo de roteamento dinâmico (LEAST_CONGESTED)
+ * para tomar decisões de encaminhamento baseadas na carga atual da rede.
+ * * @return Mapa contendo {@code intersectionId -> queueSize}.
+ */
+ public Map getCurrentQueueSizes() {
+ Map queueSizes = new HashMap<>();
+ for (Map.Entry entry : intersectionStats.entrySet()) {
+ queueSizes.put(entry.getKey(), entry.getValue().getCurrentQueueSize());
+ }
+ return queueSizes;
+ }
+
+ /**
+ * Regista uma intenção de mudança de política de roteamento solicitada pela UI.
+ * O Coordenador fará polling deste valor periodicamente.
+ */
+ public void setRequestedRoutingPolicy(String policy) {
+ this.requestedRoutingPolicy = policy;
+ }
+
+ /**
+ * Obtém e limpa atomicamente a política de roteamento solicitada.
+ * Implementa a semântica de consumo único (one-time consumption).
+ * * @return A política solicitada ou null se não houver mudança pendente.
+ */
+ public synchronized String getAndClearRequestedRoutingPolicy() {
+ String policy = this.requestedRoutingPolicy;
+ this.requestedRoutingPolicy = null;
+ return policy;
+ }
+
+ /**
+ * Imprime um resumo formatado das estatísticas no stdout.
+ * Útil para o modo CLI (Headless).
+ */
+ public void display() {
+ System.out.println("\n--- GLOBAL STATISTICS ---");
+ System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated());
+ System.out.printf("Total Vehicles Completed: %d%n", getTotalVehiclesCompleted());
+ System.out.printf("Vehicles In Transit: %d%n",
+ getTotalVehiclesGenerated() - getTotalVehiclesCompleted());
+ System.out.printf("Average System Time: %.2f ms%n", getAverageSystemTime());
+ System.out.printf("Average Waiting Time: %.2f ms%n", getAverageWaitingTime());
+
+ System.out.println("\n--- VEHICLE TYPE STATISTICS ---");
+ for (VehicleType type : VehicleType.values()) {
+ int count = getVehicleTypeCount(type);
+ double avgWait = getAverageWaitingTimeByType(type);
+ System.out.printf("%s: %d vehicles, avg wait: %.2f ms%n",
+ type, count, avgWait);
+ }
+
+ System.out.println("\n--- INTERSECTION STATISTICS ---");
+ if (intersectionStats.isEmpty()) {
+ System.out.println("(No data received yet)");
+ } else {
+ for (IntersectionStats stats : intersectionStats.values()) {
+ stats.display();
+ }
+ }
+
+ System.out.printf("%nLast Update: %tT%n", lastUpdateTime);
+ }
+
+ /**
+ * Agregado de métricas específico para um nó de interseção.
+ * Mantém contadores atómicos para garantir consistência em atualizações concorrentes.
+ */
+ public static class IntersectionStats {
+ private final String intersectionId;
+ private final AtomicInteger totalArrivals;
+ private final AtomicInteger totalDepartures;
+ private final AtomicInteger currentQueueSize;
+
+ public IntersectionStats(String intersectionId) {
+ this.intersectionId = intersectionId;
+ this.totalArrivals = new AtomicInteger(0);
+ this.totalDepartures = new AtomicInteger(0);
+ this.currentQueueSize = new AtomicInteger(0);
+ }
+
+ public void updateStats(int arrivals, int departures, int queueSize) {
+ this.totalArrivals.set(arrivals);
+ this.totalDepartures.set(departures);
+ this.currentQueueSize.set(queueSize);
+ }
+
+ public String getIntersectionId() {
+ return intersectionId;
+ }
+
+ public int getTotalArrivals() {
+ return totalArrivals.get();
+ }
+
+ public int getTotalDepartures() {
+ return totalDepartures.get();
+ }
+
+ public int getCurrentQueueSize() {
+ return currentQueueSize.get();
+ }
+
+ public void display() {
+ System.out.printf("%s: Arrivals=%d, Departures=%d, Queue=%d%n",
+ intersectionId, getTotalArrivals(), getTotalDepartures(), getCurrentQueueSize());
+ }
+ }
+}
\ No newline at end of file
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..8757784
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/DashboardUI.java
@@ -0,0 +1,619 @@
+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.Button;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+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.shape.Circle;
+import javafx.stage.Stage;
+import sd.config.SimulationConfig;
+import sd.model.VehicleType;
+
+/**
+ * Interface Gráfica (GUI) baseada em JavaFX para visualização de telemetria em tempo real.
+ *
+ * Esta classe atua como a camada de apresentação (View) do sistema. Implementa o padrão
+ * Observer (via polling) para refletir o estado do modelo {@link DashboardStatistics}
+ * nos componentes visuais.
+ *
+ * Aspetos Técnicos Relevantes:
+ *
+ * - Concorrência de UI: Utiliza um {@link ScheduledExecutorService} para buscar dados
+ * em background e {@link Platform#runLater(Runnable)} para injetar atualizações na
+ * JavaFX Application Thread, evitando exceções de "Not on FX application thread".
+ * - Data Binding: Utiliza {@link TableView} com classes internas (DTOs) para
+ * renderização tabular eficiente de tipos de veículos e interseções.
+ * - Controlo de Processos: Integra com {@link SimulationProcessManager} para orquestrar
+ * o ciclo de vida (spawn/kill) dos processos externos da simulação.
+ *
+ */
+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 (Background Thread)
+ private ScheduledExecutorService updateScheduler;
+
+ // Configuration controls
+ private ComboBox configFileSelector;
+ private String selectedConfigFile = "simulation.properties";
+ private Label configInfoLabel;
+
+ /**
+ * Ponto de entrada da aplicação JavaFX.
+ * Configura o Stage primário, inicializa o servidor de backend e constrói a árvore de cena (Scene Graph).
+ */
+ @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 (Backend listening port)
+ server.start();
+
+ // Build UI Layout
+ BorderPane root = new BorderPane();
+ root.getStyleClass().add("root");
+
+ // Header (Top)
+ VBox header = createHeader();
+ root.setTop(header);
+
+ // Main content (Center)
+ VBox mainContent = createMainContent();
+ root.setCenter(mainContent);
+
+ // Footer (Bottom)
+ HBox footer = createFooter();
+ root.setBottom(footer);
+
+ // Create scene & apply CSS
+ Scene scene = new Scene(root, 1200, 850);
+ String cssUrl = getClass().getResource("/dashboard.css").toExternalForm();
+ scene.getStylesheets().add(cssUrl);
+
+ primaryStage.setTitle("Traffic Simulation Dashboard - Real-time Statistics");
+ primaryStage.setScene(scene);
+ primaryStage.show();
+
+ // Start periodic updates loop
+ startPeriodicUpdates();
+
+ // Handle window close (Graceful shutdown)
+ primaryStage.setOnCloseRequest(event -> {
+ shutdown();
+ });
+
+ } catch (Exception e) {
+ showErrorAlert("Failed to start Dashboard Server", e.getMessage());
+ e.printStackTrace();
+ Platform.exit();
+ }
+ }
+
+ private VBox createHeader() {
+ VBox header = new VBox(10);
+ header.getStyleClass().add("header");
+ header.setAlignment(Pos.CENTER);
+
+ Label title = new Label("DISTRIBUTED TRAFFIC SIMULATION DASHBOARD");
+ title.getStyleClass().add("header-title");
+
+ Label subtitle = new Label("Real-time Statistics and Monitoring");
+ subtitle.getStyleClass().add("header-subtitle");
+
+ // Configuration Panel
+ VBox configPanel = createConfigurationPanel();
+
+ // Control Buttons
+ HBox controls = new HBox(15);
+ controls.setAlignment(Pos.CENTER);
+
+ Button btnStart = new Button("START SIMULATION");
+ btnStart.getStyleClass().add("button-start");
+
+ Button btnStop = new Button("STOP SIMULATION");
+ btnStop.getStyleClass().add("button-stop");
+ btnStop.setDisable(true);
+
+ SimulationProcessManager processManager = new SimulationProcessManager();
+
+ btnStart.setOnAction(e -> {
+ try {
+ // Passar o ficheiro de configuração selecionado
+ processManager.setConfigFile(selectedConfigFile);
+ processManager.startSimulation();
+
+ // Toggle UI state
+ btnStart.setDisable(true);
+ btnStop.setDisable(false);
+ configFileSelector.setDisable(true); // Bloquear mudanças durante simulação
+ } catch (IOException ex) {
+ showErrorAlert("Start Failed", "Could not start simulation processes: " + ex.getMessage());
+ }
+ });
+
+ btnStop.setOnAction(e -> {
+ processManager.stopSimulation();
+
+ // Toggle UI state
+ btnStart.setDisable(false);
+ btnStop.setDisable(true);
+ configFileSelector.setDisable(false); // Desbloquear para nova simulação
+ });
+
+ controls.getChildren().addAll(btnStart, btnStop);
+
+ header.getChildren().addAll(title, subtitle, configPanel, controls);
+
+ return header;
+ }
+
+ /**
+ * Cria o painel de configuração com seleção de cenário e parâmetros.
+ */
+ private VBox createConfigurationPanel() {
+ VBox configBox = new VBox(10);
+ configBox.setAlignment(Pos.CENTER);
+ configBox.setPadding(new Insets(10));
+ configBox.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;");
+
+ Label configLabel = new Label("Configuração da Simulação");
+ configLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
+
+ HBox configControls = new HBox(20);
+ configControls.setAlignment(Pos.CENTER);
+
+ // Scenario selector
+ VBox scenarioBox = new VBox(5);
+ scenarioBox.setAlignment(Pos.CENTER_LEFT);
+ Label scenarioLabel = new Label("Cenário:");
+ scenarioLabel.setStyle("-fx-font-size: 12px;");
+
+ configFileSelector = new ComboBox<>();
+ configFileSelector.getItems().addAll(
+ "simulation.properties",
+ "simulation-low.properties",
+ "simulation-medium.properties",
+ "simulation-high.properties"
+ );
+ configFileSelector.setValue("simulation.properties");
+ configFileSelector.setOnAction(e -> {
+ selectedConfigFile = configFileSelector.getValue();
+ updateConfigInfo();
+ System.out.println("Configuração selecionada: " + selectedConfigFile);
+ });
+
+ scenarioBox.getChildren().addAll(scenarioLabel, configFileSelector);
+ configControls.getChildren().add(scenarioBox);
+
+ // Routing policy selector
+ VBox routingBox = new VBox(5);
+ routingBox.setAlignment(Pos.CENTER_LEFT);
+ Label routingLabel = new Label("Política de Roteamento:");
+ routingLabel.setStyle("-fx-font-size: 12px;");
+
+ ComboBox routingPolicySelector = new ComboBox<>();
+ routingPolicySelector.getItems().addAll(
+ "RANDOM",
+ "SHORTEST_PATH",
+ "LEAST_CONGESTED"
+ );
+ routingPolicySelector.setValue("RANDOM");
+ routingPolicySelector.setOnAction(e -> {
+ String selectedPolicy = routingPolicySelector.getValue();
+ System.out.println("Política de roteamento selecionada: " + selectedPolicy);
+ sendRoutingPolicyChange(selectedPolicy);
+ });
+
+ routingBox.getChildren().addAll(routingLabel, routingPolicySelector);
+ configControls.getChildren().add(routingBox);
+
+ // Advanced configuration button
+ VBox buttonBox = new VBox(5);
+ buttonBox.setAlignment(Pos.CENTER_LEFT);
+ Label spacerLabel = new Label(" ");
+ spacerLabel.setStyle("-fx-font-size: 12px;");
+
+ Button btnAdvancedConfig = new Button("Configuração Avançada...");
+ btnAdvancedConfig.setStyle("-fx-font-size: 11px;");
+ btnAdvancedConfig.setOnAction(e -> {
+ ConfigurationDialog.showAdvancedConfig((Stage) configBox.getScene().getWindow());
+ });
+
+ Button btnBatchAnalysis = new Button("Análise em Lote...");
+ btnBatchAnalysis.setStyle("-fx-font-size: 11px;");
+ btnBatchAnalysis.setOnAction(e -> {
+ BatchAnalysisDialog.show((Stage) configBox.getScene().getWindow(), statistics);
+ });
+
+ buttonBox.getChildren().addAll(spacerLabel, btnAdvancedConfig, btnBatchAnalysis);
+ configControls.getChildren().add(buttonBox);
+
+ // Configuration info display
+ configInfoLabel = new Label();
+ configInfoLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #aaaaaa;");
+ configInfoLabel.setWrapText(true);
+ configInfoLabel.setMaxWidth(800);
+ configInfoLabel.setAlignment(Pos.CENTER);
+ updateConfigInfo();
+
+ configBox.getChildren().addAll(configLabel, configControls, configInfoLabel);
+ return configBox;
+ }
+
+ private VBox createMainContent() {
+ VBox mainContent = new VBox(20);
+ mainContent.setPadding(new Insets(20));
+
+ // Global Statistics Panel
+ VBox globalStatsCard = createGlobalStatisticsPanel();
+
+ // Tables Container
+ HBox tablesContainer = new HBox(20);
+ tablesContainer.setAlignment(Pos.TOP_CENTER);
+
+ // Vehicle Type Statistics Panel
+ VBox vehicleTypeCard = createVehicleTypePanel();
+ HBox.setHgrow(vehicleTypeCard, Priority.ALWAYS);
+
+ // Intersection Statistics Panel
+ VBox intersectionCard = createIntersectionPanel();
+ HBox.setHgrow(intersectionCard, Priority.ALWAYS);
+
+ tablesContainer.getChildren().addAll(vehicleTypeCard, intersectionCard);
+
+ mainContent.getChildren().addAll(globalStatsCard, tablesContainer);
+
+ return mainContent;
+ }
+
+ private VBox createGlobalStatisticsPanel() {
+ VBox card = new VBox();
+ card.getStyleClass().add("card");
+
+ // Card Header
+ HBox cardHeader = new HBox();
+ cardHeader.getStyleClass().add("card-header");
+ Label cardTitle = new Label("Global Statistics");
+ cardTitle.getStyleClass().add("card-title");
+ cardHeader.getChildren().add(cardTitle);
+
+ // Card Content
+ GridPane grid = new GridPane();
+ grid.getStyleClass().add("card-content");
+ grid.setHgap(40);
+ grid.setVgap(15);
+ grid.setAlignment(Pos.CENTER);
+
+ // Initialize labels
+ lblVehiclesGenerated = createStatValueLabel("0");
+ lblVehiclesCompleted = createStatValueLabel("0");
+ lblVehiclesInTransit = createStatValueLabel("0");
+ lblAvgSystemTime = createStatValueLabel("0.00 s");
+ lblAvgWaitingTime = createStatValueLabel("0.00 s");
+
+ // Add labels with descriptions
+ addStatRow(grid, 0, 0, "Total Vehicles Generated", lblVehiclesGenerated);
+ addStatRow(grid, 1, 0, "Total Vehicles Completed", lblVehiclesCompleted);
+ addStatRow(grid, 2, 0, "Vehicles In Transit", lblVehiclesInTransit);
+ addStatRow(grid, 0, 1, "Average System Time", lblAvgSystemTime);
+ addStatRow(grid, 1, 1, "Average Waiting Time", lblAvgWaitingTime);
+
+ card.getChildren().addAll(cardHeader, grid);
+ return card;
+ }
+
+ private VBox createVehicleTypePanel() {
+ VBox card = new VBox();
+ card.getStyleClass().add("card");
+
+ // Card Header
+ HBox cardHeader = new HBox();
+ cardHeader.getStyleClass().add("card-header");
+ Label cardTitle = new Label("Vehicle Type Statistics");
+ cardTitle.getStyleClass().add("card-title");
+ cardHeader.getChildren().add(cardTitle);
+
+ // Table
+ vehicleTypeTable = new TableView<>();
+ vehicleTypeTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+ vehicleTypeTable.setPrefHeight(300);
+
+ TableColumn typeCol = new TableColumn<>("Vehicle Type");
+ typeCol.setCellValueFactory(new PropertyValueFactory<>("vehicleType"));
+
+ TableColumn countCol = new TableColumn<>("Count");
+ countCol.setCellValueFactory(new PropertyValueFactory<>("count"));
+
+ TableColumn avgWaitCol = new TableColumn<>("Avg Wait Time");
+ avgWaitCol.setCellValueFactory(new PropertyValueFactory<>("avgWaitTime"));
+
+ vehicleTypeTable.getColumns().addAll(typeCol, countCol, avgWaitCol);
+
+ card.getChildren().addAll(cardHeader, vehicleTypeTable);
+ return card;
+ }
+
+ private VBox createIntersectionPanel() {
+ VBox card = new VBox();
+ card.getStyleClass().add("card");
+
+ // Card Header
+ HBox cardHeader = new HBox();
+ cardHeader.getStyleClass().add("card-header");
+ Label cardTitle = new Label("Intersection Statistics");
+ cardTitle.getStyleClass().add("card-title");
+ cardHeader.getChildren().add(cardTitle);
+
+ // Table
+ intersectionTable = new TableView<>();
+ intersectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+ intersectionTable.setPrefHeight(300);
+
+ TableColumn idCol = new TableColumn<>("Intersection ID");
+ idCol.setCellValueFactory(new PropertyValueFactory<>("intersectionId"));
+
+ TableColumn arrivalsCol = new TableColumn<>("Total Arrivals");
+ arrivalsCol.setCellValueFactory(new PropertyValueFactory<>("arrivals"));
+
+ TableColumn departuresCol = new TableColumn<>("Total Departures");
+ departuresCol.setCellValueFactory(new PropertyValueFactory<>("departures"));
+
+ TableColumn queueCol = new TableColumn<>("Current Queue");
+ queueCol.setCellValueFactory(new PropertyValueFactory<>("queueSize"));
+
+ intersectionTable.getColumns().addAll(idCol, arrivalsCol, departuresCol, queueCol);
+
+ card.getChildren().addAll(cardHeader, intersectionTable);
+ return card;
+ }
+
+ private HBox createFooter() {
+ HBox footer = new HBox(10);
+ footer.getStyleClass().add("footer");
+ footer.setAlignment(Pos.CENTER_LEFT);
+
+ Label statusLabel = new Label("Status:");
+ statusLabel.getStyleClass().add("footer-text");
+ statusLabel.setStyle("-fx-font-weight: bold;");
+
+ Circle statusIndicator = new Circle(6);
+ statusIndicator.setFill(javafx.scene.paint.Color.LIME);
+
+ Label statusText = new Label("Connected and Receiving Data");
+ statusText.getStyleClass().add("footer-text");
+
+ lblLastUpdate = new Label("Last Update: --:--:--");
+ lblLastUpdate.getStyleClass().add("footer-text");
+
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ footer.getChildren().addAll(statusLabel, statusIndicator, statusText, spacer, lblLastUpdate);
+
+ return footer;
+ }
+
+ private Label createStatValueLabel(String initialValue) {
+ Label label = new Label(initialValue);
+ label.getStyleClass().add("stat-value");
+ return label;
+ }
+
+ private void addStatRow(GridPane grid, int row, int colGroup, String description, Label valueLabel) {
+ VBox container = new VBox(5);
+ container.setAlignment(Pos.CENTER_LEFT);
+
+ Label descLabel = new Label(description);
+ descLabel.getStyleClass().add("stat-label");
+
+ container.getChildren().addAll(descLabel, valueLabel);
+
+ grid.add(container, colGroup, row);
+ }
+
+ /**
+ * Inicia o ciclo de polling em background.
+ * Atualiza a UI a cada 100ms.
+ */
+ private void startPeriodicUpdates() {
+ updateScheduler = Executors.newSingleThreadScheduledExecutor();
+ updateScheduler.scheduleAtFixedRate(() -> {
+ // Crucial: Encapsular atualização de UI em Platform.runLater
+ // para garantir execução na JavaFX Application Thread
+ Platform.runLater(this::updateUI);
+ }, 0, 100, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Sincroniza o estado atual do objeto Statistics com os controlos JavaFX.
+ * Chamado periodicamente pela thread de UI.
+ */
+ 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 s", statistics.getAverageSystemTime() / 1000.0));
+ lblAvgWaitingTime.setText(String.format("%.2f s", statistics.getAverageWaitingTime() / 1000.0));
+ 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 s", avgWait / 1000.0)));
+ }
+
+ // 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()));
+ }
+ }
+
+ /**
+ * Atualiza a informação exibida sobre a configuração selecionada.
+ */
+ private void updateConfigInfo() {
+ String info = "";
+ switch (selectedConfigFile) {
+ case "simulation-low.properties":
+ info = "CARGA BAIXA: 0.2 veículos/s (~720/hora) | Sem congestionamento esperado";
+ break;
+ case "simulation-medium.properties":
+ info = "CARGA MÉDIA: 0.5 veículos/s (~1800/hora) | Algum congestionamento esperado";
+ break;
+ case "simulation-high.properties":
+ info = "CARGA ALTA: 1.0 veículo/s (~3600/hora) | Congestionamento significativo esperado";
+ break;
+ default:
+ info = "⚙️ CONFIGURAÇÃO PADRÃO: Verificar ficheiro para parâmetros";
+ break;
+ }
+ configInfoLabel.setText(info);
+ }
+
+ 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();
+ }
+
+ /**
+ * Envia mensagem para o servidor do dashboard que notificará o coordenador.
+ * Usa uma abordagem indireta: salva a política desejada e o coordenador lerá na próxima geração.
+ */
+ private void sendRoutingPolicyChange(String newPolicy) {
+ // Store the policy change request in statistics
+ // The coordinator will check this periodically
+ if (server != null && statistics != null) {
+ statistics.setRequestedRoutingPolicy(newPolicy);
+ System.out.println("Política de roteamento solicitada: " + newPolicy);
+ System.out.println(" - A mudança será aplicada pelo coordenador na próxima atualização");
+
+ // Mostrar confirmação visual
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Política Solicitada");
+ alert.setHeaderText(null);
+ alert.setContentText("Política de roteamento solicitada: " + newPolicy + "\nSerá aplicada em breve.");
+ alert.show();
+ });
+ } else {
+ Platform.runLater(() -> {
+ showErrorAlert("Erro", "Dashboard não está conectado. Inicie a simulação primeiro.");
+ });
+ }
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+
+ // --- DTOs para Data Binding nas Tabelas ---
+
+ /** DTO para linhas da tabela de Tipos de Veículo. */
+ 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; }
+ }
+
+ /** DTO para linhas da tabela de Interseções. */
+ 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; }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/Launcher.java b/main/src/main/java/sd/dashboard/Launcher.java
new file mode 100644
index 0000000..0a45c3d
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/Launcher.java
@@ -0,0 +1,7 @@
+package sd.dashboard;
+
+public class Launcher {
+ public static void main(String[] args) {
+ DashboardUI.main(args);
+ }
+}
diff --git a/main/src/main/java/sd/dashboard/SimulationProcessManager.java b/main/src/main/java/sd/dashboard/SimulationProcessManager.java
new file mode 100644
index 0000000..063a355
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/SimulationProcessManager.java
@@ -0,0 +1,184 @@
+package sd.dashboard;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Orquestrador de processos para o ambiente de simulação distribuída.
+ *
+ * Esta classe atua como um supervisor (Process Manager), responsável pelo bootstrapping
+ * e teardown das múltiplas Java Virtual Machines (JVMs) que compõem o sistema.
+ *
+ * Funcionalidades principais:
+ *
+ * - Isolamento: Cada nó (Interseção, Coordinator, ExitNode) corre no seu próprio processo OS.
+ * - Ordem de Arranque: Garante que os servidores (Interseções) estão online antes dos clientes (Coordenador).
+ * - Gestão de Logs: Redireciona stdout/stderr de cada processo filho para ficheiros temporários para facilitar o debug.
+ *
+ */
+public class SimulationProcessManager {
+
+ private final List runningProcesses;
+ private final String classpath;
+ private String configFile;
+
+ /**
+ * Inicializa o gestor capturando o classpath da JVM atual.
+ * Isto garante que os processos filhos herdam as mesmas dependências e configurações de ambiente.
+ */
+ public SimulationProcessManager() {
+ this.runningProcesses = new ArrayList<>();
+ this.classpath = System.getProperty("java.class.path");
+ this.configFile = "src/main/resources/simulation.properties";
+ }
+
+ /**
+ * Define o perfil de configuração a ser injetado nos processos filhos.
+ * Útil para alternar entre cenários (Low/Medium/High Load) dinamicamente.
+ * * @param configFile Nome do ficheiro de propriedades (ex: "simulation-low.properties").
+ */
+ public void setConfigFile(String configFile) {
+ this.configFile = "src/main/resources/" + configFile;
+ System.out.println("Configuration file set to: " + this.configFile);
+ }
+
+ /**
+ * Executa o procedimento de arranque (Bootstrap) da simulação distribuída.
+ *
+ * A ordem de inicialização é crítica para evitar Race Conditions na conexão TCP:
+ *
+ * - Workers (Interseções): Iniciam os ServerSockets.
+ * - Sink (Exit Node): Prepara-se para receber métricas finais.
+ * - Delay de Estabilização: Pausa de 1s para garantir que os sockets estão em LISTENING.
+ * - Source (Coordinator): Inicia a geração de carga e conecta-se aos nós.
+ *
+ * * @throws IOException Se falhar o fork de algum processo.
+ */
+ public void startSimulation() throws IOException {
+ if (!runningProcesses.isEmpty()) {
+ stopSimulation();
+ }
+
+ System.out.println("Starting simulation processes...");
+
+ // 1. Start Intersections (Cr1 - Cr5)
+ String[] intersectionIds = { "Cr1", "Cr2", "Cr3", "Cr4", "Cr5" };
+ for (String id : intersectionIds) {
+ startProcess("sd.IntersectionProcess", id);
+ }
+
+ // 2. Start Exit Node
+ startProcess("sd.ExitNodeProcess", null);
+
+ // 3. Start Coordinator (Wait a bit for others to initialize)
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ startProcess("sd.coordinator.CoordinatorProcess", null);
+
+ System.out.println("All simulation processes started.");
+ }
+
+ /**
+ * Verifica o estado de "liveness" da simulação monitorizando o processo Coordenador.
+ *
+ * Como o Coordenador gere o relógio DES e a geração de eventos, a sua terminação
+ * (após o drain time) sinaliza o fim efetivo da simulação.
+ * * @return true se o Coordenador ainda estiver ativo (alive).
+ */
+ public boolean isSimulationRunning() {
+ if (runningProcesses.isEmpty()) {
+ return false;
+ }
+ // Coordinator is the last process in the list
+ Process coordinator = runningProcesses.get(runningProcesses.size() - 1);
+ return coordinator.isAlive();
+ }
+
+ /**
+ * Bloqueia a thread atual até que a simulação termine naturalmente ou ocorra timeout.
+ * * @param timeoutSeconds Tempo máximo de espera.
+ * @return true se terminou, false se o timeout expirou.
+ * @throws InterruptedException Se a espera for interrompida.
+ */
+ public boolean waitForCompletion(long timeoutSeconds) throws InterruptedException {
+ if (runningProcesses.isEmpty()) {
+ return true;
+ }
+
+ Process coordinator = runningProcesses.get(runningProcesses.size() - 1);
+ return coordinator.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS);
+ }
+
+ /**
+ * Executa o procedimento de encerramento (Teardown) de todos os processos.
+ *
+ * Tenta primeiro uma paragem graciosa (`SIGTERM`), aguarda meio segundo, e
+ * força a paragem (`SIGKILL`) para processos persistentes, garantindo que não
+ * ficam processos órfãos no SO.
+ */
+ public void stopSimulation() {
+ System.out.println("Stopping simulation processes...");
+
+ for (Process process : runningProcesses) {
+ if (process.isAlive()) {
+ process.destroy(); // Try graceful termination first
+ }
+ }
+
+ // Wait a bit and force kill if necessary
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ for (Process process : runningProcesses) {
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ }
+ }
+
+ runningProcesses.clear();
+ System.out.println("All simulation processes stopped.");
+ }
+
+ /**
+ * Helper de baixo nível para construção e lançamento de processos Java.
+ * Configura o redirecionamento de I/O para ficheiros de log na diretoria temporária do SO.
+ */
+ private void startProcess(String className, String arg) throws IOException {
+ String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
+
+ ProcessBuilder builder;
+ if (arg != null) {
+ builder = new ProcessBuilder(javaBin, "-cp", classpath, className, arg, configFile);
+ } else {
+ builder = new ProcessBuilder(javaBin, "-cp", classpath, className, configFile);
+ }
+
+ // get the OS temp folder
+ // Linux: /tmp/
+ // Windows: %AppData%\Local\Temp\
+ String tempDir = System.getProperty("java.io.tmpdir");
+
+ String logName = className.substring(className.lastIndexOf('.') + 1) + (arg != null ? "-" + arg : "") + ".log";
+
+ // use the (File parent, String child) constructor to handle slash/backslash
+ // automatically
+ File logFile = new File(tempDir, logName);
+
+ builder.redirectOutput(logFile);
+ builder.redirectError(logFile);
+
+ Process process = builder.start();
+ runningProcesses.add(process);
+ System.out.println("Started " + className + (arg != null ? " " + arg : ""));
+ // print where the logs are actually going
+ System.out.println("Logs redirected to: " + logFile.getAbsolutePath());
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/StatsMessage.java b/main/src/main/java/sd/dashboard/StatsMessage.java
new file mode 100644
index 0000000..abc4730
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/StatsMessage.java
@@ -0,0 +1,77 @@
+package sd.dashboard;
+
+import sd.model.MessageType;
+import sd.protocol.MessageProtocol;
+
+/**
+ * Implementação concreta do protocolo de mensagens destinada ao transporte de telemetria.
+ *
+ * Esta classe atua como um envelope especializado para o envio de dados estatísticos
+ * (encapsulados em {@link StatsUpdatePayload}) dos nós operacionais (Interseções, Coordenador)
+ * para o servidor de Dashboard centralizado.
+ *
+ * Diferencia-se das mensagens de controlo genéricas por ter o destino fixado no
+ * "DashboardServer" e um tipo de mensagem imutável ({@code STATS_UPDATE}).
+ */
+public class StatsMessage implements MessageProtocol {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String sourceNode;
+ private final String destinationNode;
+ private final StatsUpdatePayload payload;
+
+ /**
+ * Cria uma nova mensagem de estatística.
+ *
+ * @param sourceNode O ID do nó que gerou as estatísticas (ex: "Cr1", "ExitNode").
+ * @param payload O objeto DTO contendo os dados estatísticos brutos ou agregados.
+ */
+ public StatsMessage(String sourceNode, StatsUpdatePayload payload) {
+ this.sourceNode = sourceNode;
+ this.destinationNode = "DashboardServer"; // Destino implícito e fixo
+ this.payload = payload;
+ }
+
+ /**
+ * Retorna o tipo da mensagem, que identifica semanticamente o conteúdo para o recetor.
+ * @return Sempre {@link MessageType#STATS_UPDATE}.
+ */
+ @Override
+ public MessageType getType() {
+ return MessageType.STATS_UPDATE;
+ }
+
+ /**
+ * Obtém a carga útil da mensagem.
+ * @return O objeto {@link StatsUpdatePayload} associado.
+ */
+ @Override
+ public Object getPayload() {
+ return payload;
+ }
+
+ /**
+ * Identifica a origem da mensagem.
+ * @return O ID do nó remetente.
+ */
+ @Override
+ public String getSourceNode() {
+ return sourceNode;
+ }
+
+ /**
+ * Identifica o destino da mensagem.
+ * @return Sempre "DashboardServer".
+ */
+ @Override
+ public String getDestinationNode() {
+ return destinationNode;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("StatsMessage[from=%s, to=%s, payload=%s]",
+ sourceNode, destinationNode, payload);
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/dashboard/StatsUpdatePayload.java b/main/src/main/java/sd/dashboard/StatsUpdatePayload.java
new file mode 100644
index 0000000..6f1915e
--- /dev/null
+++ b/main/src/main/java/sd/dashboard/StatsUpdatePayload.java
@@ -0,0 +1,158 @@
+package sd.dashboard;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import sd.model.VehicleType;
+
+/**
+ * Objeto de Transferência de Dados (DTO) otimizado para transporte de telemetria.
+ *
+ * Esta classe encapsula as métricas de desempenho enviadas pelos nós da simulação (Coordenador,
+ * Interseções, ExitNode) para o Dashboard. Foi desenhada para suportar atualizações parciais
+ * (Sparse Updates):
+ *
+ * - Campos globais inicializados com {@code -1} indicam "sem alteração" (no-op). O Dashboard
+ * deve ignorar estes campos e manter o valor acumulado anterior.
+ * - Campos de interseção ({@code arrivals}, {@code departures}) representam deltas ou snapshots
+ * específicos do nó remetente.
+ *
+ * Implementa {@link Serializable} para transmissão direta via Java Sockets.
+ *
+
+[Image of data transfer object pattern]
+
+ */
+public class StatsUpdatePayload implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ // Global Metrics (Coordinator/ExitNode)
+ /** Total gerado. Valor -1 indica que este campo deve ser ignorado na atualização. */
+ private int totalVehiclesGenerated = -1;
+
+ /** Total completado. Valor -1 indica que este campo deve ser ignorado. */
+ private int totalVehiclesCompleted = -1;
+
+ /** Tempo total de sistema acumulado (ms). Valor -1 indica que deve ser ignorado. */
+ private long totalSystemTime = -1;
+
+ /** Tempo total de espera acumulado (ms). Valor -1 indica que deve ser ignorado. */
+ private long totalWaitingTime = -1;
+
+ // Intersection Metrics (Worker Nodes)
+ /** Número de veículos que entraram na interseção desde o último reporte. */
+ private int intersectionArrivals = 0;
+
+ /** Número de veículos que saíram da interseção desde o último reporte. */
+ private int intersectionDepartures = 0;
+
+ /** Snapshot do tamanho atual da fila na interseção. */
+ private int intersectionQueueSize = 0;
+
+ // Detailed Breakdowns
+ /** Contagem acumulada por tipo de veículo. */
+ private Map vehicleTypeCounts;
+
+ /** Tempos de espera acumulados por tipo de veículo. */
+ private Map vehicleTypeWaitTimes;
+
+ /**
+ * Inicializa o payload com os mapas vazios e contadores globais a -1 (estado neutro).
+ */
+ public StatsUpdatePayload() {
+ this.vehicleTypeCounts = new HashMap<>();
+ this.vehicleTypeWaitTimes = new HashMap<>();
+ }
+
+ public int getTotalVehiclesGenerated() {
+ return totalVehiclesGenerated;
+ }
+
+ public int getTotalVehiclesCompleted() {
+ return totalVehiclesCompleted;
+ }
+
+ public long getTotalSystemTime() {
+ return totalSystemTime;
+ }
+
+ public long getTotalWaitingTime() {
+ return totalWaitingTime;
+ }
+
+ public int getIntersectionArrivals() {
+ return intersectionArrivals;
+ }
+
+ public int getIntersectionDepartures() {
+ return intersectionDepartures;
+ }
+
+ public int getIntersectionQueueSize() {
+ return intersectionQueueSize;
+ }
+
+ public Map getVehicleTypeCounts() {
+ return vehicleTypeCounts;
+ }
+
+ public Map getVehicleTypeWaitTimes() {
+ return vehicleTypeWaitTimes;
+ }
+
+ // Setters implementam Fluent Interface para construção encadeada
+
+ public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) {
+ this.totalVehiclesGenerated = totalVehiclesGenerated;
+ return this;
+ }
+
+ public StatsUpdatePayload setTotalVehiclesCompleted(int totalVehiclesCompleted) {
+ this.totalVehiclesCompleted = totalVehiclesCompleted;
+ return this;
+ }
+
+ public StatsUpdatePayload setTotalSystemTime(long totalSystemTime) {
+ this.totalSystemTime = totalSystemTime;
+ return this;
+ }
+
+ public StatsUpdatePayload setTotalWaitingTime(long totalWaitingTime) {
+ this.totalWaitingTime = totalWaitingTime;
+ return this;
+ }
+
+ public StatsUpdatePayload setIntersectionArrivals(int intersectionArrivals) {
+ this.intersectionArrivals = intersectionArrivals;
+ return this;
+ }
+
+ public StatsUpdatePayload setIntersectionDepartures(int intersectionDepartures) {
+ this.intersectionDepartures = intersectionDepartures;
+ return this;
+ }
+
+ public StatsUpdatePayload setIntersectionQueueSize(int intersectionQueueSize) {
+ this.intersectionQueueSize = intersectionQueueSize;
+ return this;
+ }
+
+ public StatsUpdatePayload setVehicleTypeCounts(Map vehicleTypeCounts) {
+ this.vehicleTypeCounts = vehicleTypeCounts;
+ return this;
+ }
+
+ public StatsUpdatePayload setVehicleTypeWaitTimes(Map vehicleTypeWaitTimes) {
+ this.vehicleTypeWaitTimes = vehicleTypeWaitTimes;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("StatsUpdatePayload[generated=%d, completed=%d, arrivals=%d, departures=%d, queueSize=%d]",
+ totalVehiclesGenerated, totalVehiclesCompleted, intersectionArrivals,
+ intersectionDepartures, intersectionQueueSize);
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/des/DESEventType.java b/main/src/main/java/sd/des/DESEventType.java
new file mode 100644
index 0000000..789d2e9
--- /dev/null
+++ b/main/src/main/java/sd/des/DESEventType.java
@@ -0,0 +1,39 @@
+package sd.des;
+
+/**
+ * Tipos de eventos discretos da simulação.
+ *
+ * Representa os eventos DES que avançam o estado da simulação,
+ * não categorias de logging (EventType está noutro package).
+ */
+public enum DESEventType {
+ /** Gerar novo veículo num ponto de entrada */
+ VEHICLE_GENERATION,
+
+ /** Veículo chega a uma interseção */
+ VEHICLE_ARRIVAL,
+
+ /** Veículo começa a atravessar o semáforo */
+ VEHICLE_CROSSING_START,
+
+ /** Veículo termina a travessia */
+ VEHICLE_CROSSING_END,
+
+ /** Veículo parte para o próximo destino */
+ VEHICLE_DEPARTURE,
+
+ /** Veículo sai do sistema no nó de saída */
+ VEHICLE_EXIT,
+
+ /** Semáforo muda de estado (VERMELHO para VERDE ou vice-versa) */
+ TRAFFIC_LIGHT_CHANGE,
+
+ /** Processar veículos que esperam num semáforo recém-verde */
+ PROCESS_GREEN_LIGHT,
+
+ /** Atualização periódica de estatísticas */
+ STATISTICS_UPDATE,
+
+ /** Terminação da simulação */
+ SIMULATION_END
+}
diff --git a/main/src/main/java/sd/des/EventQueue.java b/main/src/main/java/sd/des/EventQueue.java
new file mode 100644
index 0000000..c61fc8d
--- /dev/null
+++ b/main/src/main/java/sd/des/EventQueue.java
@@ -0,0 +1,137 @@
+package sd.des;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.PriorityQueue;
+
+/**
+ * Gere a Lista de Eventos Futuros (FEL) para Simulação de Eventos Discretos.
+ *
+ *
A FEL é uma fila de prioridade que mantém todos os eventos futuros agendados,
+ * ordenados por timestamp. Este é o coração do paradigma DES - a simulação avança
+ * processando eventos em ordem cronológica.
+ */
+public class EventQueue {
+ private final PriorityQueue queue;
+ private final List processedEvents; // For logging and analysis
+ private final boolean trackHistory;
+
+ public EventQueue() {
+ this(true);
+ }
+
+ public EventQueue(boolean trackHistory) {
+ this.queue = new PriorityQueue<>();
+ this.processedEvents = trackHistory ? new ArrayList<>() : null;
+ this.trackHistory = trackHistory;
+ }
+
+ /**
+ * Agenda um novo evento.
+ *
+ * @param event evento a agendar
+ */
+ public void schedule(SimulationEvent event) {
+ queue.offer(event);
+ }
+
+ /**
+ * Agenda um evento com um atraso relativo ao tempo atual.
+ *
+ * @param currentTime tempo atual da simulação
+ * @param delay atraso em segundos
+ * @param type tipo de evento
+ * @param payload dados do evento
+ * @param location localização do evento
+ */
+ public void scheduleIn(double currentTime, double delay, DESEventType type,
+ Object payload, String location) {
+ double eventTime = currentTime + delay;
+ schedule(new SimulationEvent(eventTime, type, payload, location));
+ }
+
+ /** Obtém o próximo evento sem o remover */
+ public SimulationEvent peek() {
+ return queue.peek();
+ }
+
+ /**
+ * Obtém e remove o próximo evento.
+ * Se o rastreamento de histórico estiver ativo, adiciona-o aos eventos processados.
+ */
+ public SimulationEvent poll() {
+ SimulationEvent event = queue.poll();
+ if (event != null && trackHistory) {
+ processedEvents.add(event);
+ }
+ return event;
+ }
+
+ /** Verifica se existem eventos pendentes */
+ public boolean isEmpty() {
+ return queue.isEmpty();
+ }
+
+ /** @return número de eventos pendentes */
+ public int size() {
+ return queue.size();
+ }
+
+ /** Limpa todos os eventos pendentes */
+ public void clear() {
+ queue.clear();
+ }
+
+ /**
+ * Obtém todos os eventos processados (se o rastreamento estiver ativo).
+ * Retorna uma cópia para evitar modificações.
+ */
+ public List getProcessedEvents() {
+ if (!trackHistory) {
+ throw new UnsupportedOperationException("History tracking is disabled");
+ }
+ return new ArrayList<>(processedEvents);
+ }
+
+ /** @return número de eventos processados */
+ public int getProcessedCount() {
+ return trackHistory ? processedEvents.size() : 0;
+ }
+
+ /**
+ * Exporta o histórico de eventos para uma string formatada.
+ * Útil para debugging e visualização da lista completa de eventos.
+ */
+ public String exportEventHistory() {
+ if (!trackHistory) {
+ return "Event history tracking is disabled";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("=".repeat(80)).append("\n");
+ sb.append("SIMULATION EVENT HISTORY\n");
+ sb.append("Total Events Processed: ").append(processedEvents.size()).append("\n");
+ sb.append("=".repeat(80)).append("\n");
+ sb.append(String.format("%-10s | %-25s | %-20s | %s\n",
+ "Time", "Event Type", "Location", "Details"));
+ sb.append("-".repeat(80)).append("\n");
+
+ for (SimulationEvent event : processedEvents) {
+ String details = event.getPayload() != null ?
+ event.getPayload().getClass().getSimpleName() : "null";
+ sb.append(String.format("%-10.3f | %-25s | %-20s | %s\n",
+ event.getTimestamp(),
+ event.getType(),
+ event.getLocation() != null ? event.getLocation() : "N/A",
+ details));
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("EventQueue[pending=%d, processed=%d]",
+ queue.size(), getProcessedCount());
+ }
+}
diff --git a/main/src/main/java/sd/des/SimulationClock.java b/main/src/main/java/sd/des/SimulationClock.java
new file mode 100644
index 0000000..c43cc6a
--- /dev/null
+++ b/main/src/main/java/sd/des/SimulationClock.java
@@ -0,0 +1,71 @@
+package sd.des;
+
+/**
+ * Gere o tempo de simulação para Simulação de Eventos Discretos.
+ *
+ *
+ * No DES, o tempo avança em saltos discretos de evento para evento,
+ * não de forma contínua como o tempo real.
+ *
+ *
+ *
+ * Esta classe garante que todos os processos no sistema distribuído
+ * mantêm uma visão sincronizada do tempo de simulação.
+ *
+ */
+public class SimulationClock {
+ private double currentTime;
+ private final double startTime;
+ private final long wallClockStart;
+
+ public SimulationClock() {
+ this(0.0);
+ }
+
+ public SimulationClock(double startTime) {
+ this.currentTime = startTime;
+ this.startTime = startTime;
+ this.wallClockStart = System.currentTimeMillis();
+ }
+
+ /**
+ * Avança o tempo de simulação para o timestamp dado.
+ * O tempo só pode avançar, nunca recuar.
+ *
+ * @param newTime novo tempo de simulação
+ * @throws IllegalArgumentException se newTime for anterior ao tempo atual
+ */
+ public void advanceTo(double newTime) {
+ if (newTime < currentTime) {
+ throw new IllegalArgumentException(
+ String.format("Cannot move time backwards: %.3f -> %.3f", currentTime, newTime));
+ }
+ this.currentTime = newTime;
+ }
+
+ /** @return tempo atual da simulação */
+ public double getCurrentTime() {
+ return currentTime;
+ }
+
+ /** @return tempo de simulação decorrido desde o início */
+ public double getElapsedTime() {
+ return currentTime - startTime;
+ }
+
+ /** @return tempo real decorrido em milissegundos */
+ public long getWallClockElapsed() {
+ return System.currentTimeMillis() - wallClockStart;
+ }
+
+ /** Reinicia o relógio para o tempo inicial */
+ public void reset() {
+ this.currentTime = startTime;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SimulationClock[time=%.3fs, elapsed=%.3fs]",
+ currentTime, getElapsedTime());
+ }
+}
diff --git a/main/src/main/java/sd/des/SimulationEvent.java b/main/src/main/java/sd/des/SimulationEvent.java
new file mode 100644
index 0000000..589bc5e
--- /dev/null
+++ b/main/src/main/java/sd/des/SimulationEvent.java
@@ -0,0 +1,130 @@
+package sd.des;
+
+import java.io.Serializable;
+
+/**
+ * Representa um evento atómico e imutável no contexto da Simulação de Eventos Discretos (DES).
+ *
+ * Esta classe é a unidade fundamental de processamento. Numa arquitetura DES, o estado do sistema
+ * não muda continuamente, mas sim em instantes discretos definidos por estes eventos.
+ *
+ * Características principais:
+ *
+ * - Ordenação Temporal: Implementa {@link Comparable} para ser armazenado numa Fila de
+ * Eventos Futuros (FEL - Future Event List), garantindo execução cronológica.
+ * - Distribuído: Implementa {@link Serializable} para permitir que eventos gerados num nó
+ * (ex: Coordenador) sejam transmitidos e executados noutro (ex: Interseção).
+ * - Polimórfico: Transporta um {@code payload} genérico, permitindo associar qualquer
+ * entidade (Veículo, Sinal, etc.) ao evento.
+ *
+ */
+public class SimulationEvent implements Comparable, Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /** O instante virtual exato em que o evento deve ser processado. */
+ private final double timestamp;
+
+ /** A categoria do evento (ex: VEHICLE_ARRIVAL, LIGHT_CHANGE). */
+ private final DESEventType type;
+
+ /** Dados contextuais associados (ex: o objeto Vehicle que chegou). */
+ private final Object payload;
+
+ /**
+ * O identificador do nó de destino onde o evento deve ser executado.
+ * (ex: "Cr1", "Coordinator", "ExitNode"). Se null, é um evento local.
+ */
+ private final String location;
+
+ /**
+ * Instancia um novo evento de simulação completo.
+ *
+ * @param timestamp Instante de execução (segundos virtuais).
+ * @param type Tipo enumerado do evento.
+ * @param payload Objeto de dados associado (pode ser null).
+ * @param location ID do processo alvo para execução distribuída.
+ */
+ public SimulationEvent(double timestamp, DESEventType type, Object payload, String location) {
+ this.timestamp = timestamp;
+ this.type = type;
+ this.payload = payload;
+ this.location = location;
+ }
+
+ /**
+ * Construtor de conveniência para eventos locais (dentro do mesmo processo).
+ * Define {@code location} como null.
+ *
+ * @param timestamp Instante de execução.
+ * @param type Tipo do evento.
+ * @param payload Objeto de dados associado.
+ */
+ public SimulationEvent(double timestamp, DESEventType type, Object payload) {
+ this(timestamp, type, payload, null);
+ }
+
+ public double getTimestamp() {
+ return timestamp;
+ }
+
+ public DESEventType getType() {
+ return type;
+ }
+
+ public Object getPayload() {
+ return payload;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ /**
+ * Define a ordem natural de processamento na Fila de Prioridade.
+ *
+ * Lógica de Ordenação:
+ *
+ * - Primária (Tempo): Eventos com menor timestamp ocorrem primeiro.
+ * - Secundária (Determinismo): Em caso de empate temporal (simultaneidade),
+ * ordena alfabeticamente pelo nome do tipo. Isto garante que execuções repetidas
+ * da simulação produzam exatamente o mesmo resultado (determinismo estrito).
+ *
+ *
+ * @param other O outro evento a comparar.
+ * @return Inteiro negativo, zero ou positivo conforme a ordem.
+ */
+ @Override
+ public int compareTo(SimulationEvent other) {
+ int timeComparison = Double.compare(this.timestamp, other.timestamp);
+ if (timeComparison != 0) {
+ return timeComparison;
+ }
+ // Tie-breaker: order by event type name to ensure reproducible runs
+ return this.type.name().compareTo(other.type.name());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Event[t=%.3f, type=%s, location=%s]",
+ timestamp, type, location);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof SimulationEvent)) return false;
+ SimulationEvent other = (SimulationEvent) obj;
+ return Double.compare(timestamp, other.timestamp) == 0 &&
+ type == other.type &&
+ (location == null ? other.location == null : location.equals(other.location));
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + Double.hashCode(timestamp);
+ result = 31 * result + type.hashCode();
+ result = 31 * result + (location != null ? location.hashCode() : 0);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/des/TrafficLightEvent.java b/main/src/main/java/sd/des/TrafficLightEvent.java
new file mode 100644
index 0000000..0b27107
--- /dev/null
+++ b/main/src/main/java/sd/des/TrafficLightEvent.java
@@ -0,0 +1,55 @@
+package sd.des;
+
+import sd.model.TrafficLight;
+
+/**
+ * Encapsula o contexto de dados para eventos de mudança de estado de semáforos.
+ *
+ * Este objeto atua como o payload transportado por um {@link SimulationEvent}
+ * quando o tipo de evento é relacionado com controlo de tráfego (ex: mudança Verde -> Amarelo).
+ * Permite que o motor DES identifique exatamente qual instância de {@link TrafficLight}
+ * deve ser atualizada numa determinada interseção e direção.
+ */
+public class TrafficLightEvent {
+ private final TrafficLight light;
+ private final String direction;
+ private final String intersectionId;
+
+ /**
+ * Cria um novo payload de evento de semáforo.
+ * @param light A instância do objeto semáforo a ser manipulado.
+ * @param direction A direção cardeal associada (ex: "North", "East").
+ * @param intersectionId O identificador da interseção onde o semáforo reside.
+ */
+ public TrafficLightEvent(TrafficLight light, String direction, String intersectionId) {
+ this.light = light;
+ this.direction = direction;
+ this.intersectionId = intersectionId;
+ }
+
+ /**
+ * @return A referência direta para o objeto de domínio do semáforo.
+ */
+ public TrafficLight getLight() {
+ return light;
+ }
+
+ /**
+ * @return A direção do fluxo controlado por este semáforo.
+ */
+ public String getDirection() {
+ return direction;
+ }
+
+ /**
+ * @return O ID da interseção pai.
+ */
+ public String getIntersectionId() {
+ return intersectionId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TrafficLightEvent[%s-%s]", intersectionId, direction);
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/engine/SimulationEngine.java b/main/src/main/java/sd/engine/SimulationEngine.java
deleted file mode 100644
index 484ae80..0000000
--- a/main/src/main/java/sd/engine/SimulationEngine.java
+++ /dev/null
@@ -1,628 +0,0 @@
-package sd.engine;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.PriorityQueue;
-
-import sd.config.SimulationConfig;
-import sd.model.Event;
-import sd.model.EventType;
-import sd.model.Intersection;
-import sd.model.TrafficLight;
-import sd.model.TrafficLightState;
-import sd.model.Vehicle;
-import sd.model.VehicleType;
-import sd.util.StatisticsCollector;
-import sd.util.VehicleGenerator;
-
-/**
- * Core simulation engine using discrete event simulation (DES).
- * * This class orchestrates the entire simulation. It maintains a
- * {@link PriorityQueue} of {@link Event} objects, representing all
- * scheduled future actions. The engine processes events in strict
- * chronological order (based on their timestamp).
- * * It manages the simulation's state, including:
- * - The current simulation time ({@code currentTime}).
- * - The collection of all {@link Intersection} objects.
- * - The {@link VehicleGenerator} for creating new vehicles.
- * - The {@link StatisticsCollector} for tracking metrics.
- */
-public class SimulationEngine {
-
- /**
- * Holds all simulation parameters loaded from the properties file.
- */
- private final SimulationConfig config;
-
- /**
- * The core of the discrete event simulation. Events are pulled from this
- * queue in order of their timestamp.
- */
- private final PriorityQueue eventQueue;
-
- /**
- * A map storing all intersections in the simulation, keyed by their ID (e.g., "Cr1").
- */
- private final Map intersections;
-
- /**
- * Responsible for creating new vehicles according to the configured arrival model.
- */
- private final VehicleGenerator vehicleGenerator;
-
- /**
- * Collects and calculates statistics throughout the simulation.
- */
- private final StatisticsCollector statisticsCollector;
-
- /**
- * The current time in the simulation (in virtual seconds).
- * This time advances based on the timestamp of the event being processed.
- */
- private double currentTime;
-
- /**
- * A simple counter to generate unique IDs for vehicles.
- */
- private int vehicleCounter;
-
- /**
- * Constructs a new SimulationEngine.
- *
- * @param config The {@link SimulationConfig} object containing all
- * simulation parameters.
- */
- public SimulationEngine(SimulationConfig config) {
- this.config = config;
- this.eventQueue = new PriorityQueue<>();
- this.intersections = new HashMap<>();
- this.vehicleGenerator = new VehicleGenerator(config);
- this.statisticsCollector = new StatisticsCollector(config);
- this.currentTime = 0.0;
- this.vehicleCounter = 0;
- }
-
- /**
- * Initializes the simulation. This involves:
- * 1. Creating all {@link Intersection} and {@link TrafficLight} objects.
- * 2. Configuring the routing logic between intersections.
- * 3. Scheduling the initial events (first traffic light changes,
- * first vehicle generation, and periodic statistics updates).
- */
- public void initialize() {
- System.out.println("Initializing simulation...");
-
- setupIntersections();
- setupRouting();
-
- // Schedule initial events to "bootstrap" the simulation
- scheduleTrafficLightEvents();
- scheduleNextVehicleGeneration(0.0);
- scheduleStatisticsUpdates();
-
- System.out.println("Simulation initialized with " + intersections.size() + " intersections");
- }
-
- /**
- * Creates all intersections defined in the configuration
- * and adds their corresponding traffic lights.
- */
- private void setupIntersections() {
- String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
- // Note: "North" is commented out, so it won't be created.
- String[] directions = {/*"North",*/ "South", "East", "West"};
-
- for (String id : intersectionIds) {
- Intersection intersection = new Intersection(id);
-
- // Add traffic lights for each configured direction
- for (String direction : directions) {
- double greenTime = config.getTrafficLightGreenTime(id, direction);
- double redTime = config.getTrafficLightRedTime(id, direction);
-
- TrafficLight light = new TrafficLight(
- id + "-" + direction,
- direction,
- greenTime,
- redTime
- );
-
- intersection.addTrafficLight(light);
- }
-
- intersections.put(id, intersection);
- }
- }
-
- /**
- * Configures how vehicles should be routed between intersections.
- * This hardcoded logic defines the "map" of the city.
- * * For example, `intersections.get("Cr1").configureRoute("Cr2", "East");` means
- * "at intersection Cr1, any vehicle whose *next* destination is Cr2
- * should be sent to the 'East' traffic light queue."
- */
- private void setupRouting() {
- // Cr1 routing
- intersections.get("Cr1").configureRoute("Cr2", "East");
- intersections.get("Cr1").configureRoute("Cr4", "South");
-
- // Cr2 routing
- intersections.get("Cr2").configureRoute("Cr1", "West");
- intersections.get("Cr2").configureRoute("Cr3", "East");
- intersections.get("Cr2").configureRoute("Cr5", "South");
-
- // Cr3 routing
- intersections.get("Cr3").configureRoute("Cr2", "West");
- intersections.get("Cr3").configureRoute("S", "South"); // "S" is the exit
-
- // Cr4 routing
- //intersections.get("Cr4").configureRoute("Cr1", "North");
- intersections.get("Cr4").configureRoute("Cr5", "East");
-
- // Cr5 routing
- //intersections.get("Cr5").configureRoute("Cr2", "North");
- //intersections.get("Cr5").configureRoute("Cr4", "West");
- intersections.get("Cr5").configureRoute("S", "East"); // "S" is the exit
- }
-
- /**
- * Schedules the initial {@link EventType#TRAFFIC_LIGHT_CHANGE} event
- * for every traffic light in the simulation.
- * A small random delay is added to "stagger" the lights, preventing
- * all of them from changing at the exact same time at t=0.
- */
- private void scheduleTrafficLightEvents() {
- for (Intersection intersection : intersections.values()) {
- for (TrafficLight light : intersection.getTrafficLights()) {
- // Start with lights in RED state, schedule first GREEN change
- // Stagger the start times slightly to avoid all lights changing at once
- double staggerDelay = Math.random() * 1.5;
- scheduleTrafficLightChange(light, intersection.getId(), staggerDelay);
- }
- }
- }
-
- /**
- * Creates and schedules a new {@link EventType#TRAFFIC_LIGHT_CHANGE} event.
- * The event is scheduled to occur at {@code currentTime + delay}.
- *
- * @param light The {@link TrafficLight} that will change state.
- * @param intersectionId The ID of the intersection where the light is located.
- * @param delay The time (in seconds) from {@code currentTime} when the change should occur.
- */
- private void scheduleTrafficLightChange(TrafficLight light, String intersectionId, double delay) {
- double changeTime = currentTime + delay;
- Event event = new Event(changeTime, EventType.TRAFFIC_LIGHT_CHANGE, light, intersectionId);
- eventQueue.offer(event);
- }
-
- /**
- * Schedules the next {@link EventType#VEHICLE_GENERATION} event.
- * The time of the next arrival is determined by the {@link VehicleGenerator}.
- *
- * @param baseTime The time from which to calculate the next arrival (usually {@code currentTime}).
- */
- private void scheduleNextVehicleGeneration(double baseTime) {
- // Get the absolute time for the next arrival.
- double nextArrivalTime = vehicleGenerator.getNextArrivalTime(baseTime);
-
- // Only schedule the event if it's within the simulation's total duration.
- if (nextArrivalTime < config.getSimulationDuration()) {
- Event event = new Event(nextArrivalTime, EventType.VEHICLE_GENERATION, null, null);
- eventQueue.offer(event);
- }
- }
-
- /**
- * Schedules all periodic {@link EventType#STATISTICS_UPDATE} events
- * for the entire duration of the simulation.
- */
- private void scheduleStatisticsUpdates() {
- double interval = config.getStatisticsUpdateInterval();
- double duration = config.getSimulationDuration();
-
- for (double time = interval; time < duration; time += interval) {
- Event event = new Event(time, EventType.STATISTICS_UPDATE, null, null);
- eventQueue.offer(event);
- }
- }
-
- /**
- * Runs the main simulation loop.
- * The loop continues as long as there are events in the queue and
- * the {@code currentTime} is less than the total simulation duration.
- * * In each iteration, it:
- * 1. Polls the next event from the {@link #eventQueue}.
- * 2. Advances {@link #currentTime} to the event's timestamp.
- * 3. Calls {@link #processEvent(Event)} to handle the event.
- * * After the loop, it prints the final statistics.
- */
- public void run() {
- System.out.println("Starting simulation...");
- double duration = config.getSimulationDuration();
-
- while (!eventQueue.isEmpty() && currentTime < duration) {
- // Get the next event in chronological order
- Event event = eventQueue.poll();
-
- // Advance simulation time to this event's time
- currentTime = event.getTimestamp();
-
- // Process the event
- processEvent(event);
- }
-
- System.out.println("\nSimulation completed at t=" + String.format("%.2f", currentTime) + "s");
- printFinalStatistics();
- }
-
- /**
- * Main event processing logic.
- * Delegates the event to the appropriate handler method based on its {@link EventType}.
- *
- * @param event The {@link Event} to be processed.
- */
- private void processEvent(Event event) {
- switch (event.getType()) {
- case VEHICLE_GENERATION -> handleVehicleGeneration();
-
- case VEHICLE_ARRIVAL -> handleVehicleArrival(event);
-
- case TRAFFIC_LIGHT_CHANGE -> handleTrafficLightChange(event);
-
- case CROSSING_START -> handleCrossingStart(event);
-
- case CROSSING_END -> handleCrossingEnd(event);
-
- case STATISTICS_UPDATE -> handleStatisticsUpdate();
-
- default -> System.err.println("Unknown event type: " + event.getType());
- }
- }
-
- /**
- * Handles {@link EventType#VEHICLE_GENERATION}.
- * 1. Creates a new {@link Vehicle} using the {@link #vehicleGenerator}.
- * 2. Records the generation event with the {@link #statisticsCollector}.
- * 3. Schedules a {@link EventType#VEHICLE_ARRIVAL} event for the vehicle
- * at its first destination intersection.
- * 4. Schedules the *next* {@link EventType#VEHICLE_GENERATION} event.
- * (Note: This line is commented out in the original, which might be a bug,
- * as it implies only one vehicle is ever generated. It should likely be active.)
- */
- private void handleVehicleGeneration() {
- Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime);
-
- System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
- currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
-
- // Register with statistics collector
- statisticsCollector.recordVehicleGeneration(vehicle, currentTime);
-
- // Schedule arrival at first intersection
- String firstIntersection = vehicle.getCurrentDestination();
- if (firstIntersection != null && !firstIntersection.equals("S")) {
- // Assume minimal travel time to first intersection (e.g., 1-3 seconds)
- double arrivalTime = currentTime + 1.0 + Math.random() * 2.0;
- Event arrivalEvent = new Event(arrivalTime, EventType.VEHICLE_ARRIVAL, vehicle, firstIntersection);
- eventQueue.offer(arrivalEvent);
- }
-
- // Schedule next vehicle generation
- // This was commented out in the original file.
- // For a continuous simulation, it should be enabled:
- scheduleNextVehicleGeneration(currentTime);
- }
-
- /**
- * Handles {@link EventType#VEHICLE_ARRIVAL} at an intersection.
- * 1. Records the arrival for statistics.
- * 2. Advances the vehicle's internal route planner to its *next* destination.
- * 3. If the next destination is the exit ("S") or null,
- * the vehicle exits the system via {@link #handleVehicleExit(Vehicle)}.
- * 4. Otherwise, the vehicle is placed in the correct queue at the
- * current intersection using {@link Intersection#receiveVehicle(Vehicle)}.
- * 5. Attempts to process the vehicle immediately if its light is green.
- *
- * @param event The arrival event, containing the {@link Vehicle} and intersection ID.
- */
- private void handleVehicleArrival(Event event) {
- Vehicle vehicle = (Vehicle) event.getData();
- String intersectionId = event.getLocation();
-
- Intersection intersection = intersections.get(intersectionId);
- if (intersection == null) {
- System.err.println("Unknown intersection: " + intersectionId);
- return;
- }
-
- System.out.printf("[t=%.2f] Vehicle %s arrived at %s%n",
- currentTime, vehicle.getId(), intersectionId);
-
- // Record arrival time (used to calculate waiting time later)
- statisticsCollector.recordVehicleArrival(vehicle, intersectionId, currentTime);
-
- // Advance the vehicle's route to the *next* stop
- // (it has now arrived at its *current* destination)
- boolean hasNext = vehicle.advanceRoute();
-
- if (!hasNext) {
- // This was the last stop
- handleVehicleExit(vehicle);
- return;
- }
-
- String nextDestination = vehicle.getCurrentDestination();
- if (nextDestination == null || "S".equals(nextDestination)) {
- // Next stop is the exit
- handleVehicleExit(vehicle);
- return;
- }
-
- // Add vehicle to the appropriate traffic light queue based on its next destination
- intersection.receiveVehicle(vehicle);
-
- // Try to process the vehicle immediately if its light is already green
- tryProcessVehicle(vehicle, intersection);
- }
-
- /**
- * Checks if a newly arrived vehicle (or a vehicle in a queue
- * that just turned green) can start crossing.
- *
- * @param vehicle The vehicle to process.
- * @param intersection The intersection where the vehicle is.
- */
- private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { //FIXME
- // Find the direction (and light) this vehicle is queued at
- // This logic is a bit flawed: it just finds the *first* non-empty queue
- // A better approach would be to get the light from the vehicle's route
- String direction = intersection.getTrafficLights().stream()
- .filter(tl -> tl.getQueueSize() > 0)
- .map(TrafficLight::getDirection)
- .findFirst()
- .orElse(null);
-
- if (direction != null) {
- TrafficLight light = intersection.getTrafficLight(direction);
- // If the light is green and it's the correct one...
- if (light != null && light.getState() == TrafficLightState.GREEN) {
- // ...remove the vehicle from the queue (if it's at the front)
- Vehicle v = light.removeVehicle();
- if (v != null) {
- // ...and schedule its crossing.
- scheduleCrossing(v, intersection);
- }
- }
- }
- }
-
- /**
- * Schedules the crossing for a vehicle that has just been dequeued
- * from a green light.
- * 1. Calculates and records the vehicle's waiting time.
- * 2. Schedules an immediate {@link EventType#CROSSING_START} event.
- *
- * @param vehicle The {@link Vehicle} that is crossing.
- * @param intersection The {@link Intersection} it is crossing.
- */
- private void scheduleCrossing(Vehicle vehicle, Intersection intersection) {
- // Calculate time spent waiting at the red light
- double waitTime = currentTime - statisticsCollector.getArrivalTime(vehicle);
- vehicle.addWaitingTime(waitTime);
-
- // Schedule crossing start event *now*
- Event crossingStart = new Event(currentTime, EventType.CROSSING_START, vehicle, intersection.getId());
- processEvent(crossingStart); // Process immediately
- }
-
- /**
- * Handles {@link EventType#CROSSING_START}.
- * 1. Determines the crossing time based on vehicle type.
- * 2. Schedules a {@link EventType#CROSSING_END} event to occur
- * at {@code currentTime + crossingTime}.
- *
- * @param event The crossing start event.
- */
- private void handleCrossingStart(Event event) {
- Vehicle vehicle = (Vehicle) event.getData();
- String intersectionId = event.getLocation();
-
- double crossingTime = getCrossingTime(vehicle.getType());
-
- System.out.printf("[t=%.2f] Vehicle %s started crossing at %s (duration=%.2fs)%n",
- currentTime, vehicle.getId(), intersectionId, crossingTime);
-
- // Schedule the *end* of the crossing
- double endTime = currentTime + crossingTime;
- Event crossingEnd = new Event(endTime, EventType.CROSSING_END, vehicle, intersectionId);
- eventQueue.offer(crossingEnd);
- }
-
- /**
- * Handles {@link EventType#CROSSING_END}.
- * 1. Updates intersection and vehicle statistics.
- * 2. Checks the vehicle's *next* destination.
- * 3. If the next destination is the exit ("S"), call {@link #handleVehicleExit(Vehicle)}.
- * 4. Otherwise, schedule a {@link EventType#VEHICLE_ARRIVAL} event at the
- * *next* intersection, after some travel time.
- *
- * @param event The crossing end event.
- */
- private void handleCrossingEnd(Event event) {
- Vehicle vehicle = (Vehicle) event.getData();
- String intersectionId = event.getLocation();
-
- // Update stats
- Intersection intersection = intersections.get(intersectionId);
- if (intersection != null) {
- intersection.incrementVehiclesSent();
- }
-
- double crossingTime = getCrossingTime(vehicle.getType());
- vehicle.addCrossingTime(crossingTime);
-
- System.out.printf("[t=%.2f] Vehicle %s finished crossing at %s%n",
- currentTime, vehicle.getId(), intersectionId);
-
- // Decide what to do next
- String nextDest = vehicle.getCurrentDestination();
- if (nextDest != null && !nextDest.equals("S")) {
- // Route to the *next* intersection
- // Assume 5-10 seconds travel time between intersections
- double travelTime = 5.0 + Math.random() * 5.0;
- double arrivalTime = currentTime + travelTime;
- Event arrivalEvent = new Event(arrivalTime, EventType.VEHICLE_ARRIVAL, vehicle, nextDest);
- eventQueue.offer(arrivalEvent);
- } else {
- // Reached the exit
- handleVehicleExit(vehicle);
- }
- }
-
- /**
- * Handles a vehicle exiting the simulation.
- * Records final statistics for the vehicle.
- *
- * @param vehicle The {@link Vehicle} that has completed its route.
- */
- private void handleVehicleExit(Vehicle vehicle) {
- System.out.printf("[t=%.2f] Vehicle %s exited the system (wait=%.2fs, travel=%.2fs)%n",
- currentTime, vehicle.getId(),
- vehicle.getTotalWaitingTime(),
- vehicle.getTotalTravelTime(currentTime));
-
- // Record the exit for final statistics calculation
- statisticsCollector.recordVehicleExit(vehicle, currentTime);
- }
-
- /**
- * Handles {@link EventType#TRAFFIC_LIGHT_CHANGE}.
- * 1. Toggles the light's state (RED to GREEN or GREEN to RED).
- * 2. If the light just turned GREEN, call {@link #processGreenLight(TrafficLight, Intersection)}
- * to process any waiting vehicles.
- * 3. Schedules the *next* state change for this light based on its
- * green/red time duration.
- *
- * @param event The light change event.
- */
- private void handleTrafficLightChange(Event event) {
- TrafficLight light = (TrafficLight) event.getData();
- String intersectionId = event.getLocation();
-
- // Toggle state
- TrafficLightState newState = (light.getState() == TrafficLightState.RED)
- ? TrafficLightState.GREEN
- : TrafficLightState.RED;
-
- light.changeState(newState);
-
- System.out.printf("[t=%.2f] Traffic light %s changed to %s%n",
- currentTime, light.getId(), newState);
-
- // If changed to GREEN, process waiting vehicles
- if (newState == TrafficLightState.GREEN) {
- Intersection intersection = intersections.get(intersectionId);
- if (intersection != null) {
- processGreenLight(light, intersection);
- }
- }
-
- // Schedule the *next* state change for this same light
- double nextChangeDelay = (newState == TrafficLightState.GREEN)
- ? light.getGreenTime()
- : light.getRedTime();
-
- scheduleTrafficLightChange(light, intersectionId, nextChangeDelay);
- }
-
- /**
- * Processes vehicles when a light turns green.
- * It loops as long as the light is green and there are vehicles in the queue,
- * dequeuing one vehicle at a time and scheduling its crossing.
- * * *Note*: This is a simplified model. A real simulation would
- * account for the *time* it takes each vehicle to cross, processing
- * one vehicle every {@code crossingTime} seconds. This implementation
- * processes the entire queue "instantaneously" at the moment
- * the light turns green.
- *
- * @param light The {@link TrafficLight} that just turned green.
- * @param intersection The {@link Intersection} where the light is.
- */
- private void processGreenLight(TrafficLight light, Intersection intersection) {
- // While the light is green and vehicles are waiting...
- while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
- Vehicle vehicle = light.removeVehicle();
- if (vehicle != null) {
- // Dequeue one vehicle and schedule its crossing
- scheduleCrossing(vehicle, intersection);
- }
- }
- }
-
- /**
- * Handles {@link EventType#STATISTICS_UPDATE}.
- * Calls the {@link StatisticsCollector} to print the current
- * state of the simulation (queue sizes, averages, etc.).
- */
- private void handleStatisticsUpdate() {
- System.out.printf("\n=== Statistics at t=%.2f ===%n", currentTime);
- statisticsCollector.printCurrentStatistics(intersections, currentTime);
- System.out.println();
- }
-
- /**
- * Utility method to get the configured crossing time for a given {@link VehicleType}.
- *
- * @param type The type of vehicle.
- * @return The crossing time in seconds.
- */
- private double getCrossingTime(VehicleType type) {
- return switch (type) {
- case BIKE -> config.getBikeVehicleCrossingTime();
- case LIGHT -> config.getLightVehicleCrossingTime();
- case HEAVY -> config.getHeavyVehicleCrossingTime();
- default -> 2.0;
- }; // Default fallback
- }
-
- /**
- * Prints the final summary of statistics at the end of the simulation.
- */
- private void printFinalStatistics() {
- System.out.println("\n" + "=".repeat(60));
- System.out.println("FINAL SIMULATION STATISTICS");
- System.out.println("=".repeat(60));
-
- statisticsCollector.printFinalStatistics(intersections, currentTime);
-
- System.out.println("=".repeat(60));
- }
-
- // --- Public Getters ---
-
- /**
- * Gets the current simulation time.
- * @return The time in virtual seconds.
- */
- public double getCurrentTime() {
- return currentTime;
- }
-
- /**
- * Gets a map of all intersections in the simulation.
- * Returns a copy to prevent external modification.
- * @return A {@link Map} of intersection IDs to {@link Intersection} objects.
- */
- public Map getIntersections() {
- return new HashMap<>(intersections);
- }
-
- /**
- * Gets the statistics collector instance.
- * @return The {@link StatisticsCollector}.
- */
- public StatisticsCollector getStatisticsCollector() {
- return statisticsCollector;
- }
-}
\ No newline at end of file
diff --git a/main/src/main/java/sd/logging/EventLogger.java b/main/src/main/java/sd/logging/EventLogger.java
new file mode 100644
index 0000000..dd08b23
--- /dev/null
+++ b/main/src/main/java/sd/logging/EventLogger.java
@@ -0,0 +1,251 @@
+package sd.logging;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Motor de logging assíncrono e thread-safe para a simulação distribuída.
+ *
+ * Implementa o padrão Singleton para garantir um ponto centralizado de registo.
+ * Utiliza o padrão Producer-Consumer com uma {@link BlockingQueue} para desacoplar
+ * a geração de eventos (crítica para a performance da simulação) da persistência em disco
+ * (operação de I/O lenta).
+ *
+ * Garantias:
+ *
+ * - Non-blocking writes (para a thread chamadora, na maioria dos casos).
+ * - Ordering cronológico aproximado (FIFO na fila).
+ * - Graceful Shutdown (flush de logs pendentes ao terminar).
+ *
+ */
+public class EventLogger {
+
+ private static EventLogger instance;
+ private static final Object instanceLock = new Object();
+
+ private final PrintWriter writer;
+
+ /** Buffer de memória para absorver picos de eventos (Burst traffic). */
+ private final BlockingQueue logQueue;
+
+ /** Thread dedicada (Consumer) para escrita em ficheiro. */
+ private final Thread writerThread;
+
+ private final AtomicBoolean running;
+ private final SimpleDateFormat timestampFormat;
+ private final long simulationStartMillis;
+
+ /**
+ * Inicializa o sistema de logs.
+ * Abre o ficheiro, escreve o cabeçalho e inicia a thread consumidora.
+ *
+ * @param logFilePath Caminho relativo ou absoluto do ficheiro de log.
+ * @throws IOException Se não for possível criar ou escrever no ficheiro.
+ */
+ private EventLogger(String logFilePath) throws IOException {
+ // Auto-flush ativado para garantir persistência, mas gerido pelo buffer do BufferedWriter
+ this.writer = new PrintWriter(new BufferedWriter(new FileWriter(logFilePath, false)), true);
+ this.logQueue = new LinkedBlockingQueue<>(10000); // Backpressure: limita a 10k eventos pendentes
+ this.running = new AtomicBoolean(true);
+ this.timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ this.simulationStartMillis = System.currentTimeMillis();
+
+ // Header inicial do log
+ writer.println("=".repeat(80));
+ writer.println("SIMULATION EVENT LOG");
+ writer.println("Started: " + timestampFormat.format(new Date()));
+ writer.println("=".repeat(80));
+ writer.println();
+ writer.printf("%-23s | %-8s | %-20s | %-15s | %s\n",
+ "TIMESTAMP", "REL_TIME", "EVENT_TYPE", "COMPONENT", "DESCRIPTION");
+ writer.println("-".repeat(80));
+ writer.flush();
+
+ this.writerThread = new Thread(this::processLogQueue, "EventLogger-Writer");
+ this.writerThread.setDaemon(true); // Permite que a JVM termine se apenas esta thread sobrar
+ this.writerThread.start();
+ }
+
+ /**
+ * Obtém a instância única do logger (Lazy Initialization).
+ * Se não existir, cria uma predefinida em "logs/simulation-events.log".
+ *
+ * @return A instância singleton.
+ */
+ public static EventLogger getInstance() {
+ if (instance == null) {
+ synchronized (instanceLock) {
+ if (instance == null) {
+ try {
+ String logFile = "logs/simulation-events.log";
+ java.nio.file.Files.createDirectories(
+ java.nio.file.Paths.get("logs"));
+ instance = new EventLogger(logFile);
+ } catch (IOException e) {
+ System.err.println("Failed to initialize EventLogger: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Reinicializa o logger com um ficheiro específico.
+ * Útil para testes ou configurações personalizadas.
+ */
+ public static void initialize(String logFilePath) throws IOException {
+ synchronized (instanceLock) {
+ if (instance != null) {
+ instance.shutdown();
+ }
+ instance = new EventLogger(logFilePath);
+ }
+ }
+
+ /**
+ * Regista um evento genérico.
+ * Esta operação é não-bloqueante (retorna imediatamente após colocar na fila),
+ * exceto se a fila estiver cheia (backpressure).
+ *
+ * @param eventType Categoria do evento.
+ * @param component Nome do componente (ex: "Coordinator", "IntersectionProcess").
+ * @param description Detalhes do evento.
+ */
+ public void log(EventType eventType, String component, String description) {
+ if (!running.get()) return;
+
+ LogEntry entry = new LogEntry(
+ System.currentTimeMillis(),
+ eventType,
+ component,
+ description
+ );
+
+ // Non-blocking offer - if queue is full, drop oldest or warn
+ if (!logQueue.offer(entry)) {
+ // Queue full - this shouldn't happen with 10k buffer, but handle gracefully
+ System.err.println("EventLogger queue full - dropping event: " + eventType);
+ }
+ }
+
+ /**
+ * Regista um evento associado a um veículo específico (Helper method).
+ */
+ public void logVehicle(EventType eventType, String component, String vehicleId, String description) {
+ log(eventType, component, "[" + vehicleId + "] " + description);
+ }
+
+ /**
+ * Regista um erro ou exceção com formatação apropriada.
+ */
+ public void logError(String component, String description, Exception e) {
+ String fullDescription = description + (e != null ? ": " + e.getMessage() : "");
+ log(EventType.ERROR, component, fullDescription);
+ }
+
+ /**
+ * Lógica da thread consumidora (Worker Thread).
+ * Retira eventos da fila e escreve no disco continuamente.
+ */
+ private void processLogQueue() {
+ while (running.get() || !logQueue.isEmpty()) {
+ try {
+ // Poll com timeout para permitir verificar a flag 'running' periodicamente
+ LogEntry entry = logQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS);
+ if (entry != null) {
+ writeEntry(entry);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ // Flush final: garantir que eventos restantes na fila são escritos antes de morrer
+ while (!logQueue.isEmpty()) {
+ LogEntry entry = logQueue.poll();
+ if (entry != null) {
+ writeEntry(entry);
+ }
+ }
+ }
+
+ /**
+ * Formata e escreve uma entrada de log no PrintWriter.
+ */
+ private void writeEntry(LogEntry entry) {
+ String timestamp = timestampFormat.format(new Date(entry.timestampMillis));
+ double relativeTime = (entry.timestampMillis - simulationStartMillis) / 1000.0;
+
+ writer.printf("%-23s | %8.3fs | %-20s | %-15s | %s\n",
+ timestamp,
+ relativeTime,
+ entry.eventType.toString(),
+ truncate(entry.component, 15),
+ entry.description
+ );
+
+ // Flush periódico inteligente: se a carga for baixa, garante que vemos logs em tempo real
+ if (logQueue.size() < 10) {
+ writer.flush();
+ }
+ }
+
+ private String truncate(String str, int maxLength) {
+ if (str == null) return "";
+ return str.length() <= maxLength ? str : str.substring(0, maxLength);
+ }
+
+ /**
+ * Encerra o logger de forma segura.
+ * Desativa a aceitação de novos eventos, aguarda que a fila esvazie (flush)
+ * e fecha o ficheiro.
+ */
+ public void shutdown() {
+ if (!running.compareAndSet(true, false)) {
+ return; // Já encerrado
+ }
+
+ try {
+ // Wait for writer thread to finish flushing
+ writerThread.join(5000); // Wait up to 5 seconds
+
+ // Write footer
+ writer.println();
+ writer.println("-".repeat(80));
+ writer.println("SIMULATION ENDED");
+ writer.println("Ended: " + timestampFormat.format(new Date()));
+ writer.println("=".repeat(80));
+
+ writer.close();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * DTO interno imutável para armazenar dados do evento na fila.
+ */
+ private static class LogEntry {
+ final long timestampMillis;
+ final EventType eventType;
+ final String component;
+ final String description;
+
+ LogEntry(long timestampMillis, EventType eventType, String component, String description) {
+ this.timestampMillis = timestampMillis;
+ this.eventType = eventType;
+ this.component = component;
+ this.description = description;
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/logging/EventType.java b/main/src/main/java/sd/logging/EventType.java
new file mode 100644
index 0000000..910ae94
--- /dev/null
+++ b/main/src/main/java/sd/logging/EventType.java
@@ -0,0 +1,60 @@
+package sd.logging;
+
+/**
+ * Taxonomia oficial de eventos para o subsistema de logging centralizado.
+ *
+ * Este enumerado padroniza a categorização de todas as ocorrências na simulação, permitindo:
+ *
+ * - Filtragem granular de logs (ex: ver apenas erros ou apenas tráfego de rede).
+ * - Análise estatística post-mortem (parsear logs para calcular latências).
+ * - Correlação de eventos distribuídos (seguir o rastro de um veículo através de vários nós).
+ *
+ */
+public enum EventType {
+ // --- Ciclo de Vida do Veículo ---
+ VEHICLE_GENERATED("Vehicle Generated"),
+ VEHICLE_ARRIVED("Vehicle Arrived"),
+ VEHICLE_QUEUED("Vehicle Queued"),
+ VEHICLE_DEPARTED("Vehicle Departed"),
+ VEHICLE_EXITED("Vehicle Exited"),
+
+ // --- Controlo de Semáforos e Exclusão Mútua ---
+ LIGHT_CHANGED_GREEN("Light Changed to Green"),
+ LIGHT_CHANGED_RED("Light Changed to Red"),
+ LIGHT_REQUEST_GREEN("Light Requested Green"),
+ LIGHT_RELEASE_GREEN("Light Released Green"),
+
+ // --- Ciclo de Vida da Simulação/Processos ---
+ SIMULATION_STARTED("Simulation Started"),
+ SIMULATION_STOPPED("Simulation Stopped"),
+ PROCESS_STARTED("Process Started"),
+ PROCESS_STOPPED("Process Stopped"),
+
+ // --- Configuração e Telemetria ---
+ STATS_UPDATE("Statistics Update"),
+ CONFIG_CHANGED("Configuration Changed"),
+
+ // --- Camada de Rede (TCP/Sockets) ---
+ CONNECTION_ESTABLISHED("Connection Established"),
+ CONNECTION_LOST("Connection Lost"),
+ MESSAGE_SENT("Message Sent"),
+ MESSAGE_RECEIVED("Message Received"),
+
+ // --- Tratamento de Exceções ---
+ ERROR("Error");
+
+ private final String displayName;
+
+ EventType(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/logging/VehicleTracer.java b/main/src/main/java/sd/logging/VehicleTracer.java
new file mode 100644
index 0000000..611e6aa
--- /dev/null
+++ b/main/src/main/java/sd/logging/VehicleTracer.java
@@ -0,0 +1,364 @@
+package sd.logging;
+
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import sd.model.Vehicle;
+
+/**
+ * Subsistema de auditoria granular responsável pelo rastreio detalhado (Tracing) de veículos individuais.
+ *
+ * Diferente do {@link EventLogger} (que regista eventos globais do sistema), esta classe foca-se
+ * na perspetiva do agente. Cria um ficheiro de rastro dedicado (`.trace`) para cada veículo
+ * monitorizado, registando cronologicamente cada interação com a infraestrutura (interseções,
+ * filas, semáforos).
+ *
+ * Funcionalidades:
+ *
+ * - Análise forense de percursos individuais.
+ * - Validação de tempos de espera e travessia por nó.
+ * - Cálculo de eficiência de rota (tempo em movimento vs. tempo parado).
+ *
+ */
+public class VehicleTracer {
+
+ private static VehicleTracer instance;
+ private static final Object instanceLock = new Object();
+
+ /** Mapa thread-safe de sessões de trace ativas (VehicleID -> TraceHandler). */
+ private final Map trackedVehicles;
+
+ private final SimpleDateFormat timestampFormat;
+ private final long simulationStartMillis;
+ private final String traceDirectory;
+
+ /**
+ * Inicializa o tracer e prepara o diretório de saída.
+ *
+ * @param traceDirectory Caminho para armazenamento dos ficheiros .trace.
+ */
+ private VehicleTracer(String traceDirectory) {
+ this.trackedVehicles = new ConcurrentHashMap<>();
+ this.timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ this.simulationStartMillis = System.currentTimeMillis();
+ this.traceDirectory = traceDirectory;
+
+ try {
+ java.nio.file.Files.createDirectories(java.nio.file.Paths.get(traceDirectory));
+ } catch (IOException e) {
+ System.err.println("Failed to create trace directory: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Obtém a instância única do tracer (Singleton).
+ * @return A instância global.
+ */
+ public static VehicleTracer getInstance() {
+ if (instance == null) {
+ synchronized (instanceLock) {
+ if (instance == null) {
+ instance = new VehicleTracer("logs/traces");
+ }
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Reinicializa o tracer com um diretório personalizado.
+ * Útil para isolar logs de diferentes execuções em lote.
+ */
+ public static void initialize(String traceDirectory) {
+ synchronized (instanceLock) {
+ if (instance != null) {
+ instance.shutdown();
+ }
+ instance = new VehicleTracer(traceDirectory);
+ }
+ }
+
+ /**
+ * Inicia a sessão de rastreio para um veículo específico.
+ * Cria o ficheiro {@code logs/traces/vehicle-{id}.trace} e escreve o cabeçalho.
+ *
+ * @param vehicleId O identificador único do veículo.
+ */
+ public void startTracking(String vehicleId) {
+ if (trackedVehicles.containsKey(vehicleId)) {
+ return; // Já está a ser rastreado
+ }
+
+ VehicleTrace trace = new VehicleTrace(vehicleId, traceDirectory);
+ trackedVehicles.put(vehicleId, trace);
+
+ trace.logEvent("TRACKING_STARTED", "", "Started tracking vehicle " + vehicleId);
+ }
+
+ /**
+ * Encerra a sessão de rastreio, fecha o descritor de ficheiro e remove da memória.
+ */
+ public void stopTracking(String vehicleId) {
+ VehicleTrace trace = trackedVehicles.remove(vehicleId);
+ if (trace != null) {
+ trace.logEvent("TRACKING_STOPPED", "", "Stopped tracking vehicle " + vehicleId);
+ trace.close();
+ }
+ }
+
+ /**
+ * Verifica se um veículo está atualmente sob auditoria.
+ */
+ public boolean isTracking(String vehicleId) {
+ return trackedVehicles.containsKey(vehicleId);
+ }
+
+ /**
+ * Regista o evento de instanciação do veículo pelo Coordenador.
+ */
+ public void logGenerated(Vehicle vehicle) {
+ if (!isTracking(vehicle.getId()))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicle.getId());
+ if (trace != null) {
+ trace.logEvent("GENERATED", "Coordinator",
+ String.format("Type: %s, Entry Time: %.2fs, Route: %s",
+ vehicle.getType(), vehicle.getEntryTime(), vehicle.getRoute()));
+ }
+ }
+
+ /**
+ * Regista a chegada física do veículo à zona de deteção de uma interseção.
+ */
+ public void logArrival(String vehicleId, String intersection, double simulationTime) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("ARRIVED", intersection,
+ String.format("Arrived at %s (sim time: %.2fs)", intersection, simulationTime));
+ }
+ }
+
+ /**
+ * Regista a entrada do veículo na estrutura de fila de um semáforo.
+ */
+ public void logQueued(String vehicleId, String intersection, String direction, int queuePosition) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("QUEUED", intersection,
+ String.format("Queued at %s-%s (position: %d)", intersection, direction, queuePosition));
+ }
+ }
+
+ /**
+ * Regista o início da espera ativa (veículo parado no Vermelho).
+ */
+ public void logWaitingStart(String vehicleId, String intersection, String direction) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("WAITING_START", intersection,
+ String.format("Started waiting at %s-%s (light is RED)", intersection, direction));
+ }
+ }
+
+ /**
+ * Regista o fim da espera (Sinal Verde).
+ * @param waitTime Duração total da paragem nesta instância.
+ */
+ public void logWaitingEnd(String vehicleId, String intersection, String direction, double waitTime) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("WAITING_END", intersection,
+ String.format("Finished waiting at %s-%s (waited %.2fs)", intersection, direction, waitTime));
+ }
+ }
+
+ /**
+ * Regista o início da travessia da interseção (ocupação da zona crítica).
+ */
+ public void logCrossingStart(String vehicleId, String intersection, String direction) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("CROSSING_START", intersection,
+ String.format("Started crossing %s-%s (light is GREEN)", intersection, direction));
+ }
+ }
+
+ /**
+ * Regista a libertação da zona crítica da interseção.
+ */
+ public void logCrossingEnd(String vehicleId, String intersection, double crossingTime) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("CROSSING_END", intersection,
+ String.format("Finished crossing %s (took %.2fs)", intersection, crossingTime));
+ }
+ }
+
+ /**
+ * Regista a partida da interseção em direção ao próximo nó.
+ */
+ public void logDeparture(String vehicleId, String intersection, String nextDestination) {
+ if (!isTracking(vehicleId))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicleId);
+ if (trace != null) {
+ trace.logEvent("DEPARTED", intersection,
+ String.format("Departed from %s toward %s", intersection, nextDestination));
+ }
+ }
+
+ /**
+ * Regista a saída do sistema (no Exit Node).
+ *
+ * Este método também desencadeia a escrita do Sumário de Viagem no final do log
+ * e fecha o ficheiro automaticamente.
+ */
+ public void logExit(Vehicle vehicle, double systemTime) {
+ if (!isTracking(vehicle.getId()))
+ return;
+
+ VehicleTrace trace = trackedVehicles.get(vehicle.getId());
+ if (trace != null) {
+ trace.logEvent("EXITED", "Exit Node",
+ String.format("Exited system - Total time: %.2fs, Waiting: %.2fs, Crossing: %.2fs",
+ systemTime, vehicle.getTotalWaitingTime(), vehicle.getTotalCrossingTime()));
+
+ // Escreve estatísticas sumarizadas
+ trace.writeSummary(vehicle, systemTime);
+
+ // Stop tracking and close file
+ stopTracking(vehicle.getId());
+ }
+ }
+
+ /**
+ * Fecha forçosamente todos os traces abertos.
+ * Deve ser chamado no shutdown da simulação para evitar corrupção de logs.
+ */
+ public void shutdown() {
+ for (VehicleTrace trace : trackedVehicles.values()) {
+ trace.close();
+ }
+ trackedVehicles.clear();
+ }
+
+ /**
+ * Classe interna auxiliar que gere o descritor de ficheiro e a formatação para um único veículo.
+ */
+ private class VehicleTrace {
+ private final String vehicleId;
+ private final PrintWriter writer;
+ private final long traceStartMillis;
+
+ VehicleTrace(String vehicleId, String directory) {
+ this.vehicleId = vehicleId;
+ this.traceStartMillis = System.currentTimeMillis();
+
+ PrintWriter w = null;
+ try {
+ String filename = String.format("%s/vehicle-%s.trace", directory, vehicleId);
+ w = new PrintWriter(new BufferedWriter(new FileWriter(filename, false)), true);
+
+ // Write header
+ w.println("=".repeat(80));
+ w.println("VEHICLE TRACE: " + vehicleId);
+ w.println("Trace Started: " + timestampFormat.format(new Date()));
+ w.println("=".repeat(80));
+ w.println();
+ w.printf("%-23s | %-8s | %-15s | %-15s | %s\n",
+ "TIMESTAMP", "REL_TIME", "EVENT", "LOCATION", "DESCRIPTION");
+ w.println("-".repeat(80));
+
+ } catch (IOException e) {
+ System.err.println("Failed to create trace file for " + vehicleId + ": " + e.getMessage());
+ }
+
+ this.writer = w;
+ }
+
+ void logEvent(String eventType, String location, String description) {
+ if (writer == null)
+ return;
+
+ long now = System.currentTimeMillis();
+ String timestamp = timestampFormat.format(new Date(now));
+ double relativeTime = (now - traceStartMillis) / 1000.0;
+
+ writer.printf("%-23s | %8.3fs | %-15s | %-15s | %s\n",
+ timestamp,
+ relativeTime,
+ truncate(eventType, 15),
+ truncate(location, 15),
+ description);
+ writer.flush();
+ }
+
+ void writeSummary(Vehicle vehicle, double systemTime) {
+ if (writer == null)
+ return;
+
+ writer.println();
+ writer.println("=".repeat(80));
+ writer.println("JOURNEY SUMMARY");
+ writer.println("=".repeat(80));
+ writer.println("Vehicle ID: " + vehicle.getId());
+ writer.println("Vehicle Type: " + vehicle.getType());
+ writer.println("Route: " + vehicle.getRoute());
+ writer.println();
+ writer.printf("Entry Time: %.2f seconds\n", vehicle.getEntryTime());
+ writer.printf("Total System Time: %.2f seconds\n", systemTime);
+ writer.printf("Total Waiting Time: %.2f seconds (%.1f%%)\n",
+ vehicle.getTotalWaitingTime(),
+ 100.0 * vehicle.getTotalWaitingTime() / systemTime);
+ writer.printf("Total Crossing Time: %.2f seconds (%.1f%%)\n",
+ vehicle.getTotalCrossingTime(),
+ 100.0 * vehicle.getTotalCrossingTime() / systemTime);
+ writer.printf("Travel Time: %.2f seconds (%.1f%%)\n",
+ systemTime - vehicle.getTotalWaitingTime() - vehicle.getTotalCrossingTime(),
+ 100.0 * (systemTime - vehicle.getTotalWaitingTime() - vehicle.getTotalCrossingTime()) / systemTime);
+ writer.println("=".repeat(80));
+ }
+
+ void close() {
+ if (writer != null) {
+ writer.println();
+ writer.println("-".repeat(80));
+ writer.println("END OF TRACE");
+ writer.println("=".repeat(80));
+ writer.close();
+ }
+ }
+
+ private String truncate(String str, int maxLength) {
+ if (str == null)
+ return "";
+ return str.length() <= maxLength ? str : str.substring(0, maxLength);
+ }
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/Event.java b/main/src/main/java/sd/model/Event.java
deleted file mode 100644
index c25d734..0000000
--- a/main/src/main/java/sd/model/Event.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package sd.model;
-
-import java.io.Serializable;
-
-/**
- * Represents a single event in the discrete event simulation.
- * * An Event is the fundamental unit of action in the simulation. It contains:
- * - A {@code timestamp} (when the event should occur).
- * - A {@link EventType} (what kind of event it is).
- * - Associated {@code data} (e.g., the {@link Vehicle} or {@link TrafficLight} involved).
- * - An optional {@code location} (e.g., the ID of the {@link Intersection}).
- * * Events are {@link Comparable}, allowing them to be sorted in a
- * {@link java.util.PriorityQueue}. The primary sorting key is the
- * {@code timestamp}. If timestamps are equal, {@code EventType} is used
- * as a tie-breaker to ensure a consistent, deterministic order.
- * * Implements {@link Serializable} so events could (in theory) be sent
- * across a network in a distributed simulation.
- */
-public class Event implements Comparable, Serializable {
- private static final long serialVersionUID = 1L;
-
- /**
- * The simulation time (in seconds) when this event is scheduled to occur.
- */
- private final double timestamp;
-
- /**
- * The type of event (e.g., VEHICLE_ARRIVAL, TRAFFIC_LIGHT_CHANGE).
- */
- private final EventType type;
-
- /**
- * The data payload associated with this event.
- * This could be a {@link Vehicle}, {@link TrafficLight}, or null.
- */
- private final Object data;
-
- /**
- * The ID of the location where the event occurs (e.g., "Cr1").
- * Can be null if the event is not location-specific (like VEHICLE_GENERATION).
- */
- private final String location;
-
- /**
- * Constructs a new Event.
- *
- * @param timestamp The simulation time when the event occurs.
- * @param type The {@link EventType} of the event.
- * @param data The associated data (e.g., a Vehicle object).
- * @param location The ID of the location (e.g., an Intersection ID).
- */
- public Event(double timestamp, EventType type, Object data, String location) {
- this.timestamp = timestamp;
- this.type = type;
- this.data = data;
- this.location = location;
- }
-
- /**
- * Convenience constructor for an Event without a specific location.
- *
- * @param timestamp The simulation time when the event occurs.
- * @param type The {@link EventType} of the event.
- * @param data The associated data (e.g., a Vehicle object).
- */
- public Event(double timestamp, EventType type, Object data) {
- this(timestamp, type, data, null);
- }
-
- /**
- * Compares this event to another event for ordering.
- * * Events are ordered primarily by {@link #timestamp} (ascending).
- * If timestamps are identical, they are ordered by {@link #type} (alphabetical)
- * to provide a stable, deterministic tie-breaking mechanism.
- *
- * @param other The other Event to compare against.
- * @return A negative integer if this event comes before {@code other},
- * zero if they are "equal" in sorting (though this is rare),
- * or a positive integer if this event comes after {@code other}.
- */
- @Override
- public int compareTo(Event other) {
- // Primary sort: timestamp (earlier events come first)
- int cmp = Double.compare(this.timestamp, other.timestamp);
- if (cmp == 0) {
- // Tie-breaker: event type (ensures deterministic order)
- return this.type.compareTo(other.type);
- }
- return cmp;
- }
-
- // --- Getters ---
-
- /**
- * @return The simulation time when the event occurs.
- */
- public double getTimestamp() {
- return timestamp;
- }
-
- /**
- * @return The {@link EventType} of the event.
- */
- public EventType getType() {
- return type;
- }
-
- /**
- * @return The data payload (e.g., {@link Vehicle}, {@link TrafficLight}).
- * The caller must cast this to the expected type.
- */
- public Object getData() {
- return data;
- }
-
- /**
- * @return The location ID (e.g., "Cr1"), or null if not applicable.
- */
- public String getLocation() {
- return location;
- }
-
- /**
- * @return A string representation of the event for logging.
- */
- @Override
- public String toString() {
- return String.format("Event{t=%.2f, type=%s, loc=%s}",
- timestamp, type, location);
- }
-}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/EventType.java b/main/src/main/java/sd/model/EventType.java
deleted file mode 100644
index 5e4d9ee..0000000
--- a/main/src/main/java/sd/model/EventType.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package sd.model;
-
-/**
- * Enumeration representing all possible event types in the discrete event simulation.
- * These types are used by the {@link sd.engine.SimulationEngine} to determine
- * how to process a given {@link Event}.
- */
-public enum EventType {
-
- /**
- * Fired when a {@link Vehicle} arrives at an {@link Intersection}.
- * Data: {@link Vehicle}, Location: Intersection ID
- */
- VEHICLE_ARRIVAL,
-
- /**
- * Fired when a {@link TrafficLight} is scheduled to change its state.
- * Data: {@link TrafficLight}, Location: Intersection ID
- */
- TRAFFIC_LIGHT_CHANGE,
-
- /**
- * Fired when a {@link Vehicle} begins to cross an {@link Intersection}.
- * Data: {@link Vehicle}, Location: Intersection ID
- */
- CROSSING_START,
-
- /**
- * Fired when a {@link Vehicle} finishes crossing an {@link Intersection}.
- * Data: {@link Vehicle}, Location: Intersection ID
- */
- CROSSING_END,
-
- /**
- * Fired when a new {@link Vehicle} should be created and added to the system.
- * Data: null, Location: null
- */
- VEHICLE_GENERATION,
-
- /**
- * Fired periodically to trigger the printing or sending of simulation statistics.
- * Data: null, Location: null
- */
- STATISTICS_UPDATE
-}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/Intersection.java b/main/src/main/java/sd/model/Intersection.java
index 718c98c..612a266 100644
--- a/main/src/main/java/sd/model/Intersection.java
+++ b/main/src/main/java/sd/model/Intersection.java
@@ -6,65 +6,51 @@ import java.util.List;
import java.util.Map;
/**
- * Represents an intersection in the traffic simulation.
- * * An Intersection acts as a central hub. It does not control logic itself,
- * but it *owns* and *manages* a set of {@link TrafficLight} objects.
- * * Its primary responsibilities are:
- * 1. Holding a {@link TrafficLight} for each direction ("North", "East", etc.).
- * 2. Maintaining a {@code routing} table that maps a vehicle's *next*
- * destination (e.g., "Cr3") to a specific *direction* at *this*
- * intersection (e.g., "East").
- * 3. Receiving incoming vehicles and placing them in the correct
- * traffic light's queue based on the routing table.
- * 4. Tracking aggregate statistics for all traffic passing through it.
+ * Representa uma interseção na simulação de tráfego.
+ *
+ * Uma interseção funciona como um nó central da rede. Não controla lógica diretamente,
+ * mas gere um conjunto de semáforos ({@link TrafficLight}).
+ *
+ * Responsabilidades principais:
+ *
+ * - Manter um {@link TrafficLight} para cada direção (Norte, Este, etc.)
+ * - Gerir uma tabela de encaminhamento que mapeia destinos para direções
+ * - Receber veículos e colocá-los na fila do semáforo correto
+ * - Acompanhar estatísticas agregadas do tráfego
+ *
*/
public class Intersection {
- // --- Identity and configuration ---
-
- /**
- * Unique identifier for the intersection (e.g., "Cr1", "Cr2").
- */
+ /** Identificador único da interseção (ex: "Cr1", "Cr2") */
private final String id;
/**
- * A map holding all traffic lights managed by this intersection.
- * Key: Direction (String, e.g., "North", "East").
- * Value: The {@link TrafficLight} object for that direction.
+ * Mapa com todos os semáforos desta interseção.
+ * Chave: Direção (String, ex: "Norte", "Este")
+ * Valor: Objeto {@link TrafficLight} correspondente
*/
private final Map trafficLights;
/**
- * The routing table for this intersection.
- * Key: The *next* destination ID (String, e.g., "Cr3", "S" for exit).
- * Value: The *direction* (String, e.g., "East") a vehicle must take
- * at *this* intersection to reach that destination.
+ * Tabela de encaminhamento da interseção.
+ * Chave: Próximo destino (String, ex: "Cr3", "S" para saída)
+ * Valor: Direção que o veículo deve tomar nesta interseção
*/
private final Map routing;
-
- // --- Statistics ---
-
- /**
- * Total number of vehicles that have been received by this intersection.
- */
+ /** Número total de veículos recebidos por esta interseção */
private int totalVehiclesReceived;
- /**
- * Total number of vehicles that have successfully passed through (sent from) this intersection.
- */
+ /** Número total de veículos que partiram desta interseção */
private int totalVehiclesSent;
- /**
- * A running average of the waiting time for vehicles at this intersection.
- * Note: This calculation might be simplified.
- */
+ /** Média acumulada do tempo de espera dos veículos nesta interseção */
private double averageWaitingTime;
/**
- * Constructs a new Intersection with a given ID.
- * Initializes empty maps for traffic lights and routing.
+ * Cria uma nova interseção.
+ * Inicializa mapas vazios para semáforos e encaminhamento.
*
- * @param id The unique identifier for this intersection (e.g., "Cr1").
+ * @param id identificador único da interseção (ex: "Cr1")
*/
public Intersection(String id) {
this.id = id;
@@ -76,49 +62,56 @@ public class Intersection {
}
/**
- * Registers a new {@link TrafficLight} with this intersection.
- * The light is mapped by its direction.
+ * Regista um novo semáforo nesta interseção.
+ * O semáforo é mapeado pela sua direção.
*
- * @param trafficLight The {@link TrafficLight} object to add.
+ * @param trafficLight o semáforo a adicionar
*/
public void addTrafficLight(TrafficLight trafficLight) {
trafficLights.put(trafficLight.getDirection(), trafficLight);
}
/**
- * Defines a routing rule for this intersection.
- * * This method builds the routing table. For example, calling
- * {@code configureRoute("Cr3", "East")} means "Any vehicle
- * arriving here whose next destination is 'Cr3' should be sent to
- * the 'East' traffic light queue."
+ * Define uma regra de encaminhamento para esta interseção.
+ *
+ * Por exemplo, {@code configureRoute("Cr3", "Este")} significa:
+ * "Qualquer veículo que chegue aqui com destino 'Cr3' deve ser enviado
+ * para a fila do semáforo da direção Este."
*
- * @param nextDestination The ID of the *next* intersection or exit (e.g., "Cr3", "S").
- * @param direction The direction (and thus, the traffic light)
- * at *this* intersection to use (e.g., "East").
+ * @param nextDestination ID da próxima interseção ou saída (ex: "Cr3", "S")
+ * @param direction direção (e respetivo semáforo) a usar nesta interseção
*/
public void configureRoute(String nextDestination, String direction) {
routing.put(nextDestination, direction);
}
/**
- * Accepts an incoming vehicle and places it in the correct queue.
- * * This method:
- * 1. Increments the {@link #totalVehiclesReceived} counter.
- * 2. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
- * 3. Uses the {@link #routing} map to find the correct *direction* for that destination.
- * 4. Adds the vehicle to the queue of the {@link TrafficLight} for that direction.
- *
- * @param vehicle The {@link Vehicle} arriving at the intersection.
+ * Recebe um novo veículo e coloca-o na fila do semáforo apropriado.
+ * A direção é escolhida com base na tabela de encaminhamento.
+ *
+ * @param vehicle o veículo que está a chegar a esta interseção
+ * @param simulationTime o tempo de simulação atual (em segundos)
*/
- public void receiveVehicle(Vehicle vehicle) {
+ public void receiveVehicle(Vehicle vehicle, double simulationTime) {
totalVehiclesReceived++;
+ // Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
+ // before calling this method, so we don't advance here.
+
String nextDestination = vehicle.getCurrentDestination();
+
+ // Check if vehicle reached final destination
+ if (nextDestination == null) {
+ System.out.printf("[%s] Vehicle %s reached final destination%n",
+ this.id, vehicle.getId());
+ return;
+ }
+
String direction = routing.get(nextDestination);
if (direction != null && trafficLights.containsKey(direction)) {
// Found a valid route and light, add vehicle to the queue
- trafficLights.get(direction).addVehicle(vehicle);
+ trafficLights.get(direction).addVehicle(vehicle, simulationTime);
} else {
// Routing error: No rule for this destination or no light for that direction
System.err.printf(
@@ -126,108 +119,100 @@ public class Intersection {
this.id, vehicle.getId(), nextDestination, direction
);
}
+ } /**
+ * Retorna a direção que um veículo deve tomar para alcançar um destino.
+ *
+ * @param destination o próximo destino (ex: "Cr3", "S")
+ * @return a direção (ex: "Este"), ou null se não houver rota configurada
+ */
+ public String getDirectionForDestination(String destination) {
+ return routing.get(destination);
}
/**
- * Returns the traffic light controlling the given direction.
+ * Retorna o semáforo que controla uma determinada direção.
*
- * @param direction The direction (e.g., "North").
- * @return The {@link TrafficLight} object, or null if no light exists
- * for that direction.
+ * @param direction a direção (ex: "Norte")
+ * @return o objeto {@link TrafficLight}, ou null se não existir
*/
public TrafficLight getTrafficLight(String direction) {
return trafficLights.get(direction);
}
/**
- * Returns a list of all traffic lights managed by this intersection.
+ * Retorna uma lista com todos os semáforos desta interseção.
*
- * @return A new {@link List} containing all {@link TrafficLight} objects.
+ * @return uma nova {@link List} com todos os semáforos
*/
public List getTrafficLights() {
- // Return a copy to prevent external modification of the internal map's values
return new ArrayList<>(trafficLights.values());
}
/**
- * Returns the total number of vehicles currently queued across *all*
- * traffic lights at this intersection.
+ * Retorna o número total de veículos em fila em todos os semáforos.
+ * Usa Java Stream API para somar os tamanhos de todas as filas.
*
- * @return The sum of all queue sizes.
+ * @return a soma dos tamanhos de todas as filas
*/
public int getTotalQueueSize() {
- // Uses Java Stream API:
- // 1. trafficLights.values().stream() - Get a stream of TrafficLight objects
- // 2. .mapToInt(TrafficLight::getQueueSize) - Convert each light to its queue size (an int)
- // 3. .sum() - Sum all the integers
return trafficLights.values().stream()
.mapToInt(TrafficLight::getQueueSize)
.sum();
}
- // --- Stats and getters ---
-
/**
- * @return The unique ID of this intersection.
+ * @return o identificador único desta interseção
*/
public String getId() {
return id;
}
/**
- * @return The total number of vehicles that have arrived at this intersection.
+ * @return o número total de veículos que chegaram a esta interseção
*/
public int getTotalVehiclesReceived() {
return totalVehiclesReceived;
}
/**
- * @return The total number of vehicles that have successfully
- * departed from this intersection.
+ * @return o número total de veículos que partiram desta interseção
*/
public int getTotalVehiclesSent() {
return totalVehiclesSent;
}
/**
- * Increments the counter for vehicles that have successfully departed.
- * This is typically called by the {@link sd.engine.SimulationEngine}
- * after a vehicle finishes crossing.
+ * Incrementa o contador de veículos que partiram com sucesso.
+ * Tipicamente chamado após um veículo completar a travessia.
*/
public void incrementVehiclesSent() {
totalVehiclesSent++;
}
/**
- * @return The running average of vehicle waiting time at this intersection.
+ * @return a média do tempo de espera dos veículos nesta interseção
*/
public double getAverageWaitingTime() {
return averageWaitingTime;
}
/**
- * Updates the running average waiting time with a new sample (a new
- * vehicle's wait time).
- * * Uses an incremental/weighted average formula:
- * NewAvg = (OldAvg * (N-1) + NewValue) / N
- * where N is the total number of vehicles sent.
+ * Atualiza a média do tempo de espera com uma nova amostra.
+ * Usa a fórmula: Nova Média = (Média Antiga * (N-1) + Novo Valor) / N
*
- * @param newTime The waiting time (in seconds) of the vehicle that just
- * departed.
+ * @param newTime tempo de espera (em segundos) do veículo que acabou de partir
*/
public void updateAverageWaitingTime(double newTime) {
- // Avoid division by zero if this is called before any vehicle is sent
if (totalVehiclesSent > 0) {
averageWaitingTime = (averageWaitingTime * (totalVehiclesSent - 1) + newTime)
/ totalVehiclesSent;
} else if (totalVehiclesSent == 1) {
- // This is the first vehicle
averageWaitingTime = newTime;
}
}
/**
- * @return A string summary of the intersection's current state.
+ * @return representação textual do estado atual da interseção
*/
@Override
public String toString() {
diff --git a/main/src/main/java/sd/model/Message.java b/main/src/main/java/sd/model/Message.java
index 0217070..d7a9573 100644
--- a/main/src/main/java/sd/model/Message.java
+++ b/main/src/main/java/sd/model/Message.java
@@ -1,55 +1,56 @@
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.
+ * Envelope fundamental do protocolo de comunicação entre processos distribuídos (IPC).
+ *
+ * Esta classe atua como a Unidade de Dados de Aplicação (ADU), encapsulando tanto
+ * os metadados de roteamento (origem, destino, tipo) quanto a carga útil (payload)
+ * polimórfica. É agnóstica ao conteúdo, servindo como contentor genérico para
+ * transferência de estado (Veículos, Estatísticas) ou sinais de controlo (Semáforos).
+ *
+ * A imutabilidade dos campos (exceto via serialização) garante a integridade da mensagem
+ * durante o trânsito na rede.
*/
-public class Message implements Serializable {
+public class Message implements MessageProtocol {
private static final long serialVersionUID = 1L;
- /**
- * Unique identifier for this message.
+ /** * Identificador único universal (UUID).
+ * Essencial para rastreabilidade (tracing), logs de auditoria e mecanismos de deduplicação.
*/
private final String messageId;
- /**
- * The type of this message (e.g., VEHICLE_TRANSFER, STATS_UPDATE).
- */
+ /** Discriminador semântico que define como o recetor deve processar o payload. */
private final MessageType type;
- /**
- * Identifier of the process that sent this message.
- */
+ /** Identificador lógico do nó emissor (ex: "Cr1", "Coordinator"). */
private final String senderId;
- /**
- * Identifier of the destination process. Can be null for broadcast messages.
+ /** * Identificador lógico do nó recetor.
+ * Se {@code null}, a mensagem deve ser tratada como Broadcast.
*/
private final String destinationId;
- /**
- * The actual data being transmitted. Type depends on the message type.
+ /** * Carga útil polimórfica.
+ * Deve implementar {@link java.io.Serializable} para garantir transmissão correta.
*/
private final Object payload;
- /**
- * Timestamp when this message was created (simulation time or real time).
- */
+ /** Marca temporal da criação da mensagem (Unix Timestamp), usada para cálculo de latência de rede. */
private final long timestamp;
/**
- * Creates a new message with all parameters.
+ * Construtor completo para reconstrução de mensagens ou envio com timestamp manual.
*
- * @param type The message type
- * @param senderId The ID of the sending process
- * @param destinationId The ID of the destination process (null for broadcast)
- * @param payload The message payload
- * @param timestamp The timestamp of message creation
+ * @param type Classificação semântica da mensagem.
+ * @param senderId ID do processo origem.
+ * @param destinationId ID do processo destino (ou null para broadcast).
+ * @param payload Objeto de domínio a ser transportado.
+ * @param timestamp Instante de criação (ms).
*/
public Message(MessageType type, String senderId, String destinationId,
Object payload, long timestamp) {
@@ -62,23 +63,24 @@ public class Message implements Serializable {
}
/**
- * Creates a new message with current system time as timestamp.
+ * Construtor de conveniência que atribui automaticamente o timestamp atual do sistema.
*
- * @param type The message type
- * @param senderId The ID of the sending process
- * @param destinationId The ID of the destination process
- * @param payload The message payload
+ * @param type Classificação semântica.
+ * @param senderId ID do processo origem.
+ * @param destinationId ID do processo destino.
+ * @param payload Objeto de domínio.
*/
public Message(MessageType type, String senderId, String destinationId, Object payload) {
this(type, senderId, destinationId, payload, System.currentTimeMillis());
}
/**
- * Creates a broadcast message (no specific destination).
+ * Construtor de conveniência para mensagens de difusão (Broadcast).
+ * Define {@code destinationId} como null.
*
- * @param type The message type
- * @param senderId The ID of the sending process
- * @param payload The message payload
+ * @param type Classificação semântica.
+ * @param senderId ID do processo origem.
+ * @param payload Objeto de domínio.
*/
public Message(MessageType type, String senderId, Object payload) {
this(type, senderId, null, payload, System.currentTimeMillis());
@@ -111,27 +113,40 @@ public class Message implements Serializable {
}
/**
- * Checks if this is a broadcast message (no specific destination).
+ * Verifica se a mensagem se destina a todos os nós da rede.
*
- * @return true if destinationId is null, false otherwise
+ * @return {@code true} se o destinationId for nulo.
*/
public boolean isBroadcast() {
return destinationId == null;
}
/**
- * Gets the payload cast to a specific type.
- * Use with caution and ensure type safety.
+ * Utilitário para casting seguro e fluente do payload.
+ *
+ * Evita a necessidade de casts explícitos e supressão de warnings no código cliente.
*
- * @param The expected payload type
- * @return The payload cast to type T
- * @throws ClassCastException if the payload is not of type T
+ * @param O tipo esperado do payload.
+ * @param clazz A classe do tipo esperado para verificação em runtime (opcional no uso, mas boa prática).
+ * @return O payload convertido para o tipo T.
+ * @throws ClassCastException Se o payload não for compatível com o tipo solicitado.
*/
@SuppressWarnings("unchecked")
public T getPayloadAs(Class clazz) {
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]",
@@ -139,4 +154,4 @@ public class Message implements Serializable {
destinationId != null ? destinationId : "BROADCAST",
timestamp);
}
-}
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/MessageType.java b/main/src/main/java/sd/model/MessageType.java
index 76cb067..4089bcc 100644
--- a/main/src/main/java/sd/model/MessageType.java
+++ b/main/src/main/java/sd/model/MessageType.java
@@ -1,81 +1,49 @@
package sd.model;
/**
- * Enumeration representing all possible message types for distributed communication.
- * These types are used for inter-process communication between different components
- * of the distributed traffic simulation system.
+ * Enumeração que representa todos os tipos de mensagens possíveis para
+ * comunicação distribuída.
+ * Estes tipos são usados para a comunicação entre processos dos diferentes
+ * componentes
+ * do sistema de simulação de tráfego distribuído.
*/
public enum MessageType {
-
+
/**
- * Message to transfer a vehicle between intersections or processes.
- * Payload: Vehicle object with current state
+ * Mensagem para transferir um veículo entre interseções ou processos.
+ * Payload: Objeto Vehicle com o estado atual
*/
VEHICLE_TRANSFER,
-
+
/**
- * Message to update statistics across the distributed system.
- * Payload: Statistics data (waiting times, queue sizes, etc.)
+ * Mensagem para atualizar estatísticas em todo o sistema distribuído.
+ * Payload: Dados estatísticos (tempos de espera, tamanhos de fila, etc.)
*/
STATS_UPDATE,
-
+
/**
- * Message to synchronize traffic light states between processes.
- * Payload: TrafficLight state and timing information
+ * Mensagem para sincronizar a hora de início da simulação em todos os
+ * processos.
+ * Payload: Timestamp de início (long milissegundos)
*/
- TRAFFIC_LIGHT_SYNC,
-
+ SIMULATION_START,
+
/**
- * Heartbeat message to check if a process is alive.
- * Payload: Process ID and timestamp
- */
- HEARTBEAT,
-
- /**
- * Request to join the distributed simulation.
- * Payload: Process information and capabilities
- */
- JOIN_REQUEST,
-
- /**
- * Response to a join request.
- * Payload: Acceptance status and configuration
- */
- JOIN_RESPONSE,
-
- /**
- * Message to notify about a new vehicle generation.
- * Payload: Vehicle generation parameters
+ * Mensagem para notificar sobre a geração de um novo veículo.
+ * Payload: Parâmetros de geração do veículo
*/
VEHICLE_SPAWN,
-
+
/**
- * Message to request the current state of an intersection.
- * Payload: Intersection ID
- */
- STATE_REQUEST,
-
- /**
- * Response containing the current state of an intersection.
- * Payload: Complete intersection state
- */
- STATE_RESPONSE,
-
- /**
- * Message to signal shutdown of a process.
- * Payload: Process ID and reason
+ * Mensagem para sinalizar o encerramento de um processo.
+ * Payload: ID do processo e motivo
*/
SHUTDOWN,
-
+
/**
- * Acknowledgment message for reliable communication.
- * Payload: Message ID being acknowledged
+ * Mensagem para alterar a política de roteamento durante a simulação.
+ * Payload: String com o nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
*/
- ACK,
-
- /**
- * Error message to report problems in the distributed system.
- * Payload: Error description and context
- */
- ERROR
+ ROUTING_POLICY_CHANGE,
+
}
diff --git a/main/src/main/java/sd/model/TrafficLight.java b/main/src/main/java/sd/model/TrafficLight.java
index 1007c03..149e4e3 100644
--- a/main/src/main/java/sd/model/TrafficLight.java
+++ b/main/src/main/java/sd/model/TrafficLight.java
@@ -1,315 +1,269 @@
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;
import java.util.concurrent.locks.ReentrantLock;
/**
- * Represents a single traffic light controlling one direction at an intersection.
- * * Each light maintains its own queue of {@link Vehicle} objects and
- * alternates between {@link TrafficLightState#GREEN} and
- * {@link TrafficLightState#RED} states.
- * * This class is designed to be thread-safe for a potential concurrent
- * simulation (though the current engine {@link sd.engine.SimulationEngine}
- * is single-threaded). It uses a {@link ReentrantLock} to protect its
- * internal state (the queue and the light state) from simultaneous access.
- * * The {@link Condition} variables ({@code vehicleAdded}, {@code lightGreen})
- * are included for a concurrent model where:
- * - A "vehicle" thread might wait on {@code lightGreen} until the light changes.
- * - A "controller" thread might wait on {@code vehicleAdded} to know when to
- * process a queue.
- * (Note: These Conditions are *not* used by the current discrete-event engine).
+ * Representa um semáforo numa interseção.
+ *
+ * Cada semáforo controla uma direção específica e mantém uma fila de veículos à espera.
+ * Alterna entre os estados VERDE e VERMELHO de acordo com a temporização configurada.
+ *
+ * Thread-safety: Usa locks para permitir acesso concorrente seguro entre
+ * a thread de processamento de eventos e as threads de I/O de rede.
*/
public class TrafficLight {
- // --- Identity and configuration ---
-
- /**
- * Unique identifier for the light (e.g., "Cr1-N").
- */
+ /** Identificador único do semáforo (ex: "Cr1-N") */
private final String id;
-
- /**
- * The direction this light controls (e.g., "North", "South").
- */
+
+ /** Direção que este semáforo controla (ex: "Norte", "Sul") */
private final String direction;
-
- /**
- * The current state of the light (GREEN or RED).
- */
+
+ /** Estado atual do semáforo (VERDE ou VERMELHO) */
private TrafficLightState state;
- // --- Vehicle management ---
-
- /**
- * The queue of vehicles waiting at this light.
- * {@link LinkedList} is used as it's a standard {@link Queue} implementation.
- */
+ /** Fila de veículos à espera neste semáforo */
private final Queue queue;
- // --- Synchronization primitives (for thread-safety) ---
-
/**
- * A lock to protect all mutable state ({@link #queue} and {@link #state})
- * from concurrent access. Any method reading or writing these fields
- * *must* acquire this lock first.
+ * Lock para proteger o estado mutável ({@link #queue} e {@link #state})
+ * de acesso concorrente.
*/
private final Lock lock;
-
- /**
- * A condition variable for a potential concurrent model.
- * It could be used to signal threads (e.g., a controller) that
- * a new vehicle has been added to the queue.
- * (Not used in the current discrete-event engine).
- */
+
+ /** Variável de condição para sinalizar adição de veículos (uso futuro) */
private final Condition vehicleAdded;
-
- /**
- * A condition variable for a potential concurrent model.
- * It could be used to signal waiting vehicle threads that the
- * light has just turned GREEN.
- * (Not used in the current discrete-event engine).
- */
+
+ /** Variável de condição para sinalizar que o semáforo ficou verde (uso futuro) */
private final Condition lightGreen;
- // --- Timing configuration ---
-
- /**
- * The duration (in seconds) this light stays GREEN.
- */
+ /** Duração (segundos) que o semáforo permanece VERDE */
private double greenTime;
-
- /**
- * The duration (in seconds) this light stays RED.
- */
+
+ /** Duração (segundos) que o semáforo permanece VERMELHO */
private double redTime;
- // --- Statistics ---
-
- /**
- * Counter for the total number of vehicles that have
- * been dequeued (processed) by this light.
- */
+ /** Número total de veículos processados por este semáforo */
private int totalVehiclesProcessed;
/**
- * Constructs a new TrafficLight.
+ * Regista quando os veículos chegam ao semáforo para cálculo do tempo de espera.
+ * Mapeia ID do veículo para tempo de simulação de chegada (segundos).
+ */
+ private final Map vehicleArrivalTimes;
+
+ /**
+ * Cria um novo semáforo.
*
- * @param id The unique ID (e.g., "Cr1-N").
- * @param direction The direction (e.g., "North").
- * @param greenTime The duration of the GREEN state in seconds.
- * @param redTime The duration of the RED state in seconds.
+ * @param id identificador único (ex: "Cr1-N")
+ * @param direction direção controlada (ex: "Norte")
+ * @param greenTime duração do estado VERDE em segundos
+ * @param redTime duração do estado VERMELHO em segundos
*/
public TrafficLight(String id, String direction, double greenTime, double redTime) {
this.id = id;
this.direction = direction;
- this.state = TrafficLightState.RED; // All lights start RED
+ this.state = TrafficLightState.RED;
this.queue = new LinkedList<>();
- // Initialize synchronization objects
this.lock = new ReentrantLock();
this.vehicleAdded = lock.newCondition();
this.lightGreen = lock.newCondition();
this.greenTime = greenTime;
this.redTime = redTime;
+ this.vehicleArrivalTimes = new HashMap<>();
this.totalVehiclesProcessed = 0;
}
/**
- * Adds a vehicle to the *end* of the waiting queue.
- * This method is thread-safe.
- *
- * @param vehicle The {@link Vehicle} to add.
+ * Coloca um veículo na fila deste semáforo.
+ *
+ * Registamos a hora de chegada para podermos calcular mais tarde quanto tempo o
+ * veículo esperou.
+ *
+ * @param vehicle O veículo que chega ao semáforo.
+ * @param simulationTime O tempo de simulação atual (em segundos).
*/
- public void addVehicle(Vehicle vehicle) {
- lock.lock(); // Acquire the lock
+ public void addVehicle(Vehicle vehicle, double simulationTime) {
+ lock.lock();
try {
- queue.offer(vehicle); // Add vehicle to queue
- vehicleAdded.signalAll(); // Signal (for concurrent models)
+ queue.offer(vehicle);
+ vehicleArrivalTimes.put(vehicle.getId(), simulationTime);
+ vehicleAdded.signalAll();
} finally {
- lock.unlock(); // Always release the lock
+ lock.unlock();
}
}
/**
- * Removes and returns the {@link Vehicle} from the *front* of the queue.
- * * This only succeeds if:
- * 1. The light's state is {@link TrafficLightState#GREEN}.
- * 2. The queue is not empty.
- * * If these conditions are not met, it returns {@code null}.
- * This method is thread-safe.
- *
- * @return The {@link Vehicle} at the front of the queue, or {@code null}
- * if the light is RED or the queue is empty.
+ * Remove um veículo da fila para travessia.
+ *
+ * Só remove se:
+ *
+ * - O semáforo estiver VERDE
+ * - Existir pelo menos um veículo na fila
+ *
+ *
+ * Atualiza automaticamente as estatísticas de tempo de espera do veículo.
+ *
+ * @param simulationTime O tempo de simulação atual (em segundos).
+ * @return o veículo que vai atravessar, ou null se não for possível
*/
- public Vehicle removeVehicle() {
- lock.lock(); // Acquire the lock
+ public Vehicle removeVehicle(double simulationTime) {
+ lock.lock();
try {
if (state == TrafficLightState.GREEN && !queue.isEmpty()) {
- Vehicle vehicle = queue.poll(); // Remove vehicle from queue
+ Vehicle vehicle = queue.poll();
if (vehicle != null) {
totalVehiclesProcessed++;
+
+ Double arrivalTime = vehicleArrivalTimes.remove(vehicle.getId());
+ if (arrivalTime != null) {
+ double waitTimeSeconds = simulationTime - arrivalTime;
+ vehicle.addWaitingTime(waitTimeSeconds);
+ }
}
return vehicle;
}
- return null; // Light is RED or queue is empty
+ return null;
} finally {
- lock.unlock(); // Always release the lock
+ lock.unlock();
}
}
/**
- * Changes the light’s state (e.g., RED -> GREEN).
- * If the new state is GREEN, it signals any waiting threads
- * (for a potential concurrent model).
- * This method is thread-safe.
- *
- * @param newState The {@link TrafficLightState} to set.
+ * Muda o estado do semáforo.
+ *
+ * @param newState novo estado (VERDE ou VERMELHO)
*/
public void changeState(TrafficLightState newState) {
- lock.lock(); // Acquire the lock
+ lock.lock();
try {
this.state = newState;
if (newState == TrafficLightState.GREEN) {
- lightGreen.signalAll(); // Signal (for concurrent models)
+ lightGreen.signalAll();
}
} finally {
- lock.unlock(); // Always release the lock
+ lock.unlock();
}
}
/**
- * Returns how many vehicles are currently in the queue.
- * This method is thread-safe.
- * * @return The size of the queue.
+ * Retorna quantos veículos estão atualmente na fila.
+ * Método thread-safe.
+ *
+ * @return tamanho da fila
*/
public int getQueueSize() {
- lock.lock(); // Acquire the lock
+ lock.lock();
try {
return queue.size();
} finally {
- lock.unlock(); // Always release the lock
+ lock.unlock();
}
}
/**
- * Checks whether the queue is empty.
- * This method is thread-safe.
+ * Verifica se a fila está vazia.
+ * Método thread-safe.
*
- * @return {@code true} if the queue has no vehicles, {@code false} otherwise.
+ * @return {@code true} se não houver veículos, {@code false} caso contrário
*/
public boolean isQueueEmpty() {
- lock.lock(); // Acquire the lock
+ lock.lock();
try {
return queue.isEmpty();
} finally {
- lock.unlock(); // Always release the lock
+ lock.unlock();
}
}
- // --- Getters & Setters ---
-
- /**
- * @return The unique ID of this light (e.g., "Cr1-N").
- */
+ /** @return identificador único do semáforo */
public String getId() {
return id;
}
- /**
- * @return The direction this light controls (e.g., "North").
- */
+ /** @return direção controlada por este semáforo */
public String getDirection() {
return direction;
}
/**
- * Gets the current state of the light (GREEN or RED).
- * This method is thread-safe.
+ * Obtém o estado atual do semáforo.
+ * Método thread-safe.
*
- * @return The current {@link TrafficLightState}.
+ * @return estado atual (VERDE ou VERMELHO)
*/
public TrafficLightState getState() {
- lock.lock(); // Acquire the lock
+ lock.lock();
try {
return state;
} finally {
- lock.unlock(); // Always release the lock
+ lock.unlock();
}
}
- /**
- * @return The configured GREEN light duration in seconds.
- */
+ /** @return duração configurada do sinal verde em segundos */
public double getGreenTime() {
return greenTime;
}
/**
- * Sets the GREEN light duration.
- * @param greenTime The new duration in seconds.
+ * Define a duração do sinal verde.
+ *
+ * @param greenTime nova duração em segundos
*/
public void setGreenTime(double greenTime) {
this.greenTime = greenTime;
}
- /**
- * @return The configured RED light duration in seconds.
- */
+ /** @return duração configurada do sinal vermelho em segundos */
public double getRedTime() {
return redTime;
}
/**
- * Sets the RED light duration.
- * @param redTime The new duration in seconds.
+ * Define a duração do sinal vermelho.
+ *
+ * @param redTime nova duração em segundos
*/
public void setRedTime(double redTime) {
this.redTime = redTime;
}
- /**
- * @return The total number of vehicles processed (dequeued) by this light.
- */
+ /** @return número total de veículos processados por este semáforo */
public int getTotalVehiclesProcessed() {
- // Note: This read is not locked, assuming it's okay
- // for it to be "eventually consistent" for stats.
- // For strict accuracy, it should also be locked.
return totalVehiclesProcessed;
}
- /**
- * @return The {@link Lock} object for advanced synchronization.
- */
+ /** @return objeto {@link Lock} para sincronização avançada */
public Lock getLock() {
return lock;
}
- /**
- * @return The {@link Condition} for vehicle additions.
- */
+ /** @return condição para adição de veículos */
public Condition getVehicleAdded() {
return vehicleAdded;
}
- /**
- * @return The {@link Condition} for the light turning green.
- */
+ /** @return condição para semáforo ficar verde */
public Condition getLightGreen() {
return lightGreen;
}
- /**
- * @return A string summary of the light's current state.
- */
+ /** @return representação textual do estado atual do semáforo */
@Override
public String toString() {
return String.format(
- "TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}",
- id, direction, getState(), getQueueSize() // Use getters for thread-safety
+ "TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}",
+ id, direction, getState(), getQueueSize()
);
}
}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/TrafficLightState.java b/main/src/main/java/sd/model/TrafficLightState.java
index c21d6e4..aab7682 100644
--- a/main/src/main/java/sd/model/TrafficLightState.java
+++ b/main/src/main/java/sd/model/TrafficLightState.java
@@ -1,17 +1,13 @@
package sd.model;
/**
- * Enumeration representing the two possible states of a {@link TrafficLight}.
+ * Estados possíveis de um semáforo ({@link TrafficLight}).
*/
public enum TrafficLightState {
- /**
- * The light is GREEN, allowing vehicles to pass (be dequeued).
- */
+ /** Sinal verde - veículos podem passar */
GREEN,
- /**
- * The light is RED, blocking vehicles (they remain in the queue).
- */
+ /** Sinal vermelho - veículos aguardam na fila */
RED
}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/Vehicle.java b/main/src/main/java/sd/model/Vehicle.java
index dcf860a..51ed1de 100644
--- a/main/src/main/java/sd/model/Vehicle.java
+++ b/main/src/main/java/sd/model/Vehicle.java
@@ -5,93 +5,84 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Represents a single vehicle moving through the simulation.
- *
- * This class is a data object that holds the state of a vehicle, including:
- * - Its unique ID, type, and entry time.
- * - Its complete, pre-determined {@code route} (a list of intersection IDs).
- * - Its current position in the route ({@code currentRouteIndex}).
- * - Metrics for total time spent waiting at red lights and time spent crossing.
- * * This object is passed around the simulation, primarily inside {@link Event}
- * payloads and stored in {@link TrafficLight} queues.
- * * Implements {@link Serializable} so it can be sent between processes
- * or nodes (e.g., over a socket in a distributed version of the simulation).
+ * Representa um veículo que se move pela rede de interseções.
+ *
+ *
+ * Esta classe é o "gémeo digital" de um carro, mota ou camião.
+ * Mantém toda a informação necessária:
+ *
+ *
+ * - Identificação e tipo do veículo
+ * - Rota completa a percorrer
+ * - Métricas de tempo (espera, travessia, total)
+ *
+ *
+ *
+ * O objeto é serializado e enviado pela rede à medida que o veículo
+ * se move entre processos distribuídos.
+ *
*/
public class Vehicle implements Serializable {
private static final long serialVersionUID = 1L;
- // --- Identity and configuration ---
-
- /**
- * Unique identifier for the vehicle (e.g., "V1", "V2").
- */
+ /** Identificador único do veículo (ex: "V1", "V2") */
private final String id;
-
- /**
- * The type of vehicle (BIKE, LIGHT, HEAVY).
- */
+
+ /** Tipo de veículo (BIKE, LIGHT, HEAVY) */
private final VehicleType type;
-
- /**
- * The simulation time (in seconds) when the vehicle was generated.
- */
+
+ /** Tempo de simulação (em segundos) em que o veículo foi gerado */
private final double entryTime;
-
+
/**
- * The complete, ordered list of destinations (intersection IDs and the
- * final exit "S"). Example: ["Cr1", "Cr3", "S"].
+ * Lista ordenada completa de destinos (IDs de interseções e saída "S").
+ * Exemplo: ["Cr1", "Cr3", "S"]
*/
private final List route;
-
+
/**
- * An index that tracks the vehicle's progress along its {@link #route}.
- * {@code route.get(currentRouteIndex)} is the vehicle's *current*
- * destination (i.e., the one it is traveling *towards* or *arriving at*).
+ * Índice que acompanha o progresso do veículo ao longo da {@link #route}.
+ * {@code route.get(currentRouteIndex)} é o destino *atual* do veículo.
*/
private int currentRouteIndex;
- // --- Metrics ---
-
/**
- * The total accumulated time (in seconds) this vehicle has spent
- * waiting at red lights.
+ * Tempo total acumulado (segundos) que o veículo passou à espera em semáforos
+ * vermelhos
*/
private double totalWaitingTime;
-
+
/**
- * The total accumulated time (in seconds) this vehicle has spent
- * actively crossing intersections.
+ * Tempo total acumulado (segundos) que o veículo passou a atravessar
+ * interseções
*/
private double totalCrossingTime;
/**
- * Constructs a new Vehicle.
- *
- * @param id The unique ID for the vehicle.
- * @param type The {@link VehicleType}.
- * @param entryTime The simulation time when the vehicle is created.
- * @param route The complete list of destination IDs (e.t., ["Cr1", "Cr2", "S"]).
+ * Cria um novo veículo pronto para se fazer à estrada.
+ *
+ * @param id Identificador único (ex: "V1").
+ * @param type O tipo de veículo (determina velocidade/tamanho).
+ * @param entryTime Quando este veículo entrou na simulação (segundos).
+ * @param route A lista ordenada de paragens (Interseções -> Saída).
*/
public Vehicle(String id, VehicleType type, double entryTime, List route) {
this.id = id;
this.type = type;
this.entryTime = entryTime;
- // Create a copy of the route list to ensure immutability
this.route = new ArrayList<>(route);
- this.currentRouteIndex = 0; // Starts at the first destination
+ this.currentRouteIndex = 0;
this.totalWaitingTime = 0.0;
this.totalCrossingTime = 0.0;
}
/**
- * Advances the vehicle to the next stop in its route by
- * incrementing the {@link #currentRouteIndex}.
- * * This is typically called *after* a vehicle *arrives* at an intersection,
- * to set its *next* destination before it is queued.
- *
- * @return {@code true} if there is still at least one more destination
- * in the route, {@code false} if the vehicle has passed its
- * final destination.
+ * Move o GPS interno do veículo para o próximo destino.
+ *
+ * Chame isto quando um veículo chega a uma interseção para atualizar para onde
+ * deve ir a seguir.
+ *
+ * @return true se houver mais paragens, false se a viagem terminou.
*/
public boolean advanceRoute() {
currentRouteIndex++;
@@ -99,120 +90,94 @@ public class Vehicle implements Serializable {
}
/**
- * Gets the current destination (the next intersection or exit) that
- * the vehicle is heading towards.
+ * Obtém o destino atual (próxima interseção ou saída) para onde o veículo se
+ * dirige.
*
- * @return The ID of the current destination (e.g., "Cr1"), or
- * {@code null} if the route is complete.
+ * @return ID do destino atual (ex: "Cr1"), ou {@code null} se a rota terminou
*/
public String getCurrentDestination() {
return (currentRouteIndex < route.size()) ? route.get(currentRouteIndex) : null;
}
/**
- * Checks if the vehicle has completed its entire route.
+ * Verifica se o veículo completou toda a sua rota.
*
- * @return {@code true} if the route index is at or past the end
- * of the route list, {@code false} otherwise.
+ * @return {@code true} se chegou ao fim da rota, {@code false} caso contrário
*/
public boolean hasReachedEnd() {
return currentRouteIndex >= route.size();
}
- // --- Getters and metrics management ---
-
- /**
- * @return The vehicle's unique ID.
- */
+ /** @return identificador único do veículo */
public String getId() {
return id;
}
- /**
- * @return The vehicle's {@link VehicleType}.
- */
+ /** @return tipo do veículo */
public VehicleType getType() {
return type;
}
- /**
- * @return The simulation time when the vehicle entered the system.
- */
+ /** @return tempo de simulação em que o veículo entrou no sistema */
public double getEntryTime() {
return entryTime;
}
- /**
- * @return A *copy* of the vehicle's complete route.
- */
+ /** @return cópia da rota completa do veículo */
public List getRoute() {
- // Return a copy to prevent external modification
return new ArrayList<>(route);
}
- /**
- * @return The current index pointing to the vehicle's destination in its route list.
- */
+ /** @return índice atual apontando para o destino do veículo na sua rota */
public int getCurrentRouteIndex() {
return currentRouteIndex;
}
- /**
- * @return The total accumulated waiting time in seconds.
- */
+ /** @return tempo total acumulado de espera em segundos */
public double getTotalWaitingTime() {
return totalWaitingTime;
}
/**
- * Adds a duration to the vehicle's total waiting time.
- * This is called by the simulation engine when a vehicle
- * starts crossing an intersection.
+ * Adiciona uma duração ao tempo total de espera do veículo.
+ * Chamado quando um veículo começa a atravessar uma interseção.
*
- * @param time The duration (in seconds) to add.
+ * @param time duração (em segundos) a adicionar
*/
public void addWaitingTime(double time) {
totalWaitingTime += time;
}
- /**
- * @return The total accumulated crossing time in seconds.
- */
+ /** @return tempo total acumulado de travessia em segundos */
public double getTotalCrossingTime() {
return totalCrossingTime;
}
/**
- * Adds a duration to the vehicle's total crossing time.
- * This is called by the simulation engine when a vehicle
- * finishes crossing an intersection.
+ * Adiciona uma duração ao tempo total de travessia do veículo.
+ * Chamado quando um veículo termina de atravessar uma interseção.
*
- * @param time The duration (in seconds) to add.
+ * @param time duração (em segundos) a adicionar
*/
public void addCrossingTime(double time) {
totalCrossingTime += time;
}
/**
- * Calculates the vehicle's total time spent in the system so far.
- * This is a "live" calculation.
+ * Calcula o tempo total que o veículo passou no sistema até agora.
*
- * @param currentTime The current simulation time.
- * @return The total elapsed time (in seconds) since the vehicle
- * was generated ({@code currentTime - entryTime}).
+ * @param currentTime tempo atual da simulação
+ * @return tempo total decorrido (em segundos) desde que o veículo foi gerado
*/
public double getTotalTravelTime(double currentTime) {
return currentTime - entryTime;
}
- /**
- * @return A string summary of the vehicle's current state.
- */
+ /** @return representação textual do estado atual do veículo */
@Override
public String toString() {
return String.format(
- "Vehicle{id='%s', type=%s, next='%s', route=%s}",
- id, type, getCurrentDestination(), route
- );
+ "Vehicle{id='%s', type=%s, next='%s', route=%s}",
+ id, type, getCurrentDestination(), route);
}
}
\ No newline at end of file
diff --git a/main/src/main/java/sd/model/VehicleType.java b/main/src/main/java/sd/model/VehicleType.java
index fce00d0..e3aa195 100644
--- a/main/src/main/java/sd/model/VehicleType.java
+++ b/main/src/main/java/sd/model/VehicleType.java
@@ -1,27 +1,19 @@
package sd.model;
/**
- * Enumeration representing the different types of vehicles in the simulation.
- * Each type can have different properties, such as crossing time
- * and generation probability, defined in {@link sd.config.SimulationConfig}.
+ * Enumeração dos diferentes tipos de veículos na simulação.
+ *
+ * Cada tipo pode ter propriedades diferentes como tempo de travessia
+ * e probabilidade de geração, definidas na {@link sd.config.SimulationConfig}.
*/
public enum VehicleType {
- /**
- * A bike or motorcycle.
- * Typically has a short crossing time.
- */
+ /** Bicicleta ou motocicleta - tempo de travessia curto */
BIKE,
- /**
- * A standard light vehicle, such as a car.
- * This is usually the most common type.
- */
+ /** Veículo ligeiro padrão (carro) - tipo mais comum */
LIGHT,
- /**
- * A heavy vehicle, such as a truck or bus.
- * Typically has a long crossing time.
- */
+ /** Veículo pesado (camião ou autocarro) - tempo de travessia longo */
HEAVY
}
\ No newline at end of file
diff --git a/main/src/main/java/sd/protocol/MessageProtocol.java b/main/src/main/java/sd/protocol/MessageProtocol.java
new file mode 100644
index 0000000..9a4ba27
--- /dev/null
+++ b/main/src/main/java/sd/protocol/MessageProtocol.java
@@ -0,0 +1,45 @@
+package sd.protocol;
+
+import java.io.Serializable;
+
+import sd.model.MessageType;
+
+/**
+ * Contrato para todas as mensagens trocadas no simulador.
+ *
+ * Garante que mensagens podem ser identificadas e encaminhadas.
+ * Extende Serializable para permitir envio via sockets.
+ */
+public interface MessageProtocol extends Serializable {
+
+ /**
+ * Tipo da mensagem, indicando o seu propósito.
+ * @return tipo (ex: VEHICLE_TRANSFER, STATS_UPDATE)
+ */
+ MessageType getType();
+
+ /**
+ * Dados (payload) que esta mensagem transporta.
+ *
+ *
Tipo depende do MessageType:
+ *
+ * - VEHICLE_TRANSFER → objeto Vehicle
+ *
- STATS_UPDATE → objeto de estatísticas
+ *
+ *
+ * @return payload (deve ser Serializable)
+ */
+ Object getPayload();
+
+ /**
+ * ID do nó (processo) que enviou a mensagem.
+ * @return ID de origem (ex: "Cr1", "Cr5", "S")
+ */
+ String getSourceNode();
+
+ /**
+ * ID do nó de destino.
+ * @return ID de destino (ex: "Cr2", "DashboardServer")
+ */
+ String getDestinationNode();
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/protocol/SocketConnection.java b/main/src/main/java/sd/protocol/SocketConnection.java
new file mode 100644
index 0000000..16dcdd4
--- /dev/null
+++ b/main/src/main/java/sd/protocol/SocketConnection.java
@@ -0,0 +1,232 @@
+package sd.protocol;
+
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.concurrent.TimeUnit;
+
+import sd.serialization.MessageSerializer;
+import sd.serialization.SerializationException;
+import sd.serialization.SerializerFactory;
+
+/**
+ * Wrapper de alto nível para gestão robusta de conexões TCP.
+ *
+ * Esta classe abstrai a complexidade da API nativa {@link java.net.Socket}, oferecendo:
+ *
+ * - Resiliência: Lógica de reconexão automática (Retry Loop) no arranque, crucial para sistemas
+ * distribuídos onde a ordem de inicialização dos nós não é garantida.
+ * - Framing: Implementação transparente do protocolo "Length-Prefix" (4 bytes de tamanho + payload),
+ * resolvendo o problema de fragmentação de stream TCP.
+ * - Serialização: Integração direta com a camada de serialização JSON.
+ *
+ */
+public class SocketConnection implements Closeable {
+
+ // --- Network Resources ---
+
+ /**
+ * The underlying TCP socket used for network communication.
+ */
+ private final Socket socket;
+
+ /**
+ * The raw output stream for writing bytes to the network.
+ * Wrapped by {@link DataOutputStream} during message sending.
+ */
+ private final OutputStream outputStream;
+
+ /**
+ * The raw input stream for reading bytes from the network.
+ * Wrapped by {@link DataInputStream} during message reception.
+ */
+ private final InputStream inputStream;
+
+ // --- Serialization ---
+
+ /**
+ * The serializer strategy used to convert objects to/from byte arrays (e.g., JSON).
+ */
+ private final MessageSerializer serializer;
+
+ /** Número máximo de tentativas de ligação antes de desistir (Fail-fast). */
+ private static final int MAX_RETRIES = 5;
+
+ /** Janela de espera (backoff) linear entre tentativas (em milissegundos). */
+ private static final long RETRY_DELAY_MS = 1000;
+
+ /**
+ * Construtor para clientes (Active Open).
+ * Tenta estabelecer uma conexão TCP com um servidor, aplicando lógica de retry.
+ *
+ * Este comportamento é vital quando o processo Coordenador inicia antes das Interseções estarem
+ * prontas para aceitar conexões ({@code accept()}).
+ *
+ * @param host Endereço do nó de destino (ex: "localhost").
+ * @param port Porta de serviço.
+ * @throws IOException Se a conexão falhar após todas as {@code MAX_RETRIES} tentativas.
+ * @throws UnknownHostException Se o DNS não resolver o hostname.
+ * @throws InterruptedException Se a thread for interrompida durante o sleep de retry.
+ */
+ public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException {
+ Socket tempSocket = null;
+ IOException lastException = null;
+
+ System.out.printf("[SocketConnection] Attempting to connect to %s:%d...%n", host, port);
+
+ // --- Retry Loop ---
+ for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+ try {
+ // Try to establish the connection (SYN -> SYN-ACK -> ACK)
+ tempSocket = new Socket(host, port);
+
+ // If successful, break out of the retry loop
+ System.out.printf("[SocketConnection] Connected successfully on attempt %d.%n", attempt);
+ lastException = null; // Clear last error on success
+ break;
+
+ } catch (ConnectException | SocketTimeoutException e) {
+ // Common errors: "Connection refused" (server not up) or "Timeout" (firewall/network)
+ lastException = e;
+ System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n",
+ attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
+
+ if (attempt < MAX_RETRIES) {
+ // Blocking wait before next attempt
+ TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
+ }
+ } catch (IOException e) {
+ // Other IO errors
+ lastException = e;
+ System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n",
+ attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
+ if (attempt < MAX_RETRIES) {
+ TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
+ }
+ }
+ } // --- End of Retry Loop ---
+
+ // Final validation
+ if (tempSocket == null) {
+ System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES);
+ if (lastException != null) {
+ throw lastException; // Propagate the root cause
+ } else {
+ throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts, reason unknown.");
+ }
+ }
+
+ // Initialize streams
+ this.socket = tempSocket;
+ this.outputStream = socket.getOutputStream();
+ this.inputStream = socket.getInputStream();
+ this.serializer = SerializerFactory.createDefault();
+ }
+
+ /**
+ * Construtor para servidores (Passive Open).
+ * Envolve um socket já conectado (retornado por {@code serverSocket.accept()}).
+ * Não necessita de retry logic pois a conexão física já existe.
+ *
+ * @param acceptedSocket O socket ativo retornado pelo SO.
+ * @throws IOException Se falhar a obtenção dos streams de I/O.
+ */
+ public SocketConnection(Socket acceptedSocket) throws IOException {
+ this.socket = acceptedSocket;
+ this.outputStream = socket.getOutputStream();
+ this.inputStream = socket.getInputStream();
+ this.serializer = SerializerFactory.createDefault();
+ }
+
+ /**
+ * Serializa e transmite uma mensagem através do canal.
+ *
+ * Utiliza sincronização ({@code synchronized}) para garantir que escritas concorrentes
+ * na mesma conexão não corrompem a stream de bytes (thread-safety).
+ *
+ * @param message O objeto de protocolo a enviar.
+ * @throws IOException Se o socket estiver fechado ou ocorrer erro de escrita.
+ */
+ public synchronized void sendMessage(MessageProtocol message) throws IOException {
+ if (socket == null || !socket.isConnected()) {
+ throw new IOException("Socket is not connected");
+ }
+
+ try {
+ // Serializa para bytes JSON
+ byte[] data = serializer.serialize(message);
+
+ // Write 4-byte length prefix (Framing)
+ DataOutputStream dataOut = new DataOutputStream(outputStream);
+ dataOut.writeInt(data.length);
+ dataOut.write(data);
+ dataOut.flush(); // Force transmission immediately
+
+ } catch (SerializationException e) {
+ throw new IOException("Failed to serialize message", e);
+ }
+ }
+
+ /**
+ * Bloqueia à espera de uma mensagem completa do socket.
+ *
+ * Lê primeiro o cabeçalho de tamanho (4 bytes) e depois o payload exato,
+ * garantindo que processa mensagens completas mesmo se chegarem fragmentadas em múltiplos pacotes TCP.
+ *
+ * @return O objeto {@link MessageProtocol} reconstruído.
+ * @throws IOException Se a conexão for perdida (EOF) ou o stream corrompido.
+ * @throws ClassNotFoundException Se o tipo desserializado não for encontrado no classpath.
+ */
+ public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException {
+ if (socket == null || !socket.isConnected()) {
+ throw new IOException("Socket is not connected");
+ }
+
+ try {
+
+ DataInputStream dataIn = new DataInputStream(inputStream);
+ int length = dataIn.readInt();
+
+ // Sanity check para evitar OutOfMemory em caso de corrupção de stream
+ if (length <= 0 || length > 10_000_000) { // Max 10MB payload
+ throw new IOException("Invalid message length: " + length);
+ }
+
+ // Ler dados exatos da mensagem
+ byte[] data = new byte[length];
+ dataIn.readFully(data);
+
+ // Deserialize do JSON - força o tipo concreto Message
+ return serializer.deserialize(data, sd.model.Message.class);
+
+ } catch (SerializationException e) {
+ throw new IOException("Failed to deserialize message", e);
+ }
+ }
+
+ /**
+ * Encerra a conexão e liberta os descritores de ficheiro.
+ * Operação idempotente.
+ */
+ @Override
+ public void close() throws IOException {
+ if (inputStream != null) inputStream.close();
+ if (outputStream != null) outputStream.close();
+ if (socket != null) socket.close();
+ }
+
+ /**
+ * Verifica o estado operacional da conexão.
+ * @return true se o socket está aberto e conectado.
+ */
+ public boolean isConnected() {
+ return socket != null && socket.isConnected() && !socket.isClosed();
+ }
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/routing/LeastCongestedRouteSelector.java b/main/src/main/java/sd/routing/LeastCongestedRouteSelector.java
new file mode 100644
index 0000000..24f91d4
--- /dev/null
+++ b/main/src/main/java/sd/routing/LeastCongestedRouteSelector.java
@@ -0,0 +1,151 @@
+package sd.routing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementação da política de roteamento por menor congestionamento.
+ *
+ *
Esta política escolhe dinamicamente a rota que passa pelos cruzamentos
+ * menos congestionados, com base no tamanho atual das filas em cada interseção.
+ * É uma política dinâmica que adapta as decisões ao estado da rede.
+ *
+ * Objetivo: Distribuir o tráfego pela rede, evitando bottlenecks e
+ * minimizando o tempo de espera total.
+ *
+ * Algoritmo:
+ *
+ * - Para cada rota possível, calcula a carga total (soma das filas)
+ * - Escolhe a rota com menor carga total
+ * - Em caso de empate ou falta de informação, usa a rota mais curta
+ *
+ */
+public class LeastCongestedRouteSelector implements RouteSelector {
+
+ /** Rotas possíveis a partir do ponto de entrada E1 */
+ private final List> e1Routes;
+ /** Rotas possíveis a partir do ponto de entrada E2 */
+ private final List> e2Routes;
+ /** Rotas possíveis a partir do ponto de entrada E3 */
+ private final List> e3Routes;
+
+ /**
+ * Cria um novo seletor de rotas baseado em menor congestionamento.
+ */
+ public LeastCongestedRouteSelector() {
+ this.e1Routes = new ArrayList<>();
+ this.e2Routes = new ArrayList<>();
+ this.e3Routes = new ArrayList<>();
+ initializeRoutes();
+ }
+
+ /**
+ * Inicializa as rotas possíveis para cada ponto de entrada.
+ */
+ private void initializeRoutes() {
+ // Rotas de E1 (entrada Norte)
+ e1Routes.add(Arrays.asList("Cr1", "Cr4", "Cr5", "S"));
+ e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr5", "S"));
+ e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr3", "S"));
+
+ // Rotas de E2 (entrada Oeste)
+ e2Routes.add(Arrays.asList("Cr2", "Cr5", "S"));
+ e2Routes.add(Arrays.asList("Cr2", "Cr3", "S"));
+ e2Routes.add(Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"));
+
+ // Rotas de E3 (entrada Sul)
+ e3Routes.add(Arrays.asList("Cr3", "S"));
+ e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr5", "S"));
+ e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"));
+ }
+
+ @Override
+ public List selectRoute(String entryPoint, Map queueSizes) {
+ List> availableRoutes = getRoutesForEntryPoint(entryPoint);
+
+ // Se não temos informação sobre filas, usa a rota mais curta como fallback
+ if (queueSizes == null || queueSizes.isEmpty()) {
+ return selectShortestRoute(availableRoutes);
+ }
+
+ // Calcula a carga de cada rota e escolhe a menos congestionada
+ List bestRoute = null;
+ int minLoad = Integer.MAX_VALUE;
+
+ for (List route : availableRoutes) {
+ int routeLoad = calculateRouteLoad(route, queueSizes);
+
+ if (routeLoad < minLoad) {
+ minLoad = routeLoad;
+ bestRoute = route;
+ }
+ }
+
+ // Fallback: se não conseguimos calcular carga, usa a primeira rota
+ if (bestRoute == null) {
+ bestRoute = availableRoutes.get(0);
+ }
+
+ return new ArrayList<>(bestRoute);
+ }
+
+ /**
+ * Calcula a carga total de uma rota (soma do tamanho das filas em todos os cruzamentos).
+ *
+ * @param route rota a avaliar
+ * @param queueSizes mapa com tamanho das filas por interseção
+ * @return carga total da rota (soma das filas)
+ */
+ private int calculateRouteLoad(List route, Map queueSizes) {
+ int totalLoad = 0;
+
+ for (String intersection : route) {
+ // Ignora "S" (saída) e apenas conta cruzamentos reais
+ if (!intersection.equals("S") && queueSizes.containsKey(intersection)) {
+ totalLoad += queueSizes.get(intersection);
+ }
+ }
+
+ return totalLoad;
+ }
+
+ /**
+ * Seleciona a rota mais curta (menor número de nós) como fallback.
+ *
+ * @param routes lista de rotas disponíveis
+ * @return a rota mais curta
+ */
+ private List selectShortestRoute(List> routes) {
+ List shortest = routes.get(0);
+
+ for (List route : routes) {
+ if (route.size() < shortest.size()) {
+ shortest = route;
+ }
+ }
+
+ return new ArrayList<>(shortest);
+ }
+
+ /**
+ * Obtém as rotas disponíveis para um ponto de entrada.
+ *
+ * @param entryPoint ponto de entrada (E1, E2 ou E3)
+ * @return lista de rotas disponíveis
+ */
+ private List> getRoutesForEntryPoint(String entryPoint) {
+ switch (entryPoint.toUpperCase()) {
+ case "E1":
+ return e1Routes;
+ case "E2":
+ return e2Routes;
+ case "E3":
+ return e3Routes;
+ default:
+ System.err.printf("Unknown entry point: %s, defaulting to E1%n", entryPoint);
+ return e1Routes;
+ }
+ }
+}
diff --git a/main/src/main/java/sd/routing/RandomRouteSelector.java b/main/src/main/java/sd/routing/RandomRouteSelector.java
new file mode 100644
index 0000000..5b9df21
--- /dev/null
+++ b/main/src/main/java/sd/routing/RandomRouteSelector.java
@@ -0,0 +1,122 @@
+package sd.routing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementação da política de roteamento aleatória (baseline).
+ *
+ * Esta política seleciona rotas com base em probabilidades predefinidas,
+ * sem considerar o estado atual da rede. É a implementação de referência
+ * para comparação com outras políticas.
+ *
+ * As rotas são organizadas por ponto de entrada (E1, E2, E3) e cada rota
+ * tem uma probabilidade de seleção associada.
+ */
+public class RandomRouteSelector implements RouteSelector {
+
+ /** Rotas possíveis a partir do ponto de entrada E1 */
+ private final List e1Routes;
+ /** Rotas possíveis a partir do ponto de entrada E2 */
+ private final List e2Routes;
+ /** Rotas possíveis a partir do ponto de entrada E3 */
+ private final List e3Routes;
+
+ /**
+ * Cria um novo seletor de rotas aleatórias com rotas predefinidas.
+ */
+ public RandomRouteSelector() {
+ this.e1Routes = new ArrayList<>();
+ this.e2Routes = new ArrayList<>();
+ this.e3Routes = new ArrayList<>();
+ initializePossibleRoutes();
+ }
+
+ /**
+ * Define todas as rotas possíveis que os veículos podem tomar.
+ * As rotas são organizadas por ponto de entrada (E1, E2, E3).
+ * Cada rota tem uma probabilidade que determina a frequência com que é escolhida.
+ */
+ private void initializePossibleRoutes() {
+ // Rotas de E1 (entrada Norte)
+ e1Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr1", "Cr4", "Cr5", "S"), 0.34));
+ e1Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr1", "Cr2", "Cr5", "S"), 0.33));
+ e1Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr1", "Cr2", "Cr3", "S"), 0.33));
+
+ // Rotas de E2 (entrada Oeste)
+ e2Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr2", "Cr5", "S"), 0.34));
+ e2Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr2", "Cr3", "S"), 0.33));
+ e2Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33));
+
+ // Rotas de E3 (entrada Sul)
+ e3Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr3", "S"), 0.34));
+ e3Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr3", "Cr2", "Cr5", "S"), 0.33));
+ e3Routes.add(new RouteWithProbability(
+ Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33));
+ }
+
+ @Override
+ public List selectRoute(String entryPoint, Map queueSizes) {
+ // Ignora queueSizes - seleção aleatória não depende do estado da rede
+
+ List selectedRoutes = getRoutesForEntryPoint(entryPoint);
+
+ // Seleciona uma rota baseada em probabilidades cumulativas
+ double rand = Math.random();
+ double cumulative = 0.0;
+
+ for (RouteWithProbability routeWithProb : selectedRoutes) {
+ cumulative += routeWithProb.probability;
+ if (rand <= cumulative) {
+ // Retorna uma cópia da rota para prevenir modificações
+ return new ArrayList<>(routeWithProb.route);
+ }
+ }
+
+ // Fallback: retorna a primeira rota
+ return new ArrayList<>(selectedRoutes.get(0).route);
+ }
+
+ /**
+ * Obtém as rotas disponíveis para um ponto de entrada.
+ *
+ * @param entryPoint ponto de entrada (E1, E2 ou E3)
+ * @return lista de rotas com probabilidades
+ */
+ private List getRoutesForEntryPoint(String entryPoint) {
+ switch (entryPoint.toUpperCase()) {
+ case "E1":
+ return e1Routes;
+ case "E2":
+ return e2Routes;
+ case "E3":
+ return e3Routes;
+ default:
+ System.err.printf("Unknown entry point: %s, defaulting to E1%n", entryPoint);
+ return e1Routes;
+ }
+ }
+
+ /**
+ * Classe interna para associar uma rota com sua probabilidade de seleção.
+ */
+ private static class RouteWithProbability {
+ final List route;
+ final double probability;
+
+ RouteWithProbability(List route, double probability) {
+ this.route = route;
+ this.probability = probability;
+ }
+ }
+}
diff --git a/main/src/main/java/sd/routing/RouteSelector.java b/main/src/main/java/sd/routing/RouteSelector.java
new file mode 100644
index 0000000..0febb29
--- /dev/null
+++ b/main/src/main/java/sd/routing/RouteSelector.java
@@ -0,0 +1,25 @@
+package sd.routing;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface para implementação de políticas de seleção de rotas.
+ *
+ * Define o contrato que todas as políticas de roteamento devem seguir.
+ * Permite a implementação de diferentes estratégias de roteamento
+ * (aleatória, caminho mais curto, menor congestionamento, etc.).
+ */
+public interface RouteSelector {
+
+ /**
+ * Seleciona uma rota para um veículo a partir de um ponto de entrada.
+ *
+ * @param entryPoint ponto de entrada (E1, E2 ou E3)
+ * @param queueSizes mapa com o tamanho das filas em cada interseção (opcional, pode ser null).
+ * Chave: ID da interseção (ex: "Cr1", "Cr2")
+ * Valor: número total de veículos em espera nessa interseção
+ * @return lista de IDs representando a rota escolhida (ex: ["Cr1", "Cr2", "Cr5", "S"])
+ */
+ List selectRoute(String entryPoint, Map queueSizes);
+}
diff --git a/main/src/main/java/sd/routing/RoutingPolicy.java b/main/src/main/java/sd/routing/RoutingPolicy.java
new file mode 100644
index 0000000..06c7db2
--- /dev/null
+++ b/main/src/main/java/sd/routing/RoutingPolicy.java
@@ -0,0 +1,36 @@
+package sd.routing;
+
+/**
+ * Enumeração que define as políticas de roteamento disponíveis para a simulação.
+ *
+ * As políticas de roteamento determinam como os veículos escolhem o caminho
+ * a seguir desde o ponto de entrada até à saída da rede de interseções.
+ *
+ *
+ * - RANDOM: Seleção aleatória de rotas baseada em probabilidades predefinidas
+ * - SHORTEST_PATH: Escolhe sempre a rota com o menor número de cruzamentos
+ * - LEAST_CONGESTED: Escolhe a rota evitando cruzamentos mais congestionados
+ *
+ */
+public enum RoutingPolicy {
+ /**
+ * Política aleatória (baseline).
+ * Seleciona rotas com base em probabilidades predefinidas, sem considerar
+ * o estado atual da rede.
+ */
+ RANDOM,
+
+ /**
+ * Política do caminho mais curto.
+ * Sempre escolhe a rota com o menor número de cruzamentos entre o ponto
+ * de entrada e a saída, minimizando a distância teórica.
+ */
+ SHORTEST_PATH,
+
+ /**
+ * Política das menores filas (roteamento dinâmico).
+ * Escolhe a rota que passa pelos cruzamentos menos congestionados,
+ * com base no tamanho atual das filas em cada interseção.
+ */
+ LEAST_CONGESTED
+}
diff --git a/main/src/main/java/sd/routing/ShortestPathRouteSelector.java b/main/src/main/java/sd/routing/ShortestPathRouteSelector.java
new file mode 100644
index 0000000..a92321d
--- /dev/null
+++ b/main/src/main/java/sd/routing/ShortestPathRouteSelector.java
@@ -0,0 +1,89 @@
+package sd.routing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementação da política de roteamento por caminho mais curto.
+ *
+ * Esta política sempre escolhe a rota com o menor número de cruzamentos
+ * entre o ponto de entrada e a saída. É uma política determinística que
+ * não considera o estado da rede (tamanho das filas).
+ *
+ * Objetivo: Minimizar a distância teórica percorrida pelos veículos.
+ */
+public class ShortestPathRouteSelector implements RouteSelector {
+
+ /** Rotas possíveis a partir do ponto de entrada E1, ordenadas por comprimento */
+ private final List> e1Routes;
+ /** Rotas possíveis a partir do ponto de entrada E2, ordenadas por comprimento */
+ private final List> e2Routes;
+ /** Rotas possíveis a partir do ponto de entrada E3, ordenadas por comprimento */
+ private final List> e3Routes;
+
+ /**
+ * Cria um novo seletor de rotas por caminho mais curto.
+ * As rotas são ordenadas por comprimento (número de cruzamentos).
+ */
+ public ShortestPathRouteSelector() {
+ this.e1Routes = new ArrayList<>();
+ this.e2Routes = new ArrayList<>();
+ this.e3Routes = new ArrayList<>();
+ initializeRoutes();
+ }
+
+ /**
+ * Inicializa as rotas possíveis para cada ponto de entrada.
+ * As rotas são organizadas da mais curta para a mais longa.
+ */
+ private void initializeRoutes() {
+ // Rotas de E1 (entrada Norte) - ordenadas por comprimento
+ e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr3", "S")); // 4 nós
+ e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr5", "S")); // 4 nós
+ e1Routes.add(Arrays.asList("Cr1", "Cr4", "Cr5", "S")); // 4 nós
+
+ // Rotas de E2 (entrada Oeste) - ordenadas por comprimento
+ e2Routes.add(Arrays.asList("Cr2", "Cr3", "S")); // 3 nós (mais curta!)
+ e2Routes.add(Arrays.asList("Cr2", "Cr5", "S")); // 3 nós
+ e2Routes.add(Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S")); // 5 nós
+
+ // Rotas de E3 (entrada Sul) - ordenadas por comprimento
+ e3Routes.add(Arrays.asList("Cr3", "S")); // 2 nós (mais curta!)
+ e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr5", "S")); // 4 nós
+ e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S")); // 6 nós
+ }
+
+ @Override
+ public List selectRoute(String entryPoint, Map queueSizes) {
+ // Ignora queueSizes - política baseada apenas no comprimento do caminho
+
+ List> availableRoutes = getRoutesForEntryPoint(entryPoint);
+
+ // Retorna a rota mais curta (primeira da lista)
+ List shortestRoute = availableRoutes.get(0);
+
+ return new ArrayList<>(shortestRoute);
+ }
+
+ /**
+ * Obtém as rotas disponíveis para um ponto de entrada.
+ *
+ * @param entryPoint ponto de entrada (E1, E2 ou E3)
+ * @return lista de rotas ordenadas por comprimento
+ */
+ private List> getRoutesForEntryPoint(String entryPoint) {
+ switch (entryPoint.toUpperCase()) {
+ case "E1":
+ return e1Routes;
+ case "E2":
+ return e2Routes;
+ case "E3":
+ return e3Routes;
+ default:
+ System.err.printf("Unknown entry point: %s, defaulting to E1%n", entryPoint);
+ return e1Routes;
+ }
+ }
+}
diff --git a/main/src/main/java/sd/serialization/JsonMessageSerializer.java b/main/src/main/java/sd/serialization/JsonMessageSerializer.java
index 1b70c68..68c6b06 100644
--- a/main/src/main/java/sd/serialization/JsonMessageSerializer.java
+++ b/main/src/main/java/sd/serialization/JsonMessageSerializer.java
@@ -1,26 +1,25 @@
package sd.serialization;
+import java.nio.charset.StandardCharsets;
+
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
-import java.nio.charset.StandardCharsets;
-
/**
- * JSON-based implementation of {@link MessageSerializer} using Google's Gson library.
- *
- * This serializer converts objects to JSON format for transmission, providing:
- * - Human-readable message format (easy debugging)
- * - Cross-platform compatibility
- * - Smaller message sizes compared to Java native serialization
- * - Better security (no code execution during deserialization)
- *
- * The serializer is configured with pretty printing disabled by default for
- * production use, but can be enabled for debugging purposes.
- *
- * Thread-safety: This class is thread-safe as Gson instances are thread-safe.
- *
- * @see MessageSerializer
+ * Implementação baseada em JSON da estratégia {@link MessageSerializer}, utilizando a biblioteca Gson.
+ *
+ * Este serializador converte objetos Java para o formato de texto JSON antes da transmissão.
+ * Oferece várias vantagens técnicas sobre a serialização nativa do Java:
+ *
+ * - Legibilidade: O formato de texto facilita a depuração (sniffing de rede) sem ferramentas especializadas.
+ * - Interoperabilidade: Permite futura integração com componentes não-Java (ex: Dashboards web em JS).
+ * - Segurança: Reduz a superfície de ataque para execução remota de código (RCE), pois não desserializa classes arbitrárias, apenas dados.
+ *
+ *
+ * Thread-Safety: A instância interna do {@code Gson} é imutável e thread-safe, permitindo
+ * que este serializador seja partilhado entre múltiplas threads (ex: no pool do DashboardServer).
+ * * @see MessageSerializer
*/
public class JsonMessageSerializer implements MessageSerializer {
@@ -28,16 +27,16 @@ public class JsonMessageSerializer implements MessageSerializer {
private final boolean prettyPrint;
/**
- * Creates a new JSON serializer with default configuration (no pretty printing).
+ * Cria um novo serializador JSON com configuração otimizada para produção (compacto).
*/
public JsonMessageSerializer() {
this(false);
}
/**
- * Creates a new JSON serializer with optional pretty printing.
- *
- * @param prettyPrint If true, JSON output will be formatted with indentation
+ * Cria um novo serializador JSON com formatação opcional.
+ * * @param prettyPrint Se {@code true}, o JSON gerado incluirá indentação e quebras de linha.
+ * Útil para debug, mas aumenta significativamente o tamanho do payload.
*/
public JsonMessageSerializer(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
@@ -53,6 +52,13 @@ public class JsonMessageSerializer implements MessageSerializer {
this.gson = builder.create();
}
+ /**
+ * Converte um objeto em memória para um array de bytes JSON (UTF-8).
+ *
+ * @param object O objeto a ser serializado.
+ * @return O payload em bytes pronto para transmissão TCP.
+ * @throws SerializationException Se o objeto não for compatível com JSON ou ocorrer erro de encoding.
+ */
@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
@@ -68,6 +74,16 @@ public class JsonMessageSerializer implements MessageSerializer {
}
}
+ /**
+ * Reconstrói um objeto Java a partir de um array de bytes JSON.
+ *
+ * Realiza a validação sintática do JSON e a validação de tipo baseada na classe alvo.
+ *
+ * @param data O array de bytes recebido da rede.
+ * @param clazz A classe do objeto esperado (Type Token).
+ * @return A instância do objeto reconstruído.
+ * @throws SerializationException Se o JSON for malformado ou incompatível com a classe alvo.
+ */
@Override
public T deserialize(byte[] data, Class clazz) throws SerializationException {
if (data == null) {
@@ -95,20 +111,18 @@ public class JsonMessageSerializer implements MessageSerializer {
}
/**
- * Returns the underlying Gson instance for advanced usage.
- *
- * @return The Gson instance
+ * Retorna a instância subjacente do Gson para configurações avançadas.
+ * * @return A instância Gson configurada.
*/
public Gson getGson() {
return gson;
}
/**
- * Checks if pretty printing is enabled.
- *
- * @return true if pretty printing is enabled
+ * Verifica se a formatação "pretty print" está ativa.
+ * * @return true se a indentação estiver habilitada.
*/
public boolean isPrettyPrint() {
return prettyPrint;
}
-}
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/serialization/MessageSerializer.java b/main/src/main/java/sd/serialization/MessageSerializer.java
index 21517f4..3a5ff55 100644
--- a/main/src/main/java/sd/serialization/MessageSerializer.java
+++ b/main/src/main/java/sd/serialization/MessageSerializer.java
@@ -1,48 +1,49 @@
package sd.serialization;
/**
- * Interface for serializing and deserializing objects for network transmission.
- *
- * This interface provides a common abstraction for different serialization strategies
- * allowing the system to switch between implementations without changing the communication layer.
- *
- * Implementations must ensure:
- * - Thread-safety if used in concurrent contexts
- * - Proper exception handling with meaningful error messages
- * - Preservation of object state during round-trip serialization
- *
- * @see JsonMessageSerializer
+ * Interface que define o contrato para estratégias de serialização e desserialização de objetos.
+ *
+ * Esta abstração permite desacoplar a camada de transporte (Sockets TCP) da camada de
+ * apresentação de dados. Ao implementar o padrão Strategy, o sistema ganha flexibilidade
+ * para alternar entre diferentes formatos de codificação (JSON, Binário Nativo, XML, Protobuf)
+ * sem necessidade de refatorização da lógica de rede.
+ *
+ * Requisitos para Implementações:
+ *
+ * - Thread-Safety: As implementações devem ser seguras para uso concorrente, dado que
+ * instâncias únicas podem ser partilhadas por múltiplos ClientHandlers.
+ * - Robustez: Falhas de parsing devem resultar em exceções tipificadas ({@link SerializationException}),
+ * nunca em falhas silenciosas ou estados inconsistentes.
+ *
+ * * @see JsonMessageSerializer
*/
public interface MessageSerializer {
/**
- * Serializes an object into a byte array for transmission.
- *
- * @param object The object to serialize (must not be null)
- * @return A byte array containing the serialized representation
- * @throws SerializationException If serialization fails
- * @throws IllegalArgumentException If object is null
+ * Converte (Marshals) um objeto em memória para uma sequência de bytes para transmissão.
+ * * @param object O objeto de domínio a ser serializado (não pode ser nulo).
+ * @return Um array de bytes contendo a representação codificada do objeto.
+ * @throws SerializationException Se ocorrer um erro durante a codificação (ex: ciclo de referências).
+ * @throws IllegalArgumentException Se o objeto fornecido for nulo.
*/
byte[] serialize(Object object) throws SerializationException;
/**
- * Deserializes a byte array back into an object of the specified type.
- *
- * @param The expected type of the deserialized object
- * @param data The byte array containing serialized data (must not be null)
- * @param clazz The class of the expected object type (must not be null)
- * @return The deserialized object
- * @throws SerializationException If deserialization fails
- * @throws IllegalArgumentException If data or clazz is null
+ * Reconstrói (Unmarshals) um objeto a partir de uma sequência de bytes.
+ * * @param O tipo genérico do objeto esperado.
+ * @param data O array de bytes contendo os dados serializados (não pode ser nulo).
+ * @param clazz A classe do tipo esperado para verificação e instancialização.
+ * @return A instância do objeto reconstruído com o seu estado restaurado.
+ * @throws SerializationException Se os dados estiverem corrompidos ou incompatíveis com a classe alvo.
+ * @throws IllegalArgumentException Se os dados ou a classe forem nulos.
*/
T deserialize(byte[] data, Class clazz) throws SerializationException;
/**
- * Gets the name of this serialization strategy (e.g., "JSON", "Java Native").
- * Useful for logging and debugging.
- *
- * @return The serializer name
+ * Obtém o identificador legível desta estratégia de serialização (ex: "JSON (Gson)", "Native").
+ * Utilizado primariamente para logging, auditoria e negociação de conteúdo.
+ * * @return O nome descritivo do serializador.
*/
String getName();
-}
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/serialization/SerializationExample.java b/main/src/main/java/sd/serialization/SerializationExample.java
deleted file mode 100644
index f6bd817..0000000
--- a/main/src/main/java/sd/serialization/SerializationExample.java
+++ /dev/null
@@ -1,134 +0,0 @@
-package sd.serialization;
-
-import sd.model.Message;
-import sd.model.MessageType;
-import sd.model.Vehicle;
-import sd.model.VehicleType;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Demonstration of JSON serialization usage in the traffic simulation system.
- *
- * This class shows practical examples of how to use JSON (Gson) serialization
- * for network communication between simulation processes.
- */
-public class SerializationExample {
-
- public static void main(String[] args) {
- System.out.println("=== JSON Serialization Example ===\n");
-
- // Create a sample vehicle
- List route = Arrays.asList("Cr1", "Cr2", "Cr5", "S");
- Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 10.5, route);
- vehicle.addWaitingTime(2.3);
- vehicle.addCrossingTime(1.2);
-
- // Create a message containing the vehicle
- Message message = new Message(
- MessageType.VEHICLE_TRANSFER,
- "Cr1",
- "Cr2",
- vehicle
- );
-
- // ===== JSON Serialization =====
- demonstrateJsonSerialization(message);
-
- // ===== Factory Usage =====
- demonstrateFactoryUsage(message);
-
- // ===== Performance Test =====
- performanceTest(message);
- }
-
- private static void demonstrateJsonSerialization(Message message) {
- System.out.println("--- JSON Serialization ---");
-
- try {
- // Create JSON serializer with pretty printing for readability
- MessageSerializer serializer = new JsonMessageSerializer(true);
-
- // Serialize to bytes
- byte[] data = serializer.serialize(message);
-
- // Display the JSON
- String json = new String(data);
- System.out.println("Serialized JSON (" + data.length + " bytes):");
- System.out.println(json);
-
- // Deserialize back
- Message deserialized = serializer.deserialize(data, Message.class);
- System.out.println("\nDeserialized: " + deserialized);
- System.out.println("✓ JSON serialization successful\n");
-
- } catch (SerializationException e) {
- System.err.println("❌ JSON serialization failed: " + e.getMessage());
- }
- }
-
- private static void demonstrateFactoryUsage(Message message) {
- System.out.println("--- Using SerializerFactory ---");
-
- try {
- // Get default serializer (JSON)
- MessageSerializer serializer = SerializerFactory.createDefault();
- System.out.println("Default serializer: " + serializer.getName());
-
- // Use it
- byte[] data = serializer.serialize(message);
- Message deserialized = serializer.deserialize(data, Message.class);
-
- System.out.println("Message type: " + deserialized.getType());
- System.out.println("From: " + deserialized.getSenderId() +
- " → To: " + deserialized.getDestinationId());
- System.out.println("✓ Factory usage successful\n");
-
- } catch (SerializationException e) {
- System.err.println("❌ Factory usage failed: " + e.getMessage());
- }
- }
-
- private static void performanceTest(Message message) {
- System.out.println("--- Performance Test ---");
-
- int iterations = 1000;
-
- try {
- MessageSerializer compactSerializer = new JsonMessageSerializer(false);
- MessageSerializer prettySerializer = new JsonMessageSerializer(true);
-
- // Warm up
- for (int i = 0; i < 100; i++) {
- compactSerializer.serialize(message);
- }
-
- // Test compact JSON
- long compactStart = System.nanoTime();
- byte[] compactData = null;
- for (int i = 0; i < iterations; i++) {
- compactData = compactSerializer.serialize(message);
- }
- long compactTime = System.nanoTime() - compactStart;
-
- // Test pretty JSON
- byte[] prettyData = prettySerializer.serialize(message);
-
- // Results
- System.out.println("Iterations: " + iterations);
- System.out.println("\nJSON Compact:");
- System.out.println(" Size: " + compactData.length + " bytes");
- System.out.println(" Time: " + (compactTime / 1_000_000.0) + " ms total");
- System.out.println(" Avg: " + (compactTime / iterations / 1_000.0) + " μs/operation");
-
- System.out.println("\nJSON Pretty-Print:");
- System.out.println(" Size: " + prettyData.length + " bytes");
- System.out.println(" Size increase: " +
- String.format("%.1f%%", ((double)prettyData.length / compactData.length - 1) * 100));
-
- } catch (SerializationException e) {
- System.err.println("❌ Performance test failed: " + e.getMessage());
- }
- }
-}
diff --git a/main/src/main/java/sd/serialization/SerializationException.java b/main/src/main/java/sd/serialization/SerializationException.java
index 5cf9675..bae51b0 100644
--- a/main/src/main/java/sd/serialization/SerializationException.java
+++ b/main/src/main/java/sd/serialization/SerializationException.java
@@ -1,41 +1,40 @@
package sd.serialization;
/**
- * Exception thrown when serialization or deserialization operations fail.
- *
- * This exception wraps underlying errors (I/O exceptions, parsing errors, etc.)
- * and provides context about what went wrong during the serialization process.
+ * Exceção verificada (Checked Exception) que sinaliza falhas no processo de transformação de dados.
+ *
+ * Esta classe atua como um wrapper unificador para erros ocorridos na camada de serialização,
+ * abstraindo falhas de baixo nível (como erros de I/O, sintaxe JSON inválida ou incompatibilidade
+ * de tipos) numa única exceção de domínio. Permite que o código cliente trate falhas de
+ * protocolo de forma consistente, independentemente da implementação subjacente (Gson, Nativa, etc.).
*/
public class SerializationException extends Exception {
private static final long serialVersionUID = 1L; // Long(64bits) instead of int(32bits)
/**
- * Constructs a new serialization exception with the specified detail message.
- *
- * @param message The detail message
+ * Constrói uma nova exceção de serialização com uma mensagem descritiva.
+ * * @param message A mensagem detalhando o erro.
*/
public SerializationException(String message) {
super(message);
}
/**
- * Constructs a new serialization exception with the specified detail message
- * and cause.
- *
- * @param message The detail message
- * @param cause The cause of this exception
+ * Constrói uma nova exceção encapsulando a causa raiz do problema.
+ * Útil para preservar a stack trace original de erros de bibliotecas terceiras (ex: Gson).
+ * * @param message A mensagem detalhando o erro.
+ * @param cause A exceção original que causou a falha.
*/
public SerializationException(String message, Throwable cause) {
super(message, cause);
}
/**
- * Constructs a new serialization exception with the specified cause.
- *
- * @param cause The cause of this exception
+ * Constrói uma nova exceção baseada apenas na causa raiz.
+ * * @param cause A exceção original.
*/
public SerializationException(Throwable cause) {
super(cause);
}
-}
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/serialization/SerializerFactory.java b/main/src/main/java/sd/serialization/SerializerFactory.java
index a2261d3..70d2f0e 100644
--- a/main/src/main/java/sd/serialization/SerializerFactory.java
+++ b/main/src/main/java/sd/serialization/SerializerFactory.java
@@ -1,14 +1,14 @@
package sd.serialization;
/**
- * Factory for creating {@link MessageSerializer} instances.
- *
- * This factory provides a centralized way to create and configure JSON serializers
- * using Gson, making it easy to configure serialization throughout the application.
- *
- * The factory can be configured via system properties for easy deployment configuration.
- *
- * Example usage:
+ * Fábrica estática (Factory Pattern) para instanciação controlada de {@link MessageSerializer}.
+ *
+ * Esta classe centraliza a criação de estratégias de serialização, garantindo consistência
+ * de configuração em todo o sistema distribuído. Permite a injeção de configurações via
+ * Propriedades de Sistema (System Properties), facilitando a alternância entre modos de
+ * depuração (Pretty Print) e produção (Compacto) sem recompilação.
+ *
+ * Exemplo de Uso:
*
* MessageSerializer serializer = SerializerFactory.createDefault();
* byte[] data = serializer.serialize(myObject);
@@ -17,28 +17,27 @@ package sd.serialization;
public class SerializerFactory {
/**
- * System property key for enabling pretty-print in JSON serialization.
- * Set to "true" for debugging, "false" for production.
+ * Chave da propriedade de sistema para ativar a formatação JSON legível (Pretty Print).
+ * Defina {@code -Dsd.serialization.json.prettyPrint=true} na JVM para ativar.
*/
public static final String JSON_PRETTY_PRINT_PROPERTY = "sd.serialization.json.prettyPrint";
- // Default configuration
+ // Default configuration (Production-ready)
private static final boolean DEFAULT_JSON_PRETTY_PRINT = false;
/**
- * Private constructor to prevent instantiation.
+ * Construtor privado para prevenir instanciação acidental desta classe utilitária.
*/
private SerializerFactory() {
throw new UnsupportedOperationException("Factory class cannot be instantiated");
}
/**
- * Creates a JSON serializer based on system configuration.
- *
- * Pretty-print is determined by checking the system property
- * {@value #JSON_PRETTY_PRINT_PROPERTY}. If not set, defaults to false.
- *
- * @return A configured JsonMessageSerializer instance
+ * Cria um serializador JSON configurado dinamicamente pelo ambiente.
+ *
+ * Verifica a propriedade de sistema {@value #JSON_PRETTY_PRINT_PROPERTY}.
+ * Se não definida, assume o padrão de produção (falso/compacto).
+ * * @return Uma instância configurada de {@link JsonMessageSerializer}.
*/
public static MessageSerializer createDefault() {
boolean prettyPrint = Boolean.getBoolean(JSON_PRETTY_PRINT_PROPERTY);
@@ -46,21 +45,20 @@ public class SerializerFactory {
}
/**
- * Creates a JSON serializer with default configuration (no pretty printing).
- *
- * @return A JsonMessageSerializer instance
+ * Cria um serializador JSON com configuração padrão otimizada (sem indentação).
+ * Ideal para ambientes de produção onde a largura de banda é prioritária.
+ * * @return Uma instância compacta de {@link JsonMessageSerializer}.
*/
public static MessageSerializer createSerializer() {
return createSerializer(DEFAULT_JSON_PRETTY_PRINT);
}
/**
- * Creates a JSON serializer with specified pretty-print setting.
- *
- * @param prettyPrint Whether to enable pretty printing
- * @return A JsonMessageSerializer instance
+ * Cria um serializador JSON com configuração explícita de formatação.
+ * * @param prettyPrint {@code true} para ativar indentação (Debug), {@code false} para compacto.
+ * @return Uma instância personalizada de {@link JsonMessageSerializer}.
*/
public static MessageSerializer createSerializer(boolean prettyPrint) {
return new JsonMessageSerializer(prettyPrint);
}
-}
+}
\ No newline at end of file
diff --git a/main/src/main/java/sd/util/RandomGenerator.java b/main/src/main/java/sd/util/RandomGenerator.java
index f1122d5..e9ab436 100644
--- a/main/src/main/java/sd/util/RandomGenerator.java
+++ b/main/src/main/java/sd/util/RandomGenerator.java
@@ -3,84 +3,88 @@ package sd.util;
import java.util.Random;
/**
- * Utility class for generating random values used throughout the simulation.
- * * Provides static methods for:
- * - Generating exponentially distributed intervals (for Poisson processes).
- * - Generating random integers and doubles in a range.
- * - Making decisions based on probability.
- * - Choosing random elements from an array.
- * * It uses a single, static {@link Random} instance.
+ * Utilitário central de geração estocástica para a simulação.
+ *
+ * Esta classe fornece primitivas para geração de números pseudo-aleatórios, abstraindo
+ * a complexidade de distribuições estatísticas.
+ *
+ * Funcionalidades Principais:
+ *
+ * - Modelagem de Poisson: Geração de tempos entre chegadas usando distribuição exponencial inversa.
+ * - Amostragem Uniforme: Geração de inteiros e doubles em intervalos fechados/abertos.
+ * - Decisão Probabilística: Avaliação de eventos booleanos baseados em pesos (Bernoulli trials).
+ * - Determinismo: Suporte a sementes (seeds) manuais para reprodutibilidade exata de cenários de teste.
+ *
*/
public class RandomGenerator {
- /**
- * The single, shared Random instance for the entire simulation.
+ /** * Instância singleton estática do gerador PRNG (Pseudo-Random Number Generator).
+ * Thread-safe (java.util.Random é sincronizado), embora possa haver contenção em alta concorrência.
*/
private static final Random random = new Random();
/**
- * Returns a random time interval that follows an exponential distribution.
- * * This is a key component for modeling a Poisson process, where the
- * *inter-arrival times* (time between events) are exponentially distributed.
- * The formula used is the inverse transform sampling method:
- * {@code Time = -ln(1 - U) / λ}
- * where U is a uniform random number [0, 1) and λ (lambda) is the
- * average arrival rate.
+ * Gera um intervalo de tempo seguindo uma Distribuição Exponencial.
+ *
+ * Este método implementa o algoritmo de Inverse Transform Sampling para simular
+ * um Processo de Poisson homogêneo. É fundamental para modelar a chegada natural de
+ * veículos, onde eventos independentes ocorrem a uma taxa média constante.
+ *
+ * Fórmula Matemática: {@code T = -ln(1 - U) / λ}
+ *
Onde:
+ *
+ * - {@code U}: Variável aleatória uniforme no intervalo [0, 1).
+ * - {@code λ (lambda)}: Taxa média de eventos por unidade de tempo (ex: veículos/segundo).
+ *
*
- * @param lambda The average arrival rate (λ) (e.g., 0.5 vehicles per second).
- * @return The time interval (in seconds) until the next arrival.
+ * @param lambda A taxa média de chegada (λ > 0).
+ * @return O intervalo de tempo (delta t) até o próximo evento, em segundos.
*/
public static double generateExponentialInterval(double lambda) {
- // Math.log is the natural logarithm (ln)
- // random.nextDouble() returns a value in [0.0, 1.0)
return Math.log(1 - random.nextDouble()) / -lambda;
}
/**
- * Returns a random integer between {@code min} and {@code max}, inclusive.
+ * Gera um número inteiro uniformemente distribuído no intervalo fechado {@code [min, max]}.
*
- * @param min The minimum possible value.
- * @param max The maximum possible value.
- * @return A random integer in the range [min, max].
+ * @param min Limite inferior (inclusivo).
+ * @param max Limite superior (inclusivo).
+ * @return Um inteiro aleatório I tal que {@code min <= I <= max}.
*/
public static int generateRandomInt(int min, int max) {
- // random.nextInt(N) returns a value from 0 to N-1
- // (max - min + 1) is the total number of integers in the range
- // + min offsets the range
return random.nextInt(max - min + 1) + min;
}
/**
- * Returns a random double between {@code min} (inclusive) and {@code max} (exclusive).
+ * Gera um número de ponto flutuante uniformemente distribuído no intervalo semi-aberto {@code [min, max)}.
*
- * @param min The minimum possible value.
- * @param max The maximum possible value.
- * @return A random double in the range [min, max).
+ * @param min Limite inferior (inclusivo).
+ * @param max Limite superior (exclusivo).
+ * @return Um double aleatório D tal que {@code min <= D < max}.
*/
public static double generateRandomDouble(double min, double max) {
return min + (max - min) * random.nextDouble();
}
/**
- * Returns {@code true} with a given probability.
- * * This is useful for making weighted decisions. For example,
- * {@code occursWithProbability(0.3)} will return {@code true}
- * approximately 30% of the time.
+ * Realiza um teste de Bernoulli (Sim/Não) com uma probabilidade de sucesso especificada.
+ *
+ * Utilizado para decisões de ramificação estocástica (ex: "Este veículo é um camião?").
*
- * @param probability A value between 0.0 (never) and 1.0 (always).
- * @return {@code true} or {@code false}, based on the probability.
+ * @param probability A probabilidade de retorno {@code true} (0.0 a 1.0).
+ * @return {@code true} se o evento ocorrer, {@code false} caso contrário.
*/
public static boolean occursWithProbability(double probability) {
return random.nextDouble() < probability;
}
/**
- * Picks a random element from the given array.
+ * Seleciona aleatoriamente um elemento de um array genérico (Amostragem Uniforme Discreta).
*
- * @param The generic type of the array.
- * @param array The array to choose from.
- * @return A randomly selected element from the array.
- * @throws IllegalArgumentException if the array is null or empty.
+ * @param O tipo dos elementos no array.
+ * @param array A população de onde escolher.
+ * @return O elemento selecionado.
+ * @throws IllegalArgumentException Se o array for nulo ou vazio.
*/
public static T chooseRandom(T[] array) {
if (array == null || array.length == 0) {
@@ -90,12 +94,13 @@ public class RandomGenerator {
}
/**
- * Sets the seed of the shared random number generator.
- * This is extremely useful for debugging and testing, as it allows
- * the simulation to be run multiple times with the *exact same*
- * sequence of "random" events, making the results reproducible.
+ * Reinicializa a semente (seed) do gerador global.
+ *
+ * Importância Crítica: Permite tornar a simulação determinística. Ao fixar a seed,
+ * a sequência de números "aleatórios" gerada será idêntica em execuções subsequentes,
+ * facilitando a depuração de race conditions ou lógica complexa.
*
- * @param seed The seed to use.
+ * @param seed O valor da semente inicial (ex: timestamp ou constante).
*/
public static void setSeed(long seed) {
random.setSeed(seed);
diff --git a/main/src/main/java/sd/util/StatisticsCollector.java b/main/src/main/java/sd/util/StatisticsCollector.java
deleted file mode 100644
index fa8f8bd..0000000
--- a/main/src/main/java/sd/util/StatisticsCollector.java
+++ /dev/null
@@ -1,379 +0,0 @@
-package sd.util;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import sd.config.SimulationConfig;
-import sd.model.Intersection;
-import sd.model.Vehicle;
-import sd.model.VehicleType;
-
-/**
- * Collects, manages, and reports statistics throughout the simulation.
- * * This class acts as the central bookkeeper for simulation metrics. It tracks:
- * - Overall system statistics (total vehicles, completion time, wait time).
- * - Per-vehicle-type statistics (counts, average wait time by type).
- * - Per-intersection statistics (arrivals, departures).
- * * It also maintains "in-flight" data, such as the arrival time of a
- * vehicle at its *current* intersection, which is necessary to
- * calculate waiting time when the vehicle later departs.
- */
-public class StatisticsCollector {
-
- // --- Vehicle tracking (for in-flight vehicles) ---
-
- /**
- * Tracks the simulation time when a vehicle arrives at its *current* intersection.
- * This is used later to calculate waiting time (Depart_Time - Arrive_Time).
- * Key: Vehicle ID (String)
- * Value: Arrival Time (Double)
- */
- private final Map vehicleArrivalTimes;
-
- /**
- * Tracks the sequence of intersections a vehicle has visited.
- * Key: Vehicle ID (String)
- * Value: List of Intersection IDs (String)
- */
- private final Map> vehicleIntersectionHistory;
-
- // --- Overall system statistics ---
-
- /** Total number of vehicles created by the {@link VehicleGenerator}. */
- private int totalVehiclesGenerated;
-
- /** Total number of vehicles that have reached their final destination ("S"). */
- private int totalVehiclesCompleted;
-
- /** The sum of all *completed* vehicles' total travel times. Used for averaging. */
- private double totalSystemTime;
-
- /** The sum of all *completed* vehicles' total waiting times. Used for averaging. */
- private double totalWaitingTime;
-
- // --- Per-vehicle-type statistics ---
-
- /**
- * Tracks the total number of vehicles generated, broken down by type.
- * Key: {@link VehicleType}
- * Value: Count (Integer)
- */
- private final Map vehicleTypeCount;
-
- /**
- * Tracks the total waiting time, broken down by vehicle type.
- * Key: {@link VehicleType}
- * Value: Total Wait Time (Double)
- */
- private final Map vehicleTypeWaitTime;
-
- // --- Per-intersection statistics ---
-
- /**
- * A map to hold statistics objects for each intersection.
- * Key: Intersection ID (String)
- * Value: {@link IntersectionStats} object
- */
- private final Map intersectionStats;
-
- /**
- * Constructs a new StatisticsCollector.
- * Initializes all maps and counters.
- *
- * @param config The {@link SimulationConfig} (not currently used, but
- * could be for configuration-dependent stats).
- */
- public StatisticsCollector(SimulationConfig config) {
- this.vehicleArrivalTimes = new HashMap<>();
- this.vehicleIntersectionHistory = new HashMap<>();
- this.totalVehiclesGenerated = 0;
- this.totalVehiclesCompleted = 0;
- this.totalSystemTime = 0.0;
- this.totalWaitingTime = 0.0;
- this.vehicleTypeCount = new HashMap<>();
- this.vehicleTypeWaitTime = new HashMap<>();
- this.intersectionStats = new HashMap<>();
-
- // Initialize vehicle type counters to 0
- for (VehicleType type : VehicleType.values()) {
- vehicleTypeCount.put(type, 0);
- vehicleTypeWaitTime.put(type, 0.0);
- }
- }
-
- /**
- * Records that a new vehicle has been generated.
- * This is called by the {@link sd.engine.SimulationEngine}
- * during a {@code VEHICLE_GENERATION} event.
- *
- * @param vehicle The {@link Vehicle} that was just created.
- * @param currentTime The simulation time of the event.
- */
- public void recordVehicleGeneration(Vehicle vehicle, double currentTime) {
- totalVehiclesGenerated++;
-
- // Track by vehicle type
- VehicleType type = vehicle.getType();
- vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
-
- // Initialize history tracking for this vehicle
- vehicleIntersectionHistory.put(vehicle.getId(), new ArrayList<>());
- }
-
- /**
- * Records that a vehicle has arrived at an intersection queue.
- * This is called by the {@link sd.engine.SimulationEngine}
- * during a {@code VEHICLE_ARRIVAL} event.
- *
- * @param vehicle The {@link Vehicle} that arrived.
- * @param intersectionId The ID of the intersection it arrived at.
- * @param currentTime The simulation time of the arrival.
- */
- public void recordVehicleArrival(Vehicle vehicle, String intersectionId, double currentTime) {
- // Store arrival time - this is the "start waiting" time
- vehicleArrivalTimes.put(vehicle.getId(), currentTime);
-
- // Track intersection history
- List history = vehicleIntersectionHistory.get(vehicle.getId());
- if (history != null) {
- history.add(intersectionId);
- }
-
- // Update per-intersection statistics
- getOrCreateIntersectionStats(intersectionId).recordArrival();
- }
-
- /**
- * Records that a vehicle has completed its route and exited the system.
- * This is where final metrics for the vehicle are aggregated.
- * This is called by the {@link sd.engine.SimulationEngine}
- * when a vehicle reaches destination "S".
- *
- * @param vehicle The {@link Vehicle} that is exiting.
- * @param currentTime The simulation time of the exit.
- */
- public void recordVehicleExit(Vehicle vehicle, double currentTime) {
- totalVehiclesCompleted++;
-
- // Calculate and aggregate total system time
- double systemTime = vehicle.getTotalTravelTime(currentTime);
- totalSystemTime += systemTime;
-
- // Aggregate waiting time
- double waitTime = vehicle.getTotalWaitingTime();
- totalWaitingTime += waitTime;
-
- // Aggregate waiting time by vehicle type
- VehicleType type = vehicle.getType();
- vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
-
- // Clean up tracking maps to save memory
- vehicleArrivalTimes.remove(vehicle.getId());
- vehicleIntersectionHistory.remove(vehicle.getId());
- }
-
- /**
- * Gets the time a vehicle arrived at its *current* intersection.
- * This is used by the {@link sd.engine.SimulationEngine} to calculate
- * wait time just before the vehicle crosses.
- *
- * @param vehicle The {@link Vehicle} to check.
- * @return The arrival time, or 0.0 if not found.
- */
- public double getArrivalTime(Vehicle vehicle) {
- return vehicleArrivalTimes.getOrDefault(vehicle.getId(), 0.0);
- }
-
- /**
- * Prints a "snapshot" of the current simulation statistics.
- * This is called periodically by the {@link sd.engine.SimulationEngine}
- * during a {@code STATISTICS_UPDATE} event.
- *
- * @param intersections A map of all intersections (to get queue data).
- * @param currentTime The current simulation time.
- */
- public void printCurrentStatistics(Map intersections, double currentTime) {
- System.out.printf("--- Statistics at t=%.2f ---%n", currentTime);
- System.out.printf("Vehicles: Generated=%d, Completed=%d, In-System=%d%n",
- totalVehiclesGenerated,
- totalVehiclesCompleted,
- totalVehiclesGenerated - totalVehiclesCompleted);
-
- if (totalVehiclesCompleted > 0) {
- System.out.printf("Average System Time (so far): %.2fs%n", totalSystemTime / totalVehiclesCompleted);
- System.out.printf("Average Waiting Time (so far): %.2fs%n", totalWaitingTime / totalVehiclesCompleted);
- }
-
- // Print per-intersection queue sizes
- System.out.println("\nIntersection Queues:");
- for (Map.Entry entry : intersections.entrySet()) {
- String id = entry.getKey();
- Intersection intersection = entry.getValue();
- System.out.printf(" %s: Queue=%d, Received=%d, Sent=%d%n",
- id,
- intersection.getTotalQueueSize(),
- intersection.getTotalVehiclesReceived(),
- intersection.getTotalVehiclesSent());
- }
- }
-
- /**
- * Prints the final simulation summary statistics at the end of the run.
- *
- * @param intersections A map of all intersections.
- * @param currentTime The final simulation time.
- */
- public void printFinalStatistics(Map intersections, double currentTime) {
- System.out.println("\n=== SIMULATION SUMMARY ===");
- System.out.printf("Duration: %.2f seconds%n", currentTime);
- System.out.printf("Total Vehicles Generated: %d%n", totalVehiclesGenerated);
- System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesCompleted);
- System.out.printf("Vehicles Still in System: %d%n", totalVehiclesGenerated - totalVehiclesCompleted);
-
- // Overall averages
- if (totalVehiclesCompleted > 0) {
- System.out.printf("%nAVERAGE METRICS (for completed vehicles):%n");
- System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesCompleted);
- System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesCompleted);
- System.out.printf(" Throughput: %.2f vehicles/second%n", totalVehiclesCompleted / currentTime);
- }
-
- // Vehicle type breakdown
- System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
- for (VehicleType type : VehicleType.values()) {
- int count = vehicleTypeCount.get(type);
- if (count > 0) {
- double percentage = (count * 100.0) / totalVehiclesGenerated;
- // Calculate avg wait *only* for this type
- // This assumes all generated vehicles of this type *completed*
- // A more accurate way would be to track completed vehicle types
- double avgWait = vehicleTypeWaitTime.get(type) / count;
- System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
- type, count, percentage, avgWait);
- }
- }
-
- // Per-intersection statistics
- System.out.println("\nINTERSECTION STATISTICS:");
- for (Map.Entry entry : intersections.entrySet()) {
- String id = entry.getKey();
- Intersection intersection = entry.getValue();
-
- System.out.printf(" %s:%n", id);
- System.out.printf(" Vehicles Received: %d%n", intersection.getTotalVehiclesReceived());
- System.out.printf(" Vehicles Sent: %d%n", intersection.getTotalVehiclesSent());
- System.out.printf(" Final Queue Size: %d%n", intersection.getTotalQueueSize());
-
- // Traffic light details
- intersection.getTrafficLights().forEach(light -> {
- System.out.printf(" Light %s: State=%s, Queue=%d, Processed=%d%n",
- light.getDirection(),
- light.getState(),
- light.getQueueSize(),
- light.getTotalVehiclesProcessed());
- });
- }
-
- // System health indicators
- System.out.println("\nSYSTEM HEALTH:");
- int totalQueuedVehicles = intersections.values().stream()
- .mapToInt(Intersection::getTotalQueueSize)
- .sum();
- System.out.printf(" Total Queued Vehicles (at end): %d%n", totalQueuedVehicles);
-
- if (totalVehiclesGenerated > 0) {
- double completionRate = (totalVehiclesCompleted * 100.0) / totalVehiclesGenerated;
- System.out.printf(" Completion Rate: %.1f%%%n", completionRate);
- }
- }
-
- /**
- * Gets or creates the statistics object for a given intersection.
- * Uses {@code computeIfAbsent} for efficient, thread-safe-like instantiation.
- *
- * @param intersectionId The ID of the intersection.
- * @return The {@link IntersectionStats} object for that ID.
- */
- private IntersectionStats getOrCreateIntersectionStats(String intersectionId) {
- // If 'intersectionId' is not in the map, create a new IntersectionStats()
- // and put it in the map, then return it.
- // Otherwise, just return the one that's already there.
- return intersectionStats.computeIfAbsent(intersectionId, k -> new IntersectionStats());
- }
-
- /**
- * Inner class to track per-intersection statistics.
- * This is a simple data holder.
- */
- private static class IntersectionStats {
- private int totalArrivals;
- private int totalDepartures;
-
- public IntersectionStats() {
- this.totalArrivals = 0;
- this.totalDepartures = 0;
- }
-
- public void recordArrival() {
- totalArrivals++;
- }
-
- public void recordDeparture() {
- totalDepartures++;
- }
-
- public int getTotalArrivals() {
- return totalArrivals;
- }
-
- public int getTotalDepartures() {
- return totalDepartures;
- }
- }
-
- // --- Public Getters for Final Statistics ---
-
- /**
- * @return Total vehicles generated during the simulation.
- */
- public int getTotalVehiclesGenerated() {
- return totalVehiclesGenerated;
- }
-
- /**
- * @return Total vehicles that completed their route.
- */
- public int getTotalVehiclesCompleted() {
- return totalVehiclesCompleted;
- }
-
- /**
- * @return The sum of all travel times for *completed* vehicles.
- */
- public double getTotalSystemTime() {
- return totalSystemTime;
- }
-
- /**
- * @return The sum of all waiting times for *completed* vehicles.
- */
- public double getTotalWaitingTime() {
- return totalWaitingTime;
- }
-
- /**
- * @return The average travel time for *completed* vehicles.
- */
- public double getAverageSystemTime() {
- return totalVehiclesCompleted > 0 ? totalSystemTime / totalVehiclesCompleted : 0.0;
- }
-
- /**
- * @return The average waiting time for *completed* vehicles.
- */
- public double getAverageWaitingTime() {
- return totalVehiclesCompleted > 0 ? totalWaitingTime / totalVehiclesCompleted : 0.0;
- }
-}
\ No newline at end of file
diff --git a/main/src/main/java/sd/util/VehicleGenerator.java b/main/src/main/java/sd/util/VehicleGenerator.java
index c6c7611..344b4d7 100644
--- a/main/src/main/java/sd/util/VehicleGenerator.java
+++ b/main/src/main/java/sd/util/VehicleGenerator.java
@@ -1,147 +1,116 @@
package sd.util;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import sd.config.SimulationConfig;
import sd.model.Vehicle;
import sd.model.VehicleType;
+import sd.routing.RouteSelector;
/**
- * Generates vehicles for the simulation.
- * * This class is responsible for two key tasks:
- * 1. Determining *when* the next vehicle should arrive, based on the
- * arrival model (POISSON or FIXED) from the {@link SimulationConfig}.
- * 2. Creating a new {@link Vehicle} object with a randomly selected
- * type (e.g., BIKE, LIGHT) and a randomly selected route.
- * * Routes are predefined and organized by entry point (E1, E2, E3).
+ * Motor de injeção de carga (Load Injector) para a simulação de tráfego.
+ *
+ * Esta classe atua como uma fábrica estocástica de veículos, sendo responsável por:
+ *
+ * - Modelagem Temporal: Determinar os instantes de chegada (Inter-arrival times)
+ * usando processos de Poisson (estocástico) ou intervalos determinísticos.
+ * - Caracterização da Entidade: Atribuir tipos de veículo (Bike, Light, Heavy)
+ * baseado numa Distribuição de Probabilidade Cumulativa (CDF).
+ * - Inicialização Espacial: Distribuir a carga uniformemente entre os pontos de entrada (E1-E3).
+ * - Atribuição de Rota: Delegar a escolha do percurso à estratégia {@link RouteSelector} ativa.
+ *
*/
public class VehicleGenerator {
private final SimulationConfig config;
private final String arrivalModel;
- private final double arrivalRate; // Lambda (λ) for POISSON
- private final double fixedInterval; // Interval for FIXED
- // --- Predefined Routes ---
- // These lists store all possible routes, grouped by where they start.
+ /** Parâmetro Lambda (λ) para a distribuição de Poisson (taxa de chegada). */
+ private final double arrivalRate;
- /** Routes starting from entry point E1. */
- private final List e1Routes;
- /** Routes starting from entry point E2. */
- private final List e2Routes;
- /** Routes starting from entry point E3. */
- private final List e3Routes;
+ /** Intervalo determinístico para geração constante (modo debug/teste). */
+ private final double fixedInterval;
+
+ /** * Estratégia de roteamento atual.
+ * Não é final para permitir Hot-Swapping durante a execução.
+ */
+ private RouteSelector routeSelector;
/**
- * Constructs a new VehicleGenerator.
- * It reads the necessary configuration and initializes the
- * predefined routes.
+ * Inicializa o gerador com as configurações de simulação e estratégia de roteamento.
*
- * @param config The {@link SimulationConfig} object.
+ * @param config A configuração global contendo as taxas e probabilidades.
+ * @param routeSelector A estratégia inicial de seleção de rotas.
*/
- public VehicleGenerator(SimulationConfig config) {
+ public VehicleGenerator(SimulationConfig config, RouteSelector routeSelector) {
this.config = config;
+ this.routeSelector = routeSelector;
- // Cache configuration values for performance
+ // Cache de valores de configuração para evitar lookups repetitivos em hot-path
this.arrivalModel = config.getArrivalModel();
this.arrivalRate = config.getArrivalRate();
this.fixedInterval = config.getFixedArrivalInterval();
-
- // Initialize route lists
- this.e1Routes = new ArrayList<>();
- this.e2Routes = new ArrayList<>();
- this.e3Routes = new ArrayList<>();
- initializePossibleRoutes();
}
/**
- * Defines all possible routes that vehicles can take, organized by
- * their entry point (E1, E2, E3). Each route is given a
- * probability, which determines how often it's chosen.
- */
- private void initializePossibleRoutes() {
- // E1 routes (Starts at Cr1)
- e1Routes.add(new RouteWithProbability(
- Arrays.asList("Cr1", "Cr4", "Cr5", "S"), 0.34)); // E1 -> Cr1 -> Cr4 -> Cr5 -> Exit
- e1Routes.add(new RouteWithProbability(
- Arrays.asList("Cr1", "Cr2", "Cr5", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr5 -> Exit
- e1Routes.add(new RouteWithProbability(
- Arrays.asList("Cr1", "Cr2", "Cr3", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr3 -> Exit
-
- // E2 routes (Starts at Cr2)
- e2Routes.add(new RouteWithProbability(
- Arrays.asList("Cr2", "Cr5", "S"), 0.34)); // E2 -> Cr2 -> Cr5 -> Exit
- e2Routes.add(new RouteWithProbability(
- Arrays.asList("Cr2", "Cr3", "S"), 0.33)); // E2 -> Cr2 -> Cr3 -> Exit
- e2Routes.add(new RouteWithProbability(
- Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E2 -> Cr2 -> ... -> Exit
-
- // E3 routes (Starts at Cr3)
- e3Routes.add(new RouteWithProbability(
- Arrays.asList("Cr3", "S"), 0.34)); // E3 -> Cr3 -> Exit
- e3Routes.add(new RouteWithProbability(
- Arrays.asList("Cr3", "Cr2", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> Cr2 -> Cr5 -> Exit
- e3Routes.add(new RouteWithProbability(
- Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> ... -> Exit
- }
-
- /**
- * Calculates the *absolute* time of the next vehicle arrival
- * based on the configured model.
- * * @param currentTime The current simulation time, used as the base.
- * @return The absolute time (e.g., {@code currentTime + interval})
- * when the next vehicle should be generated.
+ * Calcula o timestamp absoluto para a próxima injeção de veículo.
+ *
+ * Se o modelo for "POISSON", utiliza a técnica de Inverse Transform Sampling
+ * (via {@link RandomGenerator}) para gerar intervalos exponencialmente distribuídos,
+ * simulando a aleatoriedade natural do tráfego.
+ * * @param currentTime O tempo atual da simulação (base de cálculo).
+ * @return O instante futuro (t + delta) para agendamento do evento de geração.
*/
public double getNextArrivalTime(double currentTime) {
if ("POISSON".equalsIgnoreCase(arrivalModel)) {
- // For a Poisson process, the time *between* arrivals
- // follows an exponential distribution.
double interval = RandomGenerator.generateExponentialInterval(arrivalRate);
return currentTime + interval;
} else {
- // For a Fixed model, the interval is constant.
return currentTime + fixedInterval;
}
}
/**
- * Generates a new {@link Vehicle} object.
- * This involves:
- * 1. Selecting a random {@link VehicleType} based on probabilities.
- * 2. Selecting a random route (entry point + path) based on probabilities.
+ * Instancia (Spawn) um novo veículo configurado e roteado.
+ *
+ * O processo de criação segue um pipeline:
+ *
+ * - Seleção de Tipo (Roda da Fortuna / CDF).
+ * - Seleção de Entrada (Uniforme).
+ * - Cálculo de Rota (Delegado ao Strategy).
+ *
*
- * @param vehicleId The unique identifier for the new vehicle (e.g., "V123").
- * @param entryTime The simulation time when this vehicle is being created.
- * @return A new, configured {@link Vehicle} object.
+ * @param vehicleId O identificador único sequencial (ex: "V104").
+ * @param entryTime O timestamp de criação.
+ * @param queueSizes Snapshot atual das filas (usado apenas por estratégias dinâmicas como LEAST_CONGESTED).
+ * @return A entidade {@link Vehicle} pronta para inserção na malha.
*/
- public Vehicle generateVehicle(String vehicleId, double entryTime) {
+ public Vehicle generateVehicle(String vehicleId, double entryTime, Map queueSizes) {
VehicleType type = selectVehicleType();
- List route = selectRandomRoute();
+ String entryPoint = selectRandomEntryPoint();
+ List route = routeSelector.selectRoute(entryPoint, queueSizes);
return new Vehicle(vehicleId, type, entryTime, route);
}
/**
- * Selects a {@link VehicleType} (BIKE, LIGHT, HEAVY) based on the
- * probabilities defined in the {@link SimulationConfig}.
- * * Uses a standard "cumulative probability" technique:
- * 1. Get a random number {@code rand} from [0, 1).
- * 2. If {@code rand < P(Bike)}, return BIKE.
- * 3. Else if {@code rand < P(Bike) + P(Light)}, return LIGHT.
- * 4. Else, return HEAVY.
+ * Seleciona o tipo de veículo usando Amostragem por Probabilidade Cumulativa.
+ *
+ * Normaliza as probabilidades configuradas e mapeia um número aleatório [0, 1)
+ * para o intervalo correspondente ao tipo de veículo.
*
- * @return The selected {@link VehicleType}.
+ * @return O tipo enumerado {@link VehicleType} selecionado.
*/
private VehicleType selectVehicleType() {
double bikeProbability = config.getBikeVehicleProbability();
double lightProbability = config.getLightVehicleProbability();
double heavyProbability = config.getHeavyVehicleProbability();
- // Normalize probabilities in case they don't sum to exactly 1.0
double total = bikeProbability + lightProbability + heavyProbability;
- if (total == 0) return VehicleType.LIGHT; // Avoid division by zero
+ if (total == 0) return VehicleType.LIGHT; // Fallback de segurança
+
+ // Normalização
bikeProbability /= total;
lightProbability /= total;
@@ -157,73 +126,42 @@ public class VehicleGenerator {
}
/**
- * Selects a random route for a new vehicle.
- * This is a two-step process:
- * 1. Randomly select an entry point (E1, E2, or E3) with equal probability.
- * 2. From the chosen entry point's list of routes, select one
- * based on their defined probabilities (using cumulative probability).
+ * Seleciona um ponto de injeção na borda da rede (Edge Node).
+ * Distribuição Uniforme: ~33.3% para cada entrada (E1, E2, E3).
*
- * @return A {@link List} of strings representing the chosen route (e.g., ["Cr1", "Cr4", "S"]).
+ * @return O ID da interseção de entrada.
*/
- private List selectRandomRoute() {
- // Step 1: Randomly select an entry point (E1, E2, or E3)
- double entryRandom = Math.random();
- List selectedRoutes;
+ private String selectRandomEntryPoint() {
+ double rand = Math.random();
- if (entryRandom < 0.333) {
- selectedRoutes = e1Routes;
- } else if (entryRandom < 0.666) {
- selectedRoutes = e2Routes;
+ if (rand < 0.333) {
+ return "E1";
+ } else if (rand < 0.666) {
+ return "E2";
} else {
- selectedRoutes = e3Routes;
+ return "E3";
}
-
- // Step 2: Select a route from the chosen list based on cumulative probabilities
- double routeRand = Math.random();
- double cumulative = 0.0;
-
- for (RouteWithProbability routeWithProb : selectedRoutes) {
- cumulative += routeWithProb.probability;
- if (routeRand <= cumulative) {
- // Return a *copy* of the route to prevent modification
- return new ArrayList<>(routeWithProb.route);
- }
- }
-
- // Fallback: This should only be reached if probabilities don't sum to 1
- // (due to floating point errors)
- return new ArrayList<>(selectedRoutes.get(0).route);
}
/**
- * @return A string providing information about the generator's configuration.
+ * Atualiza a estratégia de roteamento em tempo de execução (Hot-Swap).
+ *
+ * Permite que o Coordenador altere o comportamento da frota (ex: de RANDOM para SHORTEST_PATH)
+ * sem necessidade de reiniciar a simulação.
+ * * @param newRouteSelector A nova implementação de estratégia a utilizar.
+ */
+ public void setRouteSelector(RouteSelector newRouteSelector) {
+ this.routeSelector = newRouteSelector;
+ }
+
+ /**
+ * Retorna uma representação textual do estado interno do gerador.
+ * Útil para logs de auditoria e debugging.
*/
public String getInfo() {
- int totalRoutes = e1Routes.size() + e2Routes.size() + e3Routes.size();
return String.format(
- "VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routes=%d (E1:%d, E2:%d, E3:%d)}",
- arrivalModel, arrivalRate, fixedInterval, totalRoutes,
- e1Routes.size(), e2Routes.size(), e3Routes.size()
+ "VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routeSelector=%s}",
+ arrivalModel, arrivalRate, fixedInterval, routeSelector.getClass().getSimpleName()
);
}
-
- /**
- * A private inner "struct-like" class to hold a route (a List of strings)
- * and its associated selection probability.
- */
- private static class RouteWithProbability {
- final List route;
- final double probability;
-
- /**
- * Constructs a new RouteWithProbability pair.
- * @param route The list of intersection IDs.
- * @param probability The probability (0.0 to 1.0) of this route
- * being chosen *from its entry group*.
- */
- RouteWithProbability(List route, double probability) {
- this.route = route;
- this.probability = probability;
- }
- }
}
\ No newline at end of file
diff --git a/main/src/main/resources/dashboard.css b/main/src/main/resources/dashboard.css
new file mode 100644
index 0000000..5cd7e57
--- /dev/null
+++ b/main/src/main/resources/dashboard.css
@@ -0,0 +1,142 @@
+/* Global Styles */
+.root {
+ -fx-background-color: #f4f7f6;
+ -fx-font-family: 'Segoe UI', sans-serif;
+}
+
+/* Header */
+.header {
+ -fx-background-color: linear-gradient(to right, #2c3e50, #4ca1af);
+ -fx-padding: 20;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 10, 0, 0, 5);
+}
+
+.header-title {
+ -fx-font-size: 28px;
+ -fx-font-weight: bold;
+ -fx-text-fill: white;
+}
+
+.header-subtitle {
+ -fx-font-size: 16px;
+ -fx-text-fill: #ecf0f1;
+}
+
+/* Buttons */
+.button-start {
+ -fx-background-color: #2ecc71;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-padding: 10 20;
+ -fx-background-radius: 5;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 5, 0, 0, 2);
+}
+
+.button-start:hover {
+ -fx-background-color: #27ae60;
+}
+
+.button-start:disabled {
+ -fx-background-color: #95a5a6;
+ -fx-opacity: 0.7;
+}
+
+.button-stop {
+ -fx-background-color: #e74c3c;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-padding: 10 20;
+ -fx-background-radius: 5;
+ -fx-cursor: hand;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 5, 0, 0, 2);
+}
+
+.button-stop:hover {
+ -fx-background-color: #c0392b;
+}
+
+.button-stop:disabled {
+ -fx-background-color: #95a5a6;
+ -fx-opacity: 0.7;
+}
+
+/* Cards / Panels */
+.card {
+ -fx-background-color: white;
+ -fx-background-radius: 8;
+ -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.05), 10, 0, 0, 2);
+ -fx-padding: 0;
+}
+
+.card-header {
+ -fx-background-color: #ecf0f1;
+ -fx-background-radius: 8 8 0 0;
+ -fx-padding: 10 15;
+ -fx-border-color: #bdc3c7;
+ -fx-border-width: 0 0 1 0;
+}
+
+.card-title {
+ -fx-font-size: 16px;
+ -fx-font-weight: bold;
+ -fx-text-fill: #2c3e50;
+}
+
+.card-content {
+ -fx-padding: 15;
+}
+
+/* Statistics Grid */
+.stat-label {
+ -fx-font-size: 14px;
+ -fx-text-fill: #7f8c8d;
+}
+
+.stat-value {
+ -fx-font-size: 20px;
+ -fx-font-weight: bold;
+ -fx-text-fill: #2980b9;
+}
+
+/* Tables */
+.table-view {
+ -fx-background-color: transparent;
+ -fx-border-color: transparent;
+}
+
+.table-view .column-header-background {
+ -fx-background-color: #ecf0f1;
+ -fx-border-color: #bdc3c7;
+ -fx-border-width: 0 0 1 0;
+}
+
+.table-view .column-header .label {
+ -fx-text-fill: #2c3e50;
+ -fx-font-weight: bold;
+}
+
+.table-row-cell {
+ -fx-background-color: white;
+ -fx-border-color: transparent;
+}
+
+.table-row-cell:odd {
+ -fx-background-color: #f9f9f9;
+}
+
+.table-row-cell:selected {
+ -fx-background-color: #3498db;
+ -fx-text-fill: white;
+}
+
+/* Footer */
+.footer {
+ -fx-background-color: #34495e;
+ -fx-padding: 10 20;
+}
+
+.footer-text {
+ -fx-text-fill: #ecf0f1;
+ -fx-font-size: 12px;
+}
diff --git a/main/src/main/resources/network_config.json b/main/src/main/resources/network_config.json
new file mode 100644
index 0000000..f83f4a3
--- /dev/null
+++ b/main/src/main/resources/network_config.json
@@ -0,0 +1,46 @@
+{
+ "intersections": [
+ {
+ "id": "Cr1",
+ "lights": ["East", "South"],
+ "routes": {
+ "Cr2": "East",
+ "Cr4": "South"
+ }
+ },
+ {
+ "id": "Cr2",
+ "lights": ["West", "East", "South"],
+ "routes": {
+ "Cr1": "West",
+ "Cr3": "East",
+ "Cr5": "South"
+ }
+ },
+ {
+ "id": "Cr3",
+ "lights": ["West", "South"],
+ "routes": {
+ "Cr2": "West",
+ "S": "South"
+ }
+ },
+ {
+ "id": "Cr4",
+ "lights": ["East", "North"],
+ "routes": {
+ "Cr1": "North",
+ "Cr5": "East"
+ }
+ },
+ {
+ "id": "Cr5",
+ "lights": ["East", "West", "North"],
+ "routes": {
+ "Cr2": "North",
+ "Cr4": "West",
+ "S": "East"
+ }
+ }
+ ]
+}
diff --git a/main/src/main/resources/simulation-high.properties b/main/src/main/resources/simulation-high.properties
new file mode 100644
index 0000000..5c47079
--- /dev/null
+++ b/main/src/main/resources/simulation-high.properties
@@ -0,0 +1,126 @@
+# =========================================================
+# Traffic Simulation Configuration - HIGH LOAD SCENARIO
+# ---------------------------------------------------------
+# High traffic scenario for testing system under heavy load.
+# Expected: Significant congestion, large queues, system stress test
+# =========================================================
+
+# === NETWORK CONFIGURATION ===
+
+# Intersections (each with its host and port)
+intersection.Cr1.host=localhost
+intersection.Cr1.port=8001
+intersection.Cr2.host=localhost
+intersection.Cr2.port=8002
+intersection.Cr3.host=localhost
+intersection.Cr3.port=8003
+intersection.Cr4.host=localhost
+intersection.Cr4.port=8004
+intersection.Cr5.host=localhost
+intersection.Cr5.port=8005
+
+# Exit node
+exit.host=localhost
+exit.port=9001
+
+# Dashboard server
+dashboard.host=localhost
+dashboard.port=9000
+
+
+# === SIMULATION CONFIGURATION ===
+
+# Total duration in seconds (1800 = 30 minutes)
+simulation.duration=1800
+
+# Vehicle arrival model: FIXED or POISSON
+simulation.arrival.model=POISSON
+
+# λ (lambda): HIGH LOAD = 1.0 vehicle per second (60 vehicles/minute, 3600 vehicles/hour)
+# This is 2x medium load - tests system capacity limits
+simulation.arrival.rate=1.0
+
+# Fixed interval between arrivals (only used if model=FIXED)
+simulation.arrival.fixed.interval=2.0
+
+# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED
+simulation.routing.policy=LEAST_CONGESTED
+
+
+# === TRAFFIC LIGHT TIMINGS ===
+# Format: trafficlight...=
+# Aggressive timings to maximize throughput under high load
+
+# Intersection 1 (Entry point - longer greens to prevent early backup)
+trafficlight.Cr1.South.green=60.0
+trafficlight.Cr1.South.red=3.0
+trafficlight.Cr1.East.green=60.0
+trafficlight.Cr1.East.red=3.0
+
+# Intersection 2 (Main hub - CRITICAL BOTTLENECK, maximum green times)
+# This is the most critical intersection - all routes converge here
+trafficlight.Cr2.South.green=70.0
+trafficlight.Cr2.South.red=3.0
+trafficlight.Cr2.East.green=80.0
+trafficlight.Cr2.East.red=3.0
+trafficlight.Cr2.West.green=70.0
+trafficlight.Cr2.West.red=3.0
+
+# Intersection 3 (Path to exit - maximize East throughput to exit)
+trafficlight.Cr3.South.green=50.0
+trafficlight.Cr3.South.red=3.0
+trafficlight.Cr3.West.green=40.0
+trafficlight.Cr3.West.red=3.0
+
+# Intersection 4 (High throughput needed toward Cr5)
+trafficlight.Cr4.East.green=70.0
+trafficlight.Cr4.East.red=3.0
+trafficlight.Cr4.North.green=70.0
+trafficlight.Cr4.North.red=3.0
+
+# Intersection 5 (Near exit - MAJOR BOTTLENECK, longest green time)
+# All routes funnel through here before exit
+trafficlight.Cr5.East.green=90.0
+trafficlight.Cr5.East.red=3.0
+trafficlight.Cr5.West.green=70.0
+trafficlight.Cr5.West.red=3.0
+trafficlight.Cr5.North.green=70.0
+trafficlight.Cr5.North.red=3.0
+
+
+# === VEHICLE CONFIGURATION ===
+# Probability distribution for vehicle types (must sum to 1.0)
+vehicle.probability.bike=0.2
+vehicle.probability.light=0.6
+vehicle.probability.heavy=0.2
+
+# Average crossing times (in seconds)
+vehicle.crossing.time.bike=1.0
+vehicle.crossing.time.light=2.0
+vehicle.crossing.time.heavy=4.0
+
+# Travel times between intersections (in seconds)
+# Base time for light vehicles (cars)
+vehicle.travel.time.base=1.0
+# Bike travel time = 0.5 x car travel time
+vehicle.travel.time.bike.multiplier=0.5
+# Heavy vehicle travel time = 4.0 x base travel time
+vehicle.travel.time.heavy.multiplier=4.0
+
+# === STATISTICS ===
+
+# Interval between dashboard updates (seconds)
+statistics.update.interval=10.0
+
+# === EXPECTED BEHAVIOR - HIGH LOAD ===
+# - Average system time: 200-400+ seconds (3-7+ minutes)
+# - Maximum queue sizes: 15-30+ vehicles at Cr2 and Cr5
+# - Average queue sizes: 8-15+ vehicles
+# - Severe congestion at Cr2 (main convergence point)
+# - Severe congestion at Cr5 (pre-exit bottleneck)
+# - System utilization: ~80-95%
+# - Many vehicles will remain in system at simulation end
+# - Queue growth may be unbounded if arrival rate exceeds service rate
+# - Primary bottlenecks: Cr2 (3-way convergence) and Cr5 (final funnel)
+# - This scenario tests maximum system capacity and traffic light optimization
+# - Expected to demonstrate need for adaptive traffic light policies
diff --git a/main/src/main/resources/simulation-low.properties b/main/src/main/resources/simulation-low.properties
new file mode 100644
index 0000000..05eee1b
--- /dev/null
+++ b/main/src/main/resources/simulation-low.properties
@@ -0,0 +1,120 @@
+# =========================================================
+# Traffic Simulation Configuration - LOW LOAD SCENARIO
+# ---------------------------------------------------------
+# Low traffic scenario for testing system under light load.
+# Expected: No congestion, minimal queues, fast vehicle throughput
+# =========================================================
+
+# === NETWORK CONFIGURATION ===
+
+# Intersections (each with its host and port)
+intersection.Cr1.host=localhost
+intersection.Cr1.port=8001
+intersection.Cr2.host=localhost
+intersection.Cr2.port=8002
+intersection.Cr3.host=localhost
+intersection.Cr3.port=8003
+intersection.Cr4.host=localhost
+intersection.Cr4.port=8004
+intersection.Cr5.host=localhost
+intersection.Cr5.port=8005
+
+# Exit node
+exit.host=localhost
+exit.port=9001
+
+# Dashboard server
+dashboard.host=localhost
+dashboard.port=9000
+
+
+# === SIMULATION CONFIGURATION ===
+
+# Total duration in seconds (1800 = 30 minutes)
+simulation.duration=1800
+
+# Vehicle arrival model: FIXED or POISSON
+simulation.arrival.model=POISSON
+
+# λ (lambda): LOW LOAD = 0.2 vehicles per second (12 vehicles/minute, 720 vehicles/hour)
+# This is approximately 40% of medium load
+simulation.arrival.rate=0.2
+
+# Fixed interval between arrivals (only used if model=FIXED)
+simulation.arrival.fixed.interval=2.0
+
+# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED
+simulation.routing.policy=LEAST_CONGESTED
+
+
+# === TRAFFIC LIGHT TIMINGS ===
+# Format: trafficlight...=
+# Standard timings - should be more than adequate for low load
+
+# Intersection 1 (Entry point - balanced)
+trafficlight.Cr1.South.green=30.0
+trafficlight.Cr1.South.red=5.0
+trafficlight.Cr1.East.green=30.0
+trafficlight.Cr1.East.red=5.0
+
+# Intersection 2 (Main hub - shorter cycles, favor East-West)
+trafficlight.Cr2.South.green=30.0
+trafficlight.Cr2.South.red=5.0
+trafficlight.Cr2.East.green=30.0
+trafficlight.Cr2.East.red=5.0
+trafficlight.Cr2.West.green=30.0
+trafficlight.Cr2.West.red=5.0
+
+# Intersection 3 (Path to exit - favor East)
+trafficlight.Cr3.South.green=30.0
+trafficlight.Cr3.South.red=5.0
+trafficlight.Cr3.West.green=30.0
+trafficlight.Cr3.West.red=5.0
+
+# Intersection 4 (Favor East toward Cr5)
+trafficlight.Cr4.East.green=30.0
+trafficlight.Cr4.East.red=5.0
+trafficlight.Cr4.North.green=30.0
+trafficlight.Cr4.North.red=5.0
+
+# Intersection 5 (Near exit - favor East)
+trafficlight.Cr5.East.green=30.0
+trafficlight.Cr5.East.red=5.0
+trafficlight.Cr5.West.green=30.0
+trafficlight.Cr5.West.red=5.0
+trafficlight.Cr5.North.green=30.0
+trafficlight.Cr5.North.red=5.0
+
+
+# === VEHICLE CONFIGURATION ===
+# Probability distribution for vehicle types (must sum to 1.0)
+vehicle.probability.bike=0.2
+vehicle.probability.light=0.6
+vehicle.probability.heavy=0.2
+
+# Average crossing times (in seconds)
+vehicle.crossing.time.bike=1.0
+vehicle.crossing.time.light=2.0
+vehicle.crossing.time.heavy=4.0
+
+# Travel times between intersections (in seconds)
+# Base time for light vehicles (cars)
+vehicle.travel.time.base=1.0
+# Bike travel time = 0.5 x car travel time
+vehicle.travel.time.bike.multiplier=0.5
+# Heavy vehicle travel time = 4.0 x base travel time
+vehicle.travel.time.heavy.multiplier=4.0
+
+# === STATISTICS ===
+
+# Interval between dashboard updates (seconds)
+statistics.update.interval=10.0
+
+# === EXPECTED BEHAVIOR - LOW LOAD ===
+# - Average system time: 40-80 seconds
+# - Maximum queue sizes: 1-3 vehicles
+# - Average queue sizes: < 1 vehicle
+# - Vehicles should flow smoothly through the system
+# - Minimal waiting at traffic lights (mostly travel time)
+# - System utilization: ~20-30%
+# - All vehicles should exit within simulation time
diff --git a/main/src/main/resources/simulation-medium.properties b/main/src/main/resources/simulation-medium.properties
new file mode 100644
index 0000000..9dbe8e1
--- /dev/null
+++ b/main/src/main/resources/simulation-medium.properties
@@ -0,0 +1,121 @@
+# =========================================================
+# Traffic Simulation Configuration - MEDIUM LOAD SCENARIO
+# ---------------------------------------------------------
+# Medium traffic scenario for testing system under normal load.
+# Expected: Moderate queues, some congestion at peak intersections
+# =========================================================
+
+# === NETWORK CONFIGURATION ===
+
+# Intersections (each with its host and port)
+intersection.Cr1.host=localhost
+intersection.Cr1.port=8001
+intersection.Cr2.host=localhost
+intersection.Cr2.port=8002
+intersection.Cr3.host=localhost
+intersection.Cr3.port=8003
+intersection.Cr4.host=localhost
+intersection.Cr4.port=8004
+intersection.Cr5.host=localhost
+intersection.Cr5.port=8005
+
+# Exit node
+exit.host=localhost
+exit.port=9001
+
+# Dashboard server
+dashboard.host=localhost
+dashboard.port=9000
+
+
+# === SIMULATION CONFIGURATION ===
+
+# Total duration in seconds (1800 = 30 minutes)
+simulation.duration=1800
+
+# Vehicle arrival model: FIXED or POISSON
+simulation.arrival.model=POISSON
+
+# λ (lambda): MEDIUM LOAD = 0.5 vehicles per second (30 vehicles/minute, 1800 vehicles/hour)
+# This represents normal traffic conditions
+simulation.arrival.rate=0.5
+
+# Fixed interval between arrivals (only used if model=FIXED)
+simulation.arrival.fixed.interval=2.0
+
+# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED
+simulation.routing.policy=LEAST_CONGESTED
+
+
+# === TRAFFIC LIGHT TIMINGS ===
+# Format: trafficlight...=
+# Optimized timings for medium load
+
+# Intersection 1 (Entry point - balanced)
+trafficlight.Cr1.South.green=40.0
+trafficlight.Cr1.South.red=5.0
+trafficlight.Cr1.East.green=40.0
+trafficlight.Cr1.East.red=5.0
+
+# Intersection 2 (Main hub - CRITICAL BOTTLENECK, longer green times)
+trafficlight.Cr2.South.green=45.0
+trafficlight.Cr2.South.red=5.0
+trafficlight.Cr2.East.green=50.0
+trafficlight.Cr2.East.red=5.0
+trafficlight.Cr2.West.green=45.0
+trafficlight.Cr2.West.red=5.0
+
+# Intersection 3 (Path to exit - favor East toward exit)
+trafficlight.Cr3.South.green=40.0
+trafficlight.Cr3.South.red=5.0
+trafficlight.Cr3.West.green=35.0
+trafficlight.Cr3.West.red=5.0
+
+# Intersection 4 (Favor East toward Cr5)
+trafficlight.Cr4.East.green=40.0
+trafficlight.Cr4.East.red=5.0
+trafficlight.Cr4.North.green=40.0
+trafficlight.Cr4.North.red=5.0
+
+# Intersection 5 (Near exit - POTENTIAL BOTTLENECK, longer green)
+trafficlight.Cr5.East.green=50.0
+trafficlight.Cr5.East.red=5.0
+trafficlight.Cr5.West.green=45.0
+trafficlight.Cr5.West.red=5.0
+trafficlight.Cr5.North.green=45.0
+trafficlight.Cr5.North.red=5.0
+
+
+# === VEHICLE CONFIGURATION ===
+# Probability distribution for vehicle types (must sum to 1.0)
+vehicle.probability.bike=0.2
+vehicle.probability.light=0.6
+vehicle.probability.heavy=0.2
+
+# Average crossing times (in seconds)
+vehicle.crossing.time.bike=1.0
+vehicle.crossing.time.light=2.0
+vehicle.crossing.time.heavy=4.0
+
+# Travel times between intersections (in seconds)
+# Base time for light vehicles (cars)
+vehicle.travel.time.base=1.0
+# Bike travel time = 0.5 x car travel time
+vehicle.travel.time.bike.multiplier=0.5
+# Heavy vehicle travel time = 4.0 x base travel time
+vehicle.travel.time.heavy.multiplier=4.0
+
+# === STATISTICS ===
+
+# Interval between dashboard updates (seconds)
+statistics.update.interval=10.0
+
+# === EXPECTED BEHAVIOR - MEDIUM LOAD ===
+# - Average system time: 80-150 seconds
+# - Maximum queue sizes: 5-10 vehicles at Cr2 and Cr5
+# - Average queue sizes: 2-5 vehicles
+# - Moderate congestion at Cr2 (main hub) and Cr5 (pre-exit)
+# - System utilization: ~50-60%
+# - Most vehicles should exit, some may remain at simulation end
+# - Cr2 is the primary bottleneck (3 directions converge)
+# - Cr5 is secondary bottleneck (all routes pass through)
diff --git a/main/src/main/resources/simulation.properties b/main/src/main/resources/simulation.properties
index 278ef08..012c6dd 100644
--- a/main/src/main/resources/simulation.properties
+++ b/main/src/main/resources/simulation.properties
@@ -31,7 +31,11 @@ dashboard.port=9000
# === SIMULATION CONFIGURATION ===
# Total duration in seconds (3600 = 1 hour)
-simulation.duration=60.0
+simulation.duration=300
+
+# Time scaling factor for visualization (real_seconds = sim_seconds * scale)
+# 0 = instant (pure DES), 0.01 = 100x speed, 0.1 = 10x speed, 1.0 = real-time
+simulation.time.scale=0.01
# Vehicle arrival model: FIXED or POISSON
simulation.arrival.model=POISSON
@@ -42,59 +46,44 @@ simulation.arrival.rate=0.5
# Fixed interval between arrivals (only used if model=FIXED)
simulation.arrival.fixed.interval=2.0
+# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED
+# RANDOM: selects routes with predefined probabilities (baseline)
+# SHORTEST_PATH: always chooses the route with fewest intersections
+# LEAST_CONGESTED: dynamically chooses routes to avoid congested areas
+simulation.routing.policy=RANDOM
+
# === TRAFFIC LIGHT TIMINGS ===
# Format: trafficlight...=
-# Intersection 1
-trafficlight.Cr1.North.green=30.0
-trafficlight.Cr1.North.red=30.0
-trafficlight.Cr1.South.green=30.0
-trafficlight.Cr1.South.red=30.0
-trafficlight.Cr1.East.green=30.0
-trafficlight.Cr1.East.red=30.0
-trafficlight.Cr1.West.green=30.0
-trafficlight.Cr1.West.red=30.0
+# Intersection 1 (Entry point - balanced)
+trafficlight.Cr1.South.green=60.0
+trafficlight.Cr1.South.red=5.0
+trafficlight.Cr1.East.green=60.0
+trafficlight.Cr1.East.red=5.0
-# Intersection 2
-trafficlight.Cr2.North.green=25.0
-trafficlight.Cr2.North.red=35.0
-trafficlight.Cr2.South.green=25.0
-trafficlight.Cr2.South.red=35.0
-trafficlight.Cr2.East.green=35.0
-trafficlight.Cr2.East.red=25.0
-trafficlight.Cr2.West.green=35.0
-trafficlight.Cr2.West.red=25.0
+# Intersection 2 (Main hub - shorter cycles, favor East-West)
+trafficlight.Cr2.South.green=60.0
+trafficlight.Cr2.South.red=5.0
+trafficlight.Cr2.East.green=60.0
+trafficlight.Cr2.East.red=5.0
+trafficlight.Cr2.West.green=60.0
+trafficlight.Cr2.West.red=5.0
-# Intersection 3
-trafficlight.Cr3.North.green=30.0
-trafficlight.Cr3.North.red=30.0
-trafficlight.Cr3.South.green=30.0
-trafficlight.Cr3.South.red=30.0
-trafficlight.Cr3.East.green=30.0
-trafficlight.Cr3.East.red=30.0
-trafficlight.Cr3.West.green=30.0
-trafficlight.Cr3.West.red=30.0
+# Intersection 3 (Path to exit - favor East)
+trafficlight.Cr3.South.green=60.0
+trafficlight.Cr3.South.red=5.0
+trafficlight.Cr3.West.green=60.0
+trafficlight.Cr3.West.red=5.0
-# Intersection 4
-trafficlight.Cr4.North.green=30.0
-trafficlight.Cr4.North.red=30.0
-trafficlight.Cr4.South.green=30.0
-trafficlight.Cr4.South.red=30.0
-trafficlight.Cr4.East.green=30.0
-trafficlight.Cr4.East.red=30.0
-trafficlight.Cr4.West.green=30.0
-trafficlight.Cr4.West.red=30.0
+# Intersection 4 (Favor East toward Cr5)
+trafficlight.Cr4.East.green=60.0
+trafficlight.Cr4.East.red=5.0
+
+# Intersection 5 (Near exit - favor East)
+trafficlight.Cr5.East.green=60.0
+trafficlight.Cr5.East.red=5.0
-# Intersection 5
-trafficlight.Cr5.North.green=30.0
-trafficlight.Cr5.North.red=30.0
-trafficlight.Cr5.South.green=30.0
-trafficlight.Cr5.South.red=30.0
-trafficlight.Cr5.East.green=30.0
-trafficlight.Cr5.East.red=30.0
-trafficlight.Cr5.West.green=30.0
-trafficlight.Cr5.West.red=30.0
# === VEHICLE CONFIGURATION ===
# Probability distribution for vehicle types (must sum to 1.0)
@@ -103,11 +92,19 @@ vehicle.probability.light=0.6
vehicle.probability.heavy=0.2
# Average crossing times (in seconds)
-vehicle.crossing.time.bike=1.5
+vehicle.crossing.time.bike=1.0
vehicle.crossing.time.light=2.0
vehicle.crossing.time.heavy=4.0
+# Travel times between intersections (in seconds)
+# Base time for light vehicles (cars)
+vehicle.travel.time.base=1.0
+# Bike travel time = 0.5 x car travel time
+vehicle.travel.time.bike.multiplier=0.5
+# Heavy vehicle travel time = 4.0 x base travel time
+vehicle.travel.time.heavy.multiplier=4.0
+
# === STATISTICS ===
# Interval between dashboard updates (seconds)
-statistics.update.interval=10.0
+statistics.update.interval=0.1
diff --git a/main/src/test/java/SimulationTest.java b/main/src/test/java/SimulationTest.java
deleted file mode 100644
index b3a49df..0000000
--- a/main/src/test/java/SimulationTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-import java.io.IOException;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import org.junit.jupiter.api.Test;
-
-import sd.config.SimulationConfig;
-import sd.engine.SimulationEngine;
-import sd.model.Event;
-import sd.model.EventType;
-import sd.model.Intersection;
-import sd.model.TrafficLight;
-import sd.model.TrafficLightState;
-import sd.model.Vehicle;
-import sd.model.VehicleType;
-import sd.util.StatisticsCollector;
-import sd.util.VehicleGenerator;
-
-/**
- * Basic tests for the simulation components.
- */
-class SimulationTest {
-
- @Test
- void testConfigurationLoading() throws IOException {
- SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
-
- assertEquals(60.0, config.getSimulationDuration());
- assertEquals("POISSON", config.getArrivalModel());
- assertEquals(0.5, config.getArrivalRate());
- assertEquals(10.0, config.getStatisticsUpdateInterval());
- }
-
- @Test
- void testVehicleGeneration() throws IOException {
- SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
- VehicleGenerator generator = new VehicleGenerator(config);
-
- Vehicle vehicle = generator.generateVehicle("TEST1", 0.0);
-
- assertNotNull(vehicle);
- assertEquals("TEST1", vehicle.getId());
- assertNotNull(vehicle.getType());
- assertNotNull(vehicle.getRoute());
- assertTrue(!vehicle.getRoute().isEmpty());
- }
-
- @Test
- void testEventOrdering() {
- Event e1 = new Event(5.0, EventType.VEHICLE_ARRIVAL, null, "Cr1");
- Event e2 = new Event(3.0, EventType.VEHICLE_ARRIVAL, null, "Cr2");
- Event e3 = new Event(7.0, EventType.TRAFFIC_LIGHT_CHANGE, null, "Cr1");
-
- assertTrue(e2.compareTo(e1) < 0); // e2 should come before e1
- assertTrue(e1.compareTo(e3) < 0); // e1 should come before e3
- }
-
- @Test
- void testIntersectionVehicleQueue() {
- Intersection intersection = new Intersection("TestCr");
- TrafficLight light = new TrafficLight("TestCr-N", "North", 30.0, 30.0);
-
- intersection.addTrafficLight(light);
-
- Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
- java.util.Arrays.asList("TestCr", "S"));
-
- intersection.configureRoute("S", "North");
-
- // Advance route to next destination
- v1.advanceRoute();
-
- intersection.receiveVehicle(v1);
-
- assertEquals(1, intersection.getTotalQueueSize());
- assertEquals(1, intersection.getTotalVehiclesReceived());
- }
-
- @Test
- void testTrafficLightStateChange() {
- TrafficLight light = new TrafficLight("Test-Light", "North", 30.0, 30.0);
-
- assertEquals(TrafficLightState.RED, light.getState());
-
- light.changeState(TrafficLightState.GREEN);
- assertEquals(TrafficLightState.GREEN, light.getState());
-
- light.changeState(TrafficLightState.RED);
- assertEquals(TrafficLightState.RED, light.getState());
- }
-
- @Test
- void testSimulationEngineInitialization() throws IOException {
- SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
- SimulationEngine engine = new SimulationEngine(config);
-
- engine.initialize();
-
- assertNotNull(engine.getIntersections());
- assertEquals(5, engine.getIntersections().size());
-
- // Check that intersections have traffic lights
- for (Intersection intersection : engine.getIntersections().values()) {
- assertEquals(3, intersection.getTrafficLights().size()); // North, South, East, West
- }
- }
-
- @Test
- void testStatisticsCollector() throws IOException {
- SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
- StatisticsCollector collector = new StatisticsCollector(config);
-
- Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
- java.util.Arrays.asList("Cr1", "Cr2", "S"));
-
- collector.recordVehicleGeneration(v1, 0.0);
- assertEquals(1, collector.getTotalVehiclesGenerated());
-
- collector.recordVehicleArrival(v1, "Cr1", 1.0);
-
- collector.recordVehicleExit(v1, 10.0);
- assertEquals(1, collector.getTotalVehiclesCompleted());
- }
-}
diff --git a/main/src/test/java/sd/serialization/SerializationTest.java b/main/src/test/java/sd/serialization/SerializationTest.java
deleted file mode 100644
index b43b5a5..0000000
--- a/main/src/test/java/sd/serialization/SerializationTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package sd.serialization;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.DisplayName;
-import sd.model.Message;
-import sd.model.Vehicle;
-import sd.model.VehicleType;
-
-import java.util.Arrays;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * Test suite for JSON serialization.
- *
- * Tests JSON serialization to ensure:
- * - Correct serialization and deserialization
- * - Data integrity during round-trip conversion
- * - Proper error handling
- */
-class SerializationTest {
-
- private MessageSerializer jsonSerializer = new JsonMessageSerializer();
-
- private Vehicle testVehicle = new Vehicle("V001", VehicleType.LIGHT, 10.5,
- Arrays.asList("Cr1", "Cr2", "Cr5", "S"));
- private Message testMessage = new Message(
- sd.model.MessageType.VEHICLE_TRANSFER,
- "Cr1",
- "Cr2",
- testVehicle
- );
-
-
- // ===== JSON Serialization Tests =====
-
- @Test
- @DisplayName("JSON: Should serialize and deserialize Vehicle correctly")
- void testJsonVehicleRoundTrip() throws SerializationException {
- // Serialize
- byte[] data = jsonSerializer.serialize(testVehicle);
- assertNotNull(data);
- assertTrue(data.length > 0);
-
- // Print JSON for inspection
- System.out.println("JSON Vehicle:");
- System.out.println(new String(data));
-
- // Deserialize
- Vehicle deserialized = jsonSerializer.deserialize(data, Vehicle.class);
-
- // Verify
- assertNotNull(deserialized);
- assertEquals(testVehicle.getId(), deserialized.getId());
- assertEquals(testVehicle.getType(), deserialized.getType());
- assertEquals(testVehicle.getEntryTime(), deserialized.getEntryTime());
- assertEquals(testVehicle.getRoute(), deserialized.getRoute());
- assertEquals(testVehicle.getTotalWaitingTime(), deserialized.getTotalWaitingTime());
- assertEquals(testVehicle.getTotalCrossingTime(), deserialized.getTotalCrossingTime());
- }
-
- @Test
- @DisplayName("JSON: Should serialize and deserialize Message correctly")
- void testJsonMessageRoundTrip() throws SerializationException {
- // Serialize
- byte[] data = jsonSerializer.serialize(testMessage);
- assertNotNull(data);
-
- // Print JSON for inspection
- System.out.println("\nJSON Message:");
- System.out.println(new String(data));
-
- // Deserialize
- Message deserialized = jsonSerializer.deserialize(data, Message.class);
-
- // Verify
- assertNotNull(deserialized);
- assertEquals(testMessage.getType(), deserialized.getType());
- assertEquals(testMessage.getSenderId(), deserialized.getSenderId());
- assertEquals(testMessage.getDestinationId(), deserialized.getDestinationId());
- }
-
- @Test
- @DisplayName("JSON: Should throw exception on null object")
- void testJsonSerializeNull() {
- assertThrows(IllegalArgumentException.class, () -> {
- jsonSerializer.serialize(null);
- });
- }
-
- @Test
- @DisplayName("JSON: Should throw exception on null data")
- void testJsonDeserializeNull() {
- assertThrows(IllegalArgumentException.class, () -> {
- jsonSerializer.deserialize(null, Vehicle.class);
- });
- }
-
- @Test
- @DisplayName("JSON: Should throw exception on invalid JSON")
- void testJsonDeserializeInvalid() {
- byte[] invalidData = "{ invalid json }".getBytes();
- assertThrows(SerializationException.class, () -> {
- jsonSerializer.deserialize(invalidData, Vehicle.class);
- });
- }
-
- @Test
- @DisplayName("JSON: Should preserve data integrity for complex objects")
- void testDataIntegrity() throws SerializationException {
- // Create a more complex vehicle
- Vehicle vehicle = new Vehicle("V999", VehicleType.HEAVY, 100.5,
- Arrays.asList("Cr1", "Cr2", "Cr3", "Cr4", "Cr5", "S"));
- vehicle.addWaitingTime(10.5);
- vehicle.addWaitingTime(5.3);
- vehicle.addCrossingTime(2.1);
- vehicle.advanceRoute();
- vehicle.advanceRoute();
-
- // Serialize and deserialize
- byte[] jsonData = jsonSerializer.serialize(vehicle);
- Vehicle deserialized = jsonSerializer.deserialize(jsonData, Vehicle.class);
-
- // Verify all fields match
- assertEquals(vehicle.getId(), deserialized.getId());
- assertEquals(vehicle.getType(), deserialized.getType());
- assertEquals(vehicle.getTotalWaitingTime(), deserialized.getTotalWaitingTime());
- assertEquals(vehicle.getCurrentRouteIndex(), deserialized.getCurrentRouteIndex());
- }
-
- // ===== Factory Tests =====
-
- @Test
- @DisplayName("Factory: Should create JSON serializer by default")
- void testFactoryDefault() {
- MessageSerializer serializer = SerializerFactory.createDefault();
- assertNotNull(serializer);
- assertEquals("JSON (Gson)", serializer.getName());
- }
-}
diff --git a/main/testing.txt b/main/testing.txt
new file mode 100644
index 0000000..6636918
--- /dev/null
+++ b/main/testing.txt
@@ -0,0 +1,1055 @@
+[INFO] Scanning for projects...
+[INFO]
+[INFO] ------------------------------< sd:main >-------------------------------
+[INFO] Building main 1.0-SNAPSHOT
+[INFO] from pom.xml
+[INFO] --------------------------------[ jar ]---------------------------------
+[WARNING] 6 problems were encountered while building the effective model for org.openjfx:javafx-controls:jar:17.0.2 during dependency collection step for project (use -X to see details)
+[INFO]
+[INFO] --- resources:3.3.1:resources (default-resources) @ main ---
+[INFO] Copying 2 resources from src/main/resources to target/classes
+[INFO]
+[INFO] --- compiler:3.13.0:compile (default-compile) @ main ---
+[INFO] Nothing to compile - all classes are up to date.
+[INFO]
+[INFO] --- resources:3.3.1:testResources (default-testResources) @ main ---
+[INFO] skip non existing resourceDirectory /home/leo/uni/SD/Trabalho-Pratico-SD/main/src/test/resources
+[INFO]
+[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ main ---
+[INFO] Nothing to compile - all classes are up to date.
+[INFO]
+[INFO] --- surefire:3.2.5:test (default-test) @ main ---
+[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
+[INFO]
+[INFO] -------------------------------------------------------
+[INFO] T E S T S
+[INFO] -------------------------------------------------------
+[INFO] Running sd.coordinator.CoordinatorIntegrationTest
+Mock Cr1 listening on port 9001
+Connected to Cr1 at localhost:9001
+Mock Cr1 received: VEHICLE_SPAWN
+Mock Cr1 stopped
+Mock Cr1 listening on port 9001
+Connected to Cr1 at localhost:9001
+Mock Cr1 stopped
+Mock Cr1 listening on port 9001
+Connected to Cr1 at localhost:9001
+Mock Cr1 received: VEHICLE_SPAWN
+Mock Cr1 stopped
+Mock Cr1 listening on port 9001
+Mock Cr2 listening on port 9002
+Mock Cr3 listening on port 9003
+Connected to Cr1 at localhost:9001
+Connected to Cr2 at localhost:9002
+Connected to Cr3 at localhost:9003
+Mock Cr1 received: SHUTDOWN
+Mock Cr2 received: SHUTDOWN
+Mock Cr3 received: SHUTDOWN
+Mock Cr1 stopped
+Mock Cr2 stopped
+Mock Cr3 stopped
+[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.265 s -- in sd.coordinator.CoordinatorIntegrationTest
+[INFO] Running sd.coordinator.CoordinatorProcessTest
+Coordinator initialized with configuration:
+ - Simulation duration: 60.0s
+ - Arrival model: POISSON
+ - Arrival rate: 0.5 vehicles/s
+[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 s -- in sd.coordinator.CoordinatorProcessTest
+[INFO] Running sd.dashboard.DashboardTest
+[INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.020 s -- in sd.dashboard.DashboardTest
+[INFO] Running sd.serialization.SerializationTest
+JSON Vehicle:
+{"id":"V001","type":"LIGHT","entryTime":10.5,"route":["Cr1","Cr2","Cr5","S"],"currentRouteIndex":0,"totalWaitingTime":0.0,"totalCrossingTime":0.0}
+
+JSON Message:
+{"messageId":"2ee10daa-34c4-4629-9613-bfc4fbd03e46","type":"VEHICLE_TRANSFER","senderId":"Cr1","destinationId":"Cr2","payload":{"id":"V001","type":"LIGHT","entryTime":10.5,"route":["Cr1","Cr2","Cr5","S"],"currentRouteIndex":0,"totalWaitingTime":0.0,"totalCrossingTime":0.0},"timestamp":1763852220055}
+[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.019 s -- in sd.serialization.SerializationTest
+[INFO] Running sd.ExitNodeProcessTest
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+Exit node started on port 19001
+Waiting for vehicles...\n
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+Exit node started on port 19001
+Waiting for vehicles...\n
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+Exit node started on port 19001
+Waiting for vehicles...\n
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+Exit node started on port 19001
+Waiting for vehicles...\n
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+New connection accepted from 127.0.0.1
+[Exit] Connection closed from 127.0.0.1
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+Exit node started on port 19001
+Waiting for vehicles...\n
+New connection accepted from 127.0.0.1
+[Exit] Waiting for message from 127.0.0.1
+New connection accepted from 127.0.0.1
+[Exit] Waiting for message from 127.0.0.1
+New connection accepted from 127.0.0.1
+[Exit] Waiting for message from 127.0.0.1
+[Exit] Waiting for message from 127.0.0.1
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: [Exit] Connection closed from 127.0.0.1
+0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Connection closed from 127.0.0.1
+[Exit] Connection closed from 127.0.0.1
+[Exit] Shutdown complete.
+============================================================
+Exit node initialized
+ - Exit port: 19001
+ - Dashboard: localhost:19000
+Connecting to dashboard...
+
+[Exit] Shutting down...
+
+=== EXIT NODE STATISTICS ===
+Total Vehicles Completed: 0
+
+VEHICLE TYPE DISTRIBUTION:
+[Exit] Shutdown complete.
+============================================================
+[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.273 s -- in sd.ExitNodeProcessTest
+[INFO] Running sd.TrafficLightCoordinationTest
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 18.0s, Red: 30.0s)
+ Created traffic light: East (Green: 18.0s, Red: 30.0s)
+ Created traffic light: South (Green: 12.0s, Red: 36.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:9000...
+[Cr2] Initialization complete.
+
+=== Testing Traffic Light Fairness ===
+
+[Cr2] Server started on port 8002
+
+[Cr2] Starting traffic light threads...
+ Started thread for: West
+ Started thread for: South
+[Cr2-West] Traffic light thread started.
+ Started thread for: East
+[Cr2-South] Traffic light thread started.
+[Cr2-West] State: GREEN
+[Cr2-East] Traffic light thread started.
+[Cr2] Waiting for incoming connections...
+
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+✓ West has been GREEN
+
+=== Fairness Results ===
+West got GREEN time: ✓ YES
+South got GREEN time: ✗ NO
+East got GREEN time: ✗ NO
+
+1/3 lights were GREEN during test period
+
+[Cr2] Shutting down...
+[Cr2-South] State: GREEN
+[Cr2-East] State: GREEN
+[Cr2-West] Traffic light thread interrupted.
+[Cr2-South] Traffic light thread stopped.
+[Cr2-East] Traffic light thread stopped.
+[Cr2-West] Traffic light thread stopped.
+[Cr2] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 18.0s, Red: 30.0s)
+ Created traffic light: East (Green: 18.0s, Red: 30.0s)
+ Created traffic light: South (Green: 12.0s, Red: 36.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:9000...
+[Cr2] Initialization complete.
+
+=== Testing Traffic Light Mutual Exclusion ===
+
+[Cr2] Server started on port 8002
+
+[Cr2] Starting traffic light threads...
+ Started thread for: West
+ Started thread for: South
+[Cr2-West] Traffic light thread started.
+[Cr2-West] State: GREEN
+ Started thread for: East
+[Cr2-South] Traffic light thread started.
+[Cr2] Waiting for incoming connections...
+
+[Cr2-East] Traffic light thread started.
+
+=== Test Results ===
+Maximum simultaneous GREEN lights: 1
+Total violations detected: 0
+
+Traffic light coordination working correctly!
+
+[Cr2] Shutting down...
+[Cr2-West] Traffic light thread interrupted.
+[Cr2-South] State: GREEN
+[Cr2-West] Traffic light thread stopped.
+[Cr2-East] State: GREEN
+[Cr2-South] Traffic light thread stopped.
+[Cr2-East] Traffic light thread stopped.
+[Cr2] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 18.0s, Red: 30.0s)
+ Created traffic light: East (Green: 18.0s, Red: 30.0s)
+ Created traffic light: South (Green: 12.0s, Red: 36.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:9000...
+[Cr2] Initialization complete.
+
+=== Testing State Transition Consistency ===
+
+[Cr2] Server started on port 8002
+
+[Cr2] Starting traffic light threads...
+ Started thread for: West
+ Started thread for: South
+[Cr2-South] Traffic light thread started.
+[Cr2-South] State: GREEN
+[Cr2-West] Traffic light thread started.
+ Started thread for: East
+[Cr2] Waiting for incoming connections...
+
+[Cr2-East] Traffic light thread started.
+South transitioned: RED → GREEN
+
+Total state transitions observed: 1
+
+[Cr2] Shutting down...
+[Cr2-South] Traffic light thread interrupted.
+[Cr2-South] Traffic light thread stopped.
+[Cr2-West] State: GREEN
+[Cr2-West] Traffic light thread stopped.
+[Cr2-East] State: GREEN
+[Cr2-East] Traffic light thread stopped.
+[Cr2] Shutdown complete.
+============================================================
+
+[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 33.14 s -- in sd.TrafficLightCoordinationTest
+[INFO] Running IntersectionProcessTest
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+
+[Cr1] Server started on port 18001
+
+[Cr1] Starting traffic light threads...
+ Started thread for: South
+ Started thread for: East
+[Cr1-South] Traffic light thread started.
+[Cr1] Waiting for incoming connections...
+
+[Cr1-South] State: GREEN
+[Cr1-East] Traffic light thread started.
+
+[Cr1] Shutting down...
+[Cr1] New connection accepted from 127.0.0.1
+[Cr1-South] Traffic light thread interrupted.
+[Cr1-East] State: GREEN
+[Cr1-East] Traffic light thread stopped.
+[Cr1-South] Traffic light thread stopped.
+[Cr1] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+
+[Cr1] Server started on port 18001
+
+[Cr1] Starting traffic light threads...
+ Started thread for: South
+ Started thread for: East
+[Cr1-South] Traffic light thread started.
+[Cr1] Waiting for incoming connections...
+
+[Cr1-South] State: GREEN
+[Cr1-East] Traffic light thread started.
+
+[Cr1] Shutting down...
+[Cr1-South] Traffic light thread interrupted.
+[Cr1-South] Traffic light thread stopped.
+[Cr1-East] State: GREEN
+[Cr1-East] Traffic light thread stopped.
+[Cr1] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 30.0s, Red: 30.0s)
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:18100...
+[Cr2] Initialization complete.
+
+[Cr1] Server started on port 18001
+
+[Cr1] Starting traffic light threads...
+
+[Cr2] Server started on port 18002
+
+[Cr2] Starting traffic light threads...
+ Started thread for: South
+ Started thread for: West
+[Cr1-South] Traffic light thread started.
+[Cr1-South] State: GREEN
+ Started thread for: East
+[Cr1] Waiting for incoming connections...
+
+[Cr1-East] Traffic light thread started.
+[Cr2-West] Traffic light thread started.
+ Started thread for: South
+[Cr2-West] State: GREEN
+[Cr2-South] Traffic light thread started.
+ Started thread for: East
+[Cr2] Waiting for incoming connections...
+
+[Cr2-East] Traffic light thread started.
+[Cr1] New connection accepted from 127.0.0.1
+[Cr1] New connection accepted from 127.0.0.1
+
+[Cr1] Shutting down...
+[Cr1-South] Traffic light thread interrupted.
+[Cr1-South] Traffic light thread stopped.
+[Cr1-East] State: GREEN
+[Cr1-East] Traffic light thread stopped.
+[Cr1] Shutdown complete.
+============================================================
+
+
+[Cr2] Shutting down...
+[Cr2-West] Traffic light thread interrupted.
+[Cr2-South] State: GREEN
+[Cr2-West] Traffic light thread stopped.
+[Cr2-South] Traffic light thread stopped.
+[Cr2-East] State: GREEN
+[Cr2-East] Traffic light thread stopped.
+[Cr2] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+
+[Cr1] Server started on port 18001
+
+[Cr1] Starting traffic light threads...
+ Started thread for: South
+[Cr1-South] Traffic light thread started.
+ Started thread for: East
+[Cr1-South] State: GREEN
+[Cr1] Waiting for incoming connections...
+
+[Cr1-East] Traffic light thread started.
+
+[Cr1] Shutting down...
+[Cr1-South] Traffic light thread interrupted.
+[Cr1-South] Traffic light thread stopped.
+[Cr1-East] State: GREEN
+[Cr1-East] Traffic light thread stopped.
+[Cr1] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr4
+============================================================
+
+[Cr4] Initializing intersection...
+
+[Cr4] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+
+[Cr4] Configuring routing...
+ Route configured: To Cr5 -> Use East
+ Routing configured.
+[Cr4] Connecting to dashboard at localhost:18100...
+[Cr4] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 30.0s, Red: 30.0s)
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:18100...
+[Cr2] Initialization complete.
+
+[Cr2] Server started on port 18002
+
+[Cr2] Starting traffic light threads...
+ Started thread for: West
+ Started thread for: South
+[Cr2-West] Traffic light thread started.
+ Started thread for: East
+[Cr2-West] State: GREEN
+[Cr2] Waiting for incoming connections...
+
+[Cr2-South] Traffic light thread started.
+[Cr2-East] Traffic light thread started.
+[Cr2] New connection accepted from 127.0.0.1
+[Cr2] New connection accepted from 127.0.0.1
+
+[Cr2] Shutting down...
+[Cr2-South] State: GREEN
+[Cr2-South] Traffic light thread stopped.
+[Cr2-West] Traffic light thread interrupted.
+[Cr2-West] Traffic light thread stopped.
+[Cr2-East] State: GREEN
+[Cr2-East] Traffic light thread stopped.
+[Cr2] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr5
+============================================================
+
+[Cr5] Initializing intersection...
+
+[Cr5] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+
+[Cr5] Configuring routing...
+ Route configured: To S -> Use East
+ Routing configured.
+[Cr5] Connecting to dashboard at localhost:18100...
+[Cr5] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+
+[Cr1] Server started on port 18001
+
+[Cr1] Starting traffic light threads...
+ Started thread for: South
+ Started thread for: East
+[Cr1] Waiting for incoming connections...
+
+[Cr1-South] Traffic light thread started.
+[Cr1-South] State: GREEN
+[Cr1-East] Traffic light thread started.
+
+[Cr1] Shutting down...
+[Cr1-South] Traffic light thread interrupted.
+[Cr1-South] Traffic light thread stopped.
+[Cr1-East] State: GREEN
+[Cr1-East] Traffic light thread stopped.
+[Cr1] Shutdown complete.
+============================================================
+
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr3
+============================================================
+
+[Cr3] Initializing intersection...
+
+[Cr3] Creating traffic lights...
+ Created traffic light: West (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr3] Configuring routing...
+ Route configured: To Cr2 -> Use West
+ Route configured: To S -> Use South
+ Routing configured.
+[Cr3] Connecting to dashboard at localhost:18100...
+[Cr3] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 30.0s, Red: 30.0s)
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:18100...
+[Cr2] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr3
+============================================================
+
+[Cr3] Initializing intersection...
+
+[Cr3] Creating traffic lights...
+ Created traffic light: West (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr3] Configuring routing...
+ Route configured: To Cr2 -> Use West
+ Route configured: To S -> Use South
+ Routing configured.
+[Cr3] Connecting to dashboard at localhost:18100...
+[Cr3] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr4
+============================================================
+
+[Cr4] Initializing intersection...
+
+[Cr4] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+
+[Cr4] Configuring routing...
+ Route configured: To Cr5 -> Use East
+ Routing configured.
+[Cr4] Connecting to dashboard at localhost:18100...
+[Cr4] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr5
+============================================================
+
+[Cr5] Initializing intersection...
+
+[Cr5] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+
+[Cr5] Configuring routing...
+ Route configured: To S -> Use East
+ Routing configured.
+[Cr5] Connecting to dashboard at localhost:18100...
+[Cr5] Initialization complete.
+============================================================
+INTERSECTION PROCESS: Cr1
+============================================================
+============================================================
+INTERSECTION PROCESS: Cr2
+============================================================
+
+[Cr1] Initializing intersection...
+
+[Cr1] Creating traffic lights...
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr1] Configuring routing...
+ Route configured: To Cr2 -> Use East
+ Route configured: To Cr4 -> Use South
+ Routing configured.
+[Cr1] Connecting to dashboard at localhost:18100...
+[Cr1] Initialization complete.
+
+[Cr2] Initializing intersection...
+
+[Cr2] Creating traffic lights...
+ Created traffic light: West (Green: 30.0s, Red: 30.0s)
+ Created traffic light: East (Green: 30.0s, Red: 30.0s)
+ Created traffic light: South (Green: 30.0s, Red: 30.0s)
+
+[Cr2] Configuring routing...
+ Route configured: To Cr1 -> Use West
+ Route configured: To Cr3 -> Use East
+ Route configured: To Cr5 -> Use South
+ Routing configured.
+[Cr2] Connecting to dashboard at localhost:18100...
+[Cr2] Initialization complete.
+
+[Cr1] Server started on port 18001
+
+[Cr1] Starting traffic light threads...
+
+[Cr2] Server started on port 18002
+
+[Cr2] Starting traffic light threads...
+ Started thread for: South
+ Started thread for: West
+[Cr1-South] Traffic light thread started.
+[Cr1-South] State: GREEN
+ Started thread for: East
+[Cr1] Waiting for incoming connections...
+
+[Cr2-West] Traffic light thread started.
+[Cr2-West] State: GREEN
+[Cr1-East] Traffic light thread started.
+ Started thread for: South
+[Cr2-South] Traffic light thread started.
+ Started thread for: East
+[Cr2] Waiting for incoming connections...
+
+[Cr2-East] Traffic light thread started.
+[Cr1] New connection accepted from 127.0.0.1
+[Cr2] New connection accepted from 127.0.0.1
+
+[Cr1] Shutting down...
+[Cr1-South] Traffic light thread interrupted.
+[Cr1-South] Traffic light thread stopped.
+[Cr1-East] State: GREEN
+[Cr1-East] Traffic light thread stopped.
+[Cr1] New connection accepted from 127.0.0.1
+[Cr2] New connection accepted from 127.0.0.1
+[Cr1] Shutdown complete.
+============================================================
+
+
+[Cr2] Shutting down...
+[Cr2-West] Traffic light thread interrupted.
+[Cr2-West] Traffic light thread stopped.
+[Cr2-South] State: GREEN
+[Cr2-South] Traffic light thread stopped.
+[Cr2-East] State: GREEN
+[Cr2-East] Traffic light thread stopped.
+[Cr2] Shutdown complete.
+============================================================
+
+[INFO] Tests run: 20, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.406 s -- in IntersectionProcessTest
+[INFO] Running SimulationTest
+[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.014 s -- in SimulationTest
+[INFO]
+[INFO] Results:
+[INFO]
+[INFO] Tests run: 66, Failures: 0, Errors: 0, Skipped: 0
+[INFO]
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 44.392 s
+[INFO] Finished at: 2025-11-22T22:57:41Z
+[INFO] ------------------------------------------------------------------------