mirror of
https://github.com/davidalves04/Trabalho-Pratico-SD.git
synced 2025-12-08 20:43:32 +00:00
Compare commits
15 Commits
12-impleme
...
18-design-
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fdcf376b2 | |||
|
|
ecb70fa6a2 | ||
| 06f079ce5b | |||
| 72893f87ae | |||
| 6b94d727e2 | |||
| 84cba39597 | |||
| 5dc1b40c88 | |||
| 3117bdf332 | |||
| 1140c3ca48 | |||
|
|
484cba1eee | ||
|
|
0e5526c3f6 | ||
|
|
cf88db4297 | ||
|
|
0960a7a141 | ||
|
|
3b4f968a59 | ||
|
|
4710c96450 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ build/
|
|||||||
# Other
|
# Other
|
||||||
*.swp
|
*.swp
|
||||||
*.pdf
|
*.pdf
|
||||||
|
|
||||||
|
# JAR built pom file
|
||||||
|
dependency-reduced-pom.xml
|
||||||
21
main/pom.xml
21
main/pom.xml
@@ -29,6 +29,18 @@
|
|||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>2.10.1</version>
|
<version>2.10.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JavaFX for UI -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
<version>17.0.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-fxml</artifactId>
|
||||||
|
<version>17.0.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -42,6 +54,15 @@
|
|||||||
<mainClass>sd.Entry</mainClass>
|
<mainClass>sd.Entry</mainClass>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<!-- JavaFX Maven Plugin -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
|
<version>0.0.8</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>sd.dashboard.DashboardUI</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
|||||||
426
main/src/main/java/sd/ExitNodeProcess.java
Normal file
426
main/src/main/java/sd/ExitNodeProcess.java
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
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.model.Message;
|
||||||
|
import sd.model.MessageType;
|
||||||
|
import sd.model.Vehicle;
|
||||||
|
import sd.model.VehicleType;
|
||||||
|
import sd.protocol.MessageProtocol;
|
||||||
|
import sd.protocol.SocketConnection;
|
||||||
|
import sd.serialization.SerializationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processo responsável pelo nó de saída do sistema de simulação de tráfego distribuído.
|
||||||
|
*
|
||||||
|
* Este processo representa o ponto final ("S") onde os veículos completam as suas rotas.
|
||||||
|
* As suas principais responsabilidades são:
|
||||||
|
* - Receber veículos que terminam a sua rota vindos das interseções
|
||||||
|
* - Calcular e agregar estatísticas finais dos veículos
|
||||||
|
* - Enviar estatísticas periódicas para o dashboard
|
||||||
|
* - Gerar relatórios finais ao terminar a simulação
|
||||||
|
*/
|
||||||
|
public class ExitNodeProcess {
|
||||||
|
|
||||||
|
private final SimulationConfig config;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
private final ExecutorService connectionHandlerPool;
|
||||||
|
|
||||||
|
/** Flag para controlar a execução do processo (volatile para visibilidade entre threads) */
|
||||||
|
private volatile boolean running;
|
||||||
|
|
||||||
|
/** Simulation start time (milliseconds) to calculate relative times */
|
||||||
|
private long simulationStartMillis;
|
||||||
|
|
||||||
|
/** Counter de veículos que completaram a rota */
|
||||||
|
private int totalVehiclesReceived;
|
||||||
|
|
||||||
|
/** Soma dos tempos no sistema de todos os veículos */
|
||||||
|
private double totalSystemTime;
|
||||||
|
|
||||||
|
/** Soma dos tempos de espera de todos os veículos */
|
||||||
|
private double totalWaitingTime;
|
||||||
|
|
||||||
|
/** Soma dos tempos de travessia de todos os veículos */
|
||||||
|
private double totalCrossingTime;
|
||||||
|
|
||||||
|
/** Contagem de veículos por tipo */
|
||||||
|
private final Map<VehicleType, Integer> vehicleTypeCount;
|
||||||
|
|
||||||
|
/** Tempo total de espera acumulado por tipo de veículo */
|
||||||
|
private final Map<VehicleType, Double> vehicleTypeWaitTime;
|
||||||
|
|
||||||
|
/** Socket para comunicação com o dashboard */
|
||||||
|
private SocketClient dashboardClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Método para iniciar o processo
|
||||||
|
*
|
||||||
|
* @param args Argumentos da linha de comandos. Se fornecido, args[0] deve ser
|
||||||
|
* o caminho para um ficheiro de configuração personalizado.
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
System.out.println("EXIT NODE PROCESS");
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
|
||||||
|
try {
|
||||||
|
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());
|
||||||
|
System.exit(1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Exit node error: " + e.getMessage());
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constrói um novo processo de nó de saída.
|
||||||
|
*
|
||||||
|
* Inicializa todas as estruturas de dados necessárias para recolher estatísticas
|
||||||
|
* e configura o pool de threads para processar as ligações concorrentes.
|
||||||
|
*
|
||||||
|
* @param config Configuração da simulação contendo portas e endereços dos serviços
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Exit node initialized");
|
||||||
|
System.out.println(" - Exit port: " + config.getExitPort());
|
||||||
|
System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicializa o processo de ligação ao dashboard.
|
||||||
|
*
|
||||||
|
* Tenta conectar-se ao dashboard. Se a ligação falhar, o processo
|
||||||
|
* continua a funcionar normalmente, mas sem enviar estatísticas.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
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 o socket e começa a aceitar ligações.
|
||||||
|
*
|
||||||
|
* Este é o loop principal do processo que:
|
||||||
|
* 1. Cria um socket na porta definida
|
||||||
|
* 2. Aguarda pelas ligações das interseções
|
||||||
|
* 3. Delega cada ligação a uma thread da pool para processamento assíncrono
|
||||||
|
*
|
||||||
|
* @throws IOException Se o socket não puder ser criado ou houver erro na aceitação
|
||||||
|
*/
|
||||||
|
public void start() throws IOException {
|
||||||
|
int port = config.getExitPort();
|
||||||
|
serverSocket = new ServerSocket(port);
|
||||||
|
running = true;
|
||||||
|
simulationStartMillis = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("Exit node started on port " + port);
|
||||||
|
System.out.println("Waiting for vehicles...\\n");
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running) {
|
||||||
|
System.err.println("Error accepting connection: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processa uma ligação recebida de uma interseção.
|
||||||
|
*
|
||||||
|
* Mantém a ligação aberta e processa continuamente mensagens do tipo
|
||||||
|
* VEHICLE_TRANSFER. Cada mensagem representa um veículo que chegou ao nó de saída.
|
||||||
|
*
|
||||||
|
* @param clientSocket Socket da ligação estabelecida com a interseção
|
||||||
|
*/
|
||||||
|
private void handleIncomingConnection(Socket clientSocket) {
|
||||||
|
String clientAddress = clientSocket.getInetAddress().getHostAddress();
|
||||||
|
System.out.println("New connection accepted from " + clientAddress);
|
||||||
|
|
||||||
|
try (SocketConnection connection = new SocketConnection(clientSocket)) {
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Coordinator sends start time - use it instead of our local start
|
||||||
|
simulationStartMillis = ((Number) message.getPayload()).longValue();
|
||||||
|
System.out.println("[Exit] Simulation start time synchronized");
|
||||||
|
} else if (message.getType() == MessageType.VEHICLE_TRANSFER) {
|
||||||
|
Object payload = message.getPayload();
|
||||||
|
System.out.println("[Exit] Payload type: " + payload.getClass().getName());
|
||||||
|
|
||||||
|
// Handle Gson LinkedHashMap
|
||||||
|
Vehicle vehicle;
|
||||||
|
if (payload instanceof com.google.gson.internal.LinkedTreeMap ||
|
||||||
|
payload instanceof java.util.LinkedHashMap) {
|
||||||
|
String json = new com.google.gson.Gson().toJson(payload);
|
||||||
|
vehicle = new com.google.gson.Gson().fromJson(json, Vehicle.class);
|
||||||
|
} else {
|
||||||
|
vehicle = (Vehicle) payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
processExitingVehicle(vehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
System.err.println("[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 um veículo que chegou ao nó de saída.
|
||||||
|
*
|
||||||
|
* Método sincronizado para garantir thread-safety ao atualizar as estatísticas.
|
||||||
|
* Calcula as métricas finais do veículo e atualiza:
|
||||||
|
* - Counters globais;
|
||||||
|
* - Estatísticas por tipo de veículo;
|
||||||
|
* - Faz update ao dashboard a cada 10 veículos.
|
||||||
|
*
|
||||||
|
* @param vehicle Veículo que completou a sua rota
|
||||||
|
*/
|
||||||
|
private synchronized void processExitingVehicle(Vehicle vehicle) {
|
||||||
|
totalVehiclesReceived++;
|
||||||
|
|
||||||
|
// Calculate relative simulation time (seconds since simulation start)
|
||||||
|
double currentSimTime = (System.currentTimeMillis() - simulationStartMillis) / 1000.0;
|
||||||
|
// System time = time vehicle spent in system (current time - entry time)
|
||||||
|
double systemTime = currentSimTime - vehicle.getEntryTime();
|
||||||
|
double waitTime = vehicle.getTotalWaitingTime();
|
||||||
|
double crossingTime = vehicle.getTotalCrossingTime();
|
||||||
|
|
||||||
|
// Store times in seconds, will be converted to ms when sending to dashboard
|
||||||
|
totalSystemTime += systemTime;
|
||||||
|
totalWaitingTime += waitTime;
|
||||||
|
totalCrossingTime += crossingTime;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Send stats after every vehicle to ensure dashboard updates quickly
|
||||||
|
sendStatsToDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtém o tempo atual da simulação em segundos.
|
||||||
|
*
|
||||||
|
* @return Tempo atual em segundos desde "epoch"
|
||||||
|
*
|
||||||
|
* "Epoch" é um ponto de referência temporal Unix (1 de janeiro de 1970).
|
||||||
|
* Este método retorna os segundos decorridos desde esse momento.
|
||||||
|
*/
|
||||||
|
private double getCurrentTime() {
|
||||||
|
return System.currentTimeMillis() / 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia as estatísticas para o dashboard.
|
||||||
|
*
|
||||||
|
* Prepara e envia uma mensagem STATS_UPDATE com:
|
||||||
|
* - O total de veículos processados;
|
||||||
|
* - A média dos tempos (sistema, espera, travessia);
|
||||||
|
* - As contagens e médias por cada tipo de veículo.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private void sendStatsToDashboard() {
|
||||||
|
if (dashboardClient == null || !dashboardClient.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create stats payload
|
||||||
|
StatsUpdatePayload payload = new StatsUpdatePayload();
|
||||||
|
|
||||||
|
// Set global stats - convert seconds to milliseconds
|
||||||
|
payload.setTotalVehiclesCompleted(totalVehiclesReceived);
|
||||||
|
payload.setTotalSystemTime((long)(totalSystemTime * 1000.0)); // s -> ms
|
||||||
|
payload.setTotalWaitingTime((long)(totalWaitingTime * 1000.0)); // s -> ms
|
||||||
|
|
||||||
|
// Set vehicle type stats
|
||||||
|
Map<VehicleType, Integer> typeCounts = new HashMap<>();
|
||||||
|
Map<VehicleType, Long> typeWaitTimes = new HashMap<>();
|
||||||
|
|
||||||
|
for (VehicleType type : VehicleType.values()) {
|
||||||
|
typeCounts.put(type, vehicleTypeCount.get(type));
|
||||||
|
typeWaitTimes.put(type, (long)(vehicleTypeWaitTime.get(type) * 1000.0)); // s -> ms
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.setVehicleTypeCounts(typeCounts);
|
||||||
|
payload.setVehicleTypeWaitTimes(typeWaitTimes);
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
Message message = new Message(
|
||||||
|
MessageType.STATS_UPDATE,
|
||||||
|
"ExitNode",
|
||||||
|
"Dashboard",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboardClient.send(message);
|
||||||
|
|
||||||
|
double avgWait = totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0;
|
||||||
|
System.out.printf("[Exit] Sent stats to dashboard (total=%d, avg_wait=%.2fs)%n",
|
||||||
|
totalVehiclesReceived, avgWait);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[Exit] Failed to send stats to dashboard: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Termina o processo
|
||||||
|
*
|
||||||
|
* Executa a seguinte sequência:
|
||||||
|
* Imprime as estatísticas finais no terminal;
|
||||||
|
* Envia a última atualização de estatísticas ao dashboard;
|
||||||
|
* Fecha o socket;
|
||||||
|
* Aguarda pela finalização das threads;
|
||||||
|
* Fecha a ligação com o dashboard;
|
||||||
|
*/
|
||||||
|
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 as estatísticas finais detalhadas no terminal
|
||||||
|
*
|
||||||
|
* Gera um relatório com:
|
||||||
|
* Total de veículos que completaram a rota;
|
||||||
|
* Médias de tempo no sistema, espera e travessia;
|
||||||
|
* Distribuição e médias pelo tipo de veículo (BIKE, LIGHT, HEAVY);
|
||||||
|
*
|
||||||
|
* Este método é chamado durante o shutdown para fornecer um resumo
|
||||||
|
* da simulação antes de terminar o processo.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,19 +8,26 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import sd.config.SimulationConfig;
|
import sd.config.SimulationConfig;
|
||||||
|
import sd.coordinator.SocketClient;
|
||||||
|
import sd.dashboard.StatsUpdatePayload;
|
||||||
|
import sd.engine.TrafficLightThread;
|
||||||
import sd.model.Intersection;
|
import sd.model.Intersection;
|
||||||
|
import sd.model.Message;
|
||||||
import sd.model.MessageType;
|
import sd.model.MessageType;
|
||||||
import sd.model.TrafficLight;
|
import sd.model.TrafficLight;
|
||||||
import sd.model.TrafficLightState;
|
|
||||||
import sd.model.Vehicle;
|
import sd.model.Vehicle;
|
||||||
import sd.protocol.MessageProtocol;
|
import sd.protocol.MessageProtocol;
|
||||||
import sd.protocol.SocketConnection;
|
import sd.protocol.SocketConnection;
|
||||||
|
import sd.serialization.SerializationException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main class for an Intersection Process in the distributed traffic simulation.
|
* Main class for an Intersection Process in the distributed traffic simulation.
|
||||||
* * Each IntersectionProcess runs as an independent Java application (JVM instance)
|
* * Each IntersectionProcess runs as an independent Java application (JVM
|
||||||
|
* instance)
|
||||||
* representing one of the five intersections (Cr1-Cr5) in the network.
|
* representing one of the five intersections (Cr1-Cr5) in the network.
|
||||||
*/
|
*/
|
||||||
public class IntersectionProcess {
|
public class IntersectionProcess {
|
||||||
@@ -42,6 +49,26 @@ public class IntersectionProcess {
|
|||||||
private volatile boolean running; // Quando uma thread escreve um valor volatile, todas as outras
|
private volatile boolean running; // Quando uma thread escreve um valor volatile, todas as outras
|
||||||
// threads veem a mudança imediatamente.
|
// threads veem a mudança imediatamente.
|
||||||
|
|
||||||
|
// Traffic Light Coordination
|
||||||
|
/**
|
||||||
|
* Lock to ensure mutual exclusion between traffic lights.
|
||||||
|
* Only one traffic light can be green at any given time within this
|
||||||
|
* intersection.
|
||||||
|
*/
|
||||||
|
private final Lock trafficCoordinationLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks which direction currently has the green light.
|
||||||
|
* null means no direction is currently green (all are red).
|
||||||
|
*/
|
||||||
|
private volatile String currentGreenDirection;
|
||||||
|
|
||||||
|
private SocketClient dashboardClient;
|
||||||
|
private long simulationStartMillis;
|
||||||
|
private volatile int totalArrivals = 0;
|
||||||
|
private volatile int totalDepartures = 0;
|
||||||
|
private long lastStatsUpdateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new IntersectionProcess.
|
* Constructs a new IntersectionProcess.
|
||||||
*
|
*
|
||||||
@@ -57,12 +84,43 @@ public class IntersectionProcess {
|
|||||||
this.connectionHandlerPool = Executors.newCachedThreadPool();
|
this.connectionHandlerPool = Executors.newCachedThreadPool();
|
||||||
this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions
|
this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions
|
||||||
this.running = false;
|
this.running = false;
|
||||||
|
this.trafficCoordinationLock = new ReentrantLock();
|
||||||
|
this.currentGreenDirection = null;
|
||||||
|
|
||||||
System.out.println("=".repeat(60));
|
System.out.println("=".repeat(60));
|
||||||
System.out.println("INTERSECTION PROCESS: " + intersectionId);
|
System.out.println("INTERSECTION PROCESS: " + intersectionId);
|
||||||
System.out.println("=".repeat(60));
|
System.out.println("=".repeat(60));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Main entry point for running an intersection process
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length < 1) {
|
||||||
|
System.err.println("Usage: java IntersectionProcess <intersectionId> [configFile]");
|
||||||
|
System.err.println("Example: java IntersectionProcess Cr1");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String intersectionId = args[0];
|
||||||
|
String configFile = args.length > 1 ? args[1] : "src/main/resources/simulation.properties";
|
||||||
|
|
||||||
|
try {
|
||||||
|
IntersectionProcess process = new IntersectionProcess(intersectionId, configFile);
|
||||||
|
process.initialize();
|
||||||
|
process.start();
|
||||||
|
|
||||||
|
// Add shutdown hook
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
System.out.println("\nShutdown signal received...");
|
||||||
|
process.shutdown();
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("Failed to start intersection process: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
System.out.println("\n[" + intersectionId + "] Initializing intersection...");
|
System.out.println("\n[" + intersectionId + "] Initializing intersection...");
|
||||||
|
|
||||||
@@ -70,13 +128,39 @@ public class IntersectionProcess {
|
|||||||
|
|
||||||
configureRouting();
|
configureRouting();
|
||||||
|
|
||||||
startTrafficLights();
|
connectToDashboard();
|
||||||
|
|
||||||
System.out.println("[" + intersectionId + "] Initialization complete.");
|
System.out.println("[" + intersectionId + "] Initialization complete.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates traffic lights for this intersection based on its physical connections.
|
* Establishes connection to the dashboard server for statistics reporting.
|
||||||
|
*/
|
||||||
|
private void connectToDashboard() {
|
||||||
|
try {
|
||||||
|
String dashboardHost = config.getDashboardHost();
|
||||||
|
int dashboardPort = config.getDashboardPort();
|
||||||
|
|
||||||
|
System.out.println("[" + intersectionId + "] Connecting to dashboard at " +
|
||||||
|
dashboardHost + ":" + dashboardPort + "...");
|
||||||
|
|
||||||
|
dashboardClient = new SocketClient(intersectionId, dashboardHost, dashboardPort);
|
||||||
|
dashboardClient.connect();
|
||||||
|
|
||||||
|
System.out.println("[" + intersectionId + "] Connected to dashboard.");
|
||||||
|
lastStatsUpdateTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("[" + intersectionId + "] Failed to connect to dashboard: " +
|
||||||
|
e.getMessage());
|
||||||
|
System.err.println("[" + intersectionId + "] Will continue without dashboard reporting.");
|
||||||
|
dashboardClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates traffic lights for this intersection based on its physical
|
||||||
|
* connections.
|
||||||
* Each intersection has different number and directions of traffic lights
|
* Each intersection has different number and directions of traffic lights
|
||||||
* according to the network topology.
|
* according to the network topology.
|
||||||
*/
|
*/
|
||||||
@@ -110,8 +194,7 @@ public class IntersectionProcess {
|
|||||||
intersectionId + "-" + direction,
|
intersectionId + "-" + direction,
|
||||||
direction,
|
direction,
|
||||||
greenTime,
|
greenTime,
|
||||||
redTime
|
redTime);
|
||||||
);
|
|
||||||
|
|
||||||
intersection.addTrafficLight(light);
|
intersection.addTrafficLight(light);
|
||||||
System.out.println(" Created traffic light: " + direction +
|
System.out.println(" Created traffic light: " + direction +
|
||||||
@@ -154,6 +237,29 @@ public class IntersectionProcess {
|
|||||||
System.out.println(" Routing configured.");
|
System.out.println(" Routing configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests permission for a traffic light to turn green.
|
||||||
|
* Blocks until permission is granted (no other light is green).
|
||||||
|
*
|
||||||
|
* @param direction The direction requesting green light
|
||||||
|
*/
|
||||||
|
public void requestGreenLight(String direction) {
|
||||||
|
trafficCoordinationLock.lock();
|
||||||
|
currentGreenDirection = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the green light permission, allowing another light to turn green.
|
||||||
|
*
|
||||||
|
* @param direction The direction releasing green light
|
||||||
|
*/
|
||||||
|
public void releaseGreenLight(String direction) {
|
||||||
|
if (direction.equals(currentGreenDirection)) {
|
||||||
|
currentGreenDirection = null;
|
||||||
|
trafficCoordinationLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts all traffic light threads.
|
* Starts all traffic light threads.
|
||||||
*/
|
*/
|
||||||
@@ -161,125 +267,43 @@ public class IntersectionProcess {
|
|||||||
System.out.println("\n[" + intersectionId + "] Starting traffic light threads...");
|
System.out.println("\n[" + intersectionId + "] Starting traffic light threads...");
|
||||||
|
|
||||||
for (TrafficLight light : intersection.getTrafficLights()) {
|
for (TrafficLight light : intersection.getTrafficLights()) {
|
||||||
trafficLightPool.submit(() -> runTrafficLightCycle(light));
|
|
||||||
|
TrafficLightThread lightTask = new TrafficLightThread(light, this, config);
|
||||||
|
|
||||||
|
trafficLightPool.submit(lightTask);
|
||||||
|
|
||||||
System.out.println(" Started thread for: " + light.getDirection());
|
System.out.println(" Started thread for: " + light.getDirection());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The main loop for a traffic light thread.
|
|
||||||
* Continuously cycles between GREEN and RED states.
|
|
||||||
*
|
|
||||||
* @param light The traffic light to control.
|
|
||||||
*/
|
|
||||||
private void runTrafficLightCycle(TrafficLight light) {
|
|
||||||
System.out.println("[" + light.getId() + "] Traffic light thread started.");
|
|
||||||
|
|
||||||
while (running) {
|
|
||||||
try {
|
|
||||||
// Green state
|
|
||||||
light.changeState(TrafficLightState.GREEN);
|
|
||||||
System.out.println("[" + light.getId() + "] State: GREEN");
|
|
||||||
|
|
||||||
// Process vehicles while green
|
|
||||||
processGreenLight(light);
|
|
||||||
|
|
||||||
// Wait for green duration
|
|
||||||
Thread.sleep((long) (light.getGreenTime() * 1000));
|
|
||||||
|
|
||||||
// RED state
|
|
||||||
light.changeState(TrafficLightState.RED);
|
|
||||||
System.out.println("[" + light.getId() + "] State: RED");
|
|
||||||
|
|
||||||
// Wait for red duration
|
|
||||||
Thread.sleep((long) (light.getRedTime() * 1000));
|
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes vehicles when a traffic light is GREEN.
|
|
||||||
* Dequeues vehicles and sends them to their next destination.
|
|
||||||
*
|
|
||||||
* @param light The traffic light that is currently green.
|
|
||||||
*/
|
|
||||||
private void processGreenLight(TrafficLight light) {
|
|
||||||
while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
|
|
||||||
Vehicle vehicle = light.removeVehicle();
|
|
||||||
|
|
||||||
if (vehicle != null) {
|
|
||||||
// Get crossing time based on vehicle type
|
|
||||||
double crossingTime = getCrossingTimeForVehicle(vehicle);
|
|
||||||
|
|
||||||
// Simulate crossing time
|
|
||||||
try {
|
|
||||||
Thread.sleep((long) (crossingTime * 1000));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update vehicle statistics
|
|
||||||
vehicle.addCrossingTime(crossingTime);
|
|
||||||
|
|
||||||
// Update intersection statistics
|
|
||||||
intersection.incrementVehiclesSent();
|
|
||||||
|
|
||||||
// Send vehicle to next destination
|
|
||||||
sendVehicleToNextDestination(vehicle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the crossing time for a vehicle based on its type.
|
|
||||||
*
|
|
||||||
* @param vehicle The vehicle.
|
|
||||||
* @return The crossing time in seconds.
|
|
||||||
*/
|
|
||||||
private double getCrossingTimeForVehicle(Vehicle vehicle) {
|
|
||||||
switch (vehicle.getType()) {
|
|
||||||
case BIKE:
|
|
||||||
return config.getBikeVehicleCrossingTime();
|
|
||||||
case LIGHT:
|
|
||||||
return config.getLightVehicleCrossingTime();
|
|
||||||
case HEAVY:
|
|
||||||
return config.getHeavyVehicleCrossingTime();
|
|
||||||
default:
|
|
||||||
return config.getLightVehicleCrossingTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a vehicle to its next destination via socket connection.
|
* Sends a vehicle to its next destination via socket connection.
|
||||||
*
|
*
|
||||||
* @param vehicle The vehicle that has crossed this intersection.
|
* @param vehicle The vehicle that has crossed this intersection.
|
||||||
*/
|
*/
|
||||||
private void sendVehicleToNextDestination(Vehicle vehicle) {
|
public void sendVehicleToNextDestination(Vehicle vehicle) {
|
||||||
String nextDestination = vehicle.getCurrentDestination();
|
String nextDestination = vehicle.getCurrentDestination();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get or create connection to next destination
|
// Get or create connection to next destination
|
||||||
SocketConnection connection = getOrCreateConnection(nextDestination);
|
SocketConnection connection = getOrCreateConnection(nextDestination);
|
||||||
|
|
||||||
// Create and send message
|
// Create and send message using Message class
|
||||||
MessageProtocol message = new VehicleTransferMessage(
|
MessageProtocol message = new Message(
|
||||||
|
MessageType.VEHICLE_TRANSFER,
|
||||||
intersectionId,
|
intersectionId,
|
||||||
nextDestination,
|
nextDestination,
|
||||||
vehicle
|
vehicle,
|
||||||
);
|
System.currentTimeMillis());
|
||||||
|
|
||||||
connection.sendMessage(message);
|
connection.sendMessage(message);
|
||||||
|
|
||||||
System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() +
|
System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() +
|
||||||
" to " + nextDestination);
|
" to " + nextDestination);
|
||||||
|
|
||||||
|
// Record departure for statistics
|
||||||
|
recordVehicleDeparture();
|
||||||
|
|
||||||
// Note: vehicle route is advanced when it arrives at the next intersection
|
// Note: vehicle route is advanced when it arrives at the next intersection
|
||||||
|
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
@@ -353,6 +377,10 @@ public class IntersectionProcess {
|
|||||||
running = true;
|
running = true;
|
||||||
|
|
||||||
System.out.println("\n[" + intersectionId + "] Server started on port " + port);
|
System.out.println("\n[" + intersectionId + "] Server started on port " + port);
|
||||||
|
|
||||||
|
// Start traffic light threads when running is true
|
||||||
|
startTrafficLights();
|
||||||
|
|
||||||
System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n");
|
System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n");
|
||||||
|
|
||||||
// Main accept loop
|
// Main accept loop
|
||||||
@@ -360,17 +388,37 @@ public class IntersectionProcess {
|
|||||||
try {
|
try {
|
||||||
Socket clientSocket = serverSocket.accept();
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
|
||||||
|
System.out.println("[" + intersectionId + "] New connection accepted from " +
|
||||||
|
clientSocket.getInetAddress().getHostAddress());
|
||||||
|
|
||||||
|
// Check running flag again before handling
|
||||||
|
if (!running) {
|
||||||
|
clientSocket.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// **Set timeout before submitting to handler**
|
||||||
|
try {
|
||||||
|
clientSocket.setSoTimeout(1000);
|
||||||
|
} catch (java.net.SocketException e) {
|
||||||
|
System.err.println("[" + intersectionId + "] Failed to set timeout: " + e.getMessage());
|
||||||
|
clientSocket.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle each connection in a separate thread
|
// Handle each connection in a separate thread
|
||||||
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
|
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (running) {
|
// Expected when serverSocket.close() is called during shutdown
|
||||||
|
if (!running) {
|
||||||
|
break; // Normal shutdown
|
||||||
|
}
|
||||||
System.err.println("[" + intersectionId + "] Error accepting connection: " +
|
System.err.println("[" + intersectionId + "] Error accepting connection: " +
|
||||||
e.getMessage());
|
e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles an incoming connection from another process.
|
* Handles an incoming connection from another process.
|
||||||
@@ -379,6 +427,14 @@ public class IntersectionProcess {
|
|||||||
* @param clientSocket The accepted socket connection.
|
* @param clientSocket The accepted socket connection.
|
||||||
*/
|
*/
|
||||||
private void handleIncomingConnection(Socket clientSocket) {
|
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)) {
|
try (SocketConnection connection = new SocketConnection(clientSocket)) {
|
||||||
|
|
||||||
System.out.println("[" + intersectionId + "] New connection accepted from " +
|
System.out.println("[" + intersectionId + "] New connection accepted from " +
|
||||||
@@ -389,19 +445,61 @@ public class IntersectionProcess {
|
|||||||
try {
|
try {
|
||||||
MessageProtocol message = connection.receiveMessage();
|
MessageProtocol message = connection.receiveMessage();
|
||||||
|
|
||||||
if (message.getType() == MessageType.VEHICLE_TRANSFER) {
|
// Handle simulation start time synchronization
|
||||||
Vehicle vehicle = (Vehicle) message.getPayload();
|
if (message.getType() == MessageType.SIMULATION_START) {
|
||||||
|
simulationStartMillis = ((Number) message.getPayload()).longValue();
|
||||||
|
System.out.println("[" + intersectionId + "] Simulation start time synchronized");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept both VEHICLE_TRANSFER and VEHICLE_SPAWN (from coordinator)
|
||||||
|
if (message.getType() == MessageType.VEHICLE_TRANSFER ||
|
||||||
|
message.getType() == MessageType.VEHICLE_SPAWN) {
|
||||||
|
// Cast payload to Vehicle - handle Gson deserialization
|
||||||
|
Vehicle vehicle;
|
||||||
|
Object payload = message.getPayload();
|
||||||
|
if (payload instanceof Vehicle) {
|
||||||
|
vehicle = (Vehicle) payload;
|
||||||
|
} else if (payload instanceof java.util.Map) {
|
||||||
|
// Gson deserialized as LinkedHashMap - re-serialize and deserialize as Vehicle
|
||||||
|
com.google.gson.Gson gson = new com.google.gson.Gson();
|
||||||
|
String json = gson.toJson(payload);
|
||||||
|
vehicle = gson.fromJson(json, Vehicle.class);
|
||||||
|
} else {
|
||||||
|
System.err.println("[" + intersectionId + "] Unknown payload type: " + payload.getClass());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println("[" + intersectionId + "] Received vehicle: " +
|
System.out.println("[" + intersectionId + "] Received vehicle: " +
|
||||||
vehicle.getId() + " from " + message.getSourceNode());
|
vehicle.getId() + " from " + message.getSourceNode());
|
||||||
|
|
||||||
|
// Advance vehicle to next destination in its route
|
||||||
|
vehicle.advanceRoute();
|
||||||
|
|
||||||
// Add vehicle to appropriate queue
|
// Add vehicle to appropriate queue
|
||||||
intersection.receiveVehicle(vehicle);
|
intersection.receiveVehicle(vehicle);
|
||||||
|
|
||||||
|
// Record arrival for statistics
|
||||||
|
recordVehicleArrival();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
|
// Timeout - check running flag and continue
|
||||||
|
if (!running) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Continue waiting for next message
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
System.err.println("[" + intersectionId + "] Unknown message type received: " +
|
System.err.println("[" + intersectionId + "] Unknown message type received: " +
|
||||||
e.getMessage());
|
e.getMessage());
|
||||||
|
break; // Invalid message, close connection
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running) {
|
||||||
|
System.err.println("[" + intersectionId + "] Failed to deserialize message: " +
|
||||||
|
e.getMessage());
|
||||||
|
e.printStackTrace(); // For debugging - maybe change//remove later
|
||||||
|
}
|
||||||
|
break; // Connection error, close connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +507,7 @@ public class IntersectionProcess {
|
|||||||
if (running) {
|
if (running) {
|
||||||
System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
|
System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
// Expected during shutdown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,47 +516,139 @@ public class IntersectionProcess {
|
|||||||
* Shuts down all threads and closes all connections.
|
* Shuts down all threads and closes all connections.
|
||||||
*/
|
*/
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
// Check if already shutdown
|
||||||
|
if (!running) {
|
||||||
|
return; // Already shutdown, do nothing
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println("\n[" + intersectionId + "] Shutting down...");
|
System.out.println("\n[" + intersectionId + "] Shutting down...");
|
||||||
running = false;
|
running = false;
|
||||||
|
|
||||||
// Close server socket
|
// 1. Close ServerSocket first
|
||||||
try {
|
|
||||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||||
serverSocket.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println("[" + intersectionId + "] Error closing server socket: " +
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown thread pools
|
|
||||||
trafficLightPool.shutdown();
|
|
||||||
connectionHandlerPool.shutdown();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!trafficLightPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
serverSocket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Shutdown thread pools with force
|
||||||
|
if (trafficLightPool != null && !trafficLightPool.isShutdown()) {
|
||||||
trafficLightPool.shutdownNow();
|
trafficLightPool.shutdownNow();
|
||||||
}
|
}
|
||||||
if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
if (connectionHandlerPool != null && !connectionHandlerPool.isShutdown()) {
|
||||||
connectionHandlerPool.shutdownNow();
|
connectionHandlerPool.shutdownNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Wait briefly for termination (don't block forever)
|
||||||
|
try {
|
||||||
|
if (trafficLightPool != null) {
|
||||||
|
trafficLightPool.awaitTermination(1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
if (connectionHandlerPool != null) {
|
||||||
|
connectionHandlerPool.awaitTermination(1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
trafficLightPool.shutdownNow();
|
Thread.currentThread().interrupt();
|
||||||
connectionHandlerPool.shutdownNow();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all outgoing connections
|
// 4. Close outgoing connections
|
||||||
for (Map.Entry<String, SocketConnection> entry : outgoingConnections.entrySet()) {
|
synchronized (outgoingConnections) {
|
||||||
|
for (SocketConnection conn : outgoingConnections.values()) {
|
||||||
try {
|
try {
|
||||||
entry.getValue().close();
|
conn.close();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
System.err.println("[" + intersectionId + "] Error closing connection to " +
|
// Ignore
|
||||||
entry.getKey() + ": " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
outgoingConnections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Close dashboard connection
|
||||||
|
if (dashboardClient != null) {
|
||||||
|
dashboardClient.close();
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println("[" + intersectionId + "] Shutdown complete.");
|
System.out.println("[" + intersectionId + "] Shutdown complete.");
|
||||||
System.out.println("=".repeat(60));
|
System.out.println("============================================================\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Intersection object managed by this process.
|
||||||
|
* Useful for testing and monitoring.
|
||||||
|
*
|
||||||
|
* @return The Intersection object.
|
||||||
|
*/
|
||||||
|
public Intersection getIntersection() {
|
||||||
|
return intersection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records that a vehicle has arrived at this intersection.
|
||||||
|
*/
|
||||||
|
public void recordVehicleArrival() {
|
||||||
|
totalArrivals++;
|
||||||
|
checkAndSendStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records that a vehicle has departed from this intersection.
|
||||||
|
*/
|
||||||
|
public void recordVehicleDeparture() {
|
||||||
|
totalDepartures++;
|
||||||
|
checkAndSendStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if it's time to send statistics to the dashboard and sends them if needed.
|
||||||
|
*/
|
||||||
|
private void checkAndSendStats() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long elapsed = now - lastStatsUpdateTime;
|
||||||
|
|
||||||
|
// Send stats every 5 seconds
|
||||||
|
if (elapsed >= 5000) {
|
||||||
|
sendStatsToDashboard();
|
||||||
|
lastStatsUpdateTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends current statistics to the dashboard server.
|
||||||
|
*/
|
||||||
|
private void sendStatsToDashboard() {
|
||||||
|
if (dashboardClient == null || !dashboardClient.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Calculate current queue size
|
||||||
|
int currentQueueSize = intersection.getTrafficLights().stream()
|
||||||
|
.mapToInt(TrafficLight::getQueueSize)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
StatsUpdatePayload payload = new StatsUpdatePayload()
|
||||||
|
.setIntersectionArrivals(totalArrivals)
|
||||||
|
.setIntersectionDepartures(totalDepartures)
|
||||||
|
.setIntersectionQueueSize(currentQueueSize);
|
||||||
|
|
||||||
|
// Send StatsUpdatePayload directly as the message payload
|
||||||
|
sd.model.Message message = new sd.model.Message(
|
||||||
|
MessageType.STATS_UPDATE,
|
||||||
|
intersectionId,
|
||||||
|
"Dashboard",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboardClient.send(message);
|
||||||
|
|
||||||
|
System.out.printf("[%s] Sent stats to dashboard (arrivals=%d, departures=%d, queue=%d)%n",
|
||||||
|
intersectionId, totalArrivals, totalDepartures, currentQueueSize);
|
||||||
|
|
||||||
|
} catch (SerializationException | IOException e) {
|
||||||
|
System.err.println("[" + intersectionId + "] Failed to send stats to dashboard: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Inner class for Vehicle Transfer Messages ---
|
// --- Inner class for Vehicle Transfer Messages ---
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import sd.config.SimulationConfig;
|
import sd.config.SimulationConfig;
|
||||||
|
import sd.dashboard.StatsUpdatePayload;
|
||||||
import sd.model.Message;
|
import sd.model.Message;
|
||||||
import sd.model.MessageType;
|
import sd.model.MessageType;
|
||||||
import sd.model.Vehicle;
|
import sd.model.Vehicle;
|
||||||
@@ -24,6 +25,7 @@ public class CoordinatorProcess {
|
|||||||
private final SimulationConfig config;
|
private final SimulationConfig config;
|
||||||
private final VehicleGenerator vehicleGenerator;
|
private final VehicleGenerator vehicleGenerator;
|
||||||
private final Map<String, SocketClient> intersectionClients;
|
private final Map<String, SocketClient> intersectionClients;
|
||||||
|
private SocketClient dashboardClient;
|
||||||
private double currentTime;
|
private double currentTime;
|
||||||
private int vehicleCounter;
|
private int vehicleCounter;
|
||||||
private boolean running;
|
private boolean running;
|
||||||
@@ -75,6 +77,9 @@ public class CoordinatorProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
// Connect to dashboard first
|
||||||
|
connectToDashboard();
|
||||||
|
|
||||||
System.out.println("Connecting to intersection processes...");
|
System.out.println("Connecting to intersection processes...");
|
||||||
|
|
||||||
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
|
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
|
||||||
@@ -108,6 +113,9 @@ public class CoordinatorProcess {
|
|||||||
System.out.println("Duration: " + duration + " seconds");
|
System.out.println("Duration: " + duration + " seconds");
|
||||||
System.out.println();
|
System.out.println();
|
||||||
|
|
||||||
|
// Send simulation start time to all processes for synchronization
|
||||||
|
sendSimulationStartTime();
|
||||||
|
|
||||||
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
|
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
|
||||||
final double TIME_STEP = 0.1;
|
final double TIME_STEP = 0.1;
|
||||||
|
|
||||||
@@ -132,6 +140,9 @@ public class CoordinatorProcess {
|
|||||||
System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
|
System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
|
||||||
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
|
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
|
||||||
|
|
||||||
|
// Send generation count to dashboard
|
||||||
|
sendGenerationStatsToDashboard();
|
||||||
|
|
||||||
if (vehicle.getRoute().isEmpty()) {
|
if (vehicle.getRoute().isEmpty()) {
|
||||||
System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
|
System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
|
||||||
return;
|
return;
|
||||||
@@ -201,4 +212,77 @@ public class CoordinatorProcess {
|
|||||||
System.out.println("\nStop signal received...");
|
System.out.println("\nStop signal received...");
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void connectToDashboard() {
|
||||||
|
try {
|
||||||
|
String host = config.getDashboardHost();
|
||||||
|
int port = config.getDashboardPort();
|
||||||
|
|
||||||
|
System.out.println("Connecting to dashboard at " + host + ":" + port);
|
||||||
|
dashboardClient = new SocketClient("Dashboard", host, port);
|
||||||
|
dashboardClient.connect();
|
||||||
|
System.out.println("Successfully connected to dashboard\n");
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
|
||||||
|
System.err.println("Coordinator will continue without dashboard connection\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendGenerationStatsToDashboard() {
|
||||||
|
if (dashboardClient == null || !dashboardClient.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create stats payload with vehicle generation count
|
||||||
|
StatsUpdatePayload payload = new StatsUpdatePayload();
|
||||||
|
payload.setTotalVehiclesGenerated(vehicleCounter);
|
||||||
|
|
||||||
|
Message message = new Message(
|
||||||
|
MessageType.STATS_UPDATE,
|
||||||
|
"COORDINATOR",
|
||||||
|
"Dashboard",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
dashboardClient.send(message);
|
||||||
|
} catch (Exception e) { //This is fine - can add IOException if need be
|
||||||
|
// Don't crash if dashboard update fails
|
||||||
|
System.err.println("Failed to send stats to dashboard: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendSimulationStartTime() {
|
||||||
|
long startTimeMillis = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Send to all intersections
|
||||||
|
for (Map.Entry<String, SocketClient> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
main/src/main/java/sd/dashboard/DashboardClientHandler.java
Normal file
137
main/src/main/java/sd/dashboard/DashboardClientHandler.java
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes statistics messages from a single client connection.
|
||||||
|
* Runs in a separate thread per client.
|
||||||
|
*/
|
||||||
|
public class DashboardClientHandler implements Runnable {
|
||||||
|
|
||||||
|
private final Socket clientSocket;
|
||||||
|
private final DashboardStatistics statistics;
|
||||||
|
|
||||||
|
public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) {
|
||||||
|
this.clientSocket = clientSocket;
|
||||||
|
this.statistics = statistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
com.google.gson.Gson gson = new com.google.gson.Gson();
|
||||||
|
String json = gson.toJson(payload);
|
||||||
|
stats = gson.fromJson(json, StatsUpdatePayload.class);
|
||||||
|
} else {
|
||||||
|
System.err.println("[Handler] Unknown payload type: " +
|
||||||
|
(payload != null ? payload.getClass().getName() : "null"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatistics(senderId, stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatistics(String senderId, StatsUpdatePayload stats) {
|
||||||
|
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<sd.model.VehicleType, Integer> counts = stats.getVehicleTypeCounts();
|
||||||
|
Map<sd.model.VehicleType, Long> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
165
main/src/main/java/sd/dashboard/DashboardServer.java
Normal file
165
main/src/main/java/sd/dashboard/DashboardServer.java
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates and displays real-time statistics from all simulation processes.
|
||||||
|
* Uses a thread pool to handle concurrent client connections.
|
||||||
|
*/
|
||||||
|
public class DashboardServer {
|
||||||
|
|
||||||
|
private final int port;
|
||||||
|
private final DashboardStatistics statistics;
|
||||||
|
private final ExecutorService clientHandlerPool;
|
||||||
|
private final AtomicBoolean running;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DashboardServer(SimulationConfig config) {
|
||||||
|
this.port = config.getDashboardPort();
|
||||||
|
this.statistics = new DashboardStatistics();
|
||||||
|
this.clientHandlerPool = Executors.newFixedThreadPool(10);
|
||||||
|
this.running = new AtomicBoolean(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
224
main/src/main/java/sd/dashboard/DashboardStatistics.java
Normal file
224
main/src/main/java/sd/dashboard/DashboardStatistics.java
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread-safe storage for aggregated simulation statistics.
|
||||||
|
* Uses atomic types and concurrent collections for lock-free updates.
|
||||||
|
*/
|
||||||
|
public class DashboardStatistics {
|
||||||
|
|
||||||
|
private final AtomicInteger totalVehiclesGenerated;
|
||||||
|
private final AtomicInteger totalVehiclesCompleted;
|
||||||
|
private final AtomicLong totalSystemTime;
|
||||||
|
private final AtomicLong totalWaitingTime;
|
||||||
|
|
||||||
|
private final Map<String, IntersectionStats> intersectionStats;
|
||||||
|
private final Map<VehicleType, AtomicInteger> vehicleTypeCount;
|
||||||
|
private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime;
|
||||||
|
|
||||||
|
private volatile long lastUpdateTime;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalVehiclesGenerated() {
|
||||||
|
return totalVehiclesGenerated.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalVehiclesCompleted() {
|
||||||
|
return totalVehiclesCompleted.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageSystemTime() {
|
||||||
|
int completed = totalVehiclesCompleted.get();
|
||||||
|
if (completed == 0) return 0.0;
|
||||||
|
return (double) totalSystemTime.get() / completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String, IntersectionStats> getAllIntersectionStats() {
|
||||||
|
return new HashMap<>(intersectionStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastUpdateTime() {
|
||||||
|
return lastUpdateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
378
main/src/main/java/sd/dashboard/DashboardUI.java
Normal file
378
main/src/main/java/sd/dashboard/DashboardUI.java
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
package sd.dashboard;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.TitledPane;
|
||||||
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.shape.Circle;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.text.FontWeight;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import sd.config.SimulationConfig;
|
||||||
|
import sd.model.VehicleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX-based Dashboard UI for displaying real-time simulation statistics.
|
||||||
|
* Provides a graphical interface with auto-updating statistics panels.
|
||||||
|
*/
|
||||||
|
public class DashboardUI extends Application {
|
||||||
|
|
||||||
|
private DashboardServer server;
|
||||||
|
private DashboardStatistics statistics;
|
||||||
|
|
||||||
|
// Global Statistics Labels
|
||||||
|
private Label lblVehiclesGenerated;
|
||||||
|
private Label lblVehiclesCompleted;
|
||||||
|
private Label lblVehiclesInTransit;
|
||||||
|
private Label lblAvgSystemTime;
|
||||||
|
private Label lblAvgWaitingTime;
|
||||||
|
private Label lblLastUpdate;
|
||||||
|
|
||||||
|
// Vehicle Type Table
|
||||||
|
private TableView<VehicleTypeRow> vehicleTypeTable;
|
||||||
|
|
||||||
|
// Intersection Table
|
||||||
|
private TableView<IntersectionRow> intersectionTable;
|
||||||
|
|
||||||
|
// Update scheduler
|
||||||
|
private ScheduledExecutorService updateScheduler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Stage primaryStage) {
|
||||||
|
try {
|
||||||
|
// Initialize server
|
||||||
|
String configFile = getParameters().getRaw().isEmpty()
|
||||||
|
? "src/main/resources/simulation.properties"
|
||||||
|
: getParameters().getRaw().get(0);
|
||||||
|
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile);
|
||||||
|
server = new DashboardServer(config);
|
||||||
|
statistics = server.getStatistics();
|
||||||
|
|
||||||
|
// Start the dashboard server
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
// Build UI
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
root.setStyle("-fx-background-color: #f5f5f5;");
|
||||||
|
|
||||||
|
// Header
|
||||||
|
VBox header = createHeader();
|
||||||
|
root.setTop(header);
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
VBox mainContent = createMainContent();
|
||||||
|
root.setCenter(mainContent);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
HBox footer = createFooter();
|
||||||
|
root.setBottom(footer);
|
||||||
|
|
||||||
|
// Create scene
|
||||||
|
Scene scene = new Scene(root, 1200, 800);
|
||||||
|
primaryStage.setTitle("Traffic Simulation Dashboard - Real-time Statistics");
|
||||||
|
primaryStage.setScene(scene);
|
||||||
|
primaryStage.show();
|
||||||
|
|
||||||
|
// Start periodic updates
|
||||||
|
startPeriodicUpdates();
|
||||||
|
|
||||||
|
// Handle window close
|
||||||
|
primaryStage.setOnCloseRequest(event -> {
|
||||||
|
shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
showErrorAlert("Failed to start Dashboard Server", e.getMessage());
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox createHeader() {
|
||||||
|
VBox header = new VBox(10);
|
||||||
|
header.setPadding(new Insets(20));
|
||||||
|
header.setStyle("-fx-background-color: linear-gradient(to right, #2c3e50, #3498db);");
|
||||||
|
|
||||||
|
Label title = new Label("DISTRIBUTED TRAFFIC SIMULATION DASHBOARD");
|
||||||
|
title.setFont(Font.font("Arial", FontWeight.BOLD, 28));
|
||||||
|
title.setTextFill(Color.WHITE);
|
||||||
|
|
||||||
|
Label subtitle = new Label("Real-time Statistics and Monitoring");
|
||||||
|
subtitle.setFont(Font.font("Arial", FontWeight.NORMAL, 16));
|
||||||
|
subtitle.setTextFill(Color.web("#ecf0f1"));
|
||||||
|
|
||||||
|
header.getChildren().addAll(title, subtitle);
|
||||||
|
header.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox createMainContent() {
|
||||||
|
VBox mainContent = new VBox(15);
|
||||||
|
mainContent.setPadding(new Insets(20));
|
||||||
|
|
||||||
|
// Global Statistics Panel
|
||||||
|
TitledPane globalStatsPane = createGlobalStatisticsPanel();
|
||||||
|
|
||||||
|
// Vehicle Type Statistics Panel
|
||||||
|
TitledPane vehicleTypePane = createVehicleTypePanel();
|
||||||
|
|
||||||
|
// Intersection Statistics Panel
|
||||||
|
TitledPane intersectionPane = createIntersectionPanel();
|
||||||
|
|
||||||
|
mainContent.getChildren().addAll(globalStatsPane, vehicleTypePane, intersectionPane);
|
||||||
|
|
||||||
|
return mainContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TitledPane createGlobalStatisticsPanel() {
|
||||||
|
GridPane grid = new GridPane();
|
||||||
|
grid.setPadding(new Insets(15));
|
||||||
|
grid.setHgap(20);
|
||||||
|
grid.setVgap(15);
|
||||||
|
grid.setStyle("-fx-background-color: white; -fx-border-radius: 5;");
|
||||||
|
|
||||||
|
// Initialize labels
|
||||||
|
lblVehiclesGenerated = createStatLabel("0");
|
||||||
|
lblVehiclesCompleted = createStatLabel("0");
|
||||||
|
lblVehiclesInTransit = createStatLabel("0");
|
||||||
|
lblAvgSystemTime = createStatLabel("0.00 ms");
|
||||||
|
lblAvgWaitingTime = createStatLabel("0.00 ms");
|
||||||
|
|
||||||
|
// Add labels with descriptions
|
||||||
|
addStatRow(grid, 0, "Total Vehicles Generated:", lblVehiclesGenerated);
|
||||||
|
addStatRow(grid, 1, "Total Vehicles Completed:", lblVehiclesCompleted);
|
||||||
|
addStatRow(grid, 2, "Vehicles In Transit:", lblVehiclesInTransit);
|
||||||
|
addStatRow(grid, 3, "Average System Time:", lblAvgSystemTime);
|
||||||
|
addStatRow(grid, 4, "Average Waiting Time:", lblAvgWaitingTime);
|
||||||
|
|
||||||
|
TitledPane pane = new TitledPane("Global Statistics", grid);
|
||||||
|
pane.setCollapsible(false);
|
||||||
|
pane.setFont(Font.font("Arial", FontWeight.BOLD, 16));
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TitledPane createVehicleTypePanel() {
|
||||||
|
vehicleTypeTable = new TableView<>();
|
||||||
|
vehicleTypeTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
|
vehicleTypeTable.setPrefHeight(200);
|
||||||
|
|
||||||
|
TableColumn<VehicleTypeRow, String> typeCol = new TableColumn<>("Vehicle Type");
|
||||||
|
typeCol.setCellValueFactory(new PropertyValueFactory<>("vehicleType"));
|
||||||
|
typeCol.setPrefWidth(200);
|
||||||
|
|
||||||
|
TableColumn<VehicleTypeRow, Integer> countCol = new TableColumn<>("Count");
|
||||||
|
countCol.setCellValueFactory(new PropertyValueFactory<>("count"));
|
||||||
|
countCol.setPrefWidth(150);
|
||||||
|
|
||||||
|
TableColumn<VehicleTypeRow, String> avgWaitCol = new TableColumn<>("Avg Wait Time");
|
||||||
|
avgWaitCol.setCellValueFactory(new PropertyValueFactory<>("avgWaitTime"));
|
||||||
|
avgWaitCol.setPrefWidth(150);
|
||||||
|
|
||||||
|
vehicleTypeTable.getColumns().addAll(typeCol, countCol, avgWaitCol);
|
||||||
|
|
||||||
|
TitledPane pane = new TitledPane("Vehicle Type Statistics", vehicleTypeTable);
|
||||||
|
pane.setCollapsible(false);
|
||||||
|
pane.setFont(Font.font("Arial", FontWeight.BOLD, 16));
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TitledPane createIntersectionPanel() {
|
||||||
|
intersectionTable = new TableView<>();
|
||||||
|
intersectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
|
intersectionTable.setPrefHeight(250);
|
||||||
|
|
||||||
|
TableColumn<IntersectionRow, String> idCol = new TableColumn<>("Intersection ID");
|
||||||
|
idCol.setCellValueFactory(new PropertyValueFactory<>("intersectionId"));
|
||||||
|
idCol.setPrefWidth(200);
|
||||||
|
|
||||||
|
TableColumn<IntersectionRow, Integer> arrivalsCol = new TableColumn<>("Total Arrivals");
|
||||||
|
arrivalsCol.setCellValueFactory(new PropertyValueFactory<>("arrivals"));
|
||||||
|
arrivalsCol.setPrefWidth(150);
|
||||||
|
|
||||||
|
TableColumn<IntersectionRow, Integer> departuresCol = new TableColumn<>("Total Departures");
|
||||||
|
departuresCol.setCellValueFactory(new PropertyValueFactory<>("departures"));
|
||||||
|
departuresCol.setPrefWidth(150);
|
||||||
|
|
||||||
|
TableColumn<IntersectionRow, Integer> queueCol = new TableColumn<>("Current Queue");
|
||||||
|
queueCol.setCellValueFactory(new PropertyValueFactory<>("queueSize"));
|
||||||
|
queueCol.setPrefWidth(150);
|
||||||
|
|
||||||
|
intersectionTable.getColumns().addAll(idCol, arrivalsCol, departuresCol, queueCol);
|
||||||
|
|
||||||
|
TitledPane pane = new TitledPane("Intersection Statistics", intersectionTable);
|
||||||
|
pane.setCollapsible(false);
|
||||||
|
pane.setFont(Font.font("Arial", FontWeight.BOLD, 16));
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HBox createFooter() {
|
||||||
|
HBox footer = new HBox(10);
|
||||||
|
footer.setPadding(new Insets(10, 20, 10, 20));
|
||||||
|
footer.setStyle("-fx-background-color: #34495e;");
|
||||||
|
footer.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
Label statusLabel = new Label("Status:");
|
||||||
|
statusLabel.setTextFill(Color.WHITE);
|
||||||
|
statusLabel.setFont(Font.font("Arial", FontWeight.BOLD, 12));
|
||||||
|
|
||||||
|
Circle statusIndicator = new Circle(6);
|
||||||
|
statusIndicator.setFill(Color.LIME);
|
||||||
|
|
||||||
|
Label statusText = new Label("Connected and Receiving Data");
|
||||||
|
statusText.setTextFill(Color.WHITE);
|
||||||
|
statusText.setFont(Font.font("Arial", 12));
|
||||||
|
|
||||||
|
lblLastUpdate = new Label("Last Update: --:--:--");
|
||||||
|
lblLastUpdate.setTextFill(Color.web("#ecf0f1"));
|
||||||
|
lblLastUpdate.setFont(Font.font("Arial", 12));
|
||||||
|
|
||||||
|
Region spacer = new Region();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
footer.getChildren().addAll(statusLabel, statusIndicator, statusText, spacer, lblLastUpdate);
|
||||||
|
|
||||||
|
return footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Label createStatLabel(String initialValue) {
|
||||||
|
Label label = new Label(initialValue);
|
||||||
|
label.setFont(Font.font("Arial", FontWeight.BOLD, 20));
|
||||||
|
label.setTextFill(Color.web("#2980b9"));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStatRow(GridPane grid, int row, String description, Label valueLabel) {
|
||||||
|
Label descLabel = new Label(description);
|
||||||
|
descLabel.setFont(Font.font("Arial", FontWeight.NORMAL, 14));
|
||||||
|
descLabel.setTextFill(Color.web("#34495e"));
|
||||||
|
|
||||||
|
grid.add(descLabel, 0, row);
|
||||||
|
grid.add(valueLabel, 1, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPeriodicUpdates() {
|
||||||
|
updateScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
updateScheduler.scheduleAtFixedRate(() -> {
|
||||||
|
Platform.runLater(this::updateUI);
|
||||||
|
}, 0, 5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI() {
|
||||||
|
// Update global statistics
|
||||||
|
lblVehiclesGenerated.setText(String.valueOf(statistics.getTotalVehiclesGenerated()));
|
||||||
|
lblVehiclesCompleted.setText(String.valueOf(statistics.getTotalVehiclesCompleted()));
|
||||||
|
lblVehiclesInTransit.setText(String.valueOf(
|
||||||
|
statistics.getTotalVehiclesGenerated() - statistics.getTotalVehiclesCompleted()));
|
||||||
|
lblAvgSystemTime.setText(String.format("%.2f ms", statistics.getAverageSystemTime()));
|
||||||
|
lblAvgWaitingTime.setText(String.format("%.2f ms", statistics.getAverageWaitingTime()));
|
||||||
|
lblLastUpdate.setText(String.format("Last Update: %tT", statistics.getLastUpdateTime()));
|
||||||
|
|
||||||
|
// Update vehicle type table
|
||||||
|
vehicleTypeTable.getItems().clear();
|
||||||
|
for (VehicleType type : VehicleType.values()) {
|
||||||
|
int count = statistics.getVehicleTypeCount(type);
|
||||||
|
double avgWait = statistics.getAverageWaitingTimeByType(type);
|
||||||
|
vehicleTypeTable.getItems().add(new VehicleTypeRow(
|
||||||
|
type.toString(), count, String.format("%.2f ms", avgWait)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update intersection table
|
||||||
|
intersectionTable.getItems().clear();
|
||||||
|
Map<String, DashboardStatistics.IntersectionStats> intersectionStats =
|
||||||
|
statistics.getAllIntersectionStats();
|
||||||
|
for (DashboardStatistics.IntersectionStats stats : intersectionStats.values()) {
|
||||||
|
intersectionTable.getItems().add(new IntersectionRow(
|
||||||
|
stats.getIntersectionId(),
|
||||||
|
stats.getTotalArrivals(),
|
||||||
|
stats.getTotalDepartures(),
|
||||||
|
stats.getCurrentQueueSize()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void shutdown() {
|
||||||
|
System.out.println("Shutting down Dashboard UI...");
|
||||||
|
|
||||||
|
if (updateScheduler != null && !updateScheduler.isShutdown()) {
|
||||||
|
updateScheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server != null) {
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showErrorAlert(String title, String message) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
|
alert.setTitle(title);
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText(message);
|
||||||
|
alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner classes for TableView data models
|
||||||
|
public static class VehicleTypeRow {
|
||||||
|
private final String vehicleType;
|
||||||
|
private final int count;
|
||||||
|
private final String avgWaitTime;
|
||||||
|
|
||||||
|
public VehicleTypeRow(String vehicleType, int count, String avgWaitTime) {
|
||||||
|
this.vehicleType = vehicleType;
|
||||||
|
this.count = count;
|
||||||
|
this.avgWaitTime = avgWaitTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVehicleType() { return vehicleType; }
|
||||||
|
public int getCount() { return count; }
|
||||||
|
public String getAvgWaitTime() { return avgWaitTime; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IntersectionRow {
|
||||||
|
private final String intersectionId;
|
||||||
|
private final int arrivals;
|
||||||
|
private final int departures;
|
||||||
|
private final int queueSize;
|
||||||
|
|
||||||
|
public IntersectionRow(String intersectionId, int arrivals, int departures, int queueSize) {
|
||||||
|
this.intersectionId = intersectionId;
|
||||||
|
this.arrivals = arrivals;
|
||||||
|
this.departures = departures;
|
||||||
|
this.queueSize = queueSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIntersectionId() { return intersectionId; }
|
||||||
|
public int getArrivals() { return arrivals; }
|
||||||
|
public int getDepartures() { return departures; }
|
||||||
|
public int getQueueSize() { return queueSize; }
|
||||||
|
}
|
||||||
|
}
|
||||||
48
main/src/main/java/sd/dashboard/StatsMessage.java
Normal file
48
main/src/main/java/sd/dashboard/StatsMessage.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package sd.dashboard;
|
||||||
|
|
||||||
|
import sd.model.MessageType;
|
||||||
|
import sd.protocol.MessageProtocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message wrapper for sending statistics to the dashboard.
|
||||||
|
*/
|
||||||
|
public class StatsMessage implements MessageProtocol {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final String sourceNode;
|
||||||
|
private final String destinationNode;
|
||||||
|
private final StatsUpdatePayload payload;
|
||||||
|
|
||||||
|
public StatsMessage(String sourceNode, StatsUpdatePayload payload) {
|
||||||
|
this.sourceNode = sourceNode;
|
||||||
|
this.destinationNode = "DashboardServer";
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MessageType getType() {
|
||||||
|
return MessageType.STATS_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSourceNode() {
|
||||||
|
return sourceNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDestinationNode() {
|
||||||
|
return destinationNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("StatsMessage[from=%s, to=%s, payload=%s]",
|
||||||
|
sourceNode, destinationNode, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
main/src/main/java/sd/dashboard/StatsUpdatePayload.java
Normal file
121
main/src/main/java/sd/dashboard/StatsUpdatePayload.java
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package sd.dashboard;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import sd.model.VehicleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data transfer object for statistics updates to the dashboard.
|
||||||
|
* Use -1 for fields not being updated in this message.
|
||||||
|
*/
|
||||||
|
public class StatsUpdatePayload implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private int totalVehiclesGenerated = -1;
|
||||||
|
private int totalVehiclesCompleted = -1;
|
||||||
|
private long totalSystemTime = -1;
|
||||||
|
private long totalWaitingTime = -1;
|
||||||
|
|
||||||
|
private int intersectionArrivals = 0;
|
||||||
|
private int intersectionDepartures = 0;
|
||||||
|
private int intersectionQueueSize = 0;
|
||||||
|
|
||||||
|
private Map<VehicleType, Integer> vehicleTypeCounts;
|
||||||
|
private Map<VehicleType, Long> vehicleTypeWaitTimes;
|
||||||
|
|
||||||
|
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<VehicleType, Integer> getVehicleTypeCounts() {
|
||||||
|
return vehicleTypeCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<VehicleType, Long> getVehicleTypeWaitTimes() {
|
||||||
|
return vehicleTypeWaitTimes;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<VehicleType, Integer> vehicleTypeCounts) {
|
||||||
|
this.vehicleTypeCounts = vehicleTypeCounts;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatsUpdatePayload setVehicleTypeWaitTimes(Map<VehicleType, Long> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
main/src/main/java/sd/engine/TrafficLightThread.java
Normal file
115
main/src/main/java/sd/engine/TrafficLightThread.java
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package sd.engine;
|
||||||
|
|
||||||
|
import sd.IntersectionProcess;
|
||||||
|
import sd.config.SimulationConfig;
|
||||||
|
import sd.model.TrafficLight;
|
||||||
|
import sd.model.TrafficLightState;
|
||||||
|
import sd.model.Vehicle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the control logic for a single TrafficLight
|
||||||
|
* as a Runnable task that runs in its own Thread.
|
||||||
|
*/
|
||||||
|
public class TrafficLightThread implements Runnable {
|
||||||
|
|
||||||
|
private final TrafficLight light;
|
||||||
|
private final IntersectionProcess process;
|
||||||
|
private final SimulationConfig config;
|
||||||
|
private volatile boolean running;
|
||||||
|
|
||||||
|
// Store the thread reference for proper interruption
|
||||||
|
private Thread currentThread;
|
||||||
|
|
||||||
|
public TrafficLightThread(TrafficLight light, IntersectionProcess process, SimulationConfig config) {
|
||||||
|
this.light = light;
|
||||||
|
this.process = process;
|
||||||
|
this.config = config;
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
this.currentThread = Thread.currentThread();
|
||||||
|
this.running = true;
|
||||||
|
System.out.println("[" + light.getId() + "] Traffic light thread started.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (running && !Thread.currentThread().isInterrupted()) {
|
||||||
|
|
||||||
|
// Request permission to turn green (blocks until granted)
|
||||||
|
process.requestGreenLight(light.getDirection());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- GREEN Phase ---
|
||||||
|
light.changeState(TrafficLightState.GREEN);
|
||||||
|
System.out.println("[" + light.getId() + "] State: GREEN");
|
||||||
|
|
||||||
|
processGreenLightQueue();
|
||||||
|
|
||||||
|
if (!running || Thread.currentThread().isInterrupted()) break;
|
||||||
|
|
||||||
|
// Wait for green duration
|
||||||
|
Thread.sleep((long) (light.getGreenTime() * 1000));
|
||||||
|
|
||||||
|
if (!running || Thread.currentThread().isInterrupted()) break;
|
||||||
|
|
||||||
|
// --- RED Phase ---
|
||||||
|
light.changeState(TrafficLightState.RED);
|
||||||
|
System.out.println("[" + light.getId() + "] State: RED");
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Always release the green light permission
|
||||||
|
process.releaseGreenLight(light.getDirection());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for red duration
|
||||||
|
Thread.sleep((long) (light.getRedTime() * 1000));
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} finally {
|
||||||
|
this.running = false;
|
||||||
|
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processGreenLightQueue() throws InterruptedException {
|
||||||
|
while (running && !Thread.currentThread().isInterrupted()
|
||||||
|
&& light.getState() == TrafficLightState.GREEN
|
||||||
|
&& light.getQueueSize() > 0) {
|
||||||
|
|
||||||
|
Vehicle vehicle = light.removeVehicle();
|
||||||
|
|
||||||
|
if (vehicle != null) {
|
||||||
|
double crossingTime = getCrossingTimeForVehicle(vehicle);
|
||||||
|
|
||||||
|
Thread.sleep((long) (crossingTime * 1000));
|
||||||
|
|
||||||
|
vehicle.addCrossingTime(crossingTime);
|
||||||
|
process.getIntersection().incrementVehiclesSent();
|
||||||
|
process.sendVehicleToNextDestination(vehicle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getCrossingTimeForVehicle(Vehicle vehicle) {
|
||||||
|
return switch (vehicle.getType()) {
|
||||||
|
case BIKE -> config.getBikeVehicleCrossingTime();
|
||||||
|
case LIGHT -> config.getLightVehicleCrossingTime();
|
||||||
|
case HEAVY -> config.getHeavyVehicleCrossingTime();
|
||||||
|
default -> config.getLightVehicleCrossingTime();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the thread to stop gracefully.
|
||||||
|
* Sets the running flag and interrupts the thread to unblock any sleep() calls.
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
this.running = false;
|
||||||
|
if (currentThread != null && currentThread.isAlive()) {
|
||||||
|
currentThread.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
package sd.model;
|
package sd.model;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import sd.protocol.MessageProtocol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a message exchanged between processes in the distributed simulation.
|
* 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.
|
* Each message has a unique ID, a type, a sender, a destination, and a payload.
|
||||||
* This class implements {@link Serializable} to allow transmission over the network.
|
* This class implements {@link MessageProtocol} which extends Serializable for network transmission.
|
||||||
*/
|
*/
|
||||||
public class Message implements Serializable {
|
public class Message implements MessageProtocol {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@@ -132,6 +133,17 @@ public class Message implements Serializable {
|
|||||||
return (T) payload;
|
return (T) payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Impl MessageProtocol interface
|
||||||
|
@Override
|
||||||
|
public String getSourceNode() {
|
||||||
|
return senderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDestinationNode() {
|
||||||
|
return destinationId;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("Message[id=%s, type=%s, from=%s, to=%s, timestamp=%d]",
|
return String.format("Message[id=%s, type=%s, from=%s, to=%s, timestamp=%d]",
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ public enum MessageType {
|
|||||||
*/
|
*/
|
||||||
STATS_UPDATE,
|
STATS_UPDATE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message to synchronize simulation start time across all processes.
|
||||||
|
* Payload: Start timestamp (long milliseconds)
|
||||||
|
*/
|
||||||
|
SIMULATION_START,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message to synchronize traffic light states between processes.
|
* Message to synchronize traffic light states between processes.
|
||||||
* Payload: TrafficLight state and timing information
|
* Payload: TrafficLight state and timing information
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package sd.model;
|
package sd.model;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.locks.Condition;
|
import java.util.concurrent.locks.Condition;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
@@ -94,6 +96,12 @@ public class TrafficLight {
|
|||||||
*/
|
*/
|
||||||
private int totalVehiclesProcessed;
|
private int totalVehiclesProcessed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track when vehicles arrive at this light for wait time calculation.
|
||||||
|
* Maps vehicle ID to arrival timestamp (milliseconds).
|
||||||
|
*/
|
||||||
|
private final Map<String, Long> vehicleArrivalTimes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new TrafficLight.
|
* Constructs a new TrafficLight.
|
||||||
*
|
*
|
||||||
@@ -115,6 +123,7 @@ public class TrafficLight {
|
|||||||
|
|
||||||
this.greenTime = greenTime;
|
this.greenTime = greenTime;
|
||||||
this.redTime = redTime;
|
this.redTime = redTime;
|
||||||
|
this.vehicleArrivalTimes = new HashMap<>();
|
||||||
this.totalVehiclesProcessed = 0;
|
this.totalVehiclesProcessed = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +137,7 @@ public class TrafficLight {
|
|||||||
lock.lock(); // Acquire the lock
|
lock.lock(); // Acquire the lock
|
||||||
try {
|
try {
|
||||||
queue.offer(vehicle); // Add vehicle to queue
|
queue.offer(vehicle); // Add vehicle to queue
|
||||||
|
vehicleArrivalTimes.put(vehicle.getId(), System.currentTimeMillis());
|
||||||
vehicleAdded.signalAll(); // Signal (for concurrent models)
|
vehicleAdded.signalAll(); // Signal (for concurrent models)
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock(); // Always release the lock
|
lock.unlock(); // Always release the lock
|
||||||
@@ -152,6 +162,13 @@ public class TrafficLight {
|
|||||||
Vehicle vehicle = queue.poll(); // Remove vehicle from queue
|
Vehicle vehicle = queue.poll(); // Remove vehicle from queue
|
||||||
if (vehicle != null) {
|
if (vehicle != null) {
|
||||||
totalVehiclesProcessed++;
|
totalVehiclesProcessed++;
|
||||||
|
|
||||||
|
// Calculate wait time (time spent in queue)
|
||||||
|
Long arrivalTime = vehicleArrivalTimes.remove(vehicle.getId());
|
||||||
|
if (arrivalTime != null) {
|
||||||
|
double waitTimeSeconds = (System.currentTimeMillis() - arrivalTime) / 1000.0;
|
||||||
|
vehicle.addWaitingTime(waitTimeSeconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return vehicle;
|
return vehicle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ public class SocketConnection implements Closeable {
|
|||||||
byte[] data = new byte[length];
|
byte[] data = new byte[length];
|
||||||
dataIn.readFully(data);
|
dataIn.readFully(data);
|
||||||
|
|
||||||
// Deserialize do JSON
|
// Deserialize do JSON - use concrete Message class, not interface
|
||||||
return serializer.deserialize(data, MessageProtocol.class);
|
return serializer.deserialize(data, sd.model.Message.class);
|
||||||
|
|
||||||
} catch (SerializationException e) {
|
} catch (SerializationException e) {
|
||||||
throw new IOException("Failed to deserialize message", e);
|
throw new IOException("Failed to deserialize message", e);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectOutputStream;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -19,6 +19,7 @@ import sd.IntersectionProcess;
|
|||||||
import sd.model.MessageType;
|
import sd.model.MessageType;
|
||||||
import sd.model.Vehicle;
|
import sd.model.Vehicle;
|
||||||
import sd.model.VehicleType;
|
import sd.model.VehicleType;
|
||||||
|
import sd.protocol.SocketConnection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for IntersectionProcess - covers initialization, traffic lights,
|
* Tests for IntersectionProcess - covers initialization, traffic lights,
|
||||||
@@ -96,11 +97,17 @@ public class IntersectionProcessTest {
|
|||||||
Files.writeString(configFile, configContent);
|
Files.writeString(configFile, configContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup after tests
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
if (intersectionProcess != null) {
|
if (intersectionProcess != null) {
|
||||||
|
try {
|
||||||
|
// Only shutdown if still running
|
||||||
intersectionProcess.shutdown();
|
intersectionProcess.shutdown();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Error in tearDown: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
intersectionProcess = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +194,7 @@ public class IntersectionProcessTest {
|
|||||||
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
|
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
|
||||||
intersectionProcess.initialize();
|
intersectionProcess.initialize();
|
||||||
|
|
||||||
// start server in seperate thread
|
// start server in separate thread
|
||||||
Thread serverThread = new Thread(() -> {
|
Thread serverThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
intersectionProcess.start();
|
intersectionProcess.start();
|
||||||
@@ -197,13 +204,22 @@ public class IntersectionProcessTest {
|
|||||||
});
|
});
|
||||||
serverThread.start();
|
serverThread.start();
|
||||||
|
|
||||||
Thread.sleep(500); // wait for server to start
|
// Wait for server to actually start with retries
|
||||||
|
boolean serverReady = false;
|
||||||
// try connecting to check if its running
|
for (int i = 0; i < 20; i++) {
|
||||||
try (Socket clientSocket = new Socket("localhost", 18001)) {
|
Thread.sleep(100);
|
||||||
assertTrue(clientSocket.isConnected());
|
try (Socket testSocket = new Socket()) {
|
||||||
|
testSocket.connect(new java.net.InetSocketAddress("localhost", 18001), 500);
|
||||||
|
serverReady = true;
|
||||||
|
break;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Server not ready yet, continue waiting
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertTrue(serverReady, "Server should start and bind to port 18001");
|
||||||
|
|
||||||
|
// Shutdown immediately after confirming server is running
|
||||||
intersectionProcess.shutdown();
|
intersectionProcess.shutdown();
|
||||||
serverThread.join(2000);
|
serverThread.join(2000);
|
||||||
}
|
}
|
||||||
@@ -219,11 +235,17 @@ public class IntersectionProcessTest {
|
|||||||
cr2.initialize();
|
cr2.initialize();
|
||||||
|
|
||||||
Thread thread1 = new Thread(() -> {
|
Thread thread1 = new Thread(() -> {
|
||||||
try { cr1.start(); } catch (IOException e) { }
|
try {
|
||||||
|
cr1.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Thread thread2 = new Thread(() -> {
|
Thread thread2 = new Thread(() -> {
|
||||||
try { cr2.start(); } catch (IOException e) { }
|
try {
|
||||||
|
cr2.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
thread1.start();
|
thread1.start();
|
||||||
@@ -256,30 +278,32 @@ public class IntersectionProcessTest {
|
|||||||
Thread serverThread = new Thread(() -> {
|
Thread serverThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
intersectionProcess.start();
|
intersectionProcess.start();
|
||||||
} catch (IOException e) { }
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
serverThread.start();
|
serverThread.start();
|
||||||
|
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
|
|
||||||
// create test vehicle
|
try {
|
||||||
|
// create test vehicle - FIXED: use 4-parameter constructor
|
||||||
java.util.List<String> route = Arrays.asList("Cr2", "Cr3", "S");
|
java.util.List<String> route = Arrays.asList("Cr2", "Cr3", "S");
|
||||||
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
||||||
|
|
||||||
// send vehicle from Cr1 to Cr2
|
// send vehicle from Cr1 to Cr2 - FIXED: use SocketConnection
|
||||||
try (Socket socket = new Socket("localhost", 18002)) {
|
try (Socket socket = new Socket("localhost", 18002);
|
||||||
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
|
SocketConnection conn = new SocketConnection(socket)) {
|
||||||
|
|
||||||
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
|
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
|
||||||
out.writeObject(message);
|
conn.sendMessage(message);
|
||||||
out.flush();
|
|
||||||
|
|
||||||
Thread.sleep(1000); // wait for procesing
|
Thread.sleep(1000); // wait for processing
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
intersectionProcess.shutdown();
|
intersectionProcess.shutdown();
|
||||||
serverThread.join(2000);
|
serverThread.join(2000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// routing config tests
|
// routing config tests
|
||||||
|
|
||||||
@@ -312,7 +336,8 @@ public class IntersectionProcessTest {
|
|||||||
Thread serverThread = new Thread(() -> {
|
Thread serverThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
intersectionProcess.start();
|
intersectionProcess.start();
|
||||||
} catch (IOException e) { }
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
serverThread.start();
|
serverThread.start();
|
||||||
|
|
||||||
@@ -330,30 +355,35 @@ public class IntersectionProcessTest {
|
|||||||
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
|
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
|
||||||
intersectionProcess.initialize();
|
intersectionProcess.initialize();
|
||||||
|
|
||||||
|
// Start server in separate thread
|
||||||
Thread serverThread = new Thread(() -> {
|
Thread serverThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
intersectionProcess.start();
|
intersectionProcess.start();
|
||||||
} catch (IOException e) { }
|
} catch (IOException e) {
|
||||||
|
// Expected on shutdown
|
||||||
|
}
|
||||||
});
|
});
|
||||||
serverThread.start();
|
serverThread.start();
|
||||||
|
|
||||||
|
// Wait for server to start
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
|
|
||||||
// verify server running
|
// Shutdown
|
||||||
try (Socket socket = new Socket("localhost", 18001)) {
|
|
||||||
assertTrue(socket.isConnected());
|
|
||||||
}
|
|
||||||
|
|
||||||
intersectionProcess.shutdown();
|
intersectionProcess.shutdown();
|
||||||
serverThread.join(2000);
|
serverThread.join(2000);
|
||||||
|
|
||||||
// after shutdown conection should fail
|
// Give shutdown time to complete
|
||||||
Thread.sleep(500);
|
Thread.sleep(200);
|
||||||
Exception exception = assertThrows(IOException.class, () -> {
|
|
||||||
Socket socket = new Socket("localhost", 18001);
|
// Verify we cannot connect (server socket is closed)
|
||||||
socket.close();
|
boolean connectionFailed = false;
|
||||||
});
|
try (Socket testSocket = new Socket()) {
|
||||||
assertNotNull(exception);
|
testSocket.connect(new InetSocketAddress("localhost", 18001), 500);
|
||||||
|
} catch (IOException e) {
|
||||||
|
connectionFailed = true; // Expected - server should be closed
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(connectionFailed, "Server socket should be closed after shutdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -365,7 +395,8 @@ public class IntersectionProcessTest {
|
|||||||
Thread serverThread = new Thread(() -> {
|
Thread serverThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
intersectionProcess.start();
|
intersectionProcess.start();
|
||||||
} catch (IOException e) { }
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
serverThread.start();
|
serverThread.start();
|
||||||
|
|
||||||
@@ -388,20 +419,34 @@ public class IntersectionProcessTest {
|
|||||||
@Test
|
@Test
|
||||||
@Timeout(15)
|
@Timeout(15)
|
||||||
public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException {
|
public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException {
|
||||||
|
IntersectionProcess cr1 = null;
|
||||||
|
IntersectionProcess cr2 = null;
|
||||||
|
Thread thread1 = null;
|
||||||
|
Thread thread2 = null;
|
||||||
|
|
||||||
|
try {
|
||||||
// setup 2 intersections
|
// setup 2 intersections
|
||||||
IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
|
cr1 = new IntersectionProcess("Cr1", configFile.toString());
|
||||||
IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
|
cr2 = new IntersectionProcess("Cr2", configFile.toString());
|
||||||
|
|
||||||
cr1.initialize();
|
cr1.initialize();
|
||||||
cr2.initialize();
|
cr2.initialize();
|
||||||
|
|
||||||
// start both
|
// start both
|
||||||
Thread thread1 = new Thread(() -> {
|
final IntersectionProcess cr1Final = cr1;
|
||||||
try { cr1.start(); } catch (IOException e) { }
|
thread1 = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
cr1Final.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Thread thread2 = new Thread(() -> {
|
final IntersectionProcess cr2Final = cr2;
|
||||||
try { cr2.start(); } catch (IOException e) { }
|
thread2 = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
cr2Final.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
thread1.start();
|
thread1.start();
|
||||||
@@ -409,25 +454,34 @@ public class IntersectionProcessTest {
|
|||||||
|
|
||||||
Thread.sleep(1000); // wait for servers
|
Thread.sleep(1000); // wait for servers
|
||||||
|
|
||||||
// send vehicle to Cr1 that goes to Cr2
|
// send vehicle to Cr1 that goes to Cr2 - FIXED: use 4-parameter constructor
|
||||||
java.util.List<String> route = Arrays.asList("Cr1", "Cr2", "S");
|
java.util.List<String> route = Arrays.asList("Cr1", "Cr2", "S");
|
||||||
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
|
||||||
|
|
||||||
try (Socket socket = new Socket("localhost", 18001)) {
|
// FIXED: use SocketConnection
|
||||||
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
|
try (Socket socket = new Socket("localhost", 18001);
|
||||||
|
SocketConnection conn = new SocketConnection(socket)) {
|
||||||
|
|
||||||
TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
|
TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
|
||||||
out.writeObject(message);
|
conn.sendMessage(message);
|
||||||
out.flush();
|
|
||||||
|
|
||||||
Thread.sleep(2000); // time for processing
|
Thread.sleep(2000); // time for processing
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (cr1 != null) {
|
||||||
cr1.shutdown();
|
cr1.shutdown();
|
||||||
|
}
|
||||||
|
if (cr2 != null) {
|
||||||
cr2.shutdown();
|
cr2.shutdown();
|
||||||
|
}
|
||||||
|
if (thread1 != null) {
|
||||||
thread1.join(2000);
|
thread1.join(2000);
|
||||||
|
}
|
||||||
|
if (thread2 != null) {
|
||||||
thread2.join(2000);
|
thread2.join(2000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMain_MissingArguments() {
|
public void testMain_MissingArguments() {
|
||||||
|
|||||||
327
main/src/test/java/sd/ExitNodeProcessTest.java
Normal file
327
main/src/test/java/sd/ExitNodeProcessTest.java
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
package sd;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.Timeout;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import sd.config.SimulationConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testes unitários para a classe ExitNodeProcess.
|
||||||
|
*
|
||||||
|
* Esta classe de testes verifica:
|
||||||
|
* - Construção e inicialização do processo
|
||||||
|
* - Criação e aceitação de conexões do servidor socket
|
||||||
|
* - Gestão do ciclo de vida (start/shutdown)
|
||||||
|
* - Processamento concorrente de múltiplas conexões
|
||||||
|
* - Impressão de estatísticas finais
|
||||||
|
*
|
||||||
|
* Os testes utilizam configurações temporárias e portas dedicadas (19001)
|
||||||
|
* para evitar conflitos com outros testes ou processos em execução.
|
||||||
|
*/
|
||||||
|
public class ExitNodeProcessTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private Path configFile;
|
||||||
|
private ExitNodeProcess exitNodeProcess;
|
||||||
|
private Thread exitNodeThread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configura o ambiente de teste antes de cada teste.
|
||||||
|
* Cria um ficheiro de configuração temporário com as definições necessárias.
|
||||||
|
*/
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() throws IOException {
|
||||||
|
configFile = tempDir.resolve("test-simulation.properties");
|
||||||
|
|
||||||
|
String configContent = """
|
||||||
|
# Test Exit Node Configuration
|
||||||
|
|
||||||
|
# Exit Configuration
|
||||||
|
exit.host=localhost
|
||||||
|
exit.port=19001
|
||||||
|
|
||||||
|
# Dashboard Configuration (will not be running in tests)
|
||||||
|
dashboard.host=localhost
|
||||||
|
dashboard.port=19000
|
||||||
|
|
||||||
|
# Vehicle Crossing Times
|
||||||
|
vehicle.bike.crossingTime=2.0
|
||||||
|
vehicle.light.crossingTime=3.0
|
||||||
|
vehicle.heavy.crossingTime=5.0
|
||||||
|
|
||||||
|
# Simulation Duration
|
||||||
|
simulation.duration=60.0
|
||||||
|
""";
|
||||||
|
|
||||||
|
Files.writeString(configFile, configContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa os recursos após cada teste.
|
||||||
|
* Garante que o processo e threads são terminados corretamente.
|
||||||
|
*/
|
||||||
|
@AfterEach
|
||||||
|
public void tearDown() {
|
||||||
|
if (exitNodeProcess != null) {
|
||||||
|
exitNodeProcess.shutdown();
|
||||||
|
}
|
||||||
|
if (exitNodeThread != null && exitNodeThread.isAlive()) {
|
||||||
|
exitNodeThread.interrupt();
|
||||||
|
try {
|
||||||
|
exitNodeThread.join(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa a construção bem-sucedida do ExitNodeProcess com configuração válida.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConstructor_Success() throws IOException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
assertNotNull(exitNodeProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa que uma exceção é lançada quando a configuração é inválida.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testConstructor_InvalidConfig() {
|
||||||
|
Exception exception = assertThrows(IOException.class, () -> {
|
||||||
|
new SimulationConfig("non-existent-config.properties");
|
||||||
|
});
|
||||||
|
assertNotNull(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa a inicialização sem dashboard disponível.
|
||||||
|
* Verifica que o processo continua a funcionar mesmo sem conexão ao dashboard.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testInitialize_WithoutDashboard() throws IOException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
assertDoesNotThrow(() -> exitNodeProcess.initialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa que o servidor socket é criado corretamente na porta configurada.
|
||||||
|
* Verifica que é possível estabelecer uma conexão ao socket do servidor.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||||
|
public void testStart_ServerSocketCreated() throws IOException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
exitNodeProcess.initialize();
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
exitNodeThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
latch.countDown();
|
||||||
|
exitNodeProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// expected when shutdown
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exitNodeThread.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
assertTrue(latch.await(2, TimeUnit.SECONDS), "Exit node should start within timeout");
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> {
|
||||||
|
try (Socket testSocket = new Socket("localhost", 19001)) {
|
||||||
|
assertTrue(testSocket.isConnected());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa que o servidor aceita conexões de clientes.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||||
|
public void testStart_AcceptsConnection() throws IOException, InterruptedException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
exitNodeProcess.initialize();
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
exitNodeThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
latch.countDown();
|
||||||
|
exitNodeProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exitNodeThread.start();
|
||||||
|
|
||||||
|
assertTrue(latch.await(2, TimeUnit.SECONDS));
|
||||||
|
Thread.sleep(200);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> {
|
||||||
|
try (Socket socket = new Socket("localhost", 19001)) {
|
||||||
|
assertTrue(socket.isConnected());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa múltiplas inicializações e encerramentos do processo.
|
||||||
|
* Verifica que o processo pode ser iniciado e parado múltiplas vezes,
|
||||||
|
* permitindo reutilização da porta.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||||
|
public void testMultipleStartStop() throws IOException, InterruptedException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
exitNodeProcess.initialize();
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
exitNodeThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
latch.countDown();
|
||||||
|
exitNodeProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exitNodeThread.start();
|
||||||
|
assertTrue(latch.await(2, TimeUnit.SECONDS));
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
exitNodeProcess.shutdown();
|
||||||
|
Thread.sleep(100);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> {
|
||||||
|
SimulationConfig config2 = new SimulationConfig(configFile.toString());
|
||||||
|
ExitNodeProcess exitNode2 = new ExitNodeProcess(config2);
|
||||||
|
exitNode2.initialize();
|
||||||
|
exitNode2.shutdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa que o shutdown fecha corretamente o servidor socket.
|
||||||
|
* Após o shutdown, novas conexões ao socket devem falhar.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||||
|
public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
exitNodeProcess.initialize();
|
||||||
|
|
||||||
|
CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
exitNodeThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
startLatch.countDown();
|
||||||
|
exitNodeProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exitNodeThread.start();
|
||||||
|
assertTrue(startLatch.await(2, TimeUnit.SECONDS));
|
||||||
|
Thread.sleep(200);
|
||||||
|
|
||||||
|
exitNodeProcess.shutdown();
|
||||||
|
Thread.sleep(200);
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> {
|
||||||
|
Socket socket = new Socket("localhost", 19001);
|
||||||
|
socket.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa que as estatísticas finais são impressas corretamente durante o shutdown.
|
||||||
|
* Verifica que o método não lança exceções mesmo sem dados processados.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testPrintFinalStatistics() throws IOException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
exitNodeProcess.initialize();
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> exitNodeProcess.shutdown());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testa o processamento de múltiplas conexões concorrentes.
|
||||||
|
* Verifica que o servidor consegue lidar com vários clientes simultaneamente
|
||||||
|
* usando o pool de threads.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@Timeout(value = 3, unit = TimeUnit.SECONDS)
|
||||||
|
public void testMultipleConcurrentConnections() throws IOException, InterruptedException {
|
||||||
|
SimulationConfig config = new SimulationConfig(configFile.toString());
|
||||||
|
exitNodeProcess = new ExitNodeProcess(config);
|
||||||
|
exitNodeProcess.initialize();
|
||||||
|
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
exitNodeThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
latch.countDown();
|
||||||
|
exitNodeProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exitNodeThread.start();
|
||||||
|
assertTrue(latch.await(2, TimeUnit.SECONDS));
|
||||||
|
Thread.sleep(200);
|
||||||
|
|
||||||
|
Thread[] clients = new Thread[3];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
clients[i] = new Thread(() -> {
|
||||||
|
try (Socket socket = new Socket("localhost", 19001)) {
|
||||||
|
assertTrue(socket.isConnected());
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clients[i].start();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Thread client : clients) {
|
||||||
|
client.join(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
206
main/src/test/java/sd/TrafficLightCoordinationTest.java
Normal file
206
main/src/test/java/sd/TrafficLightCoordinationTest.java
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package sd;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import sd.model.TrafficLight;
|
||||||
|
import sd.model.TrafficLightState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test class to verify traffic light coordination within an intersection.
|
||||||
|
* Ensures that only ONE traffic light can be GREEN at any given time.
|
||||||
|
*/
|
||||||
|
public class TrafficLightCoordinationTest {
|
||||||
|
|
||||||
|
private IntersectionProcess intersectionProcess;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() throws IOException {
|
||||||
|
// Create an intersection with multiple traffic lights
|
||||||
|
intersectionProcess = new IntersectionProcess("Cr2", "src/main/resources/simulation.properties");
|
||||||
|
intersectionProcess.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void tearDown() throws InterruptedException {
|
||||||
|
if (intersectionProcess != null) {
|
||||||
|
intersectionProcess.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that verifies mutual exclusion between traffic lights.
|
||||||
|
* Monitors all traffic lights for 10 seconds and ensures that
|
||||||
|
* at most ONE light is GREEN at any point in time.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testOnlyOneGreenLightAtATime() throws InterruptedException {
|
||||||
|
System.out.println("\n=== Testing Traffic Light Mutual Exclusion ===");
|
||||||
|
|
||||||
|
// Start the intersection
|
||||||
|
Thread intersectionThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
intersectionProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
intersectionThread.start();
|
||||||
|
|
||||||
|
// Monitor traffic lights for violations
|
||||||
|
AtomicInteger maxGreenSimultaneously = new AtomicInteger(0);
|
||||||
|
AtomicInteger violationCount = new AtomicInteger(0);
|
||||||
|
List<String> violations = new ArrayList<>();
|
||||||
|
|
||||||
|
// Monitor for 10 seconds
|
||||||
|
long endTime = System.currentTimeMillis() + 10000;
|
||||||
|
|
||||||
|
while (System.currentTimeMillis() < endTime) {
|
||||||
|
int greenCount = 0;
|
||||||
|
StringBuilder currentState = new StringBuilder("States: ");
|
||||||
|
|
||||||
|
for (TrafficLight light : intersectionProcess.getIntersection().getTrafficLights()) {
|
||||||
|
TrafficLightState state = light.getState();
|
||||||
|
currentState.append(light.getDirection()).append("=").append(state).append(" ");
|
||||||
|
|
||||||
|
if (state == TrafficLightState.GREEN) {
|
||||||
|
greenCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update maximum simultaneous green lights
|
||||||
|
if (greenCount > maxGreenSimultaneously.get()) {
|
||||||
|
maxGreenSimultaneously.set(greenCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for violations (more than one green)
|
||||||
|
if (greenCount > 1) {
|
||||||
|
violationCount.incrementAndGet();
|
||||||
|
String violation = String.format("[VIOLATION] %d lights GREEN simultaneously: %s",
|
||||||
|
greenCount, currentState.toString());
|
||||||
|
violations.add(violation);
|
||||||
|
System.err.println(violation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(50); // Check every 50ms
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("\n=== Test Results ===");
|
||||||
|
System.out.println("Maximum simultaneous GREEN lights: " + maxGreenSimultaneously.get());
|
||||||
|
System.out.println("Total violations detected: " + violationCount.get());
|
||||||
|
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
System.err.println("\nViolation details:");
|
||||||
|
violations.forEach(System.err::println);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that we never had more than one green light
|
||||||
|
assertEquals(0, violationCount.get(),
|
||||||
|
"Traffic light coordination violated! Multiple lights were GREEN simultaneously.");
|
||||||
|
assertTrue(maxGreenSimultaneously.get() <= 1,
|
||||||
|
"At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get());
|
||||||
|
|
||||||
|
System.out.println("\nTraffic light coordination working correctly!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that verifies all traffic lights get a chance to be GREEN.
|
||||||
|
* Ensures fairness in the coordination mechanism.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testAllLightsGetGreenTime() throws InterruptedException {
|
||||||
|
System.out.println("\n=== Testing Traffic Light Fairness ===");
|
||||||
|
|
||||||
|
// Start the intersection
|
||||||
|
Thread intersectionThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
intersectionProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
intersectionThread.start();
|
||||||
|
|
||||||
|
// Track which lights have been green
|
||||||
|
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
|
||||||
|
boolean[] hasBeenGreen = new boolean[lights.size()];
|
||||||
|
|
||||||
|
// Monitor for 15 seconds (enough time for all lights to cycle)
|
||||||
|
long endTime = System.currentTimeMillis() + 15000;
|
||||||
|
|
||||||
|
while (System.currentTimeMillis() < endTime) {
|
||||||
|
for (int i = 0; i < lights.size(); i++) {
|
||||||
|
if (lights.get(i).getState() == TrafficLightState.GREEN) {
|
||||||
|
hasBeenGreen[i] = true;
|
||||||
|
System.out.println("✓ " + lights.get(i).getDirection() + " has been GREEN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all lights got green time
|
||||||
|
int greenCount = 0;
|
||||||
|
System.out.println("\n=== Fairness Results ===");
|
||||||
|
for (int i = 0; i < lights.size(); i++) {
|
||||||
|
String status = hasBeenGreen[i] ? "✓ YES" : "✗ NO";
|
||||||
|
System.out.println(lights.get(i).getDirection() + " got GREEN time: " + status);
|
||||||
|
if (hasBeenGreen[i]) greenCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(greenCount > 0, "At least one light should have been GREEN during the test");
|
||||||
|
System.out.println("\n" + greenCount + "/" + lights.size() + " lights were GREEN during test period");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that verifies the state transitions are consistent.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testStateTransitionsAreConsistent() throws InterruptedException {
|
||||||
|
System.out.println("\n=== Testing State Transition Consistency ===");
|
||||||
|
|
||||||
|
Thread intersectionThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
intersectionProcess.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
intersectionThread.start();
|
||||||
|
|
||||||
|
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
|
||||||
|
TrafficLightState[] previousStates = new TrafficLightState[lights.size()];
|
||||||
|
|
||||||
|
// Initialize previous states
|
||||||
|
for (int i = 0; i < lights.size(); i++) {
|
||||||
|
previousStates[i] = lights.get(i).getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
int transitionCount = 0;
|
||||||
|
long endTime = System.currentTimeMillis() + 8000;
|
||||||
|
|
||||||
|
while (System.currentTimeMillis() < endTime) {
|
||||||
|
for (int i = 0; i < lights.size(); i++) {
|
||||||
|
TrafficLightState currentState = lights.get(i).getState();
|
||||||
|
|
||||||
|
if (currentState != previousStates[i]) {
|
||||||
|
transitionCount++;
|
||||||
|
System.out.println(lights.get(i).getDirection() + " transitioned: " +
|
||||||
|
previousStates[i] + " → " + currentState);
|
||||||
|
previousStates[i] = currentState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("\nTotal state transitions observed: " + transitionCount);
|
||||||
|
assertTrue(transitionCount > 0, "There should be state transitions during the test period");
|
||||||
|
}
|
||||||
|
}
|
||||||
164
main/src/test/java/sd/dashboard/DashboardTest.java
Normal file
164
main/src/test/java/sd/dashboard/DashboardTest.java
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package sd.dashboard;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import sd.config.SimulationConfig;
|
||||||
|
import sd.model.VehicleType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for Dashboard Server components.
|
||||||
|
*/
|
||||||
|
class DashboardTest {
|
||||||
|
|
||||||
|
private DashboardStatistics statistics;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
statistics = new DashboardStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
statistics = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInitialStatistics() {
|
||||||
|
assertEquals(0, statistics.getTotalVehiclesGenerated(),
|
||||||
|
"Initial vehicles generated should be 0");
|
||||||
|
assertEquals(0, statistics.getTotalVehiclesCompleted(),
|
||||||
|
"Initial vehicles completed should be 0");
|
||||||
|
assertEquals(0.0, statistics.getAverageSystemTime(),
|
||||||
|
"Initial average system time should be 0.0");
|
||||||
|
assertEquals(0.0, statistics.getAverageWaitingTime(),
|
||||||
|
"Initial average waiting time should be 0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testVehicleCounters() {
|
||||||
|
statistics.incrementVehiclesGenerated();
|
||||||
|
assertEquals(1, statistics.getTotalVehiclesGenerated());
|
||||||
|
|
||||||
|
statistics.updateVehiclesGenerated(10);
|
||||||
|
assertEquals(10, statistics.getTotalVehiclesGenerated());
|
||||||
|
|
||||||
|
statistics.incrementVehiclesCompleted();
|
||||||
|
assertEquals(1, statistics.getTotalVehiclesCompleted());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAverageCalculations() {
|
||||||
|
// Add 3 completed vehicles with known times
|
||||||
|
statistics.updateVehiclesCompleted(3);
|
||||||
|
statistics.addSystemTime(3000); // 3000ms total
|
||||||
|
statistics.addWaitingTime(1500); // 1500ms total
|
||||||
|
|
||||||
|
assertEquals(1000.0, statistics.getAverageSystemTime(), 0.01,
|
||||||
|
"Average system time should be 1000ms");
|
||||||
|
assertEquals(500.0, statistics.getAverageWaitingTime(), 0.01,
|
||||||
|
"Average waiting time should be 500ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testVehicleTypeStatistics() {
|
||||||
|
statistics.incrementVehicleType(VehicleType.LIGHT);
|
||||||
|
statistics.incrementVehicleType(VehicleType.LIGHT);
|
||||||
|
statistics.incrementVehicleType(VehicleType.HEAVY);
|
||||||
|
|
||||||
|
assertEquals(2, statistics.getVehicleTypeCount(VehicleType.LIGHT));
|
||||||
|
assertEquals(1, statistics.getVehicleTypeCount(VehicleType.HEAVY));
|
||||||
|
assertEquals(0, statistics.getVehicleTypeCount(VehicleType.BIKE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIntersectionStatistics() {
|
||||||
|
statistics.updateIntersectionStats("Cr1", 10, 8, 2);
|
||||||
|
|
||||||
|
DashboardStatistics.IntersectionStats stats =
|
||||||
|
statistics.getIntersectionStats("Cr1");
|
||||||
|
|
||||||
|
assertNotNull(stats, "Intersection stats should not be null");
|
||||||
|
assertEquals("Cr1", stats.getIntersectionId());
|
||||||
|
assertEquals(10, stats.getTotalArrivals());
|
||||||
|
assertEquals(8, stats.getTotalDepartures());
|
||||||
|
assertEquals(2, stats.getCurrentQueueSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleIntersections() {
|
||||||
|
statistics.updateIntersectionStats("Cr1", 10, 8, 2);
|
||||||
|
statistics.updateIntersectionStats("Cr2", 15, 12, 3);
|
||||||
|
statistics.updateIntersectionStats("Cr3", 5, 5, 0);
|
||||||
|
|
||||||
|
assertEquals(3, statistics.getAllIntersectionStats().size(),
|
||||||
|
"Should have 3 intersections");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStatsUpdatePayload() {
|
||||||
|
StatsUpdatePayload payload = new StatsUpdatePayload()
|
||||||
|
.setTotalVehiclesGenerated(50)
|
||||||
|
.setTotalVehiclesCompleted(20)
|
||||||
|
.setIntersectionArrivals(30)
|
||||||
|
.setIntersectionDepartures(25)
|
||||||
|
.setIntersectionQueueSize(5);
|
||||||
|
|
||||||
|
assertEquals(50, payload.getTotalVehiclesGenerated());
|
||||||
|
assertEquals(20, payload.getTotalVehiclesCompleted());
|
||||||
|
assertEquals(30, payload.getIntersectionArrivals());
|
||||||
|
assertEquals(25, payload.getIntersectionDepartures());
|
||||||
|
assertEquals(5, payload.getIntersectionQueueSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStatsMessage() {
|
||||||
|
StatsUpdatePayload payload = new StatsUpdatePayload()
|
||||||
|
.setIntersectionArrivals(10);
|
||||||
|
|
||||||
|
StatsMessage message = new StatsMessage("Cr1", payload);
|
||||||
|
|
||||||
|
assertEquals("Cr1", message.getSourceNode());
|
||||||
|
assertEquals("DashboardServer", message.getDestinationNode());
|
||||||
|
assertEquals(sd.model.MessageType.STATS_UPDATE, message.getType());
|
||||||
|
assertNotNull(message.getPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testThreadSafety() throws InterruptedException {
|
||||||
|
// Test concurrent updates
|
||||||
|
Thread t1 = new Thread(() -> {
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
statistics.incrementVehiclesGenerated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread t2 = new Thread(() -> {
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
statistics.incrementVehiclesGenerated();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
t1.start();
|
||||||
|
t2.start();
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
|
||||||
|
assertEquals(200, statistics.getTotalVehiclesGenerated(),
|
||||||
|
"Concurrent increments should total 200");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDashboardServerCreation() throws Exception {
|
||||||
|
SimulationConfig config = new SimulationConfig("simulation.properties");
|
||||||
|
DashboardServer server = new DashboardServer(config);
|
||||||
|
|
||||||
|
assertNotNull(server, "Server should be created successfully");
|
||||||
|
assertNotNull(server.getStatistics(), "Statistics should be initialized");
|
||||||
|
assertFalse(server.isRunning(), "Server should not be running initially");
|
||||||
|
}
|
||||||
|
}
|
||||||
60
main/start.sh
Executable file
60
main/start.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Distributed Traffic Simulation Startup Script
|
||||||
|
|
||||||
|
# kill java
|
||||||
|
echo "-> Cleaning up existing processes..."
|
||||||
|
pkill -9 java 2>/dev/null
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# build
|
||||||
|
echo "-> Building project..."
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
mvn package -DskipTests -q
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "XXX Build failed! XXX"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "-> Build complete"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# start gui
|
||||||
|
echo "-> Starting JavaFX Dashboard..."
|
||||||
|
mvn javafx:run &
|
||||||
|
DASHBOARD_PID=$!
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# acho que é assim idk
|
||||||
|
echo "-> Starting 5 Intersection processes..."
|
||||||
|
for id in Cr1 Cr2 Cr3 Cr4 Cr5; do
|
||||||
|
java -cp target/classes:target/main-1.0-SNAPSHOT.jar sd.IntersectionProcess $id > /tmp/$(echo $id | tr '[:upper:]' '[:lower:]').log 2>&1 &
|
||||||
|
echo "[SUCCESS] Started $id"
|
||||||
|
done
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# exit
|
||||||
|
echo "-> Starting Exit Node..."
|
||||||
|
java -cp target/classes:target/main-1.0-SNAPSHOT.jar sd.ExitNodeProcess > /tmp/exit.log 2>&1 &
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# coordinator
|
||||||
|
echo "-> Starting Coordinator..."
|
||||||
|
java -cp target/classes:target/main-1.0-SNAPSHOT.jar sd.coordinator.CoordinatorProcess > /tmp/coordinator.log 2>&1 &
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "-> All processes started!"
|
||||||
|
echo ""
|
||||||
|
echo "-> System Status:"
|
||||||
|
ps aux | grep "java.*sd\." | grep -v grep | wc -l | xargs -I {} echo " {} Java processes running"
|
||||||
|
echo ""
|
||||||
|
echo " IMPORTANT: Keep the JavaFX Dashboard window OPEN for 60+ seconds"
|
||||||
|
echo " to see live updates! The simulation runs for 60 seconds."
|
||||||
|
echo ""
|
||||||
|
echo "-> Logs available at:"
|
||||||
|
echo " Dashboard: Check JavaFX window (live updates)"
|
||||||
|
echo " Intersections: /tmp/cr*.log"
|
||||||
|
echo " Exit Node: /tmp/exit.log"
|
||||||
|
echo " Coordinator: /tmp/coordinator.log"
|
||||||
|
echo ""
|
||||||
|
echo "-> To stop all processes: pkill -9 java"
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user