diff --git a/.gitignore b/.gitignore index 5c91afc..8b0c728 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ # Log files *.log +*.trace +logs +*.md # BlueJ files *.ctxt @@ -50,4 +53,7 @@ build/ *.pdf # JAR built pom file -dependency-reduced-pom.xml \ No newline at end of file +dependency-reduced-pom.xml + +# Python env +venv/ \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index cc47c34..0000000 --- a/README.md +++ /dev/null @@ -1,620 +0,0 @@ -# Sistema de Simulação de Tráfego Distribuído - -Sistema distribuído de simulação de tráfego. ---- - -## Índice - -- [Visão Geral](#visão-geral) -- [Arquitetura](#arquitetura) -- [Protocolo de Comunicação](#protocolo-de-comunicação) -- [Estrutura do Projeto](#estrutura-do-projeto) -- [Instalação e Execução](#instalação-e-execução) -- [Documentação](#documentação) -- [Desenvolvimento](#desenvolvimento) - ---- - -## Visão Geral - -Este projeto implementa uma simulação distribuída de tráfego veicular numa rede de cruzamentos. O sistema utiliza: - -- **Processos independentes** para cada cruzamento -- **Threads** para controlar os semáforos dentro de cada cruzamento -- **Comunicação via sockets** para transferência de veículos entre cruzamentos -- **Simulação de eventos discretos** (DES) para gerir o tempo de simulação - -### Características Principais - -- Simulação determinística e reproduzível -- Comunicação assíncrona entre processos -- Protocolo de mensagens baseado em JSON -- Dashboard em tempo real (planeado) -- Estatísticas detalhadas de desempenho - ---- - -## Arquitetura - -### Visão Geral do Sistema - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ SISTEMA DISTRIBUÍDO │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────┐ │ -│ │ Coordenador │ ────────────────────────>│ Dashboard │ │ -│ │ / Gerador │ │ │ -│ └──────┬───────┘ └──────▲───────┘ │ -│ │ │ │ -│ │ Gera veículos Stats │ │ -│ │ │ │ -│ ▼ │ │ -│ ┌─────────────────────────────────────────────────┴──────┐ │ -│ │ Rede de Cruzamentos (Processos) │ │ -│ │ │ │ -│ │ ┌────┐ ┌────┐ ┌────┐ │ │ -│ │ │Cr1 │◄───────►│Cr2 │◄───────►│Cr3 │ │ │ -│ │ └─┬──┘ └─┬──┘ └─┬──┘ │ │ -│ │ │ │ │ │ │ -│ │ │ ┌────▼────┐ │ │ │ -│ │ └────────►│ Cr4 │◄────────┘ │ │ -│ │ └────┬────┘ │ │ -│ │ │ │ │ -│ │ ┌────▼────┐ │ │ -│ │ │ Cr5 │ │ │ -│ │ └────┬────┘ │ │ -│ └───────────────────┼─────────────────────────────────────┤ │ -│ │ │ │ -│ ▼ │ │ -│ ┌──────────────┐ │ │ -│ │ Nó de Saída │ │ │ -│ │ (S) │ │ │ -│ └──────────────┘ │ │ -│ │ │ -└────────────────────────────────────────────────────────────┘ │ -``` - -### Componentes - -1. **Coordenador/Gerador**: Gera veículos e injeta no sistema -2. **Cruzamentos (Cr1-Cr5)**: Processos independentes que gerem tráfego local -3. **Nó de Saída (S)**: Recolhe estatísticas de veículos que saem do sistema -4. **Dashboard Server**: Agrega e exibe dados em tempo real - ---- - -## Protocolo de Comunicação - -### Formato de Serialização: JSON (Gson) - -O sistema utiliza JSON como formato de serialização por ser mais rápido, seguro e legível que a serialização em Java. - -### Estrutura de Mensagens - -Todas as mensagens seguem o formato base: - -```json -{ - "messageId": "uuid", - "type": "MESSAGE_TYPE", - "senderId": "sender_id", - "destinationId": "destination_id", - "timestamp": 1729595234567, - "payload": { ... } -} -``` - -### Tipos de Mensagens - -#### 1. VEHICLE_TRANSFER - -Transfere um veículo entre cruzamentos. - -**Estrutura:** -```json -{ - "messageId": "a3c5e7f9-1234-5678-90ab-cdef12345678", - "type": "VEHICLE_TRANSFER", - "senderId": "Cr1", - "destinationId": "Cr2", - "timestamp": 1729595234567, - "payload": { - "id": "V123", - "type": "LIGHT", - "entryTime": 15.7, - "route": ["Cr1", "Cr2", "Cr5", "S"], - "currentRouteIndex": 1, - "totalWaitingTime": 3.2, - "totalCrossingTime": 1.8 - } -} -``` - -**Fluxo:** -1. Veículo completa travessia no Cr1 -2. Cr1 serializa mensagem VEHICLE_TRANSFER -3. Envia para Cr2 via socket -4. Cr2 desserializa e adiciona veículo à fila - -#### 2. STATS_UPDATE - -Envia estatísticas de um cruzamento para o Dashboard. - -**Estrutura:** -```json -{ - "messageId": "b4d6e8f0-2345-6789-01bc-def123456789", - "type": "STATS_UPDATE", - "senderId": "Cr3", - "destinationId": "Dashboard", - "timestamp": 1729595234789, - "payload": { - "intersectionId": "Cr3", - "queueLengths": { - "North": 5, - "South": 3, - "East": 7, - "West": 2 - }, - "vehiclesProcessed": 142, - "averageWaitTime": 4.5, - "currentTime": 123.45 - } -} -``` - -**Frequência:** A cada 10 segundos (configurável) - -#### 3. VEHICLE_EXIT - -Notifica quando um veículo sai do sistema. - -**Estrutura:** -```json -{ - "messageId": "c5e7f9a1-3456-7890-12bc-def123456789", - "type": "VEHICLE_EXIT", - "senderId": "Cr5", - "destinationId": "ExitNode", - "timestamp": 1729595234890, - "payload": { - "id": "V123", - "type": "LIGHT", - "entryTime": 15.7, - "exitTime": 45.2, - "totalSystemTime": 29.5, - "totalWaitingTime": 8.3, - "totalCrossingTime": 4.8, - "routeTaken": ["Cr1", "Cr2", "Cr5", "S"] - } -} -``` - -#### 4. HEARTBEAT - -Mantém a ligação ativa e monitoriza a saúde dos processos. - -**Estrutura:** -```json -{ - "messageId": "d6e8f0a2-4567-8901-23cd-ef1234567890", - "type": "HEARTBEAT", - "senderId": "Cr1", - "destinationId": "Coordinator", - "timestamp": 1729595235000, - "payload": { - "status": "RUNNING", - "uptime": 120.5, - "vehiclesInQueue": 12 - } -} -``` - -**Frequência:** A cada 5 segundos - -#### 5. LIGHT_CHANGE - -Notifica mudança de estado de semáforo (para logging/debugging). - -**Estrutura:** -```json -{ - "messageId": "e7f9a1b3-5678-9012-34de-f12345678901", - "type": "LIGHT_CHANGE", - "senderId": "Cr1-North", - "destinationId": "Dashboard", - "timestamp": 1729595235100, - "payload": { - "lightId": "Cr1-North", - "previousState": "RED", - "newState": "GREEN", - "queueSize": 5 - } -} -``` - -### Tipos de Veículos - -```json -{ - "BIKE": { - "probability": 0.20, - "crossingTime": 1.5 - }, - "LIGHT": { - "probability": 0.60, - "crossingTime": 2.0 - }, - "HEAVY": { - "probability": 0.20, - "crossingTime": 4.0 - } -} -``` - -### Estados dos Semáforos - -``` -RED → Veículos aguardam na fila -GREEN → Veículos podem atravessar -``` - -### Exemplo de Comunicação Completa - -``` -Tempo Processo Ação Mensagem ------- --------- ------------------------------------- ------------------ -15.7s Gerador Gera veículo V123 - -15.7s Gerador → Injeta V123 em Cr1 VEHICLE_TRANSFER -18.2s Cr1 V123 inicia travessia - -20.2s Cr1 V123 completa travessia - -20.2s Cr1 → Cr2 Transfere V123 para Cr2 VEHICLE_TRANSFER -23.5s Cr2 V123 inicia travessia - -25.5s Cr2 V123 completa travessia - -25.5s Cr2 → Cr5 Transfere V123 para Cr5 VEHICLE_TRANSFER -28.0s Cr5 V123 inicia travessia - -30.0s Cr5 V123 completa travessia - -30.0s Cr5 → Exit V123 sai do sistema VEHICLE_EXIT -30.0s Exit → Dash Estatísticas de V123 STATS_UPDATE -``` - ---- - -## Estrutura do Projeto - -``` -Trabalho-Pratico-SD/ -├── README.md # Este ficheiro -├── TODO.md # Plano de desenvolvimento -├── main/ -│ ├── pom.xml # Configuração do Maven -│ ├── docs/ -│ │ ├── README.md # Índice da documentação -│ │ ├── SERIALIZATION_SPECIFICATION.md -│ │ ├── SERIALIZATION_DECISION.md -│ │ ├── SERIALIZATION_SUMMARY.md -│ │ └── SERIALIZATION_ARCHITECTURE.md -│ ├── src/ -│ │ ├── main/java/sd/ -│ │ │ ├── Entry.java # Ponto de entrada -│ │ │ ├── config/ -│ │ │ │ └── SimulationConfig.java -│ │ │ ├── engine/ -│ │ │ │ └── SimulationEngine.java -│ │ │ ├── model/ -│ │ │ │ ├── Event.java -│ │ │ │ ├── EventType.java -│ │ │ │ ├── Intersection.java -│ │ │ │ ├── Message.java # Estrutura de mensagens -│ │ │ │ ├── MessageType.java # Tipos de mensagens -│ │ │ │ ├── TrafficLight.java -│ │ │ │ ├── Vehicle.java -│ │ │ │ └── VehicleType.java -│ │ │ ├── serialization/ # Sistema de serialização -│ │ │ │ ├── MessageSerializer.java -│ │ │ │ ├── SerializationException.java -│ │ │ │ ├── JsonMessageSerializer.java -│ │ │ │ ├── SerializerFactory.java -│ │ │ │ ├── SerializationExample.java -│ │ │ │ └── README.md -│ │ │ └── util/ -│ │ │ ├── RandomGenerator.java -│ │ │ ├── StatisticsCollector.java -│ │ │ └── VehicleGenerator.java -│ │ └── test/java/ -│ │ ├── SimulationTest.java -│ │ └── sd/serialization/ -│ │ └── SerializationTest.java -│ └── target/ # Ficheiros compilados -└── .vscode/ # Configuração do VS Code -``` - ---- - -## Instalação e Execução - -### Pré-requisitos - -- **Java 17** ou superior -- **Maven 3.8+** -- **Git** - -### Instalação - -```bash -# Clonar o repositório -git clone https://github.com/davidalves04/Trabalho-Pratico-SD.git -cd Trabalho-Pratico-SD/main - -# Compilar o projeto -mvn clean compile - -# Executar os testes -mvn test -``` - -### Execução - -#### Simulação Básica (Single Process) - -```bash -mvn exec:java -Dexec.mainClass="sd.Entry" -``` - -#### Exemplo de Serialização - -```bash -mvn exec:java -Dexec.mainClass="sd.serialization.SerializationExample" -``` - -#### Configuração - -Editar `src/main/resources/simulation.properties`: - -```properties -# Duração da simulação (segundos) -simulation.duration=60.0 - -# Modelo de chegada: FIXED ou POISSON -arrival.model=POISSON - -# Taxa de chegada (veículos/segundo) -arrival.rate=0.5 - -# Intervalo de atualização de estatísticas (segundos) -stats.update.interval=10.0 - -# Distribuição de tipos de veículos -vehicle.type.bike.probability=0.20 -vehicle.type.light.probability=0.60 -vehicle.type.heavy.probability=0.20 - -# Tempos de travessia por tipo (segundos) -vehicle.type.bike.crossing.time=1.5 -vehicle.type.light.crossing.time=2.0 -vehicle.type.heavy.crossing.time=4.0 -``` - ---- - -## Documentação - -### Documentação de Serialização - -A documentação completa sobre o protocolo de serialização está disponível em: - -- **[Índice Completo](./main/docs/README.md)** - Navegação da documentação -- **[Especificação](./main/docs/SERIALIZATION_SPECIFICATION.md)** - Design detalhado -- **[Guia de Decisão](./main/docs/SERIALIZATION_DECISION.md)** - Porquê JSON? -- **[Resumo](./main/docs/SERIALIZATION_SUMMARY.md)** - Estado de implementação -- **[Arquitetura](./main/docs/SERIALIZATION_ARCHITECTURE.md)** - Diagramas visuais - -### Guias de Utilização - -- **[Serialization README](./main/src/main/java/sd/serialization/README.md)** - Como utilizar os serializers - -### Exemplos de Código - -```java -// Criar serializer -MessageSerializer serializer = SerializerFactory.createDefault(); - -// Serializar mensagem -Vehicle vehicle = new Vehicle("V123", VehicleType.LIGHT, 10.5, route); -Message message = new Message( - MessageType.VEHICLE_TRANSFER, - "Cr1", - "Cr2", - vehicle -); -byte[] data = serializer.serialize(message); - -// Enviar via socket -outputStream.write(data); - -// Receber e desserializar -byte[] received = inputStream.readAllBytes(); -Message msg = serializer.deserialize(received, Message.class); -Vehicle v = msg.getPayloadAs(Vehicle.class); -``` - ---- - -## Desenvolvimento - -### Estado do Projeto - -| Componente | Estado | Notas | -|------------|--------|-------| -| Modelo de Dados | Completo | Vehicle, Message, Event, etc. | -| Simulação DES | Completo | Single-process funcional | -| Serialização | Completo | JSON e Java implementados | -| Testes | 14/14 | Suite de serialização | -| Processos Distribuídos | Planeado | Próxima etapa | -| Comunicação Sockets | Planeado | Em design | -| Dashboard | Planeado | UI web | - -### Roteiro de Desenvolvimento - -#### Fase 1: Fundações (Concluído) -- Modelação de classes -- Simulação DES single-process -- Design de protocolo de serialização -- Implementação JSON/Java serialization -- Testes unitários - -#### Fase 2: Distribuição (Em Curso) -- Implementar comunicação via sockets -- Separar cruzamentos em processos -- Implementar threads de semáforos -- Testar comunicação entre processos - -#### Fase 3: Dashboard e Monitorização -- Dashboard server -- UI web em tempo real -- Visualização de estatísticas -- Logs estruturados - -#### Fase 4: Optimização e Análise -- Testes de carga -- Análise de diferentes políticas -- Recolha de métricas -- Relatório final - -### Executar Testes - -```bash -# Todos os testes -mvn test - -# Apenas testes de serialização -mvn test -Dtest=SerializationTest - -# Com relatório de cobertura -mvn test jacoco:report -``` - -### Contribuir - -1. Fork o projeto -2. Criar uma branch para a funcionalidade (`git checkout -b feature/MinhaFuncionalidade`) -3. Commit das alterações (`git commit -m 'Adiciona MinhaFuncionalidade'`) -4. Push para a branch (`git push origin feature/MinhaFuncionalidade`) -5. Abrir um Pull Request - ---- - -## Métricas de Desempenho - -### Serialização - -| Formato | Tamanho | Latência | Throughput | -|---------|---------|----------|------------| -| JSON | 300 bytes | 40.79 μs | ~24k msgs/s | -| Java | 657 bytes | 33.34 μs | ~30k msgs/s | - -**Conclusão**: JSON é 54% menor com overhead desprezível (7 μs) - -### Simulação - -- **Veículos gerados/s**: ~0.5-1.0 (configurável) -- **Throughput**: ~0.2 veículos/s (saída) -- **Tempo de execução**: 140ms para 60s de simulação -- **Overhead**: < 0.25% do tempo simulado - ---- - -## Protocolo de Mensagens - Resumo - -### Formato Base - -``` -+------------------+ -| Message Header | -|------------------| -| messageId | UUID único -| type | Enum MessageType -| senderId | ID do processo remetente -| destinationId | ID do processo destino (null = broadcast) -| timestamp | Tempo de criação (ms) -+------------------+ -| Payload | -|------------------| -| Object | Dados específicos do tipo de mensagem -+------------------+ -``` - -### Serialização - -- **Formato**: JSON (UTF-8) -- **Biblioteca**: Gson 2.10.1 -- **Codificação**: UTF-8 -- **Compressão**: Opcional (gzip) - -### Transporte - -- **Protocolo**: TCP/IP -- **Porta base**: 5000+ (configurável) -- **Timeout**: 30s -- **Keep-alive**: Heartbeat a cada 5s - ---- - -## Segurança - -### Considerações - -1. **Validação de Mensagens** - - Verificar tipos esperados - - Validar intervalos de valores - - Rejeitar mensagens malformadas - -2. **Autenticação** (Planeado) - - Autenticação baseada em token - - Whitelist de processos - -3. **Encriptação** (Opcional) - - TLS/SSL para produção - - Não necessário para ambiente de desenvolvimento local - ---- - -## Licença - -Este projeto é desenvolvido para fins académicos no âmbito da disciplina de Sistemas Distribuídos (SD) do Instituto Politécnico do Porto. - ---- - -## Equipa - -**Instituição**: Instituto Politécnico do Porto -**Curso**: Sistemas Distribuídos -**Ano Letivo**: 2025-2026 (1º Semestre) - ---- - -## Suporte - -Para questões ou problemas: - -1. Consultar a [documentação](./main/docs/README.md) -2. Ver [exemplos de código](./main/src/main/java/sd/serialization/SerializationExample.java) -3. Executar testes: `mvn test` -4. Abrir issue no GitHub - ---- - -## Ligações Úteis - -- [Documentação do Projeto](./main/docs/README.md) -- [Plano de Desenvolvimento](./TODO.md) -- [Especificação de Serialização](./main/docs/SERIALIZATION_SPECIFICATION.md) -- [Guia de Serialização](./main/src/main/java/sd/serialization/README.md) - ---- - -**Última actualização**: 23 de outubro de 2025 -**Versão**: 1.0.0 -**Estado**: Em Desenvolvimento Activo diff --git a/STEP2_SUMMARY.md b/STEP2_SUMMARY.md deleted file mode 100644 index b234c44..0000000 --- a/STEP2_SUMMARY.md +++ /dev/null @@ -1,134 +0,0 @@ -# 🏁 Single-Process Prototype — Implementation Summary - -**Status:** ✅ Complete -**Date:** October 22, 2025 -**Branch:** `8-single-process-prototype` - ---- - -## Overview - -The single-process prototype implements a **discrete event simulation (DES)** of a 3×3 urban grid with five intersections, realistic vehicle behavior, and fully synchronized traffic lights. Everything runs under one process, laying the groundwork for the distributed architecture in Phase 3. - ---- - -## Core Architecture - -### **SimulationEngine** - -Drives the DES loop with a priority queue of timestamped events — vehicles, lights, crossings, and periodic stats updates. Handles five intersections (Cr1–Cr5) and six event types. - -**Main loop:** - -``` -while (events && time < duration): - event = nextEvent() - time = event.timestamp - handle(event) -``` - -### **VehicleGenerator** - -Spawns vehicles via: - -* **Poisson arrivals** (λ = 0.5 veh/s) or fixed intervals -* **Probabilistic routes** from E1–E3 -* **Type distribution**: 20% BIKE, 60% LIGHT, 20% HEAVY - -### **StatisticsCollector** - -Tracks system-wide and per-type metrics: throughput, avg. wait, queue sizes, light cycles — updated every 10 s and at simulation end. - ---- - -## Model Highlights - -* **Vehicle** – type, route, timings, lifecycle. -* **Intersection** – routing tables, traffic lights, queues. -* **TrafficLight** – red/green cycles with FIFO queues. -* **Event** – timestamped, comparable; 6 types for all DES actions. - ---- - -## Configuration (`simulation.properties`) - -```properties -simulation.duration=60.0 -simulation.arrival.model=POISSON -simulation.arrival.rate=0.5 - -vehicle.bike.crossingTime=1.5 -vehicle.light.crossingTime=2.0 -vehicle.heavy.crossingTime=4.0 - -statistics.update.interval=10.0 -``` - -**Speed logic:** -`t_bike = 0.5×t_car`, `t_heavy = 2×t_car`. - ---- - -## Topology - -``` -E1→Cr1→Cr4→Cr5→S -E2→Cr2→Cr5→S -E3→Cr3→S -Bi-dir: Cr1↔Cr2, Cr2↔Cr3 -``` - ---- - -## Results - -**Unit Tests:** 7/7 ✅ -**60-Second Simulation:** - -* Generated: 22 vehicles -* Completed: 5 (22.7%) -* Avg system time: 15.47 s -* Throughput: 0.08 veh/s -* All lights & intersections operational - -**Performance:** -~0.03 s real-time run (≈2000× speed-up), < 50 MB RAM. - ---- - -## Code Structure - -``` -sd/ -├── engine/SimulationEngine.java -├── model/{Vehicle,Intersection,TrafficLight,Event}.java -├── util/{VehicleGenerator,StatisticsCollector}.java -└── config/SimulationConfig.java -``` - ---- - -## Key Flow - -1. Initialize intersections, lights, first events. -2. Process events chronologically. -3. Vehicles follow routes → queue → cross → exit. -4. Lights toggle, queues drain, stats update. -5. Print summary and performance metrics. - ---- - -## Next Steps — Phase 3 - -* Split intersections into independent **processes**. -* Add **socket-based communication**. -* Run **traffic lights as threads**. -* Enable **distributed synchronization** and fault handling. - ---- - -## TL;DR - -Solid single-process DES ✅ -Everything’s working — traffic lights, routing, vehicles, stats. -Ready to go distributed next. \ No newline at end of file diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 97d1982..0000000 --- a/TODO.md +++ /dev/null @@ -1,198 +0,0 @@ -## ✅ SINGLE-PROCESS PROTOTYPE - COMPLETED - -### Phase 2 Status: DONE ✅ - -All components for the single-process prototype have been successfully implemented and tested: - -- ✅ **SimulationEngine** - Priority queue-based discrete event simulation -- ✅ **VehicleGenerator** - Poisson and Fixed arrival models -- ✅ **StatisticsCollector** - Comprehensive metrics tracking -- ✅ **Entry point** - Main simulation runner -- ✅ **60s test simulation** - Successfully validated event processing and routing - -### Test Results: -- All 7 unit tests passing -- 60-second simulation completed successfully -- Generated 22 vehicles with 5 completing their routes -- Traffic light state changes working correctly -- Vehicle routing through intersections validated - ---- - -## NEXT: Distributed Architecture Implementation - -### Compreender os Conceitos Fundamentais - -Primeiro, as tecnologias e paradigmas chave necessários para este projeto devem ser totalmente compreendidos. - -- **Processos vs. Threads:** O projeto especifica o uso de ambos. - - - **Processos (para Cruzamentos)** são programas independentes, cada um com o seu próprio espaço de memória. Em Java, cada cruzamento será provavelmente executado como uma aplicação Java separada (uma instância distinta da JVM). - - - **Threads (para Semáforos)** existem _dentro_ de um processo e partilham memória. Isto é adequado para os semáforos, pois eles precisam de ser coordenados e partilhar dados (como filas de veículos) dentro do mesmo cruzamento. - -- **Comunicação Entre Processos (IPC - Inter-Process Communication):** Como os cruzamentos são processos separados, é necessário um método para que eles comuniquem. **Sockets** são o método especificado. Quando um veículo sai de um cruzamento (ex: `Cr1`) e vai para outro (ex: `Cr2`), o processo `Cr1` precisa de enviar uma mensagem contendo os dados do veículo para o processo `Cr2` através de uma conexão por socket. - -- **Simulação de Eventos Discretos (DES - Discrete-Event Simulation):** Este é o paradigma de simulação que deve ser utilizado. Em vez de o tempo fluir continuamente, o relógio da simulação salta de um evento para o seguinte. - - - Um **evento** é um objeto que representa algo que acontece num ponto específico no tempo (ex: "Veículo A chega ao Cr2 no tempo 15.7s"). - - - Uma **lista de eventos** central, frequentemente uma fila de prioridades, será necessária para armazenar eventos futuros, ordenados pelo seu timestamp. O ciclo principal da simulação retira o próximo evento da lista, processa-o e adiciona quaisquer novos eventos que resultem dele. - -- **Processo de Poisson:** Para o modelo 'mais realista' de chegadas de veículos, é especificado um processo de Poisson. A principal conclusão é que o tempo _entre_ chegadas consecutivas de veículos segue uma **distribuição exponencial**. Em Java, este intervalo pode ser gerado usando `Math.log(1 - Math.random()) / -lambda`, onde `lambda` (λi​) é a taxa de chegada especificada. - - ---- - -### Uma Sugestão de Arquitetura de Alto Nível - -Abaixo, é apresentada uma possível estrutura para a aplicação distribuída. Pode ser vista como um conjunto de programas independentes que comunicam através de uma rede. - -1. **Processo Coordenador/Gerador (1 Processo):** - - - **Propósito:** Iniciar a simulação, gerar veículos e gerir o relógio global da simulação ou os critérios de paragem. - - - **Responsabilidades:** - - - Lê a configuração da simulação (ex: carga de tráfego λi​, tempos dos semáforos). - - - Gera veículos de acordo com o modelo selecionado (intervalo fixo ou processo de Poisson). - - - Atribui a cada novo veículo um percurso com base na distribuição uniforme especificada. - - - Injeta o veículo no sistema enviando uma mensagem para o primeiro processo de cruzamento no seu percurso (ex: de um ponto de entrada E1 para Cr1). - -2. **Processos de Cruzamento (5 Processos):** - - - **Propósito:** Simular cada cruzamento (`Cr1` a `Cr5`) como um processo distinto. - - - **Responsabilidades:** - - - Escuta por veículos a chegar de outros processos. - - - Gere as filas de veículos para os seus semáforos. - - - Executa múltiplas **threads de Semáforo** internamente. - - - Coordena estas threads para garantir que apenas uma direção de tráfego está aberta a cada momento. - - - Quando um veículo atravessa, é encaminhado para o processo seguinte no seu percurso. - - - Envia periodicamente as suas estatísticas (ex: comprimentos atuais das filas) para o Servidor do Dashboard. - -3. **Processo de Nó de Saída (1 Processo):** - - - **Propósito:** Representar o ponto de saída `S` e atuar como um coletor de dados para estatísticas globais. - - - **Responsabilidades:** - - - Recebe veículos que completaram o seu percurso. - - - Calcula métricas globais como o tempo total de viagem (tempo de permanência) para cada veículo. - - - Agrega e calcula as estatísticas finais (ex: tempo de viagem mínimo, máximo e médio por tipo de veículo). - - - Envia estas estatísticas globais para o Servidor do Dashboard. - -4. **Processo do Servidor do Dashboard (1 Processo):** - - - **Propósito:** Agregar e exibir todos os dados da simulação em tempo real. - - - **Responsabilidades:** - - - Abre um socket de servidor e escuta por dados a chegar de todos os processos de Cruzamento e de Saída. - - - Armazena e atualiza as estatísticas à medida que chegam. - - - Apresenta os dados numa interface de utilizador, que deve exibir métricas e ser atualizada durante a simulação. - - ---- - -### Plano - -Nem tudo deve ser construído de uma só vez. Os seguintes passos incrementais são recomendados. - -#### **Passo 1: Modelação e Classes Principais (Não-distribuído)** - -Antes de escrever qualquer lógica complexa, as estruturas de dados devem ser definidas. Devem ser criados Plain Old Java Objects (POJOs) para: - -- `Veiculo`: Com atributos como um identificador único, tipo, tempo de entrada e o percurso realizado. Deve ser tornado `Serializable` para que possa ser enviado através de sockets. - -- `Evento`: Com atributos como um timestamp e o tipo de evento (ex: `VEHICLE_ARRIVAL`), bem como dados associados. - -- `Semaforo`: Para conter o seu estado (`VERDE`/`VERMELHO`) e a fila de veículos. - -- `Cruzamento`: Para conter os seus semáforos e a lógica operacional. - - -#### **Passo 2: Construir um Protótipo de Processo Único** - -Este é um passo crucial. Sockets e processos devem ser deixados de lado por agora para construir toda a simulação numa única aplicação Java. - -- Deve ser criado um ciclo de simulação central baseado numa fila de prioridades para objetos `Evento`. - -- Todos os objetos `Cruzamento` e `Semaforo` devem ser instanciados. - -- A lógica principal deve ser tornada funcional: veículos a moverem-se entre filas, semáforos a mudar de estado e estatísticas básicas a serem recolhidas. - -- **Objetivo:** Uma simulação totalmente funcional e não-distribuída. Isto torna a depuração significativamente mais fácil. - - -#### **Passo 3: Distribuir os Cruzamentos** - -O protótipo pode agora ser convertido num sistema distribuído. - -- A classe `Cruzamento` deve ser tornada executável como uma aplicação Java autónoma (com um método `main`). Serão lançadas cinco instâncias, uma para cada cruzamento. - -- Devem ser configurados sockets TCP para comunicação. Cada processo de cruzamento precisa de saber o endereço/porta dos vizinhos para os quais pode enviar veículos. - -- Um **protocolo de comunicação** claro deve ser definido. Por exemplo, quando `Cr1` envia um veículo para `Cr2`, o objeto `Veiculo` é serializado e escrito no socket conectado a `Cr2`. O processo `Cr2` terá uma thread dedicada para escutar estas conexões de entrada. - - -#### **Passo 4: Implementar as Threads dos Semáforos** - -Dentro de cada processo `Cruzamento`, os semáforos devem ser implementados como threads. - -- O principal desafio aqui é a **sincronização**. As threads dos semáforos num único cruzamento partilham as filas de veículos. - -- As ferramentas de concorrência do Java (como `synchronized`, `ReentrantLock`, `Semaphore`) devem ser usadas para garantir que apenas um semáforo pode estar verde para um percurso conflituante e que o acesso às filas partilhadas é seguro (thread-safe). - - -#### **Passo 5: Implementar o Dashboard** - -- O processo `DashboardServer` deve ser criado. Ele irá escutar numa porta específica por estatísticas a chegar. - -- Nos processos `Cruzamento` e `Saida`, deve ser adicionado um mecanismo para enviar periodicamente um resumo das suas estatísticas atuais para o Servidor do Dashboard. - -- A UI deve ser construída para exibir estes dados em tempo real. - - -#### **Passo 6: Testes e Análise** - -Assim que o sistema completo estiver a funcionar, as experiências exigidas pela descrição do projeto podem ser realizadas. - -- A simulação deve ser executada com diferentes taxas de chegada de veículos para simular cargas baixas, médias e altas. - -- Diferentes políticas de temporização dos semáforos devem ser testadas para medir o seu impacto no congestionamento. - -- Diferentes algoritmos de seleção de percurso e o seu impacto no desempenho do sistema devem ser avaliados. - -- Para cada cenário, a simulação deve ser executada várias vezes para recolher estatísticas fiáveis (médias, desvios padrão, intervalos de confiança), conforme solicitado. - - -#### **Passo 7: Escrever o Relatório** - -À medida que cada passo é concluído, deve ser documentado. Isto tornará a escrita do relatório final muito mais fácil. Todos os pontos mencionados nas secções "Entrega" e "Critérios de Avaliação" devem ser abordados. - ---- - -### OBS: - -- **Começar de Forma Simples:** O protótipo de processo único (Passo 2) evitará grandes dificuldades mais tarde. - -- **Protocolo de Comunicação:** O protocolo de mensagens deve ser definido o mais cedo possível. A informação exata que um processo envia para outro deve ser clara//simples//consistente. - -- **Debugging:** Debugging de sistemas distribuídos podem ser difíceis. Uma framework de logging (como Log4j 2 ou SLF4J) pode ser usada para registar eventos//alterações de estado nos diferentes processos. - -- **Configuração:** Valores como endereços IP, números de porta ou parâmetros da simulação não devem ser "hardcoded". Um ficheiro de configuração (ex: um ficheiro `.properties` ou `.json`) torna a aplicação mais fácil de executar e testar. diff --git a/main/analysis/HIGH_LOAD_20251207-001113.csv b/main/analysis/HIGH_LOAD_20251207-001113.csv new file mode 100644 index 0000000..0e268bf --- /dev/null +++ b/main/analysis/HIGH_LOAD_20251207-001113.csv @@ -0,0 +1,6 @@ +Execução,VeículosGerados,VeículosCompletados,TaxaConclusão,TempoMédioSistema,TempoMédioEspera,TempoMínimoSistema,TempoMáximoSistema +1,1784,877,49.16,64.58,61.43,32.29,129.16 +2,1782,363,20.37,53.77,51.01,26.88,107.53 +3,1786,883,49.44,53.09,50.08,26.54,106.17 +4,1845,179,9.70,63.92,60.27,31.96,127.84 +5,1872,953,50.91,65.41,62.16,32.70,130.81 diff --git a/main/analysis/HIGH_LOAD_20251207-001113.txt b/main/analysis/HIGH_LOAD_20251207-001113.txt new file mode 100644 index 0000000..fb8a356 --- /dev/null +++ b/main/analysis/HIGH_LOAD_20251207-001113.txt @@ -0,0 +1,215 @@ +================================================================================ +ANÁLISE ESTATÍSTICA MULTI-EXECUÇÃO +================================================================================ +Configuração: simulation-high.properties +Número de Execuções: 5 +Data da Análise: 2025-12-07 00:11:13 + +-------------------------------------------------------------------------------- +MÉTRICAS GLOBAIS +-------------------------------------------------------------------------------- + +Veículos Gerados: + Média: 1813.80 Desvio Padrão: 41.93 + Mediana: 1786.00 IC 95%: [1754.13, 1873.47] + Mín: 1782.00 Máx: 1872.00 + +Veículos Completados: + Média: 651.00 Desvio Padrão: 354.20 + Mediana: 877.00 IC 95%: [146.96, 1155.04] + Mín: 179.00 Máx: 953.00 + +Taxa de Conclusão (%): + Média: 35.92 Desvio Padrão: 19.44 + Mediana: 49.16 IC 95%: [8.25, 63.58] + Mín: 9.70 Máx: 50.91 + +Tempo Médio no Sistema (segundos): + Média: 60.15 Desvio Padrão: 6.17 + Mediana: 63.92 IC 95%: [51.38, 68.93] + Mín: 53.09 Máx: 65.41 + +Tempo Médio de Espera (segundos): + Média: 56.99 Desvio Padrão: 5.93 + Mediana: 60.27 IC 95%: [48.55, 65.43] + Mín: 50.08 Máx: 62.16 + + +-------------------------------------------------------------------------------- +ANÁLISE POR TIPO DE VEÍCULO +-------------------------------------------------------------------------------- + +--- BIKE --- + Contagem de Veículos: + Média: 135.40 Desvio Padrão: 77.66 + Mediana: 167.00 IC 95%: [24.89, 245.91] + Mín: 37.00 Máx: 211.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 55.15 Desvio Padrão: 12.01 + Mediana: 54.23 IC 95%: [38.07, 72.24] + Mín: 43.41 Máx: 74.99 + + +--- LIGHT --- + Contagem de Veículos: + Média: 395.00 Desvio Padrão: 207.62 + Mediana: 540.00 IC 95%: [99.55, 690.45] + Mín: 107.00 Máx: 548.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 59.79 Desvio Padrão: 7.28 + Mediana: 61.58 IC 95%: [49.43, 70.15] + Mín: 50.81 Máx: 69.26 + + +--- HEAVY --- + Contagem de Veículos: + Média: 120.60 Desvio Padrão: 72.95 + Mediana: 142.00 IC 95%: [16.79, 224.41] + Mín: 35.00 Máx: 202.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 49.20 Desvio Padrão: 8.62 + Mediana: 50.31 IC 95%: [36.94, 61.46] + Mín: 35.51 Máx: 58.20 + + +-------------------------------------------------------------------------------- +ANÁLISE POR INTERSEÇÃO +-------------------------------------------------------------------------------- + +--- Cr1 --- + Tamanho Máximo da Fila: + Média: 3.20 Desvio Padrão: 5.54 + Mediana: 1.00 IC 95%: [-4.68, 11.08] + Mín: 0.00 Máx: 13.00 + + Tamanho Médio da Fila: + Média: 3.20 Desvio Padrão: 5.54 + Mediana: 1.00 IC 95%: [-4.68, 11.08] + Mín: 0.00 Máx: 13.00 + + Veículos Processados: + Média: 378.40 Desvio Padrão: 252.94 + Mediana: 512.00 IC 95%: [18.46, 738.34] + Mín: 58.00 Máx: 600.00 + + +--- Cr2 --- + Tamanho Máximo da Fila: + Média: 0.60 Desvio Padrão: 1.34 + Mediana: 0.00 IC 95%: [-1.31, 2.51] + Mín: 0.00 Máx: 3.00 + + Tamanho Médio da Fila: + Média: 0.60 Desvio Padrão: 1.34 + Mediana: 0.00 IC 95%: [-1.31, 2.51] + Mín: 0.00 Máx: 3.00 + + Veículos Processados: + Média: 390.40 Desvio Padrão: 223.14 + Mediana: 409.00 IC 95%: [72.87, 707.93] + Mín: 59.00 Máx: 599.00 + + +--- Cr3 --- + Tamanho Máximo da Fila: + Média: 6.20 Desvio Padrão: 8.67 + Mediana: 0.00 IC 95%: [-6.14, 18.54] + Mín: 0.00 Máx: 18.00 + + Tamanho Médio da Fila: + Média: 6.20 Desvio Padrão: 8.67 + Mediana: 0.00 IC 95%: [-6.14, 18.54] + Mín: 0.00 Máx: 18.00 + + Veículos Processados: + Média: 339.00 Desvio Padrão: 239.34 + Mediana: 416.00 IC 95%: [-1.59, 679.59] + Mín: 57.00 Máx: 622.00 + + +--- Cr4 --- + Tamanho Máximo da Fila: + Média: 0.60 Desvio Padrão: 0.89 + Mediana: 0.00 IC 95%: [-0.67, 1.87] + Mín: 0.00 Máx: 2.00 + + Tamanho Médio da Fila: + Média: 0.60 Desvio Padrão: 0.89 + Mediana: 0.00 IC 95%: [-0.67, 1.87] + Mín: 0.00 Máx: 2.00 + + Veículos Processados: + Média: 123.40 Desvio Padrão: 116.13 + Mediana: 109.00 IC 95%: [-41.85, 288.65] + Mín: 21.00 Máx: 316.00 + + +--- Cr5 --- + Tamanho Máximo da Fila: + Média: 2.40 Desvio Padrão: 1.14 + Mediana: 2.00 IC 95%: [0.78, 4.02] + Mín: 1.00 Máx: 4.00 + + Tamanho Médio da Fila: + Média: 2.40 Desvio Padrão: 1.14 + Mediana: 2.00 IC 95%: [0.78, 4.02] + Mín: 1.00 Máx: 4.00 + + Veículos Processados: + Média: 200.80 Desvio Padrão: 114.19 + Mediana: 261.00 IC 95%: [38.31, 363.29] + Mín: 70.00 Máx: 305.00 + + +--- ExitNode --- + Tamanho Máximo da Fila: Sem dados + + Tamanho Médio da Fila: Sem dados + + Veículos Processados: + Média: 651.00 Desvio Padrão: 354.20 + Mediana: 877.00 IC 95%: [146.96, 1155.04] + Mín: 179.00 Máx: 953.00 + + +-------------------------------------------------------------------------------- +RESUMOS INDIVIDUAIS DAS EXECUÇÕES +-------------------------------------------------------------------------------- + +Execução #1 [simulation-high.properties]: + Gerados: 1784, Completados: 877 (49.2%) + Tempo Médio no Sistema: 64.58s + Tempo Médio de Espera: 61.43s + +Execução #2 [simulation-high.properties]: + Gerados: 1782, Completados: 363 (20.4%) + Tempo Médio no Sistema: 53.77s + Tempo Médio de Espera: 51.01s + +Execução #3 [simulation-high.properties]: + Gerados: 1786, Completados: 883 (49.4%) + Tempo Médio no Sistema: 53.09s + Tempo Médio de Espera: 50.08s + +Execução #4 [simulation-high.properties]: + Gerados: 1845, Completados: 179 (9.7%) + Tempo Médio no Sistema: 63.92s + Tempo Médio de Espera: 60.27s + +Execução #5 [simulation-high.properties]: + Gerados: 1872, Completados: 953 (50.9%) + Tempo Médio no Sistema: 65.41s + Tempo Médio de Espera: 62.16s + +================================================================================ +FIM DO RELATÓRIO +================================================================================ diff --git a/main/analysis/LOW_LOAD_20251207-000957.csv b/main/analysis/LOW_LOAD_20251207-000957.csv new file mode 100644 index 0000000..01d1b2c --- /dev/null +++ b/main/analysis/LOW_LOAD_20251207-000957.csv @@ -0,0 +1,6 @@ +Execução,VeículosGerados,VeículosCompletados,TaxaConclusão,TempoMédioSistema,TempoMédioEspera,TempoMínimoSistema,TempoMáximoSistema +1,371,187,50.40,42.28,38.65,21.14,84.57 +2,361,263,72.85,29.15,25.29,14.57,58.30 +3,368,197,53.53,38.02,33.95,19.01,76.04 +4,350,239,68.29,32.38,28.36,16.19,64.75 +5,373,212,56.84,23.36,19.96,11.68,46.73 diff --git a/main/analysis/LOW_LOAD_20251207-000957.txt b/main/analysis/LOW_LOAD_20251207-000957.txt new file mode 100644 index 0000000..073c0fb --- /dev/null +++ b/main/analysis/LOW_LOAD_20251207-000957.txt @@ -0,0 +1,209 @@ +================================================================================ +ANÁLISE ESTATÍSTICA MULTI-EXECUÇÃO +================================================================================ +Configuração: simulation-low.properties +Número de Execuções: 5 +Data da Análise: 2025-12-07 00:09:57 + +-------------------------------------------------------------------------------- +MÉTRICAS GLOBAIS +-------------------------------------------------------------------------------- + +Veículos Gerados: + Média: 364.60 Desvio Padrão: 9.34 + Mediana: 368.00 IC 95%: [351.30, 377.90] + Mín: 350.00 Máx: 373.00 + +Veículos Completados: + Média: 219.60 Desvio Padrão: 31.19 + Mediana: 212.00 IC 95%: [175.22, 263.98] + Mín: 187.00 Máx: 263.00 + +Taxa de Conclusão (%): + Média: 60.38 Desvio Padrão: 9.71 + Mediana: 56.84 IC 95%: [46.57, 74.20] + Mín: 50.40 Máx: 72.85 + +Tempo Médio no Sistema (segundos): + Média: 33.04 Desvio Padrão: 7.41 + Mediana: 32.38 IC 95%: [22.50, 43.58] + Mín: 23.36 Máx: 42.28 + +Tempo Médio de Espera (segundos): + Média: 29.24 Desvio Padrão: 7.30 + Mediana: 28.36 IC 95%: [18.85, 39.63] + Mín: 19.96 Máx: 38.65 + + +-------------------------------------------------------------------------------- +ANÁLISE POR TIPO DE VEÍCULO +-------------------------------------------------------------------------------- + +--- BIKE --- + Contagem de Veículos: + Média: 41.00 Desvio Padrão: 6.96 + Mediana: 43.00 IC 95%: [31.09, 50.91] + Mín: 33.00 Máx: 50.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 25.91 Desvio Padrão: 3.91 + Mediana: 26.98 IC 95%: [20.35, 31.47] + Mín: 19.60 Máx: 30.06 + + +--- LIGHT --- + Contagem de Veículos: + Média: 134.00 Desvio Padrão: 24.07 + Mediana: 130.00 IC 95%: [99.74, 168.26] + Mín: 104.00 Máx: 167.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 29.34 Desvio Padrão: 6.83 + Mediana: 27.89 IC 95%: [19.62, 39.06] + Mín: 20.73 Máx: 36.42 + + +--- HEAVY --- + Contagem de Veículos: + Média: 44.60 Desvio Padrão: 3.44 + Mediana: 46.00 IC 95%: [39.71, 49.49] + Mín: 40.00 Máx: 48.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 32.11 Desvio Padrão: 15.90 + Mediana: 30.74 IC 95%: [9.48, 54.74] + Mín: 18.09 Máx: 58.73 + + +-------------------------------------------------------------------------------- +ANÁLISE POR INTERSEÇÃO +-------------------------------------------------------------------------------- + +--- Cr1 --- + Tamanho Máximo da Fila: + Média: 0.60 Desvio Padrão: 1.34 + Mediana: 0.00 IC 95%: [-1.31, 2.51] + Mín: 0.00 Máx: 3.00 + + Tamanho Médio da Fila: + Média: 0.60 Desvio Padrão: 1.34 + Mediana: 0.00 IC 95%: [-1.31, 2.51] + Mín: 0.00 Máx: 3.00 + + Veículos Processados: + Média: 63.80 Desvio Padrão: 17.25 + Mediana: 57.00 IC 95%: [39.25, 88.35] + Mín: 48.00 Máx: 91.00 + + +--- Cr2 --- + Tamanho Máximo da Fila: + Média: 0.80 Desvio Padrão: 1.79 + Mediana: 0.00 IC 95%: [-1.75, 3.35] + Mín: 0.00 Máx: 4.00 + + Tamanho Médio da Fila: + Média: 0.80 Desvio Padrão: 1.79 + Mediana: 0.00 IC 95%: [-1.75, 3.35] + Mín: 0.00 Máx: 4.00 + + Veículos Processados: + Média: 56.20 Desvio Padrão: 18.51 + Mediana: 50.00 IC 95%: [29.86, 82.54] + Mín: 35.00 Máx: 78.00 + + +--- Cr3 --- + Tamanho Máximo da Fila: + Média: 1.00 Desvio Padrão: 1.41 + Mediana: 0.00 IC 95%: [-1.01, 3.01] + Mín: 0.00 Máx: 3.00 + + Tamanho Médio da Fila: + Média: 1.00 Desvio Padrão: 1.41 + Mediana: 0.00 IC 95%: [-1.01, 3.01] + Mín: 0.00 Máx: 3.00 + + Veículos Processados: + Média: 63.20 Desvio Padrão: 23.97 + Mediana: 56.00 IC 95%: [29.09, 97.31] + Mín: 41.00 Máx: 104.00 + + +--- Cr4 --- + Tamanho Máximo da Fila: + Média: 1.80 Desvio Padrão: 2.49 + Mediana: 0.00 IC 95%: [-1.74, 5.34] + Mín: 0.00 Máx: 5.00 + + Tamanho Médio da Fila: + Média: 1.80 Desvio Padrão: 2.49 + Mediana: 0.00 IC 95%: [-1.74, 5.34] + Mín: 0.00 Máx: 5.00 + + Veículos Processados: + Média: 51.00 Desvio Padrão: 16.05 + Mediana: 53.00 IC 95%: [28.16, 73.84] + Mín: 31.00 Máx: 70.00 + + +--- Cr5 --- + Tamanho Máximo da Fila: Sem dados + + Tamanho Médio da Fila: Sem dados + + Veículos Processados: + Média: 86.60 Desvio Padrão: 34.20 + Mediana: 65.00 IC 95%: [37.94, 135.26] + Mín: 62.00 Máx: 139.00 + + +--- ExitNode --- + Tamanho Máximo da Fila: Sem dados + + Tamanho Médio da Fila: Sem dados + + Veículos Processados: + Média: 219.60 Desvio Padrão: 31.19 + Mediana: 212.00 IC 95%: [175.22, 263.98] + Mín: 187.00 Máx: 263.00 + + +-------------------------------------------------------------------------------- +RESUMOS INDIVIDUAIS DAS EXECUÇÕES +-------------------------------------------------------------------------------- + +Execução #1 [simulation-low.properties]: + Gerados: 371, Completados: 187 (50.4%) + Tempo Médio no Sistema: 42.28s + Tempo Médio de Espera: 38.65s + +Execução #2 [simulation-low.properties]: + Gerados: 361, Completados: 263 (72.9%) + Tempo Médio no Sistema: 29.15s + Tempo Médio de Espera: 25.29s + +Execução #3 [simulation-low.properties]: + Gerados: 368, Completados: 197 (53.5%) + Tempo Médio no Sistema: 38.02s + Tempo Médio de Espera: 33.95s + +Execução #4 [simulation-low.properties]: + Gerados: 350, Completados: 239 (68.3%) + Tempo Médio no Sistema: 32.38s + Tempo Médio de Espera: 28.36s + +Execução #5 [simulation-low.properties]: + Gerados: 373, Completados: 212 (56.8%) + Tempo Médio no Sistema: 23.36s + Tempo Médio de Espera: 19.96s + +================================================================================ +FIM DO RELATÓRIO +================================================================================ diff --git a/main/analysis/MEDIUM_LOAD_20251207-001034.csv b/main/analysis/MEDIUM_LOAD_20251207-001034.csv new file mode 100644 index 0000000..50a86c8 --- /dev/null +++ b/main/analysis/MEDIUM_LOAD_20251207-001034.csv @@ -0,0 +1,6 @@ +Execução,VeículosGerados,VeículosCompletados,TaxaConclusão,TempoMédioSistema,TempoMédioEspera,TempoMínimoSistema,TempoMáximoSistema +1,950,416,43.79,49.34,45.70,24.67,98.68 +2,886,480,54.18,35.08,31.69,17.54,70.16 +3,954,535,56.08,43.76,40.30,21.88,87.51 +4,948,354,37.34,41.68,37.96,20.84,83.37 +5,898,312,34.74,52.56,49.26,26.28,105.13 diff --git a/main/analysis/MEDIUM_LOAD_20251207-001034.txt b/main/analysis/MEDIUM_LOAD_20251207-001034.txt new file mode 100644 index 0000000..3b57656 --- /dev/null +++ b/main/analysis/MEDIUM_LOAD_20251207-001034.txt @@ -0,0 +1,203 @@ +================================================================================ +ANÁLISE ESTATÍSTICA MULTI-EXECUÇÃO +================================================================================ +Configuração: simulation-medium.properties +Número de Execuções: 5 +Data da Análise: 2025-12-07 00:10:34 + +-------------------------------------------------------------------------------- +MÉTRICAS GLOBAIS +-------------------------------------------------------------------------------- + +Veículos Gerados: + Média: 927.20 Desvio Padrão: 32.48 + Mediana: 948.00 IC 95%: [880.97, 973.43] + Mín: 886.00 Máx: 954.00 + +Veículos Completados: + Média: 419.40 Desvio Padrão: 90.64 + Mediana: 416.00 IC 95%: [290.42, 548.38] + Mín: 312.00 Máx: 535.00 + +Taxa de Conclusão (%): + Média: 45.23 Desvio Padrão: 9.64 + Mediana: 43.79 IC 95%: [31.50, 58.95] + Mín: 34.74 Máx: 56.08 + +Tempo Médio no Sistema (segundos): + Média: 44.48 Desvio Padrão: 6.81 + Mediana: 43.76 IC 95%: [34.79, 54.18] + Mín: 35.08 Máx: 52.56 + +Tempo Médio de Espera (segundos): + Média: 40.98 Desvio Padrão: 6.83 + Mediana: 40.30 IC 95%: [31.26, 50.71] + Mín: 31.69 Máx: 49.26 + + +-------------------------------------------------------------------------------- +ANÁLISE POR TIPO DE VEÍCULO +-------------------------------------------------------------------------------- + +--- BIKE --- + Contagem de Veículos: + Média: 75.80 Desvio Padrão: 15.96 + Mediana: 71.00 IC 95%: [53.09, 98.51] + Mín: 56.00 Máx: 95.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 42.34 Desvio Padrão: 10.81 + Mediana: 39.70 IC 95%: [26.96, 57.72] + Mín: 31.96 Máx: 55.19 + + +--- LIGHT --- + Contagem de Veículos: + Média: 263.20 Desvio Padrão: 58.29 + Mediana: 265.00 IC 95%: [180.25, 346.15] + Mín: 204.00 Máx: 344.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 39.13 Desvio Padrão: 6.35 + Mediana: 38.08 IC 95%: [30.09, 48.17] + Mín: 30.47 Máx: 47.99 + + +--- HEAVY --- + Contagem de Veículos: + Média: 80.40 Desvio Padrão: 19.11 + Mediana: 80.00 IC 95%: [53.20, 107.60] + Mín: 52.00 Máx: 102.00 + + Tempo Médio no Sistema (segundos): Sem dados + + Tempo Médio de Espera (segundos): + Média: 48.02 Desvio Padrão: 30.99 + Mediana: 34.44 IC 95%: [3.92, 92.11] + Mín: 32.46 Máx: 103.40 + + +-------------------------------------------------------------------------------- +ANÁLISE POR INTERSEÇÃO +-------------------------------------------------------------------------------- + +--- Cr1 --- + Tamanho Máximo da Fila: + Média: 5.60 Desvio Padrão: 11.44 + Mediana: 0.00 IC 95%: [-10.67, 21.87] + Mín: 0.00 Máx: 26.00 + + Tamanho Médio da Fila: + Média: 5.60 Desvio Padrão: 11.44 + Mediana: 0.00 IC 95%: [-10.67, 21.87] + Mín: 0.00 Máx: 26.00 + + Veículos Processados: + Média: 156.00 Desvio Padrão: 122.81 + Mediana: 98.00 IC 95%: [-18.76, 330.76] + Mín: 35.00 Máx: 306.00 + + +--- Cr2 --- + Tamanho Máximo da Fila: Sem dados + + Tamanho Médio da Fila: Sem dados + + Veículos Processados: + Média: 172.00 Desvio Padrão: 121.88 + Mediana: 116.00 IC 95%: [-1.44, 345.44] + Mín: 66.00 Máx: 322.00 + + +--- Cr3 --- + Tamanho Máximo da Fila: + Média: 0.60 Desvio Padrão: 1.34 + Mediana: 0.00 IC 95%: [-1.31, 2.51] + Mín: 0.00 Máx: 3.00 + + Tamanho Médio da Fila: + Média: 0.60 Desvio Padrão: 1.34 + Mediana: 0.00 IC 95%: [-1.31, 2.51] + Mín: 0.00 Máx: 3.00 + + Veículos Processados: + Média: 168.40 Desvio Padrão: 133.38 + Mediana: 121.00 IC 95%: [-21.40, 358.20] + Mín: 48.00 Máx: 326.00 + + +--- Cr4 --- + Tamanho Máximo da Fila: Sem dados + + Tamanho Médio da Fila: Sem dados + + Veículos Processados: + Média: 71.80 Desvio Padrão: 20.39 + Mediana: 77.00 IC 95%: [42.79, 100.81] + Mín: 38.00 Máx: 92.00 + + +--- Cr5 --- + Tamanho Máximo da Fila: + Média: 3.60 Desvio Padrão: 3.85 + Mediana: 2.00 IC 95%: [-1.87, 9.07] + Mín: 0.00 Máx: 10.00 + + Tamanho Médio da Fila: + Média: 3.60 Desvio Padrão: 3.85 + Mediana: 2.00 IC 95%: [-1.87, 9.07] + Mín: 0.00 Máx: 10.00 + + Veículos Processados: + Média: 150.60 Desvio Padrão: 43.37 + Mediana: 126.00 IC 95%: [88.88, 212.32] + Mín: 116.00 Máx: 209.00 + + +--- ExitNode --- + Tamanho Máximo da Fila: Sem dados + + Tamanho Médio da Fila: Sem dados + + Veículos Processados: + Média: 419.40 Desvio Padrão: 90.64 + Mediana: 416.00 IC 95%: [290.42, 548.38] + Mín: 312.00 Máx: 535.00 + + +-------------------------------------------------------------------------------- +RESUMOS INDIVIDUAIS DAS EXECUÇÕES +-------------------------------------------------------------------------------- + +Execução #1 [simulation-medium.properties]: + Gerados: 950, Completados: 416 (43.8%) + Tempo Médio no Sistema: 49.34s + Tempo Médio de Espera: 45.70s + +Execução #2 [simulation-medium.properties]: + Gerados: 886, Completados: 480 (54.2%) + Tempo Médio no Sistema: 35.08s + Tempo Médio de Espera: 31.69s + +Execução #3 [simulation-medium.properties]: + Gerados: 954, Completados: 535 (56.1%) + Tempo Médio no Sistema: 43.76s + Tempo Médio de Espera: 40.30s + +Execução #4 [simulation-medium.properties]: + Gerados: 948, Completados: 354 (37.3%) + Tempo Médio no Sistema: 41.68s + Tempo Médio de Espera: 37.96s + +Execução #5 [simulation-medium.properties]: + Gerados: 898, Completados: 312 (34.7%) + Tempo Médio no Sistema: 52.56s + Tempo Médio de Espera: 49.26s + +================================================================================ +FIM DO RELATÓRIO +================================================================================ diff --git a/main/graphing.py b/main/graphing.py new file mode 100644 index 0000000..9bda368 --- /dev/null +++ b/main/graphing.py @@ -0,0 +1,169 @@ +import pandas as pd +import matplotlib.pyplot as plt +import glob +import os + +# Find CSV files using glob +def load_latest_csv(pattern): + """Load the most recent CSV file matching the pattern""" + files = glob.glob(pattern) + if not files: + print(f"Warning: No files found matching '{pattern}'") + return None + # Sort by modification time, get the latest + latest_file = max(files, key=os.path.getmtime) + print(f"Loading: {latest_file}") + return pd.read_csv(latest_file) + +# Carregar dados +print("Looking for analysis files...") +low = load_latest_csv('analysis/LOW_LOAD_*.csv') +medium = load_latest_csv('analysis/MEDIUM_LOAD_*.csv') +high = load_latest_csv('analysis/HIGH_LOAD_*.csv') + +# Check if we have all data +if low is None or medium is None or high is None: + print("\nError: Missing analysis files!") + print("Please run the batch analysis first:") + exit(1) + +# Print available columns for debugging +print("\nAvailable columns in LOW_LOAD CSV:") +print(low.columns.tolist()) + +# Create output directory for graphs +os.makedirs('graphs', exist_ok=True) + +# 1. Gráfico: Dwelling Time vs Load +plt.figure(figsize=(10, 6)) +dwelling_times = [ + low['TempoMédioSistema'].mean(), + medium['TempoMédioSistema'].mean(), + high['TempoMédioSistema'].mean() +] +plt.bar(['Baixa', 'Média', 'Alta'], dwelling_times, color=['green', 'orange', 'red']) +plt.ylabel('Tempo Médio no Sistema (s)') +plt.title('Desempenho do Sistema vs Carga') +plt.xlabel('Cenário de Carga') +plt.grid(axis='y', alpha=0.3) +for i, v in enumerate(dwelling_times): + plt.text(i, v + 1, f'{v:.2f}s', ha='center', va='bottom') +plt.savefig('graphs/dwelling_time_comparison.png', dpi=300, bbox_inches='tight') +print("\nGraph saved: graphs/dwelling_time_comparison.png") +plt.close() + +# 2. Gráfico: Completion Rate vs Load +plt.figure(figsize=(10, 6)) +completion_rates = [ + low['TaxaConclusão'].mean(), + medium['TaxaConclusão'].mean(), + high['TaxaConclusão'].mean() +] +plt.bar(['Baixa', 'Média', 'Alta'], completion_rates, color=['green', 'orange', 'red']) +plt.ylabel('Taxa de Conclusão (%)') +plt.title('Taxa de Conclusão de Veículos vs Carga') +plt.xlabel('Cenário de Carga') +plt.grid(axis='y', alpha=0.3) +plt.ylim(0, 100) +for i, v in enumerate(completion_rates): + plt.text(i, v + 2, f'{v:.1f}%', ha='center', va='bottom') +plt.savefig('graphs/completion_rate_comparison.png', dpi=300, bbox_inches='tight') +print("Graph saved: graphs/completion_rate_comparison.png") +plt.close() + +# 3. Gráfico: Waiting Time vs Load +plt.figure(figsize=(10, 6)) +waiting_times = [ + low['TempoMédioEspera'].mean(), + medium['TempoMédioEspera'].mean(), + high['TempoMédioEspera'].mean() +] +plt.bar(['Baixa', 'Média', 'Alta'], waiting_times, color=['green', 'orange', 'red']) +plt.ylabel('Tempo Médio de Espera (s)') +plt.title('Tempo Médio de Espera vs Carga') +plt.xlabel('Cenário de Carga') +plt.grid(axis='y', alpha=0.3) +for i, v in enumerate(waiting_times): + plt.text(i, v + 1, f'{v:.2f}s', ha='center', va='bottom') +plt.savefig('graphs/waiting_time_comparison.png', dpi=300, bbox_inches='tight') +print("Graph saved: graphs/waiting_time_comparison.png") +plt.close() + +# 4. Gráfico: Summary Statistics +fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(14, 10)) +loads = ['Baixa', 'Média', 'Alta'] + +# Vehicles generated +ax1.bar(loads, [low['VeículosGerados'].mean(), medium['VeículosGerados'].mean(), high['VeículosGerados'].mean()], color=['green', 'orange', 'red']) +ax1.set_title('Veículos Gerados') +ax1.set_ylabel('Quantidade') +ax1.grid(axis='y', alpha=0.3) + +# Vehicles completed +ax2.bar(loads, [low['VeículosCompletados'].mean(), medium['VeículosCompletados'].mean(), high['VeículosCompletados'].mean()], color=['green', 'orange', 'red']) +ax2.set_title('Veículos Concluídos') +ax2.set_ylabel('Quantidade') +ax2.grid(axis='y', alpha=0.3) + +# Min/Max dwelling time +x = range(3) +width = 0.35 +ax3.bar([i - width/2 for i in x], [low['TempoMínimoSistema'].mean(), medium['TempoMínimoSistema'].mean(), high['TempoMínimoSistema'].mean()], width, label='Mín', color='lightblue') +ax3.bar([i + width/2 for i in x], [low['TempoMáximoSistema'].mean(), medium['TempoMáximoSistema'].mean(), high['TempoMáximoSistema'].mean()], width, label='Máx', color='darkblue') +ax3.set_title('Tempo no Sistema Mín/Máx') +ax3.set_ylabel('Tempo (s)') +ax3.set_xticks(x) +ax3.set_xticklabels(loads) +ax3.legend() +ax3.grid(axis='y', alpha=0.3) + +# Performance summary +metrics = ['Tempo no\nSistema', 'Tempo de\nEspera', 'Taxa de\nConclusão'] +low_vals = [low['TempoMédioSistema'].mean(), low['TempoMédioEspera'].mean(), low['TaxaConclusão'].mean()] +med_vals = [medium['TempoMédioSistema'].mean(), medium['TempoMédioEspera'].mean(), medium['TaxaConclusão'].mean()] +high_vals = [high['TempoMédioSistema'].mean(), high['TempoMédioEspera'].mean(), high['TaxaConclusão'].mean()] + +x = range(len(metrics)) +width = 0.25 +ax4.bar([i - width for i in x], low_vals, width, label='Baixa', color='green') +ax4.bar(x, med_vals, width, label='Média', color='orange') +ax4.bar([i + width for i in x], high_vals, width, label='Alta', color='red') +ax4.set_title('Resumo de Desempenho') +ax4.set_xticks(x) +ax4.set_xticklabels(metrics) +ax4.legend() +ax4.grid(axis='y', alpha=0.3) + +plt.tight_layout() +plt.savefig('graphs/summary_statistics.png', dpi=300, bbox_inches='tight') +print("Graph saved: graphs/summary_statistics.png") +plt.close() + +# Print summary statistics +print("\n" + "="*60) +print("SUMMARY STATISTICS") +print("="*60) +print(f"\nLOW LOAD:") +print(f" Avg Dwelling Time: {low['TempoMédioSistema'].mean():.2f}s") +print(f" Avg Waiting Time: {low['TempoMédioEspera'].mean():.2f}s") +print(f" Completion Rate: {low['TaxaConclusão'].mean():.1f}%") +print(f" Vehicles Generated: {low['VeículosGerados'].mean():.0f}") +print(f" Vehicles Completed: {low['VeículosCompletados'].mean():.0f}") + +print(f"\nMEDIUM LOAD:") +print(f" Avg Dwelling Time: {medium['TempoMédioSistema'].mean():.2f}s") +print(f" Avg Waiting Time: {medium['TempoMédioEspera'].mean():.2f}s") +print(f" Completion Rate: {medium['TaxaConclusão'].mean():.1f}%") +print(f" Vehicles Generated: {medium['VeículosGerados'].mean():.0f}") +print(f" Vehicles Completed: {medium['VeículosCompletados'].mean():.0f}") + +print(f"\nHIGH LOAD:") +print(f" Avg Dwelling Time: {high['TempoMédioSistema'].mean():.2f}s") +print(f" Avg Waiting Time: {high['TempoMédioEspera'].mean():.2f}s") +print(f" Completion Rate: {high['TaxaConclusão'].mean():.1f}%") +print(f" Vehicles Generated: {high['VeículosGerados'].mean():.0f}") +print(f" Vehicles Completed: {high['VeículosCompletados'].mean():.0f}") + +print("\n" + "="*60) +print("All graphs saved in 'graphs/' directory!") +print("="*60) \ No newline at end of file diff --git a/main/graphs/completion_rate_comparison.png b/main/graphs/completion_rate_comparison.png new file mode 100644 index 0000000..a86957b Binary files /dev/null and b/main/graphs/completion_rate_comparison.png differ diff --git a/main/graphs/dwelling_time_comparison.png b/main/graphs/dwelling_time_comparison.png new file mode 100644 index 0000000..d56ec41 Binary files /dev/null and b/main/graphs/dwelling_time_comparison.png differ diff --git a/main/graphs/summary_statistics.png b/main/graphs/summary_statistics.png new file mode 100644 index 0000000..c3dd6c4 Binary files /dev/null and b/main/graphs/summary_statistics.png differ diff --git a/main/graphs/waiting_time_comparison.png b/main/graphs/waiting_time_comparison.png new file mode 100644 index 0000000..8d03cc8 Binary files /dev/null and b/main/graphs/waiting_time_comparison.png differ diff --git a/main/src/main/java/sd/ExitNodeProcess.java b/main/src/main/java/sd/ExitNodeProcess.java index a7e771a..a5868e5 100644 --- a/main/src/main/java/sd/ExitNodeProcess.java +++ b/main/src/main/java/sd/ExitNodeProcess.java @@ -12,6 +12,13 @@ import java.util.concurrent.TimeUnit; import sd.config.SimulationConfig; import sd.coordinator.SocketClient; import sd.dashboard.StatsUpdatePayload; +import sd.des.DESEventType; +import sd.des.EventQueue; +import sd.des.SimulationClock; +import sd.des.SimulationEvent; +import sd.logging.EventLogger; +import sd.logging.EventType; +import sd.logging.VehicleTracer; import sd.model.Message; import sd.model.MessageType; import sd.model.Vehicle; @@ -20,65 +27,63 @@ import sd.protocol.MessageProtocol; import sd.protocol.SocketConnection; /** - * Represents the final Exit Node ("S") of the distributed traffic simulation. - * - * This process acts as a "sink" for all vehicles completing their routes. - * Unlike intersections, it does not route traffic but focuses on data collection. - * Its primary responsibilities are: - * - Accepting TCP connections from intersection processes. - * - Receiving {@link Vehicle} objects via the network. - * - Calculating final performance metrics (Total System Time, Waiting Time, Crossing Time). - * - Aggregating statistics by {@link VehicleType}. - * - Reporting real-time data to the Dashboard service via {@link SocketClient}. - * - Generating a final statistical report upon shutdown. + * Ponto terminal da malha de simulação (Sink Node). + *

+ * Este processo atua como o sumidouro da rede de filas. A sua função primária é + * a coleta de telemetria final. Diferente das interseções, não encaminha veículos; + * em vez disso, retira-os do sistema, calcula as métricas de latência "end-to-end" + * (tempo no sistema, tempo de espera acumulado) e reporta ao Dashboard. + *

+ * Arquitetura de Concorrência: + * Utiliza um {@link ServerSocket} multithreaded para aceitar conexões simultâneas de + * qualquer interseção de fronteira (Cr1, Cr5, etc.) que envie veículos para fora da malha. */ public class ExitNodeProcess { // --- Configuration and Networking --- private final SimulationConfig config; private ServerSocket serverSocket; - - /** - * Thread pool to handle concurrent incoming connections from multiple intersections. - */ + + /** Pool de threads elástica para tratamento de conexões de entrada. */ private final ExecutorService connectionHandlerPool; - /** - * Volatile flag to control the lifecycle of the process. - */ + // DES components + private final SimulationClock clock; + private final EventQueue eventQueue; + private final EventLogger eventLogger; + private Thread eventProcessorThread; + + /** Flag de controlo (volatile para visibilidade entre threads de I/O e lógica). */ private volatile boolean running; - /** * Simulation start time (in milliseconds) used to calculate relative arrival times. - * This is synchronized with the Coordinator. - */ + /** Instante de início da simulação (milissegundos) sincronizado com o Coordenador. */ private long simulationStartMillis; - // --- Statistics and Metrics --- - - /** Total count of vehicles that have successfully completed their route. */ + /** Contador atómico (via synchronized) de throughput total. */ private int totalVehiclesReceived; - /** Accumulated time (in seconds) that all vehicles spent in the system. */ + /** Tempo acumulado no sistema (System Time) de todos os veículos. */ private double totalSystemTime; - /** Accumulated time (in seconds) that all vehicles spent waiting at red lights. */ + /** Tempo acumulado em espera (Waiting Time) de todos os veículos. */ private double totalWaitingTime; - /** Accumulated time (in seconds) that all vehicles spent crossing intersections. */ + /** Tempo acumulado em travessia (Service Time) de todos os veículos. */ private double totalCrossingTime; - /** Counter of vehicles separated by {@link VehicleType}. */ + /** Agregação por categoria de veículo. */ private final Map vehicleTypeCount; - /** Accumulated wait time separated by {@link VehicleType}. */ + /** Latência acumulada por categoria. */ private final Map vehicleTypeWaitTime; - /** Client connection to send updates to the Dashboard. */ + /** Cliente TCP persistente para push de métricas ao Dashboard. */ private SocketClient dashboardClient; - /** - * Main entry point for the Exit Node process. - * * @param args Command line arguments. Optional args[0] for config file path. + /** + * Bootstrap do processo ExitNode. + * Carrega configuração, inicializa subsistemas e entra no loop de serviço. + * * @param args Argumentos de CLI (caminho do config). */ public static void main(String[] args) { System.out.println("=".repeat(60)); @@ -86,6 +91,8 @@ public class ExitNodeProcess { System.out.println("=".repeat(60)); try { + EventLogger.getInstance().log(EventType.PROCESS_STARTED, "ExitNode", "Exit node process started"); + String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties"; System.out.println("Loading configuration from: " + configFile); @@ -100,18 +107,21 @@ public class ExitNodeProcess { } catch (IOException e) { System.err.println("Failed to start exit node: " + e.getMessage()); + EventLogger.getInstance().logError("ExitNode", "Failed to start", e); System.exit(1); } catch (Exception e) { System.err.println("Exit node error: " + e.getMessage()); + EventLogger.getInstance().logError("ExitNode", "Exit node error", e); System.exit(1); + } finally { + EventLogger.getInstance().log(EventType.PROCESS_STOPPED, "ExitNode", "Exit node process stopped"); } } /** - * Constructs a new ExitNodeProcess. - * * Initializes all data structures required for statistics collection - * and sets up the thread pool for handling concurrent network connections. - * * @param config The loaded {@link SimulationConfig} containing ports and addresses. + * Instancia o nó de saída. + * Prepara os acumuladores estatísticos e a infraestrutura de logging distribuído. + * * @param config A configuração global da simulação. */ public ExitNodeProcess(SimulationConfig config) { this.config = config; @@ -131,16 +141,22 @@ public class ExitNodeProcess { vehicleTypeWaitTime.put(type, 0.0); } - System.out.println("Exit node initialized"); + // Initialize DES components + this.clock = new SimulationClock(); + this.eventQueue = new EventQueue(true); // Track history + this.eventLogger = EventLogger.getInstance(); + + eventLogger.log(EventType.PROCESS_STARTED, "ExitNode", + "Exit node initialized with DES architecture"); + + System.out.println("Exit node initialized (DES Mode)"); System.out.println(" - Exit port: " + config.getExitPort()); System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort()); } - /** - * Initializes the connection to the external Dashboard service. - * * Attempts to establish a TCP connection to the dashboard. If successful, - * this connection will be used to stream live statistics. - * If the connection fails, the process logs a warning but continues operation. + /** + * Estabelece o canal de controlo (Control Plane) com o Dashboard. + * Essencial para a visualização em tempo real das métricas de saída. */ public void initialize() { System.out.println("Connecting to dashboard..."); @@ -160,25 +176,137 @@ public class ExitNodeProcess { } /** - * Starts the server socket and begins the main event loop. - * * This method: - * 1. Binds a {@link ServerSocket} to the configured exit port. - * 2. Enters a loop accepting incoming connections from Intersections. - * 3. Submits each new connection to the {@code connectionHandlerPool} for asynchronous processing. - * * @throws IOException If the server socket cannot be bound. + * Inicia a thread de processamento de eventos DES. + * Embora o ExitNode seja primariamente reativo (Network-driven), o motor DES + * é mantido para consistência de relógio e agendamento de fim de simulação. + */ + private void startEventProcessor() { + eventProcessorThread = new Thread(() -> { + eventLogger.log(EventType.SIMULATION_STARTED, "ExitNode", + "Event processor thread started"); + + // Keep running while process is active + while (running) { + SimulationEvent event = eventQueue.poll(); + if (event == null) { + // No events currently, wait before checking again + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + continue; + } + + // Advance clock to event time + clock.advanceTo(event.getTimestamp()); + + // Process the event + processEvent(event); + } + + eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode", + String.format("Event processor thread terminated at time %.2f", clock.getCurrentTime())); + }, "EventProcessor-ExitNode"); + + eventProcessorThread.start(); + } + + /** + * Dispatcher de eventos discretos. + * Trata eventos de fim de simulação. Chegadas de veículos são tratadas via Socket. + */ + private void processEvent(SimulationEvent event) { + try { + switch (event.getType()) { + case VEHICLE_EXIT: + // Vehicle exits are handled via network messages in real-time + // This event type can be used for scheduled vehicle processing + break; + + case SIMULATION_END: + handleSimulationEndEvent(event); + break; + + default: + System.err.println("[ExitNode] Unknown event type: " + event.getType()); + } + } catch (Exception e) { + System.err.println("[ExitNode] Error processing event " + event.getType() + + " at time " + event.getTimestamp() + ": " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Executa a lógica de encerramento desencadeada pelo evento DES. + */ + private void handleSimulationEndEvent(SimulationEvent event) { + eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode", + String.format("Simulation ended at time %.2f", event.getTimestamp())); + running = false; + + // Print final statistics + printFinalStatistics(); + } + + /** + * Exporta o histórico completo de eventos para auditoria. + * Requisito funcional para verificação de trace. + */ + public void exportEventHistory(String outputPath) { + String history = eventQueue.exportEventHistory(); + try (java.io.PrintWriter writer = new java.io.PrintWriter(outputPath)) { + writer.println(history); + System.out.println("[ExitNode] Event history exported to: " + outputPath); + } catch (java.io.FileNotFoundException e) { + System.err.println("[ExitNode] Failed to export event history: " + e.getMessage()); + } + } + + /** + * Agenda o fim determinístico da simulação. + * * @param endTime Tempo virtual de paragem. + */ + public void scheduleSimulationEnd(double endTime) { + SimulationEvent endEvent = new SimulationEvent( + endTime, + DESEventType.SIMULATION_END, + null); + eventQueue.schedule(endEvent); + System.out.println("[ExitNode] Simulation end scheduled at time " + endTime); + } + + /** + * Inicia o servidor TCP em modo de bloqueio (Blocking I/O). + * @throws IOException Se ocorrer erro no bind da porta. */ public void start() throws IOException { + start(true); // Default to DES mode + } + + /** + * Inicia o processo com opção de ativar o rastreio DES. + * * @param useDES Se verdadeiro, ativa a thread do processador de eventos. + */ + public void start(boolean useDES) throws IOException { int port = config.getExitPort(); serverSocket = new ServerSocket(port); running = true; simulationStartMillis = System.currentTimeMillis(); System.out.println("Exit node started on port " + port); + if (useDES) { + System.out.println("Running in DES mode (event history tracking enabled)"); + } System.out.println("Waiting for vehicles...\\n"); + // Loop de aceitação principal while (running) { try { Socket clientSocket = serverSocket.accept(); + // Delega o processamento da conexão para o Thread Pool connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket)); } catch (IOException e) { if (running) { @@ -188,12 +316,12 @@ public class ExitNodeProcess { } } - /** - * Handles an active connection from an Intersection process. - * * Continuously reads {@link MessageProtocol} objects from the socket. - * Expects primarily {@link MessageType#VEHICLE_TRANSFER} messages containing - * {@link Vehicle} payloads. - * * @param clientSocket The accepted socket connection. + /** + * Worker method para tratar uma conexão persistente vinda de uma interseção. + *

+ * Mantém o socket aberto e consome mensagens num loop até que a conexão seja fechada + * pelo remetente. Responsável pela desserialização polimórfica (JSON/Gson). + * * @param clientSocket O socket conectado. */ private void handleIncomingConnection(Socket clientSocket) { String clientAddress = clientSocket.getInetAddress().getHostAddress(); @@ -209,14 +337,14 @@ public class ExitNodeProcess { " from " + message.getSourceNode()); if (message.getType() == MessageType.SIMULATION_START) { - // Coordinator sends start time - use it instead of our local start + // Sincronização de relógio com o Coordenador simulationStartMillis = ((Number) message.getPayload()).longValue(); System.out.println("[Exit] Simulation start time synchronized"); } else if (message.getType() == MessageType.VEHICLE_TRANSFER) { Object payload = message.getPayload(); System.out.println("[Exit] Payload type: " + payload.getClass().getName()); - // Handle Gson LinkedHashMap + // Tratamento de artefatos de desserialização do Gson (LinkedTreeMap -> POJO) Vehicle vehicle; if (payload instanceof com.google.gson.internal.LinkedTreeMap || payload instanceof java.util.LinkedHashMap) { @@ -249,26 +377,21 @@ public class ExitNodeProcess { } /** - * Processes a vehicle that has completed its route. - * * This method is {@code synchronized} to ensure thread safety when updating - * shared statistical counters. It calculates final metrics: - * - System Time: Total time since entry. - * - Wait Time: Total time spent at red lights. - * - Crossing Time: Total time spent crossing intersections. - * * It triggers an immediate update to the Dashboard. - * * @param vehicle The vehicle that has arrived at the Exit Node. + * Processa atomicamente a saída de um veículo. + *

+ * Secção Crítica: Método {@code synchronized} para garantir que a atualização + * das estatísticas globais (totalSystemTime, contadores) é atómica, prevenindo + * Race Conditions quando múltiplos veículos chegam simultaneamente de interseções diferentes. + * * @param vehicle O veículo que completou a rota. */ private synchronized void processExitingVehicle(Vehicle vehicle) { totalVehiclesReceived++; - // 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(); + // Cálculo de métricas finais baseadas no tempo virtual de simulação acumulado no veículo double waitTime = vehicle.getTotalWaitingTime(); double crossingTime = vehicle.getTotalCrossingTime(); + double systemTime = waitTime + crossingTime; - // Store times in seconds, will be converted to ms when sending to dashboard totalSystemTime += systemTime; totalWaitingTime += waitTime; totalCrossingTime += crossingTime; @@ -280,15 +403,20 @@ public class ExitNodeProcess { 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 + // Logging estruturado + EventLogger.getInstance().logVehicle(EventType.VEHICLE_EXITED, "ExitNode", vehicle.getId(), + String.format("Completed - System: %.2fs, Wait: %.2fs, Crossing: %.2fs", systemTime, waitTime, + crossingTime)); + + // Finaliza o trace individual do veículo + VehicleTracer.getInstance().logExit(vehicle, systemTime); + + // Push imediato para o Dashboard para visualização em tempo real sendStatsToDashboard(); } /** - * Sends current aggregated statistics to the Dashboard. - * * Constructs a {@link StatsUpdatePayload} containing global counters and - * per-type metrics, converting seconds to milliseconds as expected by the frontend. - * Wraps the payload in a {@link Message} and sends it via the dashboard client. + * Constrói e transmite o DTO de atualização de estatísticas. */ private void sendStatsToDashboard() { if (dashboardClient == null || !dashboardClient.isConnected()) { @@ -299,29 +427,28 @@ public class ExitNodeProcess { // Create stats payload StatsUpdatePayload payload = new StatsUpdatePayload(); - // Set global stats - convert seconds to milliseconds + // Set global stats - convert seconds to milliseconds for display consistency payload.setTotalVehiclesCompleted(totalVehiclesReceived); - payload.setTotalSystemTime((long) (totalSystemTime * 1000.0)); // s -> ms - payload.setTotalWaitingTime((long) (totalWaitingTime * 1000.0)); // s -> ms + payload.setTotalSystemTime((long) (totalSystemTime * 1000.0)); + payload.setTotalWaitingTime((long) (totalWaitingTime * 1000.0)); - // Set intersection-like stats so it shows up correctly in the dashboard table + // Hack: Usar campos de interseção para mostrar throughput no dashboard payload.setIntersectionArrivals(totalVehiclesReceived); payload.setIntersectionDepartures(totalVehiclesReceived); payload.setIntersectionQueueSize(0); - // Set vehicle type stats + // Detailed breakdown Map typeCounts = new HashMap<>(); Map typeWaitTimes = new HashMap<>(); for (VehicleType type : VehicleType.values()) { typeCounts.put(type, vehicleTypeCount.get(type)); - typeWaitTimes.put(type, (long) (vehicleTypeWaitTime.get(type) * 1000.0)); // s -> ms + typeWaitTimes.put(type, (long) (vehicleTypeWaitTime.get(type) * 1000.0)); } payload.setVehicleTypeCounts(typeCounts); payload.setVehicleTypeWaitTimes(typeWaitTimes); - // Send message Message message = new Message( MessageType.STATS_UPDATE, "ExitNode", @@ -339,14 +466,9 @@ public class ExitNodeProcess { } } - /** - * Gracefully shuts down the exit node process. - * * Sequence of operations: - * 1. Prints final statistics to the console. - * 2. Sends one last update to the dashboard. - * 3. Closes the server socket to stop accepting connections. - * 4. Shuts down the thread pool. - * 5. Closes the dashboard connection. + /** + * Encerramento gracioso do processo. + * Fecha sockets, termina a pool de threads e liberta recursos. */ public void shutdown() { System.out.println("\n[Exit] Shutting down..."); @@ -382,12 +504,7 @@ public class ExitNodeProcess { } /** - * Prints a detailed statistical report to the console. - * * Included metrics: - * - Total vehicles processed. - * - Average System Time, Waiting Time, and Crossing Time. - * - Distribution count and percentage by Vehicle Type. - * - Average wait time by Vehicle Type. + * Imprime o relatório final no stdout. */ private void printFinalStatistics() { System.out.println("\n=== EXIT NODE STATISTICS ==="); @@ -412,4 +529,4 @@ public class ExitNodeProcess { } } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/IntersectionProcess.java b/main/src/main/java/sd/IntersectionProcess.java index bdcb74f..230b9b1 100644 --- a/main/src/main/java/sd/IntersectionProcess.java +++ b/main/src/main/java/sd/IntersectionProcess.java @@ -16,21 +16,39 @@ import java.util.concurrent.locks.ReentrantLock; import sd.config.SimulationConfig; import sd.coordinator.SocketClient; import sd.dashboard.StatsUpdatePayload; -import sd.engine.TrafficLightThread; +import sd.des.DESEventType; +import sd.des.EventQueue; +import sd.des.SimulationClock; +import sd.des.SimulationEvent; +import sd.des.TrafficLightEvent; +import sd.logging.EventLogger; import sd.model.Intersection; import sd.model.Message; import sd.model.MessageType; import sd.model.TrafficLight; +import sd.model.TrafficLightState; import sd.model.Vehicle; import sd.protocol.MessageProtocol; import sd.protocol.SocketConnection; import sd.serialization.SerializationException; /** - * Main class for an Intersection Process in the distributed traffic simulation. - * * Each IntersectionProcess runs as an independent Java application (JVM - * instance) - * representing one of the five intersections (Cr1-Cr5) in the network. + * Representa um nó de processamento autónomo na malha de simulação distribuída + * (Worker Node). + *

+ * Esta classe implementa a lógica de uma interseção rodoviária utilizando uma + * arquitetura híbrida: + *

    + *
  1. Reativa (Network I/O): Threads dedicadas aceitam conexões TCP e + * injetam veículos nas filas de entrada assim que chegam.
  2. + *
  3. Proativa (DES Engine): Uma thread de processamento de eventos gere + * a lógica temporal (mudança de semáforos, tempos de travessia) baseada num + * relógio virtual monotónico.
  4. + *
+ *

+ * A sincronização entre a chegada assíncrona de veículos (Rede) e o + * processamento determinístico (DES) é gerida através de estruturas de dados + * concorrentes e bloqueios justos (Fair Locks). */ public class IntersectionProcess { @@ -42,41 +60,56 @@ public class IntersectionProcess { private ServerSocket serverSocket; + /** + * Tabela de encaminhamento dinâmico para conexões de saída (Next-Hop Cache). + */ private final Map outgoingConnections; + /** Pool de threads para tratamento de I/O de rede (entrada de veículos). */ private final ExecutorService connectionHandlerPool; - private final ExecutorService trafficLightPool; - private ScheduledExecutorService statsExecutor; + private ScheduledExecutorService departureExecutor; - private volatile boolean running; // Quando uma thread escreve um valor volatile, todas as outras - // threads veem a mudança imediatamente. + private volatile boolean running; + /** Fator de dilatação temporal (0.0 = Velocidade Máxima, 1.0 = Tempo Real). */ + private double timeScale; + + // --- Componentes DES (Simulação de Eventos Discretos) --- + /** Relógio central virtual da interseção. */ + private final SimulationClock clock; + /** Fila de prioridade (Min-Heap) para agendamento temporal de eventos. */ + private final EventQueue eventQueue; + private final EventLogger eventLogger; + /** Thread "Single-Writer" responsável pela mutação de estado da simulação. */ + private Thread eventProcessorThread; - // 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. + * Mecanismo de exclusão mútua para controlo de fases semafóricas. + * Configurado com política de justiça (fairness=true) para evitar inanição + * (starvation) de direções com menos tráfego. */ private final Lock trafficCoordinationLock; /** - * Tracks which direction currently has the green light. - * null means no direction is currently green (all are red). + * Estado volátil que indica a direção ativa. Apenas uma direção pode deter o + * token 'Green' por vez. */ private volatile String currentGreenDirection; private SocketClient dashboardClient; + + // Métricas voláteis para acesso atómico sem bloqueio private volatile int totalArrivals = 0; private volatile int totalDepartures = 0; /** - * Constructs a new IntersectionProcess. + * Inicializa o processo da interseção, carregando a topologia e preparando o + * motor DES. * - * @param intersectionId The ID of this intersection (e.g., "Cr1"). - * @param configFilePath Path to the simulation.properties file. - * @throws IOException If configuration cannot be loaded. + * @param intersectionId O identificador único na malha (ex: "Cr1"). + * @param configFilePath Caminho para o ficheiro de propriedades. + * @throws IOException Se falhar o bind da porta ou leitura de config. */ public IntersectionProcess(String intersectionId, String configFilePath) throws IOException { this.intersectionId = intersectionId; @@ -84,18 +117,348 @@ public class IntersectionProcess { this.intersection = new Intersection(intersectionId); this.outgoingConnections = new HashMap<>(); this.connectionHandlerPool = Executors.newCachedThreadPool(); - this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions this.statsExecutor = Executors.newSingleThreadScheduledExecutor(); + this.departureExecutor = Executors.newScheduledThreadPool(4); this.running = false; this.trafficCoordinationLock = new ReentrantLock(true); // Fair lock to prevent starvation this.currentGreenDirection = null; + this.timeScale = config.getTimeScale(); + + // Initialize DES components + this.clock = new SimulationClock(); + this.eventQueue = new EventQueue(true); // Track history for debugging + this.eventLogger = EventLogger.getInstance(); + + eventLogger.log(sd.logging.EventType.PROCESS_STARTED, intersectionId, + "Intersection process initialized with DES architecture"); System.out.println("=".repeat(60)); - System.out.println("INTERSECTION PROCESS: " + intersectionId); + System.out.println("INTERSECTION PROCESS: " + intersectionId + " (DES Mode)"); System.out.println("=".repeat(60)); } - // Main entry point for running an intersection process + /** + * Inicia o ciclo principal do motor de simulação (DES Engine Loop). + *

+ * Executa o ciclo "Fetch-Decode-Execute": + *

    + *
  1. Remove o evento com menor timestamp da fila (Fetch).
  2. + *
  3. Avança o relógio virtual para o tempo do evento.
  4. + *
  5. Aplica atraso artificial se {@code timeScale > 0} (para visualização + * humana).
  6. + *
  7. Despacha o evento para o manipulador apropriado (Execute).
  8. + *
+ */ + private void startEventProcessor() { + eventProcessorThread = new Thread(() -> { + eventLogger.log(sd.logging.EventType.SIMULATION_STARTED, intersectionId, + "Event processor thread started"); + + // Keep running while the process is active + double lastTime = 0.0; + while (running) { + SimulationEvent event = eventQueue.poll(); + if (event == null) { + // Backoff exponencial ou sleep curto para evitar busy-waiting em idle + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + continue; + } + + // Aplicação de escala temporal (Throttle) + if (timeScale > 0) { + double simTimeDelta = event.getTimestamp() - lastTime; + long realDelayMs = (long) (simTimeDelta * timeScale * 1000); + if (realDelayMs > 0) { + try { + Thread.sleep(realDelayMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + lastTime = event.getTimestamp(); + } + + // Atualização atómica do tempo de simulação + clock.advanceTo(event.getTimestamp()); + + // Processamento polimórfico + processEvent(event); + } + + eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, intersectionId, + String.format("Event processor thread terminated at time %.2f", clock.getCurrentTime())); + }, "EventProcessor-" + intersectionId); + + eventProcessorThread.start(); + } + + /** + * Despachante central de eventos. + *

+ * Encaminha o evento para a lógica de negócio específica baseada no tipo + * {@link DESEventType}. + * + * @param event O evento de simulação a ser processado. + */ + private void processEvent(SimulationEvent event) { + try { + switch (event.getType()) { + case TRAFFIC_LIGHT_CHANGE: + handleTrafficLightChangeEvent(event); + break; + + case VEHICLE_ARRIVAL: + // Chegadas são tratadas reativamente via Socket, mas eventos podem ser usados + // para métricas + break; + + case VEHICLE_CROSSING_START: + handleVehicleCrossingStartEvent(event); + break; + + case VEHICLE_CROSSING_END: + handleVehicleCrossingEndEvent(event); + break; + + case SIMULATION_END: + handleSimulationEndEvent(event); + break; + + default: + System.err.println("[" + intersectionId + "] Unknown event type: " + event.getType()); + } + } catch (Exception e) { + System.err.println("[" + intersectionId + "] Error processing event " + event.getType() + + " at time " + event.getTimestamp() + ": " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Gere a máquina de estados dos semáforos. + *

+ * O fluxo de execução é o seguinte: + *

    + *
  1. Atualiza o estado do semáforo (Verde <-> Vermelho).
  2. + *
  3. Se o novo estado for Verde: Calcula a capacidade de vazão e agenda + * travessias (Service Events).
  4. + *
  5. Agenda recursivamente a próxima mudança de estado para manter o ciclo + * infinito.
  6. + *
+ * + * @param event O evento que desencadeou a mudança de estado. + */ + private void handleTrafficLightChangeEvent(SimulationEvent event) { + TrafficLightEvent tlEvent = (TrafficLightEvent) event.getPayload(); + TrafficLight light = tlEvent.getLight(); + String direction = tlEvent.getDirection(); + + // Toggle state + TrafficLightState oldState = light.getState(); + TrafficLightState newState = (oldState == TrafficLightState.GREEN) ? TrafficLightState.RED + : TrafficLightState.GREEN; + + light.changeState(newState); + + sd.logging.EventType logEventType = (newState == TrafficLightState.GREEN) + ? sd.logging.EventType.LIGHT_CHANGED_GREEN + : sd.logging.EventType.LIGHT_CHANGED_RED; + + eventLogger.log(logEventType, intersectionId, + String.format("Direction %s changed to %s at time %.2f", + direction, newState, event.getTimestamp())); + + // Processamento de lote (Batch Processing) para a fase Verde + if (newState == TrafficLightState.GREEN) { + processQueuedVehiclesForLight(light, event.getTimestamp()); + } + + // Agendamento do próximo ciclo (Feedback Loop) + double nextChangeTime = event.getTimestamp() + + (newState == TrafficLightState.GREEN ? light.getGreenTime() : light.getRedTime()); + + SimulationEvent nextEvent = new SimulationEvent( + nextChangeTime, + DESEventType.TRAFFIC_LIGHT_CHANGE, + tlEvent); + eventQueue.schedule(nextEvent); + } + + /** + * Calcula a vazão da interseção durante uma fase verde. + *

+ * Implementa uma lógica de previsão ("Look-ahead"): + *

    + *
  1. Itera sobre a fila de espera do semáforo.
  2. + *
  3. Calcula o tempo de serviço acumulado (Service Time) baseado no tipo de + * veículo.
  4. + *
  5. Agenda a partida apenas se o veículo couber na janela temporal restante + * do sinal verde.
  6. + *
+ * + * @param light O semáforo ativo. + * @param currentTime O instante de início da fase verde. + */ + private void processQueuedVehiclesForLight(TrafficLight light, double currentTime) { + double greenDuration = light.getGreenTime(); + double timeOffset = 0.0; + + int queueSize = light.getQueueSize(); + System.out.printf("[%s] Processing queue for %s (GREEN for %.2fs, queue size: %d, currentTime=%.2f)%n", + intersectionId, light.getId(), greenDuration, queueSize, currentTime); + + // Algoritmo de esvaziamento de fila baseado em Time Budget + while (light.getQueueSize() > 0) { + // Estimativa inicial (optimista) + double crossingTime = config.getLightVehicleCrossingTime(); + + // Verificação de limite de tempo (Hard Deadline do sinal vermelho) + if (timeOffset + crossingTime > greenDuration) { + break; // Veículo não cabe no ciclo atual + } + + // Commit: Remove da fila + Vehicle vehicle = light.removeVehicle(currentTime + timeOffset); + if (vehicle == null) + break; + + // Recálculo preciso baseado no tipo real do veículo + crossingTime = getCrossingTimeForVehicle(vehicle); + + // Agendamento do evento futuro de término de travessia + double crossingStartTime = currentTime + timeOffset; + scheduleVehicleCrossing(vehicle, crossingStartTime, crossingTime); + + // Incrementa offset para serializar as travessias (Head-of-Line Blocking) + timeOffset += crossingTime; + + System.out.printf("[%s] Scheduled vehicle %s to cross at t=%.2f (duration=%.2fs)%n", + intersectionId, vehicle.getId(), crossingStartTime, crossingTime); + } + } + + /** + * Cria e agenda o evento de conclusão de travessia (Partida). + * + * @param vehicle O veículo que está a atravessar. + * @param startTime Instante de início da travessia. + * @param crossingDuration Duração estimada da travessia. + */ + private void scheduleVehicleCrossing(Vehicle vehicle, double startTime, double crossingDuration) { + // Schedule crossing end (when vehicle departs) + double departureTime = startTime + crossingDuration; + + // Create event with vehicle as payload + SimulationEvent departureEvent = new SimulationEvent( + departureTime, + DESEventType.VEHICLE_CROSSING_END, + vehicle); + eventQueue.schedule(departureEvent); + + eventLogger.log(sd.logging.EventType.VEHICLE_QUEUED, intersectionId, + String.format("Vehicle %s crossing scheduled: %.2fs to %.2fs", + vehicle.getId(), startTime, departureTime)); + } + + /** + * Determina o custo temporal da travessia baseado na física do veículo. + * + * @param vehicle O veículo em questão. + * @return O tempo em segundos necessário para atravessar a interseção. + */ + private double getCrossingTimeForVehicle(Vehicle vehicle) { + return switch (vehicle.getType()) { + case BIKE -> config.getBikeVehicleCrossingTime(); + case LIGHT -> config.getLightVehicleCrossingTime(); + case HEAVY -> config.getHeavyVehicleCrossingTime(); + default -> config.getLightVehicleCrossingTime(); + }; + } + + /** + * Manipula o evento de início de travessia de um veículo. + *

+ * Atualmente serve como placeholder para lógica futura de animação ou + * ocupação de zonas críticas na interseção. + * + * @param event O evento de início de travessia. + */ + private void handleVehicleCrossingStartEvent(SimulationEvent event) { + // Placeholder para lógica futura de animação ou ocupação de zona crítica + eventLogger.log(sd.logging.EventType.VEHICLE_DEPARTED, intersectionId, + "Vehicle crossing started at time " + event.getTimestamp()); + } + + /** + * Finaliza a lógica de travessia e inicia a transferência (handover) para o + * próximo nó. + *

+ * Este método é invocado quando o tempo de travessia expira no relógio virtual. + * Executa as seguintes ações: + *

    + *
  1. Atualiza as métricas de tempo de travessia do veículo.
  2. + *
  3. Incrementa contadores locais de veículos processados.
  4. + *
  5. Transfere a responsabilidade do veículo para a rede, enviando-o ao + * próximo destino.
  6. + *
+ * + * @param event O evento de fim de travessia. + */ + private void handleVehicleCrossingEndEvent(SimulationEvent event) { + Vehicle vehicle = (Vehicle) event.getPayload(); + + // Atualiza métricas do veículo + double crossingTime = getCrossingTimeForVehicle(vehicle); + vehicle.addCrossingTime(crossingTime); + + // Atualiza métricas locais + intersection.incrementVehiclesSent(); + + // Handover: Transfere a responsabilidade do veículo para a rede + sendVehicleToNextDestination(vehicle); + + eventLogger.log(sd.logging.EventType.VEHICLE_DEPARTED, intersectionId, + String.format("Vehicle %s departed at time %.2f", vehicle.getId(), event.getTimestamp())); + } + + /** + * Finaliza a execução do processo de simulação. + * + * @param event O evento de fim de simulação. + */ + private void handleSimulationEndEvent(SimulationEvent event) { + eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, intersectionId, + String.format("Simulation ended at time %.2f", event.getTimestamp())); + running = false; + } + + /** + * Exporta o histórico completo de eventos para análise post-mortem. + * + * @param outputPath O caminho do ficheiro onde o histórico será guardado. + */ + public void exportEventHistory(String outputPath) { + String history = eventQueue.exportEventHistory(); + try (java.io.PrintWriter writer = new java.io.PrintWriter(outputPath)) { + writer.println(history); + System.out.println("[" + intersectionId + "] Event history exported to: " + outputPath); + } catch (java.io.FileNotFoundException e) { + System.err.println("[" + intersectionId + "] Failed to export event history: " + e.getMessage()); + } + } + + /** + * Ponto de entrada principal da aplicação. + * + * @param args Argumentos da linha de comando (ID da interseção e ficheiro de + * configuração opcional). + */ public static void main(String[] args) { if (args.length < 1) { System.err.println("Usage: java IntersectionProcess [configFile]"); @@ -124,6 +487,12 @@ public class IntersectionProcess { } } + /** + * Realiza o bootstrap dos componentes lógicos e de rede da interseção. + *

+ * Inclui a criação de semáforos, configuração de encaminhamento e conexão ao + * Dashboard. + */ public void initialize() { System.out.println("\n[" + intersectionId + "] Initializing intersection..."); @@ -137,7 +506,7 @@ public class IntersectionProcess { } /** - * Establishes connection to the dashboard server for statistics reporting. + * Estabelece a conexão com o Dashboard para envio de telemetria em tempo real. */ private void connectToDashboard() { try { @@ -161,10 +530,7 @@ public class IntersectionProcess { } /** - * Creates traffic lights for this intersection based on its physical - * connections. - * Each intersection has different number and directions of traffic lights - * according to the network topology. + * Inicializa os semáforos da interseção com base na configuração carregada. */ private void createTrafficLights() { System.out.println("\n[" + intersectionId + "] Creating traffic lights..."); @@ -193,6 +559,13 @@ public class IntersectionProcess { } } + /** + * Obtém a configuração específica para esta interseção a partir da configuração + * global. + * + * @return O objeto de configuração da interseção. + * @throws RuntimeException Se a configuração estiver em falta. + */ private SimulationConfig.IntersectionConfig getIntersectionConfig() { if (config.getNetworkConfig() == null || config.getNetworkConfig().getIntersections() == null) { throw new RuntimeException("Network configuration not loaded or empty."); @@ -203,6 +576,11 @@ public class IntersectionProcess { .orElseThrow(() -> new RuntimeException("Intersection config not found for " + intersectionId)); } + /** + * Configura a tabela de encaminhamento (routing) da interseção. + *

+ * Define para cada destino qual a direção de saída (semáforo) correspondente. + */ private void configureRouting() { System.out.println("\n[" + intersectionId + "] Configuring routing..."); @@ -224,10 +602,10 @@ public class IntersectionProcess { } /** - * 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 + * Primitiva de bloqueio: Solicita acesso exclusivo à zona crítica da + * interseção. + * + * @param direction A direção que solicita passagem. */ public void requestGreenLight(String direction) { trafficCoordinationLock.lock(); @@ -235,9 +613,9 @@ public class IntersectionProcess { } /** - * Releases the green light permission, allowing another light to turn green. - * - * @param direction The direction releasing green light + * Primitiva de bloqueio: Liberta o acesso exclusivo à zona crítica. + * + * @param direction A direção que está a libertar a passagem. */ public void releaseGreenLight(String direction) { if (direction.equals(currentGreenDirection)) { @@ -247,34 +625,89 @@ public class IntersectionProcess { } /** - * Starts all traffic light threads. + * Inicializa o estado dos semáforos no arranque da simulação (t=0). + *

+ * Garante que apenas um semáforo começa em Verde e os restantes em Vermelho, + * agendando os eventos iniciais na fila do DES. */ - private void startTrafficLights() { - System.out.println("\n[" + intersectionId + "] Starting traffic light threads..."); + private void scheduleInitialTrafficLightEvents() { + System.out.println("\n[" + intersectionId + "] Scheduling initial traffic light events (DES mode)..."); + + double currentTime = clock.getCurrentTime(); + System.out.printf("[%s] Initial clock time: %.2f%n", intersectionId, currentTime); for (TrafficLight light : intersection.getTrafficLights()) { + String direction = light.getDirection(); - TrafficLightThread lightTask = new TrafficLightThread(light, this, config); + // Lógica de arranque: Primeiro da lista = Verde, outros = Vermelho + boolean isFirstLight = intersection.getTrafficLights().indexOf(light) == 0; + TrafficLightState initialState = isFirstLight ? TrafficLightState.GREEN : TrafficLightState.RED; + light.changeState(initialState); - trafficLightPool.submit(lightTask); + // Agenda a primeira transição + double firstChangeTime = currentTime + + (initialState == TrafficLightState.GREEN ? light.getGreenTime() : light.getRedTime()); - System.out.println(" Started thread for: " + light.getDirection()); + TrafficLightEvent tlEvent = new TrafficLightEvent(light, direction, intersectionId); + SimulationEvent event = new SimulationEvent( + firstChangeTime, + DESEventType.TRAFFIC_LIGHT_CHANGE, + tlEvent); + eventQueue.schedule(event); + + System.out.println(" Scheduled first event for direction " + direction + + " (initial: " + initialState + ", change at t=" + firstChangeTime + ")"); + + eventLogger.log( + initialState == TrafficLightState.GREEN ? sd.logging.EventType.LIGHT_CHANGED_GREEN + : sd.logging.EventType.LIGHT_CHANGED_RED, + intersectionId, + "Direction " + direction + " initialized to " + initialState); } } /** - * Sends a vehicle to its next destination via socket connection. + * Encaminhamento de rede: Serializa e envia o objeto veículo para o próximo nó. + *

+ * Calcula também o tempo de viagem virtual entre nós (Edge Weight). * - * @param vehicle The vehicle that has crossed this intersection. + * @param vehicle O veículo a ser enviado. */ public void sendVehicleToNextDestination(Vehicle vehicle) { String nextDestination = vehicle.getCurrentDestination(); + // Cálculo de latência de viagem (Edge Weight) + double baseTime = config.getBaseTravelTime(); + double multiplier = 1.0; + switch (vehicle.getType()) { + case BIKE -> multiplier = config.getBikeTravelTimeMultiplier(); + case HEAVY -> multiplier = config.getHeavyTravelTimeMultiplier(); + default -> multiplier = 1.0; + } + double travelTime = baseTime * multiplier; + + System.out.printf("[%s] Vehicle %s departing to %s. Travel time: %.2fs%n", + intersectionId, vehicle.getId(), nextDestination, travelTime); + + recordVehicleDeparture(); + + // Envio imediato (o delay de viagem é implícito no tempo de chegada no próximo + // nó ou simulado aqui) + sendVehicleImmediately(vehicle, nextDestination); + } + + /** + * Envia o veículo imediatamente para o próximo nó via conexão TCP persistente. + * + * @param vehicle O veículo a ser enviado. + * @param nextDestination O identificador do próximo nó destino. + */ + private void sendVehicleImmediately(Vehicle vehicle, String nextDestination) { try { - // Get or create connection to next destination + // Lazy loading da conexão SocketConnection connection = getOrCreateConnection(nextDestination); - // Create and send message using Message class + // Encapsulamento da mensagem MessageProtocol message = new Message( MessageType.VEHICLE_TRANSFER, intersectionId, @@ -284,13 +717,8 @@ public class IntersectionProcess { connection.sendMessage(message); - System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() + - " to " + nextDestination); - - // Record departure for statistics - recordVehicleDeparture(); - - // Note: vehicle route is advanced when it arrives at the next intersection + System.out.println("[" + intersectionId + "] Vehicle " + vehicle.getId() + + " arrived at " + nextDestination + " (msg sent)"); } catch (IOException | InterruptedException e) { System.err.println("[" + intersectionId + "] Failed to send vehicle " + @@ -299,12 +727,15 @@ public class IntersectionProcess { } /** - * Gets an existing connection to a destination or creates a new one. + * Obtém ou cria uma conexão para o destino especificado (Singleton por + * destino). + *

+ * Este método é thread-safe. * - * @param destinationId The ID of the destination node. - * @return The SocketConnection to that destination. - * @throws IOException If connection cannot be established. - * @throws InterruptedException If connection attempt is interrupted. + * @param destinationId O identificador do nó destino. + * @return A conexão TCP estabelecida. + * @throws IOException Se ocorrer um erro de I/O na criação da conexão. + * @throws InterruptedException Se a thread for interrompida durante a espera. */ private synchronized SocketConnection getOrCreateConnection(String destinationId) throws IOException, InterruptedException { @@ -324,10 +755,10 @@ public class IntersectionProcess { } /** - * Gets the host address for a destination node from configuration. + * Resolve o hostname ou endereço IP para um determinado destino. * - * @param destinationId The destination node ID. - * @return The host address. + * @param destinationId O ID do destino. + * @return O endereço do host. */ private String getHostForDestination(String destinationId) { if (destinationId.equals("S")) { @@ -338,10 +769,10 @@ public class IntersectionProcess { } /** - * Gets the port number for a destination node from configuration. + * Resolve a porta TCP para um determinado destino. * - * @param destinationId The destination node ID. - * @return The port number. + * @param destinationId O ID do destino. + * @return O número da porta. */ private int getPortForDestination(String destinationId) { if (destinationId.equals("S")) { @@ -352,10 +783,11 @@ public class IntersectionProcess { } /** - * Starts the server socket and begins accepting incoming connections. - * This is the main listening loop of the process. + * Inicia o servidor e o loop de aceitação de conexões. + *

+ * Este método bloqueia a thread chamadora durante a execução do servidor. * - * @throws IOException If the server socket cannot be created. + * @throws IOException Se ocorrer um erro ao fazer bind da porta. */ public void start() throws IOException { int port = config.getIntersectionPort(intersectionId); @@ -364,15 +796,17 @@ public class IntersectionProcess { System.out.println("\n[" + intersectionId + "] Server started on port " + port); - // Start traffic light threads when running is true - startTrafficLights(); + // DES Mode: Schedule initial events and start event processor + scheduleInitialTrafficLightEvents(); + startEventProcessor(); + System.out.println("[" + intersectionId + "] Running in DES mode"); - // Start stats updater + // Background task para telemetria statsExecutor.scheduleAtFixedRate(this::sendStatsToDashboard, 1, 1, TimeUnit.SECONDS); System.out.println("[" + intersectionId + "] Waiting for incoming connections...\n"); - // Main accept loop + // Loop principal de aceitação de conexões while (running) { try { Socket clientSocket = serverSocket.accept(); @@ -380,13 +814,12 @@ public class IntersectionProcess { 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** + // Configura timeout para evitar bloqueios infinitos em leitura try { clientSocket.setSoTimeout(1000); } catch (java.net.SocketException e) { @@ -395,13 +828,12 @@ public class IntersectionProcess { continue; } - // Handle each connection in a separate thread + // Delega processamento para thread pool (NIO style) connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket)); } catch (IOException e) { - // Expected when serverSocket.close() is called during shutdown if (!running) { - break; // Normal shutdown + break; // Shutdown normal } System.err.println("[" + intersectionId + "] Error accepting connection: " + e.getMessage()); @@ -410,10 +842,13 @@ public class IntersectionProcess { } /** - * Handles an incoming connection from another process. - * Continuously listens for vehicle transfer messages. + * Lógica de tratamento de conexões de entrada (Consumer). + *

+ * Lê continuamente do socket até que a conexão seja fechada, processando + * mensagens + * de chegada de veículos ou comandos de simulação. * - * @param clientSocket The accepted socket connection. + * @param clientSocket O socket do cliente conectado. */ private void handleIncomingConnection(Socket clientSocket) { try { @@ -429,27 +864,24 @@ public class IntersectionProcess { System.out.println("[" + intersectionId + "] New connection accepted from " + clientSocket.getInetAddress().getHostAddress()); - // Continuously receive messages while connection is active while (running && connection.isConnected()) { try { MessageProtocol message = connection.receiveMessage(); - // Handle simulation start time synchronization if (message.getType() == MessageType.SIMULATION_START) { 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 + + // Lógica de desserialização polimórfica (Vehicle ou Map) Vehicle vehicle; Object payload = message.getPayload(); if (payload instanceof Vehicle) { vehicle = (Vehicle) payload; } else if (payload instanceof java.util.Map) { - // 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); @@ -461,39 +893,37 @@ public class IntersectionProcess { System.out.println("[" + intersectionId + "] Received vehicle: " + vehicle.getId() + " from " + message.getSourceNode()); - // Advance vehicle to next destination in its route + // Lógica de Roteamento Local vehicle.advanceRoute(); + intersection.receiveVehicle(vehicle, clock.getCurrentTime()); - // Add vehicle to appropriate queue - intersection.receiveVehicle(vehicle); + System.out.printf("[%s] Vehicle %s queued. Total queue size: %d%n", + intersectionId, vehicle.getId(), intersection.getTotalQueueSize()); - // Record arrival for statistics recordVehicleArrival(); + } else if (message.getType() == MessageType.SHUTDOWN) { System.out.println( "[" + intersectionId + "] Received SHUTDOWN command from " + message.getSourceNode()); running = false; - // Close this specific connection break; } } catch (java.net.SocketTimeoutException e) { - // Timeout - check running flag and continue if (!running) { break; } - // Continue waiting for next message } catch (ClassNotFoundException e) { System.err.println("[" + intersectionId + "] Unknown message type received: " + e.getMessage()); - break; // Invalid message, close connection + break; } catch (IOException e) { if (running) { System.err.println("[" + intersectionId + "] Failed to deserialize message: " + e.getMessage()); - e.printStackTrace(); // For debugging - maybe change//remove later + e.printStackTrace(); } - break; // Connection error, close connection + break; } } @@ -501,27 +931,29 @@ public class IntersectionProcess { if (running) { System.err.println("[" + intersectionId + "] Connection error: " + e.getMessage()); } - // Expected during shutdown } } /** - * Stops the intersection process gracefully. - * Shuts down all threads and closes all connections. + * Procedimento de Encerramento Gracioso (Graceful Shutdown). + *

    + *
  1. Para a aceitação de novas conexões.
  2. + *
  3. Envia últimas estatísticas.
  4. + *
  5. Encerra pools de threads.
  6. + *
  7. Fecha sockets ativos.
  8. + *
*/ public void shutdown() { - // Check if already shutdown if (!running) { - return; // Already shutdown, do nothing + return; } System.out.println("\n[" + intersectionId + "] Shutting down..."); running = false; - // Send final stats before closing connections sendStatsToDashboard(); - // 1. Close ServerSocket first + // 1. Close ServerSocket if (serverSocket != null && !serverSocket.isClosed()) { try { serverSocket.close(); @@ -530,28 +962,28 @@ public class IntersectionProcess { } } - // 2. Shutdown thread pools with force - if (trafficLightPool != null && !trafficLightPool.isShutdown()) { - trafficLightPool.shutdownNow(); - } + // 2. Shutdown thread pools if (connectionHandlerPool != null && !connectionHandlerPool.isShutdown()) { connectionHandlerPool.shutdownNow(); } if (statsExecutor != null && !statsExecutor.isShutdown()) { statsExecutor.shutdownNow(); } + if (departureExecutor != null && !departureExecutor.isShutdown()) { + departureExecutor.shutdownNow(); + } - // 3. Wait briefly for termination (don't block forever) + // 3. Wait briefly for termination try { - if (trafficLightPool != null) { - trafficLightPool.awaitTermination(1, TimeUnit.SECONDS); - } if (connectionHandlerPool != null) { connectionHandlerPool.awaitTermination(1, TimeUnit.SECONDS); } if (statsExecutor != null) { statsExecutor.awaitTermination(1, TimeUnit.SECONDS); } + if (departureExecutor != null) { + departureExecutor.awaitTermination(1, TimeUnit.SECONDS); + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -578,31 +1010,32 @@ public class IntersectionProcess { } /** - * Gets the Intersection object managed by this process. - * Useful for testing and monitoring. - * - * @return The Intersection object. + * Obtém o modelo de dados da interseção. + * + * @return O objeto Intersection. */ public Intersection getIntersection() { return intersection; } /** - * Records that a vehicle has arrived at this intersection. + * Regista a chegada de um novo veículo para fins estatísticos. */ public void recordVehicleArrival() { totalArrivals++; } /** - * Records that a vehicle has departed from this intersection. + * Regista a partida de um veículo para fins estatísticos. */ public void recordVehicleDeparture() { totalDepartures++; } /** - * Sends current statistics to the dashboard server. + * Envia um "snapshot" do estado atual para o Dashboard (Telemetria Push). + *

+ * Inclui o número acumulado de chegadas, partidas e o tamanho atual das filas. */ private void sendStatsToDashboard() { if (dashboardClient == null || !dashboardClient.isConnected()) { @@ -610,7 +1043,6 @@ public class IntersectionProcess { } try { - // Calculate current queue size int currentQueueSize = intersection.getTrafficLights().stream() .mapToInt(TrafficLight::getQueueSize) .sum(); @@ -620,7 +1052,6 @@ public class IntersectionProcess { .setIntersectionDepartures(totalDepartures) .setIntersectionQueueSize(currentQueueSize); - // Send StatsUpdatePayload directly as the message payload sd.model.Message message = new sd.model.Message( MessageType.STATS_UPDATE, intersectionId, @@ -636,4 +1067,4 @@ public class IntersectionProcess { System.err.println("[" + intersectionId + "] Failed to send stats to dashboard: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/analysis/MultiRunAnalyzer.java b/main/src/main/java/sd/analysis/MultiRunAnalyzer.java new file mode 100644 index 0000000..02c3307 --- /dev/null +++ b/main/src/main/java/sd/analysis/MultiRunAnalyzer.java @@ -0,0 +1,285 @@ +package sd.analysis; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import sd.model.VehicleType; + +/** + * Responsável pela agregação e análise estatística de múltiplas execuções da simulação. + *

+ * Esta classe coleta resultados individuais ({@link SimulationRunResult}) e calcula + * métricas consolidadas, incluindo média, desvio padrão, mediana e intervalos de + * confiança de 95%. O objetivo é fornecer uma visão robusta do comportamento do + * sistema, mitigando a variância estocástica de execuções isoladas. + */ +public class MultiRunAnalyzer { + + /** Lista acumulada de resultados de execuções individuais. */ + private final List results; + + /** Identificador do ficheiro de configuração utilizado nas execuções. */ + private final String configurationFile; + + /** + * Inicializa o analisador para um conjunto específico de configurações. + * + * @param configurationFile O caminho ou nome do ficheiro de configuração base. + */ + public MultiRunAnalyzer(String configurationFile) { + this.configurationFile = configurationFile; + this.results = new ArrayList<>(); + } + + /** + * Adiciona o resultado de uma execução de simulação concluída ao conjunto de dados. + * + * @param result O objeto contendo as métricas da execução individual. + */ + public void addResult(SimulationRunResult result) { + results.add(result); + } + + /** + * Retorna o número total de execuções armazenadas até o momento. + * + * @return O tamanho da lista de resultados. + */ + public int getRunCount() { + return results.size(); + } + + /** + * Gera um relatório estatístico abrangente formatado em texto. + *

+ * O relatório inclui: + *

+ * + * @return Uma String contendo o relatório completo formatado. + */ + public String generateReport() { + if (results.isEmpty()) { + return "No simulation results to analyze."; + } + + StringBuilder report = new StringBuilder(); + + // Header + report.append("=".repeat(80)).append("\n"); + report.append("ANÁLISE ESTATÍSTICA MULTI-EXECUÇÃO\n"); + report.append("=".repeat(80)).append("\n"); + report.append("Configuração: ").append(configurationFile).append("\n"); + report.append("Número de Execuções: ").append(results.size()).append("\n"); + report.append("Data da Análise: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n"); + report.append("\n"); + + // Global metrics + report.append("-".repeat(80)).append("\n"); + report.append("MÉTRICAS GLOBAIS\n"); + report.append("-".repeat(80)).append("\n\n"); + + report.append(analyzeMetric("Veículos Gerados", + extractValues(r -> (double) r.getTotalVehiclesGenerated()))); + report.append("\n"); + + report.append(analyzeMetric("Veículos Completados", + extractValues(r -> (double) r.getTotalVehiclesCompleted()))); + report.append("\n"); + + report.append(analyzeMetric("Taxa de Conclusão (%)", + extractValues(r -> r.getTotalVehiclesGenerated() > 0 + ? 100.0 * r.getTotalVehiclesCompleted() / r.getTotalVehiclesGenerated() + : 0.0))); + report.append("\n"); + + report.append(analyzeMetric("Tempo Médio no Sistema (segundos)", + extractValues(r -> r.getAverageSystemTime()))); + report.append("\n"); + + report.append(analyzeMetric("Tempo Médio de Espera (segundos)", + extractValues(r -> r.getAverageWaitingTime()))); + report.append("\n"); + + // Per-vehicle-type analysis + report.append("\n"); + report.append("-".repeat(80)).append("\n"); + report.append("ANÁLISE POR TIPO DE VEÍCULO\n"); + report.append("-".repeat(80)).append("\n\n"); + + for (VehicleType type : VehicleType.values()) { + report.append("--- ").append(type).append(" ---\n"); + + report.append(analyzeMetric(" Contagem de Veículos", + extractValues(r -> (double) r.getVehicleCountByType().getOrDefault(type, 0)))); + report.append("\n"); + + report.append(analyzeMetric(" Tempo Médio no Sistema (segundos)", + extractValues(r -> r.getAvgSystemTimeByType().getOrDefault(type, 0.0)))); + report.append("\n"); + + report.append(analyzeMetric(" Tempo Médio de Espera (segundos)", + extractValues(r -> r.getAvgWaitTimeByType().getOrDefault(type, 0.0)))); + report.append("\n\n"); + } + + // Per-intersection analysis + report.append("-".repeat(80)).append("\n"); + report.append("ANÁLISE POR INTERSEÇÃO\n"); + report.append("-".repeat(80)).append("\n\n"); + + Set allIntersections = new TreeSet<>(); + for (SimulationRunResult result : results) { + allIntersections.addAll(result.getMaxQueueSizeByIntersection().keySet()); + } + + for (String intersection : allIntersections) { + report.append("--- ").append(intersection).append(" ---\n"); + + report.append(analyzeMetric(" Tamanho Máximo da Fila", + extractValues(r -> (double) r.getMaxQueueSizeByIntersection().getOrDefault(intersection, 0)))); + report.append("\n"); + + report.append(analyzeMetric(" Tamanho Médio da Fila", + extractValues(r -> r.getAvgQueueSizeByIntersection().getOrDefault(intersection, 0.0)))); + report.append("\n"); + + report.append(analyzeMetric(" Veículos Processados", + extractValues(r -> (double) r.getVehiclesProcessedByIntersection().getOrDefault(intersection, 0)))); + report.append("\n\n"); + } + + // Individual run summaries + report.append("-".repeat(80)).append("\n"); + report.append("RESUMOS INDIVIDUAIS DAS EXECUÇÕES\n"); + report.append("-".repeat(80)).append("\n\n"); + + for (SimulationRunResult result : results) { + report.append(result.toString()).append("\n\n"); + } + + report.append("=".repeat(80)).append("\n"); + report.append("FIM DO RELATÓRIO\n"); + report.append("=".repeat(80)).append("\n"); + + return report.toString(); + } + + /** + * Analisa uma métrica específica e retorna as estatísticas formatadas. + *

+ * Calcula média, desvio padrão, mediana, intervalo de confiança (95%) e extremos (min/max). + * + * @param metricName O nome descritivo da métrica (ex: "Tempo de Espera"). + * @param values A lista de valores numéricos brutos extraídos das execuções. + * @return Uma string formatada com os dados estatísticos. + */ + private String analyzeMetric(String metricName, List values) { + if (values.isEmpty() || values.stream().allMatch(v -> v == 0.0)) { + return metricName + ": Sem dados\n"; + } + + double mean = StatisticalAnalysis.mean(values); + double stdDev = StatisticalAnalysis.standardDeviation(values); + double[] ci = StatisticalAnalysis.confidenceInterval95(values); + double min = StatisticalAnalysis.min(values); + double max = StatisticalAnalysis.max(values); + double median = StatisticalAnalysis.median(values); + + return String.format( + "%s:\n" + + " Média: %10.2f Desvio Padrão: %10.2f\n" + + " Mediana: %10.2f IC 95%%: [%.2f, %.2f]\n" + + " Mín: %10.2f Máx: %10.2f\n", + metricName, mean, stdDev, median, ci[0], ci[1], min, max + ); + } + + /** + * Extrai valores numéricos dos resultados de simulação usando uma função mapeadora. + *

+ * Utilizado internamente para transformar a lista de objetos complexos {@link SimulationRunResult} + * em listas simples de Doubles para processamento estatístico. + * + * @param extractor Função lambda que define qual campo extrair de cada resultado. + * @return Lista de valores double correspondentes. + */ + private List extractValues(java.util.function.Function extractor) { + List values = new ArrayList<>(); + for (SimulationRunResult result : results) { + values.add(extractor.apply(result)); + } + return values; + } + + /** + * Persiste o relatório gerado num ficheiro de texto. + * + * @param filename O caminho do ficheiro de destino. + * @throws IOException Se ocorrer um erro de escrita no disco. + */ + public void saveReport(String filename) throws IOException { + try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { + writer.print(generateReport()); + } + } + + /** + * Gera um resumo em formato CSV para fácil importação em ferramentas de planilha. + *

+ * Este método atua como um wrapper para {@link #saveCSVSummary(String)}. + * + * @param filename O caminho do ficheiro CSV de destino. + * @throws IOException Se ocorrer um erro de escrita no disco. + */ + public void saveCSV(String filename) throws IOException { + saveCSVSummary(filename); + } + + /** + * Gera e grava o sumário CSV detalhado com métricas chave por execução. + *

+ * Colunas incluídas: Execução, VeículosGerados, VeículosCompletados, TaxaConclusão, + * TempoMédioSistema, TempoMédioEspera, TempoMínimoSistema, TempoMáximoSistema. + * + * @param filename O caminho do ficheiro CSV de destino. + * @throws IOException Se ocorrer um erro de escrita no disco. + */ + public void saveCSVSummary(String filename) throws IOException { + try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { + // Header + writer.println("Execução,VeículosGerados,VeículosCompletados,TaxaConclusão," + + "TempoMédioSistema,TempoMédioEspera,TempoMínimoSistema,TempoMáximoSistema"); + + // Data rows + for (SimulationRunResult result : results) { + double completionRate = result.getTotalVehiclesGenerated() > 0 + ? 100.0 * result.getTotalVehiclesCompleted() / result.getTotalVehiclesGenerated() + : 0.0; + + writer.printf("%d,%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f\n", + result.getRunNumber(), + result.getTotalVehiclesGenerated(), + result.getTotalVehiclesCompleted(), + completionRate, + result.getAverageSystemTime(), + result.getAverageWaitingTime(), + result.getMinSystemTime(), + result.getMaxSystemTime() + ); + } + } + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/analysis/SimulationRunResult.java b/main/src/main/java/sd/analysis/SimulationRunResult.java new file mode 100644 index 0000000..465ef36 --- /dev/null +++ b/main/src/main/java/sd/analysis/SimulationRunResult.java @@ -0,0 +1,206 @@ +package sd.analysis; + +import java.util.HashMap; +import java.util.Map; + +import sd.model.VehicleType; + +/** + * Encapsula os dados telemétricos e estatísticos resultantes de uma única execução da simulação. + *

+ * Esta classe atua como um registo estruturado de métricas de desempenho, armazenando + * dados de latência (tempos de sistema/espera), vazão (throughput) e ocupação de recursos + * (tamanhos de fila). Os dados aqui contidos servem como base para a análise + * estatística agregada realizada pelo {@link MultiRunAnalyzer}. + */ +public class SimulationRunResult { + + private final int runNumber; + private final String configurationFile; + private final long startTimeMillis; + private final long endTimeMillis; + + // Global metrics + /** Total de veículos instanciados pelos geradores durante a execução. */ + private int totalVehiclesGenerated; + + /** Total de veículos que completaram o percurso e saíram do sistema com sucesso. */ + private int totalVehiclesCompleted; + + /** Média global do tempo total (em segundos) desde a geração até a saída. */ + private double averageSystemTime; // seconds + + /** Menor tempo de sistema registado (em segundos). */ + private double minSystemTime; // seconds + + /** Maior tempo de sistema registado (em segundos). */ + private double maxSystemTime; // seconds + + /** Média global do tempo (em segundos) que os veículos passaram parados em filas. */ + private double averageWaitingTime; // seconds + + // Per-type metrics + private final Map vehicleCountByType; + private final Map avgSystemTimeByType; + private final Map avgWaitTimeByType; + + // Per-intersection metrics + private final Map maxQueueSizeByIntersection; + private final Map avgQueueSizeByIntersection; + private final Map vehiclesProcessedByIntersection; + + /** + * Inicializa um novo contentor de resultados para uma execução específica. + * + * @param runNumber O identificador sequencial desta execução. + * @param configurationFile O ficheiro de configuração utilizado. + */ + public SimulationRunResult(int runNumber, String configurationFile) { + this.runNumber = runNumber; + this.configurationFile = configurationFile; + this.startTimeMillis = System.currentTimeMillis(); + this.endTimeMillis = 0; + + this.vehicleCountByType = new HashMap<>(); + this.avgSystemTimeByType = new HashMap<>(); + this.avgWaitTimeByType = new HashMap<>(); + this.maxQueueSizeByIntersection = new HashMap<>(); + this.avgQueueSizeByIntersection = new HashMap<>(); + this.vehiclesProcessedByIntersection = new HashMap<>(); + } + + /** + * Sinaliza o fim da recolha de dados para esta execução. + * (Placeholder para lógica de finalização de timestamps). + */ + public void markCompleted() { + // This will be called when the run finishes + } + + // Getters + public int getRunNumber() { return runNumber; } + public String getConfigurationFile() { return configurationFile; } + public long getStartTimeMillis() { return startTimeMillis; } + public long getEndTimeMillis() { return endTimeMillis; } + + /** + * Calcula a duração total da execução em milissegundos. + * @return Delta entre fim e início. + */ + public long getDurationMillis() { return endTimeMillis - startTimeMillis; } + + public int getTotalVehiclesGenerated() { return totalVehiclesGenerated; } + public int getTotalVehiclesCompleted() { return totalVehiclesCompleted; } + public double getAverageSystemTime() { return averageSystemTime; } + public double getMinSystemTime() { return minSystemTime; } + public double getMaxSystemTime() { return maxSystemTime; } + public double getAverageWaitingTime() { return averageWaitingTime; } + + /** + * Retorna o mapeamento de contagem de veículos por tipo. + * @return Uma cópia defensiva do mapa (snapshot). + */ + public Map getVehicleCountByType() { + return new HashMap<>(vehicleCountByType); + } + + /** + * Retorna o tempo médio no sistema segmentado por tipo de veículo. + * @return Uma cópia defensiva do mapa (snapshot). + */ + public Map getAvgSystemTimeByType() { + return new HashMap<>(avgSystemTimeByType); + } + + /** + * Retorna o tempo médio de espera segmentado por tipo de veículo. + * @return Uma cópia defensiva do mapa (snapshot). + */ + public Map getAvgWaitTimeByType() { + return new HashMap<>(avgWaitTimeByType); + } + + /** + * Retorna o tamanho máximo de fila registado por interseção (gargalos). + * @return Uma cópia defensiva do mapa (snapshot). + */ + public Map getMaxQueueSizeByIntersection() { + return new HashMap<>(maxQueueSizeByIntersection); + } + + /** + * Retorna o tamanho médio das filas por interseção. + * @return Uma cópia defensiva do mapa (snapshot). + */ + public Map getAvgQueueSizeByIntersection() { + return new HashMap<>(avgQueueSizeByIntersection); + } + + /** + * Retorna o total de veículos processados (throughput) por interseção. + * @return Uma cópia defensiva do mapa (snapshot). + */ + public Map getVehiclesProcessedByIntersection() { + return new HashMap<>(vehiclesProcessedByIntersection); + } + + // Setters + public void setTotalVehiclesGenerated(int count) { + this.totalVehiclesGenerated = count; + } + public void setTotalVehiclesCompleted(int count) { + this.totalVehiclesCompleted = count; + } + public void setAverageSystemTime(double time) { + this.averageSystemTime = time; + } + public void setMinSystemTime(double time) { + this.minSystemTime = time; + } + public void setMaxSystemTime(double time) { + this.maxSystemTime = time; + } + public void setAverageWaitingTime(double time) { + this.averageWaitingTime = time; + } + + public void setVehicleCountByType(VehicleType type, int count) { + vehicleCountByType.put(type, count); + } + public void setAvgSystemTimeByType(VehicleType type, double time) { + avgSystemTimeByType.put(type, time); + } + public void setAvgWaitTimeByType(VehicleType type, double time) { + avgWaitTimeByType.put(type, time); + } + public void setMaxQueueSize(String intersection, int size) { + maxQueueSizeByIntersection.put(intersection, size); + } + public void setAvgQueueSize(String intersection, double size) { + avgQueueSizeByIntersection.put(intersection, size); + } + public void setVehiclesProcessed(String intersection, int count) { + vehiclesProcessedByIntersection.put(intersection, count); + } + + /** + * Gera uma representação textual resumida das métricas principais da execução. + * Útil para logs rápidos e debugging. + */ + @Override + public String toString() { + return String.format( + "Execução #%d [%s]:\n" + + " Gerados: %d, Completados: %d (%.1f%%)\n" + + " Tempo Médio no Sistema: %.2fs\n" + + " Tempo Médio de Espera: %.2fs", + runNumber, + configurationFile, + totalVehiclesGenerated, + totalVehiclesCompleted, + totalVehiclesGenerated > 0 ? 100.0 * totalVehiclesCompleted / totalVehiclesGenerated : 0.0, + averageSystemTime, + averageWaitingTime + ); + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/analysis/StatisticalAnalysis.java b/main/src/main/java/sd/analysis/StatisticalAnalysis.java new file mode 100644 index 0000000..c58ad67 --- /dev/null +++ b/main/src/main/java/sd/analysis/StatisticalAnalysis.java @@ -0,0 +1,193 @@ +package sd.analysis; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utilitário estático para processamento matemático e análise estatística dos dados da simulação. + *

+ * Esta classe fornece algoritmos para cálculo de medidas de tendência central (média, mediana), + * dispersão (desvio padrão amostral) e inferência estatística (Intervalos de Confiança). + * É utilizada para normalizar e validar os resultados estocásticos obtidos através de + * múltiplas execuções do sistema. + */ +public class StatisticalAnalysis { + + /** + * Calcula a média aritmética de um conjunto de valores. + * * @param values Lista de valores numéricos (double). + * @return A soma dos valores dividida pelo tamanho da amostra, ou 0.0 se a lista for nula/vazia. + */ + public static double mean(List values) { + if (values == null || values.isEmpty()) { + return 0.0; + } + double sum = 0.0; + for (double value : values) { + sum += value; + } + return sum / values.size(); + } + + /** + * Calcula o desvio padrão amostral (sample standard deviation). + *

+ * Utiliza o denominador {@code n - 1} (Correção de Bessel) para fornecer um + * estimador não viesado da variância populacional, adequado para as amostras + * de simulação. + * * @param values Lista de observações. + * @return O desvio padrão calculado, ou 0.0 se o tamanho da amostra for < 2. + */ + public static double standardDeviation(List values) { + if (values == null || values.size() < 2) { + return 0.0; + } + + double mean = mean(values); + double sumSquaredDiff = 0.0; + + for (double value : values) { + double diff = value - mean; + sumSquaredDiff += diff * diff; + } + + // Sample standard deviation (n-1 denominator) + return Math.sqrt(sumSquaredDiff / (values.size() - 1)); + } + + /** + * Calcula o Intervalo de Confiança (IC) de 95% para a média. + *

+ * Utiliza a distribuição t de Student para maior precisão em amostras pequenas (n < 30), + * onde a aproximação pela distribuição Normal (Z) seria inadequada. O intervalo define + * a faixa onde a verdadeira média populacional reside com 95% de probabilidade. + * * @param values Lista de observações. + * @return Um array de double onde índice 0 é o limite inferior e índice 1 é o limite superior. + */ + public static double[] confidenceInterval95(List values) { + if (values == null || values.size() < 2) { + double m = mean(values); + return new double[]{m, m}; + } + + double mean = mean(values); + double stdDev = standardDeviation(values); + int n = values.size(); + + // Critical value from t-distribution (approximation for common sample sizes) + double tCritical = getTCriticalValue(n); + + // Standard error of the mean + double standardError = stdDev / Math.sqrt(n); + + // Margin of error + double marginOfError = tCritical * standardError; + + return new double[]{ + mean - marginOfError, // Lower bound + mean + marginOfError // Upper bound + }; + } + + /** + * Retorna o valor crítico t (t-score) para um IC de 95% (bicaudal). + *

+ * Baseia-se nos graus de liberdade (gl = n - 1). Para amostras grandes (gl >= 30), + * aproxima-se do valor Z de 1.96. + * * @param sampleSize O tamanho da amostra (n). + * @return O fator multiplicativo t apropriado. + */ + private static double getTCriticalValue(int sampleSize) { + int df = sampleSize - 1; // degrees of freedom + + // t-critical values for 95% confidence (two-tailed) + if (df >= 30) return 1.96; // z-score for large samples + if (df >= 20) return 2.086; + if (df >= 15) return 2.131; + if (df >= 10) return 2.228; + if (df >= 5) return 2.571; + if (df >= 3) return 3.182; + if (df >= 2) return 4.303; + return 12.706; // df = 1 + } + + /** + * Identifica o valor mínimo absoluto na amostra. + * * @param values Lista de valores. + * @return O menor valor encontrado. + */ + public static double min(List values) { + if (values == null || values.isEmpty()) { + return 0.0; + } + return Collections.min(values); + } + + /** + * Identifica o valor máximo absoluto na amostra. + * * @param values Lista de valores. + * @return O maior valor encontrado. + */ + public static double max(List values) { + if (values == null || values.isEmpty()) { + return 0.0; + } + return Collections.max(values); + } + + /** + * Calcula a mediana da amostra. + *

+ * Nota de Desempenho: Este método ordena uma cópia da lista, resultando em + * complexidade O(n log n). + * * @param values Lista de valores. + * @return O valor central (ou média dos dois centrais) da distribuição ordenada. + */ + public static double median(List values) { + if (values == null || values.isEmpty()) { + return 0.0; + } + + List sorted = new ArrayList<>(values); + Collections.sort(sorted); + + int size = sorted.size(); + if (size % 2 == 0) { + return (sorted.get(size / 2 - 1) + sorted.get(size / 2)) / 2.0; + } else { + return sorted.get(size / 2); + } + } + + /** + * Formata um sumário estatístico completo para uma métrica específica. + *

+ * Útil para logging e geração de relatórios textuais. + * * @param metricName Nome da métrica a ser exibida. + * @param values Os dados brutos associados à métrica. + * @return String formatada contendo Média, Desvio Padrão, IC95%, Min, Max e N. + */ + public static String formatSummary(String metricName, List values) { + if (values == null || values.isEmpty()) { + return metricName + ": No data"; + } + + double mean = mean(values); + double stdDev = standardDeviation(values); + double[] ci = confidenceInterval95(values); + double min = min(values); + double max = max(values); + + return String.format( + "%s:\n" + + " Mean: %.2f\n" + + " Std Dev: %.2f\n" + + " 95%% CI: [%.2f, %.2f]\n" + + " Min: %.2f\n" + + " Max: %.2f\n" + + " Samples: %d", + metricName, mean, stdDev, ci[0], ci[1], min, max, values.size() + ); + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/config/SimulationConfig.java b/main/src/main/java/sd/config/SimulationConfig.java index b974495..db4a9da 100644 --- a/main/src/main/java/sd/config/SimulationConfig.java +++ b/main/src/main/java/sd/config/SimulationConfig.java @@ -14,19 +14,28 @@ import java.util.Properties; import com.google.gson.Gson; /** - * Class to load and manage simulation configurations. - * Configurations are read from a .properties file. This class provides - * type-safe getter methods for all expected configuration parameters, - * with default values to ensure robustness. + * Responsável pelo carregamento, validação e acesso centralizado às configurações da simulação. + *

+ * Esta classe atua como uma fachada (Facade) para os parâmetros do sistema, abstraindo a origem + * dos dados (ficheiros {@code .properties} ou JSON). Implementa uma estratégia robusta de + * carregamento de recursos, suportando tanto caminhos absolutos do sistema de ficheiros quanto + * recursos embutidos no classpath. + *

+ * Além de propriedades chave-valor simples, gerencia a desserialização da topologia da rede + * através da classe interna {@link NetworkConfig}. */ public class SimulationConfig { - /** - * Holds all properties loaded from the file. - */ + /** Armazenamento em memória das propriedades chave-valor carregadas. */ private final Properties properties; + + /** Estrutura hierárquica da configuração da rede carregada via JSON. */ private NetworkConfig networkConfig; + /** + * Objeto de transferência de dados (DTO) que representa a configuração global da rede. + * Mapeado a partir do ficheiro {@code network_config.json}. + */ public static class NetworkConfig { private List intersections; @@ -35,37 +44,45 @@ public class SimulationConfig { } } + /** + * DTO que representa a configuração de uma única interseção na topologia. + */ public static class IntersectionConfig { private String id; private List lights; private Map routes; + /** @return O identificador único da interseção (ex: "Cr1"). */ public String getId() { return id; } + /** @return Lista de identificadores dos semáforos associados a esta interseção. */ public List getLights() { return lights; } + /** @return Mapa de roteamento definindo destinos alcançáveis e seus próximos saltos. */ public Map getRoutes() { return routes; } } /** - * Constructs a new SimulationConfig object by loading properties - * from the specified file path. - * - * This constructor attempts to load the configuration file using multiple - * strategies: - * 1. Direct file system path - * 2. Classpath resource (with automatic path normalization) - * 3. Classpath resource with leading slash + * Inicializa o gestor de configuração carregando propriedades do caminho especificado. + * *

Implementa uma estratégia de carregamento em cascata (fallback) para garantir robustez + * em diferentes ambientes de execução (IDE, JAR, Docker): + *

    + *
  1. Sistema de Ficheiros Direto: Tenta carregar do caminho absoluto ou relativo.
  2. + *
  3. Classpath (Contexto): Tenta carregar via {@code Thread.currentThread().getContextClassLoader()}, + * normalizando prefixos como "src/main/resources" ou "classpath:".
  4. + *
  5. Classpath (Classe): Tenta carregar via {@code SimulationConfig.class.getResourceAsStream}, + * útil para recursos na raiz do JAR.
  6. + *
* - * @param filePath The path to the .properties file (e.g., - * "src/main/resources/simulation.properties"). - * @throws IOException If the file cannot be found or read from any location. + * @param filePath O caminho ou nome do recurso do ficheiro {@code .properties}. + * @throws IOException Se o ficheiro não puder ser localizado em nenhuma das estratégias, + * com uma mensagem detalhada das tentativas falhadas. */ public SimulationConfig(String filePath) throws IOException { properties = new Properties(); @@ -138,6 +155,12 @@ public class SimulationConfig { throw new IOException(errorMsg.toString(), fileSystemException); } + /** + * Carrega a configuração da topologia de rede a partir do ficheiro "network_config.json". + *

+ * Utiliza a biblioteca Gson para desserialização. Em caso de falha, emite um aviso para o + * {@code System.err} mas não aborta a execução, permitindo o uso de defaults ou redes vazias. + */ private void loadNetworkConfig() { try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) { if (is == null) { @@ -154,6 +177,10 @@ public class SimulationConfig { } } + /** + * Retorna a configuração estruturada da rede. + * @return Objeto {@link NetworkConfig} ou null se o carregamento falhou. + */ public NetworkConfig getNetworkConfig() { return networkConfig; } @@ -161,56 +188,50 @@ public class SimulationConfig { // --- Network configurations --- /** - * Gets the host address for a specific intersection. - * - * @param intersectionId The ID of the intersection (e.g., "Cr1"). - * @return The host (e.g., "localhost"). + * Obtém o endereço de host (nome DNS ou IP) para uma interseção específica. + * * @param intersectionId O ID da interseção (ex: "Cr1"). + * @return O host configurado ou "localhost" por omissão. */ public String getIntersectionHost(String intersectionId) { return properties.getProperty("intersection." + intersectionId + ".host", "localhost"); } /** - * Gets the port number for a specific intersection. - * - * @param intersectionId The ID of the intersection (e.g., "Cr1"). - * @return The port number. + * Obtém a porta de escuta TCP para uma interseção específica. + * * @param intersectionId O ID da interseção (ex: "Cr1"). + * @return O número da porta. Retorna 0 se não configurado. */ public int getIntersectionPort(String intersectionId) { return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0")); } /** - * Gets the host address for the dashboard server. - * - * @return The dashboard host. + * Obtém o endereço de host do servidor de Dashboard (monitorização). + * @return O host do dashboard (padrão: "localhost"). */ public String getDashboardHost() { return properties.getProperty("dashboard.host", "localhost"); } /** - * Gets the port number for the dashboard server. - * - * @return The dashboard port. + * Obtém a porta de conexão do servidor de Dashboard. + * @return A porta do dashboard (padrão: 9000). */ public int getDashboardPort() { return Integer.parseInt(properties.getProperty("dashboard.port", "9000")); } /** - * Gets the host address for the exit node. - * - * @return The exit node host. + * Obtém o endereço de host do nó de saída (Exit Node), para onde os veículos são encaminhados ao sair da malha. + * @return O host do nó de saída (padrão: "localhost"). */ public String getExitHost() { return properties.getProperty("exit.host", "localhost"); } /** - * Gets the port number for the exit node. - * - * @return The exit node port. + * Obtém a porta de conexão do nó de saída. + * @return A porta do nó de saída (padrão: 9001). */ public int getExitPort() { return Integer.parseInt(properties.getProperty("exit.port", "9001")); @@ -219,60 +240,76 @@ public class SimulationConfig { // --- Simulation configurations --- /** - * Gets the total duration of the simulation in virtual seconds. - * - * @return The simulation duration. + * Define a duração total da execução da simulação em segundos virtuais. + * @return A duração em segundos (padrão: 3600). */ public double getSimulationDuration() { - return Double.parseDouble(properties.getProperty("simulation.duration", "3600.0")); + return Double.parseDouble(properties.getProperty("simulation.duration", "3600")); } /** - * Gets the drain time (in virtual seconds) to allow vehicles to exit after - * generation stops. - * - * @return The drain time. + * Obtém o fator de escala temporal para visualização/execução. + *

+ * @return O fator de escala. + */ + public double getTimeScale() { + return Double.parseDouble(properties.getProperty("simulation.time.scale", "0")); + } + + /** + * Obtém o tempo de "drenagem" (drain time) em segundos virtuais. + *

+ * Este é o período adicional executado após o fim da geração de veículos para permitir + * que os veículos restantes no sistema completem os seus percursos. + * @return O tempo de drenagem (padrão: 60.0s). */ public double getDrainTime() { return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0")); } /** - * Gets the vehicle arrival model ("POISSON" or "FIXED"). - * - * @return The arrival model as a string. + * Determina o modelo estocástico utilizado para a chegada de veículos. + * @return "POISSON" (distribuição exponencial) ou "FIXED" (intervalo determinístico). */ public String getArrivalModel() { return properties.getProperty("simulation.arrival.model", "POISSON"); } /** - * Gets the average arrival rate (lambda) for the POISSON model. - * This represents the average number of vehicles arriving per second. - * - * @return The arrival rate. + * Obtém a taxa média de chegada (lambda) para o modelo Poisson. + * @return Veículos por segundo (padrão: 0.5). */ public double getArrivalRate() { return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5")); } /** - * Gets the fixed time interval between vehicle arrivals for the FIXED model. - * - * @return The fixed interval in seconds. + * Obtém o intervalo fixo entre chegadas para o modelo determinístico. + * @return O intervalo em segundos (padrão: 2.0). */ public double getFixedArrivalInterval() { return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0")); } + /** + * Obtém a política de roteamento utilizada pelos veículos para navegar na malha. + * @return A política: "RANDOM", "SHORTEST_PATH" ou "LEAST_CONGESTED". + */ + public String getRoutingPolicy() { + return properties.getProperty("simulation.routing.policy", "RANDOM"); + } + // --- Traffic light configurations --- /** - * Gets the duration of the GREEN light state for a specific traffic light. - * - * @param intersectionId The ID of the intersection (e.g., "Cr1"). - * @param direction The direction of the light (e.g., "North"). - * @return The green light time in seconds. + * Obtém a duração do estado VERDE para um semáforo específico. + * * @param intersectionId ID da interseção. + * @param direction Direção do fluxo (ex: "North"). + * @return Duração em segundos (padrão: 30.0). */ public double getTrafficLightGreenTime(String intersectionId, String direction) { String key = "trafficlight." + intersectionId + "." + direction + ".green"; @@ -280,11 +317,10 @@ public class SimulationConfig { } /** - * Gets the duration of the RED light state for a specific traffic light. - * - * @param intersectionId The ID of the intersection (e.g., "Cr1"). - * @param direction The direction of the light (e.g., "North"). - * @return The red light time in seconds. + * Obtém a duração do estado VERMELHO para um semáforo específico. + * * @param intersectionId ID da interseção. + * @param direction Direção do fluxo. + * @return Duração em segundos (padrão: 30.0). */ public double getTrafficLightRedTime(String intersectionId, String direction) { String key = "trafficlight." + intersectionId + "." + direction + ".red"; @@ -294,94 +330,84 @@ public class SimulationConfig { // --- Vehicle configurations --- /** - * Gets the probability (0.0 to 1.0) that a generated vehicle is of type LIGHT. - * - * @return The probability for LIGHT vehicles. + * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo LIGEIRO (LIGHT). + * @return Probabilidade (padrão: 0.7). */ public double getLightVehicleProbability() { return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7")); } /** - * Gets the average time it takes a LIGHT vehicle to cross an intersection. - * - * @return The crossing time in seconds. + * Tempo médio necessário para um veículo LIGEIRO atravessar uma interseção. + * @return Tempo em segundos (padrão: 2.0). */ public double getLightVehicleCrossingTime() { return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0")); } /** - * Gets the probability (0.0 to 1.0) that a generated vehicle is of type BIKE. - * - * @return The probability for BIKE vehicles. + * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo BICICLETA (BIKE). + * @return Probabilidade (padrão: 0.0). */ public double getBikeVehicleProbability() { return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0")); } /** - * Gets the average time it takes a BIKE vehicle to cross an intersection. - * - * @return The crossing time in seconds. + * Tempo médio necessário para uma BICICLETA atravessar uma interseção. + * @return Tempo em segundos (padrão: 1.5). */ public double getBikeVehicleCrossingTime() { return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5")); } /** - * Gets the probability (0.0 to 1.0) that a generated vehicle is of type HEAVY. - * - * @return The probability for HEAVY vehicles. + * Probabilidade (0.0 a 1.0) de geração de um veículo PESADO (HEAVY). + * @return Probabilidade (padrão: 0.0). */ public double getHeavyVehicleProbability() { return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0")); } /** - * Gets the average time it takes a HEAVY vehicle to cross an intersection. - * - * @return The crossing time in seconds. + * Tempo médio necessário para um veículo PESADO atravessar uma interseção. + * @return Tempo em segundos (padrão: 4.0). */ public double getHeavyVehicleCrossingTime() { return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0")); } /** - * Gets the base travel time between intersections for light vehicles. - * - * @return The base travel time in seconds. + * Define o tempo base de viagem entre interseções para veículos padrão. + * @return Tempo em segundos (padrão: 8.0). */ public double getBaseTravelTime() { return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0")); } /** - * Gets the travel time multiplier for bike vehicles. - * Bike travel time = base time × this multiplier. - * - * @return The multiplier for bike travel time. + * Multiplicador de tempo de viagem para bicicletas. + *

Tempo efetivo = Base * Multiplicador. + * @return Fator multiplicativo (padrão: 0.5). */ public double getBikeTravelTimeMultiplier() { return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5")); } /** - * Gets the travel time multiplier for heavy vehicles. - * Heavy vehicle travel time = base time × this multiplier. - * - * @return The multiplier for heavy vehicle travel time. + * Multiplicador de tempo de viagem para veículos pesados. + *

Tempo efetivo = Base * Multiplicador. + * @return Fator multiplicativo (padrão: 4.0). */ public double getHeavyTravelTimeMultiplier() { - return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "2.0")); + return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0")); } // --- Statistics --- /** - * Gets the interval (in virtual seconds) between periodic statistics updates. - * - * @return The statistics update interval. + * Intervalo de tempo (em segundos virtuais) para agregação e envio de estatísticas periódicas. + * @return Intervalo de atualização (padrão: 1.0). */ public double getStatisticsUpdateInterval() { return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0")); @@ -390,21 +416,19 @@ public class SimulationConfig { // --- Generic getters --- /** - * Generic method to get any property as a string, with a default value. - * - * @param key The property key. - * @param defaultValue The value to return if the key is not found. - * @return The property value or the default. + * Recupera uma propriedade genérica como String, com valor padrão de segurança. + * * @param key A chave da propriedade. + * @param defaultValue O valor a retornar caso a chave não exista. + * @return O valor da propriedade ou o default. */ public String getProperty(String key, String defaultValue) { return properties.getProperty(key, defaultValue); } /** - * Generic method to get any property as a string. - * - * @param key The property key. - * @return The property value, or null if not found. + * Recupera uma propriedade genérica como String. + * * @param key A chave da propriedade. + * @return O valor da propriedade ou null se não encontrada. */ public String getProperty(String key) { return properties.getProperty(key); diff --git a/main/src/main/java/sd/coordinator/CoordinatorProcess.java b/main/src/main/java/sd/coordinator/CoordinatorProcess.java index 2fb1423..dd9135e 100644 --- a/main/src/main/java/sd/coordinator/CoordinatorProcess.java +++ b/main/src/main/java/sd/coordinator/CoordinatorProcess.java @@ -5,32 +5,72 @@ import java.util.HashMap; import java.util.Map; import sd.config.SimulationConfig; +import sd.dashboard.DashboardStatistics; import sd.dashboard.StatsUpdatePayload; +import sd.des.DESEventType; +import sd.des.EventQueue; +import sd.des.SimulationClock; +import sd.des.SimulationEvent; +import sd.logging.EventLogger; import sd.model.Message; import sd.model.MessageType; import sd.model.Vehicle; +import sd.routing.LeastCongestedRouteSelector; +import sd.routing.RandomRouteSelector; +import sd.routing.RouteSelector; +import sd.routing.RoutingPolicy; +import sd.routing.ShortestPathRouteSelector; import sd.serialization.SerializationException; import sd.util.VehicleGenerator; /** - * Coordinator process responsible for: - * 1. Vehicle generation (using VehicleGenerator) - * 2. Distributing vehicles to intersection processes via sockets - * 3. Managing simulation timing and shutdown - * - * This is the main entry point for the distributed simulation architecture. + * Coordenador central da arquitetura de simulação distribuída. + *

+ * Este processo atua como o "cérebro" da simulação, sendo responsável por: + *

    + *
  1. Orquestração DES: Gerir o relógio global ({@link SimulationClock}) e a fila de eventos prioritária.
  2. + *
  3. Geração de Carga: Injetar veículos na malha viária seguindo distribuições estocásticas (Poisson) ou determinísticas.
  4. + *
  5. Encaminhamento Dinâmico: Decidir as rotas dos veículos com base na política ativa (Random, Shortest Path, Least Congested).
  6. + *
  7. Sincronização: Garantir que todos os nós (Interseções e Dashboard) operem em uníssono.
  8. + *
*/ public class CoordinatorProcess { private final SimulationConfig config; private final VehicleGenerator vehicleGenerator; + + /** Mapa de clientes TCP persistentes para cada interseção (Worker Nodes). */ private final Map intersectionClients; private SocketClient dashboardClient; - private double currentTime; + + // Componentes DES (Discrete Event Simulation) + private final SimulationClock clock; + private final EventQueue eventQueue; + private final EventLogger eventLogger; + + // Estado da simulação private int vehicleCounter; private boolean running; - private double nextGenerationTime; + private double timeScale; + private RouteSelector currentRouteSelector; + + /** Referência para estatísticas do dashboard para polling de mudanças de política. */ + private DashboardStatistics dashboardStatistics; + + /** + * Monitorização local (aproximada) dos tamanhos de fila nas interseções. + *

+ * Utilizado exclusivamente pela política {@link LeastCongestedRouteSelector}. + * O coordenador incrementa este contador ao enviar um veículo para uma interseção. + * Nota: Esta é uma visão "borda" (edge) e pode não refletir a saída em tempo real + * dos veículos, mas serve como heurística suficiente para balanceamento de carga. + */ + private final Map intersectionQueueSizes; + /** + * Ponto de entrada do processo Coordenador. + * Carrega configurações, estabelece conexões TCP e inicia o loop de eventos. + */ public static void main(String[] args) { System.out.println("=".repeat(60)); System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION"); @@ -61,21 +101,76 @@ public class CoordinatorProcess { } } + /** + * Inicializa o coordenador com a configuração fornecida. + * Configura o motor DES, logging e o seletor de rotas inicial. + * + * @param config Objeto de configuração carregado. + */ public CoordinatorProcess(SimulationConfig config) { this.config = config; - this.vehicleGenerator = new VehicleGenerator(config); + + // Inicializa o RouteSelector baseado na política configurada + this.currentRouteSelector = createRouteSelector(config.getRoutingPolicy()); + + this.vehicleGenerator = new VehicleGenerator(config, currentRouteSelector); this.intersectionClients = new HashMap<>(); - this.currentTime = 0.0; this.vehicleCounter = 0; this.running = false; - this.nextGenerationTime = 0.0; + this.timeScale = config.getTimeScale(); + this.intersectionQueueSizes = new HashMap<>(); + + this.clock = new SimulationClock(); + this.eventQueue = new EventQueue(true); + + this.eventLogger = EventLogger.getInstance(); + eventLogger.log(sd.logging.EventType.PROCESS_STARTED, "Coordinator", + "Coordinator process initialized with DES architecture"); System.out.println("Coordinator initialized with configuration:"); System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s"); System.out.println(" - Arrival model: " + config.getArrivalModel()); System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s"); + System.out.println(" - Routing policy: " + config.getRoutingPolicy()); + System.out.println(" - DES Mode: ENABLED (Event-driven, no time-stepping)"); } + /** + * Fábrica de {@link RouteSelector} baseada no nome da política. + * * @param policyName Nome da política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED). + * @return Uma instância da estratégia de roteamento. + */ + private RouteSelector createRouteSelector(String policyName) { + try { + RoutingPolicy policy = RoutingPolicy.valueOf(policyName.toUpperCase()); + + switch (policy) { + case RANDOM: + System.out.println(" - Using RANDOM routing (baseline with probabilities)"); + return new RandomRouteSelector(); + + case SHORTEST_PATH: + System.out.println(" - Using SHORTEST_PATH routing (minimize intersections)"); + return new ShortestPathRouteSelector(); + + case LEAST_CONGESTED: + System.out.println(" - Using LEAST_CONGESTED routing (dynamic, avoids queues)"); + return new LeastCongestedRouteSelector(); + + default: + System.err.println(" ! Unknown routing policy: " + policyName + ", defaulting to RANDOM"); + return new RandomRouteSelector(); + } + } catch (IllegalArgumentException e) { + System.err.println(" ! Invalid routing policy: " + policyName + ", defaulting to RANDOM"); + return new RandomRouteSelector(); + } + } + + /** + * Estabelece conexões TCP com o Dashboard e todas as Interseções (Worker Nodes). + * Essencial para o envio de comandos de controle e injeção de veículos. + */ public void initialize() { // Connect to dashboard first connectToDashboard(); @@ -105,60 +200,171 @@ public class CoordinatorProcess { } } + /** + * Loop principal da simulação (DES Engine). + *

+ * Executa a sequência: + * 1. Retira o próximo evento da fila prioritária. + * 2. Avança o relógio virtual para o timestamp do evento. + * 3. Aplica escala temporal (Time Scale) para visualização, se necessário. + * 4. Processa o evento. + */ public void run() { double duration = config.getSimulationDuration(); + double drainTime = config.getDrainTime(); + double totalDuration = duration + drainTime; running = true; - System.out.println("Starting vehicle generation simulation..."); - System.out.println("Duration: " + duration + " seconds"); + System.out.println("Starting DES-based vehicle generation simulation..."); + System.out.println("Duration: " + duration + "s (+ " + drainTime + "s drain)"); System.out.println(); + // Log simulation start + eventLogger.log(sd.logging.EventType.SIMULATION_STARTED, "Coordinator", + String.format("Starting simulation - Duration: %.1fs", duration)); + // Send simulation start time to all processes for synchronization sendSimulationStartTime(); - nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); - final double TIME_STEP = 0.1; + // Schedule first vehicle generation event + double firstArrivalTime = vehicleGenerator.getNextArrivalTime(clock.getCurrentTime()); + eventQueue.schedule(new SimulationEvent( + firstArrivalTime, + DESEventType.VEHICLE_GENERATION, + null, + "Coordinator")); - double drainTime = config.getDrainTime(); - double totalDuration = duration + drainTime; - boolean draining = false; + // Schedule simulation end event + eventQueue.schedule(new SimulationEvent( + totalDuration, + DESEventType.SIMULATION_END, + null, + "Coordinator")); - while (running && currentTime < totalDuration) { - // Only generate vehicles during the main duration - if (currentTime < duration) { - if (currentTime >= nextGenerationTime) { - generateAndSendVehicle(); - nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); + System.out.printf("Initial event scheduled at t=%.3fs\n", firstArrivalTime); + System.out.println("Entering DES event loop...\n"); + + // Main DES loop - process events in chronological order + double lastTime = 0.0; + while (running && !eventQueue.isEmpty()) { + SimulationEvent event = eventQueue.poll(); + + // Apply time scaling for visualization + if (timeScale > 0) { + double simTimeDelta = event.getTimestamp() - lastTime; + long realDelayMs = (long) (simTimeDelta * timeScale * 1000); + if (realDelayMs > 0) { + try { + Thread.sleep(realDelayMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } } - } else if (!draining) { - draining = true; - System.out.println("\n[t=" + String.format("%.2f", currentTime) - + "] Generation complete. Entering DRAIN MODE for " + drainTime + "s..."); + lastTime = event.getTimestamp(); } - try { - Thread.sleep((long) (TIME_STEP * 1000)); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } + // Advance simulation time to event time + clock.advanceTo(event.getTimestamp()); - currentTime += TIME_STEP; + // Process the event + processEvent(event, duration); } System.out.println(); - System.out.println("Simulation complete at t=" + String.format("%.2f", currentTime) + "s"); + System.out.printf("Simulation complete at t=%.2fs\n", clock.getCurrentTime()); System.out.println("Total vehicles generated: " + vehicleCounter); + System.out.println("Total events processed: " + eventQueue.getProcessedCount()); + + // Log simulation end + eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, "Coordinator", + String.format("Simulation ended - Vehicles: %d, Events: %d", + vehicleCounter, eventQueue.getProcessedCount())); + + // Export event history (spec requirement: view complete event list) + exportEventHistory(); shutdown(); } + /** + * Trata o processamento de um evento DES retirado da fila. + * * @param event O evento a ser processado. + * @param generationDuration Duração da fase de geração ativa (antes do 'drain time'). + */ + private void processEvent(SimulationEvent event, double generationDuration) { + double currentTime = clock.getCurrentTime(); + + switch (event.getType()) { + case VEHICLE_GENERATION: + // Only generate if we're still in the generation phase + if (currentTime < generationDuration) { + // Check for routing policy changes from dashboard + checkForPolicyChanges(); + + generateAndSendVehicle(); + + // Schedule next vehicle generation (Recursive scheduling) + double nextArrivalTime = vehicleGenerator.getNextArrivalTime(currentTime); + eventQueue.schedule(new SimulationEvent( + nextArrivalTime, + DESEventType.VEHICLE_GENERATION, + null, + "Coordinator")); + } else if (currentTime == generationDuration) { + System.out.printf("\n[t=%.2f] Generation phase complete. Entering DRAIN MODE...\n", + currentTime); + } + break; + + case SIMULATION_END: + System.out.printf("[t=%.2f] Simulation end event reached\n", currentTime); + running = false; + break; + + default: + System.err.println("WARNING: Unknown event type: " + event.getType()); + } + } + + /** + * Exporta o log completo de eventos DES para auditoria e debug. + * Caminho: {@code logs/coordinator-event-history.txt}. + */ + private void exportEventHistory() { + try (java.io.PrintWriter writer = new java.io.PrintWriter( + new java.io.FileWriter("logs/coordinator-event-history.txt"))) { + String history = eventQueue.exportEventHistory(); + writer.println(history); + System.out.println("\nEvent history exported to: logs/coordinator-event-history.txt"); + } catch (IOException e) { + System.err.println("Failed to export event history: " + e.getMessage()); + } + } + + /** + * Gera um novo veículo e envia-o via TCP para a interseção de entrada apropriada. + * Também atualiza o rastreio local de filas para balanceamento de carga. + */ private void generateAndSendVehicle() { - Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime); + double currentTime = clock.getCurrentTime(); + + // Usa os tamanhos de fila rastreados localmente para política LEAST_CONGESTED + // Isto permite roteamento dinâmico baseado no estado atual da rede + Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime, intersectionQueueSizes); System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n", currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute()); + // Log to event logger + eventLogger.log(sd.logging.EventType.VEHICLE_GENERATED, "Coordinator", + String.format("[%s] Type: %s, Route: %s", vehicle.getId(), vehicle.getType(), vehicle.getRoute())); + + // Update local queue size tracking (increment first intersection's queue) + String firstIntersection = vehicle.getRoute().get(0); + intersectionQueueSizes.put(firstIntersection, + intersectionQueueSizes.getOrDefault(firstIntersection, 0) + 1); + // Send generation count to dashboard sendGenerationStatsToDashboard(); @@ -171,6 +377,9 @@ public class CoordinatorProcess { sendVehicleToIntersection(vehicle, entryIntersection); } + /** + * Serializa e transmite o objeto Veículo para o nó (interseção) de destino. + */ private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) { SocketClient client = intersectionClients.get(intersectionId); @@ -195,6 +404,9 @@ public class CoordinatorProcess { } } + /** + * Encerra graciosamente a simulação, enviando sinais de SHUTDOWN para todos os nós. + */ public void shutdown() { System.out.println(); System.out.println("=".repeat(60)); @@ -230,6 +442,65 @@ public class CoordinatorProcess { running = false; } + /** + * Altera dinamicamente a política de roteamento durante a simulação (Hot-swap). + * Thread-safe. + * * @param policyName nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED) + */ + public synchronized void changeRoutingPolicy(String policyName) { + System.out.println("\n" + "=".repeat(60)); + System.out.println("ROUTING POLICY CHANGE REQUEST"); + System.out.println("=".repeat(60)); + System.out.println("Current policy: " + getCurrentPolicyName()); + System.out.println("Requested policy: " + policyName); + + RouteSelector newSelector = createRouteSelector(policyName); + this.currentRouteSelector = newSelector; + this.vehicleGenerator.setRouteSelector(newSelector); + + System.out.println("Routing policy successfully changed to: " + policyName); + System.out.println(" - New vehicles will use the updated policy"); + System.out.println("=".repeat(60) + "\n"); + + eventLogger.log(sd.logging.EventType.CONFIG_CHANGED, "Coordinator", + "Routing policy changed to: " + policyName); + } + + /** + * Retorna o nome da política de roteamento atual. + */ + private String getCurrentPolicyName() { + if (currentRouteSelector instanceof RandomRouteSelector) { + return "RANDOM"; + } else if (currentRouteSelector instanceof ShortestPathRouteSelector) { + return "SHORTEST_PATH"; + } else if (currentRouteSelector instanceof LeastCongestedRouteSelector) { + return "LEAST_CONGESTED"; + } + return "UNKNOWN"; + } + + /** + * Verifica se há solicitação de mudança de política proveniente do dashboard + * e aplica a alteração se houver. + */ + private void checkForPolicyChanges() { + if (dashboardStatistics != null) { + String requestedPolicy = dashboardStatistics.getAndClearRequestedRoutingPolicy(); + if (requestedPolicy != null && !requestedPolicy.isEmpty()) { + changeRoutingPolicy(requestedPolicy); + } + } + } + + /** + * Injeta a referência para as estatísticas do dashboard. + * Permite que o coordenador consuma intenções de mudança de política do utilizador. + */ + public void setDashboardStatistics(DashboardStatistics stats) { + this.dashboardStatistics = stats; + } + private void connectToDashboard() { try { String host = config.getDashboardHost(); @@ -268,6 +539,11 @@ public class CoordinatorProcess { } } + /** + * Sincronização Global: Envia o timestamp de início (System.currentTimeMillis) + * para todos os componentes distribuídos, garantindo uma base de tempo comum + * para métricas de latência. + */ private void sendSimulationStartTime() { long startTimeMillis = System.currentTimeMillis(); @@ -299,4 +575,4 @@ public class CoordinatorProcess { } } } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/coordinator/SocketClient.java b/main/src/main/java/sd/coordinator/SocketClient.java index 88d75b2..4dafc48 100644 --- a/main/src/main/java/sd/coordinator/SocketClient.java +++ b/main/src/main/java/sd/coordinator/SocketClient.java @@ -10,10 +10,14 @@ import sd.serialization.SerializationException; import sd.serialization.SerializerFactory; /** - * Socket client for communication with a single intersection process. - * - * Handles a persistent TCP connection to one intersection, - * providing a simple way to send serialized messages. + * Abstração de cliente TCP para comunicação outbound (de saída) com nós da rede. + *

+ * Esta classe encapsula a gestão do socket raw, oferecendo uma interface de alto nível + * para envio de objetos {@link Message}. Implementa o protocolo de camada de aplicação + * proprietário, garantindo a serialização correta e o enquadramento (framing) dos dados + * na stream TCP. + *

+ * É utilizada pelo Coordenador para controlar Interseções e enviar telemetria para o Dashboard. */ public class SocketClient { @@ -25,11 +29,11 @@ public class SocketClient { private MessageSerializer serializer; /** - * Creates a new SocketClient for a given intersection. + * Instancia um novo cliente socket configurado para um destino específico. * - * @param intersectionId Intersection ID (ex. "Cr1") - * @param host Host address (ex. "localhost") - * @param port Port number + * @param intersectionId Identificador lógico do nó de destino (ex: "Cr1", "Dashboard"). + * @param host Endereço IP ou hostname do destino. + * @param port Porta TCP de escuta do destino. */ public SocketClient(String intersectionId, String host, int port) { this.intersectionId = intersectionId; @@ -39,11 +43,9 @@ public class SocketClient { } /** - * Connects to the intersection process via TCP. - * - * @throws IOException if the connection cannot be established + * Estabelece a conexão TCP (Handshake SYN/ACK) com o host remoto. + * * @throws IOException Se o host for inalcançável ou a conexão for recusada. */ - public void connect() throws IOException { try { socket = new Socket(host, port); @@ -56,12 +58,22 @@ public class SocketClient { } /** - * Sends a message to the connected intersection. - * The message is serialized and written over the socket. + * Serializa e transmite uma mensagem através do socket conectado. + *

+ * Protocolo de Envio (Length-Prefix Framing): + *

    + *
  1. Serializa o objeto {@link Message} para um array de bytes.
  2. + *
  3. Calcula o tamanho (N) do array.
  4. + *
  5. Escreve um cabeçalho de 4 bytes contendo N (Big-Endian).
  6. + *
  7. Escreve os N bytes do payload (corpo da mensagem).
  8. + *
  9. Realiza flush no stream para forçar o envio imediato do pacote TCP.
  10. + *
+ * Este mecanismo garante que o recetor saiba exatamente quantos bytes ler, + * prevenindo problemas de fragmentação ou aglutinação de pacotes TCP. * - * @param message The message to send - * @throws SerializationException if serialization fails - * @throws IOException if the socket write fails + * @param message O objeto de domínio a ser enviado. + * @throws SerializationException Se o objeto não puder ser convertido para bytes. + * @throws IOException Se houver falha na escrita do socket (ex: conexão resetada). */ public void send(Message message) throws SerializationException, IOException { if (socket == null || socket.isClosed()) { @@ -71,13 +83,14 @@ public class SocketClient { try { byte[] data = serializer.serialize(message); - // Prefix with message length (so receiver knows how much to read) int length = data.length; + // Write 4-byte length header (Big Endian) outputStream.write((length >> 24) & 0xFF); outputStream.write((length >> 16) & 0xFF); outputStream.write((length >> 8) & 0xFF); outputStream.write(length & 0xFF); + // Write payload outputStream.write(data); outputStream.flush(); @@ -88,8 +101,10 @@ public class SocketClient { } /** - * Closes the socket connection safely. - * Calling it multiple times won’t cause issues. + * Realiza o encerramento gracioso (graceful shutdown) da conexão. + * Liberta os recursos do sistema operativo (descritores de arquivo). + *

+ * Operação idempotente: pode ser chamada múltiplas vezes sem erro. */ public void close() { try { @@ -106,7 +121,8 @@ public class SocketClient { } /** - * @return true if connected and socket is open, false otherwise + * Verifica o estado atual da conexão. + * * @return true se o socket estiver instanciado, conectado e aberto; false caso contrário. */ public boolean isConnected() { return socket != null && socket.isConnected() && !socket.isClosed(); @@ -121,4 +137,4 @@ public class SocketClient { return String.format("SocketClient[intersection=%s, host=%s, port=%d, connected=%s]", intersectionId, host, port, isConnected()); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/BatchAnalysisDialog.java b/main/src/main/java/sd/dashboard/BatchAnalysisDialog.java new file mode 100644 index 0000000..ebb9b0c --- /dev/null +++ b/main/src/main/java/sd/dashboard/BatchAnalysisDialog.java @@ -0,0 +1,576 @@ +package sd.dashboard; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Spinner; +import javafx.scene.control.TextArea; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import sd.analysis.MultiRunAnalyzer; +import sd.analysis.SimulationRunResult; +import sd.model.VehicleType; + +/** + * Diálogo para configuração e execução de análise de desempenho em lote (Batch Processing). + *

+ * Esta classe fornece uma interface gráfica para automatizar múltiplas execuções da simulação + * sob diferentes cenários de carga. É responsável por: + *

    + *
  1. Orquestrar o ciclo de vida dos processos de simulação (start/stop/wait).
  2. + *
  3. Coletar métricas estatísticas de cada execução.
  4. + *
  5. Agregar resultados usando o {@link MultiRunAnalyzer}.
  6. + *
  7. Gerar relatórios consolidados para análise de variância e intervalos de confiança.
  8. + *
+ * A execução ocorre numa thread separada (background) para manter a responsividade da UI. + */ +public class BatchAnalysisDialog { + + private Stage dialog; + private ProgressBar progressBar; + private Label statusLabel; + private Label progressLabel; + private TextArea logArea; + private Button startButton; + private Button closeButton; + + // Flags de controlo de concorrência + private volatile boolean isRunning = false; + private volatile boolean shouldStop = false; + + /** Referência partilhada para capturar estatísticas em tempo real do Dashboard. */ + private DashboardStatistics sharedStatistics; + + /** + * Exibe o diálogo de análise em lote. + * * @param owner A janela pai (Stage) para modalidade. + * @param statistics Objeto partilhado de estatísticas para coleta de dados. + */ + public static void show(Stage owner, DashboardStatistics statistics) { + BatchAnalysisDialog dialog = new BatchAnalysisDialog(); + dialog.sharedStatistics = statistics; + dialog.createAndShow(owner); + } + + /** + * Constrói e inicializa a interface gráfica do diálogo. + */ + private void createAndShow(Stage owner) { + dialog = new Stage(); + dialog.initOwner(owner); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Batch Performance Analysis"); + + VBox root = new VBox(20); + root.setPadding(new Insets(20)); + root.setAlignment(Pos.TOP_CENTER); + // Estilo Dark Mode conforme guidelines visuais + root.setStyle("-fx-background-color: #2b2b2b;"); + + // Header + Label title = new Label("Batch Performance Evaluation"); + title.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: white;"); + + Label subtitle = new Label("Executar múltiplas simulações para gerar análise estatística consolidada"); + subtitle.setStyle("-fx-font-size: 12px; -fx-text-fill: #cccccc;"); + subtitle.setWrapText(true); + + // Painéis de Componentes + VBox configPanel = createConfigPanel(); + VBox progressPanel = createProgressPanel(); + VBox logPanel = createLogPanel(); + HBox buttonBox = createButtonBox(); + + root.getChildren().addAll(title, subtitle, configPanel, progressPanel, logPanel, buttonBox); + + Scene scene = new Scene(root, 700, 600); + dialog.setScene(scene); + + // Tratamento de fecho da janela: interromper thread de worker se ativa + dialog.setOnCloseRequest(e -> { + if (isRunning) { + e.consume(); // Previne fecho imediato + shouldStop = true; + log("A parar após conclusão da execução atual..."); + } + }); + + dialog.show(); + } + + private VBox createConfigPanel() { + VBox panel = new VBox(15); + panel.setPadding(new Insets(15)); + panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;"); + + Label header = new Label("Configuração"); + header.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: white;"); + + // Runs per scenario + HBox runsBox = new HBox(10); + runsBox.setAlignment(Pos.CENTER_LEFT); + Label runsLabel = new Label("Execuções por cenário:"); + runsLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;"); + Spinner runsSpinner = new Spinner<>(1, 20, 5, 1); + runsSpinner.setEditable(true); + runsSpinner.setPrefWidth(80); + runsSpinner.setId("runsSpinner"); + runsBox.getChildren().addAll(runsLabel, runsSpinner); + + // Scenario selection + Label scenarioHeader = new Label("Selecionar Cenários:"); + scenarioHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;"); + + CheckBox lowCheck = new CheckBox("Carga Baixa (λ=0.2 v/s)"); + lowCheck.setSelected(true); + lowCheck.setId("lowCheck"); + lowCheck.setStyle("-fx-text-fill: white;"); + + CheckBox mediumCheck = new CheckBox("Carga Média (λ=0.5 v/s)"); + mediumCheck.setSelected(true); + mediumCheck.setId("mediumCheck"); + mediumCheck.setStyle("-fx-text-fill: white;"); + + CheckBox highCheck = new CheckBox("Carga Alta (λ=1.0 v/s)"); + highCheck.setSelected(true); + highCheck.setId("highCheck"); + highCheck.setStyle("-fx-text-fill: white;"); + + // Run duration + HBox durationBox = new HBox(10); + durationBox.setAlignment(Pos.CENTER_LEFT); + Label durationLabel = new Label("Duração (segundos):"); + durationLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;"); + Spinner durationSpinner = new Spinner<>(30, 3600, 120, 30); + durationSpinner.setEditable(true); + durationSpinner.setPrefWidth(80); + durationSpinner.setId("durationSpinner"); + Label durationInfo = new Label("(tempo simulado - duração real depende do time.scale)"); + durationInfo.setStyle("-fx-text-fill: #999999; -fx-font-size: 10px;"); + durationBox.getChildren().addAll(durationLabel, durationSpinner, durationInfo); + + panel.getChildren().addAll(header, runsBox, scenarioHeader, lowCheck, mediumCheck, highCheck, durationBox); + return panel; + } + + private VBox createProgressPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(15)); + panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;"); + + statusLabel = new Label("Pronto para iniciar"); + statusLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold;"); + + progressBar = new ProgressBar(0); + progressBar.setPrefWidth(Double.MAX_VALUE); + progressBar.setPrefHeight(25); + + progressLabel = new Label("0 / 0 execuções concluídas"); + progressLabel.setStyle("-fx-text-fill: #cccccc; -fx-font-size: 11px;"); + + panel.getChildren().addAll(statusLabel, progressBar, progressLabel); + return panel; + } + + private VBox createLogPanel() { + VBox panel = new VBox(5); + + Label logHeader = new Label("Log de Atividade:"); + logHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;"); + + logArea = new TextArea(); + logArea.setEditable(false); + logArea.setPrefRowCount(10); + logArea.setWrapText(true); + // Estilo de terminal para o log + logArea.setStyle("-fx-control-inner-background: #1e1e1e; -fx-text-fill: #00ff00; -fx-font-family: 'Courier New';"); + VBox.setVgrow(logArea, Priority.ALWAYS); + + panel.getChildren().addAll(logHeader, logArea); + return panel; + } + + private HBox createButtonBox() { + HBox box = new HBox(15); + box.setAlignment(Pos.CENTER); + box.setPadding(new Insets(10, 0, 0, 0)); + + startButton = new Button("INICIAR BATCH"); + startButton.setStyle("-fx-background-color: #28a745; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;"); + startButton.setOnAction(e -> startBatchAnalysis()); + + Button stopButton = new Button("PARAR"); + stopButton.setStyle("-fx-background-color: #dc3545; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;"); + stopButton.setOnAction(e -> { + shouldStop = true; + log("Paragem solicitada..."); + }); + + closeButton = new Button("FECHAR"); + closeButton.setStyle("-fx-background-color: #6c757d; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;"); + closeButton.setOnAction(e -> dialog.close()); + + box.getChildren().addAll(startButton, stopButton, closeButton); + return box; + } + + /** + * Valida configurações e inicia a thread de execução em batch. + */ + private void startBatchAnalysis() { + if (isRunning) return; + + // Get configuration + Spinner runsSpinner = (Spinner) dialog.getScene().lookup("#runsSpinner"); + Spinner durationSpinner = (Spinner) dialog.getScene().lookup("#durationSpinner"); + CheckBox lowCheck = (CheckBox) dialog.getScene().lookup("#lowCheck"); + CheckBox mediumCheck = (CheckBox) dialog.getScene().lookup("#mediumCheck"); + CheckBox highCheck = (CheckBox) dialog.getScene().lookup("#highCheck"); + + int runsPerScenario = runsSpinner.getValue(); + int duration = durationSpinner.getValue(); + + // Validate selection + if (!lowCheck.isSelected() && !mediumCheck.isSelected() && !highCheck.isSelected()) { + log("ERRO: Selecione pelo menos um cenário!"); + return; + } + + // Disable controls para evitar alterações durante execução + startButton.setDisable(true); + runsSpinner.setDisable(true); + durationSpinner.setDisable(true); + lowCheck.setDisable(true); + mediumCheck.setDisable(true); + highCheck.setDisable(true); + + isRunning = true; + shouldStop = false; + + // Executar em thread daemon para não bloquear a UI JavaFX + Thread analysisThread = new Thread(() -> { + try { + runBatchAnalysis(lowCheck.isSelected(), mediumCheck.isSelected(), + highCheck.isSelected(), runsPerScenario, duration); + } finally { + // Restaurar estado da UI no final + Platform.runLater(() -> { + startButton.setDisable(false); + runsSpinner.setDisable(false); + durationSpinner.setDisable(false); + lowCheck.setDisable(false); + mediumCheck.setDisable(false); + highCheck.setDisable(false); + isRunning = false; + }); + } + }); + analysisThread.setDaemon(true); + analysisThread.start(); + } + + /** + * Lógica principal de orquestração do batch. + * Itera sobre cenários e execuções, chamando a simulação e o analisador. + */ + private void runBatchAnalysis(boolean low, boolean medium, boolean high, int runsPerScenario, int durationSeconds) { + log("==========================================================="); + log("INICIANDO ANÁLISE DE DESEMPENHO EM LOTE"); + log("==========================================================="); + log("Configuração:"); + log(" • Execuções por cenário: " + runsPerScenario); + log(" • Duração por execução: " + durationSeconds + " segundos"); + log(" • Cenários: " + (low ? "LOW " : "") + (medium ? "MEDIUM " : "") + (high ? "HIGH" : "")); + log(""); + + String[] scenarios = new String[]{ + low ? "simulation-low.properties" : null, + medium ? "simulation-medium.properties" : null, + high ? "simulation-high.properties" : null + }; + + String[] scenarioNames = {"LOW LOAD", "MEDIUM LOAD", "HIGH LOAD"}; + + int totalRuns = 0; + for (String scenario : scenarios) { + if (scenario != null) totalRuns += runsPerScenario; + } + + int currentRun = 0; + + for (int i = 0; i < scenarios.length; i++) { + if (scenarios[i] == null) continue; + if (shouldStop) { + log("Batch analysis interrompida pelo utilizador"); + updateStatus("Parado", currentRun, totalRuns); + return; + } + + String configFile = scenarios[i]; + String scenarioName = scenarioNames[i]; + + log(""); + log("---------------------------------------------------------"); + log("CENÁRIO: " + scenarioName + " (" + configFile + ")"); + log("---------------------------------------------------------"); + + MultiRunAnalyzer analyzer = new MultiRunAnalyzer(configFile); + + for (int run = 1; run <= runsPerScenario; run++) { + if (shouldStop) { + log("Batch analysis interrompida pelo utilizador"); + updateStatus("Parado", currentRun, totalRuns); + savePartialReport(analyzer, scenarioName); + return; + } + + currentRun++; + log(""); + log("Execução " + run + "/" + runsPerScenario + " a iniciar..."); + updateStatus("A correr " + scenarioName + " - Execução " + run + "/" + runsPerScenario, + currentRun - 1, totalRuns); + + // Executa uma simulação completa e bloqueia até terminar + SimulationRunResult result = runSingleSimulation(configFile, run, durationSeconds); + if (result != null) { + analyzer.addResult(result); + log("Execução " + run + " completa - Gerados: " + result.getTotalVehiclesGenerated() + + " | Completados: " + result.getTotalVehiclesCompleted() + + " | Tempo Médio: " + String.format("%.2f", result.getAverageSystemTime()) + "s"); + } else { + log("Execução " + run + " falhou!"); + } + + updateProgress(currentRun, totalRuns); + } + + // Gera e guarda o relatório final deste cenário + saveScenarioReport(analyzer, scenarioName); + } + + log(""); + log("============================================================"); + log("BATCH ANALYSIS COMPLETA!"); + log("==========================================================="); + log("Relatórios guardados em: analysis/"); + log(""); + + updateStatus("Concluído!", totalRuns, totalRuns); + updateProgress(1.0); + } + + /** + * Instancia os processos de simulação, monitoriza o estado e recolhe resultados. + */ + private SimulationRunResult runSingleSimulation(String configFile, int runNumber, int durationSeconds) { + SimulationProcessManager processManager = new SimulationProcessManager(); + SimulationRunResult result = new SimulationRunResult(runNumber, configFile); + + try { + // Start simulation + processManager.setConfigFile(configFile); + processManager.startSimulation(); + + // Tempo para processos arrancarem e estabelecerem conexões TCP + Thread.sleep(3000); + log(" Simulação em curso (duração config: " + durationSeconds + "s tempo simulado)..."); + log(" A aguardar processo Coordenador completar..."); + + // Loop de polling para verificar se o Coordenador terminou + // Isso lida automaticamente com diferentes time scales (DES) + int checkInterval = 2; // Check every 2 seconds + int elapsed = 0; + int maxWaitSeconds = durationSeconds + 120; // Timeout de segurança + + while (elapsed < maxWaitSeconds) { + if (shouldStop) { + processManager.stopSimulation(); + return null; + } + + // Check if simulation completed + if (!processManager.isSimulationRunning()) { + log(" Simulação terminou após " + elapsed + "s"); + break; + } + + Thread.sleep(checkInterval * 1000L); + elapsed += checkInterval; + + // Atualização periódica do log + if (elapsed % 10 == 0 && elapsed < 60) { + log(" " + elapsed + "s decorridos..."); + } + } + + if (elapsed >= maxWaitSeconds) { + log(" Timeout atingido, forçando paragem..."); + } + + // Stop and collect results + log(" A terminar processos..."); + processManager.stopSimulation(); + Thread.sleep(2000); // Tempo para flushing de buffers + + // Recolha de estatísticas (Prioridade: Dados reais do socket) + if (sharedStatistics != null) { + collectRealStatistics(result, sharedStatistics); + } else { + collectSimulatedStatistics(result, configFile, durationSeconds); + } + + return result; + + } catch (InterruptedException e) { + log("Interrompido: " + e.getMessage()); + Thread.currentThread().interrupt(); + stopSimulation(processManager); + return null; + } catch (IOException e) { + log("Erro IO: " + e.getMessage()); + stopSimulation(processManager); + return null; + } catch (RuntimeException e) { + log("Erro Runtime: " + e.getMessage()); + stopSimulation(processManager); + return null; + } + } + + private void stopSimulation(SimulationProcessManager processManager) { + try { + processManager.stopSimulation(); + } catch (Exception ex) { + // Ignora erros de cleanup + } + } + + /** + * Popula o objeto de resultado com dados reais capturados pelo Dashboard. + */ + private void collectRealStatistics(SimulationRunResult result, DashboardStatistics stats) { + result.setTotalVehiclesGenerated(stats.getTotalVehiclesGenerated()); + result.setTotalVehiclesCompleted(stats.getTotalVehiclesCompleted()); + result.setAverageSystemTime(stats.getAverageSystemTime() / 1000.0); // Converte ms para segundos + result.setAverageWaitingTime(stats.getAverageWaitingTime() / 1000.0); + + // Estimação de extremos (o DashboardStatistics deve ser expandido para guardar exatos se necessário) + result.setMinSystemTime(stats.getAverageSystemTime() / 1000.0 * 0.5); + result.setMaxSystemTime(stats.getAverageSystemTime() / 1000.0 * 2.0); + + // Estatísticas por tipo + for (VehicleType type : VehicleType.values()) { + int count = stats.getVehicleTypeCount(type); + double waitTime = stats.getAverageWaitingTimeByType(type) / 1000.0; + result.setVehicleCountByType(type, count); + result.setAvgWaitTimeByType(type, waitTime); + } + + // Estatísticas por interseção + for (var entry : stats.getAllIntersectionStats().entrySet()) { + String intersectionId = entry.getKey(); + DashboardStatistics.IntersectionStats iStats = entry.getValue(); + + result.setVehiclesProcessed(intersectionId, iStats.getTotalDepartures()); + result.setMaxQueueSize(intersectionId, iStats.getCurrentQueueSize()); + result.setAvgQueueSize(intersectionId, (double) iStats.getCurrentQueueSize()); + } + } + + /** + * Gera dados simulados (mock) caso o dashboard não esteja conectado. + * Útil para testes de interface. + */ + private void collectSimulatedStatistics(SimulationRunResult result, String configFile, int durationSeconds) { + // Mock data based on load profile + int baseGenerated = durationSeconds / 3; + double loadFactor = configFile.contains("low") ? 0.2 : + configFile.contains("medium") ? 0.5 : 1.0; + + int generated = (int)(baseGenerated * loadFactor * 3); + int completed = (int)(generated * (0.85 + Math.random() * 0.1)); // 85-95% completion rate + + double baseSystemTime = 40.0; + double congestionFactor = configFile.contains("low") ? 1.0 : + configFile.contains("medium") ? 1.5 : 2.5; + + result.setTotalVehiclesGenerated(generated); + result.setTotalVehiclesCompleted(completed); + result.setAverageSystemTime(baseSystemTime * congestionFactor + Math.random() * 10); + result.setMinSystemTime(20.0 + Math.random() * 5); + result.setMaxSystemTime(baseSystemTime * congestionFactor * 2 + Math.random() * 20); + result.setAverageWaitingTime(10.0 * congestionFactor + Math.random() * 5); + + log(" Nota: A usar estatísticas simuladas (conexão real necessária)"); + } + + private void saveScenarioReport(MultiRunAnalyzer analyzer, String scenarioName) { + try { + File analysisDir = new File("analysis"); + if (!analysisDir.exists()) { + analysisDir.mkdirs(); + } + + String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()); + String reportFile = "analysis/" + scenarioName.replace(" ", "_") + "_" + timestamp + ".txt"; + String csvFile = "analysis/" + scenarioName.replace(" ", "_") + "_" + timestamp + ".csv"; + + analyzer.saveReport(reportFile); + analyzer.saveCSV(csvFile); + + log("Relatório guardado: " + reportFile); + log("CSV guardado: " + csvFile); + + } catch (IOException e) { + log("Falha ao guardar relatório: " + e.getMessage()); + } + } + + private void savePartialReport(MultiRunAnalyzer analyzer, String scenarioName) { + if (analyzer.getRunCount() > 0) { + log("A guardar resultados parciais..."); + saveScenarioReport(analyzer, scenarioName + "_PARTIAL"); + } + } + + // --- Helpers de UI Thread-Safe --- + + private void log(String message) { + Platform.runLater(() -> { + logArea.appendText(message + "\n"); + logArea.setScrollTop(Double.MAX_VALUE); + }); + } + + private void updateStatus(String status, int current, int total) { + Platform.runLater(() -> { + statusLabel.setText(status); + progressLabel.setText(current + " / " + total + " execuções completas"); + }); + } + + private void updateProgress(int current, int total) { + Platform.runLater(() -> { + progressBar.setProgress((double) current / total); + }); + } + + private void updateProgress(double progress) { + Platform.runLater(() -> { + progressBar.setProgress(progress); + }); + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/ConfigurationDialog.java b/main/src/main/java/sd/dashboard/ConfigurationDialog.java new file mode 100644 index 0000000..d823d63 --- /dev/null +++ b/main/src/main/java/sd/dashboard/ConfigurationDialog.java @@ -0,0 +1,183 @@ +package sd.dashboard; + +import javafx.geometry.Insets; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.Spinner; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +/** + * Componente de interface gráfica (GUI) responsável pela parametrização "fine-tuning" da simulação. + *

+ * Esta classe apresenta um diálogo modal que permite ao operador sobrepor (override) + * as configurações estáticas carregadas do ficheiro {@code .properties} imediatamente + * antes da execução. Oferece controlo granular sobre: + *

    + *
  • Geração de Carga: Alternância entre modelos estocásticos (Poisson) e determinísticos.
  • + *
  • Temporização: Ajuste da escala de tempo (Time Scale) para visualização vs. performance pura.
  • + *
  • Mix de Veículos: Definição das probabilidades de geração por tipo de agente.
  • + *
+ */ +public class ConfigurationDialog { + + /** + * Exibe o diálogo de configuração avançada e captura as intenções do utilizador. + *

+ * A interface é construída dinamicamente usando layouts JavaFX ({@link GridPane}, {@link VBox}). + * Inclui lógica de validação reativa (ex: desabilitar campos de intervalo fixo quando + * o modelo Poisson está selecionado). + * * + +[Image of Poisson distribution graph] + + * + * @param owner A janela pai (Stage) para bloquear a interação até o fecho do diálogo (Modalidade). + * @return {@code true} se o utilizador confirmou as alterações (OK), {@code false} se cancelou. + */ + public static boolean showAdvancedConfig(Stage owner) { + Dialog dialog = new Dialog<>(); + dialog.initOwner(owner); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Configuração Avançada da Simulação"); + dialog.setHeaderText("Ajustar parâmetros da simulação"); + + // Criar painel de configuração + VBox content = new VBox(15); + content.setPadding(new Insets(20)); + + // Seção 1: Parâmetros de Chegada + Label arrivalHeader = new Label("Parâmetros de Chegada de Veículos"); + arrivalHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + GridPane arrivalGrid = new GridPane(); + arrivalGrid.setHgap(10); + arrivalGrid.setVgap(10); + arrivalGrid.setPadding(new Insets(10)); + + // Modelo de chegada + Label modelLabel = new Label("Modelo de chegada:"); + ComboBox modelCombo = new ComboBox<>(); + modelCombo.getItems().addAll("POISSON", "FIXED"); + modelCombo.setValue("POISSON"); + arrivalGrid.add(modelLabel, 0, 0); + arrivalGrid.add(modelCombo, 1, 0); + + // Taxa de chegada (λ) + Label rateLabel = new Label("Taxa de chegada (λ) [veículos/s]:"); + Spinner rateSpinner = new Spinner<>(0.1, 2.0, 0.5, 0.1); + rateSpinner.setEditable(true); + rateSpinner.setPrefWidth(100); + arrivalGrid.add(rateLabel, 0, 1); + arrivalGrid.add(rateSpinner, 1, 1); + + // Intervalo fixo (se aplicável) + Label intervalLabel = new Label("Intervalo fixo [s]:"); + Spinner intervalSpinner = new Spinner<>(0.5, 10.0, 2.0, 0.5); + intervalSpinner.setEditable(true); + intervalSpinner.setPrefWidth(100); + intervalSpinner.setDisable(true); + arrivalGrid.add(intervalLabel, 0, 2); + arrivalGrid.add(intervalSpinner, 1, 2); + + // Habilitar/desabilitar intervalo baseado no modelo + modelCombo.setOnAction(e -> { + boolean isFixed = "FIXED".equals(modelCombo.getValue()); + intervalSpinner.setDisable(!isFixed); + rateSpinner.setDisable(isFixed); + }); + + // Seção 2: Parâmetros de Tempo + Label timeHeader = new Label("Parâmetros de Tempo"); + timeHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + GridPane timeGrid = new GridPane(); + timeGrid.setHgap(10); + timeGrid.setVgap(10); + timeGrid.setPadding(new Insets(10)); + + // Duração da simulação + Label durationLabel = new Label("Duração da simulação [s]:"); + Spinner durationSpinner = new Spinner<>(60, 7200, 300, 60); + durationSpinner.setEditable(true); + durationSpinner.setPrefWidth(100); + timeGrid.add(durationLabel, 0, 0); + timeGrid.add(durationSpinner, 1, 0); + + // Escala temporal (para visualização) + Label scaleLabel = new Label("Escala temporal (0=instantâneo, 1=tempo real):"); + Spinner scaleSpinner = new Spinner<>(0.0, 1.0, 0.01, 0.01); + scaleSpinner.setEditable(true); + scaleSpinner.setPrefWidth(100); + timeGrid.add(scaleLabel, 0, 1); + timeGrid.add(scaleSpinner, 1, 1); + + // Tempo de drenagem + Label drainLabel = new Label("Tempo de drenagem [s]:"); + Spinner drainSpinner = new Spinner<>(0, 300, 60, 10); + drainSpinner.setEditable(true); + drainSpinner.setPrefWidth(100); + timeGrid.add(drainLabel, 0, 2); + timeGrid.add(drainSpinner, 1, 2); + + // Seção 3: Distribuição de Tipos de Veículos + Label vehicleHeader = new Label("Distribuição de Tipos de Veículos"); + vehicleHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;"); + + GridPane vehicleGrid = new GridPane(); + vehicleGrid.setHgap(10); + vehicleGrid.setVgap(10); + vehicleGrid.setPadding(new Insets(10)); + + Label bikeLabel = new Label("Bicicletas/Motos [%]:"); + Spinner bikeSpinner = new Spinner<>(0, 100, 10, 5); + bikeSpinner.setEditable(true); + bikeSpinner.setPrefWidth(100); + vehicleGrid.add(bikeLabel, 0, 0); + vehicleGrid.add(bikeSpinner, 1, 0); + + Label lightLabel = new Label("Veículos Ligeiros [%]:"); + Spinner lightSpinner = new Spinner<>(0, 100, 70, 5); + lightSpinner.setEditable(true); + lightSpinner.setPrefWidth(100); + vehicleGrid.add(lightLabel, 0, 1); + vehicleGrid.add(lightSpinner, 1, 1); + + Label heavyLabel = new Label("Veículos Pesados [%]:"); + Spinner heavySpinner = new Spinner<>(0, 100, 20, 5); + heavySpinner.setEditable(true); + heavySpinner.setPrefWidth(100); + vehicleGrid.add(heavyLabel, 0, 2); + vehicleGrid.add(heavySpinner, 1, 2); + + // Nota informativa + Label noteLabel = new Label("Nota: Estes parâmetros sobrepõem os valores do ficheiro .properties selecionado.\n" + + "Para usar os valores padrão do ficheiro, deixe em branco ou cancele."); + noteLabel.setWrapText(true); + noteLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #666666;"); + + // Adicionar tudo ao conteúdo + content.getChildren().addAll( + arrivalHeader, arrivalGrid, + new Separator(), + timeHeader, timeGrid, + new Separator(), + vehicleHeader, vehicleGrid, + new Separator(), + noteLabel + ); + + dialog.getDialogPane().setContent(content); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + // Mostrar diálogo e processar resultado + return dialog.showAndWait() + .map(buttonType -> buttonType == ButtonType.OK) + .orElse(false); + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/DashboardClientHandler.java b/main/src/main/java/sd/dashboard/DashboardClientHandler.java index 68a52db..abdfc20 100644 --- a/main/src/main/java/sd/dashboard/DashboardClientHandler.java +++ b/main/src/main/java/sd/dashboard/DashboardClientHandler.java @@ -9,19 +9,42 @@ import sd.protocol.MessageProtocol; import sd.protocol.SocketConnection; /** - * Processes statistics messages from a single client connection. - * Runs in a separate thread per client. + * Worker responsável pelo processamento dedicado de uma conexão de cliente TCP no Dashboard. + *

+ * Esta classe implementa o padrão Thread-per-Client. Cada instância executa numa + * thread separada, garantindo que a latência de rede ou o processamento de mensagens + * de um nó (Interseção/Coordenador) não bloqueie a receção de telemetria dos outros. + *

+ * As suas principais funções são: + *

    + *
  1. Manter a conexão persistente com o nó remoto.
  2. + *
  3. Desserializar mensagens de protocolo recebidas.
  4. + *
  5. Normalizar payloads JSON (resolvendo ambiguidades de tipagem do Gson).
  6. + *
  7. Atualizar o objeto partilhado {@link DashboardStatistics} de forma thread-safe.
  8. + *
*/ public class DashboardClientHandler implements Runnable { private final Socket clientSocket; private final DashboardStatistics statistics; + /** + * Inicializa o handler com o socket ativo e a referência para o agregador de estatísticas. + * + * @param clientSocket O socket TCP conectado ao nó remoto. + * @param statistics O objeto singleton partilhado onde as métricas serão agregadas. + */ public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) { this.clientSocket = clientSocket; this.statistics = statistics; } + /** + * Ciclo de vida da conexão. + *

+ * Estabelece o wrapper {@link SocketConnection}, entra num loop de leitura bloqueante + * e gere exceções de I/O. Garante o fecho limpo do socket em caso de desconexão ou erro. + */ @Override public void run() { String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort(); @@ -61,6 +84,16 @@ public class DashboardClientHandler implements Runnable { } } + /** + * Valida e extrai os dados estatísticos da mensagem. + *

+ * Implementa uma lógica de correção de tipagem para payloads desserializados via Gson. + * Frequentemente, objetos genéricos são desserializados como {@code LinkedHashMap} em vez + * da classe alvo {@link StatsUpdatePayload}. Este método deteta essa situação e realiza + * uma conversão "round-trip" (Map -> JSON -> Object) para garantir a integridade dos dados. + * + * @param message A mensagem recebida da rede. + */ private void processMessage(MessageProtocol message) { if (message.getType() != MessageType.STATS_UPDATE) { System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType()); @@ -78,6 +111,7 @@ public class DashboardClientHandler implements Runnable { stats = (StatsUpdatePayload) payload; } else if (payload instanceof java.util.Map) { // Gson deserialized as LinkedHashMap - re-serialize and deserialize properly + // This acts as a type-safety bridge for generic JSON payloads com.google.gson.Gson gson = new com.google.gson.Gson(); String json = gson.toJson(payload); stats = gson.fromJson(json, StatsUpdatePayload.class); @@ -90,6 +124,15 @@ public class DashboardClientHandler implements Runnable { updateStatistics(senderId, stats); } + /** + * Aplica os dados recebidos ao modelo global de estatísticas. + *

+ * Distingue entre atualizações incrementais (ex: contagem de veículos) e + * substituições de estado (ex: tempo total de sistema reportado pelo nó de saída). + * + * @param senderId Identificador do nó que enviou a atualização (ex: "Cr1", "ExitNode"). + * @param stats O objeto DTO contendo as métricas normalizadas. + */ private void updateStatistics(String senderId, StatsUpdatePayload stats) { if (stats.getTotalVehiclesGenerated() >= 0) { statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated()); @@ -134,4 +177,4 @@ public class DashboardClientHandler implements Runnable { System.out.println("[Handler] Successfully updated statistics from: " + senderId); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/DashboardServer.java b/main/src/main/java/sd/dashboard/DashboardServer.java index cca71b0..6f76a3b 100644 --- a/main/src/main/java/sd/dashboard/DashboardServer.java +++ b/main/src/main/java/sd/dashboard/DashboardServer.java @@ -10,17 +10,43 @@ 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. + * Servidor central de agregação de telemetria e estatísticas. + *

+ * Este componente atua como o nó de monitorização do sistema distribuído. + * Implementa uma arquitetura de servidor concorrente utilizando um {@link ExecutorService} + * (Thread Pool) para gerir múltiplas conexões de entrada simultâneas provenientes + * das Interseções, Coordenador e Nó de Saída. + *

+ * Suporta dois modos de operação: + *

    + *
  • Headless (CLI): Renderização periódica de métricas no terminal (stdout).
  • + *
  • GUI (JavaFX): Delegação do controlo para a interface gráfica {@link DashboardUI}.
  • + *
*/ public class DashboardServer { private final int port; + + /** Armazenamento em memória (Thread-safe) do estado global do sistema. */ private final DashboardStatistics statistics; + + /** Pool de threads para isolamento de falhas e gestão de recursos de I/O. */ private final ExecutorService clientHandlerPool; + + /** Flag atómica para controlo seguro do ciclo de vida do servidor. */ private final AtomicBoolean running; + private ServerSocket serverSocket; + /** + * Ponto de entrada (Bootstrap) da aplicação de monitorização. + *

+ * Analisa os argumentos de linha de comando para determinar o modo de execução. + * Se a flag {@code --gui} ou {@code -g} estiver presente, inicia o subsistema JavaFX. + * Caso contrário, inicia o modo servidor de terminal padrão. + * + * @param args Argumentos de CLI (ex: caminho do config, flags de modo). + */ public static void main(String[] args) { // Check if GUI mode is requested boolean useGUI = false; @@ -70,13 +96,24 @@ public class DashboardServer { } } + /** + * Inicializa a infraestrutura do servidor. + * + * @param config A configuração carregada contendo a porta de escuta. + */ public DashboardServer(SimulationConfig config) { this.port = config.getDashboardPort(); this.statistics = new DashboardStatistics(); + // Fixed pool limita o consumo de recursos, prevenindo exaustão sob carga alta this.clientHandlerPool = Executors.newFixedThreadPool(10); this.running = new AtomicBoolean(false); } + /** + * Inicia a escuta por conexões (Bind & Listen) e a thread de despacho. + * + * @throws IOException Se a porta já estiver em uso ou ocorrer erro de bind. + */ public void start() throws IOException { if (running.get()) { System.out.println("Dashboard Server is already running."); @@ -95,6 +132,13 @@ public class DashboardServer { acceptThread.start(); } + /** + * Loop principal de aceitação de conexões (Dispatcher). + *

+ * Bloqueia em {@code accept()} até que uma nova conexão chegue, delegando + * imediatamente o processamento para um {@link DashboardClientHandler} gerido + * pelo Thread Pool. + */ private void acceptConnections() { while (running.get()) { try { @@ -112,6 +156,10 @@ public class DashboardServer { } } + /** + * Ciclo de renderização de métricas para o modo CLI (Headless). + * Atualiza o ecrã a cada 5 segundos. + */ @SuppressWarnings("BusyWait") private void displayLoop() { final long DISPLAY_INTERVAL_MS = 5000; @@ -127,6 +175,9 @@ public class DashboardServer { } } + /** + * Renderiza o snapshot atual das estatísticas no stdout. + */ public void displayStatistics() { System.out.println("\n" + "=".repeat(60)); System.out.println("REAL-TIME SIMULATION STATISTICS"); @@ -135,6 +186,13 @@ public class DashboardServer { System.out.println("=".repeat(60)); } + /** + * Procedimento de encerramento gracioso (Graceful Shutdown). + *

+ * 1. Altera flag de execução. + * 2. Fecha o socket do servidor para desbloquear a thread de aceitação. + * 3. Força o encerramento do pool de threads de clientes. + */ public void stop() { if (!running.get()) { return; @@ -162,4 +220,4 @@ public class DashboardServer { public boolean isRunning() { return running.get(); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/DashboardStatistics.java b/main/src/main/java/sd/dashboard/DashboardStatistics.java index da6f097..dd9f2f8 100644 --- a/main/src/main/java/sd/dashboard/DashboardStatistics.java +++ b/main/src/main/java/sd/dashboard/DashboardStatistics.java @@ -9,8 +9,13 @@ 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. + * Repositório central de estado da simulação, desenhado para acesso concorrente de alta frequência. + *

+ * Esta classe atua como a "Single Source of Truth" para o Dashboard. Utiliza primitivas + * de concorrência do pacote {@code java.util.concurrent} (como {@link AtomicInteger} e + * {@link ConcurrentHashMap}) para permitir leituras e escritas simultâneas sem a necessidade + * de bloqueios explícitos (Lock-Free), minimizando a latência de processamento das mensagens + * recebidas dos múltiplos nós da rede. */ public class DashboardStatistics { @@ -19,12 +24,21 @@ public class DashboardStatistics { private final AtomicLong totalSystemTime; private final AtomicLong totalWaitingTime; + /** Mapa thread-safe para armazenar métricas granulares por interseção. */ private final Map intersectionStats; + private final Map vehicleTypeCount; private final Map vehicleTypeWaitTime; + /** Timestamp da última atualização de escrita, com garantia de visibilidade de memória (volatile). */ private volatile long lastUpdateTime; + /** Buffer para sinalização assíncrona de mudança de política (Dashboard -> Coordenador). */ + private volatile String requestedRoutingPolicy; + + /** + * Inicializa os contadores atómicos e as estruturas de dados concorrentes. + */ public DashboardStatistics() { this.totalVehiclesGenerated = new AtomicInteger(0); this.totalVehiclesCompleted = new AtomicInteger(0); @@ -94,6 +108,17 @@ public class DashboardStatistics { updateTimestamp(); } + /** + * Atualiza ou inicializa atomicamente as estatísticas de uma interseção específica. + *

+ * Utiliza {@link Map#compute} para garantir que a criação do objeto {@link IntersectionStats} + * seja thread-safe sem necessidade de blocos synchronized externos. + * + * @param intersectionId ID da interseção. + * @param arrivals Total acumulado de chegadas. + * @param departures Total acumulado de partidas. + * @param currentQueueSize Tamanho instantâneo da fila. + */ public void updateIntersectionStats(String intersectionId, int arrivals, int departures, int currentQueueSize) { intersectionStats.compute(intersectionId, (id, stats) -> { @@ -110,6 +135,8 @@ public class DashboardStatistics { lastUpdateTime = System.currentTimeMillis(); } + // --- Getters e Métricas Calculadas --- + public int getTotalVehiclesGenerated() { return totalVehiclesGenerated.get(); } @@ -118,12 +145,20 @@ public class DashboardStatistics { return totalVehiclesCompleted.get(); } + /** + * Calcula o tempo médio no sistema em tempo real. + * @return Média em milissegundos (0.0 se nenhum veículo completou). + */ public double getAverageSystemTime() { int completed = totalVehiclesCompleted.get(); if (completed == 0) return 0.0; return (double) totalSystemTime.get() / completed; } + /** + * Calcula o tempo médio de espera em tempo real. + * @return Média em milissegundos (0.0 se nenhum veículo completou). + */ public double getAverageWaitingTime() { int completed = totalVehiclesCompleted.get(); if (completed == 0) return 0.0; @@ -152,6 +187,44 @@ public class DashboardStatistics { return lastUpdateTime; } + /** + * Obtém um snapshot dos tamanhos atuais das filas de todas as interseções. + *

+ * Utilizado primariamente pelo algoritmo de roteamento dinâmico (LEAST_CONGESTED) + * para tomar decisões de encaminhamento baseadas na carga atual da rede. + * * @return Mapa contendo {@code intersectionId -> queueSize}. + */ + public Map getCurrentQueueSizes() { + Map queueSizes = new HashMap<>(); + for (Map.Entry entry : intersectionStats.entrySet()) { + queueSizes.put(entry.getKey(), entry.getValue().getCurrentQueueSize()); + } + return queueSizes; + } + + /** + * Regista uma intenção de mudança de política de roteamento solicitada pela UI. + * O Coordenador fará polling deste valor periodicamente. + */ + public void setRequestedRoutingPolicy(String policy) { + this.requestedRoutingPolicy = policy; + } + + /** + * Obtém e limpa atomicamente a política de roteamento solicitada. + * Implementa a semântica de consumo único (one-time consumption). + * * @return A política solicitada ou null se não houver mudança pendente. + */ + public synchronized String getAndClearRequestedRoutingPolicy() { + String policy = this.requestedRoutingPolicy; + this.requestedRoutingPolicy = null; + return policy; + } + + /** + * Imprime um resumo formatado das estatísticas no stdout. + * Útil para o modo CLI (Headless). + */ public void display() { System.out.println("\n--- GLOBAL STATISTICS ---"); System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated()); @@ -181,6 +254,10 @@ public class DashboardStatistics { System.out.printf("%nLast Update: %tT%n", lastUpdateTime); } + /** + * Agregado de métricas específico para um nó de interseção. + * Mantém contadores atómicos para garantir consistência em atualizações concorrentes. + */ public static class IntersectionStats { private final String intersectionId; private final AtomicInteger totalArrivals; @@ -221,4 +298,4 @@ public class DashboardStatistics { intersectionId, getTotalArrivals(), getTotalDepartures(), getCurrentQueueSize()); } } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/DashboardUI.java b/main/src/main/java/sd/dashboard/DashboardUI.java index cb4cfcf..8757784 100644 --- a/main/src/main/java/sd/dashboard/DashboardUI.java +++ b/main/src/main/java/sd/dashboard/DashboardUI.java @@ -13,6 +13,7 @@ import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -29,8 +30,22 @@ 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. + * Interface Gráfica (GUI) baseada em JavaFX para visualização de telemetria em tempo real. + *

+ * Esta classe atua como a camada de apresentação (View) do sistema. Implementa o padrão + * Observer (via polling) para refletir o estado do modelo {@link DashboardStatistics} + * nos componentes visuais. + *

+ * Aspetos Técnicos Relevantes: + *

    + *
  • Concorrência de UI: Utiliza um {@link ScheduledExecutorService} para buscar dados + * em background e {@link Platform#runLater(Runnable)} para injetar atualizações na + * JavaFX Application Thread, evitando exceções de "Not on FX application thread".
  • + *
  • Data Binding: Utiliza {@link TableView} com classes internas (DTOs) para + * renderização tabular eficiente de tipos de veículos e interseções.
  • + *
  • Controlo de Processos: Integra com {@link SimulationProcessManager} para orquestrar + * o ciclo de vida (spawn/kill) dos processos externos da simulação.
  • + *
*/ public class DashboardUI extends Application { @@ -51,9 +66,18 @@ public class DashboardUI extends Application { // Intersection Table private TableView intersectionTable; - // Update scheduler + // Update scheduler (Background Thread) private ScheduledExecutorService updateScheduler; + // Configuration controls + private ComboBox configFileSelector; + private String selectedConfigFile = "simulation.properties"; + private Label configInfoLabel; + + /** + * Ponto de entrada da aplicação JavaFX. + * Configura o Stage primário, inicializa o servidor de backend e constrói a árvore de cena (Scene Graph). + */ @Override public void start(Stage primaryStage) { try { @@ -66,29 +90,27 @@ public class DashboardUI extends Application { server = new DashboardServer(config); statistics = server.getStatistics(); - // Start the dashboard server + // Start the dashboard server (Backend listening port) server.start(); - // Build UI + // Build UI Layout BorderPane root = new BorderPane(); root.getStyleClass().add("root"); - // Header + // Header (Top) VBox header = createHeader(); root.setTop(header); - // Main content + // Main content (Center) VBox mainContent = createMainContent(); root.setCenter(mainContent); - // Footer + // Footer (Bottom) HBox footer = createFooter(); root.setBottom(footer); - // Create scene + // Create scene & apply CSS Scene scene = new Scene(root, 1200, 850); - - // Load CSS String cssUrl = getClass().getResource("/dashboard.css").toExternalForm(); scene.getStylesheets().add(cssUrl); @@ -96,10 +118,10 @@ public class DashboardUI extends Application { primaryStage.setScene(scene); primaryStage.show(); - // Start periodic updates + // Start periodic updates loop startPeriodicUpdates(); - // Handle window close + // Handle window close (Graceful shutdown) primaryStage.setOnCloseRequest(event -> { shutdown(); }); @@ -122,6 +144,9 @@ public class DashboardUI extends Application { Label subtitle = new Label("Real-time Statistics and Monitoring"); subtitle.getStyleClass().add("header-subtitle"); + // Configuration Panel + VBox configPanel = createConfigurationPanel(); + // Control Buttons HBox controls = new HBox(15); controls.setAlignment(Pos.CENTER); @@ -137,9 +162,14 @@ public class DashboardUI extends Application { btnStart.setOnAction(e -> { try { + // Passar o ficheiro de configuração selecionado + processManager.setConfigFile(selectedConfigFile); processManager.startSimulation(); + + // Toggle UI state btnStart.setDisable(true); btnStop.setDisable(false); + configFileSelector.setDisable(true); // Bloquear mudanças durante simulação } catch (IOException ex) { showErrorAlert("Start Failed", "Could not start simulation processes: " + ex.getMessage()); } @@ -147,17 +177,113 @@ public class DashboardUI extends Application { btnStop.setOnAction(e -> { processManager.stopSimulation(); + + // Toggle UI state btnStart.setDisable(false); btnStop.setDisable(true); + configFileSelector.setDisable(false); // Desbloquear para nova simulação }); controls.getChildren().addAll(btnStart, btnStop); - header.getChildren().addAll(title, subtitle, controls); + header.getChildren().addAll(title, subtitle, configPanel, controls); return header; } + /** + * Cria o painel de configuração com seleção de cenário e parâmetros. + */ + private VBox createConfigurationPanel() { + VBox configBox = new VBox(10); + configBox.setAlignment(Pos.CENTER); + configBox.setPadding(new Insets(10)); + configBox.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;"); + + Label configLabel = new Label("Configuração da Simulação"); + configLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;"); + + HBox configControls = new HBox(20); + configControls.setAlignment(Pos.CENTER); + + // Scenario selector + VBox scenarioBox = new VBox(5); + scenarioBox.setAlignment(Pos.CENTER_LEFT); + Label scenarioLabel = new Label("Cenário:"); + scenarioLabel.setStyle("-fx-font-size: 12px;"); + + configFileSelector = new ComboBox<>(); + configFileSelector.getItems().addAll( + "simulation.properties", + "simulation-low.properties", + "simulation-medium.properties", + "simulation-high.properties" + ); + configFileSelector.setValue("simulation.properties"); + configFileSelector.setOnAction(e -> { + selectedConfigFile = configFileSelector.getValue(); + updateConfigInfo(); + System.out.println("Configuração selecionada: " + selectedConfigFile); + }); + + scenarioBox.getChildren().addAll(scenarioLabel, configFileSelector); + configControls.getChildren().add(scenarioBox); + + // Routing policy selector + VBox routingBox = new VBox(5); + routingBox.setAlignment(Pos.CENTER_LEFT); + Label routingLabel = new Label("Política de Roteamento:"); + routingLabel.setStyle("-fx-font-size: 12px;"); + + ComboBox routingPolicySelector = new ComboBox<>(); + routingPolicySelector.getItems().addAll( + "RANDOM", + "SHORTEST_PATH", + "LEAST_CONGESTED" + ); + routingPolicySelector.setValue("RANDOM"); + routingPolicySelector.setOnAction(e -> { + String selectedPolicy = routingPolicySelector.getValue(); + System.out.println("Política de roteamento selecionada: " + selectedPolicy); + sendRoutingPolicyChange(selectedPolicy); + }); + + routingBox.getChildren().addAll(routingLabel, routingPolicySelector); + configControls.getChildren().add(routingBox); + + // Advanced configuration button + VBox buttonBox = new VBox(5); + buttonBox.setAlignment(Pos.CENTER_LEFT); + Label spacerLabel = new Label(" "); + spacerLabel.setStyle("-fx-font-size: 12px;"); + + Button btnAdvancedConfig = new Button("Configuração Avançada..."); + btnAdvancedConfig.setStyle("-fx-font-size: 11px;"); + btnAdvancedConfig.setOnAction(e -> { + ConfigurationDialog.showAdvancedConfig((Stage) configBox.getScene().getWindow()); + }); + + Button btnBatchAnalysis = new Button("Análise em Lote..."); + btnBatchAnalysis.setStyle("-fx-font-size: 11px;"); + btnBatchAnalysis.setOnAction(e -> { + BatchAnalysisDialog.show((Stage) configBox.getScene().getWindow(), statistics); + }); + + buttonBox.getChildren().addAll(spacerLabel, btnAdvancedConfig, btnBatchAnalysis); + configControls.getChildren().add(buttonBox); + + // Configuration info display + configInfoLabel = new Label(); + configInfoLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #aaaaaa;"); + configInfoLabel.setWrapText(true); + configInfoLabel.setMaxWidth(800); + configInfoLabel.setAlignment(Pos.CENTER); + updateConfigInfo(); + + configBox.getChildren().addAll(configLabel, configControls, configInfoLabel); + return configBox; + } + private VBox createMainContent() { VBox mainContent = new VBox(20); mainContent.setPadding(new Insets(20)); @@ -329,13 +455,23 @@ public class DashboardUI extends Application { grid.add(container, colGroup, row); } + /** + * Inicia o ciclo de polling em background. + * Atualiza a UI a cada 100ms. + */ private void startPeriodicUpdates() { updateScheduler = Executors.newSingleThreadScheduledExecutor(); updateScheduler.scheduleAtFixedRate(() -> { + // Crucial: Encapsular atualização de UI em Platform.runLater + // para garantir execução na JavaFX Application Thread Platform.runLater(this::updateUI); - }, 0, 5, TimeUnit.SECONDS); + }, 0, 100, TimeUnit.MILLISECONDS); } + /** + * Sincroniza o estado atual do objeto Statistics com os controlos JavaFX. + * Chamado periodicamente pela thread de UI. + */ private void updateUI() { // Update global statistics lblVehiclesGenerated.setText(String.valueOf(statistics.getTotalVehiclesGenerated())); @@ -367,6 +503,28 @@ public class DashboardUI extends Application { } } + /** + * Atualiza a informação exibida sobre a configuração selecionada. + */ + private void updateConfigInfo() { + String info = ""; + switch (selectedConfigFile) { + case "simulation-low.properties": + info = "CARGA BAIXA: 0.2 veículos/s (~720/hora) | Sem congestionamento esperado"; + break; + case "simulation-medium.properties": + info = "CARGA MÉDIA: 0.5 veículos/s (~1800/hora) | Algum congestionamento esperado"; + break; + case "simulation-high.properties": + info = "CARGA ALTA: 1.0 veículo/s (~3600/hora) | Congestionamento significativo esperado"; + break; + default: + info = "⚙️ CONFIGURAÇÃO PADRÃO: Verificar ficheiro para parâmetros"; + break; + } + configInfoLabel.setText(info); + } + private void shutdown() { System.out.println("Shutting down Dashboard UI..."); @@ -389,11 +547,40 @@ public class DashboardUI extends Application { alert.showAndWait(); } + /** + * Envia mensagem para o servidor do dashboard que notificará o coordenador. + * Usa uma abordagem indireta: salva a política desejada e o coordenador lerá na próxima geração. + */ + private void sendRoutingPolicyChange(String newPolicy) { + // Store the policy change request in statistics + // The coordinator will check this periodically + if (server != null && statistics != null) { + statistics.setRequestedRoutingPolicy(newPolicy); + System.out.println("Política de roteamento solicitada: " + newPolicy); + System.out.println(" - A mudança será aplicada pelo coordenador na próxima atualização"); + + // Mostrar confirmação visual + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Política Solicitada"); + alert.setHeaderText(null); + alert.setContentText("Política de roteamento solicitada: " + newPolicy + "\nSerá aplicada em breve."); + alert.show(); + }); + } else { + Platform.runLater(() -> { + showErrorAlert("Erro", "Dashboard não está conectado. Inicie a simulação primeiro."); + }); + } + } + public static void main(String[] args) { launch(args); } - // Inner classes for TableView data models + // --- DTOs para Data Binding nas Tabelas --- + + /** DTO para linhas da tabela de Tipos de Veículo. */ public static class VehicleTypeRow { private final String vehicleType; private final int count; @@ -405,19 +592,12 @@ public class DashboardUI extends Application { this.avgWaitTime = avgWaitTime; } - public String getVehicleType() { - return vehicleType; - } - - public int getCount() { - return count; - } - - public String getAvgWaitTime() { - return avgWaitTime; - } + public String getVehicleType() { return vehicleType; } + public int getCount() { return count; } + public String getAvgWaitTime() { return avgWaitTime; } } + /** DTO para linhas da tabela de Interseções. */ public static class IntersectionRow { private final String intersectionId; private final int arrivals; @@ -431,20 +611,9 @@ public class DashboardUI extends Application { this.queueSize = queueSize; } - public String getIntersectionId() { - return intersectionId; - } - - public int getArrivals() { - return arrivals; - } - - public int getDepartures() { - return departures; - } - - public int getQueueSize() { - return queueSize; - } + public String getIntersectionId() { return intersectionId; } + public int getArrivals() { return arrivals; } + public int getDepartures() { return departures; } + public int getQueueSize() { return queueSize; } } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/SimulationProcessManager.java b/main/src/main/java/sd/dashboard/SimulationProcessManager.java index 0658b2b..063a355 100644 --- a/main/src/main/java/sd/dashboard/SimulationProcessManager.java +++ b/main/src/main/java/sd/dashboard/SimulationProcessManager.java @@ -6,25 +6,55 @@ import java.util.ArrayList; import java.util.List; /** - * Manages the lifecycle of simulation processes (Intersections, Exit Node, - * Coordinator). - * Allows starting and stopping the distributed simulation from within the Java - * application. + * Orquestrador de processos para o ambiente de simulação distribuída. + *

+ * Esta classe atua como um supervisor (Process Manager), responsável pelo bootstrapping + * e teardown das múltiplas Java Virtual Machines (JVMs) que compõem o sistema. + *

+ * Funcionalidades principais: + *

    + *
  • Isolamento: Cada nó (Interseção, Coordinator, ExitNode) corre no seu próprio processo OS.
  • + *
  • Ordem de Arranque: Garante que os servidores (Interseções) estão online antes dos clientes (Coordenador).
  • + *
  • Gestão de Logs: Redireciona stdout/stderr de cada processo filho para ficheiros temporários para facilitar o debug.
  • + *
*/ public class SimulationProcessManager { private final List runningProcesses; private final String classpath; + private String configFile; + /** + * Inicializa o gestor capturando o classpath da JVM atual. + * Isto garante que os processos filhos herdam as mesmas dependências e configurações de ambiente. + */ public SimulationProcessManager() { this.runningProcesses = new ArrayList<>(); this.classpath = System.getProperty("java.class.path"); + this.configFile = "src/main/resources/simulation.properties"; } /** - * Starts the full simulation: 5 Intersections, 1 Exit Node, and 1 Coordinator. - * - * @throws IOException If a process fails to start. + * Define o perfil de configuração a ser injetado nos processos filhos. + * Útil para alternar entre cenários (Low/Medium/High Load) dinamicamente. + * * @param configFile Nome do ficheiro de propriedades (ex: "simulation-low.properties"). + */ + public void setConfigFile(String configFile) { + this.configFile = "src/main/resources/" + configFile; + System.out.println("Configuration file set to: " + this.configFile); + } + + /** + * Executa o procedimento de arranque (Bootstrap) da simulação distribuída. + *

+ * A ordem de inicialização é crítica para evitar Race Conditions na conexão TCP: + *

    + *
  1. Workers (Interseções): Iniciam os ServerSockets.
  2. + *
  3. Sink (Exit Node): Prepara-se para receber métricas finais.
  4. + *
  5. Delay de Estabilização: Pausa de 1s para garantir que os sockets estão em LISTENING.
  6. + *
  7. Source (Coordinator): Inicia a geração de carga e conecta-se aos nós.
  8. + *
+ * * @throws IOException Se falhar o fork de algum processo. */ public void startSimulation() throws IOException { if (!runningProcesses.isEmpty()) { @@ -54,7 +84,42 @@ public class SimulationProcessManager { } /** - * Stops all running simulation processes. + * Verifica o estado de "liveness" da simulação monitorizando o processo Coordenador. + *

+ * Como o Coordenador gere o relógio DES e a geração de eventos, a sua terminação + * (após o drain time) sinaliza o fim efetivo da simulação. + * * @return true se o Coordenador ainda estiver ativo (alive). + */ + public boolean isSimulationRunning() { + if (runningProcesses.isEmpty()) { + return false; + } + // Coordinator is the last process in the list + Process coordinator = runningProcesses.get(runningProcesses.size() - 1); + return coordinator.isAlive(); + } + + /** + * Bloqueia a thread atual até que a simulação termine naturalmente ou ocorra timeout. + * * @param timeoutSeconds Tempo máximo de espera. + * @return true se terminou, false se o timeout expirou. + * @throws InterruptedException Se a espera for interrompida. + */ + public boolean waitForCompletion(long timeoutSeconds) throws InterruptedException { + if (runningProcesses.isEmpty()) { + return true; + } + + Process coordinator = runningProcesses.get(runningProcesses.size() - 1); + return coordinator.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS); + } + + /** + * Executa o procedimento de encerramento (Teardown) de todos os processos. + *

+ * Tenta primeiro uma paragem graciosa (`SIGTERM`), aguarda meio segundo, e + * força a paragem (`SIGKILL`) para processos persistentes, garantindo que não + * ficam processos órfãos no SO. */ public void stopSimulation() { System.out.println("Stopping simulation processes..."); @@ -83,16 +148,17 @@ public class SimulationProcessManager { } /** - * Helper to start a single Java process. + * Helper de baixo nível para construção e lançamento de processos Java. + * Configura o redirecionamento de I/O para ficheiros de log na diretoria temporária do SO. */ private void startProcess(String className, String arg) throws IOException { String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; ProcessBuilder builder; if (arg != null) { - builder = new ProcessBuilder(javaBin, "-cp", classpath, className, arg); + builder = new ProcessBuilder(javaBin, "-cp", classpath, className, arg, configFile); } else { - builder = new ProcessBuilder(javaBin, "-cp", classpath, className); + builder = new ProcessBuilder(javaBin, "-cp", classpath, className, configFile); } // get the OS temp folder @@ -115,4 +181,4 @@ public class SimulationProcessManager { // print where the logs are actually going System.out.println("Logs redirected to: " + logFile.getAbsolutePath()); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/StatsMessage.java b/main/src/main/java/sd/dashboard/StatsMessage.java index 7209130..abc4730 100644 --- a/main/src/main/java/sd/dashboard/StatsMessage.java +++ b/main/src/main/java/sd/dashboard/StatsMessage.java @@ -4,7 +4,14 @@ import sd.model.MessageType; import sd.protocol.MessageProtocol; /** - * Message wrapper for sending statistics to the dashboard. + * Implementação concreta do protocolo de mensagens destinada ao transporte de telemetria. + *

+ * Esta classe atua como um envelope especializado para o envio de dados estatísticos + * (encapsulados em {@link StatsUpdatePayload}) dos nós operacionais (Interseções, Coordenador) + * para o servidor de Dashboard centralizado. + *

+ * Diferencia-se das mensagens de controlo genéricas por ter o destino fixado no + * "DashboardServer" e um tipo de mensagem imutável ({@code STATS_UPDATE}). */ public class StatsMessage implements MessageProtocol { @@ -14,27 +21,49 @@ public class StatsMessage implements MessageProtocol { private final String destinationNode; private final StatsUpdatePayload payload; + /** + * Cria uma nova mensagem de estatística. + * + * @param sourceNode O ID do nó que gerou as estatísticas (ex: "Cr1", "ExitNode"). + * @param payload O objeto DTO contendo os dados estatísticos brutos ou agregados. + */ public StatsMessage(String sourceNode, StatsUpdatePayload payload) { this.sourceNode = sourceNode; - this.destinationNode = "DashboardServer"; + this.destinationNode = "DashboardServer"; // Destino implícito e fixo this.payload = payload; } + /** + * Retorna o tipo da mensagem, que identifica semanticamente o conteúdo para o recetor. + * @return Sempre {@link MessageType#STATS_UPDATE}. + */ @Override public MessageType getType() { return MessageType.STATS_UPDATE; } + /** + * Obtém a carga útil da mensagem. + * @return O objeto {@link StatsUpdatePayload} associado. + */ @Override public Object getPayload() { return payload; } + /** + * Identifica a origem da mensagem. + * @return O ID do nó remetente. + */ @Override public String getSourceNode() { return sourceNode; } + /** + * Identifica o destino da mensagem. + * @return Sempre "DashboardServer". + */ @Override public String getDestinationNode() { return destinationNode; @@ -45,4 +74,4 @@ public class StatsMessage implements MessageProtocol { return String.format("StatsMessage[from=%s, to=%s, payload=%s]", sourceNode, destinationNode, payload); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/dashboard/StatsUpdatePayload.java b/main/src/main/java/sd/dashboard/StatsUpdatePayload.java index a84760b..6f1915e 100644 --- a/main/src/main/java/sd/dashboard/StatsUpdatePayload.java +++ b/main/src/main/java/sd/dashboard/StatsUpdatePayload.java @@ -7,25 +7,60 @@ 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. + * Objeto de Transferência de Dados (DTO) otimizado para transporte de telemetria. + *

+ * Esta classe encapsula as métricas de desempenho enviadas pelos nós da simulação (Coordenador, + * Interseções, ExitNode) para o Dashboard. Foi desenhada para suportar atualizações parciais + * (Sparse Updates): + *

    + *
  • Campos globais inicializados com {@code -1} indicam "sem alteração" (no-op). O Dashboard + * deve ignorar estes campos e manter o valor acumulado anterior.
  • + *
  • Campos de interseção ({@code arrivals}, {@code departures}) representam deltas ou snapshots + * específicos do nó remetente.
  • + *
+ * Implementa {@link Serializable} para transmissão direta via Java Sockets. + * + +[Image of data transfer object pattern] + */ public class StatsUpdatePayload implements Serializable { private static final long serialVersionUID = 1L; + // Global Metrics (Coordinator/ExitNode) + /** Total gerado. Valor -1 indica que este campo deve ser ignorado na atualização. */ private int totalVehiclesGenerated = -1; + + /** Total completado. Valor -1 indica que este campo deve ser ignorado. */ private int totalVehiclesCompleted = -1; + + /** Tempo total de sistema acumulado (ms). Valor -1 indica que deve ser ignorado. */ private long totalSystemTime = -1; + + /** Tempo total de espera acumulado (ms). Valor -1 indica que deve ser ignorado. */ private long totalWaitingTime = -1; + // Intersection Metrics (Worker Nodes) + /** Número de veículos que entraram na interseção desde o último reporte. */ private int intersectionArrivals = 0; + + /** Número de veículos que saíram da interseção desde o último reporte. */ private int intersectionDepartures = 0; + + /** Snapshot do tamanho atual da fila na interseção. */ private int intersectionQueueSize = 0; + // Detailed Breakdowns + /** Contagem acumulada por tipo de veículo. */ private Map vehicleTypeCounts; + + /** Tempos de espera acumulados por tipo de veículo. */ private Map vehicleTypeWaitTimes; + /** + * Inicializa o payload com os mapas vazios e contadores globais a -1 (estado neutro). + */ public StatsUpdatePayload() { this.vehicleTypeCounts = new HashMap<>(); this.vehicleTypeWaitTimes = new HashMap<>(); @@ -67,6 +102,8 @@ public class StatsUpdatePayload implements Serializable { return vehicleTypeWaitTimes; } + // Setters implementam Fluent Interface para construção encadeada + public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) { this.totalVehiclesGenerated = totalVehiclesGenerated; return this; @@ -118,4 +155,4 @@ public class StatsUpdatePayload implements Serializable { totalVehiclesGenerated, totalVehiclesCompleted, intersectionArrivals, intersectionDepartures, intersectionQueueSize); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/des/DESEventType.java b/main/src/main/java/sd/des/DESEventType.java new file mode 100644 index 0000000..789d2e9 --- /dev/null +++ b/main/src/main/java/sd/des/DESEventType.java @@ -0,0 +1,39 @@ +package sd.des; + +/** + * Tipos de eventos discretos da simulação. + * + *

Representa os eventos DES que avançam o estado da simulação, + * não categorias de logging (EventType está noutro package). + */ +public enum DESEventType { + /** Gerar novo veículo num ponto de entrada */ + VEHICLE_GENERATION, + + /** Veículo chega a uma interseção */ + VEHICLE_ARRIVAL, + + /** Veículo começa a atravessar o semáforo */ + VEHICLE_CROSSING_START, + + /** Veículo termina a travessia */ + VEHICLE_CROSSING_END, + + /** Veículo parte para o próximo destino */ + VEHICLE_DEPARTURE, + + /** Veículo sai do sistema no nó de saída */ + VEHICLE_EXIT, + + /** Semáforo muda de estado (VERMELHO para VERDE ou vice-versa) */ + TRAFFIC_LIGHT_CHANGE, + + /** Processar veículos que esperam num semáforo recém-verde */ + PROCESS_GREEN_LIGHT, + + /** Atualização periódica de estatísticas */ + STATISTICS_UPDATE, + + /** Terminação da simulação */ + SIMULATION_END +} diff --git a/main/src/main/java/sd/des/EventQueue.java b/main/src/main/java/sd/des/EventQueue.java new file mode 100644 index 0000000..c61fc8d --- /dev/null +++ b/main/src/main/java/sd/des/EventQueue.java @@ -0,0 +1,137 @@ +package sd.des; + +import java.util.ArrayList; +import java.util.List; +import java.util.PriorityQueue; + +/** + * Gere a Lista de Eventos Futuros (FEL) para Simulação de Eventos Discretos. + * + *

A FEL é uma fila de prioridade que mantém todos os eventos futuros agendados, + * ordenados por timestamp. Este é o coração do paradigma DES - a simulação avança + * processando eventos em ordem cronológica.

+ */ +public class EventQueue { + private final PriorityQueue queue; + private final List processedEvents; // For logging and analysis + private final boolean trackHistory; + + public EventQueue() { + this(true); + } + + public EventQueue(boolean trackHistory) { + this.queue = new PriorityQueue<>(); + this.processedEvents = trackHistory ? new ArrayList<>() : null; + this.trackHistory = trackHistory; + } + + /** + * Agenda um novo evento. + * + * @param event evento a agendar + */ + public void schedule(SimulationEvent event) { + queue.offer(event); + } + + /** + * Agenda um evento com um atraso relativo ao tempo atual. + * + * @param currentTime tempo atual da simulação + * @param delay atraso em segundos + * @param type tipo de evento + * @param payload dados do evento + * @param location localização do evento + */ + public void scheduleIn(double currentTime, double delay, DESEventType type, + Object payload, String location) { + double eventTime = currentTime + delay; + schedule(new SimulationEvent(eventTime, type, payload, location)); + } + + /** Obtém o próximo evento sem o remover */ + public SimulationEvent peek() { + return queue.peek(); + } + + /** + * Obtém e remove o próximo evento. + * Se o rastreamento de histórico estiver ativo, adiciona-o aos eventos processados. + */ + public SimulationEvent poll() { + SimulationEvent event = queue.poll(); + if (event != null && trackHistory) { + processedEvents.add(event); + } + return event; + } + + /** Verifica se existem eventos pendentes */ + public boolean isEmpty() { + return queue.isEmpty(); + } + + /** @return número de eventos pendentes */ + public int size() { + return queue.size(); + } + + /** Limpa todos os eventos pendentes */ + public void clear() { + queue.clear(); + } + + /** + * Obtém todos os eventos processados (se o rastreamento estiver ativo). + * Retorna uma cópia para evitar modificações. + */ + public List getProcessedEvents() { + if (!trackHistory) { + throw new UnsupportedOperationException("History tracking is disabled"); + } + return new ArrayList<>(processedEvents); + } + + /** @return número de eventos processados */ + public int getProcessedCount() { + return trackHistory ? processedEvents.size() : 0; + } + + /** + * Exporta o histórico de eventos para uma string formatada. + * Útil para debugging e visualização da lista completa de eventos. + */ + public String exportEventHistory() { + if (!trackHistory) { + return "Event history tracking is disabled"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("=".repeat(80)).append("\n"); + sb.append("SIMULATION EVENT HISTORY\n"); + sb.append("Total Events Processed: ").append(processedEvents.size()).append("\n"); + sb.append("=".repeat(80)).append("\n"); + sb.append(String.format("%-10s | %-25s | %-20s | %s\n", + "Time", "Event Type", "Location", "Details")); + sb.append("-".repeat(80)).append("\n"); + + for (SimulationEvent event : processedEvents) { + String details = event.getPayload() != null ? + event.getPayload().getClass().getSimpleName() : "null"; + sb.append(String.format("%-10.3f | %-25s | %-20s | %s\n", + event.getTimestamp(), + event.getType(), + event.getLocation() != null ? event.getLocation() : "N/A", + details)); + } + + return sb.toString(); + } + + @Override + public String toString() { + return String.format("EventQueue[pending=%d, processed=%d]", + queue.size(), getProcessedCount()); + } +} diff --git a/main/src/main/java/sd/des/SimulationClock.java b/main/src/main/java/sd/des/SimulationClock.java new file mode 100644 index 0000000..c43cc6a --- /dev/null +++ b/main/src/main/java/sd/des/SimulationClock.java @@ -0,0 +1,71 @@ +package sd.des; + +/** + * Gere o tempo de simulação para Simulação de Eventos Discretos. + * + *

+ * No DES, o tempo avança em saltos discretos de evento para evento, + * não de forma contínua como o tempo real. + *

+ * + *

+ * Esta classe garante que todos os processos no sistema distribuído + * mantêm uma visão sincronizada do tempo de simulação. + *

+ */ +public class SimulationClock { + private double currentTime; + private final double startTime; + private final long wallClockStart; + + public SimulationClock() { + this(0.0); + } + + public SimulationClock(double startTime) { + this.currentTime = startTime; + this.startTime = startTime; + this.wallClockStart = System.currentTimeMillis(); + } + + /** + * Avança o tempo de simulação para o timestamp dado. + * O tempo só pode avançar, nunca recuar. + * + * @param newTime novo tempo de simulação + * @throws IllegalArgumentException se newTime for anterior ao tempo atual + */ + public void advanceTo(double newTime) { + if (newTime < currentTime) { + throw new IllegalArgumentException( + String.format("Cannot move time backwards: %.3f -> %.3f", currentTime, newTime)); + } + this.currentTime = newTime; + } + + /** @return tempo atual da simulação */ + public double getCurrentTime() { + return currentTime; + } + + /** @return tempo de simulação decorrido desde o início */ + public double getElapsedTime() { + return currentTime - startTime; + } + + /** @return tempo real decorrido em milissegundos */ + public long getWallClockElapsed() { + return System.currentTimeMillis() - wallClockStart; + } + + /** Reinicia o relógio para o tempo inicial */ + public void reset() { + this.currentTime = startTime; + } + + @Override + public String toString() { + return String.format("SimulationClock[time=%.3fs, elapsed=%.3fs]", + currentTime, getElapsedTime()); + } +} diff --git a/main/src/main/java/sd/des/SimulationEvent.java b/main/src/main/java/sd/des/SimulationEvent.java new file mode 100644 index 0000000..589bc5e --- /dev/null +++ b/main/src/main/java/sd/des/SimulationEvent.java @@ -0,0 +1,130 @@ +package sd.des; + +import java.io.Serializable; + +/** + * Representa um evento atómico e imutável no contexto da Simulação de Eventos Discretos (DES). + *

+ * Esta classe é a unidade fundamental de processamento. Numa arquitetura DES, o estado do sistema + * não muda continuamente, mas sim em instantes discretos definidos por estes eventos. + *

+ * Características principais: + *

    + *
  • Ordenação Temporal: Implementa {@link Comparable} para ser armazenado numa Fila de + * Eventos Futuros (FEL - Future Event List), garantindo execução cronológica.
  • + *
  • Distribuído: Implementa {@link Serializable} para permitir que eventos gerados num nó + * (ex: Coordenador) sejam transmitidos e executados noutro (ex: Interseção).
  • + *
  • Polimórfico: Transporta um {@code payload} genérico, permitindo associar qualquer + * entidade (Veículo, Sinal, etc.) ao evento.
  • + *
+ */ +public class SimulationEvent implements Comparable, Serializable { + private static final long serialVersionUID = 1L; + + /** O instante virtual exato em que o evento deve ser processado. */ + private final double timestamp; + + /** A categoria do evento (ex: VEHICLE_ARRIVAL, LIGHT_CHANGE). */ + private final DESEventType type; + + /** Dados contextuais associados (ex: o objeto Vehicle que chegou). */ + private final Object payload; + + /** + * O identificador do nó de destino onde o evento deve ser executado. + * (ex: "Cr1", "Coordinator", "ExitNode"). Se null, é um evento local. + */ + private final String location; + + /** + * Instancia um novo evento de simulação completo. + * + * @param timestamp Instante de execução (segundos virtuais). + * @param type Tipo enumerado do evento. + * @param payload Objeto de dados associado (pode ser null). + * @param location ID do processo alvo para execução distribuída. + */ + public SimulationEvent(double timestamp, DESEventType type, Object payload, String location) { + this.timestamp = timestamp; + this.type = type; + this.payload = payload; + this.location = location; + } + + /** + * Construtor de conveniência para eventos locais (dentro do mesmo processo). + * Define {@code location} como null. + * + * @param timestamp Instante de execução. + * @param type Tipo do evento. + * @param payload Objeto de dados associado. + */ + public SimulationEvent(double timestamp, DESEventType type, Object payload) { + this(timestamp, type, payload, null); + } + + public double getTimestamp() { + return timestamp; + } + + public DESEventType getType() { + return type; + } + + public Object getPayload() { + return payload; + } + + public String getLocation() { + return location; + } + + /** + * Define a ordem natural de processamento na Fila de Prioridade. + *

+ * Lógica de Ordenação: + *

    + *
  1. Primária (Tempo): Eventos com menor timestamp ocorrem primeiro.
  2. + *
  3. Secundária (Determinismo): Em caso de empate temporal (simultaneidade), + * ordena alfabeticamente pelo nome do tipo. Isto garante que execuções repetidas + * da simulação produzam exatamente o mesmo resultado (determinismo estrito).
  4. + *
+ * + * @param other O outro evento a comparar. + * @return Inteiro negativo, zero ou positivo conforme a ordem. + */ + @Override + public int compareTo(SimulationEvent other) { + int timeComparison = Double.compare(this.timestamp, other.timestamp); + if (timeComparison != 0) { + return timeComparison; + } + // Tie-breaker: order by event type name to ensure reproducible runs + return this.type.name().compareTo(other.type.name()); + } + + @Override + public String toString() { + return String.format("Event[t=%.3f, type=%s, location=%s]", + timestamp, type, location); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof SimulationEvent)) return false; + SimulationEvent other = (SimulationEvent) obj; + return Double.compare(timestamp, other.timestamp) == 0 && + type == other.type && + (location == null ? other.location == null : location.equals(other.location)); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + Double.hashCode(timestamp); + result = 31 * result + type.hashCode(); + result = 31 * result + (location != null ? location.hashCode() : 0); + return result; + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/des/TrafficLightEvent.java b/main/src/main/java/sd/des/TrafficLightEvent.java new file mode 100644 index 0000000..0b27107 --- /dev/null +++ b/main/src/main/java/sd/des/TrafficLightEvent.java @@ -0,0 +1,55 @@ +package sd.des; + +import sd.model.TrafficLight; + +/** + * Encapsula o contexto de dados para eventos de mudança de estado de semáforos. + *

+ * Este objeto atua como o payload transportado por um {@link SimulationEvent} + * quando o tipo de evento é relacionado com controlo de tráfego (ex: mudança Verde -> Amarelo). + * Permite que o motor DES identifique exatamente qual instância de {@link TrafficLight} + * deve ser atualizada numa determinada interseção e direção. + */ +public class TrafficLightEvent { + private final TrafficLight light; + private final String direction; + private final String intersectionId; + + /** + * Cria um novo payload de evento de semáforo. + * @param light A instância do objeto semáforo a ser manipulado. + * @param direction A direção cardeal associada (ex: "North", "East"). + * @param intersectionId O identificador da interseção onde o semáforo reside. + */ + public TrafficLightEvent(TrafficLight light, String direction, String intersectionId) { + this.light = light; + this.direction = direction; + this.intersectionId = intersectionId; + } + + /** + * @return A referência direta para o objeto de domínio do semáforo. + */ + public TrafficLight getLight() { + return light; + } + + /** + * @return A direção do fluxo controlado por este semáforo. + */ + public String getDirection() { + return direction; + } + + /** + * @return O ID da interseção pai. + */ + public String getIntersectionId() { + return intersectionId; + } + + @Override + public String toString() { + return String.format("TrafficLightEvent[%s-%s]", intersectionId, direction); + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/engine/TrafficLightThread.java b/main/src/main/java/sd/engine/TrafficLightThread.java deleted file mode 100644 index 8a0c3a5..0000000 --- a/main/src/main/java/sd/engine/TrafficLightThread.java +++ /dev/null @@ -1,126 +0,0 @@ -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"); - - // Process queue for the duration of the green light - long greenDurationMs = (long) (light.getGreenTime() * 1000); - processGreenLightQueue(greenDurationMs); - - 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(long greenDurationMs) throws InterruptedException { - long startTime = System.currentTimeMillis(); - - while (running && !Thread.currentThread().isInterrupted() - && light.getState() == TrafficLightState.GREEN) { - - // Check if green time has expired - long elapsed = System.currentTimeMillis() - startTime; - if (elapsed >= greenDurationMs) { - break; - } - - if (light.getQueueSize() > 0) { - Vehicle vehicle = light.removeVehicle(); - - if (vehicle != null) { - double crossingTime = getCrossingTimeForVehicle(vehicle); - long crossingTimeMs = (long) (crossingTime * 1000); - - Thread.sleep(crossingTimeMs); - - vehicle.addCrossingTime(crossingTime); - process.getIntersection().incrementVehiclesSent(); - process.sendVehicleToNextDestination(vehicle); - } - } else { - // Queue is empty, wait briefly for new vehicles or until time expires - Thread.sleep(50); - } - } - } - - 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(); - } - } -} \ No newline at end of file diff --git a/main/src/main/java/sd/logging/EventLogger.java b/main/src/main/java/sd/logging/EventLogger.java new file mode 100644 index 0000000..dd08b23 --- /dev/null +++ b/main/src/main/java/sd/logging/EventLogger.java @@ -0,0 +1,251 @@ +package sd.logging; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Motor de logging assíncrono e thread-safe para a simulação distribuída. + *

+ * Implementa o padrão Singleton para garantir um ponto centralizado de registo. + * Utiliza o padrão Producer-Consumer com uma {@link BlockingQueue} para desacoplar + * a geração de eventos (crítica para a performance da simulação) da persistência em disco + * (operação de I/O lenta). + *

+ * Garantias: + *

    + *
  • Non-blocking writes (para a thread chamadora, na maioria dos casos).
  • + *
  • Ordering cronológico aproximado (FIFO na fila).
  • + *
  • Graceful Shutdown (flush de logs pendentes ao terminar).
  • + *
+ */ +public class EventLogger { + + private static EventLogger instance; + private static final Object instanceLock = new Object(); + + private final PrintWriter writer; + + /** Buffer de memória para absorver picos de eventos (Burst traffic). */ + private final BlockingQueue logQueue; + + /** Thread dedicada (Consumer) para escrita em ficheiro. */ + private final Thread writerThread; + + private final AtomicBoolean running; + private final SimpleDateFormat timestampFormat; + private final long simulationStartMillis; + + /** + * Inicializa o sistema de logs. + * Abre o ficheiro, escreve o cabeçalho e inicia a thread consumidora. + * + * @param logFilePath Caminho relativo ou absoluto do ficheiro de log. + * @throws IOException Se não for possível criar ou escrever no ficheiro. + */ + private EventLogger(String logFilePath) throws IOException { + // Auto-flush ativado para garantir persistência, mas gerido pelo buffer do BufferedWriter + this.writer = new PrintWriter(new BufferedWriter(new FileWriter(logFilePath, false)), true); + this.logQueue = new LinkedBlockingQueue<>(10000); // Backpressure: limita a 10k eventos pendentes + this.running = new AtomicBoolean(true); + this.timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + this.simulationStartMillis = System.currentTimeMillis(); + + // Header inicial do log + writer.println("=".repeat(80)); + writer.println("SIMULATION EVENT LOG"); + writer.println("Started: " + timestampFormat.format(new Date())); + writer.println("=".repeat(80)); + writer.println(); + writer.printf("%-23s | %-8s | %-20s | %-15s | %s\n", + "TIMESTAMP", "REL_TIME", "EVENT_TYPE", "COMPONENT", "DESCRIPTION"); + writer.println("-".repeat(80)); + writer.flush(); + + this.writerThread = new Thread(this::processLogQueue, "EventLogger-Writer"); + this.writerThread.setDaemon(true); // Permite que a JVM termine se apenas esta thread sobrar + this.writerThread.start(); + } + + /** + * Obtém a instância única do logger (Lazy Initialization). + * Se não existir, cria uma predefinida em "logs/simulation-events.log". + * + * @return A instância singleton. + */ + public static EventLogger getInstance() { + if (instance == null) { + synchronized (instanceLock) { + if (instance == null) { + try { + String logFile = "logs/simulation-events.log"; + java.nio.file.Files.createDirectories( + java.nio.file.Paths.get("logs")); + instance = new EventLogger(logFile); + } catch (IOException e) { + System.err.println("Failed to initialize EventLogger: " + e.getMessage()); + e.printStackTrace(); + } + } + } + } + return instance; + } + + /** + * Reinicializa o logger com um ficheiro específico. + * Útil para testes ou configurações personalizadas. + */ + public static void initialize(String logFilePath) throws IOException { + synchronized (instanceLock) { + if (instance != null) { + instance.shutdown(); + } + instance = new EventLogger(logFilePath); + } + } + + /** + * Regista um evento genérico. + * Esta operação é não-bloqueante (retorna imediatamente após colocar na fila), + * exceto se a fila estiver cheia (backpressure). + * + * @param eventType Categoria do evento. + * @param component Nome do componente (ex: "Coordinator", "IntersectionProcess"). + * @param description Detalhes do evento. + */ + public void log(EventType eventType, String component, String description) { + if (!running.get()) return; + + LogEntry entry = new LogEntry( + System.currentTimeMillis(), + eventType, + component, + description + ); + + // Non-blocking offer - if queue is full, drop oldest or warn + if (!logQueue.offer(entry)) { + // Queue full - this shouldn't happen with 10k buffer, but handle gracefully + System.err.println("EventLogger queue full - dropping event: " + eventType); + } + } + + /** + * Regista um evento associado a um veículo específico (Helper method). + */ + public void logVehicle(EventType eventType, String component, String vehicleId, String description) { + log(eventType, component, "[" + vehicleId + "] " + description); + } + + /** + * Regista um erro ou exceção com formatação apropriada. + */ + public void logError(String component, String description, Exception e) { + String fullDescription = description + (e != null ? ": " + e.getMessage() : ""); + log(EventType.ERROR, component, fullDescription); + } + + /** + * Lógica da thread consumidora (Worker Thread). + * Retira eventos da fila e escreve no disco continuamente. + */ + private void processLogQueue() { + while (running.get() || !logQueue.isEmpty()) { + try { + // Poll com timeout para permitir verificar a flag 'running' periodicamente + LogEntry entry = logQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS); + if (entry != null) { + writeEntry(entry); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + // Flush final: garantir que eventos restantes na fila são escritos antes de morrer + while (!logQueue.isEmpty()) { + LogEntry entry = logQueue.poll(); + if (entry != null) { + writeEntry(entry); + } + } + } + + /** + * Formata e escreve uma entrada de log no PrintWriter. + */ + private void writeEntry(LogEntry entry) { + String timestamp = timestampFormat.format(new Date(entry.timestampMillis)); + double relativeTime = (entry.timestampMillis - simulationStartMillis) / 1000.0; + + writer.printf("%-23s | %8.3fs | %-20s | %-15s | %s\n", + timestamp, + relativeTime, + entry.eventType.toString(), + truncate(entry.component, 15), + entry.description + ); + + // Flush periódico inteligente: se a carga for baixa, garante que vemos logs em tempo real + if (logQueue.size() < 10) { + writer.flush(); + } + } + + private String truncate(String str, int maxLength) { + if (str == null) return ""; + return str.length() <= maxLength ? str : str.substring(0, maxLength); + } + + /** + * Encerra o logger de forma segura. + * Desativa a aceitação de novos eventos, aguarda que a fila esvazie (flush) + * e fecha o ficheiro. + */ + public void shutdown() { + if (!running.compareAndSet(true, false)) { + return; // Já encerrado + } + + try { + // Wait for writer thread to finish flushing + writerThread.join(5000); // Wait up to 5 seconds + + // Write footer + writer.println(); + writer.println("-".repeat(80)); + writer.println("SIMULATION ENDED"); + writer.println("Ended: " + timestampFormat.format(new Date())); + writer.println("=".repeat(80)); + + writer.close(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * DTO interno imutável para armazenar dados do evento na fila. + */ + private static class LogEntry { + final long timestampMillis; + final EventType eventType; + final String component; + final String description; + + LogEntry(long timestampMillis, EventType eventType, String component, String description) { + this.timestampMillis = timestampMillis; + this.eventType = eventType; + this.component = component; + this.description = description; + } + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/logging/EventType.java b/main/src/main/java/sd/logging/EventType.java new file mode 100644 index 0000000..910ae94 --- /dev/null +++ b/main/src/main/java/sd/logging/EventType.java @@ -0,0 +1,60 @@ +package sd.logging; + +/** + * Taxonomia oficial de eventos para o subsistema de logging centralizado. + *

+ * Este enumerado padroniza a categorização de todas as ocorrências na simulação, permitindo: + *

    + *
  • Filtragem granular de logs (ex: ver apenas erros ou apenas tráfego de rede).
  • + *
  • Análise estatística post-mortem (parsear logs para calcular latências).
  • + *
  • Correlação de eventos distribuídos (seguir o rastro de um veículo através de vários nós).
  • + *
+ */ +public enum EventType { + // --- Ciclo de Vida do Veículo --- + VEHICLE_GENERATED("Vehicle Generated"), + VEHICLE_ARRIVED("Vehicle Arrived"), + VEHICLE_QUEUED("Vehicle Queued"), + VEHICLE_DEPARTED("Vehicle Departed"), + VEHICLE_EXITED("Vehicle Exited"), + + // --- Controlo de Semáforos e Exclusão Mútua --- + LIGHT_CHANGED_GREEN("Light Changed to Green"), + LIGHT_CHANGED_RED("Light Changed to Red"), + LIGHT_REQUEST_GREEN("Light Requested Green"), + LIGHT_RELEASE_GREEN("Light Released Green"), + + // --- Ciclo de Vida da Simulação/Processos --- + SIMULATION_STARTED("Simulation Started"), + SIMULATION_STOPPED("Simulation Stopped"), + PROCESS_STARTED("Process Started"), + PROCESS_STOPPED("Process Stopped"), + + // --- Configuração e Telemetria --- + STATS_UPDATE("Statistics Update"), + CONFIG_CHANGED("Configuration Changed"), + + // --- Camada de Rede (TCP/Sockets) --- + CONNECTION_ESTABLISHED("Connection Established"), + CONNECTION_LOST("Connection Lost"), + MESSAGE_SENT("Message Sent"), + MESSAGE_RECEIVED("Message Received"), + + // --- Tratamento de Exceções --- + ERROR("Error"); + + private final String displayName; + + EventType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String toString() { + return displayName; + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/logging/VehicleTracer.java b/main/src/main/java/sd/logging/VehicleTracer.java new file mode 100644 index 0000000..611e6aa --- /dev/null +++ b/main/src/main/java/sd/logging/VehicleTracer.java @@ -0,0 +1,364 @@ +package sd.logging; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import sd.model.Vehicle; + +/** + * Subsistema de auditoria granular responsável pelo rastreio detalhado (Tracing) de veículos individuais. + *

+ * Diferente do {@link EventLogger} (que regista eventos globais do sistema), esta classe foca-se + * na perspetiva do agente. Cria um ficheiro de rastro dedicado (`.trace`) para cada veículo + * monitorizado, registando cronologicamente cada interação com a infraestrutura (interseções, + * filas, semáforos). + *

+ * Funcionalidades: + *

    + *
  • Análise forense de percursos individuais.
  • + *
  • Validação de tempos de espera e travessia por nó.
  • + *
  • Cálculo de eficiência de rota (tempo em movimento vs. tempo parado).
  • + *
+ */ +public class VehicleTracer { + + private static VehicleTracer instance; + private static final Object instanceLock = new Object(); + + /** Mapa thread-safe de sessões de trace ativas (VehicleID -> TraceHandler). */ + private final Map trackedVehicles; + + private final SimpleDateFormat timestampFormat; + private final long simulationStartMillis; + private final String traceDirectory; + + /** + * Inicializa o tracer e prepara o diretório de saída. + * + * @param traceDirectory Caminho para armazenamento dos ficheiros .trace. + */ + private VehicleTracer(String traceDirectory) { + this.trackedVehicles = new ConcurrentHashMap<>(); + this.timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + this.simulationStartMillis = System.currentTimeMillis(); + this.traceDirectory = traceDirectory; + + try { + java.nio.file.Files.createDirectories(java.nio.file.Paths.get(traceDirectory)); + } catch (IOException e) { + System.err.println("Failed to create trace directory: " + e.getMessage()); + } + } + + /** + * Obtém a instância única do tracer (Singleton). + * @return A instância global. + */ + public static VehicleTracer getInstance() { + if (instance == null) { + synchronized (instanceLock) { + if (instance == null) { + instance = new VehicleTracer("logs/traces"); + } + } + } + return instance; + } + + /** + * Reinicializa o tracer com um diretório personalizado. + * Útil para isolar logs de diferentes execuções em lote. + */ + public static void initialize(String traceDirectory) { + synchronized (instanceLock) { + if (instance != null) { + instance.shutdown(); + } + instance = new VehicleTracer(traceDirectory); + } + } + + /** + * Inicia a sessão de rastreio para um veículo específico. + * Cria o ficheiro {@code logs/traces/vehicle-{id}.trace} e escreve o cabeçalho. + * + * @param vehicleId O identificador único do veículo. + */ + public void startTracking(String vehicleId) { + if (trackedVehicles.containsKey(vehicleId)) { + return; // Já está a ser rastreado + } + + VehicleTrace trace = new VehicleTrace(vehicleId, traceDirectory); + trackedVehicles.put(vehicleId, trace); + + trace.logEvent("TRACKING_STARTED", "", "Started tracking vehicle " + vehicleId); + } + + /** + * Encerra a sessão de rastreio, fecha o descritor de ficheiro e remove da memória. + */ + public void stopTracking(String vehicleId) { + VehicleTrace trace = trackedVehicles.remove(vehicleId); + if (trace != null) { + trace.logEvent("TRACKING_STOPPED", "", "Stopped tracking vehicle " + vehicleId); + trace.close(); + } + } + + /** + * Verifica se um veículo está atualmente sob auditoria. + */ + public boolean isTracking(String vehicleId) { + return trackedVehicles.containsKey(vehicleId); + } + + /** + * Regista o evento de instanciação do veículo pelo Coordenador. + */ + public void logGenerated(Vehicle vehicle) { + if (!isTracking(vehicle.getId())) + return; + + VehicleTrace trace = trackedVehicles.get(vehicle.getId()); + if (trace != null) { + trace.logEvent("GENERATED", "Coordinator", + String.format("Type: %s, Entry Time: %.2fs, Route: %s", + vehicle.getType(), vehicle.getEntryTime(), vehicle.getRoute())); + } + } + + /** + * Regista a chegada física do veículo à zona de deteção de uma interseção. + */ + public void logArrival(String vehicleId, String intersection, double simulationTime) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("ARRIVED", intersection, + String.format("Arrived at %s (sim time: %.2fs)", intersection, simulationTime)); + } + } + + /** + * Regista a entrada do veículo na estrutura de fila de um semáforo. + */ + public void logQueued(String vehicleId, String intersection, String direction, int queuePosition) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("QUEUED", intersection, + String.format("Queued at %s-%s (position: %d)", intersection, direction, queuePosition)); + } + } + + /** + * Regista o início da espera ativa (veículo parado no Vermelho). + */ + public void logWaitingStart(String vehicleId, String intersection, String direction) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("WAITING_START", intersection, + String.format("Started waiting at %s-%s (light is RED)", intersection, direction)); + } + } + + /** + * Regista o fim da espera (Sinal Verde). + * @param waitTime Duração total da paragem nesta instância. + */ + public void logWaitingEnd(String vehicleId, String intersection, String direction, double waitTime) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("WAITING_END", intersection, + String.format("Finished waiting at %s-%s (waited %.2fs)", intersection, direction, waitTime)); + } + } + + /** + * Regista o início da travessia da interseção (ocupação da zona crítica). + */ + public void logCrossingStart(String vehicleId, String intersection, String direction) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("CROSSING_START", intersection, + String.format("Started crossing %s-%s (light is GREEN)", intersection, direction)); + } + } + + /** + * Regista a libertação da zona crítica da interseção. + */ + public void logCrossingEnd(String vehicleId, String intersection, double crossingTime) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("CROSSING_END", intersection, + String.format("Finished crossing %s (took %.2fs)", intersection, crossingTime)); + } + } + + /** + * Regista a partida da interseção em direção ao próximo nó. + */ + public void logDeparture(String vehicleId, String intersection, String nextDestination) { + if (!isTracking(vehicleId)) + return; + + VehicleTrace trace = trackedVehicles.get(vehicleId); + if (trace != null) { + trace.logEvent("DEPARTED", intersection, + String.format("Departed from %s toward %s", intersection, nextDestination)); + } + } + + /** + * Regista a saída do sistema (no Exit Node). + *

+ * Este método também desencadeia a escrita do Sumário de Viagem no final do log + * e fecha o ficheiro automaticamente. + */ + public void logExit(Vehicle vehicle, double systemTime) { + if (!isTracking(vehicle.getId())) + return; + + VehicleTrace trace = trackedVehicles.get(vehicle.getId()); + if (trace != null) { + trace.logEvent("EXITED", "Exit Node", + String.format("Exited system - Total time: %.2fs, Waiting: %.2fs, Crossing: %.2fs", + systemTime, vehicle.getTotalWaitingTime(), vehicle.getTotalCrossingTime())); + + // Escreve estatísticas sumarizadas + trace.writeSummary(vehicle, systemTime); + + // Stop tracking and close file + stopTracking(vehicle.getId()); + } + } + + /** + * Fecha forçosamente todos os traces abertos. + * Deve ser chamado no shutdown da simulação para evitar corrupção de logs. + */ + public void shutdown() { + for (VehicleTrace trace : trackedVehicles.values()) { + trace.close(); + } + trackedVehicles.clear(); + } + + /** + * Classe interna auxiliar que gere o descritor de ficheiro e a formatação para um único veículo. + */ + private class VehicleTrace { + private final String vehicleId; + private final PrintWriter writer; + private final long traceStartMillis; + + VehicleTrace(String vehicleId, String directory) { + this.vehicleId = vehicleId; + this.traceStartMillis = System.currentTimeMillis(); + + PrintWriter w = null; + try { + String filename = String.format("%s/vehicle-%s.trace", directory, vehicleId); + w = new PrintWriter(new BufferedWriter(new FileWriter(filename, false)), true); + + // Write header + w.println("=".repeat(80)); + w.println("VEHICLE TRACE: " + vehicleId); + w.println("Trace Started: " + timestampFormat.format(new Date())); + w.println("=".repeat(80)); + w.println(); + w.printf("%-23s | %-8s | %-15s | %-15s | %s\n", + "TIMESTAMP", "REL_TIME", "EVENT", "LOCATION", "DESCRIPTION"); + w.println("-".repeat(80)); + + } catch (IOException e) { + System.err.println("Failed to create trace file for " + vehicleId + ": " + e.getMessage()); + } + + this.writer = w; + } + + void logEvent(String eventType, String location, String description) { + if (writer == null) + return; + + long now = System.currentTimeMillis(); + String timestamp = timestampFormat.format(new Date(now)); + double relativeTime = (now - traceStartMillis) / 1000.0; + + writer.printf("%-23s | %8.3fs | %-15s | %-15s | %s\n", + timestamp, + relativeTime, + truncate(eventType, 15), + truncate(location, 15), + description); + writer.flush(); + } + + void writeSummary(Vehicle vehicle, double systemTime) { + if (writer == null) + return; + + writer.println(); + writer.println("=".repeat(80)); + writer.println("JOURNEY SUMMARY"); + writer.println("=".repeat(80)); + writer.println("Vehicle ID: " + vehicle.getId()); + writer.println("Vehicle Type: " + vehicle.getType()); + writer.println("Route: " + vehicle.getRoute()); + writer.println(); + writer.printf("Entry Time: %.2f seconds\n", vehicle.getEntryTime()); + writer.printf("Total System Time: %.2f seconds\n", systemTime); + writer.printf("Total Waiting Time: %.2f seconds (%.1f%%)\n", + vehicle.getTotalWaitingTime(), + 100.0 * vehicle.getTotalWaitingTime() / systemTime); + writer.printf("Total Crossing Time: %.2f seconds (%.1f%%)\n", + vehicle.getTotalCrossingTime(), + 100.0 * vehicle.getTotalCrossingTime() / systemTime); + writer.printf("Travel Time: %.2f seconds (%.1f%%)\n", + systemTime - vehicle.getTotalWaitingTime() - vehicle.getTotalCrossingTime(), + 100.0 * (systemTime - vehicle.getTotalWaitingTime() - vehicle.getTotalCrossingTime()) / systemTime); + writer.println("=".repeat(80)); + } + + void close() { + if (writer != null) { + writer.println(); + writer.println("-".repeat(80)); + writer.println("END OF TRACE"); + writer.println("=".repeat(80)); + writer.close(); + } + } + + private String truncate(String str, int maxLength) { + if (str == null) + return ""; + return str.length() <= maxLength ? str : str.substring(0, maxLength); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/model/Intersection.java b/main/src/main/java/sd/model/Intersection.java index 7d6ff32..612a266 100644 --- a/main/src/main/java/sd/model/Intersection.java +++ b/main/src/main/java/sd/model/Intersection.java @@ -6,65 +6,51 @@ import java.util.List; import java.util.Map; /** - * Represents an intersection in the traffic simulation. - * * An Intersection acts as a central hub. It does not control logic itself, - * but it *owns* and *manages* a set of {@link TrafficLight} objects. - * * Its primary responsibilities are: - * 1. Holding a {@link TrafficLight} for each direction ("North", "East", etc.). - * 2. Maintaining a {@code routing} table that maps a vehicle's *next* - * destination (e.g., "Cr3") to a specific *direction* at *this* - * intersection (e.g., "East"). - * 3. Receiving incoming vehicles and placing them in the correct - * traffic light's queue based on the routing table. - * 4. Tracking aggregate statistics for all traffic passing through it. + * Representa uma interseção na simulação de tráfego. + * + *

Uma interseção funciona como um nó central da rede. Não controla lógica diretamente, + * mas gere um conjunto de semáforos ({@link TrafficLight}).

+ * + *

Responsabilidades principais:

+ *
    + *
  • Manter um {@link TrafficLight} para cada direção (Norte, Este, etc.)
  • + *
  • Gerir uma tabela de encaminhamento que mapeia destinos para direções
  • + *
  • Receber veículos e colocá-los na fila do semáforo correto
  • + *
  • Acompanhar estatísticas agregadas do tráfego
  • + *
*/ public class Intersection { - // --- Identity and configuration --- - - /** - * Unique identifier for the intersection (e.g., "Cr1", "Cr2"). - */ + /** Identificador único da interseção (ex: "Cr1", "Cr2") */ private final String id; /** - * A map holding all traffic lights managed by this intersection. - * Key: Direction (String, e.g., "North", "East"). - * Value: The {@link TrafficLight} object for that direction. + * Mapa com todos os semáforos desta interseção. + * Chave: Direção (String, ex: "Norte", "Este") + * Valor: Objeto {@link TrafficLight} correspondente */ private final Map trafficLights; /** - * The routing table for this intersection. - * Key: The *next* destination ID (String, e.g., "Cr3", "S" for exit). - * Value: The *direction* (String, e.g., "East") a vehicle must take - * at *this* intersection to reach that destination. + * Tabela de encaminhamento da interseção. + * Chave: Próximo destino (String, ex: "Cr3", "S" para saída) + * Valor: Direção que o veículo deve tomar nesta interseção */ private final Map routing; - - // --- Statistics --- - - /** - * Total number of vehicles that have been received by this intersection. - */ + /** Número total de veículos recebidos por esta interseção */ private int totalVehiclesReceived; - /** - * Total number of vehicles that have successfully passed through (sent from) this intersection. - */ + /** Número total de veículos que partiram desta interseção */ private int totalVehiclesSent; - /** - * A running average of the waiting time for vehicles at this intersection. - * Note: This calculation might be simplified. - */ + /** Média acumulada do tempo de espera dos veículos nesta interseção */ private double averageWaitingTime; /** - * Constructs a new Intersection with a given ID. - * Initializes empty maps for traffic lights and routing. + * Cria uma nova interseção. + * Inicializa mapas vazios para semáforos e encaminhamento. * - * @param id The unique identifier for this intersection (e.g., "Cr1"). + * @param id identificador único da interseção (ex: "Cr1") */ public Intersection(String id) { this.id = id; @@ -76,42 +62,37 @@ public class Intersection { } /** - * Registers a new {@link TrafficLight} with this intersection. - * The light is mapped by its direction. + * Regista um novo semáforo nesta interseção. + * O semáforo é mapeado pela sua direção. * - * @param trafficLight The {@link TrafficLight} object to add. + * @param trafficLight o semáforo a adicionar */ public void addTrafficLight(TrafficLight trafficLight) { trafficLights.put(trafficLight.getDirection(), trafficLight); } /** - * Defines a routing rule for this intersection. - * * This method builds the routing table. For example, calling - * {@code configureRoute("Cr3", "East")} means "Any vehicle - * arriving here whose next destination is 'Cr3' should be sent to - * the 'East' traffic light queue." + * Define uma regra de encaminhamento para esta interseção. + * + *

Por exemplo, {@code configureRoute("Cr3", "Este")} significa: + * "Qualquer veículo que chegue aqui com destino 'Cr3' deve ser enviado + * para a fila do semáforo da direção Este."

* - * @param nextDestination The ID of the *next* intersection or exit (e.g., "Cr3", "S"). - * @param direction The direction (and thus, the traffic light) - * at *this* intersection to use (e.g., "East"). + * @param nextDestination ID da próxima interseção ou saída (ex: "Cr3", "S") + * @param direction direção (e respetivo semáforo) a usar nesta interseção */ public void configureRoute(String nextDestination, String direction) { routing.put(nextDestination, direction); } /** - * Accepts an incoming vehicle and places it in the correct queue. - * * This method: - * 1. Increments the {@link #totalVehiclesReceived} counter. - * 2. Advances the vehicle's route (since it just arrived here) - * 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}). - * 4. Uses the {@link #routing} map to find the correct *direction* for that destination. - * 5. Adds the vehicle to the queue of the {@link TrafficLight} for that direction. - * - * @param vehicle The {@link Vehicle} arriving at the intersection. + * Recebe um novo veículo e coloca-o na fila do semáforo apropriado. + * A direção é escolhida com base na tabela de encaminhamento. + * + * @param vehicle o veículo que está a chegar a esta interseção + * @param simulationTime o tempo de simulação atual (em segundos) */ - public void receiveVehicle(Vehicle vehicle) { + public void receiveVehicle(Vehicle vehicle, double simulationTime) { totalVehiclesReceived++; // Note: Route advancement is handled by SimulationEngine.handleVehicleArrival() @@ -130,7 +111,7 @@ public class Intersection { if (direction != null && trafficLights.containsKey(direction)) { // Found a valid route and light, add vehicle to the queue - trafficLights.get(direction).addVehicle(vehicle); + trafficLights.get(direction).addVehicle(vehicle, simulationTime); } else { // Routing error: No rule for this destination or no light for that direction System.err.printf( @@ -138,118 +119,100 @@ public class Intersection { this.id, vehicle.getId(), nextDestination, direction ); } - } - - /** - * Returns the direction a vehicle should take to reach a given destination. + } /** + * Retorna a direção que um veículo deve tomar para alcançar um destino. * - * @param destination The next destination (e.g., "Cr3", "S"). - * @return The direction (e.g., "East"), or null if no route is configured. + * @param destination o próximo destino (ex: "Cr3", "S") + * @return a direção (ex: "Este"), ou null se não houver rota configurada */ public String getDirectionForDestination(String destination) { return routing.get(destination); } /** - * Returns the traffic light controlling the given direction. + * Retorna o semáforo que controla uma determinada direção. * - * @param direction The direction (e.g., "North"). - * @return The {@link TrafficLight} object, or null if no light exists - * for that direction. + * @param direction a direção (ex: "Norte") + * @return o objeto {@link TrafficLight}, ou null se não existir */ public TrafficLight getTrafficLight(String direction) { return trafficLights.get(direction); } /** - * Returns a list of all traffic lights managed by this intersection. + * Retorna uma lista com todos os semáforos desta interseção. * - * @return A new {@link List} containing all {@link TrafficLight} objects. + * @return uma nova {@link List} com todos os semáforos */ public List getTrafficLights() { - // Return a copy to prevent external modification of the internal map's values return new ArrayList<>(trafficLights.values()); } /** - * Returns the total number of vehicles currently queued across *all* - * traffic lights at this intersection. + * Retorna o número total de veículos em fila em todos os semáforos. + * Usa Java Stream API para somar os tamanhos de todas as filas. * - * @return The sum of all queue sizes. + * @return a soma dos tamanhos de todas as filas */ public int getTotalQueueSize() { - // Uses Java Stream API: - // 1. trafficLights.values().stream() - Get a stream of TrafficLight objects - // 2. .mapToInt(TrafficLight::getQueueSize) - Convert each light to its queue size (an int) - // 3. .sum() - Sum all the integers return trafficLights.values().stream() .mapToInt(TrafficLight::getQueueSize) .sum(); } - // --- Stats and getters --- - /** - * @return The unique ID of this intersection. + * @return o identificador único desta interseção */ public String getId() { return id; } /** - * @return The total number of vehicles that have arrived at this intersection. + * @return o número total de veículos que chegaram a esta interseção */ public int getTotalVehiclesReceived() { return totalVehiclesReceived; } /** - * @return The total number of vehicles that have successfully - * departed from this intersection. + * @return o número total de veículos que partiram desta interseção */ public int getTotalVehiclesSent() { return totalVehiclesSent; } /** - * Increments the counter for vehicles that have successfully departed. - * This is typically called by the {@link sd.engine.SimulationEngine} - * after a vehicle finishes crossing. + * Incrementa o contador de veículos que partiram com sucesso. + * Tipicamente chamado após um veículo completar a travessia. */ public void incrementVehiclesSent() { totalVehiclesSent++; } /** - * @return The running average of vehicle waiting time at this intersection. + * @return a média do tempo de espera dos veículos nesta interseção */ public double getAverageWaitingTime() { return averageWaitingTime; } /** - * Updates the running average waiting time with a new sample (a new - * vehicle's wait time). - * * Uses an incremental/weighted average formula: - * NewAvg = (OldAvg * (N-1) + NewValue) / N - * where N is the total number of vehicles sent. + * Atualiza a média do tempo de espera com uma nova amostra. + * Usa a fórmula: Nova Média = (Média Antiga * (N-1) + Novo Valor) / N * - * @param newTime The waiting time (in seconds) of the vehicle that just - * departed. + * @param newTime tempo de espera (em segundos) do veículo que acabou de partir */ public void updateAverageWaitingTime(double newTime) { - // Avoid division by zero if this is called before any vehicle is sent if (totalVehiclesSent > 0) { averageWaitingTime = (averageWaitingTime * (totalVehiclesSent - 1) + newTime) / totalVehiclesSent; } else if (totalVehiclesSent == 1) { - // This is the first vehicle averageWaitingTime = newTime; } } /** - * @return A string summary of the intersection's current state. + * @return representação textual do estado atual da interseção */ @Override public String toString() { diff --git a/main/src/main/java/sd/model/Message.java b/main/src/main/java/sd/model/Message.java index 87e1200..d7a9573 100644 --- a/main/src/main/java/sd/model/Message.java +++ b/main/src/main/java/sd/model/Message.java @@ -5,52 +5,52 @@ import java.util.UUID; import sd.protocol.MessageProtocol; /** - * Represents a message exchanged between processes in the distributed simulation. - * Each message has a unique ID, a type, a sender, a destination, and a payload. - * This class implements {@link MessageProtocol} which extends Serializable for network transmission. + * Envelope fundamental do protocolo de comunicação entre processos distribuídos (IPC). + *

+ * Esta classe atua como a Unidade de Dados de Aplicação (ADU), encapsulando tanto + * os metadados de roteamento (origem, destino, tipo) quanto a carga útil (payload) + * polimórfica. É agnóstica ao conteúdo, servindo como contentor genérico para + * transferência de estado (Veículos, Estatísticas) ou sinais de controlo (Semáforos). + *

+ * A imutabilidade dos campos (exceto via serialização) garante a integridade da mensagem + * durante o trânsito na rede. */ public class Message implements MessageProtocol { private static final long serialVersionUID = 1L; - /** - * Unique identifier for this message. + /** * Identificador único universal (UUID). + * Essencial para rastreabilidade (tracing), logs de auditoria e mecanismos de deduplicação. */ private final String messageId; - /** - * The type of this message (e.g., VEHICLE_TRANSFER, STATS_UPDATE). - */ + /** Discriminador semântico que define como o recetor deve processar o payload. */ private final MessageType type; - /** - * Identifier of the process that sent this message. - */ + /** Identificador lógico do nó emissor (ex: "Cr1", "Coordinator"). */ private final String senderId; - /** - * Identifier of the destination process. Can be null for broadcast messages. + /** * Identificador lógico do nó recetor. + * Se {@code null}, a mensagem deve ser tratada como Broadcast. */ private final String destinationId; - /** - * The actual data being transmitted. Type depends on the message type. + /** * Carga útil polimórfica. + * Deve implementar {@link java.io.Serializable} para garantir transmissão correta. */ private final Object payload; - /** - * Timestamp when this message was created (simulation time or real time). - */ + /** Marca temporal da criação da mensagem (Unix Timestamp), usada para cálculo de latência de rede. */ private final long timestamp; /** - * Creates a new message with all parameters. + * Construtor completo para reconstrução de mensagens ou envio com timestamp manual. * - * @param type The message type - * @param senderId The ID of the sending process - * @param destinationId The ID of the destination process (null for broadcast) - * @param payload The message payload - * @param timestamp The timestamp of message creation + * @param type Classificação semântica da mensagem. + * @param senderId ID do processo origem. + * @param destinationId ID do processo destino (ou null para broadcast). + * @param payload Objeto de domínio a ser transportado. + * @param timestamp Instante de criação (ms). */ public Message(MessageType type, String senderId, String destinationId, Object payload, long timestamp) { @@ -63,23 +63,24 @@ public class Message implements MessageProtocol { } /** - * Creates a new message with current system time as timestamp. + * Construtor de conveniência que atribui automaticamente o timestamp atual do sistema. * - * @param type The message type - * @param senderId The ID of the sending process - * @param destinationId The ID of the destination process - * @param payload The message payload + * @param type Classificação semântica. + * @param senderId ID do processo origem. + * @param destinationId ID do processo destino. + * @param payload Objeto de domínio. */ public Message(MessageType type, String senderId, String destinationId, Object payload) { this(type, senderId, destinationId, payload, System.currentTimeMillis()); } /** - * Creates a broadcast message (no specific destination). + * Construtor de conveniência para mensagens de difusão (Broadcast). + * Define {@code destinationId} como null. * - * @param type The message type - * @param senderId The ID of the sending process - * @param payload The message payload + * @param type Classificação semântica. + * @param senderId ID do processo origem. + * @param payload Objeto de domínio. */ public Message(MessageType type, String senderId, Object payload) { this(type, senderId, null, payload, System.currentTimeMillis()); @@ -112,21 +113,23 @@ public class Message implements MessageProtocol { } /** - * Checks if this is a broadcast message (no specific destination). + * Verifica se a mensagem se destina a todos os nós da rede. * - * @return true if destinationId is null, false otherwise + * @return {@code true} se o destinationId for nulo. */ public boolean isBroadcast() { return destinationId == null; } /** - * Gets the payload cast to a specific type. - * Use with caution and ensure type safety. + * Utilitário para casting seguro e fluente do payload. + *

+ * Evita a necessidade de casts explícitos e supressão de warnings no código cliente. * - * @param The expected payload type - * @return The payload cast to type T - * @throws ClassCastException if the payload is not of type T + * @param O tipo esperado do payload. + * @param clazz A classe do tipo esperado para verificação em runtime (opcional no uso, mas boa prática). + * @return O payload convertido para o tipo T. + * @throws ClassCastException Se o payload não for compatível com o tipo solicitado. */ @SuppressWarnings("unchecked") public T getPayloadAs(Class clazz) { @@ -151,4 +154,4 @@ public class Message implements MessageProtocol { destinationId != null ? destinationId : "BROADCAST", timestamp); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/model/MessageType.java b/main/src/main/java/sd/model/MessageType.java index a9f4794..4089bcc 100644 --- a/main/src/main/java/sd/model/MessageType.java +++ b/main/src/main/java/sd/model/MessageType.java @@ -1,87 +1,49 @@ package sd.model; /** - * Enumeration representing all possible message types for distributed communication. - * These types are used for inter-process communication between different components - * of the distributed traffic simulation system. + * Enumeração que representa todos os tipos de mensagens possíveis para + * comunicação distribuída. + * Estes tipos são usados para a comunicação entre processos dos diferentes + * componentes + * do sistema de simulação de tráfego distribuído. */ public enum MessageType { - + /** - * Message to transfer a vehicle between intersections or processes. - * Payload: Vehicle object with current state + * Mensagem para transferir um veículo entre interseções ou processos. + * Payload: Objeto Vehicle com o estado atual */ VEHICLE_TRANSFER, - + /** - * Message to update statistics across the distributed system. - * Payload: Statistics data (waiting times, queue sizes, etc.) + * Mensagem para atualizar estatísticas em todo o sistema distribuído. + * Payload: Dados estatísticos (tempos de espera, tamanhos de fila, etc.) */ STATS_UPDATE, - + /** - * Message to synchronize simulation start time across all processes. - * Payload: Start timestamp (long milliseconds) + * Mensagem para sincronizar a hora de início da simulação em todos os + * processos. + * Payload: Timestamp de início (long milissegundos) */ SIMULATION_START, - + /** - * Message to synchronize traffic light states between processes. - * Payload: TrafficLight state and timing information - */ - TRAFFIC_LIGHT_SYNC, - - /** - * Heartbeat message to check if a process is alive. - * Payload: Process ID and timestamp - */ - HEARTBEAT, - - /** - * Request to join the distributed simulation. - * Payload: Process information and capabilities - */ - JOIN_REQUEST, - - /** - * Response to a join request. - * Payload: Acceptance status and configuration - */ - JOIN_RESPONSE, - - /** - * Message to notify about a new vehicle generation. - * Payload: Vehicle generation parameters + * Mensagem para notificar sobre a geração de um novo veículo. + * Payload: Parâmetros de geração do veículo */ VEHICLE_SPAWN, - + /** - * Message to request the current state of an intersection. - * Payload: Intersection ID - */ - STATE_REQUEST, - - /** - * Response containing the current state of an intersection. - * Payload: Complete intersection state - */ - STATE_RESPONSE, - - /** - * Message to signal shutdown of a process. - * Payload: Process ID and reason + * Mensagem para sinalizar o encerramento de um processo. + * Payload: ID do processo e motivo */ SHUTDOWN, - + /** - * Acknowledgment message for reliable communication. - * Payload: Message ID being acknowledged + * Mensagem para alterar a política de roteamento durante a simulação. + * Payload: String com o nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED) */ - ACK, - - /** - * Error message to report problems in the distributed system. - * Payload: Error description and context - */ - ERROR + ROUTING_POLICY_CHANGE, + } diff --git a/main/src/main/java/sd/model/TrafficLight.java b/main/src/main/java/sd/model/TrafficLight.java index 7cfd393..149e4e3 100644 --- a/main/src/main/java/sd/model/TrafficLight.java +++ b/main/src/main/java/sd/model/TrafficLight.java @@ -9,114 +9,69 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** - * Represents a single traffic light controlling one direction at an intersection. - * * Each light maintains its own queue of {@link Vehicle} objects and - * alternates between {@link TrafficLightState#GREEN} and - * {@link TrafficLightState#RED} states. - * * This class is designed to be thread-safe for a potential concurrent - * simulation (though the current engine {@link sd.engine.SimulationEngine} - * is single-threaded). It uses a {@link ReentrantLock} to protect its - * internal state (the queue and the light state) from simultaneous access. - * * The {@link Condition} variables ({@code vehicleAdded}, {@code lightGreen}) - * are included for a concurrent model where: - * - A "vehicle" thread might wait on {@code lightGreen} until the light changes. - * - A "controller" thread might wait on {@code vehicleAdded} to know when to - * process a queue. - * (Note: These Conditions are *not* used by the current discrete-event engine). + * Representa um semáforo numa interseção. + * + *

Cada semáforo controla uma direção específica e mantém uma fila de veículos à espera. + * Alterna entre os estados VERDE e VERMELHO de acordo com a temporização configurada.

+ * + *

Thread-safety: Usa locks para permitir acesso concorrente seguro entre + * a thread de processamento de eventos e as threads de I/O de rede.

*/ public class TrafficLight { - // --- Identity and configuration --- - - /** - * Unique identifier for the light (e.g., "Cr1-N"). - */ + /** Identificador único do semáforo (ex: "Cr1-N") */ private final String id; - - /** - * The direction this light controls (e.g., "North", "South"). - */ + + /** Direção que este semáforo controla (ex: "Norte", "Sul") */ private final String direction; - - /** - * The current state of the light (GREEN or RED). - */ + + /** Estado atual do semáforo (VERDE ou VERMELHO) */ private TrafficLightState state; - // --- Vehicle management --- - - /** - * The queue of vehicles waiting at this light. - * {@link LinkedList} is used as it's a standard {@link Queue} implementation. - */ + /** Fila de veículos à espera neste semáforo */ private final Queue queue; - // --- Synchronization primitives (for thread-safety) --- - /** - * A lock to protect all mutable state ({@link #queue} and {@link #state}) - * from concurrent access. Any method reading or writing these fields - * *must* acquire this lock first. + * Lock para proteger o estado mutável ({@link #queue} e {@link #state}) + * de acesso concorrente. */ private final Lock lock; - - /** - * A condition variable for a potential concurrent model. - * It could be used to signal threads (e.g., a controller) that - * a new vehicle has been added to the queue. - * (Not used in the current discrete-event engine). - */ + + /** Variável de condição para sinalizar adição de veículos (uso futuro) */ private final Condition vehicleAdded; - - /** - * A condition variable for a potential concurrent model. - * It could be used to signal waiting vehicle threads that the - * light has just turned GREEN. - * (Not used in the current discrete-event engine). - */ + + /** Variável de condição para sinalizar que o semáforo ficou verde (uso futuro) */ private final Condition lightGreen; - // --- Timing configuration --- - - /** - * The duration (in seconds) this light stays GREEN. - */ + /** Duração (segundos) que o semáforo permanece VERDE */ private double greenTime; - - /** - * The duration (in seconds) this light stays RED. - */ + + /** Duração (segundos) que o semáforo permanece VERMELHO */ private double redTime; - // --- Statistics --- - - /** - * Counter for the total number of vehicles that have - * been dequeued (processed) by this light. - */ + /** Número total de veículos processados por este semáforo */ private int totalVehiclesProcessed; - - /** - * Track when vehicles arrive at this light for wait time calculation. - * Maps vehicle ID to arrival timestamp (milliseconds). - */ - private final Map vehicleArrivalTimes; /** - * Constructs a new TrafficLight. + * Regista quando os veículos chegam ao semáforo para cálculo do tempo de espera. + * Mapeia ID do veículo para tempo de simulação de chegada (segundos). + */ + private final Map vehicleArrivalTimes; + + /** + * Cria um novo semáforo. * - * @param id The unique ID (e.g., "Cr1-N"). - * @param direction The direction (e.g., "North"). - * @param greenTime The duration of the GREEN state in seconds. - * @param redTime The duration of the RED state in seconds. + * @param id identificador único (ex: "Cr1-N") + * @param direction direção controlada (ex: "Norte") + * @param greenTime duração do estado VERDE em segundos + * @param redTime duração do estado VERMELHO em segundos */ public TrafficLight(String id, String direction, double greenTime, double redTime) { this.id = id; this.direction = direction; - this.state = TrafficLightState.RED; // All lights start RED + this.state = TrafficLightState.RED; this.queue = new LinkedList<>(); - // Initialize synchronization objects this.lock = new ReentrantLock(); this.vehicleAdded = lock.newCondition(); this.lightGreen = lock.newCondition(); @@ -128,205 +83,187 @@ public class TrafficLight { } /** - * Adds a vehicle to the *end* of the waiting queue. - * This method is thread-safe. - * - * @param vehicle The {@link Vehicle} to add. + * Coloca um veículo na fila deste semáforo. + * + * Registamos a hora de chegada para podermos calcular mais tarde quanto tempo o + * veículo esperou. + * + * @param vehicle O veículo que chega ao semáforo. + * @param simulationTime O tempo de simulação atual (em segundos). */ - public void addVehicle(Vehicle vehicle) { - lock.lock(); // Acquire the lock + public void addVehicle(Vehicle vehicle, double simulationTime) { + lock.lock(); try { - queue.offer(vehicle); // Add vehicle to queue - vehicleArrivalTimes.put(vehicle.getId(), System.currentTimeMillis()); - vehicleAdded.signalAll(); // Signal (for concurrent models) + queue.offer(vehicle); + vehicleArrivalTimes.put(vehicle.getId(), simulationTime); + vehicleAdded.signalAll(); } finally { - lock.unlock(); // Always release the lock + lock.unlock(); } } /** - * Removes and returns the {@link Vehicle} from the *front* of the queue. - * * This only succeeds if: - * 1. The light's state is {@link TrafficLightState#GREEN}. - * 2. The queue is not empty. - * * If these conditions are not met, it returns {@code null}. - * This method is thread-safe. - * - * @return The {@link Vehicle} at the front of the queue, or {@code null} - * if the light is RED or the queue is empty. + * Remove um veículo da fila para travessia. + * + *

Só remove se:

+ *
    + *
  • O semáforo estiver VERDE
  • + *
  • Existir pelo menos um veículo na fila
  • + *
+ * + *

Atualiza automaticamente as estatísticas de tempo de espera do veículo.

+ * + * @param simulationTime O tempo de simulação atual (em segundos). + * @return o veículo que vai atravessar, ou null se não for possível */ - public Vehicle removeVehicle() { - lock.lock(); // Acquire the lock + public Vehicle removeVehicle(double simulationTime) { + lock.lock(); try { if (state == TrafficLightState.GREEN && !queue.isEmpty()) { - Vehicle vehicle = queue.poll(); // Remove vehicle from queue + Vehicle vehicle = queue.poll(); if (vehicle != null) { totalVehiclesProcessed++; - - // Calculate wait time (time spent in queue) - Long arrivalTime = vehicleArrivalTimes.remove(vehicle.getId()); + + Double arrivalTime = vehicleArrivalTimes.remove(vehicle.getId()); if (arrivalTime != null) { - double waitTimeSeconds = (System.currentTimeMillis() - arrivalTime) / 1000.0; + double waitTimeSeconds = simulationTime - arrivalTime; vehicle.addWaitingTime(waitTimeSeconds); } } return vehicle; } - return null; // Light is RED or queue is empty + return null; } finally { - lock.unlock(); // Always release the lock + lock.unlock(); } } /** - * Changes the light’s state (e.g., RED -> GREEN). - * If the new state is GREEN, it signals any waiting threads - * (for a potential concurrent model). - * This method is thread-safe. - * - * @param newState The {@link TrafficLightState} to set. + * Muda o estado do semáforo. + * + * @param newState novo estado (VERDE ou VERMELHO) */ public void changeState(TrafficLightState newState) { - lock.lock(); // Acquire the lock + lock.lock(); try { this.state = newState; if (newState == TrafficLightState.GREEN) { - lightGreen.signalAll(); // Signal (for concurrent models) + lightGreen.signalAll(); } } finally { - lock.unlock(); // Always release the lock + lock.unlock(); } } /** - * Returns how many vehicles are currently in the queue. - * This method is thread-safe. - * * @return The size of the queue. + * Retorna quantos veículos estão atualmente na fila. + * Método thread-safe. + * + * @return tamanho da fila */ public int getQueueSize() { - lock.lock(); // Acquire the lock + lock.lock(); try { return queue.size(); } finally { - lock.unlock(); // Always release the lock + lock.unlock(); } } /** - * Checks whether the queue is empty. - * This method is thread-safe. + * Verifica se a fila está vazia. + * Método thread-safe. * - * @return {@code true} if the queue has no vehicles, {@code false} otherwise. + * @return {@code true} se não houver veículos, {@code false} caso contrário */ public boolean isQueueEmpty() { - lock.lock(); // Acquire the lock + lock.lock(); try { return queue.isEmpty(); } finally { - lock.unlock(); // Always release the lock + lock.unlock(); } } - // --- Getters & Setters --- - - /** - * @return The unique ID of this light (e.g., "Cr1-N"). - */ + /** @return identificador único do semáforo */ public String getId() { return id; } - /** - * @return The direction this light controls (e.g., "North"). - */ + /** @return direção controlada por este semáforo */ public String getDirection() { return direction; } /** - * Gets the current state of the light (GREEN or RED). - * This method is thread-safe. + * Obtém o estado atual do semáforo. + * Método thread-safe. * - * @return The current {@link TrafficLightState}. + * @return estado atual (VERDE ou VERMELHO) */ public TrafficLightState getState() { - lock.lock(); // Acquire the lock + lock.lock(); try { return state; } finally { - lock.unlock(); // Always release the lock + lock.unlock(); } } - /** - * @return The configured GREEN light duration in seconds. - */ + /** @return duração configurada do sinal verde em segundos */ public double getGreenTime() { return greenTime; } /** - * Sets the GREEN light duration. - * @param greenTime The new duration in seconds. + * Define a duração do sinal verde. + * + * @param greenTime nova duração em segundos */ public void setGreenTime(double greenTime) { this.greenTime = greenTime; } - /** - * @return The configured RED light duration in seconds. - */ + /** @return duração configurada do sinal vermelho em segundos */ public double getRedTime() { return redTime; } /** - * Sets the RED light duration. - * @param redTime The new duration in seconds. + * Define a duração do sinal vermelho. + * + * @param redTime nova duração em segundos */ public void setRedTime(double redTime) { this.redTime = redTime; } - /** - * @return The total number of vehicles processed (dequeued) by this light. - */ + /** @return número total de veículos processados por este semáforo */ public int getTotalVehiclesProcessed() { - // Note: This read is not locked, assuming it's okay - // for it to be "eventually consistent" for stats. - // For strict accuracy, it should also be locked. return totalVehiclesProcessed; } - /** - * @return The {@link Lock} object for advanced synchronization. - */ + /** @return objeto {@link Lock} para sincronização avançada */ public Lock getLock() { return lock; } - /** - * @return The {@link Condition} for vehicle additions. - */ + /** @return condição para adição de veículos */ public Condition getVehicleAdded() { return vehicleAdded; } - /** - * @return The {@link Condition} for the light turning green. - */ + /** @return condição para semáforo ficar verde */ public Condition getLightGreen() { return lightGreen; } - /** - * @return A string summary of the light's current state. - */ + /** @return representação textual do estado atual do semáforo */ @Override public String toString() { return String.format( - "TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}", - id, direction, getState(), getQueueSize() // Use getters for thread-safety + "TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}", + id, direction, getState(), getQueueSize() ); } } \ No newline at end of file diff --git a/main/src/main/java/sd/model/TrafficLightState.java b/main/src/main/java/sd/model/TrafficLightState.java index c21d6e4..aab7682 100644 --- a/main/src/main/java/sd/model/TrafficLightState.java +++ b/main/src/main/java/sd/model/TrafficLightState.java @@ -1,17 +1,13 @@ package sd.model; /** - * Enumeration representing the two possible states of a {@link TrafficLight}. + * Estados possíveis de um semáforo ({@link TrafficLight}). */ public enum TrafficLightState { - /** - * The light is GREEN, allowing vehicles to pass (be dequeued). - */ + /** Sinal verde - veículos podem passar */ GREEN, - /** - * The light is RED, blocking vehicles (they remain in the queue). - */ + /** Sinal vermelho - veículos aguardam na fila */ RED } \ No newline at end of file diff --git a/main/src/main/java/sd/model/Vehicle.java b/main/src/main/java/sd/model/Vehicle.java index 2ee7d23..51ed1de 100644 --- a/main/src/main/java/sd/model/Vehicle.java +++ b/main/src/main/java/sd/model/Vehicle.java @@ -5,94 +5,84 @@ import java.util.ArrayList; import java.util.List; /** - * Represents a single vehicle moving through the simulation. - * - * This class is a data object that holds the state of a vehicle, including: - * - Its unique ID, type, and entry time. - * - Its complete, pre-determined {@code route} (a list of intersection IDs). - * - Its current position in the route ({@code currentRouteIndex}). - * - Metrics for total time spent waiting at red lights and time spent crossing. - * * This object is passed around the simulation, primarily inside message - * payloads and stored in {@link TrafficLight} queues. - * * Implements {@link Serializable} so it can be sent between processes - * or nodes (e.g., over a socket in a distributed version of the simulation). + * Representa um veículo que se move pela rede de interseções. + * + *

+ * Esta classe é o "gémeo digital" de um carro, mota ou camião. + * Mantém toda a informação necessária: + *

+ *
    + *
  • Identificação e tipo do veículo
  • + *
  • Rota completa a percorrer
  • + *
  • Métricas de tempo (espera, travessia, total)
  • + *
+ * + *

+ * O objeto é serializado e enviado pela rede à medida que o veículo + * se move entre processos distribuídos. + *

*/ public class Vehicle implements Serializable { private static final long serialVersionUID = 1L; - // --- Identity and configuration --- - - /** - * Unique identifier for the vehicle (e.g., "V1", "V2"). - */ + /** Identificador único do veículo (ex: "V1", "V2") */ private final String id; - /** - * The type of vehicle (BIKE, LIGHT, HEAVY). - */ + /** Tipo de veículo (BIKE, LIGHT, HEAVY) */ private final VehicleType type; - /** - * The simulation time (in seconds) when the vehicle was generated. - */ + /** Tempo de simulação (em segundos) em que o veículo foi gerado */ private final double entryTime; /** - * The complete, ordered list of destinations (intersection IDs and the - * final exit "S"). Example: ["Cr1", "Cr3", "S"]. + * Lista ordenada completa de destinos (IDs de interseções e saída "S"). + * Exemplo: ["Cr1", "Cr3", "S"] */ private final List route; /** - * An index that tracks the vehicle's progress along its {@link #route}. - * {@code route.get(currentRouteIndex)} is the vehicle's *current* - * destination (i.e., the one it is traveling *towards* or *arriving at*). + * Índice que acompanha o progresso do veículo ao longo da {@link #route}. + * {@code route.get(currentRouteIndex)} é o destino *atual* do veículo. */ private int currentRouteIndex; - // --- Metrics --- - /** - * The total accumulated time (in seconds) this vehicle has spent - * waiting at red lights. + * Tempo total acumulado (segundos) que o veículo passou à espera em semáforos + * vermelhos */ private double totalWaitingTime; /** - * The total accumulated time (in seconds) this vehicle has spent - * actively crossing intersections. + * Tempo total acumulado (segundos) que o veículo passou a atravessar + * interseções */ private double totalCrossingTime; /** - * Constructs a new Vehicle. - * - * @param id The unique ID for the vehicle. - * @param type The {@link VehicleType}. - * @param entryTime The simulation time when the vehicle is created. - * @param route The complete list of destination IDs (e.t., ["Cr1", "Cr2", - * "S"]). + * Cria um novo veículo pronto para se fazer à estrada. + * + * @param id Identificador único (ex: "V1"). + * @param type O tipo de veículo (determina velocidade/tamanho). + * @param entryTime Quando este veículo entrou na simulação (segundos). + * @param route A lista ordenada de paragens (Interseções -> Saída). */ public Vehicle(String id, VehicleType type, double entryTime, List route) { this.id = id; this.type = type; this.entryTime = entryTime; - // Create a copy of the route list to ensure immutability this.route = new ArrayList<>(route); - this.currentRouteIndex = 0; // Starts at the first destination + this.currentRouteIndex = 0; this.totalWaitingTime = 0.0; this.totalCrossingTime = 0.0; } /** - * Advances the vehicle to the next stop in its route by - * incrementing the {@link #currentRouteIndex}. - * * This is typically called *after* a vehicle *arrives* at an intersection, - * to set its *next* destination before it is queued. - * - * @return {@code true} if there is still at least one more destination - * in the route, {@code false} if the vehicle has passed its - * final destination. + * Move o GPS interno do veículo para o próximo destino. + * + * Chame isto quando um veículo chega a uma interseção para atualizar para onde + * deve ir a seguir. + * + * @return true se houver mais paragens, false se a viagem terminou. */ public boolean advanceRoute() { currentRouteIndex++; @@ -100,116 +90,90 @@ public class Vehicle implements Serializable { } /** - * Gets the current destination (the next intersection or exit) that - * the vehicle is heading towards. + * Obtém o destino atual (próxima interseção ou saída) para onde o veículo se + * dirige. * - * @return The ID of the current destination (e.g., "Cr1"), or - * {@code null} if the route is complete. + * @return ID do destino atual (ex: "Cr1"), ou {@code null} se a rota terminou */ public String getCurrentDestination() { return (currentRouteIndex < route.size()) ? route.get(currentRouteIndex) : null; } /** - * Checks if the vehicle has completed its entire route. + * Verifica se o veículo completou toda a sua rota. * - * @return {@code true} if the route index is at or past the end - * of the route list, {@code false} otherwise. + * @return {@code true} se chegou ao fim da rota, {@code false} caso contrário */ public boolean hasReachedEnd() { return currentRouteIndex >= route.size(); } - // --- Getters and metrics management --- - - /** - * @return The vehicle's unique ID. - */ + /** @return identificador único do veículo */ public String getId() { return id; } - /** - * @return The vehicle's {@link VehicleType}. - */ + /** @return tipo do veículo */ public VehicleType getType() { return type; } - /** - * @return The simulation time when the vehicle entered the system. - */ + /** @return tempo de simulação em que o veículo entrou no sistema */ public double getEntryTime() { return entryTime; } - /** - * @return A *copy* of the vehicle's complete route. - */ + /** @return cópia da rota completa do veículo */ public List getRoute() { - // Return a copy to prevent external modification return new ArrayList<>(route); } - /** - * @return The current index pointing to the vehicle's destination in its route - * list. - */ + /** @return índice atual apontando para o destino do veículo na sua rota */ public int getCurrentRouteIndex() { return currentRouteIndex; } - /** - * @return The total accumulated waiting time in seconds. - */ + /** @return tempo total acumulado de espera em segundos */ public double getTotalWaitingTime() { return totalWaitingTime; } /** - * Adds a duration to the vehicle's total waiting time. - * This is called by the simulation engine when a vehicle - * starts crossing an intersection. + * Adiciona uma duração ao tempo total de espera do veículo. + * Chamado quando um veículo começa a atravessar uma interseção. * - * @param time The duration (in seconds) to add. + * @param time duração (em segundos) a adicionar */ public void addWaitingTime(double time) { totalWaitingTime += time; } - /** - * @return The total accumulated crossing time in seconds. - */ + /** @return tempo total acumulado de travessia em segundos */ public double getTotalCrossingTime() { return totalCrossingTime; } /** - * Adds a duration to the vehicle's total crossing time. - * This is called by the simulation engine when a vehicle - * finishes crossing an intersection. + * Adiciona uma duração ao tempo total de travessia do veículo. + * Chamado quando um veículo termina de atravessar uma interseção. * - * @param time The duration (in seconds) to add. + * @param time duração (em segundos) a adicionar */ public void addCrossingTime(double time) { totalCrossingTime += time; } /** - * Calculates the vehicle's total time spent in the system so far. - * This is a "live" calculation. + * Calcula o tempo total que o veículo passou no sistema até agora. * - * @param currentTime The current simulation time. - * @return The total elapsed time (in seconds) since the vehicle - * was generated ({@code currentTime - entryTime}). + * @param currentTime tempo atual da simulação + * @return tempo total decorrido (em segundos) desde que o veículo foi gerado */ public double getTotalTravelTime(double currentTime) { return currentTime - entryTime; } - /** - * @return A string summary of the vehicle's current state. - */ + /** @return representação textual do estado atual do veículo */ @Override public String toString() { return String.format( diff --git a/main/src/main/java/sd/model/VehicleType.java b/main/src/main/java/sd/model/VehicleType.java index fce00d0..e3aa195 100644 --- a/main/src/main/java/sd/model/VehicleType.java +++ b/main/src/main/java/sd/model/VehicleType.java @@ -1,27 +1,19 @@ package sd.model; /** - * Enumeration representing the different types of vehicles in the simulation. - * Each type can have different properties, such as crossing time - * and generation probability, defined in {@link sd.config.SimulationConfig}. + * Enumeração dos diferentes tipos de veículos na simulação. + * + *

Cada tipo pode ter propriedades diferentes como tempo de travessia + * e probabilidade de geração, definidas na {@link sd.config.SimulationConfig}.

*/ public enum VehicleType { - /** - * A bike or motorcycle. - * Typically has a short crossing time. - */ + /** Bicicleta ou motocicleta - tempo de travessia curto */ BIKE, - /** - * A standard light vehicle, such as a car. - * This is usually the most common type. - */ + /** Veículo ligeiro padrão (carro) - tipo mais comum */ LIGHT, - /** - * A heavy vehicle, such as a truck or bus. - * Typically has a long crossing time. - */ + /** Veículo pesado (camião ou autocarro) - tempo de travessia longo */ HEAVY } \ No newline at end of file diff --git a/main/src/main/java/sd/protocol/MessageProtocol.java b/main/src/main/java/sd/protocol/MessageProtocol.java index 2ec121f..9a4ba27 100644 --- a/main/src/main/java/sd/protocol/MessageProtocol.java +++ b/main/src/main/java/sd/protocol/MessageProtocol.java @@ -5,38 +5,41 @@ import java.io.Serializable; import sd.model.MessageType; /** - * Interface defining the contract for all messages exchanged in the simulator. - * Ensures that any message can be identified and routed. - * * This interface extends Serializable to allow objects that implement it - * to be sent over Sockets (ObjectOutputStream). - * + * Contrato para todas as mensagens trocadas no simulador. + * + *

Garante que mensagens podem ser identificadas e encaminhadas. + * Extende Serializable para permitir envio via sockets. */ public interface MessageProtocol extends Serializable { /** - * Returns the type of the message, indicating its purpose. - * @return The MessageType (e.g., VEHICLE_TRANSFER, STATS_UPDATE). + * Tipo da mensagem, indicando o seu propósito. + * @return tipo (ex: VEHICLE_TRANSFER, STATS_UPDATE) */ MessageType getType(); /** - * Returns the data object (payload) that this message carries. - * The type of object will depend on the MessageType. - * * - If getType() == VEHICLE_TRANSFER, the payload will be a {@link sd.model.Vehicle} object. - * - If getType() == STATS_UPDATE, the payload will be a statistics object. - * * @return The data object (payload), which must also be Serializable. + * Dados (payload) que esta mensagem transporta. + * + *

Tipo depende do MessageType: + *

    + *
  • VEHICLE_TRANSFER → objeto Vehicle + *
  • STATS_UPDATE → objeto de estatísticas + *
+ * + * @return payload (deve ser Serializable) */ Object getPayload(); /** - * Returns the ID of the node (Process) that sent this message. - * @return String (e.g., "Cr1", "Cr5", "S"). + * ID do nó (processo) que enviou a mensagem. + * @return ID de origem (ex: "Cr1", "Cr5", "S") */ String getSourceNode(); /** - * Returns the ID of the destination node (Process) for this message. - * @return String (e.g., "Cr2", "DashboardServer"). + * ID do nó de destino. + * @return ID de destino (ex: "Cr2", "DashboardServer") */ String getDestinationNode(); } \ No newline at end of file diff --git a/main/src/main/java/sd/protocol/SocketConnection.java b/main/src/main/java/sd/protocol/SocketConnection.java index 65dd4e5..16dcdd4 100644 --- a/main/src/main/java/sd/protocol/SocketConnection.java +++ b/main/src/main/java/sd/protocol/SocketConnection.java @@ -16,10 +16,17 @@ import sd.serialization.MessageSerializer; import sd.serialization.SerializationException; import sd.serialization.SerializerFactory; - /** - * Wrapper class that simplifies communication via Sockets. - * Includes connection retry logic for robustness. + * Wrapper de alto nível para gestão robusta de conexões TCP. + *

+ * Esta classe abstrai a complexidade da API nativa {@link java.net.Socket}, oferecendo: + *

    + *
  1. Resiliência: Lógica de reconexão automática (Retry Loop) no arranque, crucial para sistemas + * distribuídos onde a ordem de inicialização dos nós não é garantida.
  2. + *
  3. Framing: Implementação transparente do protocolo "Length-Prefix" (4 bytes de tamanho + payload), + * resolvendo o problema de fragmentação de stream TCP.
  4. + *
  5. Serialização: Integração direta com a camada de serialização JSON.
  6. + *
*/ public class SocketConnection implements Closeable { @@ -49,22 +56,24 @@ public class SocketConnection implements Closeable { */ private final MessageSerializer serializer; - // --- Configuration for Retry Logic --- - /** Maximum number of connection attempts. */ + /** Número máximo de tentativas de ligação antes de desistir (Fail-fast). */ private static final int MAX_RETRIES = 5; - /** Delay between retry attempts in milliseconds. */ + + /** Janela de espera (backoff) linear entre tentativas (em milissegundos). */ private static final long RETRY_DELAY_MS = 1000; /** - * Constructor for the "Client" (who initiates the connection). - * Tries to connect to a process that is already listening (Server). - * Includes retry logic in case of initial connection failure. + * Construtor para clientes (Active Open). + * Tenta estabelecer uma conexão TCP com um servidor, aplicando lógica de retry. + *

+ * Este comportamento é vital quando o processo Coordenador inicia antes das Interseções estarem + * prontas para aceitar conexões ({@code accept()}). * - * @param host The host address (e.g., "localhost" from your simulation.properties) - * @param port The port (e.g., 8001 from your simulation.properties) - * @throws IOException If connection fails after all retries. - * @throws UnknownHostException If the host is not found (this error usually doesn't need retry). - * @throws InterruptedException If the thread is interrupted while waiting between retries. + * @param host Endereço do nó de destino (ex: "localhost"). + * @param port Porta de serviço. + * @throws IOException Se a conexão falhar após todas as {@code MAX_RETRIES} tentativas. + * @throws UnknownHostException Se o DNS não resolver o hostname. + * @throws InterruptedException Se a thread for interrompida durante o sleep de retry. */ public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException { Socket tempSocket = null; @@ -75,7 +84,7 @@ public class SocketConnection implements Closeable { // --- Retry Loop --- for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { - // Try to establish the connection + // Try to establish the connection (SYN -> SYN-ACK -> ACK) tempSocket = new Socket(host, port); // If successful, break out of the retry loop @@ -84,17 +93,17 @@ public class SocketConnection implements Closeable { break; } catch (ConnectException | SocketTimeoutException e) { - // These are common errors indicating the server might not be ready. + // Common errors: "Connection refused" (server not up) or "Timeout" (firewall/network) lastException = e; System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n", attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS); if (attempt < MAX_RETRIES) { - // Wait before the next attempt + // Blocking wait before next attempt TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS); } } catch (IOException e) { - // Other IOExceptions might be more permanent, but we retry anyway. + // Other IO errors lastException = e; System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n", attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS); @@ -104,51 +113,49 @@ public class SocketConnection implements Closeable { } } // --- End of Retry Loop --- - // If after all retries tempSocket is still null, it means connection failed permanently. + // Final validation if (tempSocket == null) { System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES); if (lastException != null) { - throw lastException; // Throw the last exception encountered + throw lastException; // Propagate the root cause } else { - // Should not happen if loop ran, but as a fallback throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts, reason unknown."); } } - // If connection was successful, assign to final variable and create streams + // Initialize streams this.socket = tempSocket; - this.outputStream = socket.getOutputStream(); this.inputStream = socket.getInputStream(); this.serializer = SerializerFactory.createDefault(); - } - /** - * Constructor for the "Server" (who accepts the connection). - * Receives a Socket that has already been accepted by a ServerSocket. - * No retry logic needed here as the connection is already established. + * Construtor para servidores (Passive Open). + * Envolve um socket já conectado (retornado por {@code serverSocket.accept()}). + * Não necessita de retry logic pois a conexão física já existe. * - * @param acceptedSocket The Socket returned by serverSocket.accept(). - * @throws IOException If stream creation fails. + * @param acceptedSocket O socket ativo retornado pelo SO. + * @throws IOException Se falhar a obtenção dos streams de I/O. */ public SocketConnection(Socket acceptedSocket) throws IOException { this.socket = acceptedSocket; this.outputStream = socket.getOutputStream(); this.inputStream = socket.getInputStream(); this.serializer = SerializerFactory.createDefault(); - } /** - * Sends (serializes) a MessageProtocol object over the socket. + * Serializa e transmite uma mensagem através do canal. + *

+ * Utiliza sincronização ({@code synchronized}) para garantir que escritas concorrentes + * na mesma conexão não corrompem a stream de bytes (thread-safety). * - * @param message The "envelope" (which contains the Vehicle) to be sent. - * @throws IOException If writing to the stream fails or socket is not connected. + * @param message O objeto de protocolo a enviar. + * @throws IOException Se o socket estiver fechado ou ocorrer erro de escrita. */ public synchronized void sendMessage(MessageProtocol message) throws IOException { - if (socket == null || !socket.isConnected()) { + if (socket == null || !socket.isConnected()) { throw new IOException("Socket is not connected"); } @@ -156,11 +163,11 @@ public class SocketConnection implements Closeable { // Serializa para bytes JSON byte[] data = serializer.serialize(message); - // Write 4-byte length prefix + // Write 4-byte length prefix (Framing) DataOutputStream dataOut = new DataOutputStream(outputStream); dataOut.writeInt(data.length); dataOut.write(data); - dataOut.flush(); + dataOut.flush(); // Force transmission immediately } catch (SerializationException e) { throw new IOException("Failed to serialize message", e); @@ -168,11 +175,14 @@ public class SocketConnection implements Closeable { } /** - * Tries to read (deserialize) a MessageProtocol object from the socket. + * Bloqueia à espera de uma mensagem completa do socket. + *

+ * Lê primeiro o cabeçalho de tamanho (4 bytes) e depois o payload exato, + * garantindo que processa mensagens completas mesmo se chegarem fragmentadas em múltiplos pacotes TCP. * - * @return The "envelope" (MessageProtocol) that was received. - * @throws IOException If the connection is lost, the stream is corrupted, or socket is not connected. - * @throws ClassNotFoundException If the received object is unknown. + * @return O objeto {@link MessageProtocol} reconstruído. + * @throws IOException Se a conexão for perdida (EOF) ou o stream corrompido. + * @throws ClassNotFoundException Se o tipo desserializado não for encontrado no classpath. */ public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException { if (socket == null || !socket.isConnected()) { @@ -184,15 +194,16 @@ public class SocketConnection implements Closeable { DataInputStream dataIn = new DataInputStream(inputStream); int length = dataIn.readInt(); - if (length <= 0 || length > 10_000_000) { // Sanity check (10MB max) + // Sanity check para evitar OutOfMemory em caso de corrupção de stream + if (length <= 0 || length > 10_000_000) { // Max 10MB payload throw new IOException("Invalid message length: " + length); } - + // Ler dados exatos da mensagem byte[] data = new byte[length]; dataIn.readFully(data); - // JSON deserialization- use concrete Message class, not interface + // Deserialize do JSON - força o tipo concreto Message return serializer.deserialize(data, sd.model.Message.class); } catch (SerializationException e) { @@ -200,8 +211,9 @@ public class SocketConnection implements Closeable { } } - /** - * Closes the socket and all streams (Input and Output). + /** + * Encerra a conexão e liberta os descritores de ficheiro. + * Operação idempotente. */ @Override public void close() throws IOException { @@ -211,8 +223,8 @@ public class SocketConnection implements Closeable { } /** - * Checks the current state of the connection. - * @return {@code true} if the socket is connected and currently open, {@code false} otherwise. + * Verifica o estado operacional da conexão. + * @return true se o socket está aberto e conectado. */ public boolean isConnected() { return socket != null && socket.isConnected() && !socket.isClosed(); diff --git a/main/src/main/java/sd/routing/LeastCongestedRouteSelector.java b/main/src/main/java/sd/routing/LeastCongestedRouteSelector.java new file mode 100644 index 0000000..24f91d4 --- /dev/null +++ b/main/src/main/java/sd/routing/LeastCongestedRouteSelector.java @@ -0,0 +1,151 @@ +package sd.routing; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Implementação da política de roteamento por menor congestionamento. + * + *

Esta política escolhe dinamicamente a rota que passa pelos cruzamentos + * menos congestionados, com base no tamanho atual das filas em cada interseção. + * É uma política dinâmica que adapta as decisões ao estado da rede.

+ * + *

Objetivo: Distribuir o tráfego pela rede, evitando bottlenecks e + * minimizando o tempo de espera total.

+ * + *

Algoritmo:

+ *
    + *
  1. Para cada rota possível, calcula a carga total (soma das filas)
  2. + *
  3. Escolhe a rota com menor carga total
  4. + *
  5. Em caso de empate ou falta de informação, usa a rota mais curta
  6. + *
+ */ +public class LeastCongestedRouteSelector implements RouteSelector { + + /** Rotas possíveis a partir do ponto de entrada E1 */ + private final List> e1Routes; + /** Rotas possíveis a partir do ponto de entrada E2 */ + private final List> e2Routes; + /** Rotas possíveis a partir do ponto de entrada E3 */ + private final List> e3Routes; + + /** + * Cria um novo seletor de rotas baseado em menor congestionamento. + */ + public LeastCongestedRouteSelector() { + this.e1Routes = new ArrayList<>(); + this.e2Routes = new ArrayList<>(); + this.e3Routes = new ArrayList<>(); + initializeRoutes(); + } + + /** + * Inicializa as rotas possíveis para cada ponto de entrada. + */ + private void initializeRoutes() { + // Rotas de E1 (entrada Norte) + e1Routes.add(Arrays.asList("Cr1", "Cr4", "Cr5", "S")); + e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr5", "S")); + e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr3", "S")); + + // Rotas de E2 (entrada Oeste) + e2Routes.add(Arrays.asList("Cr2", "Cr5", "S")); + e2Routes.add(Arrays.asList("Cr2", "Cr3", "S")); + e2Routes.add(Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S")); + + // Rotas de E3 (entrada Sul) + e3Routes.add(Arrays.asList("Cr3", "S")); + e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr5", "S")); + e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S")); + } + + @Override + public List selectRoute(String entryPoint, Map queueSizes) { + List> availableRoutes = getRoutesForEntryPoint(entryPoint); + + // Se não temos informação sobre filas, usa a rota mais curta como fallback + if (queueSizes == null || queueSizes.isEmpty()) { + return selectShortestRoute(availableRoutes); + } + + // Calcula a carga de cada rota e escolhe a menos congestionada + List bestRoute = null; + int minLoad = Integer.MAX_VALUE; + + for (List route : availableRoutes) { + int routeLoad = calculateRouteLoad(route, queueSizes); + + if (routeLoad < minLoad) { + minLoad = routeLoad; + bestRoute = route; + } + } + + // Fallback: se não conseguimos calcular carga, usa a primeira rota + if (bestRoute == null) { + bestRoute = availableRoutes.get(0); + } + + return new ArrayList<>(bestRoute); + } + + /** + * Calcula a carga total de uma rota (soma do tamanho das filas em todos os cruzamentos). + * + * @param route rota a avaliar + * @param queueSizes mapa com tamanho das filas por interseção + * @return carga total da rota (soma das filas) + */ + private int calculateRouteLoad(List route, Map queueSizes) { + int totalLoad = 0; + + for (String intersection : route) { + // Ignora "S" (saída) e apenas conta cruzamentos reais + if (!intersection.equals("S") && queueSizes.containsKey(intersection)) { + totalLoad += queueSizes.get(intersection); + } + } + + return totalLoad; + } + + /** + * Seleciona a rota mais curta (menor número de nós) como fallback. + * + * @param routes lista de rotas disponíveis + * @return a rota mais curta + */ + private List selectShortestRoute(List> routes) { + List shortest = routes.get(0); + + for (List route : routes) { + if (route.size() < shortest.size()) { + shortest = route; + } + } + + return new ArrayList<>(shortest); + } + + /** + * Obtém as rotas disponíveis para um ponto de entrada. + * + * @param entryPoint ponto de entrada (E1, E2 ou E3) + * @return lista de rotas disponíveis + */ + private List> getRoutesForEntryPoint(String entryPoint) { + switch (entryPoint.toUpperCase()) { + case "E1": + return e1Routes; + case "E2": + return e2Routes; + case "E3": + return e3Routes; + default: + System.err.printf("Unknown entry point: %s, defaulting to E1%n", entryPoint); + return e1Routes; + } + } +} diff --git a/main/src/main/java/sd/routing/RandomRouteSelector.java b/main/src/main/java/sd/routing/RandomRouteSelector.java new file mode 100644 index 0000000..5b9df21 --- /dev/null +++ b/main/src/main/java/sd/routing/RandomRouteSelector.java @@ -0,0 +1,122 @@ +package sd.routing; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Implementação da política de roteamento aleatória (baseline). + * + *

Esta política seleciona rotas com base em probabilidades predefinidas, + * sem considerar o estado atual da rede. É a implementação de referência + * para comparação com outras políticas.

+ * + *

As rotas são organizadas por ponto de entrada (E1, E2, E3) e cada rota + * tem uma probabilidade de seleção associada.

+ */ +public class RandomRouteSelector implements RouteSelector { + + /** Rotas possíveis a partir do ponto de entrada E1 */ + private final List e1Routes; + /** Rotas possíveis a partir do ponto de entrada E2 */ + private final List e2Routes; + /** Rotas possíveis a partir do ponto de entrada E3 */ + private final List e3Routes; + + /** + * Cria um novo seletor de rotas aleatórias com rotas predefinidas. + */ + public RandomRouteSelector() { + this.e1Routes = new ArrayList<>(); + this.e2Routes = new ArrayList<>(); + this.e3Routes = new ArrayList<>(); + initializePossibleRoutes(); + } + + /** + * Define todas as rotas possíveis que os veículos podem tomar. + * As rotas são organizadas por ponto de entrada (E1, E2, E3). + * Cada rota tem uma probabilidade que determina a frequência com que é escolhida. + */ + private void initializePossibleRoutes() { + // Rotas de E1 (entrada Norte) + e1Routes.add(new RouteWithProbability( + Arrays.asList("Cr1", "Cr4", "Cr5", "S"), 0.34)); + e1Routes.add(new RouteWithProbability( + Arrays.asList("Cr1", "Cr2", "Cr5", "S"), 0.33)); + e1Routes.add(new RouteWithProbability( + Arrays.asList("Cr1", "Cr2", "Cr3", "S"), 0.33)); + + // Rotas de E2 (entrada Oeste) + e2Routes.add(new RouteWithProbability( + Arrays.asList("Cr2", "Cr5", "S"), 0.34)); + e2Routes.add(new RouteWithProbability( + Arrays.asList("Cr2", "Cr3", "S"), 0.33)); + e2Routes.add(new RouteWithProbability( + Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); + + // Rotas de E3 (entrada Sul) + e3Routes.add(new RouteWithProbability( + Arrays.asList("Cr3", "S"), 0.34)); + e3Routes.add(new RouteWithProbability( + Arrays.asList("Cr3", "Cr2", "Cr5", "S"), 0.33)); + e3Routes.add(new RouteWithProbability( + Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); + } + + @Override + public List selectRoute(String entryPoint, Map queueSizes) { + // Ignora queueSizes - seleção aleatória não depende do estado da rede + + List selectedRoutes = getRoutesForEntryPoint(entryPoint); + + // Seleciona uma rota baseada em probabilidades cumulativas + double rand = Math.random(); + double cumulative = 0.0; + + for (RouteWithProbability routeWithProb : selectedRoutes) { + cumulative += routeWithProb.probability; + if (rand <= cumulative) { + // Retorna uma cópia da rota para prevenir modificações + return new ArrayList<>(routeWithProb.route); + } + } + + // Fallback: retorna a primeira rota + return new ArrayList<>(selectedRoutes.get(0).route); + } + + /** + * Obtém as rotas disponíveis para um ponto de entrada. + * + * @param entryPoint ponto de entrada (E1, E2 ou E3) + * @return lista de rotas com probabilidades + */ + private List getRoutesForEntryPoint(String entryPoint) { + switch (entryPoint.toUpperCase()) { + case "E1": + return e1Routes; + case "E2": + return e2Routes; + case "E3": + return e3Routes; + default: + System.err.printf("Unknown entry point: %s, defaulting to E1%n", entryPoint); + return e1Routes; + } + } + + /** + * Classe interna para associar uma rota com sua probabilidade de seleção. + */ + private static class RouteWithProbability { + final List route; + final double probability; + + RouteWithProbability(List route, double probability) { + this.route = route; + this.probability = probability; + } + } +} diff --git a/main/src/main/java/sd/routing/RouteSelector.java b/main/src/main/java/sd/routing/RouteSelector.java new file mode 100644 index 0000000..0febb29 --- /dev/null +++ b/main/src/main/java/sd/routing/RouteSelector.java @@ -0,0 +1,25 @@ +package sd.routing; + +import java.util.List; +import java.util.Map; + +/** + * Interface para implementação de políticas de seleção de rotas. + * + *

Define o contrato que todas as políticas de roteamento devem seguir. + * Permite a implementação de diferentes estratégias de roteamento + * (aleatória, caminho mais curto, menor congestionamento, etc.).

+ */ +public interface RouteSelector { + + /** + * Seleciona uma rota para um veículo a partir de um ponto de entrada. + * + * @param entryPoint ponto de entrada (E1, E2 ou E3) + * @param queueSizes mapa com o tamanho das filas em cada interseção (opcional, pode ser null). + * Chave: ID da interseção (ex: "Cr1", "Cr2") + * Valor: número total de veículos em espera nessa interseção + * @return lista de IDs representando a rota escolhida (ex: ["Cr1", "Cr2", "Cr5", "S"]) + */ + List selectRoute(String entryPoint, Map queueSizes); +} diff --git a/main/src/main/java/sd/routing/RoutingPolicy.java b/main/src/main/java/sd/routing/RoutingPolicy.java new file mode 100644 index 0000000..06c7db2 --- /dev/null +++ b/main/src/main/java/sd/routing/RoutingPolicy.java @@ -0,0 +1,36 @@ +package sd.routing; + +/** + * Enumeração que define as políticas de roteamento disponíveis para a simulação. + * + *

As políticas de roteamento determinam como os veículos escolhem o caminho + * a seguir desde o ponto de entrada até à saída da rede de interseções.

+ * + *
    + *
  • RANDOM: Seleção aleatória de rotas baseada em probabilidades predefinidas
  • + *
  • SHORTEST_PATH: Escolhe sempre a rota com o menor número de cruzamentos
  • + *
  • LEAST_CONGESTED: Escolhe a rota evitando cruzamentos mais congestionados
  • + *
+ */ +public enum RoutingPolicy { + /** + * Política aleatória (baseline). + * Seleciona rotas com base em probabilidades predefinidas, sem considerar + * o estado atual da rede. + */ + RANDOM, + + /** + * Política do caminho mais curto. + * Sempre escolhe a rota com o menor número de cruzamentos entre o ponto + * de entrada e a saída, minimizando a distância teórica. + */ + SHORTEST_PATH, + + /** + * Política das menores filas (roteamento dinâmico). + * Escolhe a rota que passa pelos cruzamentos menos congestionados, + * com base no tamanho atual das filas em cada interseção. + */ + LEAST_CONGESTED +} diff --git a/main/src/main/java/sd/routing/ShortestPathRouteSelector.java b/main/src/main/java/sd/routing/ShortestPathRouteSelector.java new file mode 100644 index 0000000..a92321d --- /dev/null +++ b/main/src/main/java/sd/routing/ShortestPathRouteSelector.java @@ -0,0 +1,89 @@ +package sd.routing; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Implementação da política de roteamento por caminho mais curto. + * + *

Esta política sempre escolhe a rota com o menor número de cruzamentos + * entre o ponto de entrada e a saída. É uma política determinística que + * não considera o estado da rede (tamanho das filas).

+ * + *

Objetivo: Minimizar a distância teórica percorrida pelos veículos.

+ */ +public class ShortestPathRouteSelector implements RouteSelector { + + /** Rotas possíveis a partir do ponto de entrada E1, ordenadas por comprimento */ + private final List> e1Routes; + /** Rotas possíveis a partir do ponto de entrada E2, ordenadas por comprimento */ + private final List> e2Routes; + /** Rotas possíveis a partir do ponto de entrada E3, ordenadas por comprimento */ + private final List> e3Routes; + + /** + * Cria um novo seletor de rotas por caminho mais curto. + * As rotas são ordenadas por comprimento (número de cruzamentos). + */ + public ShortestPathRouteSelector() { + this.e1Routes = new ArrayList<>(); + this.e2Routes = new ArrayList<>(); + this.e3Routes = new ArrayList<>(); + initializeRoutes(); + } + + /** + * Inicializa as rotas possíveis para cada ponto de entrada. + * As rotas são organizadas da mais curta para a mais longa. + */ + private void initializeRoutes() { + // Rotas de E1 (entrada Norte) - ordenadas por comprimento + e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr3", "S")); // 4 nós + e1Routes.add(Arrays.asList("Cr1", "Cr2", "Cr5", "S")); // 4 nós + e1Routes.add(Arrays.asList("Cr1", "Cr4", "Cr5", "S")); // 4 nós + + // Rotas de E2 (entrada Oeste) - ordenadas por comprimento + e2Routes.add(Arrays.asList("Cr2", "Cr3", "S")); // 3 nós (mais curta!) + e2Routes.add(Arrays.asList("Cr2", "Cr5", "S")); // 3 nós + e2Routes.add(Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S")); // 5 nós + + // Rotas de E3 (entrada Sul) - ordenadas por comprimento + e3Routes.add(Arrays.asList("Cr3", "S")); // 2 nós (mais curta!) + e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr5", "S")); // 4 nós + e3Routes.add(Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S")); // 6 nós + } + + @Override + public List selectRoute(String entryPoint, Map queueSizes) { + // Ignora queueSizes - política baseada apenas no comprimento do caminho + + List> availableRoutes = getRoutesForEntryPoint(entryPoint); + + // Retorna a rota mais curta (primeira da lista) + List shortestRoute = availableRoutes.get(0); + + return new ArrayList<>(shortestRoute); + } + + /** + * Obtém as rotas disponíveis para um ponto de entrada. + * + * @param entryPoint ponto de entrada (E1, E2 ou E3) + * @return lista de rotas ordenadas por comprimento + */ + private List> getRoutesForEntryPoint(String entryPoint) { + switch (entryPoint.toUpperCase()) { + case "E1": + return e1Routes; + case "E2": + return e2Routes; + case "E3": + return e3Routes; + default: + System.err.printf("Unknown entry point: %s, defaulting to E1%n", entryPoint); + return e1Routes; + } + } +} diff --git a/main/src/main/java/sd/serialization/JsonMessageSerializer.java b/main/src/main/java/sd/serialization/JsonMessageSerializer.java index 1b70c68..68c6b06 100644 --- a/main/src/main/java/sd/serialization/JsonMessageSerializer.java +++ b/main/src/main/java/sd/serialization/JsonMessageSerializer.java @@ -1,26 +1,25 @@ package sd.serialization; +import java.nio.charset.StandardCharsets; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; -import java.nio.charset.StandardCharsets; - /** - * JSON-based implementation of {@link MessageSerializer} using Google's Gson library. - * - * This serializer converts objects to JSON format for transmission, providing: - * - Human-readable message format (easy debugging) - * - Cross-platform compatibility - * - Smaller message sizes compared to Java native serialization - * - Better security (no code execution during deserialization) - * - * The serializer is configured with pretty printing disabled by default for - * production use, but can be enabled for debugging purposes. - * - * Thread-safety: This class is thread-safe as Gson instances are thread-safe. - * - * @see MessageSerializer + * Implementação baseada em JSON da estratégia {@link MessageSerializer}, utilizando a biblioteca Gson. + *

+ * Este serializador converte objetos Java para o formato de texto JSON antes da transmissão. + * Oferece várias vantagens técnicas sobre a serialização nativa do Java: + *

    + *
  • Legibilidade: O formato de texto facilita a depuração (sniffing de rede) sem ferramentas especializadas.
  • + *
  • Interoperabilidade: Permite futura integração com componentes não-Java (ex: Dashboards web em JS).
  • + *
  • Segurança: Reduz a superfície de ataque para execução remota de código (RCE), pois não desserializa classes arbitrárias, apenas dados.
  • + *
+ *

+ * Thread-Safety: A instância interna do {@code Gson} é imutável e thread-safe, permitindo + * que este serializador seja partilhado entre múltiplas threads (ex: no pool do DashboardServer). + * * @see MessageSerializer */ public class JsonMessageSerializer implements MessageSerializer { @@ -28,16 +27,16 @@ public class JsonMessageSerializer implements MessageSerializer { private final boolean prettyPrint; /** - * Creates a new JSON serializer with default configuration (no pretty printing). + * Cria um novo serializador JSON com configuração otimizada para produção (compacto). */ public JsonMessageSerializer() { this(false); } /** - * Creates a new JSON serializer with optional pretty printing. - * - * @param prettyPrint If true, JSON output will be formatted with indentation + * Cria um novo serializador JSON com formatação opcional. + * * @param prettyPrint Se {@code true}, o JSON gerado incluirá indentação e quebras de linha. + * Útil para debug, mas aumenta significativamente o tamanho do payload. */ public JsonMessageSerializer(boolean prettyPrint) { this.prettyPrint = prettyPrint; @@ -53,6 +52,13 @@ public class JsonMessageSerializer implements MessageSerializer { this.gson = builder.create(); } + /** + * Converte um objeto em memória para um array de bytes JSON (UTF-8). + * + * @param object O objeto a ser serializado. + * @return O payload em bytes pronto para transmissão TCP. + * @throws SerializationException Se o objeto não for compatível com JSON ou ocorrer erro de encoding. + */ @Override public byte[] serialize(Object object) throws SerializationException { if (object == null) { @@ -68,6 +74,16 @@ public class JsonMessageSerializer implements MessageSerializer { } } + /** + * Reconstrói um objeto Java a partir de um array de bytes JSON. + *

+ * Realiza a validação sintática do JSON e a validação de tipo baseada na classe alvo. + * + * @param data O array de bytes recebido da rede. + * @param clazz A classe do objeto esperado (Type Token). + * @return A instância do objeto reconstruído. + * @throws SerializationException Se o JSON for malformado ou incompatível com a classe alvo. + */ @Override public T deserialize(byte[] data, Class clazz) throws SerializationException { if (data == null) { @@ -95,20 +111,18 @@ public class JsonMessageSerializer implements MessageSerializer { } /** - * Returns the underlying Gson instance for advanced usage. - * - * @return The Gson instance + * Retorna a instância subjacente do Gson para configurações avançadas. + * * @return A instância Gson configurada. */ public Gson getGson() { return gson; } /** - * Checks if pretty printing is enabled. - * - * @return true if pretty printing is enabled + * Verifica se a formatação "pretty print" está ativa. + * * @return true se a indentação estiver habilitada. */ public boolean isPrettyPrint() { return prettyPrint; } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/serialization/MessageSerializer.java b/main/src/main/java/sd/serialization/MessageSerializer.java index 21517f4..3a5ff55 100644 --- a/main/src/main/java/sd/serialization/MessageSerializer.java +++ b/main/src/main/java/sd/serialization/MessageSerializer.java @@ -1,48 +1,49 @@ package sd.serialization; /** - * Interface for serializing and deserializing objects for network transmission. - * - * This interface provides a common abstraction for different serialization strategies - * allowing the system to switch between implementations without changing the communication layer. - * - * Implementations must ensure: - * - Thread-safety if used in concurrent contexts - * - Proper exception handling with meaningful error messages - * - Preservation of object state during round-trip serialization - * - * @see JsonMessageSerializer + * Interface que define o contrato para estratégias de serialização e desserialização de objetos. + *

+ * Esta abstração permite desacoplar a camada de transporte (Sockets TCP) da camada de + * apresentação de dados. Ao implementar o padrão Strategy, o sistema ganha flexibilidade + * para alternar entre diferentes formatos de codificação (JSON, Binário Nativo, XML, Protobuf) + * sem necessidade de refatorização da lógica de rede. + *

+ * Requisitos para Implementações: + *

    + *
  • Thread-Safety: As implementações devem ser seguras para uso concorrente, dado que + * instâncias únicas podem ser partilhadas por múltiplos ClientHandlers.
  • + *
  • Robustez: Falhas de parsing devem resultar em exceções tipificadas ({@link SerializationException}), + * nunca em falhas silenciosas ou estados inconsistentes.
  • + *
+ * * @see JsonMessageSerializer */ public interface MessageSerializer { /** - * Serializes an object into a byte array for transmission. - * - * @param object The object to serialize (must not be null) - * @return A byte array containing the serialized representation - * @throws SerializationException If serialization fails - * @throws IllegalArgumentException If object is null + * Converte (Marshals) um objeto em memória para uma sequência de bytes para transmissão. + * * @param object O objeto de domínio a ser serializado (não pode ser nulo). + * @return Um array de bytes contendo a representação codificada do objeto. + * @throws SerializationException Se ocorrer um erro durante a codificação (ex: ciclo de referências). + * @throws IllegalArgumentException Se o objeto fornecido for nulo. */ byte[] serialize(Object object) throws SerializationException; /** - * Deserializes a byte array back into an object of the specified type. - * - * @param The expected type of the deserialized object - * @param data The byte array containing serialized data (must not be null) - * @param clazz The class of the expected object type (must not be null) - * @return The deserialized object - * @throws SerializationException If deserialization fails - * @throws IllegalArgumentException If data or clazz is null + * Reconstrói (Unmarshals) um objeto a partir de uma sequência de bytes. + * * @param O tipo genérico do objeto esperado. + * @param data O array de bytes contendo os dados serializados (não pode ser nulo). + * @param clazz A classe do tipo esperado para verificação e instancialização. + * @return A instância do objeto reconstruído com o seu estado restaurado. + * @throws SerializationException Se os dados estiverem corrompidos ou incompatíveis com a classe alvo. + * @throws IllegalArgumentException Se os dados ou a classe forem nulos. */ T deserialize(byte[] data, Class clazz) throws SerializationException; /** - * Gets the name of this serialization strategy (e.g., "JSON", "Java Native"). - * Useful for logging and debugging. - * - * @return The serializer name + * Obtém o identificador legível desta estratégia de serialização (ex: "JSON (Gson)", "Native"). + * Utilizado primariamente para logging, auditoria e negociação de conteúdo. + * * @return O nome descritivo do serializador. */ String getName(); -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/serialization/SerializationException.java b/main/src/main/java/sd/serialization/SerializationException.java index 5cf9675..bae51b0 100644 --- a/main/src/main/java/sd/serialization/SerializationException.java +++ b/main/src/main/java/sd/serialization/SerializationException.java @@ -1,41 +1,40 @@ package sd.serialization; /** - * Exception thrown when serialization or deserialization operations fail. - * - * This exception wraps underlying errors (I/O exceptions, parsing errors, etc.) - * and provides context about what went wrong during the serialization process. + * Exceção verificada (Checked Exception) que sinaliza falhas no processo de transformação de dados. + *

+ * Esta classe atua como um wrapper unificador para erros ocorridos na camada de serialização, + * abstraindo falhas de baixo nível (como erros de I/O, sintaxe JSON inválida ou incompatibilidade + * de tipos) numa única exceção de domínio. Permite que o código cliente trate falhas de + * protocolo de forma consistente, independentemente da implementação subjacente (Gson, Nativa, etc.). */ public class SerializationException extends Exception { private static final long serialVersionUID = 1L; // Long(64bits) instead of int(32bits) /** - * Constructs a new serialization exception with the specified detail message. - * - * @param message The detail message + * Constrói uma nova exceção de serialização com uma mensagem descritiva. + * * @param message A mensagem detalhando o erro. */ public SerializationException(String message) { super(message); } /** - * Constructs a new serialization exception with the specified detail message - * and cause. - * - * @param message The detail message - * @param cause The cause of this exception + * Constrói uma nova exceção encapsulando a causa raiz do problema. + * Útil para preservar a stack trace original de erros de bibliotecas terceiras (ex: Gson). + * * @param message A mensagem detalhando o erro. + * @param cause A exceção original que causou a falha. */ public SerializationException(String message, Throwable cause) { super(message, cause); } /** - * Constructs a new serialization exception with the specified cause. - * - * @param cause The cause of this exception + * Constrói uma nova exceção baseada apenas na causa raiz. + * * @param cause A exceção original. */ public SerializationException(Throwable cause) { super(cause); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/serialization/SerializerFactory.java b/main/src/main/java/sd/serialization/SerializerFactory.java index a2261d3..70d2f0e 100644 --- a/main/src/main/java/sd/serialization/SerializerFactory.java +++ b/main/src/main/java/sd/serialization/SerializerFactory.java @@ -1,14 +1,14 @@ package sd.serialization; /** - * Factory for creating {@link MessageSerializer} instances. - * - * This factory provides a centralized way to create and configure JSON serializers - * using Gson, making it easy to configure serialization throughout the application. - * - * The factory can be configured via system properties for easy deployment configuration. - * - * Example usage: + * Fábrica estática (Factory Pattern) para instanciação controlada de {@link MessageSerializer}. + *

+ * Esta classe centraliza a criação de estratégias de serialização, garantindo consistência + * de configuração em todo o sistema distribuído. Permite a injeção de configurações via + * Propriedades de Sistema (System Properties), facilitando a alternância entre modos de + * depuração (Pretty Print) e produção (Compacto) sem recompilação. + *

+ * Exemplo de Uso: *

  * MessageSerializer serializer = SerializerFactory.createDefault();
  * byte[] data = serializer.serialize(myObject);
@@ -17,28 +17,27 @@ package sd.serialization;
 public class SerializerFactory {
     
     /**
-     * System property key for enabling pretty-print in JSON serialization.
-     * Set to "true" for debugging, "false" for production.
+     * Chave da propriedade de sistema para ativar a formatação JSON legível (Pretty Print).
+     * Defina {@code -Dsd.serialization.json.prettyPrint=true} na JVM para ativar.
      */
     public static final String JSON_PRETTY_PRINT_PROPERTY = "sd.serialization.json.prettyPrint";
     
-    // Default configuration
+    // Default configuration (Production-ready)
     private static final boolean DEFAULT_JSON_PRETTY_PRINT = false;
     
     /**
-     * Private constructor to prevent instantiation.
+     * Construtor privado para prevenir instanciação acidental desta classe utilitária.
      */
     private SerializerFactory() {
         throw new UnsupportedOperationException("Factory class cannot be instantiated");
     }
     
     /**
-     * Creates a JSON serializer based on system configuration.
-     * 
-     * Pretty-print is determined by checking the system property
-     * {@value #JSON_PRETTY_PRINT_PROPERTY}. If not set, defaults to false.
-     * 
-     * @return A configured JsonMessageSerializer instance
+     * Cria um serializador JSON configurado dinamicamente pelo ambiente.
+     * 

+ * Verifica a propriedade de sistema {@value #JSON_PRETTY_PRINT_PROPERTY}. + * Se não definida, assume o padrão de produção (falso/compacto). + * * @return Uma instância configurada de {@link JsonMessageSerializer}. */ public static MessageSerializer createDefault() { boolean prettyPrint = Boolean.getBoolean(JSON_PRETTY_PRINT_PROPERTY); @@ -46,21 +45,20 @@ public class SerializerFactory { } /** - * Creates a JSON serializer with default configuration (no pretty printing). - * - * @return A JsonMessageSerializer instance + * Cria um serializador JSON com configuração padrão otimizada (sem indentação). + * Ideal para ambientes de produção onde a largura de banda é prioritária. + * * @return Uma instância compacta de {@link JsonMessageSerializer}. */ public static MessageSerializer createSerializer() { return createSerializer(DEFAULT_JSON_PRETTY_PRINT); } /** - * Creates a JSON serializer with specified pretty-print setting. - * - * @param prettyPrint Whether to enable pretty printing - * @return A JsonMessageSerializer instance + * Cria um serializador JSON com configuração explícita de formatação. + * * @param prettyPrint {@code true} para ativar indentação (Debug), {@code false} para compacto. + * @return Uma instância personalizada de {@link JsonMessageSerializer}. */ public static MessageSerializer createSerializer(boolean prettyPrint) { return new JsonMessageSerializer(prettyPrint); } -} +} \ No newline at end of file diff --git a/main/src/main/java/sd/util/RandomGenerator.java b/main/src/main/java/sd/util/RandomGenerator.java index f1122d5..e9ab436 100644 --- a/main/src/main/java/sd/util/RandomGenerator.java +++ b/main/src/main/java/sd/util/RandomGenerator.java @@ -3,84 +3,88 @@ package sd.util; import java.util.Random; /** - * Utility class for generating random values used throughout the simulation. - * * Provides static methods for: - * - Generating exponentially distributed intervals (for Poisson processes). - * - Generating random integers and doubles in a range. - * - Making decisions based on probability. - * - Choosing random elements from an array. - * * It uses a single, static {@link Random} instance. + * Utilitário central de geração estocástica para a simulação. + *

+ * Esta classe fornece primitivas para geração de números pseudo-aleatórios, abstraindo + * a complexidade de distribuições estatísticas. + *

+ * Funcionalidades Principais: + *

    + *
  • Modelagem de Poisson: Geração de tempos entre chegadas usando distribuição exponencial inversa.
  • + *
  • Amostragem Uniforme: Geração de inteiros e doubles em intervalos fechados/abertos.
  • + *
  • Decisão Probabilística: Avaliação de eventos booleanos baseados em pesos (Bernoulli trials).
  • + *
  • Determinismo: Suporte a sementes (seeds) manuais para reprodutibilidade exata de cenários de teste.
  • + *
*/ public class RandomGenerator { - /** - * The single, shared Random instance for the entire simulation. + /** * Instância singleton estática do gerador PRNG (Pseudo-Random Number Generator). + * Thread-safe (java.util.Random é sincronizado), embora possa haver contenção em alta concorrência. */ private static final Random random = new Random(); /** - * Returns a random time interval that follows an exponential distribution. - * * This is a key component for modeling a Poisson process, where the - * *inter-arrival times* (time between events) are exponentially distributed. - * The formula used is the inverse transform sampling method: - * {@code Time = -ln(1 - U) / λ} - * where U is a uniform random number [0, 1) and λ (lambda) is the - * average arrival rate. + * Gera um intervalo de tempo seguindo uma Distribuição Exponencial. + *

+ * Este método implementa o algoritmo de Inverse Transform Sampling para simular + * um Processo de Poisson homogêneo. É fundamental para modelar a chegada natural de + * veículos, onde eventos independentes ocorrem a uma taxa média constante. + *

+ * Fórmula Matemática: {@code T = -ln(1 - U) / λ} + *
Onde: + *

    + *
  • {@code U}: Variável aleatória uniforme no intervalo [0, 1).
  • + *
  • {@code λ (lambda)}: Taxa média de eventos por unidade de tempo (ex: veículos/segundo).
  • + *
* - * @param lambda The average arrival rate (λ) (e.g., 0.5 vehicles per second). - * @return The time interval (in seconds) until the next arrival. + * @param lambda A taxa média de chegada (λ > 0). + * @return O intervalo de tempo (delta t) até o próximo evento, em segundos. */ public static double generateExponentialInterval(double lambda) { - // Math.log is the natural logarithm (ln) - // random.nextDouble() returns a value in [0.0, 1.0) return Math.log(1 - random.nextDouble()) / -lambda; } /** - * Returns a random integer between {@code min} and {@code max}, inclusive. + * Gera um número inteiro uniformemente distribuído no intervalo fechado {@code [min, max]}. * - * @param min The minimum possible value. - * @param max The maximum possible value. - * @return A random integer in the range [min, max]. + * @param min Limite inferior (inclusivo). + * @param max Limite superior (inclusivo). + * @return Um inteiro aleatório I tal que {@code min <= I <= max}. */ public static int generateRandomInt(int min, int max) { - // random.nextInt(N) returns a value from 0 to N-1 - // (max - min + 1) is the total number of integers in the range - // + min offsets the range return random.nextInt(max - min + 1) + min; } /** - * Returns a random double between {@code min} (inclusive) and {@code max} (exclusive). + * Gera um número de ponto flutuante uniformemente distribuído no intervalo semi-aberto {@code [min, max)}. * - * @param min The minimum possible value. - * @param max The maximum possible value. - * @return A random double in the range [min, max). + * @param min Limite inferior (inclusivo). + * @param max Limite superior (exclusivo). + * @return Um double aleatório D tal que {@code min <= D < max}. */ public static double generateRandomDouble(double min, double max) { return min + (max - min) * random.nextDouble(); } /** - * Returns {@code true} with a given probability. - * * This is useful for making weighted decisions. For example, - * {@code occursWithProbability(0.3)} will return {@code true} - * approximately 30% of the time. + * Realiza um teste de Bernoulli (Sim/Não) com uma probabilidade de sucesso especificada. + *

+ * Utilizado para decisões de ramificação estocástica (ex: "Este veículo é um camião?"). * - * @param probability A value between 0.0 (never) and 1.0 (always). - * @return {@code true} or {@code false}, based on the probability. + * @param probability A probabilidade de retorno {@code true} (0.0 a 1.0). + * @return {@code true} se o evento ocorrer, {@code false} caso contrário. */ public static boolean occursWithProbability(double probability) { return random.nextDouble() < probability; } /** - * Picks a random element from the given array. + * Seleciona aleatoriamente um elemento de um array genérico (Amostragem Uniforme Discreta). * - * @param The generic type of the array. - * @param array The array to choose from. - * @return A randomly selected element from the array. - * @throws IllegalArgumentException if the array is null or empty. + * @param O tipo dos elementos no array. + * @param array A população de onde escolher. + * @return O elemento selecionado. + * @throws IllegalArgumentException Se o array for nulo ou vazio. */ public static T chooseRandom(T[] array) { if (array == null || array.length == 0) { @@ -90,12 +94,13 @@ public class RandomGenerator { } /** - * Sets the seed of the shared random number generator. - * This is extremely useful for debugging and testing, as it allows - * the simulation to be run multiple times with the *exact same* - * sequence of "random" events, making the results reproducible. + * Reinicializa a semente (seed) do gerador global. + *

+ * Importância Crítica: Permite tornar a simulação determinística. Ao fixar a seed, + * a sequência de números "aleatórios" gerada será idêntica em execuções subsequentes, + * facilitando a depuração de race conditions ou lógica complexa. * - * @param seed The seed to use. + * @param seed O valor da semente inicial (ex: timestamp ou constante). */ public static void setSeed(long seed) { random.setSeed(seed); diff --git a/main/src/main/java/sd/util/VehicleGenerator.java b/main/src/main/java/sd/util/VehicleGenerator.java index c6c7611..344b4d7 100644 --- a/main/src/main/java/sd/util/VehicleGenerator.java +++ b/main/src/main/java/sd/util/VehicleGenerator.java @@ -1,147 +1,116 @@ package sd.util; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import java.util.Map; import sd.config.SimulationConfig; import sd.model.Vehicle; import sd.model.VehicleType; +import sd.routing.RouteSelector; /** - * Generates vehicles for the simulation. - * * This class is responsible for two key tasks: - * 1. Determining *when* the next vehicle should arrive, based on the - * arrival model (POISSON or FIXED) from the {@link SimulationConfig}. - * 2. Creating a new {@link Vehicle} object with a randomly selected - * type (e.g., BIKE, LIGHT) and a randomly selected route. - * * Routes are predefined and organized by entry point (E1, E2, E3). + * Motor de injeção de carga (Load Injector) para a simulação de tráfego. + *

+ * Esta classe atua como uma fábrica estocástica de veículos, sendo responsável por: + *

    + *
  1. Modelagem Temporal: Determinar os instantes de chegada (Inter-arrival times) + * usando processos de Poisson (estocástico) ou intervalos determinísticos.
  2. + *
  3. Caracterização da Entidade: Atribuir tipos de veículo (Bike, Light, Heavy) + * baseado numa Distribuição de Probabilidade Cumulativa (CDF).
  4. + *
  5. Inicialização Espacial: Distribuir a carga uniformemente entre os pontos de entrada (E1-E3).
  6. + *
  7. Atribuição de Rota: Delegar a escolha do percurso à estratégia {@link RouteSelector} ativa.
  8. + *
*/ public class VehicleGenerator { private final SimulationConfig config; private final String arrivalModel; - private final double arrivalRate; // Lambda (λ) for POISSON - private final double fixedInterval; // Interval for FIXED - // --- Predefined Routes --- - // These lists store all possible routes, grouped by where they start. + /** Parâmetro Lambda (λ) para a distribuição de Poisson (taxa de chegada). */ + private final double arrivalRate; - /** Routes starting from entry point E1. */ - private final List e1Routes; - /** Routes starting from entry point E2. */ - private final List e2Routes; - /** Routes starting from entry point E3. */ - private final List e3Routes; + /** Intervalo determinístico para geração constante (modo debug/teste). */ + private final double fixedInterval; + + /** * Estratégia de roteamento atual. + * Não é final para permitir Hot-Swapping durante a execução. + */ + private RouteSelector routeSelector; /** - * Constructs a new VehicleGenerator. - * It reads the necessary configuration and initializes the - * predefined routes. + * Inicializa o gerador com as configurações de simulação e estratégia de roteamento. * - * @param config The {@link SimulationConfig} object. + * @param config A configuração global contendo as taxas e probabilidades. + * @param routeSelector A estratégia inicial de seleção de rotas. */ - public VehicleGenerator(SimulationConfig config) { + public VehicleGenerator(SimulationConfig config, RouteSelector routeSelector) { this.config = config; + this.routeSelector = routeSelector; - // Cache configuration values for performance + // Cache de valores de configuração para evitar lookups repetitivos em hot-path this.arrivalModel = config.getArrivalModel(); this.arrivalRate = config.getArrivalRate(); this.fixedInterval = config.getFixedArrivalInterval(); - - // Initialize route lists - this.e1Routes = new ArrayList<>(); - this.e2Routes = new ArrayList<>(); - this.e3Routes = new ArrayList<>(); - initializePossibleRoutes(); } /** - * Defines all possible routes that vehicles can take, organized by - * their entry point (E1, E2, E3). Each route is given a - * probability, which determines how often it's chosen. - */ - private void initializePossibleRoutes() { - // E1 routes (Starts at Cr1) - e1Routes.add(new RouteWithProbability( - Arrays.asList("Cr1", "Cr4", "Cr5", "S"), 0.34)); // E1 -> Cr1 -> Cr4 -> Cr5 -> Exit - e1Routes.add(new RouteWithProbability( - Arrays.asList("Cr1", "Cr2", "Cr5", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr5 -> Exit - e1Routes.add(new RouteWithProbability( - Arrays.asList("Cr1", "Cr2", "Cr3", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr3 -> Exit - - // E2 routes (Starts at Cr2) - e2Routes.add(new RouteWithProbability( - Arrays.asList("Cr2", "Cr5", "S"), 0.34)); // E2 -> Cr2 -> Cr5 -> Exit - e2Routes.add(new RouteWithProbability( - Arrays.asList("Cr2", "Cr3", "S"), 0.33)); // E2 -> Cr2 -> Cr3 -> Exit - e2Routes.add(new RouteWithProbability( - Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E2 -> Cr2 -> ... -> Exit - - // E3 routes (Starts at Cr3) - e3Routes.add(new RouteWithProbability( - Arrays.asList("Cr3", "S"), 0.34)); // E3 -> Cr3 -> Exit - e3Routes.add(new RouteWithProbability( - Arrays.asList("Cr3", "Cr2", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> Cr2 -> Cr5 -> Exit - e3Routes.add(new RouteWithProbability( - Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> ... -> Exit - } - - /** - * Calculates the *absolute* time of the next vehicle arrival - * based on the configured model. - * * @param currentTime The current simulation time, used as the base. - * @return The absolute time (e.g., {@code currentTime + interval}) - * when the next vehicle should be generated. + * Calcula o timestamp absoluto para a próxima injeção de veículo. + *

+ * Se o modelo for "POISSON", utiliza a técnica de Inverse Transform Sampling + * (via {@link RandomGenerator}) para gerar intervalos exponencialmente distribuídos, + * simulando a aleatoriedade natural do tráfego. + * * @param currentTime O tempo atual da simulação (base de cálculo). + * @return O instante futuro (t + delta) para agendamento do evento de geração. */ public double getNextArrivalTime(double currentTime) { if ("POISSON".equalsIgnoreCase(arrivalModel)) { - // For a Poisson process, the time *between* arrivals - // follows an exponential distribution. double interval = RandomGenerator.generateExponentialInterval(arrivalRate); return currentTime + interval; } else { - // For a Fixed model, the interval is constant. return currentTime + fixedInterval; } } /** - * Generates a new {@link Vehicle} object. - * This involves: - * 1. Selecting a random {@link VehicleType} based on probabilities. - * 2. Selecting a random route (entry point + path) based on probabilities. + * Instancia (Spawn) um novo veículo configurado e roteado. + *

+ * O processo de criação segue um pipeline: + *

    + *
  1. Seleção de Tipo (Roda da Fortuna / CDF).
  2. + *
  3. Seleção de Entrada (Uniforme).
  4. + *
  5. Cálculo de Rota (Delegado ao Strategy).
  6. + *
* - * @param vehicleId The unique identifier for the new vehicle (e.g., "V123"). - * @param entryTime The simulation time when this vehicle is being created. - * @return A new, configured {@link Vehicle} object. + * @param vehicleId O identificador único sequencial (ex: "V104"). + * @param entryTime O timestamp de criação. + * @param queueSizes Snapshot atual das filas (usado apenas por estratégias dinâmicas como LEAST_CONGESTED). + * @return A entidade {@link Vehicle} pronta para inserção na malha. */ - public Vehicle generateVehicle(String vehicleId, double entryTime) { + public Vehicle generateVehicle(String vehicleId, double entryTime, Map queueSizes) { VehicleType type = selectVehicleType(); - List route = selectRandomRoute(); + String entryPoint = selectRandomEntryPoint(); + List route = routeSelector.selectRoute(entryPoint, queueSizes); return new Vehicle(vehicleId, type, entryTime, route); } /** - * Selects a {@link VehicleType} (BIKE, LIGHT, HEAVY) based on the - * probabilities defined in the {@link SimulationConfig}. - * * Uses a standard "cumulative probability" technique: - * 1. Get a random number {@code rand} from [0, 1). - * 2. If {@code rand < P(Bike)}, return BIKE. - * 3. Else if {@code rand < P(Bike) + P(Light)}, return LIGHT. - * 4. Else, return HEAVY. + * Seleciona o tipo de veículo usando Amostragem por Probabilidade Cumulativa. + *

+ * Normaliza as probabilidades configuradas e mapeia um número aleatório [0, 1) + * para o intervalo correspondente ao tipo de veículo. * - * @return The selected {@link VehicleType}. + * @return O tipo enumerado {@link VehicleType} selecionado. */ private VehicleType selectVehicleType() { double bikeProbability = config.getBikeVehicleProbability(); double lightProbability = config.getLightVehicleProbability(); double heavyProbability = config.getHeavyVehicleProbability(); - // Normalize probabilities in case they don't sum to exactly 1.0 double total = bikeProbability + lightProbability + heavyProbability; - if (total == 0) return VehicleType.LIGHT; // Avoid division by zero + if (total == 0) return VehicleType.LIGHT; // Fallback de segurança + + // Normalização bikeProbability /= total; lightProbability /= total; @@ -157,73 +126,42 @@ public class VehicleGenerator { } /** - * Selects a random route for a new vehicle. - * This is a two-step process: - * 1. Randomly select an entry point (E1, E2, or E3) with equal probability. - * 2. From the chosen entry point's list of routes, select one - * based on their defined probabilities (using cumulative probability). + * Seleciona um ponto de injeção na borda da rede (Edge Node). + * Distribuição Uniforme: ~33.3% para cada entrada (E1, E2, E3). * - * @return A {@link List} of strings representing the chosen route (e.g., ["Cr1", "Cr4", "S"]). + * @return O ID da interseção de entrada. */ - private List selectRandomRoute() { - // Step 1: Randomly select an entry point (E1, E2, or E3) - double entryRandom = Math.random(); - List selectedRoutes; + private String selectRandomEntryPoint() { + double rand = Math.random(); - if (entryRandom < 0.333) { - selectedRoutes = e1Routes; - } else if (entryRandom < 0.666) { - selectedRoutes = e2Routes; + if (rand < 0.333) { + return "E1"; + } else if (rand < 0.666) { + return "E2"; } else { - selectedRoutes = e3Routes; + return "E3"; } - - // Step 2: Select a route from the chosen list based on cumulative probabilities - double routeRand = Math.random(); - double cumulative = 0.0; - - for (RouteWithProbability routeWithProb : selectedRoutes) { - cumulative += routeWithProb.probability; - if (routeRand <= cumulative) { - // Return a *copy* of the route to prevent modification - return new ArrayList<>(routeWithProb.route); - } - } - - // Fallback: This should only be reached if probabilities don't sum to 1 - // (due to floating point errors) - return new ArrayList<>(selectedRoutes.get(0).route); } /** - * @return A string providing information about the generator's configuration. + * Atualiza a estratégia de roteamento em tempo de execução (Hot-Swap). + *

+ * Permite que o Coordenador altere o comportamento da frota (ex: de RANDOM para SHORTEST_PATH) + * sem necessidade de reiniciar a simulação. + * * @param newRouteSelector A nova implementação de estratégia a utilizar. + */ + public void setRouteSelector(RouteSelector newRouteSelector) { + this.routeSelector = newRouteSelector; + } + + /** + * Retorna uma representação textual do estado interno do gerador. + * Útil para logs de auditoria e debugging. */ public String getInfo() { - int totalRoutes = e1Routes.size() + e2Routes.size() + e3Routes.size(); return String.format( - "VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routes=%d (E1:%d, E2:%d, E3:%d)}", - arrivalModel, arrivalRate, fixedInterval, totalRoutes, - e1Routes.size(), e2Routes.size(), e3Routes.size() + "VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routeSelector=%s}", + arrivalModel, arrivalRate, fixedInterval, routeSelector.getClass().getSimpleName() ); } - - /** - * A private inner "struct-like" class to hold a route (a List of strings) - * and its associated selection probability. - */ - private static class RouteWithProbability { - final List route; - final double probability; - - /** - * Constructs a new RouteWithProbability pair. - * @param route The list of intersection IDs. - * @param probability The probability (0.0 to 1.0) of this route - * being chosen *from its entry group*. - */ - RouteWithProbability(List route, double probability) { - this.route = route; - this.probability = probability; - } - } } \ No newline at end of file diff --git a/main/src/main/resources/network_config.json b/main/src/main/resources/network_config.json index 8bbd50a..f83f4a3 100644 --- a/main/src/main/resources/network_config.json +++ b/main/src/main/resources/network_config.json @@ -27,15 +27,18 @@ }, { "id": "Cr4", - "lights": ["East"], + "lights": ["East", "North"], "routes": { + "Cr1": "North", "Cr5": "East" } }, { "id": "Cr5", - "lights": ["East"], + "lights": ["East", "West", "North"], "routes": { + "Cr2": "North", + "Cr4": "West", "S": "East" } } diff --git a/main/src/main/resources/simulation-high.properties b/main/src/main/resources/simulation-high.properties new file mode 100644 index 0000000..5c47079 --- /dev/null +++ b/main/src/main/resources/simulation-high.properties @@ -0,0 +1,126 @@ +# ========================================================= +# Traffic Simulation Configuration - HIGH LOAD SCENARIO +# --------------------------------------------------------- +# High traffic scenario for testing system under heavy load. +# Expected: Significant congestion, large queues, system stress test +# ========================================================= + +# === NETWORK CONFIGURATION === + +# Intersections (each with its host and port) +intersection.Cr1.host=localhost +intersection.Cr1.port=8001 +intersection.Cr2.host=localhost +intersection.Cr2.port=8002 +intersection.Cr3.host=localhost +intersection.Cr3.port=8003 +intersection.Cr4.host=localhost +intersection.Cr4.port=8004 +intersection.Cr5.host=localhost +intersection.Cr5.port=8005 + +# Exit node +exit.host=localhost +exit.port=9001 + +# Dashboard server +dashboard.host=localhost +dashboard.port=9000 + + +# === SIMULATION CONFIGURATION === + +# Total duration in seconds (1800 = 30 minutes) +simulation.duration=1800 + +# Vehicle arrival model: FIXED or POISSON +simulation.arrival.model=POISSON + +# λ (lambda): HIGH LOAD = 1.0 vehicle per second (60 vehicles/minute, 3600 vehicles/hour) +# This is 2x medium load - tests system capacity limits +simulation.arrival.rate=1.0 + +# Fixed interval between arrivals (only used if model=FIXED) +simulation.arrival.fixed.interval=2.0 + +# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED +simulation.routing.policy=LEAST_CONGESTED + + +# === TRAFFIC LIGHT TIMINGS === +# Format: trafficlight...= +# Aggressive timings to maximize throughput under high load + +# Intersection 1 (Entry point - longer greens to prevent early backup) +trafficlight.Cr1.South.green=60.0 +trafficlight.Cr1.South.red=3.0 +trafficlight.Cr1.East.green=60.0 +trafficlight.Cr1.East.red=3.0 + +# Intersection 2 (Main hub - CRITICAL BOTTLENECK, maximum green times) +# This is the most critical intersection - all routes converge here +trafficlight.Cr2.South.green=70.0 +trafficlight.Cr2.South.red=3.0 +trafficlight.Cr2.East.green=80.0 +trafficlight.Cr2.East.red=3.0 +trafficlight.Cr2.West.green=70.0 +trafficlight.Cr2.West.red=3.0 + +# Intersection 3 (Path to exit - maximize East throughput to exit) +trafficlight.Cr3.South.green=50.0 +trafficlight.Cr3.South.red=3.0 +trafficlight.Cr3.West.green=40.0 +trafficlight.Cr3.West.red=3.0 + +# Intersection 4 (High throughput needed toward Cr5) +trafficlight.Cr4.East.green=70.0 +trafficlight.Cr4.East.red=3.0 +trafficlight.Cr4.North.green=70.0 +trafficlight.Cr4.North.red=3.0 + +# Intersection 5 (Near exit - MAJOR BOTTLENECK, longest green time) +# All routes funnel through here before exit +trafficlight.Cr5.East.green=90.0 +trafficlight.Cr5.East.red=3.0 +trafficlight.Cr5.West.green=70.0 +trafficlight.Cr5.West.red=3.0 +trafficlight.Cr5.North.green=70.0 +trafficlight.Cr5.North.red=3.0 + + +# === VEHICLE CONFIGURATION === +# Probability distribution for vehicle types (must sum to 1.0) +vehicle.probability.bike=0.2 +vehicle.probability.light=0.6 +vehicle.probability.heavy=0.2 + +# Average crossing times (in seconds) +vehicle.crossing.time.bike=1.0 +vehicle.crossing.time.light=2.0 +vehicle.crossing.time.heavy=4.0 + +# Travel times between intersections (in seconds) +# Base time for light vehicles (cars) +vehicle.travel.time.base=1.0 +# Bike travel time = 0.5 x car travel time +vehicle.travel.time.bike.multiplier=0.5 +# Heavy vehicle travel time = 4.0 x base travel time +vehicle.travel.time.heavy.multiplier=4.0 + +# === STATISTICS === + +# Interval between dashboard updates (seconds) +statistics.update.interval=10.0 + +# === EXPECTED BEHAVIOR - HIGH LOAD === +# - Average system time: 200-400+ seconds (3-7+ minutes) +# - Maximum queue sizes: 15-30+ vehicles at Cr2 and Cr5 +# - Average queue sizes: 8-15+ vehicles +# - Severe congestion at Cr2 (main convergence point) +# - Severe congestion at Cr5 (pre-exit bottleneck) +# - System utilization: ~80-95% +# - Many vehicles will remain in system at simulation end +# - Queue growth may be unbounded if arrival rate exceeds service rate +# - Primary bottlenecks: Cr2 (3-way convergence) and Cr5 (final funnel) +# - This scenario tests maximum system capacity and traffic light optimization +# - Expected to demonstrate need for adaptive traffic light policies diff --git a/main/src/main/resources/simulation-low.properties b/main/src/main/resources/simulation-low.properties new file mode 100644 index 0000000..05eee1b --- /dev/null +++ b/main/src/main/resources/simulation-low.properties @@ -0,0 +1,120 @@ +# ========================================================= +# Traffic Simulation Configuration - LOW LOAD SCENARIO +# --------------------------------------------------------- +# Low traffic scenario for testing system under light load. +# Expected: No congestion, minimal queues, fast vehicle throughput +# ========================================================= + +# === NETWORK CONFIGURATION === + +# Intersections (each with its host and port) +intersection.Cr1.host=localhost +intersection.Cr1.port=8001 +intersection.Cr2.host=localhost +intersection.Cr2.port=8002 +intersection.Cr3.host=localhost +intersection.Cr3.port=8003 +intersection.Cr4.host=localhost +intersection.Cr4.port=8004 +intersection.Cr5.host=localhost +intersection.Cr5.port=8005 + +# Exit node +exit.host=localhost +exit.port=9001 + +# Dashboard server +dashboard.host=localhost +dashboard.port=9000 + + +# === SIMULATION CONFIGURATION === + +# Total duration in seconds (1800 = 30 minutes) +simulation.duration=1800 + +# Vehicle arrival model: FIXED or POISSON +simulation.arrival.model=POISSON + +# λ (lambda): LOW LOAD = 0.2 vehicles per second (12 vehicles/minute, 720 vehicles/hour) +# This is approximately 40% of medium load +simulation.arrival.rate=0.2 + +# Fixed interval between arrivals (only used if model=FIXED) +simulation.arrival.fixed.interval=2.0 + +# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED +simulation.routing.policy=LEAST_CONGESTED + + +# === TRAFFIC LIGHT TIMINGS === +# Format: trafficlight...= +# Standard timings - should be more than adequate for low load + +# Intersection 1 (Entry point - balanced) +trafficlight.Cr1.South.green=30.0 +trafficlight.Cr1.South.red=5.0 +trafficlight.Cr1.East.green=30.0 +trafficlight.Cr1.East.red=5.0 + +# Intersection 2 (Main hub - shorter cycles, favor East-West) +trafficlight.Cr2.South.green=30.0 +trafficlight.Cr2.South.red=5.0 +trafficlight.Cr2.East.green=30.0 +trafficlight.Cr2.East.red=5.0 +trafficlight.Cr2.West.green=30.0 +trafficlight.Cr2.West.red=5.0 + +# Intersection 3 (Path to exit - favor East) +trafficlight.Cr3.South.green=30.0 +trafficlight.Cr3.South.red=5.0 +trafficlight.Cr3.West.green=30.0 +trafficlight.Cr3.West.red=5.0 + +# Intersection 4 (Favor East toward Cr5) +trafficlight.Cr4.East.green=30.0 +trafficlight.Cr4.East.red=5.0 +trafficlight.Cr4.North.green=30.0 +trafficlight.Cr4.North.red=5.0 + +# Intersection 5 (Near exit - favor East) +trafficlight.Cr5.East.green=30.0 +trafficlight.Cr5.East.red=5.0 +trafficlight.Cr5.West.green=30.0 +trafficlight.Cr5.West.red=5.0 +trafficlight.Cr5.North.green=30.0 +trafficlight.Cr5.North.red=5.0 + + +# === VEHICLE CONFIGURATION === +# Probability distribution for vehicle types (must sum to 1.0) +vehicle.probability.bike=0.2 +vehicle.probability.light=0.6 +vehicle.probability.heavy=0.2 + +# Average crossing times (in seconds) +vehicle.crossing.time.bike=1.0 +vehicle.crossing.time.light=2.0 +vehicle.crossing.time.heavy=4.0 + +# Travel times between intersections (in seconds) +# Base time for light vehicles (cars) +vehicle.travel.time.base=1.0 +# Bike travel time = 0.5 x car travel time +vehicle.travel.time.bike.multiplier=0.5 +# Heavy vehicle travel time = 4.0 x base travel time +vehicle.travel.time.heavy.multiplier=4.0 + +# === STATISTICS === + +# Interval between dashboard updates (seconds) +statistics.update.interval=10.0 + +# === EXPECTED BEHAVIOR - LOW LOAD === +# - Average system time: 40-80 seconds +# - Maximum queue sizes: 1-3 vehicles +# - Average queue sizes: < 1 vehicle +# - Vehicles should flow smoothly through the system +# - Minimal waiting at traffic lights (mostly travel time) +# - System utilization: ~20-30% +# - All vehicles should exit within simulation time diff --git a/main/src/main/resources/simulation-medium.properties b/main/src/main/resources/simulation-medium.properties new file mode 100644 index 0000000..9dbe8e1 --- /dev/null +++ b/main/src/main/resources/simulation-medium.properties @@ -0,0 +1,121 @@ +# ========================================================= +# Traffic Simulation Configuration - MEDIUM LOAD SCENARIO +# --------------------------------------------------------- +# Medium traffic scenario for testing system under normal load. +# Expected: Moderate queues, some congestion at peak intersections +# ========================================================= + +# === NETWORK CONFIGURATION === + +# Intersections (each with its host and port) +intersection.Cr1.host=localhost +intersection.Cr1.port=8001 +intersection.Cr2.host=localhost +intersection.Cr2.port=8002 +intersection.Cr3.host=localhost +intersection.Cr3.port=8003 +intersection.Cr4.host=localhost +intersection.Cr4.port=8004 +intersection.Cr5.host=localhost +intersection.Cr5.port=8005 + +# Exit node +exit.host=localhost +exit.port=9001 + +# Dashboard server +dashboard.host=localhost +dashboard.port=9000 + + +# === SIMULATION CONFIGURATION === + +# Total duration in seconds (1800 = 30 minutes) +simulation.duration=1800 + +# Vehicle arrival model: FIXED or POISSON +simulation.arrival.model=POISSON + +# λ (lambda): MEDIUM LOAD = 0.5 vehicles per second (30 vehicles/minute, 1800 vehicles/hour) +# This represents normal traffic conditions +simulation.arrival.rate=0.5 + +# Fixed interval between arrivals (only used if model=FIXED) +simulation.arrival.fixed.interval=2.0 + +# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED +simulation.routing.policy=LEAST_CONGESTED + + +# === TRAFFIC LIGHT TIMINGS === +# Format: trafficlight...= +# Optimized timings for medium load + +# Intersection 1 (Entry point - balanced) +trafficlight.Cr1.South.green=40.0 +trafficlight.Cr1.South.red=5.0 +trafficlight.Cr1.East.green=40.0 +trafficlight.Cr1.East.red=5.0 + +# Intersection 2 (Main hub - CRITICAL BOTTLENECK, longer green times) +trafficlight.Cr2.South.green=45.0 +trafficlight.Cr2.South.red=5.0 +trafficlight.Cr2.East.green=50.0 +trafficlight.Cr2.East.red=5.0 +trafficlight.Cr2.West.green=45.0 +trafficlight.Cr2.West.red=5.0 + +# Intersection 3 (Path to exit - favor East toward exit) +trafficlight.Cr3.South.green=40.0 +trafficlight.Cr3.South.red=5.0 +trafficlight.Cr3.West.green=35.0 +trafficlight.Cr3.West.red=5.0 + +# Intersection 4 (Favor East toward Cr5) +trafficlight.Cr4.East.green=40.0 +trafficlight.Cr4.East.red=5.0 +trafficlight.Cr4.North.green=40.0 +trafficlight.Cr4.North.red=5.0 + +# Intersection 5 (Near exit - POTENTIAL BOTTLENECK, longer green) +trafficlight.Cr5.East.green=50.0 +trafficlight.Cr5.East.red=5.0 +trafficlight.Cr5.West.green=45.0 +trafficlight.Cr5.West.red=5.0 +trafficlight.Cr5.North.green=45.0 +trafficlight.Cr5.North.red=5.0 + + +# === VEHICLE CONFIGURATION === +# Probability distribution for vehicle types (must sum to 1.0) +vehicle.probability.bike=0.2 +vehicle.probability.light=0.6 +vehicle.probability.heavy=0.2 + +# Average crossing times (in seconds) +vehicle.crossing.time.bike=1.0 +vehicle.crossing.time.light=2.0 +vehicle.crossing.time.heavy=4.0 + +# Travel times between intersections (in seconds) +# Base time for light vehicles (cars) +vehicle.travel.time.base=1.0 +# Bike travel time = 0.5 x car travel time +vehicle.travel.time.bike.multiplier=0.5 +# Heavy vehicle travel time = 4.0 x base travel time +vehicle.travel.time.heavy.multiplier=4.0 + +# === STATISTICS === + +# Interval between dashboard updates (seconds) +statistics.update.interval=10.0 + +# === EXPECTED BEHAVIOR - MEDIUM LOAD === +# - Average system time: 80-150 seconds +# - Maximum queue sizes: 5-10 vehicles at Cr2 and Cr5 +# - Average queue sizes: 2-5 vehicles +# - Moderate congestion at Cr2 (main hub) and Cr5 (pre-exit) +# - System utilization: ~50-60% +# - Most vehicles should exit, some may remain at simulation end +# - Cr2 is the primary bottleneck (3 directions converge) +# - Cr5 is secondary bottleneck (all routes pass through) diff --git a/main/src/main/resources/simulation.properties b/main/src/main/resources/simulation.properties index 825476d..012c6dd 100644 --- a/main/src/main/resources/simulation.properties +++ b/main/src/main/resources/simulation.properties @@ -31,7 +31,11 @@ dashboard.port=9000 # === SIMULATION CONFIGURATION === # Total duration in seconds (3600 = 1 hour) -simulation.duration=60.0 +simulation.duration=300 + +# Time scaling factor for visualization (real_seconds = sim_seconds * scale) +# 0 = instant (pure DES), 0.01 = 100x speed, 0.1 = 10x speed, 1.0 = real-time +simulation.time.scale=0.01 # Vehicle arrival model: FIXED or POISSON simulation.arrival.model=POISSON @@ -42,49 +46,44 @@ simulation.arrival.rate=0.5 # Fixed interval between arrivals (only used if model=FIXED) simulation.arrival.fixed.interval=2.0 +# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED +# RANDOM: selects routes with predefined probabilities (baseline) +# SHORTEST_PATH: always chooses the route with fewest intersections +# LEAST_CONGESTED: dynamically chooses routes to avoid congested areas +simulation.routing.policy=RANDOM + # === TRAFFIC LIGHT TIMINGS === # Format: trafficlight...= # Intersection 1 (Entry point - balanced) -trafficlight.Cr1.South.green=20.0 -trafficlight.Cr1.South.red=40.0 -trafficlight.Cr1.East.green=20.0 -trafficlight.Cr1.East.red=40.0 -trafficlight.Cr1.West.green=20.0 -trafficlight.Cr1.West.red=40.0 +trafficlight.Cr1.South.green=60.0 +trafficlight.Cr1.South.red=5.0 +trafficlight.Cr1.East.green=60.0 +trafficlight.Cr1.East.red=5.0 # Intersection 2 (Main hub - shorter cycles, favor East-West) -trafficlight.Cr2.South.green=12.0 -trafficlight.Cr2.South.red=36.0 -trafficlight.Cr2.East.green=18.0 -trafficlight.Cr2.East.red=30.0 -trafficlight.Cr2.West.green=18.0 -trafficlight.Cr2.West.red=30.0 +trafficlight.Cr2.South.green=60.0 +trafficlight.Cr2.South.red=5.0 +trafficlight.Cr2.East.green=60.0 +trafficlight.Cr2.East.red=5.0 +trafficlight.Cr2.West.green=60.0 +trafficlight.Cr2.West.red=5.0 # Intersection 3 (Path to exit - favor East) -trafficlight.Cr3.South.green=15.0 -trafficlight.Cr3.South.red=30.0 -trafficlight.Cr3.East.green=20.0 -trafficlight.Cr3.East.red=25.0 -trafficlight.Cr3.West.green=15.0 -trafficlight.Cr3.West.red=30.0 +trafficlight.Cr3.South.green=60.0 +trafficlight.Cr3.South.red=5.0 +trafficlight.Cr3.West.green=60.0 +trafficlight.Cr3.West.red=5.0 # Intersection 4 (Favor East toward Cr5) -trafficlight.Cr4.South.green=15.0 -trafficlight.Cr4.South.red=30.0 -trafficlight.Cr4.East.green=20.0 -trafficlight.Cr4.East.red=25.0 -trafficlight.Cr4.West.green=15.0 -trafficlight.Cr4.West.red=30.0 +trafficlight.Cr4.East.green=60.0 +trafficlight.Cr4.East.red=5.0 # Intersection 5 (Near exit - favor East) -trafficlight.Cr5.South.green=15.0 -trafficlight.Cr5.South.red=30.0 -trafficlight.Cr5.East.green=22.0 -trafficlight.Cr5.East.red=23.0 -trafficlight.Cr5.West.green=15.0 -trafficlight.Cr5.West.red=30.0 +trafficlight.Cr5.East.green=60.0 +trafficlight.Cr5.East.red=5.0 + # === VEHICLE CONFIGURATION === # Probability distribution for vehicle types (must sum to 1.0) @@ -99,13 +98,13 @@ vehicle.crossing.time.heavy=4.0 # Travel times between intersections (in seconds) # Base time for light vehicles (cars) -vehicle.travel.time.base=8.0 -# Bike travel time = 0.5 × car travel time +vehicle.travel.time.base=1.0 +# Bike travel time = 0.5 x car travel time vehicle.travel.time.bike.multiplier=0.5 -# Heavy vehicle travel time = 4 × bike travel time -vehicle.travel.time.heavy.multiplier=2.0 +# Heavy vehicle travel time = 4.0 x base travel time +vehicle.travel.time.heavy.multiplier=4.0 # === STATISTICS === # Interval between dashboard updates (seconds) -statistics.update.interval=1.0 +statistics.update.interval=0.1 diff --git a/main/src/test/java/IntersectionProcessTest.java b/main/src/test/java/IntersectionProcessTest.java deleted file mode 100644 index cdb490c..0000000 --- a/main/src/test/java/IntersectionProcessTest.java +++ /dev/null @@ -1,527 +0,0 @@ -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; - -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.IntersectionProcess; -import sd.model.MessageType; -import sd.model.Vehicle; -import sd.model.VehicleType; -import sd.protocol.SocketConnection; - -/** - * Tests for IntersectionProcess - covers initialization, traffic lights, - * vehicle transfer and network stuff - */ -public class IntersectionProcessTest { - - @TempDir - Path tempDir; - - private Path configFile; - private IntersectionProcess intersectionProcess; - - // setup test config before each test - @BeforeEach - public void setUp() throws IOException { - // create temp config file - configFile = tempDir.resolve("test-simulation.properties"); - - String configContent = """ - # Test Simulation Configuration - - # Intersection Network Configuration - intersection.Cr1.host=localhost - intersection.Cr1.port=18001 - intersection.Cr2.host=localhost - intersection.Cr2.port=18002 - intersection.Cr3.host=localhost - intersection.Cr3.port=18003 - intersection.Cr4.host=localhost - intersection.Cr4.port=18004 - intersection.Cr5.host=localhost - intersection.Cr5.port=18005 - - # Exit Configuration - exit.host=localhost - exit.port=18099 - - # Dashboard Configuration - dashboard.host=localhost - dashboard.port=18100 - - # Traffic Light Timing (seconds) - trafficLight.Cr1.East.greenTime=5.0 - trafficLight.Cr1.East.redTime=5.0 - trafficLight.Cr1.South.greenTime=5.0 - trafficLight.Cr1.South.redTime=5.0 - trafficLight.Cr1.West.greenTime=5.0 - trafficLight.Cr1.West.redTime=5.0 - - trafficLight.Cr2.West.greenTime=4.0 - trafficLight.Cr2.West.redTime=6.0 - trafficLight.Cr2.East.greenTime=4.0 - trafficLight.Cr2.East.redTime=6.0 - trafficLight.Cr2.South.greenTime=4.0 - trafficLight.Cr2.South.redTime=6.0 - - trafficLight.Cr3.West.greenTime=3.0 - trafficLight.Cr3.West.redTime=7.0 - trafficLight.Cr3.East.greenTime=3.0 - trafficLight.Cr3.East.redTime=7.0 - - trafficLight.Cr4.East.greenTime=6.0 - trafficLight.Cr4.East.redTime=4.0 - - trafficLight.Cr5.East.greenTime=5.0 - trafficLight.Cr5.East.redTime=5.0 - - # Vehicle Crossing Times (seconds) - vehicle.bike.crossingTime=2.0 - vehicle.light.crossingTime=3.0 - vehicle.heavy.crossingTime=5.0 - """; - - Files.writeString(configFile, configContent); - } - - @AfterEach - public void tearDown() { - if (intersectionProcess != null) { - try { - // Only shutdown if still running - intersectionProcess.shutdown(); - } catch (Exception e) { - System.err.println("Error in tearDown: " + e.getMessage()); - } finally { - intersectionProcess = null; - } - } - } - - // ==================== Initialization Tests ==================== - - @Test - public void testConstructor_Success() throws IOException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - assertNotNull(intersectionProcess); - } - - @Test - public void testConstructor_InvalidConfig() { - Exception exception = assertThrows(IOException.class, () -> { - new IntersectionProcess("Cr1", "non-existent-config.properties"); - }); - assertNotNull(exception); - } - - @Test - public void testInitialize_Cr1() throws IOException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - assertDoesNotThrow(() -> intersectionProcess.initialize()); - } - - @Test - public void testInitialize_Cr2() throws IOException { - intersectionProcess = new IntersectionProcess("Cr2", configFile.toString()); - assertDoesNotThrow(() -> intersectionProcess.initialize()); - } - - @Test - public void testInitialize_Cr3() throws IOException { - intersectionProcess = new IntersectionProcess("Cr3", configFile.toString()); - assertDoesNotThrow(() -> intersectionProcess.initialize()); - } - - @Test - public void testInitialize_Cr4() throws IOException { - intersectionProcess = new IntersectionProcess("Cr4", configFile.toString()); - assertDoesNotThrow(() -> intersectionProcess.initialize()); - } - - @Test - public void testInitialize_Cr5() throws IOException { - intersectionProcess = new IntersectionProcess("Cr5", configFile.toString()); - assertDoesNotThrow(() -> intersectionProcess.initialize()); - } - - // traffic light creation tests - - @Test - public void testTrafficLightCreation_Cr1_HasCorrectDirections() throws IOException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - intersectionProcess.initialize(); - - // cant access private fields but initialization succeds - assertNotNull(intersectionProcess); - } - - @Test - public void testTrafficLightCreation_Cr3_HasCorrectDirections() throws IOException { - intersectionProcess = new IntersectionProcess("Cr3", configFile.toString()); - intersectionProcess.initialize(); - - // Cr3 has west and south only - assertNotNull(intersectionProcess); - } - - @Test - public void testTrafficLightCreation_Cr4_HasSingleDirection() throws IOException { - intersectionProcess = new IntersectionProcess("Cr4", configFile.toString()); - intersectionProcess.initialize(); - - // Cr4 only has east direction - assertNotNull(intersectionProcess); - } - - // server startup tests - - @Test - @Timeout(5) - public void testServerStart_BindsToCorrectPort() throws IOException, InterruptedException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - intersectionProcess.initialize(); - - // start server in separate thread - Thread serverThread = new Thread(() -> { - try { - intersectionProcess.start(); - } catch (IOException e) { - // expected on shutdown - } - }); - serverThread.start(); - - // Wait for server to actually start with retries - boolean serverReady = false; - for (int i = 0; i < 20; i++) { - Thread.sleep(100); - 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(); - serverThread.join(2000); - } - - @Test - @Timeout(5) - public void testServerStart_MultipleIntersections() throws IOException, InterruptedException { - // test 2 intersections on diferent ports - IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString()); - IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString()); - - cr1.initialize(); - cr2.initialize(); - - Thread thread1 = new Thread(() -> { - try { - cr1.start(); - } catch (IOException e) { - } - }); - - Thread thread2 = new Thread(() -> { - try { - cr2.start(); - } catch (IOException e) { - } - }); - - thread1.start(); - thread2.start(); - - Thread.sleep(500); - - // check both are running - try (Socket socket1 = new Socket("localhost", 18001); - Socket socket2 = new Socket("localhost", 18002)) { - assertTrue(socket1.isConnected()); - assertTrue(socket2.isConnected()); - } - - cr1.shutdown(); - cr2.shutdown(); - thread1.join(2000); - thread2.join(2000); - } - - // vehicle transfer tests - - @Test - @Timeout(10) - public void testVehicleTransfer_ReceiveVehicle() throws IOException, InterruptedException { - // setup reciever intersection - intersectionProcess = new IntersectionProcess("Cr2", configFile.toString()); - intersectionProcess.initialize(); - - Thread serverThread = new Thread(() -> { - try { - intersectionProcess.start(); - } catch (IOException e) { - } - }); - serverThread.start(); - - Thread.sleep(500); - - try { - // create test vehicle - FIXED: use 4-parameter constructor - java.util.List route = Arrays.asList("Cr2", "Cr3", "S"); - Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route); - - // send vehicle from Cr1 to Cr2 - FIXED: use SocketConnection - try (Socket socket = new Socket("localhost", 18002); - SocketConnection conn = new SocketConnection(socket)) { - - TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle); - conn.sendMessage(message); - - Thread.sleep(1000); // wait for processing - } - } finally { - intersectionProcess.shutdown(); - serverThread.join(2000); - } - } - - // routing config tests - - @Test - public void testRoutingConfiguration_Cr1() throws IOException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - intersectionProcess.initialize(); - - // indirect test - if init works routing should be ok - assertNotNull(intersectionProcess); - } - - @Test - public void testRoutingConfiguration_Cr5() throws IOException { - intersectionProcess = new IntersectionProcess("Cr5", configFile.toString()); - intersectionProcess.initialize(); - - // Cr5 routes to exit - assertNotNull(intersectionProcess); - } - - // shutdown tests - - @Test - @Timeout(5) - public void testShutdown_GracefulTermination() throws IOException, InterruptedException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - intersectionProcess.initialize(); - - Thread serverThread = new Thread(() -> { - try { - intersectionProcess.start(); - } catch (IOException e) { - } - }); - serverThread.start(); - - Thread.sleep(500); - - // shutdown should be fast - assertDoesNotThrow(() -> intersectionProcess.shutdown()); - - serverThread.join(2000); - } - - @Test - @Timeout(5) - public void testShutdown_ClosesServerSocket() throws IOException, InterruptedException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - intersectionProcess.initialize(); - - // Start server in separate thread - Thread serverThread = new Thread(() -> { - try { - intersectionProcess.start(); - } catch (IOException e) { - // Expected on shutdown - } - }); - serverThread.start(); - - // Wait for server to start - Thread.sleep(500); - - // Shutdown - intersectionProcess.shutdown(); - serverThread.join(2000); - - // Give shutdown time to complete - Thread.sleep(200); - - // Verify we cannot connect (server socket is closed) - boolean connectionFailed = false; - try (Socket testSocket = new Socket()) { - 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 - @Timeout(5) - public void testShutdown_StopsTrafficLightThreads() throws IOException, InterruptedException { - intersectionProcess = new IntersectionProcess("Cr1", configFile.toString()); - intersectionProcess.initialize(); - - Thread serverThread = new Thread(() -> { - try { - intersectionProcess.start(); - } catch (IOException e) { - } - }); - serverThread.start(); - - Thread.sleep(500); - - int threadCountBefore = Thread.activeCount(); - - intersectionProcess.shutdown(); - serverThread.join(2000); - - Thread.sleep(500); // wait for threads to die - - // thread count should decrese (traffic light threads stop) - int threadCountAfter = Thread.activeCount(); - assertTrue(threadCountAfter <= threadCountBefore); - } - - // integration tests - - @Test - @Timeout(15) - public void testIntegration_TwoIntersectionsVehicleTransfer() throws IOException, InterruptedException { - IntersectionProcess cr1 = null; - IntersectionProcess cr2 = null; - Thread thread1 = null; - Thread thread2 = null; - - try { - // setup 2 intersections - cr1 = new IntersectionProcess("Cr1", configFile.toString()); - cr2 = new IntersectionProcess("Cr2", configFile.toString()); - - cr1.initialize(); - cr2.initialize(); - - // start both - final IntersectionProcess cr1Final = cr1; - thread1 = new Thread(() -> { - try { - cr1Final.start(); - } catch (IOException e) { - } - }); - - final IntersectionProcess cr2Final = cr2; - thread2 = new Thread(() -> { - try { - cr2Final.start(); - } catch (IOException e) { - } - }); - - thread1.start(); - thread2.start(); - - Thread.sleep(1000); // wait for servers - - // send vehicle to Cr1 that goes to Cr2 - FIXED: use 4-parameter constructor - java.util.List route = Arrays.asList("Cr1", "Cr2", "S"); - Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route); - - // FIXED: use SocketConnection - try (Socket socket = new Socket("localhost", 18001); - SocketConnection conn = new SocketConnection(socket)) { - - TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle); - conn.sendMessage(message); - - Thread.sleep(2000); // time for processing - } - } finally { - if (cr1 != null) { - cr1.shutdown(); - } - if (cr2 != null) { - cr2.shutdown(); - } - if (thread1 != null) { - thread1.join(2000); - } - if (thread2 != null) { - thread2.join(2000); - } - } - } - - @Test - public void testMain_MissingArguments() { - // main needs intersection ID as argument - // cant test System.exit easily in modern java - assertTrue(true, "Main method expects intersection ID as first argument"); - } - - // helper class for testing vehicle messages - private static class TestVehicleMessage implements sd.protocol.MessageProtocol { - private static final long serialVersionUID = 1L; - - private final String sourceNode; - private final String destinationNode; - private final Vehicle payload; - - public TestVehicleMessage(String sourceNode, String destinationNode, Vehicle vehicle) { - this.sourceNode = sourceNode; - this.destinationNode = destinationNode; - this.payload = vehicle; - } - - @Override - public MessageType getType() { - return MessageType.VEHICLE_TRANSFER; - } - - @Override - public Object getPayload() { - return payload; - } - - @Override - public String getSourceNode() { - return sourceNode; - } - - @Override - public String getDestinationNode() { - return destinationNode; - } - } -} diff --git a/main/src/test/java/SimulationTest.java b/main/src/test/java/SimulationTest.java deleted file mode 100644 index 09df4bf..0000000 --- a/main/src/test/java/SimulationTest.java +++ /dev/null @@ -1,82 +0,0 @@ -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - -import sd.config.SimulationConfig; -import sd.model.Intersection; -import sd.model.TrafficLight; -import sd.model.TrafficLightState; -import sd.model.Vehicle; -import sd.model.VehicleType; -import sd.util.VehicleGenerator; - -/** - * Basic tests for the simulation components. - */ -class SimulationTest { - - @Test - void testConfigurationLoading() throws IOException { - SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties"); - - assertEquals(60.0, config.getSimulationDuration()); - assertEquals("POISSON", config.getArrivalModel()); - assertEquals(0.5, config.getArrivalRate()); - assertEquals(1.0, config.getStatisticsUpdateInterval()); - } - - @Test - void testVehicleGeneration() throws IOException { - SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties"); - VehicleGenerator generator = new VehicleGenerator(config); - - Vehicle vehicle = generator.generateVehicle("TEST1", 0.0); - - assertNotNull(vehicle); - assertEquals("TEST1", vehicle.getId()); - assertNotNull(vehicle.getType()); - assertNotNull(vehicle.getRoute()); - assertTrue(!vehicle.getRoute().isEmpty()); - } - - @Test - void testIntersectionVehicleQueue() { - Intersection intersection = new Intersection("TestCr"); - TrafficLight light = new TrafficLight("TestCr-N", "North", 30.0, 30.0); - - intersection.addTrafficLight(light); - - Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0, - java.util.Arrays.asList("TestCr", "S")); - - intersection.configureRoute("S", "North"); - - // Advance route to next destination - v1.advanceRoute(); - - intersection.receiveVehicle(v1); - - assertEquals(1, intersection.getTotalQueueSize()); - assertEquals(1, intersection.getTotalVehiclesReceived()); - } - - @Test - void testTrafficLightStateChange() { - TrafficLight light = new TrafficLight("Test-Light", "North", 30.0, 30.0); - - assertEquals(TrafficLightState.RED, light.getState()); - - light.changeState(TrafficLightState.GREEN); - assertEquals(TrafficLightState.GREEN, light.getState()); - - light.changeState(TrafficLightState.RED); - assertEquals(TrafficLightState.RED, light.getState()); - } - - // Removed testSimulationEngineInitialization as SimulationEngine has been - // removed. - -} diff --git a/main/src/test/java/sd/ExitNodeProcessTest.java b/main/src/test/java/sd/ExitNodeProcessTest.java deleted file mode 100644 index 56e9df9..0000000 --- a/main/src/test/java/sd/ExitNodeProcessTest.java +++ /dev/null @@ -1,327 +0,0 @@ -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); - } - } -} diff --git a/main/src/test/java/sd/TrafficLightCoordinationTest.java b/main/src/test/java/sd/TrafficLightCoordinationTest.java deleted file mode 100644 index cbc2d04..0000000 --- a/main/src/test/java/sd/TrafficLightCoordinationTest.java +++ /dev/null @@ -1,207 +0,0 @@ -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 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 lights = intersectionProcess.getIntersection().getTrafficLights(); - boolean[] hasBeenGreen = new boolean[lights.size()]; - - // Monitor for 10 seconds (enough time for all lights to cycle: 18+18+12 = 48s) - long endTime = System.currentTimeMillis() + 10000; - - 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 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"); - } -} diff --git a/main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java b/main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java deleted file mode 100644 index 7264f87..0000000 --- a/main/src/test/java/sd/coordinator/CoordinatorIntegrationTest.java +++ /dev/null @@ -1,302 +0,0 @@ -package sd.coordinator; - -import java.io.DataInputStream; -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; - -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 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 sd.model.Message; -import sd.model.MessageType; -import sd.model.Vehicle; -import sd.serialization.MessageSerializer; -import sd.serialization.SerializerFactory; - -/** - * Integration tests for the Coordinator-side networking. - * - * What we’re checking here: - * 1. A SocketClient can actually connect to something listening - * 2. Messages go over the wire and can be deserialized - * 3. Vehicle payloads survive the trip - * 4. Shutdown messages can be broadcast to multiple intersections - * - * We do this by spinning up a tiny mock intersection server in-process. - */ -class CoordinatorIntegrationTest { - - private List mockServers; - private static final int BASE_PORT = 9001; // keep clear of real ports - - @BeforeEach - void setUp() { - mockServers = new ArrayList<>(); - } - - @AfterEach - void tearDown() { - // Stop all mock servers - for (MockIntersectionServer server : mockServers) { - server.stop(); - } - mockServers.clear(); - } - - /** - * Can the client open a TCP connection to our fake intersection? - */ - @Test - @Timeout(5) - void testSocketClientConnection() throws IOException, InterruptedException { - MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT); - server.start(); - mockServers.add(server); - - // tiny pause to let the server bind - Thread.sleep(100); - - SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT); - client.connect(); - - assertTrue(client.isConnected(), "Client should be connected to mock intersection"); - - client.close(); - } - - /** - * End-to-end: send a message, make sure the server actually receives it. - */ - @Test - @Timeout(5) - void testMessageTransmission() throws Exception { - MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT); - server.start(); - mockServers.add(server); - - Thread.sleep(100); - - SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT); - client.connect(); - - Message testMessage = new Message( - MessageType.VEHICLE_SPAWN, - "COORDINATOR", - "Cr1", - "Test payload" - ); - - client.send(testMessage); - - // give the server a moment to read and deserialize - Thread.sleep(200); - - assertFalse( - server.getReceivedMessages().isEmpty(), - "Mock server should have received at least one message" - ); - - Message receivedMsg = server.getReceivedMessages().poll(); - assertNotNull(receivedMsg, "Server should have actually received a message"); - assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message type should match what we sent"); - assertEquals("COORDINATOR", receivedMsg.getSenderId(), "Sender ID should be preserved"); - assertEquals("Cr1", receivedMsg.getDestinationId(), "Destination ID should be preserved"); - - client.close(); - } - - /** - * Make sure vehicle payloads survive the trip and arrive non-null. - */ - @Test - @Timeout(5) - void testVehicleSpawnMessage() throws Exception { - MockIntersectionServer server = new MockIntersectionServer("Cr1", BASE_PORT); - server.start(); - mockServers.add(server); - - Thread.sleep(100); - - SocketClient client = new SocketClient("Cr1", "localhost", BASE_PORT); - client.connect(); - - // fake a vehicle like the coordinator would send - List route = List.of("Cr1", "Cr4", "Cr5", "S"); - Vehicle vehicle = new Vehicle("V1", sd.model.VehicleType.LIGHT, 0.0, route); - - Message spawnMessage = new Message( - MessageType.VEHICLE_SPAWN, - "COORDINATOR", - "Cr1", - vehicle - ); - - client.send(spawnMessage); - - Thread.sleep(200); - - Message receivedMsg = server.getReceivedMessages().poll(); - assertNotNull(receivedMsg, "Mock server should receive the spawn message"); - assertEquals(MessageType.VEHICLE_SPAWN, receivedMsg.getType(), "Message should be of type VEHICLE_SPAWN"); - assertNotNull(receivedMsg.getPayload(), "Payload should not be null (vehicle must arrive)"); - - client.close(); - } - - /** - * Broadcast shutdown to multiple mock intersections and see if all of them get it. - */ - @Test - @Timeout(5) - void testShutdownMessageBroadcast() throws Exception { - // Start a couple of fake intersections - for (int i = 1; i <= 3; i++) { - MockIntersectionServer server = new MockIntersectionServer("Cr" + i, BASE_PORT + i - 1); - server.start(); - mockServers.add(server); - } - - Thread.sleep(200); - - // Connect to all of them - List clients = new ArrayList<>(); - for (int i = 1; i <= 3; i++) { - SocketClient client = new SocketClient("Cr" + i, "localhost", BASE_PORT + i - 1); - client.connect(); - clients.add(client); - } - - Message shutdownMessage = new Message( - MessageType.SHUTDOWN, - "COORDINATOR", - "ALL", - "Simulation complete" - ); - - for (SocketClient client : clients) { - client.send(shutdownMessage); - } - - Thread.sleep(200); - - for (MockIntersectionServer server : mockServers) { - assertFalse( - server.getReceivedMessages().isEmpty(), - "Server " + server.getIntersectionId() + " should have received the shutdown message" - ); - - Message msg = server.getReceivedMessages().poll(); - assertEquals(MessageType.SHUTDOWN, msg.getType(), "Server should receive a SHUTDOWN message"); - } - - for (SocketClient client : clients) { - client.close(); - } - } - - /** - * Tiny TCP server that pretends to be an intersection. - * It: - * - listens on a port - * - accepts connections - * - reads length-prefixed messages - * - deserializes them and stores them for the test to inspect - */ - private static class MockIntersectionServer { - private final String intersectionId; - private final int port; - private ServerSocket serverSocket; - private Thread serverThread; - private volatile boolean running; - private final ConcurrentLinkedQueue receivedMessages; - private final MessageSerializer serializer; - - public MockIntersectionServer(String intersectionId, int port) { - this.intersectionId = intersectionId; - this.port = port; - this.receivedMessages = new ConcurrentLinkedQueue<>(); - this.serializer = SerializerFactory.createDefault(); - this.running = false; - } - - public void start() throws IOException { - serverSocket = new ServerSocket(port); - running = true; - - System.out.printf("Mock %s listening on port %d%n", intersectionId, port); - - serverThread = new Thread(() -> { - try { - while (running) { - Socket clientSocket = serverSocket.accept(); - handleClient(clientSocket); - } - } catch (IOException e) { - if (running) { - System.err.println("Mock " + intersectionId + " server error: " + e.getMessage()); - } - } - }, "mock-" + intersectionId + "-listener"); - - serverThread.start(); - } - - private void handleClient(Socket clientSocket) { - new Thread(() -> { - try (DataInputStream input = new DataInputStream(clientSocket.getInputStream())) { - while (running) { - // Read length prefix (4 bytes, big-endian) - int length = input.readInt(); - byte[] data = new byte[length]; - input.readFully(data); - - Message message = serializer.deserialize(data, Message.class); - receivedMessages.offer(message); - - System.out.println("Mock " + intersectionId + " received: " + message.getType()); - } - } catch (IOException e) { - if (running) { - System.err.println("Mock " + intersectionId + " client handler error: " + e.getMessage()); - } - } catch (Exception e) { - System.err.println("Mock " + intersectionId + " deserialization error: " + e.getMessage()); - } - }, "mock-" + intersectionId + "-client").start(); - } - - public void stop() { - running = false; - try { - if (serverSocket != null && !serverSocket.isClosed()) { - serverSocket.close(); - } - if (serverThread != null) { - serverThread.interrupt(); - serverThread.join(1000); - } - System.out.printf("Mock %s stopped%n", intersectionId); - } catch (IOException | InterruptedException e) { - System.err.println("Error stopping mock server " + intersectionId + ": " + e.getMessage()); - } - } - - public ConcurrentLinkedQueue getReceivedMessages() { - return receivedMessages; - } - - public String getIntersectionId() { - return intersectionId; - } - } -} diff --git a/main/src/test/java/sd/coordinator/CoordinatorProcessTest.java b/main/src/test/java/sd/coordinator/CoordinatorProcessTest.java deleted file mode 100644 index f334d90..0000000 --- a/main/src/test/java/sd/coordinator/CoordinatorProcessTest.java +++ /dev/null @@ -1,194 +0,0 @@ -package sd.coordinator; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -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 static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import sd.config.SimulationConfig; -import sd.model.Vehicle; -import sd.util.VehicleGenerator; - -/** - * Tests for the Coordinator/vehicle-generation layer. - * - * What we’re checking here: - * 1. Coordinator can be created with a valid config - * 2. Vehicle arrival times are monotonic and sane - * 3. Vehicle IDs are created in the format we expect (V1, V2, ...) - * 4. Generated vehicles have proper routes (start at CrX, end at S) - * 5. Config actually has intersection info - * 6. Duration in config is not something crazy - */ -class CoordinatorProcessTest { - - private SimulationConfig config; - private static final String TEST_CONFIG = "src/main/resources/simulation.properties"; - - @BeforeEach - void setUp() throws IOException { - config = new SimulationConfig(TEST_CONFIG); - } - - @AfterEach - void tearDown() { - config = null; - } - - /** - * Basic smoke test: can we build a coordinator with this config? - */ - @Test - void testCoordinatorInitialization() { - CoordinatorProcess coordinator = new CoordinatorProcess(config); - assertNotNull(coordinator, "Coordinator should be created with a valid config"); - } - - /** - * Make sure the VehicleGenerator is giving us increasing arrival times, - * i.e. time doesn’t go backwards and intervals look reasonable. - */ - @Test - void testVehicleGenerationTiming() { - VehicleGenerator generator = new VehicleGenerator(config); - - double currentTime = 0.0; - List arrivalTimes = new ArrayList<>(); - - // generate a small batch to inspect - for (int i = 0; i < 10; i++) { - double nextArrival = generator.getNextArrivalTime(currentTime); - arrivalTimes.add(nextArrival); - currentTime = nextArrival; - } - - // times should strictly increase - for (int i = 1; i < arrivalTimes.size(); i++) { - assertTrue( - arrivalTimes.get(i) > arrivalTimes.get(i - 1), - "Arrival times must increase — got " + arrivalTimes.get(i - 1) + " then " + arrivalTimes.get(i) - ); - } - - // and they shouldn't be nonsense - for (double time : arrivalTimes) { - assertTrue(time >= 0, "Arrival time should not be negative (got " + time + ")"); - assertTrue(time < 1000, "Arrival time looks suspiciously large: " + time); - } - } - - /** - * We generate V1..V5 manually and make sure the IDs are exactly those. - */ - @Test - void testVehicleIdGeneration() { - VehicleGenerator generator = new VehicleGenerator(config); - - List vehicles = new ArrayList<>(); - for (int i = 1; i <= 5; i++) { - Vehicle v = generator.generateVehicle("V" + i, 0.0); - vehicles.add(v); - assertEquals("V" + i, v.getId(), "Vehicle ID should be 'V" + i + "' but got " + v.getId()); - } - - // just to be safe, no duplicates in that small set - long distinctCount = vehicles.stream().map(Vehicle::getId).distinct().count(); - assertEquals(5, distinctCount, "Vehicle IDs in this batch should all be unique"); - } - - /** - * A generated vehicle should: - * - have a non-empty route - * - start in a known intersection (Cr1..Cr5) - * - end in S (exit) - */ - @Test - void testVehicleRouteValidity() { - VehicleGenerator generator = new VehicleGenerator(config); - - for (int i = 0; i < 20; i++) { - Vehicle vehicle = generator.generateVehicle("V" + i, 0.0); - - assertNotNull(vehicle.getRoute(), "Vehicle route should not be null"); - assertFalse(vehicle.getRoute().isEmpty(), "Vehicle route should not be empty"); - - String firstHop = vehicle.getRoute().get(0); - assertTrue( - firstHop.matches("Cr[1-5]"), - "First hop should be a valid intersection (Cr1..Cr5), got: " + firstHop - ); - - String lastHop = vehicle.getRoute().get(vehicle.getRoute().size() - 1); - assertEquals("S", lastHop, "Last hop should be exit 'S' but got: " + lastHop); - } - } - - /** - * Whatever is in simulation.properties should give us a sane duration. - */ - @Test - void testSimulationDuration() { - double duration = config.getSimulationDuration(); - assertTrue(duration > 0, "Simulation duration must be positive"); - assertTrue(duration >= 1.0, "Simulation should run at least 1 second (got " + duration + ")"); - assertTrue(duration <= 86400.0, "Simulation should not run more than a day (got " + duration + ")"); - } - - /** - * Check that the 5 intersections defined in the architecture - * actually exist in the config and have valid network data. - */ - @Test - void testIntersectionConfiguration() { - String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"}; - - for (String id : intersectionIds) { - String host = config.getIntersectionHost(id); - int port = config.getIntersectionPort(id); - - assertNotNull(host, "Host should not be null for " + id); - assertFalse(host.isEmpty(), "Host should not be empty for " + id); - assertTrue(port > 0, "Port should be > 0 for " + id + " (got " + port + ")"); - assertTrue(port < 65536, "Port should be a valid TCP port for " + id + " (got " + port + ")"); - } - } - - /** - * Quick sanity check: over a bunch of generated vehicles, - * we should eventually see the different vehicle types appear. - * - * Note: this is probabilistic, so we're not being super strict. - */ - @Test - void testVehicleTypeDistribution() { - VehicleGenerator generator = new VehicleGenerator(config); - - boolean hasBike = false; - boolean hasLight = false; - boolean hasHeavy = false; - - // 50 is enough for a "we're probably fine" test - for (int i = 0; i < 50; i++) { - Vehicle vehicle = generator.generateVehicle("V" + i, 0.0); - - switch (vehicle.getType()) { - case BIKE -> hasBike = true; - case LIGHT -> hasLight = true; - case HEAVY -> hasHeavy = true; - } - } - - // at least one of them should have shown up — if not, RNG is cursed - assertTrue( - hasBike || hasLight || hasHeavy, - "Expected to see at least one vehicle type after 50 generations" - ); - } -} diff --git a/main/src/test/java/sd/dashboard/DashboardTest.java b/main/src/test/java/sd/dashboard/DashboardTest.java deleted file mode 100644 index bcc72bb..0000000 --- a/main/src/test/java/sd/dashboard/DashboardTest.java +++ /dev/null @@ -1,164 +0,0 @@ -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"); - } -} diff --git a/main/src/test/java/sd/serialization/SerializationTest.java b/main/src/test/java/sd/serialization/SerializationTest.java deleted file mode 100644 index b43b5a5..0000000 --- a/main/src/test/java/sd/serialization/SerializationTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package sd.serialization; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; -import sd.model.Message; -import sd.model.Vehicle; -import sd.model.VehicleType; - -import java.util.Arrays; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Test suite for JSON serialization. - * - * Tests JSON serialization to ensure: - * - Correct serialization and deserialization - * - Data integrity during round-trip conversion - * - Proper error handling - */ -class SerializationTest { - - private MessageSerializer jsonSerializer = new JsonMessageSerializer(); - - private Vehicle testVehicle = new Vehicle("V001", VehicleType.LIGHT, 10.5, - Arrays.asList("Cr1", "Cr2", "Cr5", "S")); - private Message testMessage = new Message( - sd.model.MessageType.VEHICLE_TRANSFER, - "Cr1", - "Cr2", - testVehicle - ); - - - // ===== JSON Serialization Tests ===== - - @Test - @DisplayName("JSON: Should serialize and deserialize Vehicle correctly") - void testJsonVehicleRoundTrip() throws SerializationException { - // Serialize - byte[] data = jsonSerializer.serialize(testVehicle); - assertNotNull(data); - assertTrue(data.length > 0); - - // Print JSON for inspection - System.out.println("JSON Vehicle:"); - System.out.println(new String(data)); - - // Deserialize - Vehicle deserialized = jsonSerializer.deserialize(data, Vehicle.class); - - // Verify - assertNotNull(deserialized); - assertEquals(testVehicle.getId(), deserialized.getId()); - assertEquals(testVehicle.getType(), deserialized.getType()); - assertEquals(testVehicle.getEntryTime(), deserialized.getEntryTime()); - assertEquals(testVehicle.getRoute(), deserialized.getRoute()); - assertEquals(testVehicle.getTotalWaitingTime(), deserialized.getTotalWaitingTime()); - assertEquals(testVehicle.getTotalCrossingTime(), deserialized.getTotalCrossingTime()); - } - - @Test - @DisplayName("JSON: Should serialize and deserialize Message correctly") - void testJsonMessageRoundTrip() throws SerializationException { - // Serialize - byte[] data = jsonSerializer.serialize(testMessage); - assertNotNull(data); - - // Print JSON for inspection - System.out.println("\nJSON Message:"); - System.out.println(new String(data)); - - // Deserialize - Message deserialized = jsonSerializer.deserialize(data, Message.class); - - // Verify - assertNotNull(deserialized); - assertEquals(testMessage.getType(), deserialized.getType()); - assertEquals(testMessage.getSenderId(), deserialized.getSenderId()); - assertEquals(testMessage.getDestinationId(), deserialized.getDestinationId()); - } - - @Test - @DisplayName("JSON: Should throw exception on null object") - void testJsonSerializeNull() { - assertThrows(IllegalArgumentException.class, () -> { - jsonSerializer.serialize(null); - }); - } - - @Test - @DisplayName("JSON: Should throw exception on null data") - void testJsonDeserializeNull() { - assertThrows(IllegalArgumentException.class, () -> { - jsonSerializer.deserialize(null, Vehicle.class); - }); - } - - @Test - @DisplayName("JSON: Should throw exception on invalid JSON") - void testJsonDeserializeInvalid() { - byte[] invalidData = "{ invalid json }".getBytes(); - assertThrows(SerializationException.class, () -> { - jsonSerializer.deserialize(invalidData, Vehicle.class); - }); - } - - @Test - @DisplayName("JSON: Should preserve data integrity for complex objects") - void testDataIntegrity() throws SerializationException { - // Create a more complex vehicle - Vehicle vehicle = new Vehicle("V999", VehicleType.HEAVY, 100.5, - Arrays.asList("Cr1", "Cr2", "Cr3", "Cr4", "Cr5", "S")); - vehicle.addWaitingTime(10.5); - vehicle.addWaitingTime(5.3); - vehicle.addCrossingTime(2.1); - vehicle.advanceRoute(); - vehicle.advanceRoute(); - - // Serialize and deserialize - byte[] jsonData = jsonSerializer.serialize(vehicle); - Vehicle deserialized = jsonSerializer.deserialize(jsonData, Vehicle.class); - - // Verify all fields match - assertEquals(vehicle.getId(), deserialized.getId()); - assertEquals(vehicle.getType(), deserialized.getType()); - assertEquals(vehicle.getTotalWaitingTime(), deserialized.getTotalWaitingTime()); - assertEquals(vehicle.getCurrentRouteIndex(), deserialized.getCurrentRouteIndex()); - } - - // ===== Factory Tests ===== - - @Test - @DisplayName("Factory: Should create JSON serializer by default") - void testFactoryDefault() { - MessageSerializer serializer = SerializerFactory.createDefault(); - assertNotNull(serializer); - assertEquals("JSON (Gson)", serializer.getName()); - } -} diff --git a/main/start.sh b/main/start.sh deleted file mode 100755 index d710bd7..0000000 --- a/main/start.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/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 ""