17 Commits

Author SHA1 Message Date
926245986f finish 1st javadoc round 2025-12-07 22:39:21 +00:00
83c3d65e38 more javadoc - dashboard, des, logging 2025-12-07 19:57:40 +00:00
a8ce95e08c Refactor and enhance documentation across multiple classes (Analysis through DashboardStatistics) 2025-12-07 19:33:40 +00:00
a2f9e725de feat: Implement batch performance analysis dialog and routing policies
- Added BatchAnalysisDialog for running multiple simulations and generating reports.
- Implemented LeastCongestedRouteSelector for dynamic routing based on congestion levels.
- Created RandomRouteSelector for baseline random routing strategy.
- Developed ShortestPathRouteSelector to select routes based on the shortest path.
- Defined RouteSelector interface to standardize routing policy implementations.
- Introduced RoutingPolicy enum to manage available routing strategies.
2025-12-07 00:35:06 +00:00
92ff883d4c fixed dash formatting 2025-12-06 00:59:09 +00:00
ea33d61a9e removed tests 2025-12-05 02:42:31 +00:00
240563419b removed empty impl test files 2025-12-05 02:38:11 +00:00
90db380f61 Dash editor and DES impl 2025-12-05 02:29:33 +00:00
d28a77b6a4 small fixes + debug 2025-11-29 00:07:53 +00:00
173d9e54ce test: Reduce traffic light coordination test monitoring duration from 60s to 10s 2025-11-23 23:06:08 +00:00
5202032471 feat: Dynamically set simulation log file path using OS temporary directory and remove isSimulationRunning method. 2025-11-23 23:03:07 +00:00
46d148c9d5 Allow manual trigger for publish-release job 2025-11-23 22:23:13 +00:00
0d85d010bf Sync CI with main branch 2025-11-23 22:14:10 +00:00
906e958729 feat: Introduce Launcher class as the application entry point and update pom.xml to use it. 2025-11-23 21:53:52 +00:00
19709f0d7a feat: update main class to sd.dashboard.DashboardUI in pom.xml configurations. 2025-11-23 21:29:38 +00:00
13fa2f877d refactor: improve traffic light queue processing, add graceful intersection shutdown, and remove obsolete event and serialization classes. 2025-11-23 21:23:33 +00:00
96c5680f41 moved start to dashboard + fixed holding queue - looped sleep might be fine in this case + better customization via CSS file 2025-11-22 23:52:51 +00:00
84 changed files with 8310 additions and 5353 deletions

View File

@@ -1,8 +1,9 @@
name: Java CI with Maven name: Java CI with Maven
on: on:
workflow_dispatch:
push: push:
branches: [ "main" ] branches: [ "dev", "cleanup" ]
tags: tags:
- 'v*.*.*' - 'v*.*.*'
pull_request: pull_request:
@@ -11,51 +12,93 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
cache: maven cache: maven
- name: Build with Maven - name: Build with Maven
run: mvn -B package run: mvn -B package
working-directory: main working-directory: main
- name: Upload built JAR - name: Upload built JAR
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: package name: package
path: main/target/*.jar path: main/target/*.jar
- name: Generate dependency graph - name: Generate dependency graph
run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph
- name: Upload dependency graph artifact - name: Upload dependency graph artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: dependency-graph name: dependency-graph
path: main/target/** path: main/target/**
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven (Skip Tests)
run: mvn -B package -DskipTests
working-directory: main
- name: Create JPackage App Image
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "dist"
jpackage --name "DTSS" `
--input main/target `
--main-jar main-1.0-SNAPSHOT.jar `
--dest dist `
--type app-image `
--win-console
- name: Inject java.exe
shell: pwsh
run: |
$javaPath = (Get-Command java).Source
Copy-Item -Path $javaPath -Destination "dist/DTSS/runtime/bin/"
- name: Zip Windows Release
shell: pwsh
run: |
Compress-Archive -Path "dist/DTSS" -DestinationPath "dist/DTSS-Windows.zip"
- name: Upload Windows Artifact
uses: actions/upload-artifact@v4
with:
name: windows-package
path: dist/DTSS-Windows.zip
publish-release: publish-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build] needs: [build, build-windows]
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Download built JAR - name: Download Linux JAR
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: package name: package
path: main/target/ path: main/target/
- name: Download Windows Zip
uses: actions/download-artifact@v4
with:
name: windows-package
path: windows-dist/
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: main/target/*.jar tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'snapshot-build' }}
name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'Manual Snapshot Build' }}
draft: false
prerelease: true
make_latest: false
files: |
main/target/*.jar
windows-dist/*.zip

6
.gitignore vendored
View File

@@ -3,6 +3,9 @@
# Log files # Log files
*.log *.log
*.trace
logs
*.md
# BlueJ files # BlueJ files
*.ctxt *.ctxt
@@ -51,3 +54,6 @@ build/
# JAR built pom file # JAR built pom file
dependency-reduced-pom.xml dependency-reduced-pom.xml
# Python env
venv/

620
README.md
View File

@@ -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 ( 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

View File

@@ -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 (Cr1Cr5) 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 E1E3
* **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
Everythings working traffic lights, routing, vehicles, stats.
Ready to go distributed next.

198
TODO.md
View File

@@ -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.

View File

@@ -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
1 Execução VeículosGerados VeículosCompletados TaxaConclusão TempoMédioSistema TempoMédioEspera TempoMínimoSistema TempoMáximoSistema
2 1 1784 877 49.16 64.58 61.43 32.29 129.16
3 2 1782 363 20.37 53.77 51.01 26.88 107.53
4 3 1786 883 49.44 53.09 50.08 26.54 106.17
5 4 1845 179 9.70 63.92 60.27 31.96 127.84
6 5 1872 953 50.91 65.41 62.16 32.70 130.81

View File

@@ -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
================================================================================

View File

@@ -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
1 Execução VeículosGerados VeículosCompletados TaxaConclusão TempoMédioSistema TempoMédioEspera TempoMínimoSistema TempoMáximoSistema
2 1 371 187 50.40 42.28 38.65 21.14 84.57
3 2 361 263 72.85 29.15 25.29 14.57 58.30
4 3 368 197 53.53 38.02 33.95 19.01 76.04
5 4 350 239 68.29 32.38 28.36 16.19 64.75
6 5 373 212 56.84 23.36 19.96 11.68 46.73

View File

@@ -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
================================================================================

View File

@@ -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
1 Execução VeículosGerados VeículosCompletados TaxaConclusão TempoMédioSistema TempoMédioEspera TempoMínimoSistema TempoMáximoSistema
2 1 950 416 43.79 49.34 45.70 24.67 98.68
3 2 886 480 54.18 35.08 31.69 17.54 70.16
4 3 954 535 56.08 43.76 40.30 21.88 87.51
5 4 948 354 37.34 41.68 37.96 20.84 83.37
6 5 898 312 34.74 52.56 49.26 26.28 105.13

View File

@@ -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
================================================================================

169
main/graphing.py Normal file
View File

@@ -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(['Low', 'Medium', 'High'], dwelling_times, color=['green', 'orange', 'red'])
plt.ylabel('Average Dwelling Time (s)')
plt.title('System Performance vs Load')
plt.xlabel('Load Scenario')
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(['Low', 'Medium', 'High'], completion_rates, color=['green', 'orange', 'red'])
plt.ylabel('Completion Rate (%)')
plt.title('Vehicle Completion Rate vs Load')
plt.xlabel('Load Scenario')
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(['Low', 'Medium', 'High'], waiting_times, color=['green', 'orange', 'red'])
plt.ylabel('Average Waiting Time (s)')
plt.title('Average Waiting Time vs Load')
plt.xlabel('Load Scenario')
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 = ['Low', 'Medium', 'High']
# Vehicles generated
ax1.bar(loads, [low['VeículosGerados'].mean(), medium['VeículosGerados'].mean(), high['VeículosGerados'].mean()], color=['green', 'orange', 'red'])
ax1.set_title('Vehicles Generated')
ax1.set_ylabel('Count')
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('Vehicles Completed')
ax2.set_ylabel('Count')
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='Min', 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='Max', color='darkblue')
ax3.set_title('Min/Max Dwelling Time')
ax3.set_ylabel('Time (s)')
ax3.set_xticks(x)
ax3.set_xticklabels(loads)
ax3.legend()
ax3.grid(axis='y', alpha=0.3)
# Performance summary
metrics = ['Dwelling\nTime', 'Waiting\nTime', 'Completion\nRate']
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='Low', color='green')
ax4.bar(x, med_vals, width, label='Medium', color='orange')
ax4.bar([i + width for i in x], high_vals, width, label='High', color='red')
ax4.set_title('Performance Summary')
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -51,7 +51,7 @@
<artifactId>exec-maven-plugin</artifactId> <artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version> <version>3.1.0</version>
<configuration> <configuration>
<mainClass>sd.Entry</mainClass> <mainClass>sd.dashboard.Launcher</mainClass>
</configuration> </configuration>
</plugin> </plugin>
<!-- JavaFX Maven Plugin --> <!-- JavaFX Maven Plugin -->
@@ -60,7 +60,7 @@
<artifactId>javafx-maven-plugin</artifactId> <artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version> <version>0.0.8</version>
<configuration> <configuration>
<mainClass>sd.dashboard.DashboardUI</mainClass> <mainClass>sd.dashboard.Launcher</mainClass>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@@ -76,7 +76,7 @@
<configuration> <configuration>
<transformers> <transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>sd.Entry</mainClass> <mainClass>sd.dashboard.Launcher</mainClass>
</transformer> </transformer>
</transformers> </transformers>
</configuration> </configuration>

View File

@@ -12,6 +12,13 @@ import java.util.concurrent.TimeUnit;
import sd.config.SimulationConfig; import sd.config.SimulationConfig;
import sd.coordinator.SocketClient; import sd.coordinator.SocketClient;
import sd.dashboard.StatsUpdatePayload; 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.Message;
import sd.model.MessageType; import sd.model.MessageType;
import sd.model.Vehicle; import sd.model.Vehicle;
@@ -20,58 +27,62 @@ import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection; import sd.protocol.SocketConnection;
/** /**
* Processo responsável pelo nó de saída do sistema de simulação de tráfego * Ponto terminal da malha de simulação (Sink Node).
* distribuído. * <p>
* * Este processo atua como o sumidouro da rede de filas. A sua função primária é
* Este processo representa o ponto final ("S") onde os veículos completam as * a <b>coleta de telemetria final</b>. Diferente das interseções, não encaminha veículos;
* suas rotas. * em vez disso, retira-os do sistema, calcula as métricas de latência "end-to-end"
* As suas principais responsabilidades são: * (tempo no sistema, tempo de espera acumulado) e reporta ao Dashboard.
* - Receber veículos que terminam a sua rota vindos das interseções * <p>
* - Calcular e agregar estatísticas finais dos veículos * <b>Arquitetura de Concorrência:</b>
* - Enviar estatísticas periódicas para o dashboard * Utiliza um {@link ServerSocket} multithreaded para aceitar conexões simultâneas de
* - Gerar relatórios finais ao terminar a simulação * qualquer interseção de fronteira (Cr1, Cr5, etc.) que envie veículos para fora da malha.
*/ */
public class ExitNodeProcess { public class ExitNodeProcess {
private final SimulationConfig config; private final SimulationConfig config;
private ServerSocket serverSocket; private ServerSocket serverSocket;
/** Pool de threads elástica para tratamento de conexões de entrada. */
private final ExecutorService connectionHandlerPool; private final ExecutorService connectionHandlerPool;
/** // DES components
* Flag para controlar a execução do processo (volatile para visibilidade entre private final SimulationClock clock;
* threads) 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; private volatile boolean running;
/** Simulation start time (milliseconds) to calculate relative times */ /** Instante de início da simulação (milissegundos) sincronizado com o Coordenador. */
private long simulationStartMillis; private long simulationStartMillis;
/** Counter de veículos que completaram a rota */ /** Contador atómico (via synchronized) de throughput total. */
private int totalVehiclesReceived; private int totalVehiclesReceived;
/** Soma dos tempos no sistema de todos os veículos */ /** Tempo acumulado no sistema (System Time) de todos os veículos. */
private double totalSystemTime; private double totalSystemTime;
/** Soma dos tempos de espera de todos os veículos */ /** Tempo acumulado em espera (Waiting Time) de todos os veículos. */
private double totalWaitingTime; private double totalWaitingTime;
/** Soma dos tempos de travessia de todos os veículos */ /** Tempo acumulado em travessia (Service Time) de todos os veículos. */
private double totalCrossingTime; private double totalCrossingTime;
/** Contagem de veículos por tipo */ /** Agregação por categoria de veículo. */
private final Map<VehicleType, Integer> vehicleTypeCount; private final Map<VehicleType, Integer> vehicleTypeCount;
/** Tempo total de espera acumulado por tipo de veículo */ /** Latência acumulada por categoria. */
private final Map<VehicleType, Double> vehicleTypeWaitTime; private final Map<VehicleType, Double> vehicleTypeWaitTime;
/** Socket para comunicação com o dashboard */ /** Cliente TCP persistente para push de métricas ao Dashboard. */
private SocketClient dashboardClient; private SocketClient dashboardClient;
/** /**
* Método para iniciar o processo * Bootstrap do processo ExitNode.
* * Carrega configuração, inicializa subsistemas e entra no loop de serviço.
* @param args Argumentos da linha de comandos. Se fornecido, args[0] deve ser * * @param args Argumentos de CLI (caminho do config).
* o caminho para um ficheiro de configuração personalizado.
*/ */
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
@@ -79,6 +90,8 @@ public class ExitNodeProcess {
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
try { try {
EventLogger.getInstance().log(EventType.PROCESS_STARTED, "ExitNode", "Exit node process started");
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties"; String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
System.out.println("Loading configuration from: " + configFile); System.out.println("Loading configuration from: " + configFile);
@@ -93,22 +106,21 @@ public class ExitNodeProcess {
} catch (IOException e) { } catch (IOException e) {
System.err.println("Failed to start exit node: " + e.getMessage()); System.err.println("Failed to start exit node: " + e.getMessage());
EventLogger.getInstance().logError("ExitNode", "Failed to start", e);
System.exit(1); System.exit(1);
} catch (Exception e) { } catch (Exception e) {
System.err.println("Exit node error: " + e.getMessage()); System.err.println("Exit node error: " + e.getMessage());
EventLogger.getInstance().logError("ExitNode", "Exit node error", e);
System.exit(1); System.exit(1);
} finally {
EventLogger.getInstance().log(EventType.PROCESS_STOPPED, "ExitNode", "Exit node process stopped");
} }
} }
/** /**
* Constrói um novo processo de nó de saída. * Instancia o nó de saída.
* * Prepara os acumuladores estatísticos e a infraestrutura de logging distribuído.
* Inicializa todas as estruturas de dados necessárias para recolher * * @param config A configuração global da simulação.
* estatísticas
* e configura o pool de threads para processar as ligações concorrentes.
*
* @param config Configuração da simulação contendo portas e endereços dos
* serviços
*/ */
public ExitNodeProcess(SimulationConfig config) { public ExitNodeProcess(SimulationConfig config) {
this.config = config; this.config = config;
@@ -128,17 +140,22 @@ public class ExitNodeProcess {
vehicleTypeWaitTime.put(type, 0.0); 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(" - Exit port: " + config.getExitPort());
System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort()); System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
} }
/** /**
* Inicializa o processo de ligação ao dashboard. * Estabelece o canal de controlo (Control Plane) com o Dashboard.
* * Essencial para a visualização em tempo real das métricas de saída.
* Tenta conectar-se ao dashboard. Se a ligação falhar, o processo
* continua a funcionar normalmente, mas sem enviar estatísticas.
*
*/ */
public void initialize() { public void initialize() {
System.out.println("Connecting to dashboard..."); System.out.println("Connecting to dashboard...");
@@ -158,28 +175,137 @@ public class ExitNodeProcess {
} }
/** /**
* Inicia o socket e começa a aceitar ligações. * Inicia a thread de processamento de eventos DES.
* * Embora o ExitNode seja primariamente reativo (Network-driven), o motor DES
* Este é o loop principal do processo que: * é mantido para consistência de relógio e agendamento de fim de simulação.
* 1. Cria um socket na porta definida */
* 2. Aguarda pelas ligações das interseções private void startEventProcessor() {
* 3. Delega cada ligação a uma thread da pool para processamento assíncrono eventProcessorThread = new Thread(() -> {
* eventLogger.log(EventType.SIMULATION_STARTED, "ExitNode",
* @throws IOException Se o socket não puder ser criado ou houver erro na "Event processor thread started");
* aceitação
// 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 { 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(); int port = config.getExitPort();
serverSocket = new ServerSocket(port); serverSocket = new ServerSocket(port);
running = true; running = true;
simulationStartMillis = System.currentTimeMillis(); simulationStartMillis = System.currentTimeMillis();
System.out.println("Exit node started on port " + port); 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"); System.out.println("Waiting for vehicles...\\n");
// Loop de aceitação principal
while (running) { while (running) {
try { try {
Socket clientSocket = serverSocket.accept(); Socket clientSocket = serverSocket.accept();
// Delega o processamento da conexão para o Thread Pool
connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket)); connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
} catch (IOException e) { } catch (IOException e) {
if (running) { if (running) {
@@ -190,13 +316,11 @@ public class ExitNodeProcess {
} }
/** /**
* Processa uma ligação recebida de uma interseção. * Worker method para tratar uma conexão persistente vinda de uma interseção.
* * <p>
* Mantém a ligação aberta e processa continuamente mensagens do tipo * Mantém o socket aberto e consome mensagens num loop até que a conexão seja fechada
* VEHICLE_TRANSFER. Cada mensagem representa um veículo que chegou ao nó de * pelo remetente. Responsável pela desserialização polimórfica (JSON/Gson).
* saída. * * @param clientSocket O socket conectado.
*
* @param clientSocket Socket da ligação estabelecida com a interseção
*/ */
private void handleIncomingConnection(Socket clientSocket) { private void handleIncomingConnection(Socket clientSocket) {
String clientAddress = clientSocket.getInetAddress().getHostAddress(); String clientAddress = clientSocket.getInetAddress().getHostAddress();
@@ -212,14 +336,14 @@ public class ExitNodeProcess {
" from " + message.getSourceNode()); " from " + message.getSourceNode());
if (message.getType() == MessageType.SIMULATION_START) { 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(); simulationStartMillis = ((Number) message.getPayload()).longValue();
System.out.println("[Exit] Simulation start time synchronized"); System.out.println("[Exit] Simulation start time synchronized");
} else if (message.getType() == MessageType.VEHICLE_TRANSFER) { } else if (message.getType() == MessageType.VEHICLE_TRANSFER) {
Object payload = message.getPayload(); Object payload = message.getPayload();
System.out.println("[Exit] Payload type: " + payload.getClass().getName()); System.out.println("[Exit] Payload type: " + payload.getClass().getName());
// Handle Gson LinkedHashMap // Tratamento de artefatos de desserialização do Gson (LinkedTreeMap -> POJO)
Vehicle vehicle; Vehicle vehicle;
if (payload instanceof com.google.gson.internal.LinkedTreeMap || if (payload instanceof com.google.gson.internal.LinkedTreeMap ||
payload instanceof java.util.LinkedHashMap) { payload instanceof java.util.LinkedHashMap) {
@@ -252,27 +376,21 @@ public class ExitNodeProcess {
} }
/** /**
* Processa um veículo que chegou ao nó de saída. * Processa atomicamente a saída de um veículo.
* * <p>
* Método sincronizado para garantir thread-safety ao atualizar as estatísticas. * <b>Secção Crítica:</b> Método {@code synchronized} para garantir que a atualização
* Calcula as métricas finais do veículo e atualiza: * das estatísticas globais (totalSystemTime, contadores) é atómica, prevenindo
* - Counters globais; * Race Conditions quando múltiplos veículos chegam simultaneamente de interseções diferentes.
* - Estatísticas por tipo de veículo; * * @param vehicle O veículo que completou a rota.
* - Faz update ao dashboard a cada 10 veículos.
*
* @param vehicle Veículo que completou a sua rota
*/ */
private synchronized void processExitingVehicle(Vehicle vehicle) { private synchronized void processExitingVehicle(Vehicle vehicle) {
totalVehiclesReceived++; totalVehiclesReceived++;
// Calculate relative simulation time (seconds since simulation start) // Cálculo de métricas finais baseadas no tempo virtual de simulação acumulado no veículo
double currentSimTime = (System.currentTimeMillis() - simulationStartMillis) / 1000.0;
// System time = time vehicle spent in system (current time - entry time)
double systemTime = currentSimTime - vehicle.getEntryTime();
double waitTime = vehicle.getTotalWaitingTime(); double waitTime = vehicle.getTotalWaitingTime();
double crossingTime = vehicle.getTotalCrossingTime(); double crossingTime = vehicle.getTotalCrossingTime();
double systemTime = waitTime + crossingTime;
// Store times in seconds, will be converted to ms when sending to dashboard
totalSystemTime += systemTime; totalSystemTime += systemTime;
totalWaitingTime += waitTime; totalWaitingTime += waitTime;
totalCrossingTime += crossingTime; totalCrossingTime += crossingTime;
@@ -284,18 +402,20 @@ public class ExitNodeProcess {
System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs, crossing=%.2fs)%n", System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs, crossing=%.2fs)%n",
vehicle.getId(), vehicle.getType(), systemTime, waitTime, crossingTime); 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(); sendStatsToDashboard();
} }
/** /**
* Envia as estatísticas para o dashboard. * Constrói e transmite o DTO de atualização de estatísticas.
*
* Prepara e envia uma mensagem STATS_UPDATE com:
* - O total de veículos processados;
* - A média dos tempos (sistema, espera, travessia);
* - As contagens e médias por cada tipo de veículo.
*
*/ */
private void sendStatsToDashboard() { private void sendStatsToDashboard() {
if (dashboardClient == null || !dashboardClient.isConnected()) { if (dashboardClient == null || !dashboardClient.isConnected()) {
@@ -306,29 +426,28 @@ public class ExitNodeProcess {
// Create stats payload // Create stats payload
StatsUpdatePayload payload = new StatsUpdatePayload(); 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.setTotalVehiclesCompleted(totalVehiclesReceived);
payload.setTotalSystemTime((long) (totalSystemTime * 1000.0)); // s -> ms payload.setTotalSystemTime((long) (totalSystemTime * 1000.0));
payload.setTotalWaitingTime((long) (totalWaitingTime * 1000.0)); // s -> ms 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.setIntersectionArrivals(totalVehiclesReceived);
payload.setIntersectionDepartures(totalVehiclesReceived); payload.setIntersectionDepartures(totalVehiclesReceived);
payload.setIntersectionQueueSize(0); payload.setIntersectionQueueSize(0);
// Set vehicle type stats // Detailed breakdown
Map<VehicleType, Integer> typeCounts = new HashMap<>(); Map<VehicleType, Integer> typeCounts = new HashMap<>();
Map<VehicleType, Long> typeWaitTimes = new HashMap<>(); Map<VehicleType, Long> typeWaitTimes = new HashMap<>();
for (VehicleType type : VehicleType.values()) { for (VehicleType type : VehicleType.values()) {
typeCounts.put(type, vehicleTypeCount.get(type)); 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.setVehicleTypeCounts(typeCounts);
payload.setVehicleTypeWaitTimes(typeWaitTimes); payload.setVehicleTypeWaitTimes(typeWaitTimes);
// Send message
Message message = new Message( Message message = new Message(
MessageType.STATS_UPDATE, MessageType.STATS_UPDATE,
"ExitNode", "ExitNode",
@@ -347,14 +466,8 @@ public class ExitNodeProcess {
} }
/** /**
* Termina o processo * Encerramento gracioso do processo.
* * Fecha sockets, termina a pool de threads e liberta recursos.
* Executa a seguinte sequência:
* Imprime as estatísticas finais no terminal;
* Envia a última atualização de estatísticas ao dashboard;
* Fecha o socket;
* Aguarda pela finalização das threads;
* Fecha a ligação com o dashboard;
*/ */
public void shutdown() { public void shutdown() {
System.out.println("\n[Exit] Shutting down..."); System.out.println("\n[Exit] Shutting down...");
@@ -390,15 +503,7 @@ public class ExitNodeProcess {
} }
/** /**
* Imprime as estatísticas finais detalhadas no terminal * Imprime o relatório final no stdout.
*
* Gera um relatório com:
* Total de veículos que completaram a rota;
* Médias de tempo no sistema, espera e travessia;
* Distribuição e médias pelo tipo de veículo (BIKE, LIGHT, HEAVY);
*
* Este método é chamado durante o shutdown para fornecer um resumo
* da simulação antes de terminar o processo.
*/ */
private void printFinalStatistics() { private void printFinalStatistics() {
System.out.println("\n=== EXIT NODE STATISTICS ==="); System.out.println("\n=== EXIT NODE STATISTICS ===");

File diff suppressed because it is too large Load Diff

View File

@@ -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.
* <p>
* 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<SimulationRunResult> 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.
* <p>
* O relatório inclui:
* <ul>
* <li>Métricas globais (throughput, tempos de espera, tempos no sistema).</li>
* <li>Análise segmentada por tipo de veículo ({@link VehicleType}).</li>
* <li>Análise de gargalos por interseção (tamanhos de fila).</li>
* <li>Resumos brutos das execuções individuais.</li>
* </ul>
*
* @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<String> 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.
* <p>
* 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<Double> 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.
* <p>
* 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<Double> extractValues(java.util.function.Function<SimulationRunResult, Double> extractor) {
List<Double> 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.
* <p>
* 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.
* <p>
* 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()
);
}
}
}
}

View File

@@ -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.
* <p>
* 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<VehicleType, Integer> vehicleCountByType;
private final Map<VehicleType, Double> avgSystemTimeByType;
private final Map<VehicleType, Double> avgWaitTimeByType;
// Per-intersection metrics
private final Map<String, Integer> maxQueueSizeByIntersection;
private final Map<String, Double> avgQueueSizeByIntersection;
private final Map<String, Integer> 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<VehicleType, Integer> 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<VehicleType, Double> 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<VehicleType, Double> 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<String, Integer> 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<String, Double> 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<String, Integer> 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
);
}
}

View File

@@ -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.
* <p>
* 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<Double> 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).
* <p>
* 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<Double> 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.
* <p>
* 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<Double> 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).
* <p>
* 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<Double> 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<Double> values) {
if (values == null || values.isEmpty()) {
return 0.0;
}
return Collections.max(values);
}
/**
* Calcula a mediana da amostra.
* <p>
* <b>Nota de Desempenho:</b> 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<Double> values) {
if (values == null || values.isEmpty()) {
return 0.0;
}
List<Double> 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.
* <p>
* Ú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<Double> 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()
);
}
}

View File

@@ -14,19 +14,28 @@ import java.util.Properties;
import com.google.gson.Gson; import com.google.gson.Gson;
/** /**
* Class to load and manage simulation configurations. * Responsável pelo carregamento, validação e acesso centralizado às configurações da simulação.
* Configurations are read from a .properties file. This class provides * <p>
* type-safe getter methods for all expected configuration parameters, * Esta classe atua como uma fachada (Facade) para os parâmetros do sistema, abstraindo a origem
* with default values to ensure robustness. * 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 <i>classpath</i>.
* <p>
* 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 { public class SimulationConfig {
/** /** Armazenamento em memória das propriedades chave-valor carregadas. */
* Holds all properties loaded from the file.
*/
private final Properties properties; private final Properties properties;
/** Estrutura hierárquica da configuração da rede carregada via JSON. */
private NetworkConfig networkConfig; 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 { public static class NetworkConfig {
private List<IntersectionConfig> intersections; private List<IntersectionConfig> 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 { public static class IntersectionConfig {
private String id; private String id;
private List<String> lights; private List<String> lights;
private Map<String, String> routes; private Map<String, String> routes;
/** @return O identificador único da interseção (ex: "Cr1"). */
public String getId() { public String getId() {
return id; return id;
} }
/** @return Lista de identificadores dos semáforos associados a esta interseção. */
public List<String> getLights() { public List<String> getLights() {
return lights; return lights;
} }
/** @return Mapa de roteamento definindo destinos alcançáveis e seus próximos saltos. */
public Map<String, String> getRoutes() { public Map<String, String> getRoutes() {
return routes; return routes;
} }
} }
/** /**
* Constructs a new SimulationConfig object by loading properties * Inicializa o gestor de configuração carregando propriedades do caminho especificado.
* from the specified file path. * * <p>Implementa uma estratégia de carregamento em cascata (fallback) para garantir robustez
* em diferentes ambientes de execução (IDE, JAR, Docker):
* <ol>
* <li><b>Sistema de Ficheiros Direto:</b> Tenta carregar do caminho absoluto ou relativo.</li>
* <li><b>Classpath (Contexto):</b> Tenta carregar via {@code Thread.currentThread().getContextClassLoader()},
* normalizando prefixos como "src/main/resources" ou "classpath:".</li>
* <li><b>Classpath (Classe):</b> Tenta carregar via {@code SimulationConfig.class.getResourceAsStream},
* útil para recursos na raiz do JAR.</li>
* </ol>
* *
* This constructor attempts to load the configuration file using multiple * @param filePath O caminho ou nome do recurso do ficheiro {@code .properties}.
* strategies: * @throws IOException Se o ficheiro não puder ser localizado em nenhuma das estratégias,
* 1. Direct file system path * com uma mensagem detalhada das tentativas falhadas.
* 2. Classpath resource (with automatic path normalization)
* 3. Classpath resource with leading slash
*
* @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.
*/ */
public SimulationConfig(String filePath) throws IOException { public SimulationConfig(String filePath) throws IOException {
properties = new Properties(); properties = new Properties();
@@ -138,6 +155,12 @@ public class SimulationConfig {
throw new IOException(errorMsg.toString(), fileSystemException); throw new IOException(errorMsg.toString(), fileSystemException);
} }
/**
* Carrega a configuração da topologia de rede a partir do ficheiro "network_config.json".
* <p>
* 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() { private void loadNetworkConfig() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) { try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) {
if (is == null) { 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() { public NetworkConfig getNetworkConfig() {
return networkConfig; return networkConfig;
} }
@@ -161,56 +188,50 @@ public class SimulationConfig {
// --- Network configurations --- // --- Network configurations ---
/** /**
* Gets the host address for a specific intersection. * 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").
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @return O host configurado ou "localhost" por omissão.
* @return The host (e.g., "localhost").
*/ */
public String getIntersectionHost(String intersectionId) { public String getIntersectionHost(String intersectionId) {
return properties.getProperty("intersection." + intersectionId + ".host", "localhost"); return properties.getProperty("intersection." + intersectionId + ".host", "localhost");
} }
/** /**
* Gets the port number for a specific intersection. * Obtém a porta de escuta TCP para uma interseção específica.
* * * @param intersectionId O ID da interseção (ex: "Cr1").
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @return O número da porta. Retorna 0 se não configurado.
* @return The port number.
*/ */
public int getIntersectionPort(String intersectionId) { public int getIntersectionPort(String intersectionId) {
return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0")); return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0"));
} }
/** /**
* Gets the host address for the dashboard server. * Obtém o endereço de host do servidor de Dashboard (monitorização).
* * @return O host do dashboard (padrão: "localhost").
* @return The dashboard host.
*/ */
public String getDashboardHost() { public String getDashboardHost() {
return properties.getProperty("dashboard.host", "localhost"); return properties.getProperty("dashboard.host", "localhost");
} }
/** /**
* Gets the port number for the dashboard server. * Obtém a porta de conexão do servidor de Dashboard.
* * @return A porta do dashboard (padrão: 9000).
* @return The dashboard port.
*/ */
public int getDashboardPort() { public int getDashboardPort() {
return Integer.parseInt(properties.getProperty("dashboard.port", "9000")); return Integer.parseInt(properties.getProperty("dashboard.port", "9000"));
} }
/** /**
* Gets the host address for the exit node. * 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").
* @return The exit node host.
*/ */
public String getExitHost() { public String getExitHost() {
return properties.getProperty("exit.host", "localhost"); return properties.getProperty("exit.host", "localhost");
} }
/** /**
* Gets the port number for the exit node. * Obtém a porta de conexão do nó de saída.
* * @return A porta do nó de saída (padrão: 9001).
* @return The exit node port.
*/ */
public int getExitPort() { public int getExitPort() {
return Integer.parseInt(properties.getProperty("exit.port", "9001")); return Integer.parseInt(properties.getProperty("exit.port", "9001"));
@@ -219,50 +240,76 @@ public class SimulationConfig {
// --- Simulation configurations --- // --- Simulation configurations ---
/** /**
* Gets the total duration of the simulation in virtual seconds. * Define a duração total da execução da simulação em segundos virtuais.
* * @return A duração em segundos (padrão: 3600).
* @return The simulation duration.
*/ */
public double getSimulationDuration() { public double getSimulationDuration() {
return Double.parseDouble(properties.getProperty("simulation.duration", "3600.0")); return Double.parseDouble(properties.getProperty("simulation.duration", "3600"));
} }
/** /**
* Gets the vehicle arrival model ("POISSON" or "FIXED"). * Obtém o fator de escala temporal para visualização/execução.
* * <ul>
* @return The arrival model as a string. * <li>0.0: Execução instantânea (DES puro, velocidade máxima).</li>
* <li>1.0: Tempo real (1 segundo simulado = 1 segundo real).</li>
* <li>0.01: Acelerado 100x.</li>
* </ul>
* @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.
* <p>
* Este é o período adicional executado após o fim da geração de veículos para permitir
* que os veículos restantes no sistema completem os seus percursos.
* @return O tempo de drenagem (padrão: 60.0s).
*/
public double getDrainTime() {
return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0"));
}
/**
* Determina o modelo estocástico utilizado para a chegada de veículos.
* @return "POISSON" (distribuição exponencial) ou "FIXED" (intervalo determinístico).
*/ */
public String getArrivalModel() { public String getArrivalModel() {
return properties.getProperty("simulation.arrival.model", "POISSON"); return properties.getProperty("simulation.arrival.model", "POISSON");
} }
/** /**
* Gets the average arrival rate (lambda) for the POISSON model. * Obtém a taxa média de chegada (lambda) para o modelo Poisson.
* This represents the average number of vehicles arriving per second. * @return Veículos por segundo (padrão: 0.5).
*
* @return The arrival rate.
*/ */
public double getArrivalRate() { public double getArrivalRate() {
return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5")); return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5"));
} }
/** /**
* Gets the fixed time interval between vehicle arrivals for the FIXED model. * Obtém o intervalo fixo entre chegadas para o modelo determinístico.
* * @return O intervalo em segundos (padrão: 2.0).
* @return The fixed interval in seconds.
*/ */
public double getFixedArrivalInterval() { public double getFixedArrivalInterval() {
return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0")); 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 --- // --- Traffic light configurations ---
/** /**
* Gets the duration of the GREEN light state for a specific traffic light. * Obtém a duração do estado VERDE para um semáforo específico.
* * * @param intersectionId ID da interseção.
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @param direction Direção do fluxo (ex: "North").
* @param direction The direction of the light (e.g., "North"). * @return Duração em segundos (padrão: 30.0).
* @return The green light time in seconds.
*/ */
public double getTrafficLightGreenTime(String intersectionId, String direction) { public double getTrafficLightGreenTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".green"; String key = "trafficlight." + intersectionId + "." + direction + ".green";
@@ -270,11 +317,10 @@ public class SimulationConfig {
} }
/** /**
* Gets the duration of the RED light state for a specific traffic light. * Obtém a duração do estado VERMELHO para um semáforo específico.
* * * @param intersectionId ID da interseção.
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @param direction Direção do fluxo.
* @param direction The direction of the light (e.g., "North"). * @return Duração em segundos (padrão: 30.0).
* @return The red light time in seconds.
*/ */
public double getTrafficLightRedTime(String intersectionId, String direction) { public double getTrafficLightRedTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".red"; String key = "trafficlight." + intersectionId + "." + direction + ".red";
@@ -284,94 +330,84 @@ public class SimulationConfig {
// --- Vehicle configurations --- // --- Vehicle configurations ---
/** /**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type LIGHT. * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo LIGEIRO (LIGHT).
* * @return Probabilidade (padrão: 0.7).
* @return The probability for LIGHT vehicles.
*/ */
public double getLightVehicleProbability() { public double getLightVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7")); return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7"));
} }
/** /**
* Gets the average time it takes a LIGHT vehicle to cross an intersection. * Tempo médio necessário para um veículo LIGEIRO atravessar uma interseção.
* * @return Tempo em segundos (padrão: 2.0).
* @return The crossing time in seconds.
*/ */
public double getLightVehicleCrossingTime() { public double getLightVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0")); 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. * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo BICICLETA (BIKE).
* * @return Probabilidade (padrão: 0.0).
* @return The probability for BIKE vehicles.
*/ */
public double getBikeVehicleProbability() { public double getBikeVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0")); return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0"));
} }
/** /**
* Gets the average time it takes a BIKE vehicle to cross an intersection. * Tempo médio necessário para uma BICICLETA atravessar uma interseção.
* * @return Tempo em segundos (padrão: 1.5).
* @return The crossing time in seconds.
*/ */
public double getBikeVehicleCrossingTime() { public double getBikeVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5")); 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. * Probabilidade (0.0 a 1.0) de geração de um veículo PESADO (HEAVY).
* * @return Probabilidade (padrão: 0.0).
* @return The probability for HEAVY vehicles.
*/ */
public double getHeavyVehicleProbability() { public double getHeavyVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0")); return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0"));
} }
/** /**
* Gets the average time it takes a HEAVY vehicle to cross an intersection. * Tempo médio necessário para um veículo PESADO atravessar uma interseção.
* * @return Tempo em segundos (padrão: 4.0).
* @return The crossing time in seconds.
*/ */
public double getHeavyVehicleCrossingTime() { public double getHeavyVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0")); return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0"));
} }
/** /**
* Gets the base travel time between intersections for light vehicles. * Define o tempo base de viagem entre interseções para veículos padrão.
* * @return Tempo em segundos (padrão: 8.0).
* @return The base travel time in seconds.
*/ */
public double getBaseTravelTime() { public double getBaseTravelTime() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0")); return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0"));
} }
/** /**
* Gets the travel time multiplier for bike vehicles. * Multiplicador de tempo de viagem para bicicletas.
* Bike travel time = base time × this multiplier. * <p>Tempo efetivo = Base * Multiplicador.
* * @return Fator multiplicativo (padrão: 0.5).
* @return The multiplier for bike travel time.
*/ */
public double getBikeTravelTimeMultiplier() { public double getBikeTravelTimeMultiplier() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5")); return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5"));
} }
/** /**
* Gets the travel time multiplier for heavy vehicles. * Multiplicador de tempo de viagem para veículos pesados.
* Heavy vehicle travel time = base time × this multiplier. * <p>Tempo efetivo = Base * Multiplicador.
* * @return Fator multiplicativo (padrão: 4.0).
* @return The multiplier for heavy vehicle travel time.
*/ */
public double getHeavyTravelTimeMultiplier() { 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 --- // --- Statistics ---
/** /**
* Gets the interval (in virtual seconds) between periodic statistics updates. * 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).
* @return The statistics update interval.
*/ */
public double getStatisticsUpdateInterval() { public double getStatisticsUpdateInterval() {
return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0")); return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0"));
@@ -380,21 +416,19 @@ public class SimulationConfig {
// --- Generic getters --- // --- Generic getters ---
/** /**
* Generic method to get any property as a string, with a default value. * Recupera uma propriedade genérica como String, com valor padrão de segurança.
* * * @param key A chave da propriedade.
* @param key The property key. * @param defaultValue O valor a retornar caso a chave não exista.
* @param defaultValue The value to return if the key is not found. * @return O valor da propriedade ou o default.
* @return The property value or the default.
*/ */
public String getProperty(String key, String defaultValue) { public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue); return properties.getProperty(key, defaultValue);
} }
/** /**
* Generic method to get any property as a string. * Recupera uma propriedade genérica como String.
* * * @param key A chave da propriedade.
* @param key The property key. * @return O valor da propriedade ou null se não encontrada.
* @return The property value, or null if not found.
*/ */
public String getProperty(String key) { public String getProperty(String key) {
return properties.getProperty(key); return properties.getProperty(key);

View File

@@ -5,32 +5,72 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import sd.config.SimulationConfig; import sd.config.SimulationConfig;
import sd.dashboard.DashboardStatistics;
import sd.dashboard.StatsUpdatePayload; 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.Message;
import sd.model.MessageType; import sd.model.MessageType;
import sd.model.Vehicle; 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.serialization.SerializationException;
import sd.util.VehicleGenerator; import sd.util.VehicleGenerator;
/** /**
* Coordinator process responsible for: * Coordenador central da arquitetura de simulação distribuída.
* 1. Vehicle generation (using VehicleGenerator) * <p>
* 2. Distributing vehicles to intersection processes via sockets * Este processo atua como o "cérebro" da simulação, sendo responsável por:
* 3. Managing simulation timing and shutdown * <ol>
* * <li><b>Orquestração DES:</b> Gerir o relógio global ({@link SimulationClock}) e a fila de eventos prioritária.</li>
* This is the main entry point for the distributed simulation architecture. * <li><b>Geração de Carga:</b> Injetar veículos na malha viária seguindo distribuições estocásticas (Poisson) ou determinísticas.</li>
* <li><b>Encaminhamento Dinâmico:</b> Decidir as rotas dos veículos com base na política ativa (Random, Shortest Path, Least Congested).</li>
* <li><b>Sincronização:</b> Garantir que todos os nós (Interseções e Dashboard) operem em uníssono.</li>
* </ol>
*/ */
public class CoordinatorProcess { public class CoordinatorProcess {
private final SimulationConfig config; private final SimulationConfig config;
private final VehicleGenerator vehicleGenerator; private final VehicleGenerator vehicleGenerator;
/** Mapa de clientes TCP persistentes para cada interseção (Worker Nodes). */
private final Map<String, SocketClient> intersectionClients; private final Map<String, SocketClient> intersectionClients;
private SocketClient dashboardClient; 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 int vehicleCounter;
private boolean running; 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.
* <p>
* 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<String, Integer> 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) { public static void main(String[] args) {
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION"); System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
@@ -61,28 +101,83 @@ 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) { public CoordinatorProcess(SimulationConfig config) {
this.config = 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.intersectionClients = new HashMap<>();
this.currentTime = 0.0;
this.vehicleCounter = 0; this.vehicleCounter = 0;
this.running = false; 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("Coordinator initialized with configuration:");
System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s"); System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s");
System.out.println(" - Arrival model: " + config.getArrivalModel()); System.out.println(" - Arrival model: " + config.getArrivalModel());
System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s"); 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() { public void initialize() {
// Connect to dashboard first // Connect to dashboard first
connectToDashboard(); connectToDashboard();
System.out.println("Connecting to intersection processes..."); System.out.println("Connecting to intersection processes...");
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"}; String[] intersectionIds = { "Cr1", "Cr2", "Cr3", "Cr4", "Cr5" };
for (String intersectionId : intersectionIds) { for (String intersectionId : intersectionIds) {
try { try {
@@ -105,40 +200,170 @@ public class CoordinatorProcess {
} }
} }
/**
* Loop principal da simulação (DES Engine).
* <p>
* 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() { public void run() {
double duration = config.getSimulationDuration(); double duration = config.getSimulationDuration();
double drainTime = config.getDrainTime();
double totalDuration = duration + drainTime;
running = true; running = true;
System.out.println("Starting vehicle generation simulation..."); System.out.println("Starting DES-based vehicle generation simulation...");
System.out.println("Duration: " + duration + " seconds"); System.out.println("Duration: " + duration + "s (+ " + drainTime + "s drain)");
System.out.println(); 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 // Send simulation start time to all processes for synchronization
sendSimulationStartTime(); sendSimulationStartTime();
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); // Schedule first vehicle generation event
final double TIME_STEP = 0.1; double firstArrivalTime = vehicleGenerator.getNextArrivalTime(clock.getCurrentTime());
eventQueue.schedule(new SimulationEvent(
firstArrivalTime,
DESEventType.VEHICLE_GENERATION,
null,
"Coordinator"));
while (running && currentTime < duration) { // Schedule simulation end event
if (currentTime >= nextGenerationTime) { eventQueue.schedule(new SimulationEvent(
generateAndSendVehicle(); totalDuration,
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime); DESEventType.SIMULATION_END,
null,
"Coordinator"));
System.out.printf("Initial event scheduled at t=%.3fs\n", firstArrivalTime);
System.out.println("Entering DES event loop...\n");
// Main DES loop - process events in chronological order
double lastTime = 0.0;
while (running && !eventQueue.isEmpty()) {
SimulationEvent event = eventQueue.poll();
// Apply time scaling for visualization
if (timeScale > 0) {
double simTimeDelta = event.getTimestamp() - lastTime;
long realDelayMs = (long) (simTimeDelta * timeScale * 1000);
if (realDelayMs > 0) {
try {
Thread.sleep(realDelayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
lastTime = event.getTimestamp();
} }
currentTime += TIME_STEP;
// Advance simulation time to event time
clock.advanceTo(event.getTimestamp());
// Process the event
processEvent(event, duration);
} }
System.out.println(); 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 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(); 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() { 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", System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute()); currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
// 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 // Send generation count to dashboard
sendGenerationStatsToDashboard(); sendGenerationStatsToDashboard();
@@ -152,6 +377,9 @@ public class CoordinatorProcess {
sendVehicleToIntersection(vehicle, entryIntersection); sendVehicleToIntersection(vehicle, entryIntersection);
} }
/**
* Serializa e transmite o objeto Veículo para o nó (interseção) de destino.
*/
private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) { private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
SocketClient client = intersectionClients.get(intersectionId); SocketClient client = intersectionClients.get(intersectionId);
@@ -162,11 +390,10 @@ public class CoordinatorProcess {
try { try {
Message message = new Message( Message message = new Message(
MessageType.VEHICLE_SPAWN, MessageType.VEHICLE_SPAWN,
"COORDINATOR", "COORDINATOR",
intersectionId, intersectionId,
vehicle vehicle);
);
client.send(message); client.send(message);
System.out.printf("->Sent to %s%n", intersectionId); System.out.printf("->Sent to %s%n", intersectionId);
@@ -177,6 +404,9 @@ public class CoordinatorProcess {
} }
} }
/**
* Encerra graciosamente a simulação, enviando sinais de SHUTDOWN para todos os nós.
*/
public void shutdown() { public void shutdown() {
System.out.println(); System.out.println();
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
@@ -189,11 +419,10 @@ public class CoordinatorProcess {
try { try {
if (client.isConnected()) { if (client.isConnected()) {
Message personalizedShutdown = new Message( Message personalizedShutdown = new Message(
MessageType.SHUTDOWN, MessageType.SHUTDOWN,
"COORDINATOR", "COORDINATOR",
intersectionId, intersectionId,
"Simulation complete" "Simulation complete");
);
client.send(personalizedShutdown); client.send(personalizedShutdown);
System.out.println("Sent shutdown message to " + intersectionId); System.out.println("Sent shutdown message to " + intersectionId);
} }
@@ -213,6 +442,65 @@ public class CoordinatorProcess {
running = false; 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() { private void connectToDashboard() {
try { try {
String host = config.getDashboardHost(); String host = config.getDashboardHost();
@@ -239,19 +527,23 @@ public class CoordinatorProcess {
payload.setTotalVehiclesGenerated(vehicleCounter); payload.setTotalVehiclesGenerated(vehicleCounter);
Message message = new Message( Message message = new Message(
MessageType.STATS_UPDATE, MessageType.STATS_UPDATE,
"COORDINATOR", "COORDINATOR",
"Dashboard", "Dashboard",
payload payload);
);
dashboardClient.send(message); dashboardClient.send(message);
} catch (Exception e) { //This is fine - can add IOException if need be } catch (Exception e) { // This is fine - can add IOException if need be
// Don't crash if dashboard update fails // Don't crash if dashboard update fails
System.err.println("Failed to send stats to dashboard: " + e.getMessage()); System.err.println("Failed to send stats to dashboard: " + e.getMessage());
} }
} }
/**
* Sincronização Global: Envia o timestamp de início (System.currentTimeMillis)
* para todos os componentes distribuídos, garantindo uma base de tempo comum
* para métricas de latência.
*/
private void sendSimulationStartTime() { private void sendSimulationStartTime() {
long startTimeMillis = System.currentTimeMillis(); long startTimeMillis = System.currentTimeMillis();
@@ -259,11 +551,10 @@ public class CoordinatorProcess {
for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) { for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
try { try {
Message message = new Message( Message message = new Message(
MessageType.SIMULATION_START, MessageType.SIMULATION_START,
"COORDINATOR", "COORDINATOR",
entry.getKey(), entry.getKey(),
startTimeMillis startTimeMillis);
);
entry.getValue().send(message); entry.getValue().send(message);
} catch (Exception e) { // Same thing here } catch (Exception e) { // Same thing here
System.err.println("Failed to send start time to " + entry.getKey() + ": " + e.getMessage()); System.err.println("Failed to send start time to " + entry.getKey() + ": " + e.getMessage());
@@ -274,11 +565,10 @@ public class CoordinatorProcess {
if (dashboardClient != null && dashboardClient.isConnected()) { if (dashboardClient != null && dashboardClient.isConnected()) {
try { try {
Message message = new Message( Message message = new Message(
MessageType.SIMULATION_START, MessageType.SIMULATION_START,
"COORDINATOR", "COORDINATOR",
"Dashboard", "Dashboard",
startTimeMillis startTimeMillis);
);
dashboardClient.send(message); dashboardClient.send(message);
} catch (Exception e) { // And here } catch (Exception e) { // And here
// Don't crash // Don't crash

View File

@@ -10,10 +10,14 @@ import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory; import sd.serialization.SerializerFactory;
/** /**
* Socket client for communication with a single intersection process. * Abstração de cliente TCP para comunicação outbound (de saída) com nós da rede.
* * <p>
* Handles a persistent TCP connection to one intersection, * Esta classe encapsula a gestão do socket raw, oferecendo uma interface de alto nível
* providing a simple way to send serialized messages. * 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.
* <p>
* É utilizada pelo Coordenador para controlar Interseções e enviar telemetria para o Dashboard.
*/ */
public class SocketClient { public class SocketClient {
@@ -25,11 +29,11 @@ public class SocketClient {
private MessageSerializer serializer; 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 intersectionId Identificador lógico do nó de destino (ex: "Cr1", "Dashboard").
* @param host Host address (ex. "localhost") * @param host Endereço IP ou hostname do destino.
* @param port Port number * @param port Porta TCP de escuta do destino.
*/ */
public SocketClient(String intersectionId, String host, int port) { public SocketClient(String intersectionId, String host, int port) {
this.intersectionId = intersectionId; this.intersectionId = intersectionId;
@@ -39,11 +43,9 @@ public class SocketClient {
} }
/** /**
* Connects to the intersection process via TCP. * 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.
* @throws IOException if the connection cannot be established
*/ */
public void connect() throws IOException { public void connect() throws IOException {
try { try {
socket = new Socket(host, port); socket = new Socket(host, port);
@@ -56,12 +58,22 @@ public class SocketClient {
} }
/** /**
* Sends a message to the connected intersection. * Serializa e transmite uma mensagem através do socket conectado.
* The message is serialized and written over the socket. * <p>
* <b>Protocolo de Envio (Length-Prefix Framing):</b>
* <ol>
* <li>Serializa o objeto {@link Message} para um array de bytes.</li>
* <li>Calcula o tamanho (N) do array.</li>
* <li>Escreve um cabeçalho de 4 bytes contendo N (Big-Endian).</li>
* <li>Escreve os N bytes do payload (corpo da mensagem).</li>
* <li>Realiza flush no stream para forçar o envio imediato do pacote TCP.</li>
* </ol>
* 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 * @param message O objeto de domínio a ser enviado.
* @throws SerializationException if serialization fails * @throws SerializationException Se o objeto não puder ser convertido para bytes.
* @throws IOException if the socket write fails * @throws IOException Se houver falha na escrita do socket (ex: conexão resetada).
*/ */
public void send(Message message) throws SerializationException, IOException { public void send(Message message) throws SerializationException, IOException {
if (socket == null || socket.isClosed()) { if (socket == null || socket.isClosed()) {
@@ -71,13 +83,14 @@ public class SocketClient {
try { try {
byte[] data = serializer.serialize(message); byte[] data = serializer.serialize(message);
// Prefix with message length (so receiver knows how much to read)
int length = data.length; int length = data.length;
// Write 4-byte length header (Big Endian)
outputStream.write((length >> 24) & 0xFF); outputStream.write((length >> 24) & 0xFF);
outputStream.write((length >> 16) & 0xFF); outputStream.write((length >> 16) & 0xFF);
outputStream.write((length >> 8) & 0xFF); outputStream.write((length >> 8) & 0xFF);
outputStream.write(length & 0xFF); outputStream.write(length & 0xFF);
// Write payload
outputStream.write(data); outputStream.write(data);
outputStream.flush(); outputStream.flush();
@@ -88,8 +101,10 @@ public class SocketClient {
} }
/** /**
* Closes the socket connection safely. * Realiza o encerramento gracioso (graceful shutdown) da conexão.
* Calling it multiple times wont cause issues. * Liberta os recursos do sistema operativo (descritores de arquivo).
* <p>
* Operação idempotente: pode ser chamada múltiplas vezes sem erro.
*/ */
public void close() { public void close() {
try { 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() { public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed(); return socket != null && socket.isConnected() && !socket.isClosed();

View File

@@ -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).
* <p>
* 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:
* <ol>
* <li>Orquestrar o ciclo de vida dos processos de simulação (start/stop/wait).</li>
* <li>Coletar métricas estatísticas de cada execução.</li>
* <li>Agregar resultados usando o {@link MultiRunAnalyzer}.</li>
* <li>Gerar relatórios consolidados para análise de variância e intervalos de confiança.</li>
* </ol>
* 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<Integer> 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<Integer> 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<Integer> runsSpinner = (Spinner<Integer>) dialog.getScene().lookup("#runsSpinner");
Spinner<Integer> durationSpinner = (Spinner<Integer>) 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);
});
}
}

View File

@@ -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.
* <p>
* 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:
* <ul>
* <li><b>Geração de Carga:</b> Alternância entre modelos estocásticos (Poisson) e determinísticos.</li>
* <li><b>Temporização:</b> Ajuste da escala de tempo (Time Scale) para visualização vs. performance pura.</li>
* <li><b>Mix de Veículos:</b> Definição das probabilidades de geração por tipo de agente.</li>
* </ul>
*/
public class ConfigurationDialog {
/**
* Exibe o diálogo de configuração avançada e captura as intenções do utilizador.
* <p>
* 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<ButtonType> 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<String> 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<Double> 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<Double> 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<Integer> 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<Double> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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);
}
}

View File

@@ -9,19 +9,42 @@ import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection; import sd.protocol.SocketConnection;
/** /**
* Processes statistics messages from a single client connection. * Worker responsável pelo processamento dedicado de uma conexão de cliente TCP no Dashboard.
* Runs in a separate thread per client. * <p>
* Esta classe implementa o padrão <i>Thread-per-Client</i>. 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.
* <p>
* As suas principais funções são:
* <ol>
* <li>Manter a conexão persistente com o nó remoto.</li>
* <li>Desserializar mensagens de protocolo recebidas.</li>
* <li>Normalizar payloads JSON (resolvendo ambiguidades de tipagem do Gson).</li>
* <li>Atualizar o objeto partilhado {@link DashboardStatistics} de forma thread-safe.</li>
* </ol>
*/ */
public class DashboardClientHandler implements Runnable { public class DashboardClientHandler implements Runnable {
private final Socket clientSocket; private final Socket clientSocket;
private final DashboardStatistics statistics; 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) { public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) {
this.clientSocket = clientSocket; this.clientSocket = clientSocket;
this.statistics = statistics; this.statistics = statistics;
} }
/**
* Ciclo de vida da conexão.
* <p>
* 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 @Override
public void run() { public void run() {
String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort(); 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.
* <p>
* 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) { private void processMessage(MessageProtocol message) {
if (message.getType() != MessageType.STATS_UPDATE) { if (message.getType() != MessageType.STATS_UPDATE) {
System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType()); System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType());
@@ -78,6 +111,7 @@ public class DashboardClientHandler implements Runnable {
stats = (StatsUpdatePayload) payload; stats = (StatsUpdatePayload) payload;
} else if (payload instanceof java.util.Map) { } else if (payload instanceof java.util.Map) {
// Gson deserialized as LinkedHashMap - re-serialize and deserialize properly // 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(); com.google.gson.Gson gson = new com.google.gson.Gson();
String json = gson.toJson(payload); String json = gson.toJson(payload);
stats = gson.fromJson(json, StatsUpdatePayload.class); stats = gson.fromJson(json, StatsUpdatePayload.class);
@@ -90,6 +124,15 @@ public class DashboardClientHandler implements Runnable {
updateStatistics(senderId, stats); updateStatistics(senderId, stats);
} }
/**
* Aplica os dados recebidos ao modelo global de estatísticas.
* <p>
* 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) { private void updateStatistics(String senderId, StatsUpdatePayload stats) {
if (stats.getTotalVehiclesGenerated() >= 0) { if (stats.getTotalVehiclesGenerated() >= 0) {
statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated()); statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated());

View File

@@ -10,17 +10,43 @@ import java.util.concurrent.atomic.AtomicBoolean;
import sd.config.SimulationConfig; import sd.config.SimulationConfig;
/** /**
* Aggregates and displays real-time statistics from all simulation processes. * Servidor central de agregação de telemetria e estatísticas.
* Uses a thread pool to handle concurrent client connections. * <p>
* 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.
* <p>
* Suporta dois modos de operação:
* <ul>
* <li><b>Headless (CLI):</b> Renderização periódica de métricas no terminal (stdout).</li>
* <li><b>GUI (JavaFX):</b> Delegação do controlo para a interface gráfica {@link DashboardUI}.</li>
* </ul>
*/ */
public class DashboardServer { public class DashboardServer {
private final int port; private final int port;
/** Armazenamento em memória (Thread-safe) do estado global do sistema. */
private final DashboardStatistics statistics; private final DashboardStatistics statistics;
/** Pool de threads para isolamento de falhas e gestão de recursos de I/O. */
private final ExecutorService clientHandlerPool; private final ExecutorService clientHandlerPool;
/** Flag atómica para controlo seguro do ciclo de vida do servidor. */
private final AtomicBoolean running; private final AtomicBoolean running;
private ServerSocket serverSocket; private ServerSocket serverSocket;
/**
* Ponto de entrada (Bootstrap) da aplicação de monitorização.
* <p>
* 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) { public static void main(String[] args) {
// Check if GUI mode is requested // Check if GUI mode is requested
boolean useGUI = false; 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) { public DashboardServer(SimulationConfig config) {
this.port = config.getDashboardPort(); this.port = config.getDashboardPort();
this.statistics = new DashboardStatistics(); this.statistics = new DashboardStatistics();
// Fixed pool limita o consumo de recursos, prevenindo exaustão sob carga alta
this.clientHandlerPool = Executors.newFixedThreadPool(10); this.clientHandlerPool = Executors.newFixedThreadPool(10);
this.running = new AtomicBoolean(false); 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 { public void start() throws IOException {
if (running.get()) { if (running.get()) {
System.out.println("Dashboard Server is already running."); System.out.println("Dashboard Server is already running.");
@@ -95,6 +132,13 @@ public class DashboardServer {
acceptThread.start(); acceptThread.start();
} }
/**
* Loop principal de aceitação de conexões (Dispatcher).
* <p>
* 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() { private void acceptConnections() {
while (running.get()) { while (running.get()) {
try { 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") @SuppressWarnings("BusyWait")
private void displayLoop() { private void displayLoop() {
final long DISPLAY_INTERVAL_MS = 5000; 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() { public void displayStatistics() {
System.out.println("\n" + "=".repeat(60)); System.out.println("\n" + "=".repeat(60));
System.out.println("REAL-TIME SIMULATION STATISTICS"); System.out.println("REAL-TIME SIMULATION STATISTICS");
@@ -135,6 +186,13 @@ public class DashboardServer {
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
} }
/**
* Procedimento de encerramento gracioso (Graceful Shutdown).
* <p>
* 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() { public void stop() {
if (!running.get()) { if (!running.get()) {
return; return;

View File

@@ -9,8 +9,13 @@ import java.util.concurrent.atomic.AtomicLong;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* Thread-safe storage for aggregated simulation statistics. * Repositório central de estado da simulação, desenhado para acesso concorrente de alta frequência.
* Uses atomic types and concurrent collections for lock-free updates. * <p>
* 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 { public class DashboardStatistics {
@@ -19,12 +24,21 @@ public class DashboardStatistics {
private final AtomicLong totalSystemTime; private final AtomicLong totalSystemTime;
private final AtomicLong totalWaitingTime; private final AtomicLong totalWaitingTime;
/** Mapa thread-safe para armazenar métricas granulares por interseção. */
private final Map<String, IntersectionStats> intersectionStats; private final Map<String, IntersectionStats> intersectionStats;
private final Map<VehicleType, AtomicInteger> vehicleTypeCount; private final Map<VehicleType, AtomicInteger> vehicleTypeCount;
private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime; private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime;
/** Timestamp da última atualização de escrita, com garantia de visibilidade de memória (volatile). */
private volatile long lastUpdateTime; 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() { public DashboardStatistics() {
this.totalVehiclesGenerated = new AtomicInteger(0); this.totalVehiclesGenerated = new AtomicInteger(0);
this.totalVehiclesCompleted = new AtomicInteger(0); this.totalVehiclesCompleted = new AtomicInteger(0);
@@ -94,6 +108,17 @@ public class DashboardStatistics {
updateTimestamp(); updateTimestamp();
} }
/**
* Atualiza ou inicializa atomicamente as estatísticas de uma interseção específica.
* <p>
* 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, public void updateIntersectionStats(String intersectionId, int arrivals,
int departures, int currentQueueSize) { int departures, int currentQueueSize) {
intersectionStats.compute(intersectionId, (id, stats) -> { intersectionStats.compute(intersectionId, (id, stats) -> {
@@ -110,6 +135,8 @@ public class DashboardStatistics {
lastUpdateTime = System.currentTimeMillis(); lastUpdateTime = System.currentTimeMillis();
} }
// --- Getters e Métricas Calculadas ---
public int getTotalVehiclesGenerated() { public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated.get(); return totalVehiclesGenerated.get();
} }
@@ -118,12 +145,20 @@ public class DashboardStatistics {
return totalVehiclesCompleted.get(); 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() { public double getAverageSystemTime() {
int completed = totalVehiclesCompleted.get(); int completed = totalVehiclesCompleted.get();
if (completed == 0) return 0.0; if (completed == 0) return 0.0;
return (double) totalSystemTime.get() / completed; 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() { public double getAverageWaitingTime() {
int completed = totalVehiclesCompleted.get(); int completed = totalVehiclesCompleted.get();
if (completed == 0) return 0.0; if (completed == 0) return 0.0;
@@ -152,6 +187,44 @@ public class DashboardStatistics {
return lastUpdateTime; return lastUpdateTime;
} }
/**
* Obtém um snapshot dos tamanhos atuais das filas de todas as interseções.
* <p>
* 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<String, Integer> getCurrentQueueSizes() {
Map<String, Integer> queueSizes = new HashMap<>();
for (Map.Entry<String, IntersectionStats> 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() { public void display() {
System.out.println("\n--- GLOBAL STATISTICS ---"); System.out.println("\n--- GLOBAL STATISTICS ---");
System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated()); System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated());
@@ -181,6 +254,10 @@ public class DashboardStatistics {
System.out.printf("%nLast Update: %tT%n", lastUpdateTime); 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 { public static class IntersectionStats {
private final String intersectionId; private final String intersectionId;
private final AtomicInteger totalArrivals; private final AtomicInteger totalArrivals;

View File

@@ -12,10 +12,11 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TitledPane;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
@@ -23,17 +24,28 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage; import javafx.stage.Stage;
import sd.config.SimulationConfig; import sd.config.SimulationConfig;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* JavaFX-based Dashboard UI for displaying real-time simulation statistics. * Interface Gráfica (GUI) baseada em JavaFX para visualização de telemetria em tempo real.
* Provides a graphical interface with auto-updating statistics panels. * <p>
* Esta classe atua como a camada de apresentação (View) do sistema. Implementa o padrão
* <i>Observer</i> (via polling) para refletir o estado do modelo {@link DashboardStatistics}
* nos componentes visuais.
* <p>
* <b>Aspetos Técnicos Relevantes:</b>
* <ul>
* <li><b>Concorrência de UI:</b> Utiliza um {@link ScheduledExecutorService} para buscar dados
* em background e {@link Platform#runLater(Runnable)} para injetar atualizações na
* <i>JavaFX Application Thread</i>, evitando exceções de "Not on FX application thread".</li>
* <li><b>Data Binding:</b> Utiliza {@link TableView} com classes internas (DTOs) para
* renderização tabular eficiente de tipos de veículos e interseções.</li>
* <li><b>Controlo de Processos:</b> Integra com {@link SimulationProcessManager} para orquestrar
* o ciclo de vida (spawn/kill) dos processos externos da simulação.</li>
* </ul>
*/ */
public class DashboardUI extends Application { public class DashboardUI extends Application {
@@ -54,201 +66,368 @@ public class DashboardUI extends Application {
// Intersection Table // Intersection Table
private TableView<IntersectionRow> intersectionTable; private TableView<IntersectionRow> intersectionTable;
// Update scheduler // Update scheduler (Background Thread)
private ScheduledExecutorService updateScheduler; private ScheduledExecutorService updateScheduler;
// Configuration controls
private ComboBox<String> 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 @Override
public void start(Stage primaryStage) { public void start(Stage primaryStage) {
try { try {
// Initialize server // Initialize server
String configFile = getParameters().getRaw().isEmpty() String configFile = getParameters().getRaw().isEmpty()
? "src/main/resources/simulation.properties" ? "src/main/resources/simulation.properties"
: getParameters().getRaw().get(0); : getParameters().getRaw().get(0);
SimulationConfig config = new SimulationConfig(configFile); SimulationConfig config = new SimulationConfig(configFile);
server = new DashboardServer(config); server = new DashboardServer(config);
statistics = server.getStatistics(); statistics = server.getStatistics();
// Start the dashboard server // Start the dashboard server (Backend listening port)
server.start(); server.start();
// Build UI // Build UI Layout
BorderPane root = new BorderPane(); BorderPane root = new BorderPane();
root.setStyle("-fx-background-color: #f5f5f5;"); root.getStyleClass().add("root");
// Header // Header (Top)
VBox header = createHeader(); VBox header = createHeader();
root.setTop(header); root.setTop(header);
// Main content // Main content (Center)
VBox mainContent = createMainContent(); VBox mainContent = createMainContent();
root.setCenter(mainContent); root.setCenter(mainContent);
// Footer // Footer (Bottom)
HBox footer = createFooter(); HBox footer = createFooter();
root.setBottom(footer); root.setBottom(footer);
// Create scene // Create scene & apply CSS
Scene scene = new Scene(root, 1200, 800); Scene scene = new Scene(root, 1200, 850);
String cssUrl = getClass().getResource("/dashboard.css").toExternalForm();
scene.getStylesheets().add(cssUrl);
primaryStage.setTitle("Traffic Simulation Dashboard - Real-time Statistics"); primaryStage.setTitle("Traffic Simulation Dashboard - Real-time Statistics");
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.show(); primaryStage.show();
// Start periodic updates // Start periodic updates loop
startPeriodicUpdates(); startPeriodicUpdates();
// Handle window close // Handle window close (Graceful shutdown)
primaryStage.setOnCloseRequest(event -> { primaryStage.setOnCloseRequest(event -> {
shutdown(); shutdown();
}); });
} catch (IOException e) { } catch (Exception e) {
showErrorAlert("Failed to start Dashboard Server", e.getMessage()); showErrorAlert("Failed to start Dashboard Server", e.getMessage());
e.printStackTrace();
Platform.exit(); Platform.exit();
} }
} }
private VBox createHeader() { private VBox createHeader() {
VBox header = new VBox(10); VBox header = new VBox(10);
header.setPadding(new Insets(20)); header.getStyleClass().add("header");
header.setStyle("-fx-background-color: linear-gradient(to right, #2c3e50, #3498db);"); header.setAlignment(Pos.CENTER);
Label title = new Label("DISTRIBUTED TRAFFIC SIMULATION DASHBOARD"); Label title = new Label("DISTRIBUTED TRAFFIC SIMULATION DASHBOARD");
title.setFont(Font.font("Arial", FontWeight.BOLD, 28)); title.getStyleClass().add("header-title");
title.setTextFill(Color.WHITE);
Label subtitle = new Label("Real-time Statistics and Monitoring"); Label subtitle = new Label("Real-time Statistics and Monitoring");
subtitle.setFont(Font.font("Arial", FontWeight.NORMAL, 16)); subtitle.getStyleClass().add("header-subtitle");
subtitle.setTextFill(Color.web("#ecf0f1"));
header.getChildren().addAll(title, subtitle); // Configuration Panel
header.setAlignment(Pos.CENTER); VBox configPanel = createConfigurationPanel();
// Control Buttons
HBox controls = new HBox(15);
controls.setAlignment(Pos.CENTER);
Button btnStart = new Button("START SIMULATION");
btnStart.getStyleClass().add("button-start");
Button btnStop = new Button("STOP SIMULATION");
btnStop.getStyleClass().add("button-stop");
btnStop.setDisable(true);
SimulationProcessManager processManager = new SimulationProcessManager();
btnStart.setOnAction(e -> {
try {
// Passar o ficheiro de configuração selecionado
processManager.setConfigFile(selectedConfigFile);
processManager.startSimulation();
// Toggle UI state
btnStart.setDisable(true);
btnStop.setDisable(false);
configFileSelector.setDisable(true); // Bloquear mudanças durante simulação
} catch (IOException ex) {
showErrorAlert("Start Failed", "Could not start simulation processes: " + ex.getMessage());
}
});
btnStop.setOnAction(e -> {
processManager.stopSimulation();
// Toggle UI state
btnStart.setDisable(false);
btnStop.setDisable(true);
configFileSelector.setDisable(false); // Desbloquear para nova simulação
});
controls.getChildren().addAll(btnStart, btnStop);
header.getChildren().addAll(title, subtitle, configPanel, controls);
return header; 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<String> 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() { private VBox createMainContent() {
VBox mainContent = new VBox(15); VBox mainContent = new VBox(20);
mainContent.setPadding(new Insets(20)); mainContent.setPadding(new Insets(20));
// Global Statistics Panel // Global Statistics Panel
TitledPane globalStatsPane = createGlobalStatisticsPanel(); VBox globalStatsCard = createGlobalStatisticsPanel();
// Tables Container
HBox tablesContainer = new HBox(20);
tablesContainer.setAlignment(Pos.TOP_CENTER);
// Vehicle Type Statistics Panel // Vehicle Type Statistics Panel
TitledPane vehicleTypePane = createVehicleTypePanel(); VBox vehicleTypeCard = createVehicleTypePanel();
HBox.setHgrow(vehicleTypeCard, Priority.ALWAYS);
// Intersection Statistics Panel // Intersection Statistics Panel
TitledPane intersectionPane = createIntersectionPanel(); VBox intersectionCard = createIntersectionPanel();
HBox.setHgrow(intersectionCard, Priority.ALWAYS);
mainContent.getChildren().addAll(globalStatsPane, vehicleTypePane, intersectionPane); tablesContainer.getChildren().addAll(vehicleTypeCard, intersectionCard);
mainContent.getChildren().addAll(globalStatsCard, tablesContainer);
return mainContent; return mainContent;
} }
private TitledPane createGlobalStatisticsPanel() { private VBox createGlobalStatisticsPanel() {
VBox card = new VBox();
card.getStyleClass().add("card");
// Card Header
HBox cardHeader = new HBox();
cardHeader.getStyleClass().add("card-header");
Label cardTitle = new Label("Global Statistics");
cardTitle.getStyleClass().add("card-title");
cardHeader.getChildren().add(cardTitle);
// Card Content
GridPane grid = new GridPane(); GridPane grid = new GridPane();
grid.setPadding(new Insets(15)); grid.getStyleClass().add("card-content");
grid.setHgap(20); grid.setHgap(40);
grid.setVgap(15); grid.setVgap(15);
grid.setStyle("-fx-background-color: white; -fx-border-radius: 5;"); grid.setAlignment(Pos.CENTER);
// Initialize labels // Initialize labels
lblVehiclesGenerated = createStatLabel("0"); lblVehiclesGenerated = createStatValueLabel("0");
lblVehiclesCompleted = createStatLabel("0"); lblVehiclesCompleted = createStatValueLabel("0");
lblVehiclesInTransit = createStatLabel("0"); lblVehiclesInTransit = createStatValueLabel("0");
lblAvgSystemTime = createStatLabel("0.00 ms"); lblAvgSystemTime = createStatValueLabel("0.00 s");
lblAvgWaitingTime = createStatLabel("0.00 ms"); lblAvgWaitingTime = createStatValueLabel("0.00 s");
// Add labels with descriptions // Add labels with descriptions
addStatRow(grid, 0, "Total Vehicles Generated:", lblVehiclesGenerated); addStatRow(grid, 0, 0, "Total Vehicles Generated", lblVehiclesGenerated);
addStatRow(grid, 1, "Total Vehicles Completed:", lblVehiclesCompleted); addStatRow(grid, 1, 0, "Total Vehicles Completed", lblVehiclesCompleted);
addStatRow(grid, 2, "Vehicles In Transit:", lblVehiclesInTransit); addStatRow(grid, 2, 0, "Vehicles In Transit", lblVehiclesInTransit);
addStatRow(grid, 3, "Average System Time:", lblAvgSystemTime); addStatRow(grid, 0, 1, "Average System Time", lblAvgSystemTime);
addStatRow(grid, 4, "Average Waiting Time:", lblAvgWaitingTime); addStatRow(grid, 1, 1, "Average Waiting Time", lblAvgWaitingTime);
TitledPane pane = new TitledPane("Global Statistics", grid); card.getChildren().addAll(cardHeader, grid);
pane.setCollapsible(false); return card;
pane.setFont(Font.font("Arial", FontWeight.BOLD, 16));
return pane;
} }
private TitledPane createVehicleTypePanel() { private VBox createVehicleTypePanel() {
VBox card = new VBox();
card.getStyleClass().add("card");
// Card Header
HBox cardHeader = new HBox();
cardHeader.getStyleClass().add("card-header");
Label cardTitle = new Label("Vehicle Type Statistics");
cardTitle.getStyleClass().add("card-title");
cardHeader.getChildren().add(cardTitle);
// Table
vehicleTypeTable = new TableView<>(); vehicleTypeTable = new TableView<>();
vehicleTypeTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); vehicleTypeTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
vehicleTypeTable.setPrefHeight(200); vehicleTypeTable.setPrefHeight(300);
TableColumn<VehicleTypeRow, String> typeCol = new TableColumn<>("Vehicle Type"); TableColumn<VehicleTypeRow, String> typeCol = new TableColumn<>("Vehicle Type");
typeCol.setCellValueFactory(new PropertyValueFactory<>("vehicleType")); typeCol.setCellValueFactory(new PropertyValueFactory<>("vehicleType"));
typeCol.setPrefWidth(200);
TableColumn<VehicleTypeRow, Integer> countCol = new TableColumn<>("Count"); TableColumn<VehicleTypeRow, Integer> countCol = new TableColumn<>("Count");
countCol.setCellValueFactory(new PropertyValueFactory<>("count")); countCol.setCellValueFactory(new PropertyValueFactory<>("count"));
countCol.setPrefWidth(150);
TableColumn<VehicleTypeRow, String> avgWaitCol = new TableColumn<>("Avg Wait Time"); TableColumn<VehicleTypeRow, String> avgWaitCol = new TableColumn<>("Avg Wait Time");
avgWaitCol.setCellValueFactory(new PropertyValueFactory<>("avgWaitTime")); avgWaitCol.setCellValueFactory(new PropertyValueFactory<>("avgWaitTime"));
avgWaitCol.setPrefWidth(150);
vehicleTypeTable.getColumns().addAll(typeCol, countCol, avgWaitCol); vehicleTypeTable.getColumns().addAll(typeCol, countCol, avgWaitCol);
TitledPane pane = new TitledPane("Vehicle Type Statistics", vehicleTypeTable); card.getChildren().addAll(cardHeader, vehicleTypeTable);
pane.setCollapsible(false); return card;
pane.setFont(Font.font("Arial", FontWeight.BOLD, 16));
return pane;
} }
private TitledPane createIntersectionPanel() { private VBox createIntersectionPanel() {
VBox card = new VBox();
card.getStyleClass().add("card");
// Card Header
HBox cardHeader = new HBox();
cardHeader.getStyleClass().add("card-header");
Label cardTitle = new Label("Intersection Statistics");
cardTitle.getStyleClass().add("card-title");
cardHeader.getChildren().add(cardTitle);
// Table
intersectionTable = new TableView<>(); intersectionTable = new TableView<>();
intersectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); intersectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
intersectionTable.setPrefHeight(250); intersectionTable.setPrefHeight(300);
TableColumn<IntersectionRow, String> idCol = new TableColumn<>("Intersection ID"); TableColumn<IntersectionRow, String> idCol = new TableColumn<>("Intersection ID");
idCol.setCellValueFactory(new PropertyValueFactory<>("intersectionId")); idCol.setCellValueFactory(new PropertyValueFactory<>("intersectionId"));
idCol.setPrefWidth(200);
TableColumn<IntersectionRow, Integer> arrivalsCol = new TableColumn<>("Total Arrivals"); TableColumn<IntersectionRow, Integer> arrivalsCol = new TableColumn<>("Total Arrivals");
arrivalsCol.setCellValueFactory(new PropertyValueFactory<>("arrivals")); arrivalsCol.setCellValueFactory(new PropertyValueFactory<>("arrivals"));
arrivalsCol.setPrefWidth(150);
TableColumn<IntersectionRow, Integer> departuresCol = new TableColumn<>("Total Departures"); TableColumn<IntersectionRow, Integer> departuresCol = new TableColumn<>("Total Departures");
departuresCol.setCellValueFactory(new PropertyValueFactory<>("departures")); departuresCol.setCellValueFactory(new PropertyValueFactory<>("departures"));
departuresCol.setPrefWidth(150);
TableColumn<IntersectionRow, Integer> queueCol = new TableColumn<>("Current Queue"); TableColumn<IntersectionRow, Integer> queueCol = new TableColumn<>("Current Queue");
queueCol.setCellValueFactory(new PropertyValueFactory<>("queueSize")); queueCol.setCellValueFactory(new PropertyValueFactory<>("queueSize"));
queueCol.setPrefWidth(150);
intersectionTable.getColumns().addAll(idCol, arrivalsCol, departuresCol, queueCol); intersectionTable.getColumns().addAll(idCol, arrivalsCol, departuresCol, queueCol);
TitledPane pane = new TitledPane("Intersection Statistics", intersectionTable); card.getChildren().addAll(cardHeader, intersectionTable);
pane.setCollapsible(false); return card;
pane.setFont(Font.font("Arial", FontWeight.BOLD, 16));
return pane;
} }
private HBox createFooter() { private HBox createFooter() {
HBox footer = new HBox(10); HBox footer = new HBox(10);
footer.setPadding(new Insets(10, 20, 10, 20)); footer.getStyleClass().add("footer");
footer.setStyle("-fx-background-color: #34495e;");
footer.setAlignment(Pos.CENTER_LEFT); footer.setAlignment(Pos.CENTER_LEFT);
Label statusLabel = new Label("Status:"); Label statusLabel = new Label("Status:");
statusLabel.setTextFill(Color.WHITE); statusLabel.getStyleClass().add("footer-text");
statusLabel.setFont(Font.font("Arial", FontWeight.BOLD, 12)); statusLabel.setStyle("-fx-font-weight: bold;");
Circle statusIndicator = new Circle(6); Circle statusIndicator = new Circle(6);
statusIndicator.setFill(Color.LIME); statusIndicator.setFill(javafx.scene.paint.Color.LIME);
Label statusText = new Label("Connected and Receiving Data"); Label statusText = new Label("Connected and Receiving Data");
statusText.setTextFill(Color.WHITE); statusText.getStyleClass().add("footer-text");
statusText.setFont(Font.font("Arial", 12));
lblLastUpdate = new Label("Last Update: --:--:--"); lblLastUpdate = new Label("Last Update: --:--:--");
lblLastUpdate.setTextFill(Color.web("#ecf0f1")); lblLastUpdate.getStyleClass().add("footer-text");
lblLastUpdate.setFont(Font.font("Arial", 12));
Region spacer = new Region(); Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS); HBox.setHgrow(spacer, Priority.ALWAYS);
@@ -258,37 +437,49 @@ public class DashboardUI extends Application {
return footer; return footer;
} }
private Label createStatLabel(String initialValue) { private Label createStatValueLabel(String initialValue) {
Label label = new Label(initialValue); Label label = new Label(initialValue);
label.setFont(Font.font("Arial", FontWeight.BOLD, 20)); label.getStyleClass().add("stat-value");
label.setTextFill(Color.web("#2980b9"));
return label; return label;
} }
private void addStatRow(GridPane grid, int row, String description, Label valueLabel) { private void addStatRow(GridPane grid, int row, int colGroup, String description, Label valueLabel) {
Label descLabel = new Label(description); VBox container = new VBox(5);
descLabel.setFont(Font.font("Arial", FontWeight.NORMAL, 14)); container.setAlignment(Pos.CENTER_LEFT);
descLabel.setTextFill(Color.web("#34495e"));
grid.add(descLabel, 0, row); Label descLabel = new Label(description);
grid.add(valueLabel, 1, row); descLabel.getStyleClass().add("stat-label");
container.getChildren().addAll(descLabel, valueLabel);
grid.add(container, colGroup, row);
} }
/**
* Inicia o ciclo de polling em background.
* Atualiza a UI a cada 100ms.
*/
private void startPeriodicUpdates() { private void startPeriodicUpdates() {
updateScheduler = Executors.newSingleThreadScheduledExecutor(); updateScheduler = Executors.newSingleThreadScheduledExecutor();
updateScheduler.scheduleAtFixedRate(() -> { updateScheduler.scheduleAtFixedRate(() -> {
// Crucial: Encapsular atualização de UI em Platform.runLater
// para garantir execução na JavaFX Application Thread
Platform.runLater(this::updateUI); 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() { private void updateUI() {
// Update global statistics // Update global statistics
lblVehiclesGenerated.setText(String.valueOf(statistics.getTotalVehiclesGenerated())); lblVehiclesGenerated.setText(String.valueOf(statistics.getTotalVehiclesGenerated()));
lblVehiclesCompleted.setText(String.valueOf(statistics.getTotalVehiclesCompleted())); lblVehiclesCompleted.setText(String.valueOf(statistics.getTotalVehiclesCompleted()));
lblVehiclesInTransit.setText(String.valueOf( lblVehiclesInTransit.setText(String.valueOf(
statistics.getTotalVehiclesGenerated() - statistics.getTotalVehiclesCompleted())); statistics.getTotalVehiclesGenerated() - statistics.getTotalVehiclesCompleted()));
lblAvgSystemTime.setText(String.format("%.2f ms", statistics.getAverageSystemTime())); lblAvgSystemTime.setText(String.format("%.2f s", statistics.getAverageSystemTime() / 1000.0));
lblAvgWaitingTime.setText(String.format("%.2f ms", statistics.getAverageWaitingTime())); lblAvgWaitingTime.setText(String.format("%.2f s", statistics.getAverageWaitingTime() / 1000.0));
lblLastUpdate.setText(String.format("Last Update: %tT", statistics.getLastUpdateTime())); lblLastUpdate.setText(String.format("Last Update: %tT", statistics.getLastUpdateTime()));
// Update vehicle type table // Update vehicle type table
@@ -297,23 +488,43 @@ public class DashboardUI extends Application {
int count = statistics.getVehicleTypeCount(type); int count = statistics.getVehicleTypeCount(type);
double avgWait = statistics.getAverageWaitingTimeByType(type); double avgWait = statistics.getAverageWaitingTimeByType(type);
vehicleTypeTable.getItems().add(new VehicleTypeRow( vehicleTypeTable.getItems().add(new VehicleTypeRow(
type.toString(), count, String.format("%.2f ms", avgWait))); type.toString(), count, String.format("%.2f s", avgWait / 1000.0)));
} }
// Update intersection table // Update intersection table
intersectionTable.getItems().clear(); intersectionTable.getItems().clear();
Map<String, DashboardStatistics.IntersectionStats> intersectionStats = Map<String, DashboardStatistics.IntersectionStats> intersectionStats = statistics.getAllIntersectionStats();
statistics.getAllIntersectionStats();
for (DashboardStatistics.IntersectionStats stats : intersectionStats.values()) { for (DashboardStatistics.IntersectionStats stats : intersectionStats.values()) {
intersectionTable.getItems().add(new IntersectionRow( intersectionTable.getItems().add(new IntersectionRow(
stats.getIntersectionId(), stats.getIntersectionId(),
stats.getTotalArrivals(), stats.getTotalArrivals(),
stats.getTotalDepartures(), stats.getTotalDepartures(),
stats.getCurrentQueueSize() stats.getCurrentQueueSize()));
));
} }
} }
/**
* Atualiza a informação exibida sobre a configuração selecionada.
*/
private void updateConfigInfo() {
String info = "";
switch (selectedConfigFile) {
case "simulation-low.properties":
info = "CARGA BAIXA: 0.2 veículos/s (~720/hora) | Sem congestionamento esperado";
break;
case "simulation-medium.properties":
info = "CARGA MÉDIA: 0.5 veículos/s (~1800/hora) | Algum congestionamento esperado";
break;
case "simulation-high.properties":
info = "CARGA ALTA: 1.0 veículo/s (~3600/hora) | Congestionamento significativo esperado";
break;
default:
info = "⚙️ CONFIGURAÇÃO PADRÃO: Verificar ficheiro para parâmetros";
break;
}
configInfoLabel.setText(info);
}
private void shutdown() { private void shutdown() {
System.out.println("Shutting down Dashboard UI..."); System.out.println("Shutting down Dashboard UI...");
@@ -336,11 +547,40 @@ public class DashboardUI extends Application {
alert.showAndWait(); 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) { public static void main(String[] args) {
launch(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 { public static class VehicleTypeRow {
private final String vehicleType; private final String vehicleType;
private final int count; private final int count;
@@ -357,6 +597,7 @@ public class DashboardUI extends Application {
public String getAvgWaitTime() { return avgWaitTime; } public String getAvgWaitTime() { return avgWaitTime; }
} }
/** DTO para linhas da tabela de Interseções. */
public static class IntersectionRow { public static class IntersectionRow {
private final String intersectionId; private final String intersectionId;
private final int arrivals; private final int arrivals;

View File

@@ -0,0 +1,7 @@
package sd.dashboard;
public class Launcher {
public static void main(String[] args) {
DashboardUI.main(args);
}
}

View File

@@ -0,0 +1,184 @@
package sd.dashboard;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Orquestrador de processos para o ambiente de simulação distribuída.
* <p>
* Esta classe atua como um supervisor (Process Manager), responsável pelo <i>bootstrapping</i>
* e <i>teardown</i> das múltiplas Java Virtual Machines (JVMs) que compõem o sistema.
* <p>
* Funcionalidades principais:
* <ul>
* <li><b>Isolamento:</b> Cada nó (Interseção, Coordinator, ExitNode) corre no seu próprio processo OS.</li>
* <li><b>Ordem de Arranque:</b> Garante que os servidores (Interseções) estão online antes dos clientes (Coordenador).</li>
* <li><b>Gestão de Logs:</b> Redireciona stdout/stderr de cada processo filho para ficheiros temporários para facilitar o debug.</li>
* </ul>
*/
public class SimulationProcessManager {
private final List<Process> runningProcesses;
private final String classpath;
private String configFile;
/**
* Inicializa o gestor capturando o classpath da JVM atual.
* Isto garante que os processos filhos herdam as mesmas dependências e configurações de ambiente.
*/
public SimulationProcessManager() {
this.runningProcesses = new ArrayList<>();
this.classpath = System.getProperty("java.class.path");
this.configFile = "src/main/resources/simulation.properties";
}
/**
* Define o perfil de configuração a ser injetado nos processos filhos.
* Útil para alternar entre cenários (Low/Medium/High Load) dinamicamente.
* * @param configFile Nome do ficheiro de propriedades (ex: "simulation-low.properties").
*/
public void setConfigFile(String configFile) {
this.configFile = "src/main/resources/" + configFile;
System.out.println("Configuration file set to: " + this.configFile);
}
/**
* Executa o procedimento de arranque (Bootstrap) da simulação distribuída.
* <p>
* A ordem de inicialização é crítica para evitar <i>Race Conditions</i> na conexão TCP:
* <ol>
* <li><b>Workers (Interseções):</b> Iniciam os ServerSockets.</li>
* <li><b>Sink (Exit Node):</b> Prepara-se para receber métricas finais.</li>
* <li><b>Delay de Estabilização:</b> Pausa de 1s para garantir que os sockets estão em LISTENING.</li>
* <li><b>Source (Coordinator):</b> Inicia a geração de carga e conecta-se aos nós.</li>
* </ol>
* * @throws IOException Se falhar o fork de algum processo.
*/
public void startSimulation() throws IOException {
if (!runningProcesses.isEmpty()) {
stopSimulation();
}
System.out.println("Starting simulation processes...");
// 1. Start Intersections (Cr1 - Cr5)
String[] intersectionIds = { "Cr1", "Cr2", "Cr3", "Cr4", "Cr5" };
for (String id : intersectionIds) {
startProcess("sd.IntersectionProcess", id);
}
// 2. Start Exit Node
startProcess("sd.ExitNodeProcess", null);
// 3. Start Coordinator (Wait a bit for others to initialize)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
startProcess("sd.coordinator.CoordinatorProcess", null);
System.out.println("All simulation processes started.");
}
/**
* Verifica o estado de "liveness" da simulação monitorizando o processo Coordenador.
* <p>
* 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.
* <p>
* Tenta primeiro uma paragem graciosa (`SIGTERM`), aguarda meio segundo, e
* força a paragem (`SIGKILL`) para processos persistentes, garantindo que não
* ficam processos órfãos no SO.
*/
public void stopSimulation() {
System.out.println("Stopping simulation processes...");
for (Process process : runningProcesses) {
if (process.isAlive()) {
process.destroy(); // Try graceful termination first
}
}
// Wait a bit and force kill if necessary
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
for (Process process : runningProcesses) {
if (process.isAlive()) {
process.destroyForcibly();
}
}
runningProcesses.clear();
System.out.println("All simulation processes stopped.");
}
/**
* Helper de baixo nível para construção e lançamento de processos Java.
* Configura o redirecionamento de I/O para ficheiros de log na diretoria temporária do SO.
*/
private void startProcess(String className, String arg) throws IOException {
String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
ProcessBuilder builder;
if (arg != null) {
builder = new ProcessBuilder(javaBin, "-cp", classpath, className, arg, configFile);
} else {
builder = new ProcessBuilder(javaBin, "-cp", classpath, className, configFile);
}
// get the OS temp folder
// Linux: /tmp/
// Windows: %AppData%\Local\Temp\
String tempDir = System.getProperty("java.io.tmpdir");
String logName = className.substring(className.lastIndexOf('.') + 1) + (arg != null ? "-" + arg : "") + ".log";
// use the (File parent, String child) constructor to handle slash/backslash
// automatically
File logFile = new File(tempDir, logName);
builder.redirectOutput(logFile);
builder.redirectError(logFile);
Process process = builder.start();
runningProcesses.add(process);
System.out.println("Started " + className + (arg != null ? " " + arg : ""));
// print where the logs are actually going
System.out.println("Logs redirected to: " + logFile.getAbsolutePath());
}
}

View File

@@ -4,7 +4,14 @@ import sd.model.MessageType;
import sd.protocol.MessageProtocol; import sd.protocol.MessageProtocol;
/** /**
* Message wrapper for sending statistics to the dashboard. * Implementação concreta do protocolo de mensagens destinada ao transporte de telemetria.
* <p>
* 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.
* <p>
* 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 { public class StatsMessage implements MessageProtocol {
@@ -14,27 +21,49 @@ public class StatsMessage implements MessageProtocol {
private final String destinationNode; private final String destinationNode;
private final StatsUpdatePayload payload; 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) { public StatsMessage(String sourceNode, StatsUpdatePayload payload) {
this.sourceNode = sourceNode; this.sourceNode = sourceNode;
this.destinationNode = "DashboardServer"; this.destinationNode = "DashboardServer"; // Destino implícito e fixo
this.payload = payload; this.payload = payload;
} }
/**
* Retorna o tipo da mensagem, que identifica semanticamente o conteúdo para o recetor.
* @return Sempre {@link MessageType#STATS_UPDATE}.
*/
@Override @Override
public MessageType getType() { public MessageType getType() {
return MessageType.STATS_UPDATE; return MessageType.STATS_UPDATE;
} }
/**
* Obtém a carga útil da mensagem.
* @return O objeto {@link StatsUpdatePayload} associado.
*/
@Override @Override
public Object getPayload() { public Object getPayload() {
return payload; return payload;
} }
/**
* Identifica a origem da mensagem.
* @return O ID do nó remetente.
*/
@Override @Override
public String getSourceNode() { public String getSourceNode() {
return sourceNode; return sourceNode;
} }
/**
* Identifica o destino da mensagem.
* @return Sempre "DashboardServer".
*/
@Override @Override
public String getDestinationNode() { public String getDestinationNode() {
return destinationNode; return destinationNode;

View File

@@ -7,25 +7,60 @@ import java.util.Map;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* Data transfer object for statistics updates to the dashboard. * Objeto de Transferência de Dados (DTO) otimizado para transporte de telemetria.
* Use -1 for fields not being updated in this message. * <p>
* 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 <b>atualizações parciais</b>
* (Sparse Updates):
* <ul>
* <li>Campos globais inicializados com {@code -1} indicam "sem alteração" (no-op). O Dashboard
* deve ignorar estes campos e manter o valor acumulado anterior.</li>
* <li>Campos de interseção ({@code arrivals}, {@code departures}) representam deltas ou snapshots
* específicos do nó remetente.</li>
* </ul>
* Implementa {@link Serializable} para transmissão direta via Java Sockets.
*
[Image of data transfer object pattern]
*/ */
public class StatsUpdatePayload implements Serializable { public class StatsUpdatePayload implements Serializable {
private static final long serialVersionUID = 1L; 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; private int totalVehiclesGenerated = -1;
/** Total completado. Valor -1 indica que este campo deve ser ignorado. */
private int totalVehiclesCompleted = -1; private int totalVehiclesCompleted = -1;
/** Tempo total de sistema acumulado (ms). Valor -1 indica que deve ser ignorado. */
private long totalSystemTime = -1; private long totalSystemTime = -1;
/** Tempo total de espera acumulado (ms). Valor -1 indica que deve ser ignorado. */
private long totalWaitingTime = -1; 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; private int intersectionArrivals = 0;
/** Número de veículos que saíram da interseção desde o último reporte. */
private int intersectionDepartures = 0; private int intersectionDepartures = 0;
/** Snapshot do tamanho atual da fila na interseção. */
private int intersectionQueueSize = 0; private int intersectionQueueSize = 0;
// Detailed Breakdowns
/** Contagem acumulada por tipo de veículo. */
private Map<VehicleType, Integer> vehicleTypeCounts; private Map<VehicleType, Integer> vehicleTypeCounts;
/** Tempos de espera acumulados por tipo de veículo. */
private Map<VehicleType, Long> vehicleTypeWaitTimes; private Map<VehicleType, Long> vehicleTypeWaitTimes;
/**
* Inicializa o payload com os mapas vazios e contadores globais a -1 (estado neutro).
*/
public StatsUpdatePayload() { public StatsUpdatePayload() {
this.vehicleTypeCounts = new HashMap<>(); this.vehicleTypeCounts = new HashMap<>();
this.vehicleTypeWaitTimes = new HashMap<>(); this.vehicleTypeWaitTimes = new HashMap<>();
@@ -67,6 +102,8 @@ public class StatsUpdatePayload implements Serializable {
return vehicleTypeWaitTimes; return vehicleTypeWaitTimes;
} }
// Setters implementam Fluent Interface para construção encadeada
public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) { public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) {
this.totalVehiclesGenerated = totalVehiclesGenerated; this.totalVehiclesGenerated = totalVehiclesGenerated;
return this; return this;

View File

@@ -0,0 +1,39 @@
package sd.des;
/**
* Tipos de eventos discretos da simulação.
*
* <p>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
}

View File

@@ -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.
*
* <p>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.</p>
*/
public class EventQueue {
private final PriorityQueue<SimulationEvent> queue;
private final List<SimulationEvent> 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<SimulationEvent> 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());
}
}

View File

@@ -0,0 +1,71 @@
package sd.des;
/**
* Gere o tempo de simulação para Simulação de Eventos Discretos.
*
* <p>
* No DES, o tempo avança em saltos discretos de evento para evento,
* não de forma contínua como o tempo real.
* </p>
*
* <p>
* Esta classe garante que todos os processos no sistema distribuído
* mantêm uma visão sincronizada do tempo de simulação.
* </p>
*/
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());
}
}

View File

@@ -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).
* <p>
* 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.
* <p>
* Características principais:
* <ul>
* <li><b>Ordenação Temporal:</b> Implementa {@link Comparable} para ser armazenado numa Fila de
* Eventos Futuros (FEL - Future Event List), garantindo execução cronológica.</li>
* <li><b>Distribuído:</b> Implementa {@link Serializable} para permitir que eventos gerados num nó
* (ex: Coordenador) sejam transmitidos e executados noutro (ex: Interseção).</li>
* <li><b>Polimórfico:</b> Transporta um {@code payload} genérico, permitindo associar qualquer
* entidade (Veículo, Sinal, etc.) ao evento.</li>
* </ul>
*/
public class SimulationEvent implements Comparable<SimulationEvent>, 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.
* <p>
* <b>Lógica de Ordenação:</b>
* <ol>
* <li><b>Primária (Tempo):</b> Eventos com menor timestamp ocorrem primeiro.</li>
* <li><b>Secundária (Determinismo):</b> 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).</li>
* </ol>
*
* @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;
}
}

View File

@@ -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.
* <p>
* Este objeto atua como o <i>payload</i> 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);
}
}

View File

@@ -1,115 +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");
processGreenLightQueue();
if (!running || Thread.currentThread().isInterrupted()) break;
// Wait for green duration
Thread.sleep((long) (light.getGreenTime() * 1000));
if (!running || Thread.currentThread().isInterrupted()) break;
// --- RED Phase ---
light.changeState(TrafficLightState.RED);
System.out.println("[" + light.getId() + "] State: RED");
} finally {
// Always release the green light permission
process.releaseGreenLight(light.getDirection());
}
// Wait for red duration
Thread.sleep((long) (light.getRedTime() * 1000));
}
} catch (InterruptedException e) {
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
Thread.currentThread().interrupt();
} finally {
this.running = false;
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
}
}
private void processGreenLightQueue() throws InterruptedException {
while (running && !Thread.currentThread().isInterrupted()
&& light.getState() == TrafficLightState.GREEN
&& light.getQueueSize() > 0) {
Vehicle vehicle = light.removeVehicle();
if (vehicle != null) {
double crossingTime = getCrossingTimeForVehicle(vehicle);
Thread.sleep((long) (crossingTime * 1000));
vehicle.addCrossingTime(crossingTime);
process.getIntersection().incrementVehiclesSent();
process.sendVehicleToNextDestination(vehicle);
}
}
}
private double getCrossingTimeForVehicle(Vehicle vehicle) {
return switch (vehicle.getType()) {
case BIKE -> config.getBikeVehicleCrossingTime();
case LIGHT -> config.getLightVehicleCrossingTime();
case HEAVY -> config.getHeavyVehicleCrossingTime();
default -> config.getLightVehicleCrossingTime();
};
}
/**
* Requests the thread to stop gracefully.
* Sets the running flag and interrupts the thread to unblock any sleep() calls.
*/
public void shutdown() {
this.running = false;
if (currentThread != null && currentThread.isAlive()) {
currentThread.interrupt();
}
}
}

View File

@@ -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.
* <p>
* Implementa o padrão <i>Singleton</i> para garantir um ponto centralizado de registo.
* Utiliza o padrão <i>Producer-Consumer</i> 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).
* <p>
* <b>Garantias:</b>
* <ul>
* <li>Non-blocking writes (para a thread chamadora, na maioria dos casos).</li>
* <li>Ordering cronológico aproximado (FIFO na fila).</li>
* <li>Graceful Shutdown (flush de logs pendentes ao terminar).</li>
* </ul>
*/
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<LogEntry> 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;
}
}
}

View File

@@ -0,0 +1,60 @@
package sd.logging;
/**
* Taxonomia oficial de eventos para o subsistema de logging centralizado.
* <p>
* Este enumerado padroniza a categorização de todas as ocorrências na simulação, permitindo:
* <ul>
* <li>Filtragem granular de logs (ex: ver apenas erros ou apenas tráfego de rede).</li>
* <li>Análise estatística post-mortem (parsear logs para calcular latências).</li>
* <li>Correlação de eventos distribuídos (seguir o rastro de um veículo através de vários nós).</li>
* </ul>
*/
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;
}
}

View File

@@ -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.
* <p>
* Diferente do {@link EventLogger} (que regista eventos globais do sistema), esta classe foca-se
* na perspetiva do <b>agente</b>. 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).
* <p>
* <b>Funcionalidades:</b>
* <ul>
* <li>Análise forense de percursos individuais.</li>
* <li>Validação de tempos de espera e travessia por nó.</li>
* <li>Cálculo de eficiência de rota (tempo em movimento vs. tempo parado).</li>
* </ul>
*/
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<String, VehicleTrace> 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).
* <p>
* Este método também desencadeia a escrita do <b>Sumário de Viagem</b> 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);
}
}
}

View File

@@ -1,131 +0,0 @@
package sd.model;
import java.io.Serializable;
/**
* Represents a single event in the discrete event simulation.
* * An Event is the fundamental unit of action in the simulation. It contains:
* - A {@code timestamp} (when the event should occur).
* - A {@link EventType} (what kind of event it is).
* - Associated {@code data} (e.g., the {@link Vehicle} or {@link TrafficLight} involved).
* - An optional {@code location} (e.g., the ID of the {@link Intersection}).
* * Events are {@link Comparable}, allowing them to be sorted in a
* {@link java.util.PriorityQueue}. The primary sorting key is the
* {@code timestamp}. If timestamps are equal, {@code EventType} is used
* as a tie-breaker to ensure a consistent, deterministic order.
* * Implements {@link Serializable} so events could (in theory) be sent
* across a network in a distributed simulation.
*/
public class Event implements Comparable<Event>, Serializable {
private static final long serialVersionUID = 1L;
/**
* The simulation time (in seconds) when this event is scheduled to occur.
*/
private final double timestamp;
/**
* The type of event (e.g., VEHICLE_ARRIVAL, TRAFFIC_LIGHT_CHANGE).
*/
private final EventType type;
/**
* The data payload associated with this event.
* This could be a {@link Vehicle}, {@link TrafficLight}, or null.
*/
private final Object data;
/**
* The ID of the location where the event occurs (e.g., "Cr1").
* Can be null if the event is not location-specific (like VEHICLE_GENERATION).
*/
private final String location;
/**
* Constructs a new Event.
*
* @param timestamp The simulation time when the event occurs.
* @param type The {@link EventType} of the event.
* @param data The associated data (e.g., a Vehicle object).
* @param location The ID of the location (e.g., an Intersection ID).
*/
public Event(double timestamp, EventType type, Object data, String location) {
this.timestamp = timestamp;
this.type = type;
this.data = data;
this.location = location;
}
/**
* Convenience constructor for an Event without a specific location.
*
* @param timestamp The simulation time when the event occurs.
* @param type The {@link EventType} of the event.
* @param data The associated data (e.g., a Vehicle object).
*/
public Event(double timestamp, EventType type, Object data) {
this(timestamp, type, data, null);
}
/**
* Compares this event to another event for ordering.
* * Events are ordered primarily by {@link #timestamp} (ascending).
* If timestamps are identical, they are ordered by {@link #type} (alphabetical)
* to provide a stable, deterministic tie-breaking mechanism.
*
* @param other The other Event to compare against.
* @return A negative integer if this event comes before {@code other},
* zero if they are "equal" in sorting (though this is rare),
* or a positive integer if this event comes after {@code other}.
*/
@Override
public int compareTo(Event other) {
// Primary sort: timestamp (earlier events come first)
int cmp = Double.compare(this.timestamp, other.timestamp);
if (cmp == 0) {
// Tie-breaker: event type (ensures deterministic order)
return this.type.compareTo(other.type);
}
return cmp;
}
// --- Getters ---
/**
* @return The simulation time when the event occurs.
*/
public double getTimestamp() {
return timestamp;
}
/**
* @return The {@link EventType} of the event.
*/
public EventType getType() {
return type;
}
/**
* @return The data payload (e.g., {@link Vehicle}, {@link TrafficLight}).
* The caller must cast this to the expected type.
*/
public Object getData() {
return data;
}
/**
* @return The location ID (e.g., "Cr1"), or null if not applicable.
*/
public String getLocation() {
return location;
}
/**
* @return A string representation of the event for logging.
*/
@Override
public String toString() {
return String.format("Event{t=%.2f, type=%s, loc=%s}",
timestamp, type, location);
}
}

View File

@@ -1,45 +0,0 @@
package sd.model;
/**
* Enumeration representing all possible event types in the discrete event simulation.
* These types are used by the {@link sd.engine.SimulationEngine} to determine
* how to process a given {@link Event}.
*/
public enum EventType {
/**
* Fired when a {@link Vehicle} arrives at an {@link Intersection}.
* Data: {@link Vehicle}, Location: Intersection ID
*/
VEHICLE_ARRIVAL,
/**
* Fired when a {@link TrafficLight} is scheduled to change its state.
* Data: {@link TrafficLight}, Location: Intersection ID
*/
TRAFFIC_LIGHT_CHANGE,
/**
* Fired when a {@link Vehicle} begins to cross an {@link Intersection}.
* Data: {@link Vehicle}, Location: Intersection ID
*/
CROSSING_START,
/**
* Fired when a {@link Vehicle} finishes crossing an {@link Intersection}.
* Data: {@link Vehicle}, Location: Intersection ID
*/
CROSSING_END,
/**
* Fired when a new {@link Vehicle} should be created and added to the system.
* Data: null, Location: null
*/
VEHICLE_GENERATION,
/**
* Fired periodically to trigger the printing or sending of simulation statistics.
* Data: null, Location: null
*/
STATISTICS_UPDATE
}

View File

@@ -6,65 +6,51 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Represents an intersection in the traffic simulation. * Representa uma interseção na simulação de tráfego.
* * An Intersection acts as a central hub. It does not control logic itself, *
* but it *owns* and *manages* a set of {@link TrafficLight} objects. * <p>Uma interseção funciona como um nó central da rede. Não controla lógica diretamente,
* * Its primary responsibilities are: * mas gere um conjunto de semáforos ({@link TrafficLight}).</p>
* 1. Holding a {@link TrafficLight} for each direction ("North", "East", etc.). *
* 2. Maintaining a {@code routing} table that maps a vehicle's *next* * <p>Responsabilidades principais:</p>
* destination (e.g., "Cr3") to a specific *direction* at *this* * <ul>
* intersection (e.g., "East"). * <li>Manter um {@link TrafficLight} para cada direção (Norte, Este, etc.)</li>
* 3. Receiving incoming vehicles and placing them in the correct * <li>Gerir uma tabela de encaminhamento que mapeia destinos para direções</li>
* traffic light's queue based on the routing table. * <li>Receber veículos e colocá-los na fila do semáforo correto</li>
* 4. Tracking aggregate statistics for all traffic passing through it. * <li>Acompanhar estatísticas agregadas do tráfego</li>
* </ul>
*/ */
public class Intersection { public class Intersection {
// --- Identity and configuration --- /** Identificador único da interseção (ex: "Cr1", "Cr2") */
/**
* Unique identifier for the intersection (e.g., "Cr1", "Cr2").
*/
private final String id; private final String id;
/** /**
* A map holding all traffic lights managed by this intersection. * Mapa com todos os semáforos desta interseção.
* Key: Direction (String, e.g., "North", "East"). * Chave: Direção (String, ex: "Norte", "Este")
* Value: The {@link TrafficLight} object for that direction. * Valor: Objeto {@link TrafficLight} correspondente
*/ */
private final Map<String, TrafficLight> trafficLights; private final Map<String, TrafficLight> trafficLights;
/** /**
* The routing table for this intersection. * Tabela de encaminhamento da interseção.
* Key: The *next* destination ID (String, e.g., "Cr3", "S" for exit). * Chave: Próximo destino (String, ex: "Cr3", "S" para saída)
* Value: The *direction* (String, e.g., "East") a vehicle must take * Valor: Direção que o veículo deve tomar nesta interseção
* at *this* intersection to reach that destination.
*/ */
private final Map<String, String> routing; private final Map<String, String> routing;
/** Número total de veículos recebidos por esta interseção */
// --- Statistics ---
/**
* Total number of vehicles that have been received by this intersection.
*/
private int totalVehiclesReceived; private int totalVehiclesReceived;
/** /** Número total de veículos que partiram desta interseção */
* Total number of vehicles that have successfully passed through (sent from) this intersection.
*/
private int totalVehiclesSent; private int totalVehiclesSent;
/** /** Média acumulada do tempo de espera dos veículos nesta interseção */
* A running average of the waiting time for vehicles at this intersection.
* Note: This calculation might be simplified.
*/
private double averageWaitingTime; private double averageWaitingTime;
/** /**
* Constructs a new Intersection with a given ID. * Cria uma nova interseção.
* Initializes empty maps for traffic lights and routing. * 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) { public Intersection(String id) {
this.id = id; this.id = id;
@@ -76,42 +62,37 @@ public class Intersection {
} }
/** /**
* Registers a new {@link TrafficLight} with this intersection. * Regista um novo semáforo nesta interseção.
* The light is mapped by its direction. * 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) { public void addTrafficLight(TrafficLight trafficLight) {
trafficLights.put(trafficLight.getDirection(), trafficLight); trafficLights.put(trafficLight.getDirection(), trafficLight);
} }
/** /**
* Defines a routing rule for this intersection. * Define uma regra de encaminhamento para esta interseção.
* * 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."
* *
* @param nextDestination The ID of the *next* intersection or exit (e.g., "Cr3", "S"). * <p>Por exemplo, {@code configureRoute("Cr3", "Este")} significa:
* @param direction The direction (and thus, the traffic light) * "Qualquer veículo que chegue aqui com destino 'Cr3' deve ser enviado
* at *this* intersection to use (e.g., "East"). * para a fila do semáforo da direção Este."</p>
*
* @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) { public void configureRoute(String nextDestination, String direction) {
routing.put(nextDestination, direction); routing.put(nextDestination, direction);
} }
/** /**
* Accepts an incoming vehicle and places it in the correct queue. * Recebe um novo veículo e coloca-o na fila do semáforo apropriado.
* * This method: * A direção é escolhida com base na tabela de encaminhamento.
* 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. * @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++; totalVehiclesReceived++;
// Note: Route advancement is handled by SimulationEngine.handleVehicleArrival() // Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
@@ -130,7 +111,7 @@ public class Intersection {
if (direction != null && trafficLights.containsKey(direction)) { if (direction != null && trafficLights.containsKey(direction)) {
// Found a valid route and light, add vehicle to the queue // Found a valid route and light, add vehicle to the queue
trafficLights.get(direction).addVehicle(vehicle); trafficLights.get(direction).addVehicle(vehicle, simulationTime);
} else { } else {
// Routing error: No rule for this destination or no light for that direction // Routing error: No rule for this destination or no light for that direction
System.err.printf( System.err.printf(
@@ -138,118 +119,100 @@ public class Intersection {
this.id, vehicle.getId(), nextDestination, direction this.id, vehicle.getId(), nextDestination, direction
); );
} }
} } /**
* Retorna a direção que um veículo deve tomar para alcançar um destino.
/**
* Returns the direction a vehicle should take to reach a given destination.
* *
* @param destination The next destination (e.g., "Cr3", "S"). * @param destination o próximo destino (ex: "Cr3", "S")
* @return The direction (e.g., "East"), or null if no route is configured. * @return a direção (ex: "Este"), ou null se não houver rota configurada
*/ */
public String getDirectionForDestination(String destination) { public String getDirectionForDestination(String destination) {
return routing.get(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"). * @param direction a direção (ex: "Norte")
* @return The {@link TrafficLight} object, or null if no light exists * @return o objeto {@link TrafficLight}, ou null se não existir
* for that direction.
*/ */
public TrafficLight getTrafficLight(String direction) { public TrafficLight getTrafficLight(String direction) {
return trafficLights.get(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<TrafficLight> getTrafficLights() { public List<TrafficLight> getTrafficLights() {
// Return a copy to prevent external modification of the internal map's values
return new ArrayList<>(trafficLights.values()); return new ArrayList<>(trafficLights.values());
} }
/** /**
* Returns the total number of vehicles currently queued across *all* * Retorna o número total de veículos em fila em todos os semáforos.
* traffic lights at this intersection. * 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() { 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() return trafficLights.values().stream()
.mapToInt(TrafficLight::getQueueSize) .mapToInt(TrafficLight::getQueueSize)
.sum(); .sum();
} }
// --- Stats and getters ---
/** /**
* @return The unique ID of this intersection. * @return o identificador único desta interseção
*/ */
public String getId() { public String getId() {
return id; 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() { public int getTotalVehiclesReceived() {
return totalVehiclesReceived; return totalVehiclesReceived;
} }
/** /**
* @return The total number of vehicles that have successfully * @return o número total de veículos que partiram desta interseção
* departed from this intersection.
*/ */
public int getTotalVehiclesSent() { public int getTotalVehiclesSent() {
return totalVehiclesSent; return totalVehiclesSent;
} }
/** /**
* Increments the counter for vehicles that have successfully departed. * Incrementa o contador de veículos que partiram com sucesso.
* This is typically called by the {@link sd.engine.SimulationEngine} * Tipicamente chamado após um veículo completar a travessia.
* after a vehicle finishes crossing.
*/ */
public void incrementVehiclesSent() { public void incrementVehiclesSent() {
totalVehiclesSent++; 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() { public double getAverageWaitingTime() {
return averageWaitingTime; return averageWaitingTime;
} }
/** /**
* Updates the running average waiting time with a new sample (a new * Atualiza a média do tempo de espera com uma nova amostra.
* vehicle's wait time). * Usa a fórmula: Nova Média = (Média Antiga * (N-1) + Novo Valor) / N
* * Uses an incremental/weighted average formula:
* NewAvg = (OldAvg * (N-1) + NewValue) / N
* where N is the total number of vehicles sent.
* *
* @param newTime The waiting time (in seconds) of the vehicle that just * @param newTime tempo de espera (em segundos) do veículo que acabou de partir
* departed.
*/ */
public void updateAverageWaitingTime(double newTime) { public void updateAverageWaitingTime(double newTime) {
// Avoid division by zero if this is called before any vehicle is sent
if (totalVehiclesSent > 0) { if (totalVehiclesSent > 0) {
averageWaitingTime = (averageWaitingTime * (totalVehiclesSent - 1) + newTime) averageWaitingTime = (averageWaitingTime * (totalVehiclesSent - 1) + newTime)
/ totalVehiclesSent; / totalVehiclesSent;
} else if (totalVehiclesSent == 1) { } else if (totalVehiclesSent == 1) {
// This is the first vehicle
averageWaitingTime = newTime; averageWaitingTime = newTime;
} }
} }
/** /**
* @return A string summary of the intersection's current state. * @return representação textual do estado atual da interseção
*/ */
@Override @Override
public String toString() { public String toString() {

View File

@@ -5,52 +5,52 @@ import java.util.UUID;
import sd.protocol.MessageProtocol; import sd.protocol.MessageProtocol;
/** /**
* Represents a message exchanged between processes in the distributed simulation. * Envelope fundamental do protocolo de comunicação entre processos distribuídos (IPC).
* Each message has a unique ID, a type, a sender, a destination, and a payload. * <p>
* This class implements {@link MessageProtocol} which extends Serializable for network transmission. * 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).
* <p>
* A imutabilidade dos campos (exceto via serialização) garante a integridade da mensagem
* durante o trânsito na rede.
*/ */
public class Message implements MessageProtocol { public class Message implements MessageProtocol {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /** * Identificador único universal (UUID).
* Unique identifier for this message. * Essencial para rastreabilidade (tracing), logs de auditoria e mecanismos de deduplicação.
*/ */
private final String messageId; private final String messageId;
/** /** Discriminador semântico que define como o recetor deve processar o payload. */
* The type of this message (e.g., VEHICLE_TRANSFER, STATS_UPDATE).
*/
private final MessageType type; private final MessageType type;
/** /** Identificador lógico do nó emissor (ex: "Cr1", "Coordinator"). */
* Identifier of the process that sent this message.
*/
private final String senderId; private final String senderId;
/** /** * Identificador lógico do nó recetor.
* Identifier of the destination process. Can be null for broadcast messages. * Se {@code null}, a mensagem deve ser tratada como <b>Broadcast</b>.
*/ */
private final String destinationId; private final String destinationId;
/** /** * Carga útil polimórfica.
* The actual data being transmitted. Type depends on the message type. * Deve implementar {@link java.io.Serializable} para garantir transmissão correta.
*/ */
private final Object payload; private final Object payload;
/** /** Marca temporal da criação da mensagem (Unix Timestamp), usada para cálculo de latência de rede. */
* Timestamp when this message was created (simulation time or real time).
*/
private final long timestamp; 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 type Classificação semântica da mensagem.
* @param senderId The ID of the sending process * @param senderId ID do processo origem.
* @param destinationId The ID of the destination process (null for broadcast) * @param destinationId ID do processo destino (ou null para broadcast).
* @param payload The message payload * @param payload Objeto de domínio a ser transportado.
* @param timestamp The timestamp of message creation * @param timestamp Instante de criação (ms).
*/ */
public Message(MessageType type, String senderId, String destinationId, public Message(MessageType type, String senderId, String destinationId,
Object payload, long timestamp) { 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 type Classificação semântica.
* @param senderId The ID of the sending process * @param senderId ID do processo origem.
* @param destinationId The ID of the destination process * @param destinationId ID do processo destino.
* @param payload The message payload * @param payload Objeto de domínio.
*/ */
public Message(MessageType type, String senderId, String destinationId, Object payload) { public Message(MessageType type, String senderId, String destinationId, Object payload) {
this(type, senderId, destinationId, payload, System.currentTimeMillis()); 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 type Classificação semântica.
* @param senderId The ID of the sending process * @param senderId ID do processo origem.
* @param payload The message payload * @param payload Objeto de domínio.
*/ */
public Message(MessageType type, String senderId, Object payload) { public Message(MessageType type, String senderId, Object payload) {
this(type, senderId, null, payload, System.currentTimeMillis()); 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() { public boolean isBroadcast() {
return destinationId == null; return destinationId == null;
} }
/** /**
* Gets the payload cast to a specific type. * Utilitário para casting seguro e fluente do payload.
* Use with caution and ensure type safety. * <p>
* Evita a necessidade de casts explícitos e supressão de warnings no código cliente.
* *
* @param <T> The expected payload type * @param <T> O tipo esperado do payload.
* @return The payload cast to type T * @param clazz A classe do tipo esperado para verificação em runtime (opcional no uso, mas boa prática).
* @throws ClassCastException if the payload is not of type T * @return O payload convertido para o tipo T.
* @throws ClassCastException Se o payload não for compatível com o tipo solicitado.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> T getPayloadAs(Class<T> clazz) { public <T> T getPayloadAs(Class<T> clazz) {

View File

@@ -1,87 +1,49 @@
package sd.model; package sd.model;
/** /**
* Enumeration representing all possible message types for distributed communication. * Enumeração que representa todos os tipos de mensagens possíveis para
* These types are used for inter-process communication between different components * comunicação distribuída.
* of the distributed traffic simulation system. * 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 { public enum MessageType {
/** /**
* Message to transfer a vehicle between intersections or processes. * Mensagem para transferir um veículo entre interseções ou processos.
* Payload: Vehicle object with current state * Payload: Objeto Vehicle com o estado atual
*/ */
VEHICLE_TRANSFER, VEHICLE_TRANSFER,
/** /**
* Message to update statistics across the distributed system. * Mensagem para atualizar estatísticas em todo o sistema distribuído.
* Payload: Statistics data (waiting times, queue sizes, etc.) * Payload: Dados estatísticos (tempos de espera, tamanhos de fila, etc.)
*/ */
STATS_UPDATE, STATS_UPDATE,
/** /**
* Message to synchronize simulation start time across all processes. * Mensagem para sincronizar a hora de início da simulação em todos os
* Payload: Start timestamp (long milliseconds) * processos.
* Payload: Timestamp de início (long milissegundos)
*/ */
SIMULATION_START, SIMULATION_START,
/** /**
* Message to synchronize traffic light states between processes. * Mensagem para notificar sobre a geração de um novo veículo.
* Payload: TrafficLight state and timing information * Payload: Parâmetros de geração do veículo
*/
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
*/ */
VEHICLE_SPAWN, VEHICLE_SPAWN,
/** /**
* Message to request the current state of an intersection. * Mensagem para sinalizar o encerramento de um processo.
* Payload: Intersection ID * Payload: ID do processo e motivo
*/
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
*/ */
SHUTDOWN, SHUTDOWN,
/** /**
* Acknowledgment message for reliable communication. * Mensagem para alterar a política de roteamento durante a simulação.
* Payload: Message ID being acknowledged * Payload: String com o nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
*/ */
ACK, ROUTING_POLICY_CHANGE,
/**
* Error message to report problems in the distributed system.
* Payload: Error description and context
*/
ERROR
} }

View File

@@ -9,114 +9,69 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
/** /**
* Represents a single traffic light controlling one direction at an intersection. * Representa um semáforo numa interseção.
* * Each light maintains its own queue of {@link Vehicle} objects and *
* alternates between {@link TrafficLightState#GREEN} and * <p>Cada semáforo controla uma direção específica e mantém uma fila de veículos à espera.
* {@link TrafficLightState#RED} states. * Alterna entre os estados VERDE e VERMELHO de acordo com a temporização configurada.</p>
* * This class is designed to be thread-safe for a potential concurrent *
* simulation (though the current engine {@link sd.engine.SimulationEngine} * <p><strong>Thread-safety:</strong> Usa locks para permitir acesso concorrente seguro entre
* is single-threaded). It uses a {@link ReentrantLock} to protect its * a thread de processamento de eventos e as threads de I/O de rede.</p>
* 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).
*/ */
public class TrafficLight { public class TrafficLight {
// --- Identity and configuration --- /** Identificador único do semáforo (ex: "Cr1-N") */
/**
* Unique identifier for the light (e.g., "Cr1-N").
*/
private final String id; private final String id;
/** /** Direção que este semáforo controla (ex: "Norte", "Sul") */
* The direction this light controls (e.g., "North", "South").
*/
private final String direction; private final String direction;
/** /** Estado atual do semáforo (VERDE ou VERMELHO) */
* The current state of the light (GREEN or RED).
*/
private TrafficLightState state; private TrafficLightState state;
// --- Vehicle management --- /** Fila de veículos à espera neste semáforo */
/**
* The queue of vehicles waiting at this light.
* {@link LinkedList} is used as it's a standard {@link Queue} implementation.
*/
private final Queue<Vehicle> queue; private final Queue<Vehicle> queue;
// --- Synchronization primitives (for thread-safety) ---
/** /**
* A lock to protect all mutable state ({@link #queue} and {@link #state}) * Lock para proteger o estado mutável ({@link #queue} e {@link #state})
* from concurrent access. Any method reading or writing these fields * de acesso concorrente.
* *must* acquire this lock first.
*/ */
private final Lock lock; private final Lock lock;
/** /** Variável de condição para sinalizar adição de veículos (uso futuro) */
* 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).
*/
private final Condition vehicleAdded; private final Condition vehicleAdded;
/** /** Variável de condição para sinalizar que o semáforo ficou verde (uso futuro) */
* 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).
*/
private final Condition lightGreen; private final Condition lightGreen;
// --- Timing configuration --- /** Duração (segundos) que o semáforo permanece VERDE */
/**
* The duration (in seconds) this light stays GREEN.
*/
private double greenTime; private double greenTime;
/** /** Duração (segundos) que o semáforo permanece VERMELHO */
* The duration (in seconds) this light stays RED.
*/
private double redTime; private double redTime;
// --- Statistics --- /** Número total de veículos processados por este semáforo */
/**
* Counter for the total number of vehicles that have
* been dequeued (processed) by this light.
*/
private int totalVehiclesProcessed; private int totalVehiclesProcessed;
/** /**
* Track when vehicles arrive at this light for wait time calculation. * Regista quando os veículos chegam ao semáforo para cálculo do tempo de espera.
* Maps vehicle ID to arrival timestamp (milliseconds). * Mapeia ID do veículo para tempo de simulação de chegada (segundos).
*/ */
private final Map<String, Long> vehicleArrivalTimes; private final Map<String, Double> vehicleArrivalTimes;
/** /**
* Constructs a new TrafficLight. * Cria um novo semáforo.
* *
* @param id The unique ID (e.g., "Cr1-N"). * @param id identificador único (ex: "Cr1-N")
* @param direction The direction (e.g., "North"). * @param direction direção controlada (ex: "Norte")
* @param greenTime The duration of the GREEN state in seconds. * @param greenTime duração do estado VERDE em segundos
* @param redTime The duration of the RED state in seconds. * @param redTime duração do estado VERMELHO em segundos
*/ */
public TrafficLight(String id, String direction, double greenTime, double redTime) { public TrafficLight(String id, String direction, double greenTime, double redTime) {
this.id = id; this.id = id;
this.direction = direction; this.direction = direction;
this.state = TrafficLightState.RED; // All lights start RED this.state = TrafficLightState.RED;
this.queue = new LinkedList<>(); this.queue = new LinkedList<>();
// Initialize synchronization objects
this.lock = new ReentrantLock(); this.lock = new ReentrantLock();
this.vehicleAdded = lock.newCondition(); this.vehicleAdded = lock.newCondition();
this.lightGreen = lock.newCondition(); this.lightGreen = lock.newCondition();
@@ -128,205 +83,187 @@ public class TrafficLight {
} }
/** /**
* Adds a vehicle to the *end* of the waiting queue. * Coloca um veículo na fila deste semáforo.
* This method is thread-safe.
* *
* @param vehicle The {@link Vehicle} to add. * 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) { public void addVehicle(Vehicle vehicle, double simulationTime) {
lock.lock(); // Acquire the lock lock.lock();
try { try {
queue.offer(vehicle); // Add vehicle to queue queue.offer(vehicle);
vehicleArrivalTimes.put(vehicle.getId(), System.currentTimeMillis()); vehicleArrivalTimes.put(vehicle.getId(), simulationTime);
vehicleAdded.signalAll(); // Signal (for concurrent models) vehicleAdded.signalAll();
} finally { } finally {
lock.unlock(); // Always release the lock lock.unlock();
} }
} }
/** /**
* Removes and returns the {@link Vehicle} from the *front* of the queue. * Remove um veículo da fila para travessia.
* * 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} * <p>Só remove se:</p>
* if the light is RED or the queue is empty. * <ul>
* <li>O semáforo estiver VERDE</li>
* <li>Existir pelo menos um veículo na fila</li>
* </ul>
*
* <p>Atualiza automaticamente as estatísticas de tempo de espera do veículo.</p>
*
* @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() { public Vehicle removeVehicle(double simulationTime) {
lock.lock(); // Acquire the lock lock.lock();
try { try {
if (state == TrafficLightState.GREEN && !queue.isEmpty()) { if (state == TrafficLightState.GREEN && !queue.isEmpty()) {
Vehicle vehicle = queue.poll(); // Remove vehicle from queue Vehicle vehicle = queue.poll();
if (vehicle != null) { if (vehicle != null) {
totalVehiclesProcessed++; totalVehiclesProcessed++;
// Calculate wait time (time spent in queue) Double arrivalTime = vehicleArrivalTimes.remove(vehicle.getId());
Long arrivalTime = vehicleArrivalTimes.remove(vehicle.getId());
if (arrivalTime != null) { if (arrivalTime != null) {
double waitTimeSeconds = (System.currentTimeMillis() - arrivalTime) / 1000.0; double waitTimeSeconds = simulationTime - arrivalTime;
vehicle.addWaitingTime(waitTimeSeconds); vehicle.addWaitingTime(waitTimeSeconds);
} }
} }
return vehicle; return vehicle;
} }
return null; // Light is RED or queue is empty return null;
} finally { } finally {
lock.unlock(); // Always release the lock lock.unlock();
} }
} }
/** /**
* Changes the lights state (e.g., RED -> GREEN). * Muda o estado do semáforo.
* 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. * @param newState novo estado (VERDE ou VERMELHO)
*/ */
public void changeState(TrafficLightState newState) { public void changeState(TrafficLightState newState) {
lock.lock(); // Acquire the lock lock.lock();
try { try {
this.state = newState; this.state = newState;
if (newState == TrafficLightState.GREEN) { if (newState == TrafficLightState.GREEN) {
lightGreen.signalAll(); // Signal (for concurrent models) lightGreen.signalAll();
} }
} finally { } finally {
lock.unlock(); // Always release the lock lock.unlock();
} }
} }
/** /**
* Returns how many vehicles are currently in the queue. * Retorna quantos veículos estão atualmente na fila.
* This method is thread-safe. * Método thread-safe.
* * @return The size of the queue. *
* @return tamanho da fila
*/ */
public int getQueueSize() { public int getQueueSize() {
lock.lock(); // Acquire the lock lock.lock();
try { try {
return queue.size(); return queue.size();
} finally { } finally {
lock.unlock(); // Always release the lock lock.unlock();
} }
} }
/** /**
* Checks whether the queue is empty. * Verifica se a fila está vazia.
* This method is thread-safe. * 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() { public boolean isQueueEmpty() {
lock.lock(); // Acquire the lock lock.lock();
try { try {
return queue.isEmpty(); return queue.isEmpty();
} finally { } finally {
lock.unlock(); // Always release the lock lock.unlock();
} }
} }
// --- Getters & Setters --- /** @return identificador único do semáforo */
/**
* @return The unique ID of this light (e.g., "Cr1-N").
*/
public String getId() { public String getId() {
return id; return id;
} }
/** /** @return direção controlada por este semáforo */
* @return The direction this light controls (e.g., "North").
*/
public String getDirection() { public String getDirection() {
return direction; return direction;
} }
/** /**
* Gets the current state of the light (GREEN or RED). * Obtém o estado atual do semáforo.
* This method is thread-safe. * Método thread-safe.
* *
* @return The current {@link TrafficLightState}. * @return estado atual (VERDE ou VERMELHO)
*/ */
public TrafficLightState getState() { public TrafficLightState getState() {
lock.lock(); // Acquire the lock lock.lock();
try { try {
return state; return state;
} finally { } finally {
lock.unlock(); // Always release the lock lock.unlock();
} }
} }
/** /** @return duração configurada do sinal verde em segundos */
* @return The configured GREEN light duration in seconds.
*/
public double getGreenTime() { public double getGreenTime() {
return greenTime; return greenTime;
} }
/** /**
* Sets the GREEN light duration. * Define a duração do sinal verde.
* @param greenTime The new duration in seconds. *
* @param greenTime nova duração em segundos
*/ */
public void setGreenTime(double greenTime) { public void setGreenTime(double greenTime) {
this.greenTime = greenTime; this.greenTime = greenTime;
} }
/** /** @return duração configurada do sinal vermelho em segundos */
* @return The configured RED light duration in seconds.
*/
public double getRedTime() { public double getRedTime() {
return redTime; return redTime;
} }
/** /**
* Sets the RED light duration. * Define a duração do sinal vermelho.
* @param redTime The new duration in seconds. *
* @param redTime nova duração em segundos
*/ */
public void setRedTime(double redTime) { public void setRedTime(double redTime) {
this.redTime = redTime; this.redTime = redTime;
} }
/** /** @return número total de veículos processados por este semáforo */
* @return The total number of vehicles processed (dequeued) by this light.
*/
public int getTotalVehiclesProcessed() { 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 totalVehiclesProcessed;
} }
/** /** @return objeto {@link Lock} para sincronização avançada */
* @return The {@link Lock} object for advanced synchronization.
*/
public Lock getLock() { public Lock getLock() {
return lock; return lock;
} }
/** /** @return condição para adição de veículos */
* @return The {@link Condition} for vehicle additions.
*/
public Condition getVehicleAdded() { public Condition getVehicleAdded() {
return vehicleAdded; return vehicleAdded;
} }
/** /** @return condição para semáforo ficar verde */
* @return The {@link Condition} for the light turning green.
*/
public Condition getLightGreen() { public Condition getLightGreen() {
return lightGreen; return lightGreen;
} }
/** /** @return representação textual do estado atual do semáforo */
* @return A string summary of the light's current state.
*/
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format(
"TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}", "TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}",
id, direction, getState(), getQueueSize() // Use getters for thread-safety id, direction, getState(), getQueueSize()
); );
} }
} }

View File

@@ -1,17 +1,13 @@
package sd.model; 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 { public enum TrafficLightState {
/** /** Sinal verde - veículos podem passar */
* The light is GREEN, allowing vehicles to pass (be dequeued).
*/
GREEN, GREEN,
/** /** Sinal vermelho - veículos aguardam na fila */
* The light is RED, blocking vehicles (they remain in the queue).
*/
RED RED
} }

View File

@@ -5,93 +5,84 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Represents a single vehicle moving through the simulation. * Representa um veículo que se move pela rede de interseções.
* *
* This class is a data object that holds the state of a vehicle, including: * <p>
* - Its unique ID, type, and entry time. * Esta classe é o "gémeo digital" de um carro, mota ou camião.
* - Its complete, pre-determined {@code route} (a list of intersection IDs). * Mantém toda a informação necessária:
* - Its current position in the route ({@code currentRouteIndex}). * </p>
* - Metrics for total time spent waiting at red lights and time spent crossing. * <ul>
* * This object is passed around the simulation, primarily inside {@link Event} * <li>Identificação e tipo do veículo</li>
* payloads and stored in {@link TrafficLight} queues. * <li>Rota completa a percorrer</li>
* * Implements {@link Serializable} so it can be sent between processes * <li>Métricas de tempo (espera, travessia, total)</li>
* or nodes (e.g., over a socket in a distributed version of the simulation). * </ul>
*
* <p>
* O objeto é serializado e enviado pela rede à medida que o veículo
* se move entre processos distribuídos.
* </p>
*/ */
public class Vehicle implements Serializable { public class Vehicle implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// --- Identity and configuration --- /** Identificador único do veículo (ex: "V1", "V2") */
/**
* Unique identifier for the vehicle (e.g., "V1", "V2").
*/
private final String id; private final String id;
/** /** Tipo de veículo (BIKE, LIGHT, HEAVY) */
* The type of vehicle (BIKE, LIGHT, HEAVY).
*/
private final VehicleType type; private final VehicleType type;
/** /** Tempo de simulação (em segundos) em que o veículo foi gerado */
* The simulation time (in seconds) when the vehicle was generated.
*/
private final double entryTime; private final double entryTime;
/** /**
* The complete, ordered list of destinations (intersection IDs and the * Lista ordenada completa de destinos (IDs de interseções e saída "S").
* final exit "S"). Example: ["Cr1", "Cr3", "S"]. * Exemplo: ["Cr1", "Cr3", "S"]
*/ */
private final List<String> route; private final List<String> route;
/** /**
* An index that tracks the vehicle's progress along its {@link #route}. * Índice que acompanha o progresso do veículo ao longo da {@link #route}.
* {@code route.get(currentRouteIndex)} is the vehicle's *current* * {@code route.get(currentRouteIndex)} é o destino *atual* do veículo.
* destination (i.e., the one it is traveling *towards* or *arriving at*).
*/ */
private int currentRouteIndex; private int currentRouteIndex;
// --- Metrics ---
/** /**
* The total accumulated time (in seconds) this vehicle has spent * Tempo total acumulado (segundos) que o veículo passou à espera em semáforos
* waiting at red lights. * vermelhos
*/ */
private double totalWaitingTime; private double totalWaitingTime;
/** /**
* The total accumulated time (in seconds) this vehicle has spent * Tempo total acumulado (segundos) que o veículo passou a atravessar
* actively crossing intersections. * interseções
*/ */
private double totalCrossingTime; private double totalCrossingTime;
/** /**
* Constructs a new Vehicle. * Cria um novo veículo pronto para se fazer à estrada.
* *
* @param id The unique ID for the vehicle. * @param id Identificador único (ex: "V1").
* @param type The {@link VehicleType}. * @param type O tipo de veículo (determina velocidade/tamanho).
* @param entryTime The simulation time when the vehicle is created. * @param entryTime Quando este veículo entrou na simulação (segundos).
* @param route The complete list of destination IDs (e.t., ["Cr1", "Cr2", "S"]). * @param route A lista ordenada de paragens (Interseções -> Saída).
*/ */
public Vehicle(String id, VehicleType type, double entryTime, List<String> route) { public Vehicle(String id, VehicleType type, double entryTime, List<String> route) {
this.id = id; this.id = id;
this.type = type; this.type = type;
this.entryTime = entryTime; this.entryTime = entryTime;
// Create a copy of the route list to ensure immutability
this.route = new ArrayList<>(route); this.route = new ArrayList<>(route);
this.currentRouteIndex = 0; // Starts at the first destination this.currentRouteIndex = 0;
this.totalWaitingTime = 0.0; this.totalWaitingTime = 0.0;
this.totalCrossingTime = 0.0; this.totalCrossingTime = 0.0;
} }
/** /**
* Advances the vehicle to the next stop in its route by * Move o GPS interno do veículo para o próximo destino.
* 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 * Chame isto quando um veículo chega a uma interseção para atualizar para onde
* in the route, {@code false} if the vehicle has passed its * deve ir a seguir.
* final destination. *
* @return true se houver mais paragens, false se a viagem terminou.
*/ */
public boolean advanceRoute() { public boolean advanceRoute() {
currentRouteIndex++; currentRouteIndex++;
@@ -99,120 +90,94 @@ public class Vehicle implements Serializable {
} }
/** /**
* Gets the current destination (the next intersection or exit) that * Obtém o destino atual (próxima interseção ou saída) para onde o veículo se
* the vehicle is heading towards. * dirige.
* *
* @return The ID of the current destination (e.g., "Cr1"), or * @return ID do destino atual (ex: "Cr1"), ou {@code null} se a rota terminou
* {@code null} if the route is complete.
*/ */
public String getCurrentDestination() { public String getCurrentDestination() {
return (currentRouteIndex < route.size()) ? route.get(currentRouteIndex) : null; 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 * @return {@code true} se chegou ao fim da rota, {@code false} caso contrário
* of the route list, {@code false} otherwise.
*/ */
public boolean hasReachedEnd() { public boolean hasReachedEnd() {
return currentRouteIndex >= route.size(); return currentRouteIndex >= route.size();
} }
// --- Getters and metrics management --- /** @return identificador único do veículo */
/**
* @return The vehicle's unique ID.
*/
public String getId() { public String getId() {
return id; return id;
} }
/** /** @return tipo do veículo */
* @return The vehicle's {@link VehicleType}.
*/
public VehicleType getType() { public VehicleType getType() {
return type; return type;
} }
/** /** @return tempo de simulação em que o veículo entrou no sistema */
* @return The simulation time when the vehicle entered the system.
*/
public double getEntryTime() { public double getEntryTime() {
return entryTime; return entryTime;
} }
/** /** @return cópia da rota completa do veículo */
* @return A *copy* of the vehicle's complete route.
*/
public List<String> getRoute() { public List<String> getRoute() {
// Return a copy to prevent external modification
return new ArrayList<>(route); return new ArrayList<>(route);
} }
/** /** @return índice atual apontando para o destino do veículo na sua rota */
* @return The current index pointing to the vehicle's destination in its route list.
*/
public int getCurrentRouteIndex() { public int getCurrentRouteIndex() {
return currentRouteIndex; return currentRouteIndex;
} }
/** /** @return tempo total acumulado de espera em segundos */
* @return The total accumulated waiting time in seconds.
*/
public double getTotalWaitingTime() { public double getTotalWaitingTime() {
return totalWaitingTime; return totalWaitingTime;
} }
/** /**
* Adds a duration to the vehicle's total waiting time. * Adiciona uma duração ao tempo total de espera do veículo.
* This is called by the simulation engine when a vehicle * Chamado quando um veículo começa a atravessar uma interseção.
* starts crossing an intersection.
* *
* @param time The duration (in seconds) to add. * @param time duração (em segundos) a adicionar
*/ */
public void addWaitingTime(double time) { public void addWaitingTime(double time) {
totalWaitingTime += time; totalWaitingTime += time;
} }
/** /** @return tempo total acumulado de travessia em segundos */
* @return The total accumulated crossing time in seconds.
*/
public double getTotalCrossingTime() { public double getTotalCrossingTime() {
return totalCrossingTime; return totalCrossingTime;
} }
/** /**
* Adds a duration to the vehicle's total crossing time. * Adiciona uma duração ao tempo total de travessia do veículo.
* This is called by the simulation engine when a vehicle * Chamado quando um veículo termina de atravessar uma interseção.
* finishes crossing an intersection.
* *
* @param time The duration (in seconds) to add. * @param time duração (em segundos) a adicionar
*/ */
public void addCrossingTime(double time) { public void addCrossingTime(double time) {
totalCrossingTime += time; totalCrossingTime += time;
} }
/** /**
* Calculates the vehicle's total time spent in the system so far. * Calcula o tempo total que o veículo passou no sistema até agora.
* This is a "live" calculation.
* *
* @param currentTime The current simulation time. * @param currentTime tempo atual da simulação
* @return The total elapsed time (in seconds) since the vehicle * @return tempo total decorrido (em segundos) desde que o veículo foi gerado
* was generated ({@code currentTime - entryTime}).
*/ */
public double getTotalTravelTime(double currentTime) { public double getTotalTravelTime(double currentTime) {
return currentTime - entryTime; return currentTime - entryTime;
} }
/** /** @return representação textual do estado atual do veículo */
* @return A string summary of the vehicle's current state.
*/
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format(
"Vehicle{id='%s', type=%s, next='%s', route=%s}", "Vehicle{id='%s', type=%s, next='%s', route=%s}",
id, type, getCurrentDestination(), route id, type, getCurrentDestination(), route);
);
} }
} }

View File

@@ -1,27 +1,19 @@
package sd.model; package sd.model;
/** /**
* Enumeration representing the different types of vehicles in the simulation. * Enumeração dos diferentes tipos de veículos na simulação.
* Each type can have different properties, such as crossing time *
* and generation probability, defined in {@link sd.config.SimulationConfig}. * <p>Cada tipo pode ter propriedades diferentes como tempo de travessia
* e probabilidade de geração, definidas na {@link sd.config.SimulationConfig}.</p>
*/ */
public enum VehicleType { public enum VehicleType {
/** /** Bicicleta ou motocicleta - tempo de travessia curto */
* A bike or motorcycle.
* Typically has a short crossing time.
*/
BIKE, BIKE,
/** /** Veículo ligeiro padrão (carro) - tipo mais comum */
* A standard light vehicle, such as a car.
* This is usually the most common type.
*/
LIGHT, LIGHT,
/** /** Veículo pesado (camião ou autocarro) - tempo de travessia longo */
* A heavy vehicle, such as a truck or bus.
* Typically has a long crossing time.
*/
HEAVY HEAVY
} }

View File

@@ -1,41 +1,45 @@
package sd.protocol; package sd.protocol;
import java.io.Serializable; import java.io.Serializable;
import sd.model.MessageType; // Assuming MessageType is in sd.model or sd.protocol import sd.model.MessageType; // Assuming MessageType is in sd.model or sd.protocol
/** /**
* Interface defining the contract for all messages exchanged in the simulator. * Contrato para todas as mensagens trocadas no simulador.
* 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).
* *
* <p>Garante que mensagens podem ser identificadas e encaminhadas.
* Extende Serializable para permitir envio via sockets.
*/ */
public interface MessageProtocol extends Serializable { public interface MessageProtocol extends Serializable {
/** /**
* Returns the type of the message, indicating its purpose. * Tipo da mensagem, indicando o seu propósito.
* @return The MessageType (e.g., VEHICLE_TRANSFER, STATS_UPDATE). * @return tipo (ex: VEHICLE_TRANSFER, STATS_UPDATE)
*/ */
MessageType getType(); MessageType getType();
/** /**
* Returns the data object (payload) that this message carries. * Dados (payload) que esta mensagem transporta.
* The type of object will depend on the MessageType. *
* * - If getType() == VEHICLE_TRANSFER, the payload will be a {@link sd.model.Vehicle} object. * <p>Tipo depende do MessageType:
* - If getType() == STATS_UPDATE, the payload will be a statistics object. * <ul>
* * @return The data object (payload), which must also be Serializable. * <li>VEHICLE_TRANSFER → objeto Vehicle
* <li>STATS_UPDATE → objeto de estatísticas
* </ul>
*
* @return payload (deve ser Serializable)
*/ */
Object getPayload(); Object getPayload();
/** /**
* Returns the ID of the node (Process) that sent this message. * ID do nó (processo) que enviou a mensagem.
* @return String (e.g., "Cr1", "Cr5", "S"). * @return ID de origem (ex: "Cr1", "Cr5", "S")
*/ */
String getSourceNode(); String getSourceNode();
/** /**
* Returns the ID of the destination node (Process) for this message. * ID do nó de destino.
* @return String (e.g., "Cr2", "DashboardServer"). * @return ID de destino (ex: "Cr2", "DashboardServer")
*/ */
String getDestinationNode(); String getDestinationNode();
} }

View File

@@ -4,7 +4,6 @@ import java.io.Closeable;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.ConnectException; import java.net.ConnectException;
@@ -17,10 +16,17 @@ import sd.serialization.MessageSerializer;
import sd.serialization.SerializationException; import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory; import sd.serialization.SerializerFactory;
/** /**
* Wrapper class that simplifies communication via Sockets. * Wrapper de alto nível para gestão robusta de conexões TCP.
* Includes connection retry logic for robustness. * <p>
* Esta classe abstrai a complexidade da API nativa {@link java.net.Socket}, oferecendo:
* <ol>
* <li><b>Resiliência:</b> 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.</li>
* <li><b>Framing:</b> Implementação transparente do protocolo "Length-Prefix" (4 bytes de tamanho + payload),
* resolvendo o problema de fragmentação de stream TCP.</li>
* <li><b>Serialização:</b> Integração direta com a camada de serialização JSON.</li>
* </ol>
*/ */
public class SocketConnection implements Closeable { public class SocketConnection implements Closeable {
@@ -29,22 +35,24 @@ public class SocketConnection implements Closeable {
private final InputStream inputStream; private final InputStream inputStream;
private final MessageSerializer serializer; private final MessageSerializer serializer;
// --- Configuration for Retry Logic --- /** Número máximo de tentativas de ligação antes de desistir (Fail-fast). */
/** Maximum number of connection attempts. */
private static final int MAX_RETRIES = 5; 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; private static final long RETRY_DELAY_MS = 1000;
/** /**
* Constructor for the "Client" (who initiates the connection). * Construtor para clientes (Active Open).
* Tries to connect to a process that is already listening (Server). * Tenta estabelecer uma conexão TCP com um servidor, aplicando lógica de retry.
* Includes retry logic in case of initial connection failure. * <p>
* 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 host Endereço do nó de destino (ex: "localhost").
* @param port The port (e.g., 8001 from your simulation.properties) * @param port Porta de serviço.
* @throws IOException If connection fails after all retries. * @throws IOException Se a conexão falhar após todas as {@code MAX_RETRIES} tentativas.
* @throws UnknownHostException If the host is not found (this error usually doesn't need retry). * @throws UnknownHostException Se o DNS não resolver o hostname.
* @throws InterruptedException If the thread is interrupted while waiting between retries. * @throws InterruptedException Se a thread for interrompida durante o sleep de retry.
*/ */
public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException { public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException {
Socket tempSocket = null; Socket tempSocket = null;
@@ -55,7 +63,7 @@ public class SocketConnection implements Closeable {
// --- Retry Loop --- // --- Retry Loop ---
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try { try {
// Try to establish the connection // Try to establish the connection (SYN -> SYN-ACK -> ACK)
tempSocket = new Socket(host, port); tempSocket = new Socket(host, port);
// If successful, break out of the retry loop // If successful, break out of the retry loop
@@ -64,17 +72,17 @@ public class SocketConnection implements Closeable {
break; break;
} catch (ConnectException | SocketTimeoutException e) { } 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; lastException = e;
System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n", System.out.printf("[SocketConnection] Attempt %d/%d failed: %s. Retrying in %d ms...%n",
attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS); attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
if (attempt < MAX_RETRIES) { if (attempt < MAX_RETRIES) {
// Wait before the next attempt // Blocking wait before next attempt
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS); TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS);
} }
} catch (IOException e) { } catch (IOException e) {
// Other IOExceptions might be more permanent, but we retry anyway. // Other IO errors
lastException = e; lastException = e;
System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n", System.out.printf("[SocketConnection] Attempt %d/%d failed with IOException: %s. Retrying in %d ms...%n",
attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS); attempt, MAX_RETRIES, e.getMessage(), RETRY_DELAY_MS);
@@ -84,51 +92,49 @@ public class SocketConnection implements Closeable {
} }
} // --- End of Retry Loop --- } // --- End of Retry Loop ---
// If after all retries tempSocket is still null, it means connection failed permanently. // Final validation
if (tempSocket == null) { if (tempSocket == null) {
System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES); System.err.printf("[SocketConnection] Failed to connect to %s:%d after %d attempts.%n", host, port, MAX_RETRIES);
if (lastException != null) { if (lastException != null) {
throw lastException; // Throw the last exception encountered throw lastException; // Propagate the root cause
} else { } else {
// Should not happen if loop ran, but as a fallback
throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts, reason unknown."); 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.socket = tempSocket;
this.outputStream = socket.getOutputStream(); this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream(); this.inputStream = socket.getInputStream();
this.serializer = SerializerFactory.createDefault(); this.serializer = SerializerFactory.createDefault();
} }
/** /**
* Constructor for the "Server" (who accepts the connection). * Construtor para servidores (Passive Open).
* Receives a Socket that has already been accepted by a ServerSocket. * Envolve um socket já conectado (retornado por {@code serverSocket.accept()}).
* No retry logic needed here as the connection is already established. * Não necessita de retry logic pois a conexão física já existe.
* *
* @param acceptedSocket The Socket returned by serverSocket.accept(). * @param acceptedSocket O socket ativo retornado pelo SO.
* @throws IOException If stream creation fails. * @throws IOException Se falhar a obtenção dos streams de I/O.
*/ */
public SocketConnection(Socket acceptedSocket) throws IOException { public SocketConnection(Socket acceptedSocket) throws IOException {
this.socket = acceptedSocket; this.socket = acceptedSocket;
this.outputStream = socket.getOutputStream(); this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream(); this.inputStream = socket.getInputStream();
this.serializer = SerializerFactory.createDefault(); this.serializer = SerializerFactory.createDefault();
} }
/** /**
* Sends (serializes) a MessageProtocol object over the socket. * Serializa e transmite uma mensagem através do canal.
* <p>
* 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. * @param message O objeto de protocolo a enviar.
* @throws IOException If writing to the stream fails or socket is not connected. * @throws IOException Se o socket estiver fechado ou ocorrer erro de escrita.
*/ */
public void sendMessage(MessageProtocol message) throws IOException { 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"); throw new IOException("Socket is not connected");
} }
@@ -136,11 +142,11 @@ public class SocketConnection implements Closeable {
// Serializa para bytes JSON // Serializa para bytes JSON
byte[] data = serializer.serialize(message); byte[] data = serializer.serialize(message);
// Write 4-byte length prefix // Write 4-byte length prefix (Framing)
DataOutputStream dataOut = new DataOutputStream(outputStream); DataOutputStream dataOut = new DataOutputStream(outputStream);
dataOut.writeInt(data.length); dataOut.writeInt(data.length);
dataOut.write(data); dataOut.write(data);
dataOut.flush(); dataOut.flush(); // Force transmission immediately
} catch (SerializationException e) { } catch (SerializationException e) {
throw new IOException("Failed to serialize message", e); throw new IOException("Failed to serialize message", e);
@@ -148,11 +154,14 @@ public class SocketConnection implements Closeable {
} }
/** /**
* Tries to read (deserialize) a MessageProtocol object from the socket. * Bloqueia à espera de uma mensagem completa do socket.
* <p>
* 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. * @return O objeto {@link MessageProtocol} reconstruído.
* @throws IOException If the connection is lost, the stream is corrupted, or socket is not connected. * @throws IOException Se a conexão for perdida (EOF) ou o stream corrompido.
* @throws ClassNotFoundException If the received object is unknown. * @throws ClassNotFoundException Se o tipo desserializado não for encontrado no classpath.
*/ */
public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException { public MessageProtocol receiveMessage() throws IOException, ClassNotFoundException {
if (socket == null || !socket.isConnected()) { if (socket == null || !socket.isConnected()) {
@@ -164,15 +173,16 @@ public class SocketConnection implements Closeable {
DataInputStream dataIn = new DataInputStream(inputStream); DataInputStream dataIn = new DataInputStream(inputStream);
int length = dataIn.readInt(); 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); throw new IOException("Invalid message length: " + length);
} }
// Ler dados da mensagem // Ler dados exatos da mensagem
byte[] data = new byte[length]; byte[] data = new byte[length];
dataIn.readFully(data); dataIn.readFully(data);
// Deserialize do JSON - use concrete Message class, not interface // Deserialize do JSON - força o tipo concreto Message
return serializer.deserialize(data, sd.model.Message.class); return serializer.deserialize(data, sd.model.Message.class);
} catch (SerializationException e) { } catch (SerializationException e) {
@@ -181,7 +191,8 @@ 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 @Override
public void close() throws IOException { public void close() throws IOException {
@@ -191,7 +202,8 @@ public class SocketConnection implements Closeable {
} }
/** /**
* @return true if the socket is still connected and not closed. * Verifica o estado operacional da conexão.
* @return true se o socket está aberto e conectado.
*/ */
public boolean isConnected() { public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed(); return socket != null && socket.isConnected() && !socket.isClosed();

View File

@@ -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.
*
* <p>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.</p>
*
* <p>Objetivo: Distribuir o tráfego pela rede, evitando bottlenecks e
* minimizando o tempo de espera total.</p>
*
* <p><strong>Algoritmo:</strong></p>
* <ol>
* <li>Para cada rota possível, calcula a carga total (soma das filas)</li>
* <li>Escolhe a rota com menor carga total</li>
* <li>Em caso de empate ou falta de informação, usa a rota mais curta</li>
* </ol>
*/
public class LeastCongestedRouteSelector implements RouteSelector {
/** Rotas possíveis a partir do ponto de entrada E1 */
private final List<List<String>> e1Routes;
/** Rotas possíveis a partir do ponto de entrada E2 */
private final List<List<String>> e2Routes;
/** Rotas possíveis a partir do ponto de entrada E3 */
private final List<List<String>> 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<String> selectRoute(String entryPoint, Map<String, Integer> queueSizes) {
List<List<String>> 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<String> bestRoute = null;
int minLoad = Integer.MAX_VALUE;
for (List<String> 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<String> route, Map<String, Integer> 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<String> selectShortestRoute(List<List<String>> routes) {
List<String> shortest = routes.get(0);
for (List<String> 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<List<String>> 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;
}
}
}

View File

@@ -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).
*
* <p>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.</p>
*
* <p>As rotas são organizadas por ponto de entrada (E1, E2, E3) e cada rota
* tem uma probabilidade de seleção associada.</p>
*/
public class RandomRouteSelector implements RouteSelector {
/** Rotas possíveis a partir do ponto de entrada E1 */
private final List<RouteWithProbability> e1Routes;
/** Rotas possíveis a partir do ponto de entrada E2 */
private final List<RouteWithProbability> e2Routes;
/** Rotas possíveis a partir do ponto de entrada E3 */
private final List<RouteWithProbability> 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<String> selectRoute(String entryPoint, Map<String, Integer> queueSizes) {
// Ignora queueSizes - seleção aleatória não depende do estado da rede
List<RouteWithProbability> 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<RouteWithProbability> 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<String> route;
final double probability;
RouteWithProbability(List<String> route, double probability) {
this.route = route;
this.probability = probability;
}
}
}

View File

@@ -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.
*
* <p>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.).</p>
*/
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<String> selectRoute(String entryPoint, Map<String, Integer> queueSizes);
}

View File

@@ -0,0 +1,36 @@
package sd.routing;
/**
* Enumeração que define as políticas de roteamento disponíveis para a simulação.
*
* <p>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.</p>
*
* <ul>
* <li><strong>RANDOM:</strong> Seleção aleatória de rotas baseada em probabilidades predefinidas</li>
* <li><strong>SHORTEST_PATH:</strong> Escolhe sempre a rota com o menor número de cruzamentos</li>
* <li><strong>LEAST_CONGESTED:</strong> Escolhe a rota evitando cruzamentos mais congestionados</li>
* </ul>
*/
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
}

View File

@@ -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.
*
* <p>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).</p>
*
* <p>Objetivo: Minimizar a distância teórica percorrida pelos veículos.</p>
*/
public class ShortestPathRouteSelector implements RouteSelector {
/** Rotas possíveis a partir do ponto de entrada E1, ordenadas por comprimento */
private final List<List<String>> e1Routes;
/** Rotas possíveis a partir do ponto de entrada E2, ordenadas por comprimento */
private final List<List<String>> e2Routes;
/** Rotas possíveis a partir do ponto de entrada E3, ordenadas por comprimento */
private final List<List<String>> 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<String> selectRoute(String entryPoint, Map<String, Integer> queueSizes) {
// Ignora queueSizes - política baseada apenas no comprimento do caminho
List<List<String>> availableRoutes = getRoutesForEntryPoint(entryPoint);
// Retorna a rota mais curta (primeira da lista)
List<String> 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<List<String>> 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;
}
}
}

View File

@@ -1,26 +1,25 @@
package sd.serialization; package sd.serialization;
import java.nio.charset.StandardCharsets;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import java.nio.charset.StandardCharsets;
/** /**
* JSON-based implementation of {@link MessageSerializer} using Google's Gson library. * Implementação baseada em JSON da estratégia {@link MessageSerializer}, utilizando a biblioteca Gson.
* * <p>
* This serializer converts objects to JSON format for transmission, providing: * Este serializador converte objetos Java para o formato de texto JSON antes da transmissão.
* - Human-readable message format (easy debugging) * Oferece várias vantagens técnicas sobre a serialização nativa do Java:
* - Cross-platform compatibility * <ul>
* - Smaller message sizes compared to Java native serialization * <li><b>Legibilidade:</b> O formato de texto facilita a depuração (sniffing de rede) sem ferramentas especializadas.</li>
* - Better security (no code execution during deserialization) * <li><b>Interoperabilidade:</b> Permite futura integração com componentes não-Java (ex: Dashboards web em JS).</li>
* * <li><b>Segurança:</b> Reduz a superfície de ataque para execução remota de código (RCE), pois não desserializa classes arbitrárias, apenas dados.</li>
* The serializer is configured with pretty printing disabled by default for * </ul>
* production use, but can be enabled for debugging purposes. * <p>
* * <b>Thread-Safety:</b> A instância interna do {@code Gson} é imutável e thread-safe, permitindo
* Thread-safety: This class is thread-safe as Gson instances are thread-safe. * que este serializador seja partilhado entre múltiplas threads (ex: no pool do DashboardServer).
* * * @see MessageSerializer
* @see MessageSerializer
*/ */
public class JsonMessageSerializer implements MessageSerializer { public class JsonMessageSerializer implements MessageSerializer {
@@ -28,16 +27,16 @@ public class JsonMessageSerializer implements MessageSerializer {
private final boolean prettyPrint; 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() { public JsonMessageSerializer() {
this(false); this(false);
} }
/** /**
* Creates a new JSON serializer with optional pretty printing. * Cria um novo serializador JSON com formatação opcional.
* * * @param prettyPrint Se {@code true}, o JSON gerado incluirá indentação e quebras de linha.
* @param prettyPrint If true, JSON output will be formatted with indentation * Útil para debug, mas aumenta significativamente o tamanho do payload.
*/ */
public JsonMessageSerializer(boolean prettyPrint) { public JsonMessageSerializer(boolean prettyPrint) {
this.prettyPrint = prettyPrint; this.prettyPrint = prettyPrint;
@@ -53,6 +52,13 @@ public class JsonMessageSerializer implements MessageSerializer {
this.gson = builder.create(); 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 @Override
public byte[] serialize(Object object) throws SerializationException { public byte[] serialize(Object object) throws SerializationException {
if (object == null) { 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.
* <p>
* 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 @Override
public <T> T deserialize(byte[] data, Class<T> clazz) throws SerializationException { public <T> T deserialize(byte[] data, Class<T> clazz) throws SerializationException {
if (data == null) { if (data == null) {
@@ -95,18 +111,16 @@ public class JsonMessageSerializer implements MessageSerializer {
} }
/** /**
* Returns the underlying Gson instance for advanced usage. * Retorna a instância subjacente do Gson para configurações avançadas.
* * * @return A instância Gson configurada.
* @return The Gson instance
*/ */
public Gson getGson() { public Gson getGson() {
return gson; return gson;
} }
/** /**
* Checks if pretty printing is enabled. * Verifica se a formatação "pretty print" está ativa.
* * * @return true se a indentação estiver habilitada.
* @return true if pretty printing is enabled
*/ */
public boolean isPrettyPrint() { public boolean isPrettyPrint() {
return prettyPrint; return prettyPrint;

View File

@@ -1,47 +1,48 @@
package sd.serialization; package sd.serialization;
/** /**
* Interface for serializing and deserializing objects for network transmission. * Interface que define o contrato para estratégias de serialização e desserialização de objetos.
* * <p>
* This interface provides a common abstraction for different serialization strategies * Esta abstração permite desacoplar a camada de transporte (Sockets TCP) da camada de
* allowing the system to switch between implementations without changing the communication layer. * apresentação de dados. Ao implementar o padrão <b>Strategy</b>, o sistema ganha flexibilidade
* * para alternar entre diferentes formatos de codificação (JSON, Binário Nativo, XML, Protobuf)
* Implementations must ensure: * sem necessidade de refatorização da lógica de rede.
* - Thread-safety if used in concurrent contexts * <p>
* - Proper exception handling with meaningful error messages * <b>Requisitos para Implementações:</b>
* - Preservation of object state during round-trip serialization * <ul>
* * <li><b>Thread-Safety:</b> As implementações devem ser seguras para uso concorrente, dado que
* @see JsonMessageSerializer * instâncias únicas podem ser partilhadas por múltiplos <i>ClientHandlers</i>.</li>
* <li><b>Robustez:</b> Falhas de parsing devem resultar em exceções tipificadas ({@link SerializationException}),
* nunca em falhas silenciosas ou estados inconsistentes.</li>
* </ul>
* * @see JsonMessageSerializer
*/ */
public interface MessageSerializer { public interface MessageSerializer {
/** /**
* Serializes an object into a byte array for transmission. * 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).
* @param object The object to serialize (must not be null) * @return Um array de bytes contendo a representação codificada do objeto.
* @return A byte array containing the serialized representation * @throws SerializationException Se ocorrer um erro durante a codificação (ex: ciclo de referências).
* @throws SerializationException If serialization fails * @throws IllegalArgumentException Se o objeto fornecido for nulo.
* @throws IllegalArgumentException If object is null
*/ */
byte[] serialize(Object object) throws SerializationException; byte[] serialize(Object object) throws SerializationException;
/** /**
* Deserializes a byte array back into an object of the specified type. * Reconstrói (Unmarshals) um objeto a partir de uma sequência de bytes.
* * * @param <T> O tipo genérico do objeto esperado.
* @param <T> The expected type of the deserialized object * @param data O array de bytes contendo os dados serializados (não pode ser nulo).
* @param data The byte array containing serialized data (must not be null) * @param clazz A classe do tipo esperado para verificação e instancialização.
* @param clazz The class of the expected object type (must not be null) * @return A instância do objeto reconstruído com o seu estado restaurado.
* @return The deserialized object * @throws SerializationException Se os dados estiverem corrompidos ou incompatíveis com a classe alvo.
* @throws SerializationException If deserialization fails * @throws IllegalArgumentException Se os dados ou a classe forem nulos.
* @throws IllegalArgumentException If data or clazz is null
*/ */
<T> T deserialize(byte[] data, Class<T> clazz) throws SerializationException; <T> T deserialize(byte[] data, Class<T> clazz) throws SerializationException;
/** /**
* Gets the name of this serialization strategy (e.g., "JSON", "Java Native"). * Obtém o identificador legível desta estratégia de serialização (ex: "JSON (Gson)", "Native").
* Useful for logging and debugging. * Utilizado primariamente para logging, auditoria e negociação de conteúdo.
* * * @return O nome descritivo do serializador.
* @return The serializer name
*/ */
String getName(); String getName();

View File

@@ -1,134 +0,0 @@
package sd.serialization;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import java.util.Arrays;
import java.util.List;
/**
* Demonstration of JSON serialization usage in the traffic simulation system.
*
* This class shows practical examples of how to use JSON (Gson) serialization
* for network communication between simulation processes.
*/
public class SerializationExample {
public static void main(String[] args) {
System.out.println("=== JSON Serialization Example ===\n");
// Create a sample vehicle
List<String> route = Arrays.asList("Cr1", "Cr2", "Cr5", "S");
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 10.5, route);
vehicle.addWaitingTime(2.3);
vehicle.addCrossingTime(1.2);
// Create a message containing the vehicle
Message message = new Message(
MessageType.VEHICLE_TRANSFER,
"Cr1",
"Cr2",
vehicle
);
// ===== JSON Serialization =====
demonstrateJsonSerialization(message);
// ===== Factory Usage =====
demonstrateFactoryUsage(message);
// ===== Performance Test =====
performanceTest(message);
}
private static void demonstrateJsonSerialization(Message message) {
System.out.println("--- JSON Serialization ---");
try {
// Create JSON serializer with pretty printing for readability
MessageSerializer serializer = new JsonMessageSerializer(true);
// Serialize to bytes
byte[] data = serializer.serialize(message);
// Display the JSON
String json = new String(data);
System.out.println("Serialized JSON (" + data.length + " bytes):");
System.out.println(json);
// Deserialize back
Message deserialized = serializer.deserialize(data, Message.class);
System.out.println("\nDeserialized: " + deserialized);
System.out.println("✓ JSON serialization successful\n");
} catch (SerializationException e) {
System.err.println("❌ JSON serialization failed: " + e.getMessage());
}
}
private static void demonstrateFactoryUsage(Message message) {
System.out.println("--- Using SerializerFactory ---");
try {
// Get default serializer (JSON)
MessageSerializer serializer = SerializerFactory.createDefault();
System.out.println("Default serializer: " + serializer.getName());
// Use it
byte[] data = serializer.serialize(message);
Message deserialized = serializer.deserialize(data, Message.class);
System.out.println("Message type: " + deserialized.getType());
System.out.println("From: " + deserialized.getSenderId() +
" → To: " + deserialized.getDestinationId());
System.out.println("✓ Factory usage successful\n");
} catch (SerializationException e) {
System.err.println("❌ Factory usage failed: " + e.getMessage());
}
}
private static void performanceTest(Message message) {
System.out.println("--- Performance Test ---");
int iterations = 1000;
try {
MessageSerializer compactSerializer = new JsonMessageSerializer(false);
MessageSerializer prettySerializer = new JsonMessageSerializer(true);
// Warm up
for (int i = 0; i < 100; i++) {
compactSerializer.serialize(message);
}
// Test compact JSON
long compactStart = System.nanoTime();
byte[] compactData = null;
for (int i = 0; i < iterations; i++) {
compactData = compactSerializer.serialize(message);
}
long compactTime = System.nanoTime() - compactStart;
// Test pretty JSON
byte[] prettyData = prettySerializer.serialize(message);
// Results
System.out.println("Iterations: " + iterations);
System.out.println("\nJSON Compact:");
System.out.println(" Size: " + compactData.length + " bytes");
System.out.println(" Time: " + (compactTime / 1_000_000.0) + " ms total");
System.out.println(" Avg: " + (compactTime / iterations / 1_000.0) + " μs/operation");
System.out.println("\nJSON Pretty-Print:");
System.out.println(" Size: " + prettyData.length + " bytes");
System.out.println(" Size increase: " +
String.format("%.1f%%", ((double)prettyData.length / compactData.length - 1) * 100));
} catch (SerializationException e) {
System.err.println("❌ Performance test failed: " + e.getMessage());
}
}
}

View File

@@ -1,39 +1,38 @@
package sd.serialization; package sd.serialization;
/** /**
* Exception thrown when serialization or deserialization operations fail. * Exceção verificada (Checked Exception) que sinaliza falhas no processo de transformação de dados.
* * <p>
* This exception wraps underlying errors (I/O exceptions, parsing errors, etc.) * Esta classe atua como um wrapper unificador para erros ocorridos na camada de serialização,
* and provides context about what went wrong during the serialization process. * 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 { public class SerializationException extends Exception {
private static final long serialVersionUID = 1L; // Long(64bits) instead of int(32bits) private static final long serialVersionUID = 1L; // Long(64bits) instead of int(32bits)
/** /**
* Constructs a new serialization exception with the specified detail message. * Constrói uma nova exceção de serialização com uma mensagem descritiva.
* * * @param message A mensagem detalhando o erro.
* @param message The detail message
*/ */
public SerializationException(String message) { public SerializationException(String message) {
super(message); super(message);
} }
/** /**
* Constructs a new serialization exception with the specified detail message * Constrói uma nova exceção encapsulando a causa raiz do problema.
* and cause. * Útil para preservar a stack trace original de erros de bibliotecas terceiras (ex: Gson).
* * * @param message A mensagem detalhando o erro.
* @param message The detail message * @param cause A exceção original que causou a falha.
* @param cause The cause of this exception
*/ */
public SerializationException(String message, Throwable cause) { public SerializationException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
/** /**
* Constructs a new serialization exception with the specified cause. * Constrói uma nova exceção baseada apenas na causa raiz.
* * * @param cause A exceção original.
* @param cause The cause of this exception
*/ */
public SerializationException(Throwable cause) { public SerializationException(Throwable cause) {
super(cause); super(cause);

View File

@@ -1,14 +1,14 @@
package sd.serialization; package sd.serialization;
/** /**
* Factory for creating {@link MessageSerializer} instances. * Fábrica estática (Factory Pattern) para instanciação controlada de {@link MessageSerializer}.
* * <p>
* This factory provides a centralized way to create and configure JSON serializers * Esta classe centraliza a criação de estratégias de serialização, garantindo consistência
* using Gson, making it easy to configure serialization throughout the application. * 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
* The factory can be configured via system properties for easy deployment configuration. * depuração (Pretty Print) e produção (Compacto) sem recompilação.
* * <p>
* Example usage: * <b>Exemplo de Uso:</b>
* <pre> * <pre>
* MessageSerializer serializer = SerializerFactory.createDefault(); * MessageSerializer serializer = SerializerFactory.createDefault();
* byte[] data = serializer.serialize(myObject); * byte[] data = serializer.serialize(myObject);
@@ -17,28 +17,27 @@ package sd.serialization;
public class SerializerFactory { public class SerializerFactory {
/** /**
* System property key for enabling pretty-print in JSON serialization. * Chave da propriedade de sistema para ativar a formatação JSON legível (Pretty Print).
* Set to "true" for debugging, "false" for production. * Defina {@code -Dsd.serialization.json.prettyPrint=true} na JVM para ativar.
*/ */
public static final String JSON_PRETTY_PRINT_PROPERTY = "sd.serialization.json.prettyPrint"; 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 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() { private SerializerFactory() {
throw new UnsupportedOperationException("Factory class cannot be instantiated"); throw new UnsupportedOperationException("Factory class cannot be instantiated");
} }
/** /**
* Creates a JSON serializer based on system configuration. * Cria um serializador JSON configurado dinamicamente pelo ambiente.
* * <p>
* Pretty-print is determined by checking the system property * Verifica a propriedade de sistema {@value #JSON_PRETTY_PRINT_PROPERTY}.
* {@value #JSON_PRETTY_PRINT_PROPERTY}. If not set, defaults to false. * Se não definida, assume o padrão de produção (falso/compacto).
* * * @return Uma instância configurada de {@link JsonMessageSerializer}.
* @return A configured JsonMessageSerializer instance
*/ */
public static MessageSerializer createDefault() { public static MessageSerializer createDefault() {
boolean prettyPrint = Boolean.getBoolean(JSON_PRETTY_PRINT_PROPERTY); boolean prettyPrint = Boolean.getBoolean(JSON_PRETTY_PRINT_PROPERTY);
@@ -46,19 +45,18 @@ public class SerializerFactory {
} }
/** /**
* Creates a JSON serializer with default configuration (no pretty printing). * 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 A JsonMessageSerializer instance * * @return Uma instância compacta de {@link JsonMessageSerializer}.
*/ */
public static MessageSerializer createSerializer() { public static MessageSerializer createSerializer() {
return createSerializer(DEFAULT_JSON_PRETTY_PRINT); return createSerializer(DEFAULT_JSON_PRETTY_PRINT);
} }
/** /**
* Creates a JSON serializer with specified pretty-print setting. * 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.
* @param prettyPrint Whether to enable pretty printing * @return Uma instância personalizada de {@link JsonMessageSerializer}.
* @return A JsonMessageSerializer instance
*/ */
public static MessageSerializer createSerializer(boolean prettyPrint) { public static MessageSerializer createSerializer(boolean prettyPrint) {
return new JsonMessageSerializer(prettyPrint); return new JsonMessageSerializer(prettyPrint);

View File

@@ -3,84 +3,88 @@ package sd.util;
import java.util.Random; import java.util.Random;
/** /**
* Utility class for generating random values used throughout the simulation. * Utilitário central de geração estocástica para a simulação.
* * Provides static methods for: * <p>
* - Generating exponentially distributed intervals (for Poisson processes). * Esta classe fornece primitivas para geração de números pseudo-aleatórios, abstraindo
* - Generating random integers and doubles in a range. * a complexidade de distribuições estatísticas.
* - Making decisions based on probability. * <p>
* - Choosing random elements from an array. * <b>Funcionalidades Principais:</b>
* * It uses a single, static {@link Random} instance. * <ul>
* <li><b>Modelagem de Poisson:</b> Geração de tempos entre chegadas usando distribuição exponencial inversa.</li>
* <li><b>Amostragem Uniforme:</b> Geração de inteiros e doubles em intervalos fechados/abertos.</li>
* <li><b>Decisão Probabilística:</b> Avaliação de eventos booleanos baseados em pesos (Bernoulli trials).</li>
* <li><b>Determinismo:</b> Suporte a sementes (seeds) manuais para reprodutibilidade exata de cenários de teste.</li>
* </ul>
*/ */
public class RandomGenerator { public class RandomGenerator {
/** /** * Instância singleton estática do gerador PRNG (Pseudo-Random Number Generator).
* The single, shared Random instance for the entire simulation. * Thread-safe (java.util.Random é sincronizado), embora possa haver contenção em alta concorrência.
*/ */
private static final Random random = new Random(); private static final Random random = new Random();
/** /**
* Returns a random time interval that follows an exponential distribution. * Gera um intervalo de tempo seguindo uma Distribuição Exponencial.
* * This is a key component for modeling a Poisson process, where the * <p>
* *inter-arrival times* (time between events) are exponentially distributed. * Este método implementa o algoritmo de <i>Inverse Transform Sampling</i> para simular
* The formula used is the inverse transform sampling method: * um Processo de Poisson homogêneo. É fundamental para modelar a chegada natural de
* {@code Time = -ln(1 - U) / λ} * veículos, onde eventos independentes ocorrem a uma taxa média constante.
* where U is a uniform random number [0, 1) and λ (lambda) is the * <p>
* average arrival rate. * <b>Fórmula Matemática:</b> {@code T = -ln(1 - U) / λ}
* <br>Onde:
* <ul>
* <li>{@code U}: Variável aleatória uniforme no intervalo [0, 1).</li>
* <li>{@code λ (lambda)}: Taxa média de eventos por unidade de tempo (ex: veículos/segundo).</li>
* </ul>
* *
* @param lambda The average arrival rate (λ) (e.g., 0.5 vehicles per second). * @param lambda A taxa média de chegada (λ > 0).
* @return The time interval (in seconds) until the next arrival. * @return O intervalo de tempo (delta t) até o próximo evento, em segundos.
*/ */
public static double generateExponentialInterval(double lambda) { 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; 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 min Limite inferior (inclusivo).
* @param max The maximum possible value. * @param max Limite superior (inclusivo).
* @return A random integer in the range [min, max]. * @return Um inteiro aleatório I tal que {@code min <= I <= max}.
*/ */
public static int generateRandomInt(int min, int 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; 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 min Limite inferior (inclusivo).
* @param max The maximum possible value. * @param max Limite superior (exclusivo).
* @return A random double in the range [min, max). * @return Um double aleatório D tal que {@code min <= D < max}.
*/ */
public static double generateRandomDouble(double min, double max) { public static double generateRandomDouble(double min, double max) {
return min + (max - min) * random.nextDouble(); return min + (max - min) * random.nextDouble();
} }
/** /**
* Returns {@code true} with a given probability. * Realiza um teste de Bernoulli (Sim/Não) com uma probabilidade de sucesso especificada.
* * This is useful for making weighted decisions. For example, * <p>
* {@code occursWithProbability(0.3)} will return {@code true} * Utilizado para decisões de ramificação estocástica (ex: "Este veículo é um camião?").
* approximately 30% of the time.
* *
* @param probability A value between 0.0 (never) and 1.0 (always). * @param probability A probabilidade de retorno {@code true} (0.0 a 1.0).
* @return {@code true} or {@code false}, based on the probability. * @return {@code true} se o evento ocorrer, {@code false} caso contrário.
*/ */
public static boolean occursWithProbability(double probability) { public static boolean occursWithProbability(double probability) {
return random.nextDouble() < 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 <T> The generic type of the array. * @param <T> O tipo dos elementos no array.
* @param array The array to choose from. * @param array A população de onde escolher.
* @return A randomly selected element from the array. * @return O elemento selecionado.
* @throws IllegalArgumentException if the array is null or empty. * @throws IllegalArgumentException Se o array for nulo ou vazio.
*/ */
public static <T> T chooseRandom(T[] array) { public static <T> T chooseRandom(T[] array) {
if (array == null || array.length == 0) { if (array == null || array.length == 0) {
@@ -90,12 +94,13 @@ public class RandomGenerator {
} }
/** /**
* Sets the seed of the shared random number generator. * Reinicializa a semente (seed) do gerador global.
* This is extremely useful for debugging and testing, as it allows * <p>
* the simulation to be run multiple times with the *exact same* * <b>Importância Crítica:</b> Permite tornar a simulação determinística. Ao fixar a seed,
* sequence of "random" events, making the results reproducible. * 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) { public static void setSeed(long seed) {
random.setSeed(seed); random.setSeed(seed);

View File

@@ -1,381 +0,0 @@
package sd.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import sd.config.SimulationConfig;
import sd.model.Intersection;
import sd.model.Vehicle;
import sd.model.VehicleType;
/**
* Collects, manages, and reports statistics throughout the simulation.
* * This class acts as the central bookkeeper for simulation metrics. It
* tracks:
* - Overall system statistics (total vehicles, completion time, wait time).
* - Per-vehicle-type statistics (counts, average wait time by type).
* - Per-intersection statistics (arrivals, departures).
* * It also maintains "in-flight" data, such as the arrival time of a
* vehicle at its *current* intersection, which is necessary to
* calculate waiting time when the vehicle later departs.
*/
public class StatisticsCollector {
// --- Vehicle tracking (for in-flight vehicles) ---
/**
* Tracks the simulation time when a vehicle arrives at its *current*
* intersection.
* This is used later to calculate waiting time (Depart_Time - Arrive_Time).
* Key: Vehicle ID (String)
* Value: Arrival Time (Double)
*/
private final Map<String, Double> vehicleArrivalTimes;
/**
* Tracks the sequence of intersections a vehicle has visited.
* Key: Vehicle ID (String)
* Value: List of Intersection IDs (String)
*/
private final Map<String, List<String>> vehicleIntersectionHistory;
// --- Overall system statistics ---
/** Total number of vehicles created by the {@link VehicleGenerator}. */
private int totalVehiclesGenerated;
/** Total number of vehicles that have reached their final destination ("S"). */
private int totalVehiclesCompleted;
/**
* The sum of all *completed* vehicles' total travel times. Used for averaging.
*/
private double totalSystemTime;
/**
* The sum of all *completed* vehicles' total waiting times. Used for averaging.
*/
private double totalWaitingTime;
// --- Per-vehicle-type statistics ---
/**
* Tracks the total number of vehicles generated, broken down by type.
* Key: {@link VehicleType}
* Value: Count (Integer)
*/
private final Map<VehicleType, Integer> vehicleTypeCount;
/**
* Tracks the total waiting time, broken down by vehicle type.
* Key: {@link VehicleType}
* Value: Total Wait Time (Double)
*/
private final Map<VehicleType, Double> vehicleTypeWaitTime;
// --- Per-intersection statistics ---
/**
* A map to hold statistics objects for each intersection.
* Key: Intersection ID (String)
* Value: {@link IntersectionStats} object
*/
private final Map<String, IntersectionStats> intersectionStats;
/**
* Constructs a new StatisticsCollector.
* Initializes all maps and counters.
*
* @param config The {@link SimulationConfig} (not currently used, but
* could be for configuration-dependent stats).
*/
public StatisticsCollector(SimulationConfig config) {
this.vehicleArrivalTimes = new HashMap<>();
this.vehicleIntersectionHistory = new HashMap<>();
this.totalVehiclesGenerated = 0;
this.totalVehiclesCompleted = 0;
this.totalSystemTime = 0.0;
this.totalWaitingTime = 0.0;
this.vehicleTypeCount = new HashMap<>();
this.vehicleTypeWaitTime = new HashMap<>();
this.intersectionStats = new HashMap<>();
// Initialize vehicle type counters to 0
for (VehicleType type : VehicleType.values()) {
vehicleTypeCount.put(type, 0);
vehicleTypeWaitTime.put(type, 0.0);
}
}
/**
* Records that a new vehicle has been generated.
* This is called by the vehicle generation component
* during a {@code VEHICLE_GENERATION} event.
*
* @param vehicle The {@link Vehicle} that was just created.
* @param currentTime The simulation time of the event.
*/
public void recordVehicleGeneration(Vehicle vehicle, double currentTime) {
totalVehiclesGenerated++;
// Track by vehicle type
VehicleType type = vehicle.getType();
vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
// Initialize history tracking for this vehicle
vehicleIntersectionHistory.put(vehicle.getId(), new ArrayList<>());
}
/**
* Records that a vehicle has arrived at an intersection queue.
* This is called by the vehicle generation component
* during a {@code VEHICLE_ARRIVAL} event.
*
* @param vehicle The {@link Vehicle} that arrived.
* @param intersectionId The ID of the intersection it arrived at.
* @param currentTime The simulation time of the arrival.
*/
public void recordVehicleArrival(Vehicle vehicle, String intersectionId, double currentTime) {
// Store arrival time - this is the "start waiting" time
vehicleArrivalTimes.put(vehicle.getId(), currentTime);
// Track intersection history
List<String> history = vehicleIntersectionHistory.get(vehicle.getId());
if (history != null) {
history.add(intersectionId);
}
// Update per-intersection statistics
getOrCreateIntersectionStats(intersectionId).recordArrival();
}
/**
* Records that a vehicle has completed its route and exited the system.
* This is where final metrics for the vehicle are aggregated.
* This is called by the vehicle generation component
* when a vehicle reaches destination "S".
*
* @param vehicle The {@link Vehicle} that is exiting.
* @param currentTime The simulation time of the exit.
*/
public void recordVehicleExit(Vehicle vehicle, double currentTime) {
totalVehiclesCompleted++;
// Calculate and aggregate total system time
double systemTime = vehicle.getTotalTravelTime(currentTime);
totalSystemTime += systemTime;
// Aggregate waiting time
double waitTime = vehicle.getTotalWaitingTime();
totalWaitingTime += waitTime;
// Aggregate waiting time by vehicle type
VehicleType type = vehicle.getType();
vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
// Clean up tracking maps to save memory
vehicleArrivalTimes.remove(vehicle.getId());
vehicleIntersectionHistory.remove(vehicle.getId());
}
/**
* Gets the time a vehicle arrived at its *current* intersection.
* This is used by the intersection component to calculate
* wait time just before the vehicle crosses.
*
* @param vehicle The {@link Vehicle} to check.
* @return The arrival time, or 0.0 if not found.
*/
public double getArrivalTime(Vehicle vehicle) {
return vehicleArrivalTimes.getOrDefault(vehicle.getId(), 0.0);
}
/**
* Prints a "snapshot" of the current simulation statistics.
* This is called periodically by the simulation components
* during a {@code STATISTICS_UPDATE} event.
*
* @param intersections A map of all intersections (to get queue data).
* @param currentTime The current simulation time.
*/
public void printCurrentStatistics(Map<String, Intersection> intersections, double currentTime) {
System.out.printf("--- Statistics at t=%.2f ---%n", currentTime);
System.out.printf("Vehicles: Generated=%d, Completed=%d, In-System=%d%n",
totalVehiclesGenerated,
totalVehiclesCompleted,
totalVehiclesGenerated - totalVehiclesCompleted);
if (totalVehiclesCompleted > 0) {
System.out.printf("Average System Time (so far): %.2fs%n", totalSystemTime / totalVehiclesCompleted);
System.out.printf("Average Waiting Time (so far): %.2fs%n", totalWaitingTime / totalVehiclesCompleted);
}
// Print per-intersection queue sizes
System.out.println("\nIntersection Queues:");
for (Map.Entry<String, Intersection> entry : intersections.entrySet()) {
String id = entry.getKey();
Intersection intersection = entry.getValue();
System.out.printf(" %s: Queue=%d, Received=%d, Sent=%d%n",
id,
intersection.getTotalQueueSize(),
intersection.getTotalVehiclesReceived(),
intersection.getTotalVehiclesSent());
}
}
/**
* Prints the final simulation summary statistics at the end of the run.
*
* @param intersections A map of all intersections.
* @param currentTime The final simulation time.
*/
public void printFinalStatistics(Map<String, Intersection> intersections, double currentTime) {
System.out.println("\n=== SIMULATION SUMMARY ===");
System.out.printf("Duration: %.2f seconds%n", currentTime);
System.out.printf("Total Vehicles Generated: %d%n", totalVehiclesGenerated);
System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesCompleted);
System.out.printf("Vehicles Still in System: %d%n", totalVehiclesGenerated - totalVehiclesCompleted);
// Overall averages
if (totalVehiclesCompleted > 0) {
System.out.printf("%nAVERAGE METRICS (for completed vehicles):%n");
System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesCompleted);
System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesCompleted);
System.out.printf(" Throughput: %.2f vehicles/second%n", totalVehiclesCompleted / currentTime);
}
// Vehicle type breakdown
System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
for (VehicleType type : VehicleType.values()) {
int count = vehicleTypeCount.get(type);
if (count > 0) {
double percentage = (count * 100.0) / totalVehiclesGenerated;
// Calculate avg wait *only* for this type
// This assumes all generated vehicles of this type *completed*
// A more accurate way would be to track completed vehicle types
double avgWait = vehicleTypeWaitTime.get(type) / count;
System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
type, count, percentage, avgWait);
}
}
// Per-intersection statistics
System.out.println("\nINTERSECTION STATISTICS:");
for (Map.Entry<String, Intersection> entry : intersections.entrySet()) {
String id = entry.getKey();
Intersection intersection = entry.getValue();
System.out.printf(" %s:%n", id);
System.out.printf(" Vehicles Received: %d%n", intersection.getTotalVehiclesReceived());
System.out.printf(" Vehicles Sent: %d%n", intersection.getTotalVehiclesSent());
System.out.printf(" Final Queue Size: %d%n", intersection.getTotalQueueSize());
// Traffic light details
intersection.getTrafficLights().forEach(light -> {
System.out.printf(" Light %s: State=%s, Queue=%d, Processed=%d%n",
light.getDirection(),
light.getState(),
light.getQueueSize(),
light.getTotalVehiclesProcessed());
});
}
// System health indicators
System.out.println("\nSYSTEM HEALTH:");
int totalQueuedVehicles = intersections.values().stream()
.mapToInt(Intersection::getTotalQueueSize)
.sum();
System.out.printf(" Total Queued Vehicles (at end): %d%n", totalQueuedVehicles);
if (totalVehiclesGenerated > 0) {
double completionRate = (totalVehiclesCompleted * 100.0) / totalVehiclesGenerated;
System.out.printf(" Completion Rate: %.1f%%%n", completionRate);
}
}
/**
* Gets or creates the statistics object for a given intersection.
* Uses {@code computeIfAbsent} for efficient, thread-safe-like instantiation.
*
* @param intersectionId The ID of the intersection.
* @return The {@link IntersectionStats} object for that ID.
*/
private IntersectionStats getOrCreateIntersectionStats(String intersectionId) {
// If 'intersectionId' is not in the map, create a new IntersectionStats()
// and put it in the map, then return it.
// Otherwise, just return the one that's already there.
return intersectionStats.computeIfAbsent(intersectionId, k -> new IntersectionStats());
}
/**
* Inner class to track per-intersection statistics.
* This is a simple data holder.
*/
private static class IntersectionStats {
private int totalArrivals;
private int totalDepartures;
public IntersectionStats() {
this.totalArrivals = 0;
this.totalDepartures = 0;
}
public void recordArrival() {
totalArrivals++;
}
public int getTotalArrivals() {
return totalArrivals;
}
public int getTotalDepartures() {
return totalDepartures;
}
}
// --- Public Getters for Final Statistics ---
/**
* @return Total vehicles generated during the simulation.
*/
public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated;
}
/**
* @return Total vehicles that completed their route.
*/
public int getTotalVehiclesCompleted() {
return totalVehiclesCompleted;
}
/**
* @return The sum of all travel times for *completed* vehicles.
*/
public double getTotalSystemTime() {
return totalSystemTime;
}
/**
* @return The sum of all waiting times for *completed* vehicles.
*/
public double getTotalWaitingTime() {
return totalWaitingTime;
}
/**
* @return The average travel time for *completed* vehicles.
*/
public double getAverageSystemTime() {
return totalVehiclesCompleted > 0 ? totalSystemTime / totalVehiclesCompleted : 0.0;
}
/**
* @return The average waiting time for *completed* vehicles.
*/
public double getAverageWaitingTime() {
return totalVehiclesCompleted > 0 ? totalWaitingTime / totalVehiclesCompleted : 0.0;
}
}

View File

@@ -1,147 +1,116 @@
package sd.util; package sd.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import sd.config.SimulationConfig; import sd.config.SimulationConfig;
import sd.model.Vehicle; import sd.model.Vehicle;
import sd.model.VehicleType; import sd.model.VehicleType;
import sd.routing.RouteSelector;
/** /**
* Generates vehicles for the simulation. * Motor de injeção de carga (Load Injector) para a simulação de tráfego.
* * This class is responsible for two key tasks: * <p>
* 1. Determining *when* the next vehicle should arrive, based on the * Esta classe atua como uma fábrica estocástica de veículos, sendo responsável por:
* arrival model (POISSON or FIXED) from the {@link SimulationConfig}. * <ol>
* 2. Creating a new {@link Vehicle} object with a randomly selected * <li><b>Modelagem Temporal:</b> Determinar os instantes de chegada (Inter-arrival times)
* type (e.g., BIKE, LIGHT) and a randomly selected route. * usando processos de Poisson (estocástico) ou intervalos determinísticos.</li>
* * Routes are predefined and organized by entry point (E1, E2, E3). * <li><b>Caracterização da Entidade:</b> Atribuir tipos de veículo (Bike, Light, Heavy)
* baseado numa Distribuição de Probabilidade Cumulativa (CDF).</li>
* <li><b>Inicialização Espacial:</b> Distribuir a carga uniformemente entre os pontos de entrada (E1-E3).</li>
* <li><b>Atribuição de Rota:</b> Delegar a escolha do percurso à estratégia {@link RouteSelector} ativa.</li>
* </ol>
*/ */
public class VehicleGenerator { public class VehicleGenerator {
private final SimulationConfig config; private final SimulationConfig config;
private final String arrivalModel; private final String arrivalModel;
private final double arrivalRate; // Lambda (λ) for POISSON
private final double fixedInterval; // Interval for FIXED
// --- Predefined Routes --- /** Parâmetro Lambda (λ) para a distribuição de Poisson (taxa de chegada). */
// These lists store all possible routes, grouped by where they start. private final double arrivalRate;
/** Routes starting from entry point E1. */ /** Intervalo determinístico para geração constante (modo debug/teste). */
private final List<RouteWithProbability> e1Routes; private final double fixedInterval;
/** Routes starting from entry point E2. */
private final List<RouteWithProbability> e2Routes; /** * Estratégia de roteamento atual.
/** Routes starting from entry point E3. */ * Não é final para permitir Hot-Swapping durante a execução.
private final List<RouteWithProbability> e3Routes; */
private RouteSelector routeSelector;
/** /**
* Constructs a new VehicleGenerator. * Inicializa o gerador com as configurações de simulação e estratégia de roteamento.
* It reads the necessary configuration and initializes the
* predefined routes.
* *
* @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.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.arrivalModel = config.getArrivalModel();
this.arrivalRate = config.getArrivalRate(); this.arrivalRate = config.getArrivalRate();
this.fixedInterval = config.getFixedArrivalInterval(); 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 * Calcula o timestamp absoluto para a próxima injeção de veículo.
* their entry point (E1, E2, E3). Each route is given a * <p>
* probability, which determines how often it's chosen. * Se o modelo for "POISSON", utiliza a técnica de <i>Inverse Transform Sampling</i>
*/ * (via {@link RandomGenerator}) para gerar intervalos exponencialmente distribuídos,
private void initializePossibleRoutes() { * simulando a aleatoriedade natural do tráfego.
// E1 routes (Starts at Cr1) * * @param currentTime O tempo atual da simulação (base de cálculo).
e1Routes.add(new RouteWithProbability( * @return O instante futuro (t + delta) para agendamento do evento de geração.
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.
*/ */
public double getNextArrivalTime(double currentTime) { public double getNextArrivalTime(double currentTime) {
if ("POISSON".equalsIgnoreCase(arrivalModel)) { if ("POISSON".equalsIgnoreCase(arrivalModel)) {
// For a Poisson process, the time *between* arrivals
// follows an exponential distribution.
double interval = RandomGenerator.generateExponentialInterval(arrivalRate); double interval = RandomGenerator.generateExponentialInterval(arrivalRate);
return currentTime + interval; return currentTime + interval;
} else { } else {
// For a Fixed model, the interval is constant.
return currentTime + fixedInterval; return currentTime + fixedInterval;
} }
} }
/** /**
* Generates a new {@link Vehicle} object. * Instancia (Spawn) um novo veículo configurado e roteado.
* This involves: * <p>
* 1. Selecting a random {@link VehicleType} based on probabilities. * O processo de criação segue um pipeline:
* 2. Selecting a random route (entry point + path) based on probabilities. * <ol>
* <li>Seleção de Tipo (Roda da Fortuna / CDF).</li>
* <li>Seleção de Entrada (Uniforme).</li>
* <li>Cálculo de Rota (Delegado ao Strategy).</li>
* </ol>
* *
* @param vehicleId The unique identifier for the new vehicle (e.g., "V123"). * @param vehicleId O identificador único sequencial (ex: "V104").
* @param entryTime The simulation time when this vehicle is being created. * @param entryTime O timestamp de criação.
* @return A new, configured {@link Vehicle} object. * @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<String, Integer> queueSizes) {
VehicleType type = selectVehicleType(); VehicleType type = selectVehicleType();
List<String> route = selectRandomRoute(); String entryPoint = selectRandomEntryPoint();
List<String> route = routeSelector.selectRoute(entryPoint, queueSizes);
return new Vehicle(vehicleId, type, entryTime, route); return new Vehicle(vehicleId, type, entryTime, route);
} }
/** /**
* Selects a {@link VehicleType} (BIKE, LIGHT, HEAVY) based on the * Seleciona o tipo de veículo usando Amostragem por Probabilidade Cumulativa.
* probabilities defined in the {@link SimulationConfig}. * <p>
* * Uses a standard "cumulative probability" technique: * Normaliza as probabilidades configuradas e mapeia um número aleatório [0, 1)
* 1. Get a random number {@code rand} from [0, 1). * para o intervalo correspondente ao tipo de veículo.
* 2. If {@code rand < P(Bike)}, return BIKE.
* 3. Else if {@code rand < P(Bike) + P(Light)}, return LIGHT.
* 4. Else, return HEAVY.
* *
* @return The selected {@link VehicleType}. * @return O tipo enumerado {@link VehicleType} selecionado.
*/ */
private VehicleType selectVehicleType() { private VehicleType selectVehicleType() {
double bikeProbability = config.getBikeVehicleProbability(); double bikeProbability = config.getBikeVehicleProbability();
double lightProbability = config.getLightVehicleProbability(); double lightProbability = config.getLightVehicleProbability();
double heavyProbability = config.getHeavyVehicleProbability(); double heavyProbability = config.getHeavyVehicleProbability();
// Normalize probabilities in case they don't sum to exactly 1.0
double total = bikeProbability + lightProbability + heavyProbability; 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; bikeProbability /= total;
lightProbability /= total; lightProbability /= total;
@@ -157,73 +126,42 @@ public class VehicleGenerator {
} }
/** /**
* Selects a random route for a new vehicle. * Seleciona um ponto de injeção na borda da rede (Edge Node).
* This is a two-step process: * Distribuição Uniforme: ~33.3% para cada entrada (E1, E2, E3).
* 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).
* *
* @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<String> selectRandomRoute() { private String selectRandomEntryPoint() {
// Step 1: Randomly select an entry point (E1, E2, or E3) double rand = Math.random();
double entryRandom = Math.random();
List<RouteWithProbability> selectedRoutes;
if (entryRandom < 0.333) { if (rand < 0.333) {
selectedRoutes = e1Routes; return "E1";
} else if (entryRandom < 0.666) { } else if (rand < 0.666) {
selectedRoutes = e2Routes; return "E2";
} else { } 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).
* <p>
* 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() { public String getInfo() {
int totalRoutes = e1Routes.size() + e2Routes.size() + e3Routes.size();
return String.format( return String.format(
"VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routes=%d (E1:%d, E2:%d, E3:%d)}", "VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routeSelector=%s}",
arrivalModel, arrivalRate, fixedInterval, totalRoutes, arrivalModel, arrivalRate, fixedInterval, routeSelector.getClass().getSimpleName()
e1Routes.size(), e2Routes.size(), e3Routes.size()
); );
} }
/**
* 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<String> 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<String> route, double probability) {
this.route = route;
this.probability = probability;
}
}
} }

View File

@@ -0,0 +1,142 @@
/* Global Styles */
.root {
-fx-background-color: #f4f7f6;
-fx-font-family: 'Segoe UI', sans-serif;
}
/* Header */
.header {
-fx-background-color: linear-gradient(to right, #2c3e50, #4ca1af);
-fx-padding: 20;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 10, 0, 0, 5);
}
.header-title {
-fx-font-size: 28px;
-fx-font-weight: bold;
-fx-text-fill: white;
}
.header-subtitle {
-fx-font-size: 16px;
-fx-text-fill: #ecf0f1;
}
/* Buttons */
.button-start {
-fx-background-color: #2ecc71;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-padding: 10 20;
-fx-background-radius: 5;
-fx-cursor: hand;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 5, 0, 0, 2);
}
.button-start:hover {
-fx-background-color: #27ae60;
}
.button-start:disabled {
-fx-background-color: #95a5a6;
-fx-opacity: 0.7;
}
.button-stop {
-fx-background-color: #e74c3c;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-padding: 10 20;
-fx-background-radius: 5;
-fx-cursor: hand;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 5, 0, 0, 2);
}
.button-stop:hover {
-fx-background-color: #c0392b;
}
.button-stop:disabled {
-fx-background-color: #95a5a6;
-fx-opacity: 0.7;
}
/* Cards / Panels */
.card {
-fx-background-color: white;
-fx-background-radius: 8;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.05), 10, 0, 0, 2);
-fx-padding: 0;
}
.card-header {
-fx-background-color: #ecf0f1;
-fx-background-radius: 8 8 0 0;
-fx-padding: 10 15;
-fx-border-color: #bdc3c7;
-fx-border-width: 0 0 1 0;
}
.card-title {
-fx-font-size: 16px;
-fx-font-weight: bold;
-fx-text-fill: #2c3e50;
}
.card-content {
-fx-padding: 15;
}
/* Statistics Grid */
.stat-label {
-fx-font-size: 14px;
-fx-text-fill: #7f8c8d;
}
.stat-value {
-fx-font-size: 20px;
-fx-font-weight: bold;
-fx-text-fill: #2980b9;
}
/* Tables */
.table-view {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.table-view .column-header-background {
-fx-background-color: #ecf0f1;
-fx-border-color: #bdc3c7;
-fx-border-width: 0 0 1 0;
}
.table-view .column-header .label {
-fx-text-fill: #2c3e50;
-fx-font-weight: bold;
}
.table-row-cell {
-fx-background-color: white;
-fx-border-color: transparent;
}
.table-row-cell:odd {
-fx-background-color: #f9f9f9;
}
.table-row-cell:selected {
-fx-background-color: #3498db;
-fx-text-fill: white;
}
/* Footer */
.footer {
-fx-background-color: #34495e;
-fx-padding: 10 20;
}
.footer-text {
-fx-text-fill: #ecf0f1;
-fx-font-size: 12px;
}

View File

@@ -27,15 +27,18 @@
}, },
{ {
"id": "Cr4", "id": "Cr4",
"lights": ["East"], "lights": ["East", "North"],
"routes": { "routes": {
"Cr1": "North",
"Cr5": "East" "Cr5": "East"
} }
}, },
{ {
"id": "Cr5", "id": "Cr5",
"lights": ["East"], "lights": ["East", "West", "North"],
"routes": { "routes": {
"Cr2": "North",
"Cr4": "West",
"S": "East" "S": "East"
} }
} }

View File

@@ -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.<intersection>.<direction>.<state>=<seconds>
# 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

View File

@@ -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.<intersection>.<direction>.<state>=<seconds>
# 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

View File

@@ -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.<intersection>.<direction>.<state>=<seconds>
# 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)

View File

@@ -31,7 +31,11 @@ dashboard.port=9000
# === SIMULATION CONFIGURATION === # === SIMULATION CONFIGURATION ===
# Total duration in seconds (3600 = 1 hour) # 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 # Vehicle arrival model: FIXED or POISSON
simulation.arrival.model=POISSON simulation.arrival.model=POISSON
@@ -42,49 +46,44 @@ simulation.arrival.rate=0.5
# Fixed interval between arrivals (only used if model=FIXED) # Fixed interval between arrivals (only used if model=FIXED)
simulation.arrival.fixed.interval=2.0 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 === # === TRAFFIC LIGHT TIMINGS ===
# Format: trafficlight.<intersection>.<direction>.<state>=<seconds> # Format: trafficlight.<intersection>.<direction>.<state>=<seconds>
# Intersection 1 (Entry point - balanced) # Intersection 1 (Entry point - balanced)
trafficlight.Cr1.South.green=20.0 trafficlight.Cr1.South.green=60.0
trafficlight.Cr1.South.red=40.0 trafficlight.Cr1.South.red=5.0
trafficlight.Cr1.East.green=20.0 trafficlight.Cr1.East.green=60.0
trafficlight.Cr1.East.red=40.0 trafficlight.Cr1.East.red=5.0
trafficlight.Cr1.West.green=20.0
trafficlight.Cr1.West.red=40.0
# Intersection 2 (Main hub - shorter cycles, favor East-West) # Intersection 2 (Main hub - shorter cycles, favor East-West)
trafficlight.Cr2.South.green=12.0 trafficlight.Cr2.South.green=60.0
trafficlight.Cr2.South.red=36.0 trafficlight.Cr2.South.red=5.0
trafficlight.Cr2.East.green=18.0 trafficlight.Cr2.East.green=60.0
trafficlight.Cr2.East.red=30.0 trafficlight.Cr2.East.red=5.0
trafficlight.Cr2.West.green=18.0 trafficlight.Cr2.West.green=60.0
trafficlight.Cr2.West.red=30.0 trafficlight.Cr2.West.red=5.0
# Intersection 3 (Path to exit - favor East) # Intersection 3 (Path to exit - favor East)
trafficlight.Cr3.South.green=15.0 trafficlight.Cr3.South.green=60.0
trafficlight.Cr3.South.red=30.0 trafficlight.Cr3.South.red=5.0
trafficlight.Cr3.East.green=20.0 trafficlight.Cr3.West.green=60.0
trafficlight.Cr3.East.red=25.0 trafficlight.Cr3.West.red=5.0
trafficlight.Cr3.West.green=15.0
trafficlight.Cr3.West.red=30.0
# Intersection 4 (Favor East toward Cr5) # Intersection 4 (Favor East toward Cr5)
trafficlight.Cr4.South.green=15.0 trafficlight.Cr4.East.green=60.0
trafficlight.Cr4.South.red=30.0 trafficlight.Cr4.East.red=5.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
# Intersection 5 (Near exit - favor East) # Intersection 5 (Near exit - favor East)
trafficlight.Cr5.South.green=15.0 trafficlight.Cr5.East.green=60.0
trafficlight.Cr5.South.red=30.0 trafficlight.Cr5.East.red=5.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
# === VEHICLE CONFIGURATION === # === VEHICLE CONFIGURATION ===
# Probability distribution for vehicle types (must sum to 1.0) # 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) # Travel times between intersections (in seconds)
# Base time for light vehicles (cars) # Base time for light vehicles (cars)
vehicle.travel.time.base=8.0 vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time # Bike travel time = 0.5 x car travel time
vehicle.travel.time.bike.multiplier=0.5 vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4 × bike travel time # Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=2.0 vehicle.travel.time.heavy.multiplier=4.0
# === STATISTICS === # === STATISTICS ===
# Interval between dashboard updates (seconds) # Interval between dashboard updates (seconds)
statistics.update.interval=1.0 statistics.update.interval=0.1

View File

@@ -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<String> 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<String> 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;
}
}
}

View File

@@ -1,111 +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.Event;
import sd.model.EventType;
import sd.model.Intersection;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.util.StatisticsCollector;
import sd.util.VehicleGenerator;
/**
* Basic tests for the simulation components.
*/
class SimulationTest {
@Test
void testConfigurationLoading() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
assertEquals(60.0, config.getSimulationDuration());
assertEquals("POISSON", config.getArrivalModel());
assertEquals(0.5, config.getArrivalRate());
assertEquals(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 testEventOrdering() {
Event e1 = new Event(5.0, EventType.VEHICLE_ARRIVAL, null, "Cr1");
Event e2 = new Event(3.0, EventType.VEHICLE_ARRIVAL, null, "Cr2");
Event e3 = new Event(7.0, EventType.TRAFFIC_LIGHT_CHANGE, null, "Cr1");
assertTrue(e2.compareTo(e1) < 0); // e2 should come before e1
assertTrue(e1.compareTo(e3) < 0); // e1 should come before e3
}
@Test
void testIntersectionVehicleQueue() {
Intersection intersection = new Intersection("TestCr");
TrafficLight light = new TrafficLight("TestCr-N", "North", 30.0, 30.0);
intersection.addTrafficLight(light);
Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
java.util.Arrays.asList("TestCr", "S"));
intersection.configureRoute("S", "North");
// Advance route to next destination
v1.advanceRoute();
intersection.receiveVehicle(v1);
assertEquals(1, intersection.getTotalQueueSize());
assertEquals(1, intersection.getTotalVehiclesReceived());
}
@Test
void testTrafficLightStateChange() {
TrafficLight light = new TrafficLight("Test-Light", "North", 30.0, 30.0);
assertEquals(TrafficLightState.RED, light.getState());
light.changeState(TrafficLightState.GREEN);
assertEquals(TrafficLightState.GREEN, light.getState());
light.changeState(TrafficLightState.RED);
assertEquals(TrafficLightState.RED, light.getState());
}
// Removed testSimulationEngineInitialization as SimulationEngine has been
// removed.
@Test
void testStatisticsCollector() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
StatisticsCollector collector = new StatisticsCollector(config);
Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
java.util.Arrays.asList("Cr1", "Cr2", "S"));
collector.recordVehicleGeneration(v1, 0.0);
assertEquals(1, collector.getTotalVehiclesGenerated());
collector.recordVehicleArrival(v1, "Cr1", 1.0);
collector.recordVehicleExit(v1, 10.0);
assertEquals(1, collector.getTotalVehiclesCompleted());
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,206 +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<String> violations = new ArrayList<>();
// Monitor for 10 seconds
long endTime = System.currentTimeMillis() + 10000;
while (System.currentTimeMillis() < endTime) {
int greenCount = 0;
StringBuilder currentState = new StringBuilder("States: ");
for (TrafficLight light : intersectionProcess.getIntersection().getTrafficLights()) {
TrafficLightState state = light.getState();
currentState.append(light.getDirection()).append("=").append(state).append(" ");
if (state == TrafficLightState.GREEN) {
greenCount++;
}
}
// Update maximum simultaneous green lights
if (greenCount > maxGreenSimultaneously.get()) {
maxGreenSimultaneously.set(greenCount);
}
// Check for violations (more than one green)
if (greenCount > 1) {
violationCount.incrementAndGet();
String violation = String.format("[VIOLATION] %d lights GREEN simultaneously: %s",
greenCount, currentState.toString());
violations.add(violation);
System.err.println(violation);
}
Thread.sleep(50); // Check every 50ms
}
System.out.println("\n=== Test Results ===");
System.out.println("Maximum simultaneous GREEN lights: " + maxGreenSimultaneously.get());
System.out.println("Total violations detected: " + violationCount.get());
if (!violations.isEmpty()) {
System.err.println("\nViolation details:");
violations.forEach(System.err::println);
}
// Assert that we never had more than one green light
assertEquals(0, violationCount.get(),
"Traffic light coordination violated! Multiple lights were GREEN simultaneously.");
assertTrue(maxGreenSimultaneously.get() <= 1,
"At most ONE light should be GREEN at any time. Found: " + maxGreenSimultaneously.get());
System.out.println("\nTraffic light coordination working correctly!");
}
/**
* Test that verifies all traffic lights get a chance to be GREEN.
* Ensures fairness in the coordination mechanism.
*/
@Test
public void testAllLightsGetGreenTime() throws InterruptedException {
System.out.println("\n=== Testing Traffic Light Fairness ===");
// Start the intersection
Thread intersectionThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
e.printStackTrace();
}
});
intersectionThread.start();
// Track which lights have been green
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
boolean[] hasBeenGreen = new boolean[lights.size()];
// Monitor for 15 seconds (enough time for all lights to cycle)
long endTime = System.currentTimeMillis() + 15000;
while (System.currentTimeMillis() < endTime) {
for (int i = 0; i < lights.size(); i++) {
if (lights.get(i).getState() == TrafficLightState.GREEN) {
hasBeenGreen[i] = true;
System.out.println("" + lights.get(i).getDirection() + " has been GREEN");
}
}
Thread.sleep(100);
}
// Check if all lights got green time
int greenCount = 0;
System.out.println("\n=== Fairness Results ===");
for (int i = 0; i < lights.size(); i++) {
String status = hasBeenGreen[i] ? "✓ YES" : "✗ NO";
System.out.println(lights.get(i).getDirection() + " got GREEN time: " + status);
if (hasBeenGreen[i]) greenCount++;
}
assertTrue(greenCount > 0, "At least one light should have been GREEN during the test");
System.out.println("\n" + greenCount + "/" + lights.size() + " lights were GREEN during test period");
}
/**
* Test that verifies the state transitions are consistent.
*/
@Test
public void testStateTransitionsAreConsistent() throws InterruptedException {
System.out.println("\n=== Testing State Transition Consistency ===");
Thread intersectionThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
e.printStackTrace();
}
});
intersectionThread.start();
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
TrafficLightState[] previousStates = new TrafficLightState[lights.size()];
// Initialize previous states
for (int i = 0; i < lights.size(); i++) {
previousStates[i] = lights.get(i).getState();
}
int transitionCount = 0;
long endTime = System.currentTimeMillis() + 8000;
while (System.currentTimeMillis() < endTime) {
for (int i = 0; i < lights.size(); i++) {
TrafficLightState currentState = lights.get(i).getState();
if (currentState != previousStates[i]) {
transitionCount++;
System.out.println(lights.get(i).getDirection() + " transitioned: " +
previousStates[i] + "" + currentState);
previousStates[i] = currentState;
}
}
Thread.sleep(100);
}
System.out.println("\nTotal state transitions observed: " + transitionCount);
assertTrue(transitionCount > 0, "There should be state transitions during the test period");
}
}

View File

@@ -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 were 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<MockIntersectionServer> 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<String> 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<SocketClient> 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<Message> 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<Message> getReceivedMessages() {
return receivedMessages;
}
public String getIntersectionId() {
return intersectionId;
}
}
}

View File

@@ -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 were 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 doesnt go backwards and intervals look reasonable.
*/
@Test
void testVehicleGenerationTiming() {
VehicleGenerator generator = new VehicleGenerator(config);
double currentTime = 0.0;
List<Double> 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<Vehicle> 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"
);
}
}

View File

@@ -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");
}
}

View File

@@ -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());
}
}

View File

@@ -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 ""

1055
main/testing.txt Normal file

File diff suppressed because it is too large Load Diff