24 Commits

Author SHA1 Message Date
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
d74517a27b starting the codebase cleanup for final delivery- single process prototype removal 2025-11-22 22:52:01 +00:00
ce7f642246 slight sim change and engine code fomat 2025-11-22 21:45:16 +00:00
8f97aab836 Merge pull request #34 from davidalves04/dev
testing
2025-11-22 21:43:33 +00:00
David Alves
86c0c4b5b3 Add configurable travel times by vehicle type
@0x1eo can u check this pls
2025-11-22 16:18:02 +00:00
6fdcf376b2 i might kms 2025-11-22 00:13:19 +00:00
David Alves
ecb70fa6a2 Merge pull request #33 from davidalves04/17-create-dashboardserver-process
Dashboard Server Implementation
2025-11-19 19:16:50 +00:00
06f079ce5b fix intersections starting independently with no coordination 2025-11-18 14:29:11 +00:00
72893f87ae added dashboard server and built an example implementation for the message protocol 2025-11-14 02:01:51 +00:00
6b94d727e2 shutdown and teardown fixes + incoming connection handler 2025-11-11 17:28:44 +00:00
84cba39597 bullshit fixes 2025-11-06 20:31:59 +00:00
81 changed files with 9160 additions and 5607 deletions

View File

@@ -1,8 +1,9 @@
name: Java CI with Maven
on:
workflow_dispatch:
push:
branches: [ "main" ]
branches: [ "dev", "cleanup" ]
tags:
- 'v*.*.*'
pull_request:
@@ -11,51 +12,93 @@ on:
jobs:
build:
runs-on: ubuntu-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
run: mvn -B package
working-directory: main
- name: Upload built JAR
uses: actions/upload-artifact@v4
with:
name: package
path: main/target/*.jar
- name: Generate dependency graph
run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph
- name: Upload dependency graph artifact
uses: actions/upload-artifact@v4
with:
name: dependency-graph
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:
runs-on: ubuntu-latest
needs: [build]
if: startsWith(github.ref, 'refs/tags/')
needs: [build, build-windows]
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
permissions:
contents: write
steps:
- name: Download built JAR
- name: Download Linux JAR
uses: actions/download-artifact@v4
with:
name: package
path: main/target/
- name: Download Windows Zip
uses: actions/download-artifact@v4
with:
name: windows-package
path: windows-dist/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
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

9
.gitignore vendored
View File

@@ -3,6 +3,9 @@
# Log files
*.log
*.trace
logs
*.md
# BlueJ files
*.ctxt
@@ -48,3 +51,9 @@ build/
# Other
*.swp
*.pdf
# JAR built pom file
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

@@ -29,6 +29,18 @@
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- JavaFX for UI -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>17.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>17.0.2</version>
</dependency>
</dependencies>
<build>
@@ -39,7 +51,16 @@
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>sd.Entry</mainClass>
<mainClass>sd.dashboard.Launcher</mainClass>
</configuration>
</plugin>
<!-- JavaFX Maven Plugin -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>sd.dashboard.Launcher</mainClass>
</configuration>
</plugin>
<plugin>
@@ -55,7 +76,7 @@
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>sd.Entry</mainClass>
<mainClass>sd.dashboard.Launcher</mainClass>
</transformer>
</transformers>
</configuration>

View File

@@ -1,94 +0,0 @@
package sd;
import java.io.IOException;
import sd.config.SimulationConfig;
import sd.engine.SimulationEngine;
/**
* Main entry point for the traffic simulation.
* * This class is responsible for loading the simulation configuration,
* initializing the {@link SimulationEngine}, and starting the simulation run.
* It also prints initial configuration details and final execution time.
*/
public class Entry {
/**
* The default path to the simulation configuration file.
* This is used if no command-line arguments are provided.
*/
private static final String DEFAULT_CONFIG_FILE = "src/main/resources/simulation.properties";
/**
* The main method to start the simulation.
* * @param args Command-line arguments. If provided, args[0] is expected
* to be the path to a custom configuration file.
*/
public static void main(String[] args) {
System.out.println("=".repeat(60));
System.out.println("TRAFFIC SIMULATION - DISCRETE EVENT SIMULATOR");
System.out.println("=".repeat(60));
try {
// 1. Load configuration
String configFile = args.length > 0 ? args[0] : DEFAULT_CONFIG_FILE;
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
// 2. Display configuration
displayConfiguration(config);
// 3. Create and initialize simulation engine
SimulationEngine engine = new SimulationEngine(config);
engine.initialize();
System.out.println("\n" + "=".repeat(60));
// 4. Run simulation
long startTime = System.currentTimeMillis();
engine.run();
long endTime = System.currentTimeMillis();
// 5. Display execution time
double executionTime = (endTime - startTime) / 1000.0;
System.out.println("\nExecution time: " + String.format("%.2f", executionTime) + " seconds");
System.out.println("=".repeat(60));
} catch (IOException e) {
System.err.println("Error loading configuration: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("Error during simulation: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Displays the main configuration parameters to the console.
* This provides a summary of the simulation settings before it starts.
*
* @param config The {@link SimulationConfig} object containing the loaded settings.
*/
private static void displayConfiguration(SimulationConfig config) {
System.out.println("\nSIMULATION CONFIGURATION:");
System.out.println(" Duration: " + config.getSimulationDuration() + " seconds");
System.out.println(" Arrival Model: " + config.getArrivalModel());
if ("POISSON".equalsIgnoreCase(config.getArrivalModel())) {
System.out.println(" Arrival Rate (λ): " + config.getArrivalRate() + " vehicles/second");
} else {
System.out.println(" Fixed Interval: " + config.getFixedArrivalInterval() + " seconds");
}
System.out.println(" Statistics Update Interval: " + config.getStatisticsUpdateInterval() + " seconds");
System.out.println("\nVEHICLE TYPES:");
System.out.println(" Bike: " + (config.getBikeVehicleProbability() * 100) + "% " +
"(crossing time: " + config.getBikeVehicleCrossingTime() + "s)");
System.out.println(" Light: " + (config.getLightVehicleProbability() * 100) + "% " +
"(crossing time: " + config.getLightVehicleCrossingTime() + "s)");
System.out.println(" Heavy: " + (config.getHeavyVehicleProbability() * 100) + "% " +
"(crossing time: " + config.getHeavyVehicleCrossingTime() + "s)");
}
}

View File

@@ -11,160 +11,311 @@ import java.util.concurrent.TimeUnit;
import sd.config.SimulationConfig;
import sd.coordinator.SocketClient;
import sd.dashboard.StatsUpdatePayload;
import sd.des.DESEventType;
import sd.des.EventQueue;
import sd.des.SimulationClock;
import sd.des.SimulationEvent;
import sd.logging.EventLogger;
import sd.logging.EventType;
import sd.logging.VehicleTracer;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection;
import sd.serialization.SerializationException;
/**
* Processo responsável pelo nó de saída do sistema de simulação de tráfego distribuído.
* Destino final de todos os veículos da simulação (nó de saída S).
*
* Este processo representa o ponto final ("S") onde os veículos completam as suas rotas.
* As suas principais responsabilidades são:
* - Receber veículos que terminam a sua rota vindos das interseções
* - Calcular e agregar estatísticas finais dos veículos
* - Enviar estatísticas periódicas para o dashboard
* - Gerar relatórios finais ao terminar a simulação
* <p>Opera como sumidouro da rede:
* <ol>
* <li>Recebe veículos que completaram a viagem
* <li>Regista estatísticas finais (tempo total, espera, travessia)
* <li>Envia métricas ao dashboard em tempo real
* </ol>
*
* <p>Participa no DES rastreando eventos, mas opera principalmente
* de forma reativa, aguardando chegadas via socket.
*/
public class ExitNodeProcess {
private final SimulationConfig config;
private ServerSocket serverSocket;
private final ExecutorService connectionHandlerPool;
/** Flag para controlar a execução do processo (volatile para visibilidade entre threads) */
// DES components
private final SimulationClock clock;
private final EventQueue eventQueue;
private final EventLogger eventLogger;
private Thread eventProcessorThread;
/** Flag de controlo (volatile para visibilidade entre threads) */
private volatile boolean running;
/** Counter de veículos que completaram a rota */
/** Instante de início da simulação (milissegundos) */
private long simulationStartMillis;
/** Contador de veículos que completaram a rota */
private int totalVehiclesReceived;
/** Soma dos tempos no sistema de todos os veículos */
/** Tempo acumulado no sistema de todos os veículos */
private double totalSystemTime;
/** Soma dos tempos de espera de todos os veículos */
/** Tempo acumulado em espera de todos os veículos */
private double totalWaitingTime;
/** Soma dos tempos de travessia de todos os veículos */
/** Tempo acumulado em travessia de todos os veículos */
private double totalCrossingTime;
/** Contagem de veículos por tipo */
private final Map<VehicleType, Integer> vehicleTypeCount;
/** Tempo total de espera acumulado por tipo de veículo */
/** Tempo de espera acumulado por tipo de veículo */
private final Map<VehicleType, Double> vehicleTypeWaitTime;
/** Socket para comunicação com o dashboard */
/** Cliente socket para envio de estatísticas ao dashboard */
private SocketClient dashboardClient;
/**
* Método para iniciar o processo
* Ponto de entrada do processo.
*
* @param args Argumentos da linha de comandos. Se fornecido, args[0] deve ser
* o caminho para um ficheiro de configuração personalizado.
* @param args args[0] (opcional) = caminho do ficheiro de configuração
*/
public static void main(String[] args) {
System.out.println("=".repeat(60));
System.out.println("EXIT NODE PROCESS");
System.out.println("=".repeat(60));
try {
EventLogger.getInstance().log(EventType.PROCESS_STARTED, "ExitNode", "Exit node process started");
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
ExitNodeProcess exitNode = new ExitNodeProcess(config);
System.out.println("\n" + "=".repeat(60));
exitNode.initialize();
System.out.println("\n" + "=".repeat(60));
exitNode.start();
} catch (IOException e) {
System.err.println("Failed to start exit node: " + e.getMessage());
EventLogger.getInstance().logError("ExitNode", "Failed to start", e);
System.exit(1);
} catch (Exception e) {
System.err.println("Exit node error: " + e.getMessage());
EventLogger.getInstance().logError("ExitNode", "Exit node error", e);
System.exit(1);
} finally {
EventLogger.getInstance().log(EventType.PROCESS_STOPPED, "ExitNode", "Exit node process stopped");
}
}
/**
* Constrói um novo processo de nó de saída.
* Configura o Nó de Saída.
*
* Inicializa todas as estruturas de dados necessárias para recolher estatísticas
* e configura o pool de threads para processar as ligações concorrentes.
* Inicializamos os nossos contadores, preparamos a pool de threads para tratar
* das ligações de veículos recebidas,
* e configuramos os componentes DES para rastreio de eventos.
*
* @param config Configuração da simulação contendo portas e endereços dos serviços
* @param config A configuração da simulação.
*/
public ExitNodeProcess(SimulationConfig config) {
this.config = config;
this.connectionHandlerPool = Executors.newCachedThreadPool();
this.running = false;
this.totalVehiclesReceived = 0;
this.totalSystemTime = 0.0;
this.totalWaitingTime = 0.0;
this.totalCrossingTime = 0.0;
this.vehicleTypeCount = new HashMap<>();
this.vehicleTypeWaitTime = new HashMap<>();
// Inicializa os counters para cada tipo de veículo
for (VehicleType type : VehicleType.values()) {
vehicleTypeCount.put(type, 0);
vehicleTypeWaitTime.put(type, 0.0);
}
System.out.println("Exit node initialized");
// Initialize DES components
this.clock = new SimulationClock();
this.eventQueue = new EventQueue(true); // Track history
this.eventLogger = EventLogger.getInstance();
eventLogger.log(EventType.PROCESS_STARTED, "ExitNode",
"Exit node initialized with DES architecture");
System.out.println("Exit node initialized (DES Mode)");
System.out.println(" - Exit port: " + config.getExitPort());
System.out.println(" - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
}
/**
* Inicializa o processo de ligação ao dashboard.
*
* Tenta conectar-se ao dashboard. Se a ligação falhar, o processo
* continua a funcionar normalmente, mas sem enviar estatísticas.
*
* Tenta estabelecer uma ligação ao dashboard.
* Se for bem-sucedido, poderemos enviar estatísticas em tempo real. Se não,
* apenas registamos localmente.
*/
public void initialize() {
System.out.println("Connecting to dashboard...");
try {
String host = config.getDashboardHost();
int port = config.getDashboardPort();
dashboardClient = new SocketClient("Dashboard", host, port);
dashboardClient.connect();
System.out.println("Successfully connected to dashboard");
} catch (IOException e) {
System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
System.err.println("Exit node will continue without dashboard connection");
}
}
/**
* Inicia o socket e começa a aceitar ligações.
* Starts the DES event processing thread.
* Currently, ExitNode is primarily reactive (receives vehicles via network),
* but maintains event queue for potential scheduled events and history
* tracking.
*/
private void startEventProcessor() {
eventProcessorThread = new Thread(() -> {
eventLogger.log(EventType.SIMULATION_STARTED, "ExitNode",
"Event processor thread started");
// Keep running while process is active
while (running) {
SimulationEvent event = eventQueue.poll();
if (event == null) {
// No events currently, wait before checking again
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
continue;
}
// Advance clock to event time
clock.advanceTo(event.getTimestamp());
// Process the event
processEvent(event);
}
eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode",
String.format("Event processor thread terminated at time %.2f", clock.getCurrentTime()));
}, "EventProcessor-ExitNode");
eventProcessorThread.start();
}
/**
* Processes a discrete event based on its type.
* Currently supports VEHICLE_EXIT and SIMULATION_END events.
*/
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();
}
}
/**
* Handles simulation end event.
*/
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();
}
/**
* Exports the complete event history for the exit node.
* This satisfies the spec requirement: "Deve ser possível verificar a lista
* completa de eventos"
*/
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());
}
}
/**
* Schedules a simulation end event at the specified time.
*
* Este é o loop principal do processo que:
* 1. Cria um socket na porta definida
* 2. Aguarda pelas ligações das interseções
* 3. Delega cada ligação a uma thread da pool para processamento assíncrono
* @param endTime The simulation time when the simulation should end
*/
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);
}
/**
* Abre o socket do servidor e começa a escutar por veículos.
*
* @throws IOException Se o socket não puder ser criado ou houver erro na aceitação
* Este é o loop principal. Aceitamos ligações das interseções (de onde vêm os
* veículos)
* e passamo-las para a nossa pool de threads para processamento.
*
* @throws IOException Se não conseguirmos fazer bind à porta.
*/
public void start() throws IOException {
start(true); // Default to DES mode
}
/**
* Starts the exit node process.
*
* @param useDES If true, starts event processor for DES mode tracking
*/
public void start(boolean useDES) throws IOException {
int port = config.getExitPort();
serverSocket = new ServerSocket(port);
running = true;
simulationStartMillis = System.currentTimeMillis();
System.out.println("Exit node started on port " + port);
System.out.println("Waiting for vehicles...\n");
if (useDES) {
// Note: ExitNode is primarily reactive (network-driven), but maintains
// event queue for simulation end events and history tracking
System.out.println("Running in DES mode (event history tracking enabled)");
}
System.out.println("Waiting for vehicles...\\n");
while (running) {
try {
Socket clientSocket = serverSocket.accept();
@@ -176,150 +327,180 @@ public class ExitNodeProcess {
}
}
}
/**
* Processa uma ligação recebida de uma interseção.
* Trata uma ligação de uma interseção.
*
* Mantém a ligação aberta e processa continuamente mensagens do tipo
* VEHICLE_TRANSFER. Cada mensagem representa um veículo que chegou ao nó de saída.
* Mantemos a ligação aberta e escutamos por mensagens `VEHICLE_TRANSFER`.
* Cada mensagem contém um veículo que acabou de terminar a sua viagem.
*
* @param clientSocket Socket da ligação estabelecida com a interseção
* @param clientSocket O socket ligado à interseção.
*/
private void handleIncomingConnection(Socket clientSocket) {
String clientAddress = clientSocket.getInetAddress().getHostAddress();
System.out.println("New connection accepted from " + clientAddress);
try (SocketConnection connection = new SocketConnection(clientSocket)) {
System.out.println("New connection accepted from " +
clientSocket.getInetAddress().getHostAddress());
while (running && connection.isConnected()) {
try {
System.out.println("[Exit] Waiting for message from " + clientAddress);
MessageProtocol message = connection.receiveMessage();
if (message.getType() == MessageType.VEHICLE_TRANSFER) {
Vehicle vehicle = (Vehicle) message.getPayload();
System.out.println("[Exit] Received message type: " + message.getType() +
" from " + message.getSourceNode());
if (message.getType() == MessageType.SIMULATION_START) {
// Coordinator sends start time - use it instead of our local start
simulationStartMillis = ((Number) message.getPayload()).longValue();
System.out.println("[Exit] Simulation start time synchronized");
} else if (message.getType() == MessageType.VEHICLE_TRANSFER) {
Object payload = message.getPayload();
System.out.println("[Exit] Payload type: " + payload.getClass().getName());
// Handle Gson LinkedHashMap
Vehicle vehicle;
if (payload instanceof com.google.gson.internal.LinkedTreeMap ||
payload instanceof java.util.LinkedHashMap) {
String json = new com.google.gson.Gson().toJson(payload);
vehicle = new com.google.gson.Gson().fromJson(json, Vehicle.class);
} else {
vehicle = (Vehicle) payload;
}
processExitingVehicle(vehicle);
}
} catch (ClassNotFoundException e) {
System.err.println("Unknown message type received: " + e.getMessage());
System.err.println("[Exit] Unknown message type: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("[Exit] Error processing message: " + e.getMessage());
e.printStackTrace();
}
}
System.out.println("[Exit] Connection closed from " + clientAddress);
} catch (IOException e) {
if (running) {
System.err.println("Connection error: " + e.getMessage());
System.err.println("[Exit] Connection error from " + clientAddress + ": " + e.getMessage());
e.printStackTrace();
}
}
}
/**
* Processa um veículo que chegou ao nó de saída.
* Processa um veículo que acabou de sair do sistema.
*
* Método sincronizado para garantir thread-safety ao atualizar as estatísticas.
* Calcula as métricas finais do veículo e atualiza:
* - Counters globais;
* - Estatísticas por tipo de veículo;
* - Faz update ao dashboard a cada 10 veículos.
* Calculamos quanto tempo demorou, atualizamos as nossas estatísticas globais e
* notificamos o dashboard.
* Este método é sincronizado porque múltiplos veículos podem chegar ao mesmo
* tempo.
*
* @param vehicle Veículo que completou a sua rota
* @param vehicle O veículo que completou a sua rota.
*/
private synchronized void processExitingVehicle(Vehicle vehicle) {
totalVehiclesReceived++;
double systemTime = vehicle.getTotalTravelTime(getCurrentTime());
// Use simulation time instead of wall-clock time
// System time = total time vehicle spent in system (wait + crossing times)
// This represents the actual simulation time elapsed, not real-time
double waitTime = vehicle.getTotalWaitingTime();
double crossingTime = vehicle.getTotalCrossingTime();
double systemTime = waitTime + crossingTime;
// Store times in seconds, will be converted to ms when sending to dashboard
totalSystemTime += systemTime;
totalWaitingTime += waitTime;
totalCrossingTime += crossingTime;
VehicleType type = vehicle.getType();
vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs)%n",
vehicle.getId(), vehicle.getType(), systemTime, waitTime);
if (totalVehiclesReceived % 10 == 0) {
sendStatsToDashboard();
}
System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs, crossing=%.2fs)%n",
vehicle.getId(), vehicle.getType(), systemTime, waitTime, crossingTime);
// Log vehicle exit
EventLogger.getInstance().logVehicle(EventType.VEHICLE_EXITED, "ExitNode", vehicle.getId(),
String.format("Completed - System: %.2fs, Wait: %.2fs, Crossing: %.2fs", systemTime, waitTime,
crossingTime));
// Complete vehicle trace if tracking
VehicleTracer.getInstance().logExit(vehicle, systemTime);
// Send stats after every vehicle to ensure dashboard updates quickly
sendStatsToDashboard();
}
/**
* Obtém o tempo atual da simulação em segundos.
*
* @return Tempo atual em segundos desde "epoch"
*
* "Epoch" é um ponto de referência temporal Unix (1 de janeiro de 1970).
* Este método retorna os segundos decorridos desde esse momento.
*/
private double getCurrentTime() {
return System.currentTimeMillis() / 1000.0;
}
/**
* Envia as estatísticas para o dashboard.
*
* Prepara e envia uma mensagem STATS_UPDATE com:
* - O total de veículos processados;
* - A média dos tempos (sistema, espera, travessia);
* - As contagens e médias por cada tipo de veículo.
* Envia as estatísticas mais recentes para o dashboard.
*
* Empacotamos as contagens totais e os tempos médios num `StatsUpdatePayload`
* e enviamo-lo.
*/
private void sendStatsToDashboard() {
if (dashboardClient == null || !dashboardClient.isConnected()) {
return;
}
try {
Map<String, Object> stats = new HashMap<>();
stats.put("totalVehicles", totalVehiclesReceived);
stats.put("avgSystemTime", totalVehiclesReceived > 0 ? totalSystemTime / totalVehiclesReceived : 0.0);
stats.put("avgWaitingTime", totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0);
stats.put("avgCrossingTime", totalVehiclesReceived > 0 ? totalCrossingTime / totalVehiclesReceived : 0.0);
Map<String, Integer> typeCounts = new HashMap<>();
Map<String, Double> typeAvgWait = new HashMap<>();
// Create stats payload
StatsUpdatePayload payload = new StatsUpdatePayload();
// Set global stats - convert seconds to milliseconds
payload.setTotalVehiclesCompleted(totalVehiclesReceived);
payload.setTotalSystemTime((long) (totalSystemTime * 1000.0)); // s -> ms
payload.setTotalWaitingTime((long) (totalWaitingTime * 1000.0)); // s -> ms
// Set intersection-like stats so it shows up correctly in the dashboard table
payload.setIntersectionArrivals(totalVehiclesReceived);
payload.setIntersectionDepartures(totalVehiclesReceived);
payload.setIntersectionQueueSize(0);
// Set vehicle type stats
Map<VehicleType, Integer> typeCounts = new HashMap<>();
Map<VehicleType, Long> typeWaitTimes = new HashMap<>();
for (VehicleType type : VehicleType.values()) {
int count = vehicleTypeCount.get(type);
typeCounts.put(type.name(), count);
if (count > 0) {
typeAvgWait.put(type.name(), vehicleTypeWaitTime.get(type) / count);
}
typeCounts.put(type, vehicleTypeCount.get(type));
typeWaitTimes.put(type, (long) (vehicleTypeWaitTime.get(type) * 1000.0)); // s -> ms
}
stats.put("vehicleTypeCounts", typeCounts);
stats.put("vehicleTypeAvgWait", typeAvgWait);
Message message = new Message(MessageType.STATS_UPDATE, "ExitNode", "Dashboard", stats);
payload.setVehicleTypeCounts(typeCounts);
payload.setVehicleTypeWaitTimes(typeWaitTimes);
// Send message
Message message = new Message(
MessageType.STATS_UPDATE,
"ExitNode",
"Dashboard",
payload);
dashboardClient.send(message);
double avgWait = totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0;
System.out.printf("[Exit] Sent stats to dashboard (total=%d, avg_wait=%.2fs)%n",
totalVehiclesReceived, totalWaitingTime / totalVehiclesReceived);
} catch (SerializationException | IOException e) {
System.err.println("Failed to send stats to dashboard: " + e.getMessage());
totalVehiclesReceived, avgWait);
} catch (Exception e) {
System.err.println("[Exit] Failed to send stats to dashboard: " + e.getMessage());
}
}
/**
* Termina o processo
* Encerra graciosamente o processo.
*
* Executa a seguinte sequência:
* Imprime as estatísticas finais no terminal;
* Envia a última atualização de estatísticas ao dashboard;
* Fecha o socket;
* Aguarda pela finalização das threads;
* Fecha a ligação com o dashboard;
* Imprimimos as estatísticas finais, fechamos ligações e limpamos threads.
*/
public void shutdown() {
System.out.println("\n[Exit] Shutting down...");
running = false;
printFinalStatistics();
sendStatsToDashboard();
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
@@ -327,7 +508,7 @@ public class ExitNodeProcess {
} catch (IOException e) {
System.err.println("Error closing server socket: " + e.getMessage());
}
connectionHandlerPool.shutdown();
try {
if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
@@ -336,37 +517,31 @@ public class ExitNodeProcess {
} catch (InterruptedException e) {
connectionHandlerPool.shutdownNow();
}
if (dashboardClient != null) {
dashboardClient.close();
}
System.out.println("[Exit] Shutdown complete.");
System.out.println("=".repeat(60));
}
/**
* Imprime as estatísticas finais detalhadas no terminal
*
* Gera um relatório com:
* Total de veículos que completaram a rota;
* Médias de tempo no sistema, espera e travessia;
* Distribuição e médias pelo tipo de veículo (BIKE, LIGHT, HEAVY);
*
* Este método é chamado durante o shutdown para fornecer um resumo
* da simulação antes de terminar o processo.
* Imprime um resumo dos resultados da simulação na consola.
* Isto dá-nos uma visão rápida de como a simulação correu (médias, contagens de
* veículos, etc.).
*/
private void printFinalStatistics() {
System.out.println("\n=== EXIT NODE STATISTICS ===");
System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesReceived);
if (totalVehiclesReceived > 0) {
System.out.printf("%nAVERAGE METRICS:%n");
System.out.printf(" System Time: %.2f seconds%n", totalSystemTime / totalVehiclesReceived);
System.out.printf(" Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesReceived);
System.out.printf(" Crossing Time: %.2f seconds%n", totalCrossingTime / totalVehiclesReceived);
}
System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
for (VehicleType type : VehicleType.values()) {
int count = vehicleTypeCount.get(type);
@@ -374,9 +549,9 @@ public class ExitNodeProcess {
double percentage = (count * 100.0) / totalVehiclesReceived;
double avgWait = vehicleTypeWaitTime.get(type) / count;
System.out.printf(" %s: %d (%.1f%%), Avg Wait: %.2fs%n",
type, count, percentage, avgWait);
type, count, percentage, avgWait);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,234 @@
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;
/**
* Executes multiple simulation runs and aggregates results.
* Calculates statistical measures including mean, standard deviation,
* and confidence intervals across all runs.
*/
public class MultiRunAnalyzer {
private final List<SimulationRunResult> results;
private final String configurationFile;
public MultiRunAnalyzer(String configurationFile) {
this.configurationFile = configurationFile;
this.results = new ArrayList<>();
}
/**
* Adds a completed simulation run result.
*/
public void addResult(SimulationRunResult result) {
results.add(result);
}
/**
* Gets the number of completed runs.
*/
public int getRunCount() {
return results.size();
}
/**
* Generates a comprehensive statistical report.
*/
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();
}
/**
* Analyzes a single metric and returns formatted statistics.
*/
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
);
}
/**
* Extracts values using a lambda function.
*/
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;
}
/**
* Saves the report to a file.
*/
public void saveReport(String filename) throws IOException {
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) {
writer.print(generateReport());
}
}
/**
* Generates a CSV summary for easy import into spreadsheet tools.
*/
public void saveCSV(String filename) throws IOException {
saveCSVSummary(filename);
}
/**
* Generates a CSV summary for easy import into spreadsheet tools.
*/
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,172 @@
package sd.analysis;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Orquestra múltiplas execuções de simulação para análise estatística.
*
* Em vez de correr uma única simulação manualmente, esta ferramenta permite
* correr um "lote"
* de N simulações consecutivas. Isto é essencial para recolher dados
* estatisticamente significativos
* (calcular intervalos de confiança, etc.) conforme exigido pelas
* especificações do projeto.
*
* Utilização:
* java sd.analysis.SimulationBatchRunner <ficheiro-config> <num-execucoes>
* <dir-saida>
*/
public class SimulationBatchRunner {
public static void main(String[] args) {
if (args.length < 3) {
System.err.println("Usage: SimulationBatchRunner <config-file> <num-runs> <output-dir>");
System.err.println("Example: SimulationBatchRunner simulation-medium.properties 10 results/medium");
System.exit(1);
}
String configFile = args[0];
int numRuns;
String outputDir = args[2];
try {
numRuns = Integer.parseInt(args[1]);
if (numRuns < 1 || numRuns > 100) {
throw new IllegalArgumentException("Number of runs must be between 1 and 100");
}
} catch (NumberFormatException e) {
System.err.println("Error: Invalid number of runs: " + args[1]);
System.exit(1);
return;
}
System.out.println("=".repeat(80));
System.out.println("SIMULATION BATCH RUNNER");
System.out.println("=".repeat(80));
System.out.println("Configuration: " + configFile);
System.out.println("Number of Runs: " + numRuns);
System.out.println("Output Directory: " + outputDir);
System.out.println("=".repeat(80));
System.out.println();
// Create output directory
try {
Files.createDirectories(Paths.get(outputDir));
} catch (IOException e) {
System.err.println("Failed to create output directory: " + e.getMessage());
System.exit(1);
}
MultiRunAnalyzer analyzer = new MultiRunAnalyzer(configFile);
// Execute runs
for (int i = 1; i <= numRuns; i++) {
System.out.println("\n" + "=".repeat(80));
System.out.println("STARTING RUN " + i + " OF " + numRuns);
System.out.println("=".repeat(80));
SimulationRunResult result = executeSimulationRun(i, configFile, outputDir);
if (result != null) {
analyzer.addResult(result);
System.out.println("\n" + result);
} else {
System.err.println("Run " + i + " failed!");
}
// Pause between runs
if (i < numRuns) {
System.out.println("\nWaiting 10 seconds before next run...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
// Generate reports
System.out.println("\n\n" + "=".repeat(80));
System.out.println("ALL RUNS COMPLETE - GENERATING REPORTS");
System.out.println("=".repeat(80));
try {
String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date());
String reportFile = outputDir + "/analysis-report-" + timestamp + ".txt";
String csvFile = outputDir + "/summary-" + timestamp + ".csv";
analyzer.saveReport(reportFile);
analyzer.saveCSVSummary(csvFile);
System.out.println("\nReports generated:");
System.out.println(" - Analysis Report: " + reportFile);
System.out.println(" - CSV Summary: " + csvFile);
System.out.println();
// Print report to console
System.out.println(analyzer.generateReport());
} catch (IOException e) {
System.err.println("Failed to generate reports: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Executa uma única instância da simulação.
*
* Idealmente, este método iniciaria todos os processos necessários
* (Interseções, Nó de Saída, Coordenador),
* esperaria que terminassem e depois recolheria os resultados.
*
* Atualmente, serve como um espaço reservado estrutural para demonstrar como
* funciona o pipeline de análise.
* Para correr uma simulação real, deve iniciar os componentes manualmente ou
* usar um script shell.
*/
private static SimulationRunResult executeSimulationRun(int runNumber, String configFile, String outputDir) {
SimulationRunResult result = new SimulationRunResult(runNumber, configFile);
try {
// TODO: Implement actual simulation execution
// This would involve:
// 1. Starting intersection processes
// 2. Starting exit node process
// 3. Starting dashboard process
// 4. Running coordinator
// 5. Collecting results from dashboard/exit node
// 6. Shutting down all processes
System.out.println("NOTE: Actual simulation execution not yet implemented.");
System.out.println("This batch runner demonstrates the framework structure.");
System.out.println("To run actual simulations, you need to:");
System.out.println(" 1. Start all intersection processes manually");
System.out.println(" 2. Start exit node process");
System.out.println(" 3. Start dashboard process");
System.out.println(" 4. Run coordinator with the configuration file");
System.out.println(" 5. Results will be collected automatically");
// Placeholder: simulate some results
// In real implementation, these would be collected from the actual simulation
result.setTotalVehiclesGenerated(100);
result.setTotalVehiclesCompleted(85);
result.setAverageSystemTime(120.5);
result.setMinSystemTime(45.2);
result.setMaxSystemTime(250.8);
result.setAverageWaitingTime(45.3);
return result;
} catch (Exception e) {
System.err.println("Error executing run " + runNumber + ": " + e.getMessage());
e.printStackTrace();
return null;
}
}
}

View File

@@ -0,0 +1,143 @@
package sd.analysis;
import java.util.HashMap;
import java.util.Map;
import sd.model.VehicleType;
/**
* Stores the results of a single simulation run.
* Contains all key metrics for post-simulation analysis.
*/
public class SimulationRunResult {
private final int runNumber;
private final String configurationFile;
private final long startTimeMillis;
private final long endTimeMillis;
// Global metrics
private int totalVehiclesGenerated;
private int totalVehiclesCompleted;
private double averageSystemTime; // seconds
private double minSystemTime; // seconds
private double maxSystemTime; // seconds
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;
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<>();
}
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; }
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; }
public Map<VehicleType, Integer> getVehicleCountByType() {
return new HashMap<>(vehicleCountByType);
}
public Map<VehicleType, Double> getAvgSystemTimeByType() {
return new HashMap<>(avgSystemTimeByType);
}
public Map<VehicleType, Double> getAvgWaitTimeByType() {
return new HashMap<>(avgWaitTimeByType);
}
public Map<String, Integer> getMaxQueueSizeByIntersection() {
return new HashMap<>(maxQueueSizeByIntersection);
}
public Map<String, Double> getAvgQueueSizeByIntersection() {
return new HashMap<>(avgQueueSizeByIntersection);
}
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);
}
@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,160 @@
package sd.analysis;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Statistical analysis utilities for simulation results.
* Calculates mean, standard deviation, and confidence intervals.
*/
public class StatisticalAnalysis {
/**
* Calculates the mean (average) of a list of values.
*/
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();
}
/**
* Calculates the sample standard deviation.
*/
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));
}
/**
* Calculates the 95% confidence interval for the mean.
* Uses t-distribution for small samples (n < 30).
*
* @return Array of [lowerBound, upperBound]
*/
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
};
}
/**
* Returns the t-critical value for 95% confidence interval.
* Approximations for common degrees of freedom (n-1).
*/
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
}
/**
* Calculates the minimum value.
*/
public static double min(List<Double> values) {
if (values == null || values.isEmpty()) {
return 0.0;
}
return Collections.min(values);
}
/**
* Calculates the maximum value.
*/
public static double max(List<Double> values) {
if (values == null || values.isEmpty()) {
return 0.0;
}
return Collections.max(values);
}
/**
* Calculates the median value.
*/
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);
}
}
/**
* Formats a statistical summary as a string.
*/
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

@@ -3,73 +3,163 @@ package sd.config;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.google.gson.Gson;
/**
* Class to load and manage simulation configurations.
* Configurations are read from a .properties file. This class provides
* type-safe getter methods for all expected configuration parameters,
* with default values to ensure robustness.
* Carrega e gere configurações da simulação.
*
* <p>Lê propriedades de um ficheiro .properties e fornece getters
* type-safe com valores padrão para robustez.
*/
public class SimulationConfig {
/**
* Holds all properties loaded from the file.
*/
/** Propriedades carregadas do ficheiro */
private final Properties properties;
private NetworkConfig networkConfig;
public static class NetworkConfig {
private List<IntersectionConfig> intersections;
public List<IntersectionConfig> getIntersections() {
return intersections;
}
}
public static class IntersectionConfig {
private String id;
private List<String> lights;
private Map<String, String> routes;
public String getId() {
return id;
}
public List<String> getLights() {
return lights;
}
public Map<String, String> getRoutes() {
return routes;
}
}
/**
* Constructs a new SimulationConfig object by loading properties
* from the specified file path.
* Carrega propriedades do ficheiro especificado.
*
* <p>Tenta múltiplas estratégias:
* <ol>
* <li>Caminho direto no sistema de ficheiros
* <li>Recurso no classpath (com normalização automática)
* <li>Recurso no classpath com barra inicial
* </ol>
*
* @param filePath The path to the .properties file (e.g., "src/main/resources/simulation.properties").
* @throws IOException If the file cannot be found or read.
* @param filePath caminho do ficheiro .properties
* @throws IOException se o ficheiro não for encontrado
*/
public SimulationConfig(String filePath) throws IOException {
properties = new Properties();
/**Tenta carregar diretamente a partir do sistema de ficheiros, se o ficheiro não existir
* (por exemplo quando executado a partir do classpath/jar),
* faz fallback para carregar a partir do classpath usando o ClassLoader.
*/
IOException lastException = null; //FIXME: melhorar esta parte para reportar erros de forma mais clara
try {
try (InputStream input = new FileInputStream(filePath)) {
properties.load(input);
return; // carregado com sucesso a partir do caminho fornecido
}
// List to track all attempted paths for better error reporting
List<String> attemptedPaths = new ArrayList<>();
IOException fileSystemException = null;
// Strategy 1: Try to load directly from file system
try (InputStream input = new FileInputStream(filePath)) {
properties.load(input);
loadNetworkConfig();
return; // Successfully loaded from file system
} catch (IOException e) {
lastException = e;
//tenta carregar a partir do classpath sem prefixos comuns
String resourcePath = filePath;
//Remove prefixos que apontam para src/main/resources quando presentes
resourcePath = resourcePath.replace("src/main/resources/", "").replace("src\\main\\resources\\", "");
//Remove prefixo classpath: se fornecido
if (resourcePath.startsWith("classpath:")) {
resourcePath = resourcePath.substring("classpath:".length());
if (resourcePath.startsWith("/")) resourcePath = resourcePath.substring(1);
}
fileSystemException = e;
attemptedPaths.add("File system: " + filePath);
}
InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
if (resourceStream == null) {
//como último recurso, tentar com um leading slash
resourceStream = SimulationConfig.class.getResourceAsStream('/' + resourcePath);
}
// Strategy 2: Try to load from classpath with path normalization
String resourcePath = filePath;
if (resourceStream != null) {
try (InputStream input = resourceStream) {
properties.load(input);
return;
}
// Remove common src/main/resources prefixes
resourcePath = resourcePath.replace("src/main/resources/", "").replace("src\\main\\resources\\", "");
// Remove classpath: prefix if provided
if (resourcePath.startsWith("classpath:")) {
resourcePath = resourcePath.substring("classpath:".length());
if (resourcePath.startsWith("/")) {
resourcePath = resourcePath.substring(1);
}
}
if (lastException != null) throw lastException;
// Try loading from classpath using thread context class loader
InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
attemptedPaths.add("Classpath (context): " + resourcePath);
if (resourceStream == null) {
// Strategy 3: Try with leading slash
String slashPath = "/" + resourcePath;
resourceStream = SimulationConfig.class.getResourceAsStream(slashPath);
attemptedPaths.add("Classpath (class): " + slashPath);
}
if (resourceStream != null) {
try (InputStream input = resourceStream) {
properties.load(input);
loadNetworkConfig();
return; // Successfully loaded from classpath
} catch (IOException e) {
// Failed to read from classpath resource
throw new IOException(
String.format("Failed to read properties from classpath resource '%s': %s",
resourcePath, e.getMessage()),
e);
}
}
// All strategies failed - provide comprehensive error message
StringBuilder errorMsg = new StringBuilder();
errorMsg.append("Configuration file '").append(filePath).append("' could not be found.\n");
errorMsg.append("Attempted locations:\n");
for (String path : attemptedPaths) {
errorMsg.append(" - ").append(path).append("\n");
}
if (fileSystemException != null) {
errorMsg.append("\nOriginal error: ").append(fileSystemException.getMessage());
}
throw new IOException(errorMsg.toString(), fileSystemException);
}
private void loadNetworkConfig() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) {
if (is == null) {
System.err.println("Warning: network_config.json not found in classpath. Using defaults/empty.");
return;
}
try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
this.networkConfig = gson.fromJson(reader, NetworkConfig.class);
}
} catch (IOException e) {
System.err.println("Failed to load network_config.json: " + e.getMessage());
e.printStackTrace();
}
}
public NetworkConfig getNetworkConfig() {
return networkConfig;
}
// --- Network configurations ---
/**
* Gets the host address for a specific intersection.
*
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @return The host (e.g., "localhost").
*/
@@ -79,6 +169,7 @@ public class SimulationConfig {
/**
* Gets the port number for a specific intersection.
*
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @return The port number.
*/
@@ -88,6 +179,7 @@ public class SimulationConfig {
/**
* Gets the host address for the dashboard server.
*
* @return The dashboard host.
*/
public String getDashboardHost() {
@@ -96,6 +188,7 @@ public class SimulationConfig {
/**
* Gets the port number for the dashboard server.
*
* @return The dashboard port.
*/
public int getDashboardPort() {
@@ -104,6 +197,7 @@ public class SimulationConfig {
/**
* Gets the host address for the exit node.
*
* @return The exit node host.
*/
public String getExitHost() {
@@ -112,6 +206,7 @@ public class SimulationConfig {
/**
* Gets the port number for the exit node.
*
* @return The exit node port.
*/
public int getExitPort() {
@@ -122,14 +217,34 @@ public class SimulationConfig {
/**
* Gets the total duration of the simulation in virtual seconds.
*
* @return The simulation duration.
*/
public double getSimulationDuration() {
return Double.parseDouble(properties.getProperty("simulation.duration", "3600.0"));
return Double.parseDouble(properties.getProperty("simulation.duration", "3600"));
}
/**
* Get time scaling factor for visualization.
* 0 = instant (pure DES), 0.01 = 100x speed, 0.1 = 10x speed, 1.0 = real-time
*/
public double getTimeScale() {
return Double.parseDouble(properties.getProperty("simulation.time.scale", "0"));
}
/**
* Gets the drain time (in virtual seconds) to allow vehicles to exit after
* generation stops.
*
* @return The drain time.
*/
public double getDrainTime() {
return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0"));
}
/**
* Gets the vehicle arrival model ("POISSON" or "FIXED").
*
* @return The arrival model as a string.
*/
public String getArrivalModel() {
@@ -139,6 +254,7 @@ public class SimulationConfig {
/**
* Gets the average arrival rate (lambda) for the POISSON model.
* This represents the average number of vehicles arriving per second.
*
* @return The arrival rate.
*/
public double getArrivalRate() {
@@ -147,18 +263,29 @@ public class SimulationConfig {
/**
* Gets the fixed time interval between vehicle arrivals for the FIXED model.
*
* @return The fixed interval in seconds.
*/
public double getFixedArrivalInterval() {
return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0"));
}
/**
* Gets the routing policy to use for vehicle route selection.
*
* @return The routing policy (RANDOM, SHORTEST_PATH, or LEAST_CONGESTED).
*/
public String getRoutingPolicy() {
return properties.getProperty("simulation.routing.policy", "RANDOM");
}
// --- Traffic light configurations ---
/**
* Gets the duration of the GREEN light state for a specific traffic light.
*
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @param direction The direction of the light (e.g., "North").
* @param direction The direction of the light (e.g., "North").
* @return The green light time in seconds.
*/
public double getTrafficLightGreenTime(String intersectionId, String direction) {
@@ -168,8 +295,9 @@ public class SimulationConfig {
/**
* Gets the duration of the RED light state for a specific traffic light.
*
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @param direction The direction of the light (e.g., "North").
* @param direction The direction of the light (e.g., "North").
* @return The red light time in seconds.
*/
public double getTrafficLightRedTime(String intersectionId, String direction) {
@@ -181,6 +309,7 @@ public class SimulationConfig {
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type LIGHT.
*
* @return The probability for LIGHT vehicles.
*/
public double getLightVehicleProbability() {
@@ -189,6 +318,7 @@ public class SimulationConfig {
/**
* Gets the average time it takes a LIGHT vehicle to cross an intersection.
*
* @return The crossing time in seconds.
*/
public double getLightVehicleCrossingTime() {
@@ -197,6 +327,7 @@ public class SimulationConfig {
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type BIKE.
*
* @return The probability for BIKE vehicles.
*/
public double getBikeVehicleProbability() {
@@ -205,6 +336,7 @@ public class SimulationConfig {
/**
* Gets the average time it takes a BIKE vehicle to cross an intersection.
*
* @return The crossing time in seconds.
*/
public double getBikeVehicleCrossingTime() {
@@ -213,6 +345,7 @@ public class SimulationConfig {
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type HEAVY.
*
* @return The probability for HEAVY vehicles.
*/
public double getHeavyVehicleProbability() {
@@ -221,27 +354,59 @@ public class SimulationConfig {
/**
* Gets the average time it takes a HEAVY vehicle to cross an intersection.
*
* @return The crossing time in seconds.
*/
public double getHeavyVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0"));
}
/**
* Gets the base travel time between intersections for light vehicles.
*
* @return The base travel time in seconds.
*/
public double getBaseTravelTime() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0"));
}
/**
* Gets the travel time multiplier for bike vehicles.
* Bike travel time = base time × this multiplier.
*
* @return The multiplier for bike travel time.
*/
public double getBikeTravelTimeMultiplier() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5"));
}
/**
* Gets the travel time multiplier for heavy vehicles.
* Heavy vehicle travel time = base time × this multiplier.
*
* @return The multiplier for heavy vehicle travel time.
*/
public double getHeavyTravelTimeMultiplier() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0"));
}
// --- Statistics ---
/**
* Gets the interval (in virtual seconds) between periodic statistics updates.
*
* @return The statistics update interval.
*/
public double getStatisticsUpdateInterval() {
return Double.parseDouble(properties.getProperty("statistics.update.interval", "10.0"));
return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0"));
}
// --- Generic getters ---
/**
* Generic method to get any property as a string, with a default value.
* @param key The property key.
*
* @param key The property key.
* @param defaultValue The value to return if the key is not found.
* @return The property value or the default.
*/
@@ -251,6 +416,7 @@ public class SimulationConfig {
/**
* Generic method to get any property as a string.
*
* @param key The property key.
* @return The property value, or null if not found.
*/

View File

@@ -5,51 +5,87 @@ import java.util.HashMap;
import java.util.Map;
import sd.config.SimulationConfig;
import sd.dashboard.DashboardStatistics;
import sd.dashboard.StatsUpdatePayload;
import sd.des.DESEventType;
import sd.des.EventQueue;
import sd.des.SimulationClock;
import sd.des.SimulationEvent;
import sd.logging.EventLogger;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.routing.LeastCongestedRouteSelector;
import sd.routing.RandomRouteSelector;
import sd.routing.RouteSelector;
import sd.routing.RoutingPolicy;
import sd.routing.ShortestPathRouteSelector;
import sd.serialization.SerializationException;
import sd.util.VehicleGenerator;
/**
* Coordinator process responsible for:
* 1. Vehicle generation (using VehicleGenerator)
* 2. Distributing vehicles to intersection processes via sockets
* 3. Managing simulation timing and shutdown
* Coordenador central da simulação distribuída.
*
* This is the main entry point for the distributed simulation architecture.
* <p>Responsabilidades:
* <ol>
* <li>Gerar veículos segundo modelo configurado (Poisson/Fixed)
* <li>Injetar veículos nas interseções de entrada
* <li>Gerir relógio global e sincronizar componentes
* </ol>
*
* <p>Usa motor DES para agendar eventos de geração com precisão.
* Mantém fila de prioridade e processa eventos em ordem cronológica.
*/
public class CoordinatorProcess {
private final SimulationConfig config;
private final VehicleGenerator vehicleGenerator;
private final Map<String, SocketClient> intersectionClients;
private double currentTime;
private SocketClient dashboardClient;
private final SimulationClock clock;
private final EventQueue eventQueue;
private final EventLogger eventLogger;
private int vehicleCounter;
private boolean running;
private double nextGenerationTime;
private double timeScale;
private RouteSelector currentRouteSelector;
private DashboardStatistics dashboardStatistics;
/**
* Local tracking of intersection queue sizes for dynamic routing.
*
* <p>This approximation tracks queue sizes by incrementing when vehicles are sent
* to intersections. While not perfectly accurate (doesn't track departures in real-time),
* it provides useful congestion information for the LEAST_CONGESTED routing policy.</p>
*
* <p>This is a practical solution that enables dynamic routing without requiring
* bidirectional communication or complex state synchronization.</p>
*/
private final Map<String, Integer> intersectionQueueSizes;
public static void main(String[] args) {
System.out.println("=".repeat(60));
System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
System.out.println("=".repeat(60));
try {
// 1. Load configuration
String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
CoordinatorProcess coordinator = new CoordinatorProcess(config);
// 2. Connect to intersection processes
System.out.println("\n" + "=".repeat(60));
coordinator.initialize();
// 3. Run the sim
System.out.println("\n" + "=".repeat(60));
coordinator.run();
} catch (IOException e) {
System.err.println("Failed to load configuration: " + e.getMessage());
System.exit(1);
@@ -58,131 +94,307 @@ public class CoordinatorProcess {
System.exit(1);
}
}
public CoordinatorProcess(SimulationConfig config) {
this.config = config;
this.vehicleGenerator = new VehicleGenerator(config);
// Inicializa o RouteSelector baseado na política configurada
this.currentRouteSelector = createRouteSelector(config.getRoutingPolicy());
this.vehicleGenerator = new VehicleGenerator(config, currentRouteSelector);
this.intersectionClients = new HashMap<>();
this.currentTime = 0.0;
this.vehicleCounter = 0;
this.running = false;
this.nextGenerationTime = 0.0;
this.timeScale = config.getTimeScale();
this.intersectionQueueSizes = new HashMap<>();
this.clock = new SimulationClock();
this.eventQueue = new EventQueue(true);
this.eventLogger = EventLogger.getInstance();
eventLogger.log(sd.logging.EventType.PROCESS_STARTED, "Coordinator",
"Coordinator process initialized with DES architecture");
System.out.println("Coordinator initialized with configuration:");
System.out.println(" - Simulation duration: " + config.getSimulationDuration() + "s");
System.out.println(" - Arrival model: " + config.getArrivalModel());
System.out.println(" - Arrival rate: " + config.getArrivalRate() + " vehicles/s");
System.out.println(" - Routing policy: " + config.getRoutingPolicy());
System.out.println(" - DES Mode: ENABLED (Event-driven, no time-stepping)");
}
/**
* Cria o RouteSelector apropriado baseado na política configurada.
*
* @param policyName nome da política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
* @return instância do RouteSelector correspondente
*/
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();
}
}
public void initialize() {
// Connect to dashboard first
connectToDashboard();
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) {
try {
String host = config.getIntersectionHost(intersectionId);
int port = config.getIntersectionPort(intersectionId);
SocketClient client = new SocketClient(intersectionId, host, port);
client.connect();
intersectionClients.put(intersectionId, client);
} catch (IOException e) {
System.err.println("Failed to connect to " + intersectionId + ": " + e.getMessage());
}
}
System.out.println("Successfully connected to " + intersectionClients.size() + " intersection(s)");
if (intersectionClients.isEmpty()) {
System.err.println("WARNING: No intersections connected. Simulation cannot proceed.");
}
}
public void run() {
double duration = config.getSimulationDuration();
double drainTime = config.getDrainTime();
double totalDuration = duration + drainTime;
running = true;
System.out.println("Starting vehicle generation simulation...");
System.out.println("Duration: " + duration + " seconds");
System.out.println("Starting DES-based vehicle generation simulation...");
System.out.println("Duration: " + duration + "s (+ " + drainTime + "s drain)");
System.out.println();
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
final double TIME_STEP = 0.1;
while (running && currentTime < duration) {
if (currentTime >= nextGenerationTime) {
generateAndSendVehicle();
nextGenerationTime = vehicleGenerator.getNextArrivalTime(currentTime);
}
currentTime += TIME_STEP;
// Log simulation start
eventLogger.log(sd.logging.EventType.SIMULATION_STARTED, "Coordinator",
String.format("Starting simulation - Duration: %.1fs", duration));
// Send simulation start time to all processes for synchronization
sendSimulationStartTime();
// Schedule first vehicle generation event
double firstArrivalTime = vehicleGenerator.getNextArrivalTime(clock.getCurrentTime());
eventQueue.schedule(new SimulationEvent(
firstArrivalTime,
DESEventType.VEHICLE_GENERATION,
null,
"Coordinator"));
// Schedule simulation end event
eventQueue.schedule(new SimulationEvent(
totalDuration,
DESEventType.SIMULATION_END,
null,
"Coordinator"));
System.out.printf("Initial event scheduled at t=%.3fs\n", firstArrivalTime);
System.out.println("Entering DES event loop...\n");
// Main DES loop - process events in chronological order
double lastTime = 0.0;
while (running && !eventQueue.isEmpty()) {
SimulationEvent event = eventQueue.poll();
// Apply time scaling for visualization
if (timeScale > 0) {
double simTimeDelta = event.getTimestamp() - lastTime;
long realDelayMs = (long) (simTimeDelta * timeScale * 1000);
if (realDelayMs > 0) {
try {
Thread.sleep(realDelayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
lastTime = event.getTimestamp();
}
// Advance simulation time to event time
clock.advanceTo(event.getTimestamp());
// Process the event
processEvent(event, duration);
}
System.out.println();
System.out.println("Simulation complete at t=" + String.format("%.2f", currentTime) + "s");
System.out.printf("Simulation complete at t=%.2fs\n", clock.getCurrentTime());
System.out.println("Total vehicles generated: " + vehicleCounter);
System.out.println("Total events processed: " + eventQueue.getProcessedCount());
// Log simulation end
eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, "Coordinator",
String.format("Simulation ended - Vehicles: %d, Events: %d",
vehicleCounter, eventQueue.getProcessedCount()));
// Export event history (spec requirement: view complete event list)
exportEventHistory();
shutdown();
}
/**
* Trata um único evento de simulação.
*
* É aqui que a magia acontece. Dependendo do tipo de evento (como
* VEHICLE_GENERATION),
* atualizamos o estado do mundo. Para a geração de veículos, criamos um novo
* veículo,
* enviamo-lo para uma interseção e depois agendamos o *próximo* evento de
* geração.
*/
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
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());
}
}
/**
* Guarda o histórico completo de eventos de simulação num ficheiro de texto.
* Isto permite-nos auditar exatamente o que aconteceu e quando, o que é crucial
* para depuração e verificação.
*/
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());
}
}
private void generateAndSendVehicle() {
Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime);
double currentTime = clock.getCurrentTime();
// Usa os tamanhos de fila rastreados localmente para política LEAST_CONGESTED
// Isto permite roteamento dinâmico baseado no estado atual da rede
Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime, intersectionQueueSizes);
System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
// Log to event logger
eventLogger.log(sd.logging.EventType.VEHICLE_GENERATED, "Coordinator",
String.format("[%s] Type: %s, Route: %s", vehicle.getId(), vehicle.getType(), vehicle.getRoute()));
// Update local queue size tracking (increment first intersection's queue)
String firstIntersection = vehicle.getRoute().get(0);
intersectionQueueSizes.put(firstIntersection,
intersectionQueueSizes.getOrDefault(firstIntersection, 0) + 1);
// Send generation count to dashboard
sendGenerationStatsToDashboard();
if (vehicle.getRoute().isEmpty()) {
System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
return;
}
String entryIntersection = vehicle.getRoute().get(0);
sendVehicleToIntersection(vehicle, entryIntersection);
}
private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
SocketClient client = intersectionClients.get(intersectionId);
if (client == null || !client.isConnected()) {
System.err.println("ERROR: No connection to " + intersectionId + " for vehicle " + vehicle.getId());
return;
}
try {
Message message = new Message(
MessageType.VEHICLE_SPAWN,
"COORDINATOR",
intersectionId,
vehicle
);
MessageType.VEHICLE_SPAWN,
"COORDINATOR",
intersectionId,
vehicle);
client.send(message);
System.out.printf("->Sent to %s%n", intersectionId);
} catch (SerializationException | IOException e) {
System.err.println("ERROR: Failed to send vehicle " + vehicle.getId() + " to " + intersectionId);
System.err.println("Reason: " + e.getMessage());
}
}
public void shutdown() {
System.out.println();
System.out.println("=".repeat(60));
System.out.println("Shutting down coordinator...");
for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
String intersectionId = entry.getKey();
SocketClient client = entry.getValue();
try {
if (client.isConnected()) {
Message personalizedShutdown = new Message(
MessageType.SHUTDOWN,
"COORDINATOR",
intersectionId,
"Simulation complete"
);
MessageType.SHUTDOWN,
"COORDINATOR",
intersectionId,
"Simulation complete");
client.send(personalizedShutdown);
System.out.println("Sent shutdown message to " + intersectionId);
}
@@ -192,13 +404,143 @@ public class CoordinatorProcess {
client.close();
}
}
System.out.println("Coordinator shutdown complete");
System.out.println("=".repeat(60));
}
public void stop() {
System.out.println("\nStop signal received...");
running = false;
}
/**
* Altera dinamicamente a política de roteamento durante a simulação.
* Novos veículos gerados usarão a nova política.
*
* @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 do dashboard
* e aplica se houver.
*/
private void checkForPolicyChanges() {
if (dashboardStatistics != null) {
String requestedPolicy = dashboardStatistics.getAndClearRequestedRoutingPolicy();
if (requestedPolicy != null && !requestedPolicy.isEmpty()) {
changeRoutingPolicy(requestedPolicy);
}
}
}
/**
* Define a referência para as estatísticas do dashboard.
* Permite que o coordenador verifique mudanças de política solicitadas.
*/
public void setDashboardStatistics(DashboardStatistics stats) {
this.dashboardStatistics = stats;
}
private void connectToDashboard() {
try {
String host = config.getDashboardHost();
int port = config.getDashboardPort();
System.out.println("Connecting to dashboard at " + host + ":" + port);
dashboardClient = new SocketClient("Dashboard", host, port);
dashboardClient.connect();
System.out.println("Successfully connected to dashboard\n");
} catch (IOException e) {
System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
System.err.println("Coordinator will continue without dashboard connection\n");
}
}
private void sendGenerationStatsToDashboard() {
if (dashboardClient == null || !dashboardClient.isConnected()) {
return;
}
try {
// Create stats payload with vehicle generation count
StatsUpdatePayload payload = new StatsUpdatePayload();
payload.setTotalVehiclesGenerated(vehicleCounter);
Message message = new Message(
MessageType.STATS_UPDATE,
"COORDINATOR",
"Dashboard",
payload);
dashboardClient.send(message);
} catch (Exception e) { // This is fine - can add IOException if need be
// Don't crash if dashboard update fails
System.err.println("Failed to send stats to dashboard: " + e.getMessage());
}
}
private void sendSimulationStartTime() {
long startTimeMillis = System.currentTimeMillis();
// Send to all intersections
for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
try {
Message message = new Message(
MessageType.SIMULATION_START,
"COORDINATOR",
entry.getKey(),
startTimeMillis);
entry.getValue().send(message);
} catch (Exception e) { // Same thing here
System.err.println("Failed to send start time to " + entry.getKey() + ": " + e.getMessage());
}
}
// Send to dashboard
if (dashboardClient != null && dashboardClient.isConnected()) {
try {
Message message = new Message(
MessageType.SIMULATION_START,
"COORDINATOR",
"Dashboard",
startTimeMillis);
dashboardClient.send(message);
} catch (Exception e) { // And here
// Don't crash
}
}
}
}

View File

@@ -10,10 +10,10 @@ import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory;
/**
* Socket client for communication with a single intersection process.
* Cliente socket para comunicação com um processo de interseção.
*
* Handles a persistent TCP connection to one intersection,
* providing a simple way to send serialized messages.
* <p>Gere uma ligação TCP persistente para uma interseção,
* fornecendo uma forma simples de enviar mensagens serializadas.</p>
*/
public class SocketClient {
@@ -25,11 +25,11 @@ public class SocketClient {
private MessageSerializer serializer;
/**
* Creates a new SocketClient for a given intersection.
* Cria um novo cliente socket para uma interseção.
*
* @param intersectionId Intersection ID (ex. "Cr1")
* @param host Host address (ex. "localhost")
* @param port Port number
* @param intersectionId ID da interseção (ex: "Cr1")
* @param host endereço do host (ex: "localhost")
* @param port número da porta
*/
public SocketClient(String intersectionId, String host, int port) {
this.intersectionId = intersectionId;
@@ -39,11 +39,10 @@ public class SocketClient {
}
/**
* Connects to the intersection process via TCP.
* Liga-se ao processo da interseção via TCP.
*
* @throws IOException if the connection cannot be established
* @throws IOException se a ligação não puder ser estabelecida
*/
public void connect() throws IOException {
try {
socket = new Socket(host, port);
@@ -56,12 +55,12 @@ public class SocketClient {
}
/**
* Sends a message to the connected intersection.
* The message is serialized and written over the socket.
* Envia uma mensagem para a interseção ligada.
* A mensagem é serializada e enviada pelo socket.
*
* @param message The message to send
* @throws SerializationException if serialization fails
* @throws IOException if the socket write fails
* @param message mensagem a enviar
* @throws SerializationException se a serialização falhar
* @throws IOException se a escrita no socket falhar
*/
public void send(Message message) throws SerializationException, IOException {
if (socket == null || socket.isClosed()) {
@@ -71,7 +70,6 @@ public class SocketClient {
try {
byte[] data = serializer.serialize(message);
// Prefix with message length (so receiver knows how much to read)
int length = data.length;
outputStream.write((length >> 24) & 0xFF);
outputStream.write((length >> 16) & 0xFF);

View File

@@ -0,0 +1,545 @@
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;
/**
* Dialog for running batch performance analysis.
* Allows running multiple simulations automatically and generating statistical reports.
*/
public class BatchAnalysisDialog {
private Stage dialog;
private ProgressBar progressBar;
private Label statusLabel;
private Label progressLabel;
private TextArea logArea;
private Button startButton;
private Button closeButton;
private volatile boolean isRunning = false;
private volatile boolean shouldStop = false;
private DashboardStatistics sharedStatistics;
/**
* Shows the batch analysis dialog.
*
* @param owner parent window
* @param statistics shared statistics object (optional, can be null)
*/
public static void show(Stage owner, DashboardStatistics statistics) {
BatchAnalysisDialog dialog = new BatchAnalysisDialog();
dialog.sharedStatistics = statistics;
dialog.createAndShow(owner);
}
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);
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("Run multiple simulations automatically to generate statistical analysis");
subtitle.setStyle("-fx-font-size: 12px; -fx-text-fill: #cccccc;");
subtitle.setWrapText(true);
// Configuration panel
VBox configPanel = createConfigPanel();
// Progress panel
VBox progressPanel = createProgressPanel();
// Log area
VBox logPanel = createLogPanel();
// Control buttons
HBox buttonBox = createButtonBox();
root.getChildren().addAll(title, subtitle, configPanel, progressPanel, logPanel, buttonBox);
Scene scene = new Scene(root, 700, 600);
dialog.setScene(scene);
dialog.setOnCloseRequest(e -> {
if (isRunning) {
e.consume();
shouldStop = true;
log("Stopping after current run completes...");
}
});
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("Configuration");
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("Runs per scenario:");
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("Select scenarios to test:");
scenarioHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;");
CheckBox lowCheck = new CheckBox("Low Load (λ=0.2 v/s)");
lowCheck.setSelected(true);
lowCheck.setId("lowCheck");
lowCheck.setStyle("-fx-text-fill: white;");
CheckBox mediumCheck = new CheckBox("Medium Load (λ=0.5 v/s)");
mediumCheck.setSelected(true);
mediumCheck.setId("mediumCheck");
mediumCheck.setStyle("-fx-text-fill: white;");
CheckBox highCheck = new CheckBox("High Load (λ=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("Run duration (seconds):");
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("(simulated time - actual duration depends on 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("Ready to start");
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 runs completed");
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("Activity Log:");
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);
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("START BATCH ANALYSIS");
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("STOP");
stopButton.setStyle("-fx-background-color: #dc3545; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
stopButton.setOnAction(e -> {
shouldStop = true;
log("Stop requested...");
});
closeButton = new Button("CLOSE");
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;
}
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("ERROR: Please select at least one scenario!");
return;
}
// Disable controls
startButton.setDisable(true);
runsSpinner.setDisable(true);
durationSpinner.setDisable(true);
lowCheck.setDisable(true);
mediumCheck.setDisable(true);
highCheck.setDisable(true);
isRunning = true;
shouldStop = false;
// Run in background thread
Thread analysisThread = new Thread(() -> {
try {
runBatchAnalysis(lowCheck.isSelected(), mediumCheck.isSelected(),
highCheck.isSelected(), runsPerScenario, duration);
} finally {
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();
}
private void runBatchAnalysis(boolean low, boolean medium, boolean high, int runsPerScenario, int durationSeconds) {
log("===========================================================");
log("STARTING BATCH PERFORMANCE ANALYSIS");
log("===========================================================");
log("Configuration:");
log(" • Runs per scenario: " + runsPerScenario);
log(" • Duration per run: " + durationSeconds + " seconds");
log(" • Scenarios: " + (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 stopped by user");
updateStatus("Stopped", currentRun, totalRuns);
return;
}
String configFile = scenarios[i];
String scenarioName = scenarioNames[i];
log("");
log("---------------------------------------------------------");
log("SCENARIO: " + scenarioName + " (" + configFile + ")");
log("---------------------------------------------------------");
MultiRunAnalyzer analyzer = new MultiRunAnalyzer(configFile);
for (int run = 1; run <= runsPerScenario; run++) {
if (shouldStop) {
log("Batch analysis stopped by user");
updateStatus("Stopped", currentRun, totalRuns);
savePartialReport(analyzer, scenarioName);
return;
}
currentRun++;
log("");
log("Run " + run + "/" + runsPerScenario + " starting...");
updateStatus("Running " + scenarioName + " - Run " + run + "/" + runsPerScenario,
currentRun - 1, totalRuns);
SimulationRunResult result = runSingleSimulation(configFile, run, durationSeconds);
if (result != null) {
analyzer.addResult(result);
log("Run " + run + " completed - Generated: " + result.getTotalVehiclesGenerated() +
" | Completed: " + result.getTotalVehiclesCompleted() +
" | Avg Time: " + String.format("%.2f", result.getAverageSystemTime()) + "s");
} else {
log("Run " + run + " failed!");
}
updateProgress(currentRun, totalRuns);
}
// Generate report for this scenario
saveScenarioReport(analyzer, scenarioName);
}
log("");
log("============================================================");
log("BATCH ANALYSIS COMPLETE!");
log("===========================================================");
log("Reports saved to: analysis/");
log("");
updateStatus("Complete!", totalRuns, totalRuns);
updateProgress(1.0);
}
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();
// Give time for processes to start and connect
Thread.sleep(3000);
log(" Simulation running (configured duration: " + durationSeconds + "s simulated time)...");
log(" Waiting for coordinator process to complete...");
// Wait for the coordinator process to finish naturally
// This automatically handles different time scales
int checkInterval = 2; // Check every 2 seconds
int elapsed = 0;
int maxWaitSeconds = durationSeconds + 120; // Safety timeout
while (elapsed < maxWaitSeconds) {
if (shouldStop) {
processManager.stopSimulation();
return null;
}
// Check if simulation completed
if (!processManager.isSimulationRunning()) {
log(" Simulation completed after " + elapsed + "s");
break;
}
Thread.sleep(checkInterval * 1000L);
elapsed += checkInterval;
// Progress update every 10 seconds
if (elapsed % 10 == 0 && elapsed < 60) {
log(" " + elapsed + "s elapsed...");
}
}
if (elapsed >= maxWaitSeconds) {
log(" Timeout reached, forcing stop...");
}
// Stop and collect results
log(" Stopping processes...");
processManager.stopSimulation();
Thread.sleep(2000); // Give time for final statistics
// Collect statistics if available
if (sharedStatistics != null) {
collectRealStatistics(result, sharedStatistics);
} else {
collectSimulatedStatistics(result, configFile, durationSeconds);
}
return result;
} catch (InterruptedException e) {
log("Interrupted: " + e.getMessage());
Thread.currentThread().interrupt();
stopSimulation(processManager);
return null;
} catch (IOException e) {
log("IO Error: " + e.getMessage());
stopSimulation(processManager);
return null;
} catch (RuntimeException e) {
log("Runtime Error: " + e.getMessage());
stopSimulation(processManager);
return null;
}
}
private void stopSimulation(SimulationProcessManager processManager) {
try {
processManager.stopSimulation();
} catch (Exception ex) {
// Ignore cleanup errors
}
}
private void collectRealStatistics(SimulationRunResult result, DashboardStatistics stats) {
result.setTotalVehiclesGenerated(stats.getTotalVehiclesGenerated());
result.setTotalVehiclesCompleted(stats.getTotalVehiclesCompleted());
result.setAverageSystemTime(stats.getAverageSystemTime() / 1000.0); // Convert ms to seconds
result.setAverageWaitingTime(stats.getAverageWaitingTime() / 1000.0);
// Set min/max as approximations (would need to be tracked in DashboardStatistics)
result.setMinSystemTime(stats.getAverageSystemTime() / 1000.0 * 0.5);
result.setMaxSystemTime(stats.getAverageSystemTime() / 1000.0 * 2.0);
// Collect per-type statistics
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);
}
// Collect per-intersection statistics
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());
// Average queue size could be tracked over time, but current queue is better than nothing
result.setAvgQueueSize(intersectionId, (double) iStats.getCurrentQueueSize());
}
}
private void collectSimulatedStatistics(SimulationRunResult result, String configFile, int durationSeconds) {
// Simulated results based on load profile for demonstration
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
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(" Note: Using simulated statistics (real collection requires dashboard integration)");
}
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("Report saved: " + reportFile);
log("CSV saved: " + csvFile);
} catch (IOException e) {
log("Failed to save report: " + e.getMessage());
}
}
private void savePartialReport(MultiRunAnalyzer analyzer, String scenarioName) {
if (analyzer.getRunCount() > 0) {
log("Saving partial results...");
saveScenarioReport(analyzer, scenarioName + "_PARTIAL");
}
}
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 + " runs completed");
});
}
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,167 @@
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;
/**
* Diálogo para configuração avançada de parâmetros da simulação.
* Permite ajustar parâmetros em runtime antes de iniciar a simulação.
*/
public class ConfigurationDialog {
/**
* Mostra um diálogo com opções avançadas de configuração.
*
* @param owner janela pai
* @return true se o utilizador confirmar, false se cancelar
*/
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

@@ -0,0 +1,137 @@
package sd.dashboard;
import java.io.IOException;
import java.net.Socket;
import java.util.Map;
import sd.model.MessageType;
import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection;
/**
* Processes statistics messages from a single client connection.
* Runs in a separate thread per client.
*/
public class DashboardClientHandler implements Runnable {
private final Socket clientSocket;
private final DashboardStatistics statistics;
public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) {
this.clientSocket = clientSocket;
this.statistics = statistics;
}
@Override
public void run() {
String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
try (SocketConnection connection = new SocketConnection(clientSocket)) {
System.out.println("[Handler] Started handling client: " + clientInfo);
while (!Thread.currentThread().isInterrupted()) {
try {
MessageProtocol message = connection.receiveMessage();
if (message == null) {
System.out.println("[Handler] Client disconnected: " + clientInfo);
break;
}
processMessage(message);
} catch (ClassNotFoundException e) {
System.err.println("[Handler] Unknown message class from " + clientInfo + ": " + e.getMessage());
} catch (IOException e) {
System.out.println("[Handler] Connection error with " + clientInfo + ": " + e.getMessage());
break;
}
}
} catch (IOException e) {
System.err.println("[Handler] Error initializing connection with " + clientInfo + ": " + e.getMessage());
} finally {
try {
if (!clientSocket.isClosed()) {
clientSocket.close();
}
} catch (IOException e) {
System.err.println("[Handler] Error closing socket for " + clientInfo + ": " + e.getMessage());
}
}
}
private void processMessage(MessageProtocol message) {
if (message.getType() != MessageType.STATS_UPDATE) {
System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType());
return;
}
String senderId = message.getSourceNode();
Object payload = message.getPayload();
System.out.println("[Handler] Received STATS_UPDATE from: " + senderId);
// Handle both direct StatsUpdatePayload and Gson-deserialized Map
StatsUpdatePayload stats;
if (payload instanceof StatsUpdatePayload) {
stats = (StatsUpdatePayload) payload;
} else if (payload instanceof java.util.Map) {
// Gson deserialized as LinkedHashMap - re-serialize and deserialize properly
com.google.gson.Gson gson = new com.google.gson.Gson();
String json = gson.toJson(payload);
stats = gson.fromJson(json, StatsUpdatePayload.class);
} else {
System.err.println("[Handler] Unknown payload type: " +
(payload != null ? payload.getClass().getName() : "null"));
return;
}
updateStatistics(senderId, stats);
}
private void updateStatistics(String senderId, StatsUpdatePayload stats) {
if (stats.getTotalVehiclesGenerated() >= 0) {
statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated());
}
if (stats.getTotalVehiclesCompleted() >= 0) {
statistics.updateVehiclesCompleted(stats.getTotalVehiclesCompleted());
}
// Exit Node sends cumulative totals, so we SET rather than ADD
if (stats.getTotalSystemTime() >= 0) {
statistics.setTotalSystemTime(stats.getTotalSystemTime());
}
if (stats.getTotalWaitingTime() >= 0) {
statistics.setTotalWaitingTime(stats.getTotalWaitingTime());
}
// Process vehicle type statistics (from Exit Node)
if (stats.getVehicleTypeCounts() != null && !stats.getVehicleTypeCounts().isEmpty()) {
Map<sd.model.VehicleType, Integer> counts = stats.getVehicleTypeCounts();
Map<sd.model.VehicleType, Long> waitTimes = stats.getVehicleTypeWaitTimes();
for (var entry : counts.entrySet()) {
sd.model.VehicleType type = entry.getKey();
int count = entry.getValue();
long waitTime = (waitTimes != null && waitTimes.containsKey(type))
? waitTimes.get(type) : 0L;
statistics.updateVehicleTypeStats(type, count, waitTime);
}
}
// Process intersection statistics (from Intersection processes)
if (senderId.startsWith("Cr") || senderId.startsWith("E")) {
statistics.updateIntersectionStats(
senderId,
stats.getIntersectionArrivals(),
stats.getIntersectionDepartures(),
stats.getIntersectionQueueSize()
);
}
System.out.println("[Handler] Successfully updated statistics from: " + senderId);
}
}

View File

@@ -0,0 +1,165 @@
package sd.dashboard;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import sd.config.SimulationConfig;
/**
* Agrega e apresenta estatísticas em tempo real de todos os processos da simulação.
* Usa um thread pool para gerir ligações concorrentes de clientes.
*/
public class DashboardServer {
private final int port;
private final DashboardStatistics statistics;
private final ExecutorService clientHandlerPool;
private final AtomicBoolean running;
private ServerSocket serverSocket;
public static void main(String[] args) {
// Check if GUI mode is requested
boolean useGUI = false;
String configFile = "src/main/resources/simulation.properties";
for (int i = 0; i < args.length; i++) {
if (args[i].equals("--gui") || args[i].equals("-g")) {
useGUI = true;
} else {
configFile = args[i];
}
}
if (useGUI) {
// Launch JavaFX UI
System.out.println("Launching Dashboard with JavaFX GUI...");
DashboardUI.main(args);
} else {
// Traditional terminal mode
System.out.println("=".repeat(60));
System.out.println("DASHBOARD SERVER - DISTRIBUTED TRAFFIC SIMULATION");
System.out.println("=".repeat(60));
try {
System.out.println("Loading configuration from: " + configFile);
SimulationConfig config = new SimulationConfig(configFile);
DashboardServer server = new DashboardServer(config);
// Start the server
System.out.println("\n" + "=".repeat(60));
server.start();
// Keep running until interrupted
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\n\nShutdown signal received...");
server.stop();
}));
// Display statistics periodically
server.displayLoop();
} catch (IOException e) {
System.err.println("Failed to start Dashboard Server: " + e.getMessage());
System.exit(1);
}
}
}
public DashboardServer(SimulationConfig config) {
this.port = config.getDashboardPort();
this.statistics = new DashboardStatistics();
this.clientHandlerPool = Executors.newFixedThreadPool(10);
this.running = new AtomicBoolean(false);
}
public void start() throws IOException {
if (running.get()) {
System.out.println("Dashboard Server is already running.");
return;
}
serverSocket = new ServerSocket(port);
running.set(true);
System.out.println("Dashboard Server started on port " + port);
System.out.println("Waiting for statistics updates from simulation processes...");
System.out.println("=".repeat(60));
Thread acceptThread = new Thread(this::acceptConnections, "DashboardServer-Accept");
acceptThread.setDaemon(false);
acceptThread.start();
}
private void acceptConnections() {
while (running.get()) {
try {
Socket clientSocket = serverSocket.accept();
System.out.println("[Connection] New client connected: " +
clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort());
clientHandlerPool.execute(new DashboardClientHandler(clientSocket, statistics));
} catch (IOException e) {
if (running.get()) {
System.err.println("[Error] Failed to accept client connection: " + e.getMessage());
}
}
}
}
@SuppressWarnings("BusyWait")
private void displayLoop() {
final long DISPLAY_INTERVAL_MS = 5000;
while (running.get()) {
try {
Thread.sleep(DISPLAY_INTERVAL_MS);
displayStatistics();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
public void displayStatistics() {
System.out.println("\n" + "=".repeat(60));
System.out.println("REAL-TIME SIMULATION STATISTICS");
System.out.println("=".repeat(60));
statistics.display();
System.out.println("=".repeat(60));
}
public void stop() {
if (!running.get()) {
return;
}
System.out.println("\nStopping Dashboard Server...");
running.set(false);
try {
if (serverSocket != null && !serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
System.err.println("Error closing server socket: " + e.getMessage());
}
clientHandlerPool.shutdownNow();
System.out.println("Dashboard Server stopped.");
}
public DashboardStatistics getStatistics() {
return statistics;
}
public boolean isRunning() {
return running.get();
}
}

View File

@@ -0,0 +1,257 @@
package sd.dashboard;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import sd.model.VehicleType;
/**
* Armazenamento thread-safe de estatísticas agregadas da simulação.
* Usa tipos atómicos e coleções concorrentes para atualizações sem locks.
*/
public class DashboardStatistics {
private final AtomicInteger totalVehiclesGenerated;
private final AtomicInteger totalVehiclesCompleted;
private final AtomicLong totalSystemTime;
private final AtomicLong totalWaitingTime;
private final Map<String, IntersectionStats> intersectionStats;
private final Map<VehicleType, AtomicInteger> vehicleTypeCount;
private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime;
private volatile long lastUpdateTime;
private volatile String requestedRoutingPolicy;
public DashboardStatistics() {
this.totalVehiclesGenerated = new AtomicInteger(0);
this.totalVehiclesCompleted = new AtomicInteger(0);
this.totalSystemTime = new AtomicLong(0);
this.totalWaitingTime = new AtomicLong(0);
this.intersectionStats = new ConcurrentHashMap<>();
this.vehicleTypeCount = new ConcurrentHashMap<>();
this.vehicleTypeWaitTime = new ConcurrentHashMap<>();
for (VehicleType type : VehicleType.values()) {
vehicleTypeCount.put(type, new AtomicInteger(0));
vehicleTypeWaitTime.put(type, new AtomicLong(0));
}
this.lastUpdateTime = System.currentTimeMillis();
}
public void updateVehiclesGenerated(int count) {
totalVehiclesGenerated.set(count);
updateTimestamp();
}
public void incrementVehiclesGenerated() {
totalVehiclesGenerated.incrementAndGet();
updateTimestamp();
}
public void updateVehiclesCompleted(int count) {
totalVehiclesCompleted.set(count);
updateTimestamp();
}
public void incrementVehiclesCompleted() {
totalVehiclesCompleted.incrementAndGet();
updateTimestamp();
}
public void addSystemTime(long timeMs) {
totalSystemTime.addAndGet(timeMs);
updateTimestamp();
}
public void setTotalSystemTime(long timeMs) {
totalSystemTime.set(timeMs);
updateTimestamp();
}
public void addWaitingTime(long timeMs) {
totalWaitingTime.addAndGet(timeMs);
updateTimestamp();
}
public void setTotalWaitingTime(long timeMs) {
totalWaitingTime.set(timeMs);
updateTimestamp();
}
public void updateVehicleTypeStats(VehicleType type, int count, long waitTimeMs) {
vehicleTypeCount.get(type).set(count);
vehicleTypeWaitTime.get(type).set(waitTimeMs);
updateTimestamp();
}
public void incrementVehicleType(VehicleType type) {
vehicleTypeCount.get(type).incrementAndGet();
updateTimestamp();
}
public void updateIntersectionStats(String intersectionId, int arrivals,
int departures, int currentQueueSize) {
intersectionStats.compute(intersectionId, (id, stats) -> {
if (stats == null) {
stats = new IntersectionStats(intersectionId);
}
stats.updateStats(arrivals, departures, currentQueueSize);
return stats;
});
updateTimestamp();
}
private void updateTimestamp() {
lastUpdateTime = System.currentTimeMillis();
}
public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated.get();
}
public int getTotalVehiclesCompleted() {
return totalVehiclesCompleted.get();
}
public double getAverageSystemTime() {
int completed = totalVehiclesCompleted.get();
if (completed == 0) return 0.0;
return (double) totalSystemTime.get() / completed;
}
public double getAverageWaitingTime() {
int completed = totalVehiclesCompleted.get();
if (completed == 0) return 0.0;
return (double) totalWaitingTime.get() / completed;
}
public int getVehicleTypeCount(VehicleType type) {
return vehicleTypeCount.get(type).get();
}
public double getAverageWaitingTimeByType(VehicleType type) {
int count = vehicleTypeCount.get(type).get();
if (count == 0) return 0.0;
return (double) vehicleTypeWaitTime.get(type).get() / count;
}
public IntersectionStats getIntersectionStats(String intersectionId) {
return intersectionStats.get(intersectionId);
}
public Map<String, IntersectionStats> getAllIntersectionStats() {
return new HashMap<>(intersectionStats);
}
public long getLastUpdateTime() {
return lastUpdateTime;
}
/**
* Obtém os tamanhos atuais das filas de todas as interseções.
* Usado pela política LEAST_CONGESTED para roteamento dinâmico.
*
* @return mapa com 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;
}
/**
* Define a política de roteamento solicitada pelo dashboard.
* O coordenador deve verificar periodicamente e aplicar a mudança.
*/
public void setRequestedRoutingPolicy(String policy) {
this.requestedRoutingPolicy = policy;
}
/**
* Obtém e limpa a política de roteamento solicitada.
* Retorna null se não houver mudança pendente.
*/
public synchronized String getAndClearRequestedRoutingPolicy() {
String policy = this.requestedRoutingPolicy;
this.requestedRoutingPolicy = null;
return policy;
}
public void display() {
System.out.println("\n--- GLOBAL STATISTICS ---");
System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated());
System.out.printf("Total Vehicles Completed: %d%n", getTotalVehiclesCompleted());
System.out.printf("Vehicles In Transit: %d%n",
getTotalVehiclesGenerated() - getTotalVehiclesCompleted());
System.out.printf("Average System Time: %.2f ms%n", getAverageSystemTime());
System.out.printf("Average Waiting Time: %.2f ms%n", getAverageWaitingTime());
System.out.println("\n--- VEHICLE TYPE STATISTICS ---");
for (VehicleType type : VehicleType.values()) {
int count = getVehicleTypeCount(type);
double avgWait = getAverageWaitingTimeByType(type);
System.out.printf("%s: %d vehicles, avg wait: %.2f ms%n",
type, count, avgWait);
}
System.out.println("\n--- INTERSECTION STATISTICS ---");
if (intersectionStats.isEmpty()) {
System.out.println("(No data received yet)");
} else {
for (IntersectionStats stats : intersectionStats.values()) {
stats.display();
}
}
System.out.printf("%nLast Update: %tT%n", lastUpdateTime);
}
public static class IntersectionStats {
private final String intersectionId;
private final AtomicInteger totalArrivals;
private final AtomicInteger totalDepartures;
private final AtomicInteger currentQueueSize;
public IntersectionStats(String intersectionId) {
this.intersectionId = intersectionId;
this.totalArrivals = new AtomicInteger(0);
this.totalDepartures = new AtomicInteger(0);
this.currentQueueSize = new AtomicInteger(0);
}
public void updateStats(int arrivals, int departures, int queueSize) {
this.totalArrivals.set(arrivals);
this.totalDepartures.set(departures);
this.currentQueueSize.set(queueSize);
}
public String getIntersectionId() {
return intersectionId;
}
public int getTotalArrivals() {
return totalArrivals.get();
}
public int getTotalDepartures() {
return totalDepartures.get();
}
public int getCurrentQueueSize() {
return currentQueueSize.get();
}
public void display() {
System.out.printf("%s: Arrivals=%d, Departures=%d, Queue=%d%n",
intersectionId, getTotalArrivals(), getTotalDepartures(), getCurrentQueueSize());
}
}
}

View File

@@ -0,0 +1,605 @@
package sd.dashboard;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import sd.config.SimulationConfig;
import sd.model.VehicleType;
/**
* JavaFX-based Dashboard UI for displaying real-time simulation statistics.
* Provides a graphical interface with auto-updating statistics panels.
*/
public class DashboardUI extends Application {
private DashboardServer server;
private DashboardStatistics statistics;
// Global Statistics Labels
private Label lblVehiclesGenerated;
private Label lblVehiclesCompleted;
private Label lblVehiclesInTransit;
private Label lblAvgSystemTime;
private Label lblAvgWaitingTime;
private Label lblLastUpdate;
// Vehicle Type Table
private TableView<VehicleTypeRow> vehicleTypeTable;
// Intersection Table
private TableView<IntersectionRow> intersectionTable;
// Update scheduler
private ScheduledExecutorService updateScheduler;
// Configuration controls
private ComboBox<String> configFileSelector;
private String selectedConfigFile = "simulation.properties";
private Label configInfoLabel;
@Override
public void start(Stage primaryStage) {
try {
// Initialize server
String configFile = getParameters().getRaw().isEmpty()
? "src/main/resources/simulation.properties"
: getParameters().getRaw().get(0);
SimulationConfig config = new SimulationConfig(configFile);
server = new DashboardServer(config);
statistics = server.getStatistics();
// Start the dashboard server
server.start();
// Build UI
BorderPane root = new BorderPane();
root.getStyleClass().add("root");
// Header
VBox header = createHeader();
root.setTop(header);
// Main content
VBox mainContent = createMainContent();
root.setCenter(mainContent);
// Footer
HBox footer = createFooter();
root.setBottom(footer);
// Create scene
Scene scene = new Scene(root, 1200, 850);
// Load CSS
String cssUrl = getClass().getResource("/dashboard.css").toExternalForm();
scene.getStylesheets().add(cssUrl);
primaryStage.setTitle("Traffic Simulation Dashboard - Real-time Statistics");
primaryStage.setScene(scene);
primaryStage.show();
// Start periodic updates
startPeriodicUpdates();
// Handle window close
primaryStage.setOnCloseRequest(event -> {
shutdown();
});
} catch (Exception e) {
showErrorAlert("Failed to start Dashboard Server", e.getMessage());
e.printStackTrace();
Platform.exit();
}
}
private VBox createHeader() {
VBox header = new VBox(10);
header.getStyleClass().add("header");
header.setAlignment(Pos.CENTER);
Label title = new Label("DISTRIBUTED TRAFFIC SIMULATION DASHBOARD");
title.getStyleClass().add("header-title");
Label subtitle = new Label("Real-time Statistics and Monitoring");
subtitle.getStyleClass().add("header-subtitle");
// Configuration Panel
VBox configPanel = createConfigurationPanel();
// Control Buttons
HBox controls = new HBox(15);
controls.setAlignment(Pos.CENTER);
Button btnStart = new Button("START SIMULATION");
btnStart.getStyleClass().add("button-start");
Button btnStop = new Button("STOP SIMULATION");
btnStop.getStyleClass().add("button-stop");
btnStop.setDisable(true);
SimulationProcessManager processManager = new SimulationProcessManager();
btnStart.setOnAction(e -> {
try {
// Passar o ficheiro de configuração selecionado
processManager.setConfigFile(selectedConfigFile);
processManager.startSimulation();
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();
btnStart.setDisable(false);
btnStop.setDisable(true);
configFileSelector.setDisable(false); // Desbloquear para nova simulação
});
controls.getChildren().addAll(btnStart, btnStop);
header.getChildren().addAll(title, subtitle, configPanel, controls);
return header;
}
/**
* Cria o painel de configuração com seleção de cenário e parâmetros.
*/
private VBox createConfigurationPanel() {
VBox configBox = new VBox(10);
configBox.setAlignment(Pos.CENTER);
configBox.setPadding(new Insets(10));
configBox.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;");
Label configLabel = new Label("Configuração da Simulação");
configLabel.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
HBox configControls = new HBox(20);
configControls.setAlignment(Pos.CENTER);
// Scenario selector
VBox scenarioBox = new VBox(5);
scenarioBox.setAlignment(Pos.CENTER_LEFT);
Label scenarioLabel = new Label("Cenário:");
scenarioLabel.setStyle("-fx-font-size: 12px;");
configFileSelector = new ComboBox<>();
configFileSelector.getItems().addAll(
"simulation.properties",
"simulation-low.properties",
"simulation-medium.properties",
"simulation-high.properties"
);
configFileSelector.setValue("simulation.properties");
configFileSelector.setOnAction(e -> {
selectedConfigFile = configFileSelector.getValue();
updateConfigInfo();
System.out.println("Configuração selecionada: " + selectedConfigFile);
});
scenarioBox.getChildren().addAll(scenarioLabel, configFileSelector);
configControls.getChildren().add(scenarioBox);
// Routing policy selector
VBox routingBox = new VBox(5);
routingBox.setAlignment(Pos.CENTER_LEFT);
Label routingLabel = new Label("Política de Roteamento:");
routingLabel.setStyle("-fx-font-size: 12px;");
ComboBox<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() {
VBox mainContent = new VBox(20);
mainContent.setPadding(new Insets(20));
// Global Statistics Panel
VBox globalStatsCard = createGlobalStatisticsPanel();
// Tables Container
HBox tablesContainer = new HBox(20);
tablesContainer.setAlignment(Pos.TOP_CENTER);
// Vehicle Type Statistics Panel
VBox vehicleTypeCard = createVehicleTypePanel();
HBox.setHgrow(vehicleTypeCard, Priority.ALWAYS);
// Intersection Statistics Panel
VBox intersectionCard = createIntersectionPanel();
HBox.setHgrow(intersectionCard, Priority.ALWAYS);
tablesContainer.getChildren().addAll(vehicleTypeCard, intersectionCard);
mainContent.getChildren().addAll(globalStatsCard, tablesContainer);
return mainContent;
}
private VBox createGlobalStatisticsPanel() {
VBox card = new VBox();
card.getStyleClass().add("card");
// Card Header
HBox cardHeader = new HBox();
cardHeader.getStyleClass().add("card-header");
Label cardTitle = new Label("Global Statistics");
cardTitle.getStyleClass().add("card-title");
cardHeader.getChildren().add(cardTitle);
// Card Content
GridPane grid = new GridPane();
grid.getStyleClass().add("card-content");
grid.setHgap(40);
grid.setVgap(15);
grid.setAlignment(Pos.CENTER);
// Initialize labels
lblVehiclesGenerated = createStatValueLabel("0");
lblVehiclesCompleted = createStatValueLabel("0");
lblVehiclesInTransit = createStatValueLabel("0");
lblAvgSystemTime = createStatValueLabel("0.00 s");
lblAvgWaitingTime = createStatValueLabel("0.00 s");
// Add labels with descriptions
addStatRow(grid, 0, 0, "Total Vehicles Generated", lblVehiclesGenerated);
addStatRow(grid, 1, 0, "Total Vehicles Completed", lblVehiclesCompleted);
addStatRow(grid, 2, 0, "Vehicles In Transit", lblVehiclesInTransit);
addStatRow(grid, 0, 1, "Average System Time", lblAvgSystemTime);
addStatRow(grid, 1, 1, "Average Waiting Time", lblAvgWaitingTime);
card.getChildren().addAll(cardHeader, grid);
return card;
}
private VBox createVehicleTypePanel() {
VBox card = new VBox();
card.getStyleClass().add("card");
// Card Header
HBox cardHeader = new HBox();
cardHeader.getStyleClass().add("card-header");
Label cardTitle = new Label("Vehicle Type Statistics");
cardTitle.getStyleClass().add("card-title");
cardHeader.getChildren().add(cardTitle);
// Table
vehicleTypeTable = new TableView<>();
vehicleTypeTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
vehicleTypeTable.setPrefHeight(300);
TableColumn<VehicleTypeRow, String> typeCol = new TableColumn<>("Vehicle Type");
typeCol.setCellValueFactory(new PropertyValueFactory<>("vehicleType"));
TableColumn<VehicleTypeRow, Integer> countCol = new TableColumn<>("Count");
countCol.setCellValueFactory(new PropertyValueFactory<>("count"));
TableColumn<VehicleTypeRow, String> avgWaitCol = new TableColumn<>("Avg Wait Time");
avgWaitCol.setCellValueFactory(new PropertyValueFactory<>("avgWaitTime"));
vehicleTypeTable.getColumns().addAll(typeCol, countCol, avgWaitCol);
card.getChildren().addAll(cardHeader, vehicleTypeTable);
return card;
}
private VBox createIntersectionPanel() {
VBox card = new VBox();
card.getStyleClass().add("card");
// Card Header
HBox cardHeader = new HBox();
cardHeader.getStyleClass().add("card-header");
Label cardTitle = new Label("Intersection Statistics");
cardTitle.getStyleClass().add("card-title");
cardHeader.getChildren().add(cardTitle);
// Table
intersectionTable = new TableView<>();
intersectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
intersectionTable.setPrefHeight(300);
TableColumn<IntersectionRow, String> idCol = new TableColumn<>("Intersection ID");
idCol.setCellValueFactory(new PropertyValueFactory<>("intersectionId"));
TableColumn<IntersectionRow, Integer> arrivalsCol = new TableColumn<>("Total Arrivals");
arrivalsCol.setCellValueFactory(new PropertyValueFactory<>("arrivals"));
TableColumn<IntersectionRow, Integer> departuresCol = new TableColumn<>("Total Departures");
departuresCol.setCellValueFactory(new PropertyValueFactory<>("departures"));
TableColumn<IntersectionRow, Integer> queueCol = new TableColumn<>("Current Queue");
queueCol.setCellValueFactory(new PropertyValueFactory<>("queueSize"));
intersectionTable.getColumns().addAll(idCol, arrivalsCol, departuresCol, queueCol);
card.getChildren().addAll(cardHeader, intersectionTable);
return card;
}
private HBox createFooter() {
HBox footer = new HBox(10);
footer.getStyleClass().add("footer");
footer.setAlignment(Pos.CENTER_LEFT);
Label statusLabel = new Label("Status:");
statusLabel.getStyleClass().add("footer-text");
statusLabel.setStyle("-fx-font-weight: bold;");
Circle statusIndicator = new Circle(6);
statusIndicator.setFill(javafx.scene.paint.Color.LIME);
Label statusText = new Label("Connected and Receiving Data");
statusText.getStyleClass().add("footer-text");
lblLastUpdate = new Label("Last Update: --:--:--");
lblLastUpdate.getStyleClass().add("footer-text");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
footer.getChildren().addAll(statusLabel, statusIndicator, statusText, spacer, lblLastUpdate);
return footer;
}
private Label createStatValueLabel(String initialValue) {
Label label = new Label(initialValue);
label.getStyleClass().add("stat-value");
return label;
}
private void addStatRow(GridPane grid, int row, int colGroup, String description, Label valueLabel) {
VBox container = new VBox(5);
container.setAlignment(Pos.CENTER_LEFT);
Label descLabel = new Label(description);
descLabel.getStyleClass().add("stat-label");
container.getChildren().addAll(descLabel, valueLabel);
grid.add(container, colGroup, row);
}
private void startPeriodicUpdates() {
updateScheduler = Executors.newSingleThreadScheduledExecutor();
updateScheduler.scheduleAtFixedRate(() -> {
Platform.runLater(this::updateUI);
}, 0, 100, TimeUnit.MILLISECONDS);
}
private void updateUI() {
// Update global statistics
lblVehiclesGenerated.setText(String.valueOf(statistics.getTotalVehiclesGenerated()));
lblVehiclesCompleted.setText(String.valueOf(statistics.getTotalVehiclesCompleted()));
lblVehiclesInTransit.setText(String.valueOf(
statistics.getTotalVehiclesGenerated() - statistics.getTotalVehiclesCompleted()));
lblAvgSystemTime.setText(String.format("%.2f s", statistics.getAverageSystemTime() / 1000.0));
lblAvgWaitingTime.setText(String.format("%.2f s", statistics.getAverageWaitingTime() / 1000.0));
lblLastUpdate.setText(String.format("Last Update: %tT", statistics.getLastUpdateTime()));
// Update vehicle type table
vehicleTypeTable.getItems().clear();
for (VehicleType type : VehicleType.values()) {
int count = statistics.getVehicleTypeCount(type);
double avgWait = statistics.getAverageWaitingTimeByType(type);
vehicleTypeTable.getItems().add(new VehicleTypeRow(
type.toString(), count, String.format("%.2f s", avgWait / 1000.0)));
}
// Update intersection table
intersectionTable.getItems().clear();
Map<String, DashboardStatistics.IntersectionStats> intersectionStats = statistics.getAllIntersectionStats();
for (DashboardStatistics.IntersectionStats stats : intersectionStats.values()) {
intersectionTable.getItems().add(new IntersectionRow(
stats.getIntersectionId(),
stats.getTotalArrivals(),
stats.getTotalDepartures(),
stats.getCurrentQueueSize()));
}
}
/**
* Atualiza a informação exibida sobre a configuração selecionada.
*/
private void updateConfigInfo() {
String info = "";
switch (selectedConfigFile) {
case "simulation-low.properties":
info = "CARGA BAIXA: 0.2 veículos/s (~720/hora) | Sem congestionamento esperado";
break;
case "simulation-medium.properties":
info = "CARGA MÉDIA: 0.5 veículos/s (~1800/hora) | Algum congestionamento esperado";
break;
case "simulation-high.properties":
info = "CARGA ALTA: 1.0 veículo/s (~3600/hora) | Congestionamento significativo esperado";
break;
default:
info = "⚙️ CONFIGURAÇÃO PADRÃO: Verificar ficheiro para parâmetros";
break;
}
configInfoLabel.setText(info);
}
private void shutdown() {
System.out.println("Shutting down Dashboard UI...");
if (updateScheduler != null && !updateScheduler.isShutdown()) {
updateScheduler.shutdownNow();
}
if (server != null) {
server.stop();
}
Platform.exit();
}
private void showErrorAlert(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
/**
* Envia mensagem para o servidor do dashboard que notificará o coordenador.
* Usa uma abordagem indireta: salva a política desejada e o coordenador lerá na próxima geração.
*/
private void sendRoutingPolicyChange(String newPolicy) {
// Store the policy change request in statistics
// The coordinator will check this periodically
if (server != null && statistics != null) {
statistics.setRequestedRoutingPolicy(newPolicy);
System.out.println("Política de roteamento solicitada: " + newPolicy);
System.out.println(" - A mudança será aplicada pelo coordenador na próxima atualização");
// Mostrar confirmação visual
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Política Solicitada");
alert.setHeaderText(null);
alert.setContentText("Política de roteamento solicitada: " + newPolicy + "\nSerá aplicada em breve.");
alert.show();
});
} else {
Platform.runLater(() -> {
showErrorAlert("Erro", "Dashboard não está conectado. Inicie a simulação primeiro.");
});
}
}
public static void main(String[] args) {
launch(args);
}
// Inner classes for TableView data models
public static class VehicleTypeRow {
private final String vehicleType;
private final int count;
private final String avgWaitTime;
public VehicleTypeRow(String vehicleType, int count, String avgWaitTime) {
this.vehicleType = vehicleType;
this.count = count;
this.avgWaitTime = avgWaitTime;
}
public String getVehicleType() {
return vehicleType;
}
public int getCount() {
return count;
}
public String getAvgWaitTime() {
return avgWaitTime;
}
}
public static class IntersectionRow {
private final String intersectionId;
private final int arrivals;
private final int departures;
private final int queueSize;
public IntersectionRow(String intersectionId, int arrivals, int departures, int queueSize) {
this.intersectionId = intersectionId;
this.arrivals = arrivals;
this.departures = departures;
this.queueSize = queueSize;
}
public String getIntersectionId() {
return intersectionId;
}
public int getArrivals() {
return arrivals;
}
public int getDepartures() {
return departures;
}
public int getQueueSize() {
return queueSize;
}
}
}

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,155 @@
package sd.dashboard;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Gere o ciclo de vida dos processos de simulação (Intersections, Exit Node,
* Coordinator).
* Permite iniciar e parar a simulação distribuída dentro da aplicação Java.
*/
public class SimulationProcessManager {
private final List<Process> runningProcesses;
private final String classpath;
private String configFile;
public SimulationProcessManager() {
this.runningProcesses = new ArrayList<>();
this.classpath = System.getProperty("java.class.path");
this.configFile = "src/main/resources/simulation.properties";
}
/**
* Define o ficheiro de configuração a usar.
*
* @param configFile nome do ficheiro (ex: "simulation-low.properties")
*/
public void setConfigFile(String configFile) {
this.configFile = "src/main/resources/" + configFile;
System.out.println("Configuration file set to: " + this.configFile);
}
/**
* Inicia a simulação completa: 5 Intersections, 1 Exit Node, e 1 Coordinator.
*
* @throws IOException se um processo falhar ao iniciar
*/
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.");
}
/**
* Checks if the coordinator process (last process started) is still running.
* When the coordinator finishes, the simulation is complete.
*/
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();
}
/**
* Waits for the simulation to complete naturally.
* Returns true if completed, false if timeout.
*/
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);
}
/**
* Stops all running simulation processes.
*/
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 para iniciar um único processo Java.
*/
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

@@ -0,0 +1,48 @@
package sd.dashboard;
import sd.model.MessageType;
import sd.protocol.MessageProtocol;
/**
* Message wrapper for sending statistics to the dashboard.
*/
public class StatsMessage implements MessageProtocol {
private static final long serialVersionUID = 1L;
private final String sourceNode;
private final String destinationNode;
private final StatsUpdatePayload payload;
public StatsMessage(String sourceNode, StatsUpdatePayload payload) {
this.sourceNode = sourceNode;
this.destinationNode = "DashboardServer";
this.payload = payload;
}
@Override
public MessageType getType() {
return MessageType.STATS_UPDATE;
}
@Override
public Object getPayload() {
return payload;
}
@Override
public String getSourceNode() {
return sourceNode;
}
@Override
public String getDestinationNode() {
return destinationNode;
}
@Override
public String toString() {
return String.format("StatsMessage[from=%s, to=%s, payload=%s]",
sourceNode, destinationNode, payload);
}
}

View File

@@ -0,0 +1,121 @@
package sd.dashboard;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import sd.model.VehicleType;
/**
* DTO para atualizações de estatísticas ao dashboard.
* Campos com valor -1 não são atualizados nesta mensagem.
*/
public class StatsUpdatePayload implements Serializable {
private static final long serialVersionUID = 1L;
private int totalVehiclesGenerated = -1;
private int totalVehiclesCompleted = -1;
private long totalSystemTime = -1;
private long totalWaitingTime = -1;
private int intersectionArrivals = 0;
private int intersectionDepartures = 0;
private int intersectionQueueSize = 0;
private Map<VehicleType, Integer> vehicleTypeCounts;
private Map<VehicleType, Long> vehicleTypeWaitTimes;
public StatsUpdatePayload() {
this.vehicleTypeCounts = new HashMap<>();
this.vehicleTypeWaitTimes = new HashMap<>();
}
public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated;
}
public int getTotalVehiclesCompleted() {
return totalVehiclesCompleted;
}
public long getTotalSystemTime() {
return totalSystemTime;
}
public long getTotalWaitingTime() {
return totalWaitingTime;
}
public int getIntersectionArrivals() {
return intersectionArrivals;
}
public int getIntersectionDepartures() {
return intersectionDepartures;
}
public int getIntersectionQueueSize() {
return intersectionQueueSize;
}
public Map<VehicleType, Integer> getVehicleTypeCounts() {
return vehicleTypeCounts;
}
public Map<VehicleType, Long> getVehicleTypeWaitTimes() {
return vehicleTypeWaitTimes;
}
public StatsUpdatePayload setTotalVehiclesGenerated(int totalVehiclesGenerated) {
this.totalVehiclesGenerated = totalVehiclesGenerated;
return this;
}
public StatsUpdatePayload setTotalVehiclesCompleted(int totalVehiclesCompleted) {
this.totalVehiclesCompleted = totalVehiclesCompleted;
return this;
}
public StatsUpdatePayload setTotalSystemTime(long totalSystemTime) {
this.totalSystemTime = totalSystemTime;
return this;
}
public StatsUpdatePayload setTotalWaitingTime(long totalWaitingTime) {
this.totalWaitingTime = totalWaitingTime;
return this;
}
public StatsUpdatePayload setIntersectionArrivals(int intersectionArrivals) {
this.intersectionArrivals = intersectionArrivals;
return this;
}
public StatsUpdatePayload setIntersectionDepartures(int intersectionDepartures) {
this.intersectionDepartures = intersectionDepartures;
return this;
}
public StatsUpdatePayload setIntersectionQueueSize(int intersectionQueueSize) {
this.intersectionQueueSize = intersectionQueueSize;
return this;
}
public StatsUpdatePayload setVehicleTypeCounts(Map<VehicleType, Integer> vehicleTypeCounts) {
this.vehicleTypeCounts = vehicleTypeCounts;
return this;
}
public StatsUpdatePayload setVehicleTypeWaitTimes(Map<VehicleType, Long> vehicleTypeWaitTimes) {
this.vehicleTypeWaitTimes = vehicleTypeWaitTimes;
return this;
}
@Override
public String toString() {
return String.format("StatsUpdatePayload[generated=%d, completed=%d, arrivals=%d, departures=%d, queueSize=%d]",
totalVehiclesGenerated, totalVehiclesCompleted, intersectionArrivals,
intersectionDepartures, intersectionQueueSize);
}
}

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,67 @@
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,98 @@
package sd.des;
import java.io.Serializable;
/**
* Evento discreto da simulação.
*
* <p>Unidade fundamental de execução num sistema DES:
* <ul>
* <li>timestamp - quando ocorre
* <li>type - o que acontece
* <li>payload - dados associados
* <li>location - qual processo o trata
* </ul>
*/
public class SimulationEvent implements Comparable<SimulationEvent>, Serializable {
private static final long serialVersionUID = 1L;
private final double timestamp;
private final DESEventType type;
private final Object payload;
private final String location; // Process ID (e.g., "Cr1", "Coordinator", "Exit")
/**
* Cria um novo evento de simulação.
*
* @param timestamp instante do evento (tempo de simulação em segundos)
* @param type tipo de evento
* @param payload dados associados (ex: objeto Vehicle)
* @param location processo que trata o evento
*/
public SimulationEvent(double timestamp, DESEventType type, Object payload, String location) {
this.timestamp = timestamp;
this.type = type;
this.payload = payload;
this.location = location;
}
/** Cria evento sem localização (para eventos locais) */
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;
}
/**
* Ordena eventos por timestamp (mais cedo primeiro).
* Em caso de empate, ordena por tipo para determinismo.
*/
@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
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,36 @@
package sd.des;
import sd.model.TrafficLight;
/**
* Payload for traffic light change events.
* Contains the traffic light and its direction.
*/
public class TrafficLightEvent {
private final TrafficLight light;
private final String direction;
private final String intersectionId;
public TrafficLightEvent(TrafficLight light, String direction, String intersectionId) {
this.light = light;
this.direction = direction;
this.intersectionId = intersectionId;
}
public TrafficLight getLight() {
return light;
}
public String getDirection() {
return direction;
}
public String getIntersectionId() {
return intersectionId;
}
@Override
public String toString() {
return String.format("TrafficLightEvent[%s-%s]", intersectionId, direction);
}
}

View File

@@ -1,628 +0,0 @@
package sd.engine;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import sd.config.SimulationConfig;
import sd.model.Event;
import sd.model.EventType;
import sd.model.Intersection;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.util.StatisticsCollector;
import sd.util.VehicleGenerator;
/**
* Core simulation engine using discrete event simulation (DES).
* * This class orchestrates the entire simulation. It maintains a
* {@link PriorityQueue} of {@link Event} objects, representing all
* scheduled future actions. The engine processes events in strict
* chronological order (based on their timestamp).
* * It manages the simulation's state, including:
* - The current simulation time ({@code currentTime}).
* - The collection of all {@link Intersection} objects.
* - The {@link VehicleGenerator} for creating new vehicles.
* - The {@link StatisticsCollector} for tracking metrics.
*/
public class SimulationEngine {
/**
* Holds all simulation parameters loaded from the properties file.
*/
private final SimulationConfig config;
/**
* The core of the discrete event simulation. Events are pulled from this
* queue in order of their timestamp.
*/
private final PriorityQueue<Event> eventQueue;
/**
* A map storing all intersections in the simulation, keyed by their ID (e.g., "Cr1").
*/
private final Map<String, Intersection> intersections;
/**
* Responsible for creating new vehicles according to the configured arrival model.
*/
private final VehicleGenerator vehicleGenerator;
/**
* Collects and calculates statistics throughout the simulation.
*/
private final StatisticsCollector statisticsCollector;
/**
* The current time in the simulation (in virtual seconds).
* This time advances based on the timestamp of the event being processed.
*/
private double currentTime;
/**
* A simple counter to generate unique IDs for vehicles.
*/
private int vehicleCounter;
/**
* Constructs a new SimulationEngine.
*
* @param config The {@link SimulationConfig} object containing all
* simulation parameters.
*/
public SimulationEngine(SimulationConfig config) {
this.config = config;
this.eventQueue = new PriorityQueue<>();
this.intersections = new HashMap<>();
this.vehicleGenerator = new VehicleGenerator(config);
this.statisticsCollector = new StatisticsCollector(config);
this.currentTime = 0.0;
this.vehicleCounter = 0;
}
/**
* Initializes the simulation. This involves:
* 1. Creating all {@link Intersection} and {@link TrafficLight} objects.
* 2. Configuring the routing logic between intersections.
* 3. Scheduling the initial events (first traffic light changes,
* first vehicle generation, and periodic statistics updates).
*/
public void initialize() {
System.out.println("Initializing simulation...");
setupIntersections();
setupRouting();
// Schedule initial events to "bootstrap" the simulation
scheduleTrafficLightEvents();
scheduleNextVehicleGeneration(0.0);
scheduleStatisticsUpdates();
System.out.println("Simulation initialized with " + intersections.size() + " intersections");
}
/**
* Creates all intersections defined in the configuration
* and adds their corresponding traffic lights.
*/
private void setupIntersections() {
String[] intersectionIds = {"Cr1", "Cr2", "Cr3", "Cr4", "Cr5"};
// Note: "North" is commented out, so it won't be created.
String[] directions = {/*"North",*/ "South", "East", "West"};
for (String id : intersectionIds) {
Intersection intersection = new Intersection(id);
// Add traffic lights for each configured direction
for (String direction : directions) {
double greenTime = config.getTrafficLightGreenTime(id, direction);
double redTime = config.getTrafficLightRedTime(id, direction);
TrafficLight light = new TrafficLight(
id + "-" + direction,
direction,
greenTime,
redTime
);
intersection.addTrafficLight(light);
}
intersections.put(id, intersection);
}
}
/**
* Configures how vehicles should be routed between intersections.
* This hardcoded logic defines the "map" of the city.
* * For example, `intersections.get("Cr1").configureRoute("Cr2", "East");` means
* "at intersection Cr1, any vehicle whose *next* destination is Cr2
* should be sent to the 'East' traffic light queue."
*/
private void setupRouting() {
// Cr1 routing
intersections.get("Cr1").configureRoute("Cr2", "East");
intersections.get("Cr1").configureRoute("Cr4", "South");
// Cr2 routing
intersections.get("Cr2").configureRoute("Cr1", "West");
intersections.get("Cr2").configureRoute("Cr3", "East");
intersections.get("Cr2").configureRoute("Cr5", "South");
// Cr3 routing
intersections.get("Cr3").configureRoute("Cr2", "West");
intersections.get("Cr3").configureRoute("S", "South"); // "S" is the exit
// Cr4 routing
//intersections.get("Cr4").configureRoute("Cr1", "North");
intersections.get("Cr4").configureRoute("Cr5", "East");
// Cr5 routing
//intersections.get("Cr5").configureRoute("Cr2", "North");
//intersections.get("Cr5").configureRoute("Cr4", "West");
intersections.get("Cr5").configureRoute("S", "East"); // "S" is the exit
}
/**
* Schedules the initial {@link EventType#TRAFFIC_LIGHT_CHANGE} event
* for every traffic light in the simulation.
* A small random delay is added to "stagger" the lights, preventing
* all of them from changing at the exact same time at t=0.
*/
private void scheduleTrafficLightEvents() {
for (Intersection intersection : intersections.values()) {
for (TrafficLight light : intersection.getTrafficLights()) {
// Start with lights in RED state, schedule first GREEN change
// Stagger the start times slightly to avoid all lights changing at once
double staggerDelay = Math.random() * 1.5;
scheduleTrafficLightChange(light, intersection.getId(), staggerDelay);
}
}
}
/**
* Creates and schedules a new {@link EventType#TRAFFIC_LIGHT_CHANGE} event.
* The event is scheduled to occur at {@code currentTime + delay}.
*
* @param light The {@link TrafficLight} that will change state.
* @param intersectionId The ID of the intersection where the light is located.
* @param delay The time (in seconds) from {@code currentTime} when the change should occur.
*/
private void scheduleTrafficLightChange(TrafficLight light, String intersectionId, double delay) {
double changeTime = currentTime + delay;
Event event = new Event(changeTime, EventType.TRAFFIC_LIGHT_CHANGE, light, intersectionId);
eventQueue.offer(event);
}
/**
* Schedules the next {@link EventType#VEHICLE_GENERATION} event.
* The time of the next arrival is determined by the {@link VehicleGenerator}.
*
* @param baseTime The time from which to calculate the next arrival (usually {@code currentTime}).
*/
private void scheduleNextVehicleGeneration(double baseTime) {
// Get the absolute time for the next arrival.
double nextArrivalTime = vehicleGenerator.getNextArrivalTime(baseTime);
// Only schedule the event if it's within the simulation's total duration.
if (nextArrivalTime < config.getSimulationDuration()) {
Event event = new Event(nextArrivalTime, EventType.VEHICLE_GENERATION, null, null);
eventQueue.offer(event);
}
}
/**
* Schedules all periodic {@link EventType#STATISTICS_UPDATE} events
* for the entire duration of the simulation.
*/
private void scheduleStatisticsUpdates() {
double interval = config.getStatisticsUpdateInterval();
double duration = config.getSimulationDuration();
for (double time = interval; time < duration; time += interval) {
Event event = new Event(time, EventType.STATISTICS_UPDATE, null, null);
eventQueue.offer(event);
}
}
/**
* Runs the main simulation loop.
* The loop continues as long as there are events in the queue and
* the {@code currentTime} is less than the total simulation duration.
* * In each iteration, it:
* 1. Polls the next event from the {@link #eventQueue}.
* 2. Advances {@link #currentTime} to the event's timestamp.
* 3. Calls {@link #processEvent(Event)} to handle the event.
* * After the loop, it prints the final statistics.
*/
public void run() {
System.out.println("Starting simulation...");
double duration = config.getSimulationDuration();
while (!eventQueue.isEmpty() && currentTime < duration) {
// Get the next event in chronological order
Event event = eventQueue.poll();
// Advance simulation time to this event's time
currentTime = event.getTimestamp();
// Process the event
processEvent(event);
}
System.out.println("\nSimulation completed at t=" + String.format("%.2f", currentTime) + "s");
printFinalStatistics();
}
/**
* Main event processing logic.
* Delegates the event to the appropriate handler method based on its {@link EventType}.
*
* @param event The {@link Event} to be processed.
*/
private void processEvent(Event event) {
switch (event.getType()) {
case VEHICLE_GENERATION -> handleVehicleGeneration();
case VEHICLE_ARRIVAL -> handleVehicleArrival(event);
case TRAFFIC_LIGHT_CHANGE -> handleTrafficLightChange(event);
case CROSSING_START -> handleCrossingStart(event);
case CROSSING_END -> handleCrossingEnd(event);
case STATISTICS_UPDATE -> handleStatisticsUpdate();
default -> System.err.println("Unknown event type: " + event.getType());
}
}
/**
* Handles {@link EventType#VEHICLE_GENERATION}.
* 1. Creates a new {@link Vehicle} using the {@link #vehicleGenerator}.
* 2. Records the generation event with the {@link #statisticsCollector}.
* 3. Schedules a {@link EventType#VEHICLE_ARRIVAL} event for the vehicle
* at its first destination intersection.
* 4. Schedules the *next* {@link EventType#VEHICLE_GENERATION} event.
* (Note: This line is commented out in the original, which might be a bug,
* as it implies only one vehicle is ever generated. It should likely be active.)
*/
private void handleVehicleGeneration() {
Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime);
System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());
// Register with statistics collector
statisticsCollector.recordVehicleGeneration(vehicle, currentTime);
// Schedule arrival at first intersection
String firstIntersection = vehicle.getCurrentDestination();
if (firstIntersection != null && !firstIntersection.equals("S")) {
// Assume minimal travel time to first intersection (e.g., 1-3 seconds)
double arrivalTime = currentTime + 1.0 + Math.random() * 2.0;
Event arrivalEvent = new Event(arrivalTime, EventType.VEHICLE_ARRIVAL, vehicle, firstIntersection);
eventQueue.offer(arrivalEvent);
}
// Schedule next vehicle generation
// This was commented out in the original file.
// For a continuous simulation, it should be enabled:
scheduleNextVehicleGeneration(currentTime);
}
/**
* Handles {@link EventType#VEHICLE_ARRIVAL} at an intersection.
* 1. Records the arrival for statistics.
* 2. Advances the vehicle's internal route planner to its *next* destination.
* 3. If the next destination is the exit ("S") or null,
* the vehicle exits the system via {@link #handleVehicleExit(Vehicle)}.
* 4. Otherwise, the vehicle is placed in the correct queue at the
* current intersection using {@link Intersection#receiveVehicle(Vehicle)}.
* 5. Attempts to process the vehicle immediately if its light is green.
*
* @param event The arrival event, containing the {@link Vehicle} and intersection ID.
*/
private void handleVehicleArrival(Event event) {
Vehicle vehicle = (Vehicle) event.getData();
String intersectionId = event.getLocation();
Intersection intersection = intersections.get(intersectionId);
if (intersection == null) {
System.err.println("Unknown intersection: " + intersectionId);
return;
}
System.out.printf("[t=%.2f] Vehicle %s arrived at %s%n",
currentTime, vehicle.getId(), intersectionId);
// Record arrival time (used to calculate waiting time later)
statisticsCollector.recordVehicleArrival(vehicle, intersectionId, currentTime);
// Advance the vehicle's route to the *next* stop
// (it has now arrived at its *current* destination)
boolean hasNext = vehicle.advanceRoute();
if (!hasNext) {
// This was the last stop
handleVehicleExit(vehicle);
return;
}
String nextDestination = vehicle.getCurrentDestination();
if (nextDestination == null || "S".equals(nextDestination)) {
// Next stop is the exit
handleVehicleExit(vehicle);
return;
}
// Add vehicle to the appropriate traffic light queue based on its next destination
intersection.receiveVehicle(vehicle);
// Try to process the vehicle immediately if its light is already green
tryProcessVehicle(vehicle, intersection);
}
/**
* Checks if a newly arrived vehicle (or a vehicle in a queue
* that just turned green) can start crossing.
*
* @param vehicle The vehicle to process.
* @param intersection The intersection where the vehicle is.
*/
private void tryProcessVehicle(Vehicle vehicle, Intersection intersection) { //FIXME
// Find the direction (and light) this vehicle is queued at
// This logic is a bit flawed: it just finds the *first* non-empty queue
// A better approach would be to get the light from the vehicle's route
String direction = intersection.getTrafficLights().stream()
.filter(tl -> tl.getQueueSize() > 0)
.map(TrafficLight::getDirection)
.findFirst()
.orElse(null);
if (direction != null) {
TrafficLight light = intersection.getTrafficLight(direction);
// If the light is green and it's the correct one...
if (light != null && light.getState() == TrafficLightState.GREEN) {
// ...remove the vehicle from the queue (if it's at the front)
Vehicle v = light.removeVehicle();
if (v != null) {
// ...and schedule its crossing.
scheduleCrossing(v, intersection);
}
}
}
}
/**
* Schedules the crossing for a vehicle that has just been dequeued
* from a green light.
* 1. Calculates and records the vehicle's waiting time.
* 2. Schedules an immediate {@link EventType#CROSSING_START} event.
*
* @param vehicle The {@link Vehicle} that is crossing.
* @param intersection The {@link Intersection} it is crossing.
*/
private void scheduleCrossing(Vehicle vehicle, Intersection intersection) {
// Calculate time spent waiting at the red light
double waitTime = currentTime - statisticsCollector.getArrivalTime(vehicle);
vehicle.addWaitingTime(waitTime);
// Schedule crossing start event *now*
Event crossingStart = new Event(currentTime, EventType.CROSSING_START, vehicle, intersection.getId());
processEvent(crossingStart); // Process immediately
}
/**
* Handles {@link EventType#CROSSING_START}.
* 1. Determines the crossing time based on vehicle type.
* 2. Schedules a {@link EventType#CROSSING_END} event to occur
* at {@code currentTime + crossingTime}.
*
* @param event The crossing start event.
*/
private void handleCrossingStart(Event event) {
Vehicle vehicle = (Vehicle) event.getData();
String intersectionId = event.getLocation();
double crossingTime = getCrossingTime(vehicle.getType());
System.out.printf("[t=%.2f] Vehicle %s started crossing at %s (duration=%.2fs)%n",
currentTime, vehicle.getId(), intersectionId, crossingTime);
// Schedule the *end* of the crossing
double endTime = currentTime + crossingTime;
Event crossingEnd = new Event(endTime, EventType.CROSSING_END, vehicle, intersectionId);
eventQueue.offer(crossingEnd);
}
/**
* Handles {@link EventType#CROSSING_END}.
* 1. Updates intersection and vehicle statistics.
* 2. Checks the vehicle's *next* destination.
* 3. If the next destination is the exit ("S"), call {@link #handleVehicleExit(Vehicle)}.
* 4. Otherwise, schedule a {@link EventType#VEHICLE_ARRIVAL} event at the
* *next* intersection, after some travel time.
*
* @param event The crossing end event.
*/
private void handleCrossingEnd(Event event) {
Vehicle vehicle = (Vehicle) event.getData();
String intersectionId = event.getLocation();
// Update stats
Intersection intersection = intersections.get(intersectionId);
if (intersection != null) {
intersection.incrementVehiclesSent();
}
double crossingTime = getCrossingTime(vehicle.getType());
vehicle.addCrossingTime(crossingTime);
System.out.printf("[t=%.2f] Vehicle %s finished crossing at %s%n",
currentTime, vehicle.getId(), intersectionId);
// Decide what to do next
String nextDest = vehicle.getCurrentDestination();
if (nextDest != null && !nextDest.equals("S")) {
// Route to the *next* intersection
// Assume 5-10 seconds travel time between intersections
double travelTime = 5.0 + Math.random() * 5.0;
double arrivalTime = currentTime + travelTime;
Event arrivalEvent = new Event(arrivalTime, EventType.VEHICLE_ARRIVAL, vehicle, nextDest);
eventQueue.offer(arrivalEvent);
} else {
// Reached the exit
handleVehicleExit(vehicle);
}
}
/**
* Handles a vehicle exiting the simulation.
* Records final statistics for the vehicle.
*
* @param vehicle The {@link Vehicle} that has completed its route.
*/
private void handleVehicleExit(Vehicle vehicle) {
System.out.printf("[t=%.2f] Vehicle %s exited the system (wait=%.2fs, travel=%.2fs)%n",
currentTime, vehicle.getId(),
vehicle.getTotalWaitingTime(),
vehicle.getTotalTravelTime(currentTime));
// Record the exit for final statistics calculation
statisticsCollector.recordVehicleExit(vehicle, currentTime);
}
/**
* Handles {@link EventType#TRAFFIC_LIGHT_CHANGE}.
* 1. Toggles the light's state (RED to GREEN or GREEN to RED).
* 2. If the light just turned GREEN, call {@link #processGreenLight(TrafficLight, Intersection)}
* to process any waiting vehicles.
* 3. Schedules the *next* state change for this light based on its
* green/red time duration.
*
* @param event The light change event.
*/
private void handleTrafficLightChange(Event event) {
TrafficLight light = (TrafficLight) event.getData();
String intersectionId = event.getLocation();
// Toggle state
TrafficLightState newState = (light.getState() == TrafficLightState.RED)
? TrafficLightState.GREEN
: TrafficLightState.RED;
light.changeState(newState);
System.out.printf("[t=%.2f] Traffic light %s changed to %s%n",
currentTime, light.getId(), newState);
// If changed to GREEN, process waiting vehicles
if (newState == TrafficLightState.GREEN) {
Intersection intersection = intersections.get(intersectionId);
if (intersection != null) {
processGreenLight(light, intersection);
}
}
// Schedule the *next* state change for this same light
double nextChangeDelay = (newState == TrafficLightState.GREEN)
? light.getGreenTime()
: light.getRedTime();
scheduleTrafficLightChange(light, intersectionId, nextChangeDelay);
}
/**
* Processes vehicles when a light turns green.
* It loops as long as the light is green and there are vehicles in the queue,
* dequeuing one vehicle at a time and scheduling its crossing.
* * *Note*: This is a simplified model. A real simulation would
* account for the *time* it takes each vehicle to cross, processing
* one vehicle every {@code crossingTime} seconds. This implementation
* processes the entire queue "instantaneously" at the moment
* the light turns green.
*
* @param light The {@link TrafficLight} that just turned green.
* @param intersection The {@link Intersection} where the light is.
*/
private void processGreenLight(TrafficLight light, Intersection intersection) {
// While the light is green and vehicles are waiting...
while (light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
Vehicle vehicle = light.removeVehicle();
if (vehicle != null) {
// Dequeue one vehicle and schedule its crossing
scheduleCrossing(vehicle, intersection);
}
}
}
/**
* Handles {@link EventType#STATISTICS_UPDATE}.
* Calls the {@link StatisticsCollector} to print the current
* state of the simulation (queue sizes, averages, etc.).
*/
private void handleStatisticsUpdate() {
System.out.printf("\n=== Statistics at t=%.2f ===%n", currentTime);
statisticsCollector.printCurrentStatistics(intersections, currentTime);
System.out.println();
}
/**
* Utility method to get the configured crossing time for a given {@link VehicleType}.
*
* @param type The type of vehicle.
* @return The crossing time in seconds.
*/
private double getCrossingTime(VehicleType type) {
return switch (type) {
case BIKE -> config.getBikeVehicleCrossingTime();
case LIGHT -> config.getLightVehicleCrossingTime();
case HEAVY -> config.getHeavyVehicleCrossingTime();
default -> 2.0;
}; // Default fallback
}
/**
* Prints the final summary of statistics at the end of the simulation.
*/
private void printFinalStatistics() {
System.out.println("\n" + "=".repeat(60));
System.out.println("FINAL SIMULATION STATISTICS");
System.out.println("=".repeat(60));
statisticsCollector.printFinalStatistics(intersections, currentTime);
System.out.println("=".repeat(60));
}
// --- Public Getters ---
/**
* Gets the current simulation time.
* @return The time in virtual seconds.
*/
public double getCurrentTime() {
return currentTime;
}
/**
* Gets a map of all intersections in the simulation.
* Returns a copy to prevent external modification.
* @return A {@link Map} of intersection IDs to {@link Intersection} objects.
*/
public Map<String, Intersection> getIntersections() {
return new HashMap<>(intersections);
}
/**
* Gets the statistics collector instance.
* @return The {@link StatisticsCollector}.
*/
public StatisticsCollector getStatisticsCollector() {
return statisticsCollector;
}
}

View File

@@ -1,158 +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 {
/**
* The TrafficLight object (the *model*) that this thread controls.
* Contains the queue and the state.
*/
private final TrafficLight light;
/**
* The IntersectionProcess (the Process) that "owns" this thread.
* Used to call methods on the process, such as sendVehicleToNextDestination().
*/
private final IntersectionProcess process;
/**
* The simulation configuration, used to get timings (e.g., crossing time).
*/
private final SimulationConfig config;
/**
* Volatile flag to control the graceful shutdown mechanism.
* When set to 'false', the 'run()' loop terminates.
*/
private volatile boolean running;
/**
* Constructor for the Traffic Light Thread.
*
* @param light The TrafficLight object (model) to be controlled.
* @param process The parent IntersectionProcess (for callbacks).
* @param config The simulation configuration (to get timings).
*/
public TrafficLightThread(TrafficLight light, IntersectionProcess process, SimulationConfig config) {
this.light = light;
this.process = process;
this.config = config;
this.running = false; // Starts as 'stopped'
}
/**
* The main entry point for the thread.
* Implements the GREEN/RED cycle logic extracted from IntersectionProcess.
*
*/
@Override
public void run() {
this.running = true;
System.out.println("[" + light.getId() + "] Traffic light thread started.");
try {
// Main thread loop, continues while 'running' is true
// This 'running' flag is controlled by the parent IntersectionProcess
while (running) {
// --- GREEN Phase ---
light.changeState(TrafficLightState.GREEN); //
System.out.println("[" + light.getId() + "] State: GREEN");
// Process vehicles in the queue
processGreenLightQueue();
// Wait for green duration
Thread.sleep((long) (light.getGreenTime() * 1000)); //
if (!running) break; // Check flag after sleep
// --- RED Phase ---
light.changeState(TrafficLightState.RED); //
System.out.println("[" + light.getId() + "] State: RED");
// Wait for red duration
Thread.sleep((long) (light.getRedTime() * 1000)); //
}
} catch (InterruptedException e) {
// Apanha a InterruptedException (outra forma de parar a thread)
System.out.println("[" + light.getId() + "] Traffic light thread interrupted.");
this.running = false; // Garante que o loop termina
}
System.out.println("[" + light.getId() + "] Traffic light thread stopped.");
}
/**
* Processes vehicles in the queue while the traffic light is GREEN.
* Logic extracted from IntersectionProcess.processGreenLight()
*
*/
private void processGreenLightQueue() throws InterruptedException {
//
while (running && light.getState() == TrafficLightState.GREEN && light.getQueueSize() > 0) {
Vehicle vehicle = light.removeVehicle(); //
if (vehicle != null) {
// 1. Get the crossing time (t_sem)
double crossingTime = getCrossingTimeForVehicle(vehicle); //
// 2. Simulate the time the vehicle takes to cross
Thread.sleep((long) (crossingTime * 1000)); //
// 3. Update vehicle statistics
vehicle.addCrossingTime(crossingTime); //
// 4. Update intersection statistics
process.getIntersection().incrementVehiclesSent(); //
// 5. Call the parent Process to send the vehicle
process.sendVehicleToNextDestination(vehicle); //
}
}
}
/**
* Gets the crossing time for a vehicle based on its type.
* Logic extracted from IntersectionProcess.getCrossingTimeForVehicle()
*
*
* @param vehicle The vehicle.
* @return The crossing time in seconds.
*/
private double getCrossingTimeForVehicle(Vehicle vehicle) {
switch (vehicle.getType()) { //
case BIKE:
return config.getBikeVehicleCrossingTime(); //
case LIGHT:
return config.getLightVehicleCrossingTime(); //
case HEAVY:
return config.getHeavyVehicleCrossingTime(); //
default:
return config.getLightVehicleCrossingTime(); //
}
}
/**
* Requests the thread to stop gracefully (graceful shutdown).
* Sets the 'running' flag to false. The thread will finish
* its current sleep cycle and exit the 'run()' loop.
*/
public void shutdown() {
this.running = false;
}
}

View File

@@ -0,0 +1,213 @@
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;
/**
* Sistema de registo centralizado de eventos para a simulação distribuída.
*
* <p>Regista todos os eventos da simulação num ficheiro com timestamps e categorização.
* Thread-safe e não-bloqueante para impacto mínimo na performance.</p>
*/
public class EventLogger {
private static EventLogger instance;
private static final Object instanceLock = new Object();
private final PrintWriter writer;
private final BlockingQueue<LogEntry> logQueue;
private final Thread writerThread;
private final AtomicBoolean running;
private final SimpleDateFormat timestampFormat;
private final long simulationStartMillis;
/** Construtor privado para padrão singleton */
private EventLogger(String logFilePath) throws IOException {
this.writer = new PrintWriter(new BufferedWriter(new FileWriter(logFilePath, false)), true);
this.logQueue = new LinkedBlockingQueue<>(10000);
this.running = new AtomicBoolean(true);
this.timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
this.simulationStartMillis = System.currentTimeMillis();
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);
this.writerThread.start();
}
/** Obtém ou cria 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;
}
/**
* Initialize with custom log file path.
*/
public static void initialize(String logFilePath) throws IOException {
synchronized (instanceLock) {
if (instance != null) {
instance.shutdown();
}
instance = new EventLogger(logFilePath);
}
}
/**
* Logs an event (non-blocking).
*/
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
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);
}
}
/**
* Logs an event with vehicle context.
*/
public void logVehicle(EventType eventType, String component, String vehicleId, String description) {
log(eventType, component, "[" + vehicleId + "] " + description);
}
/**
* Logs an error event.
*/
public void logError(String component, String description, Exception e) {
String fullDescription = description + (e != null ? ": " + e.getMessage() : "");
log(EventType.ERROR, component, fullDescription);
}
/**
* Background thread that writes log entries to file.
*/
private void processLogQueue() {
while (running.get() || !logQueue.isEmpty()) {
try {
LogEntry entry = logQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS);
if (entry != null) {
writeEntry(entry);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// Flush remaining entries
while (!logQueue.isEmpty()) {
LogEntry entry = logQueue.poll();
if (entry != null) {
writeEntry(entry);
}
}
}
/**
* Writes a single log entry to file.
*/
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 periodically for real-time viewing
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);
}
/**
* Shuts down the logger and flushes all pending entries.
*/
public void shutdown() {
if (!running.compareAndSet(true, false)) {
return; // Already shut down
}
try {
// Wait for writer thread to finish
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();
}
}
/**
* Internal class to represent a log entry.
*/
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,48 @@
package sd.logging;
/**
* Tipos de eventos que podem ocorrer na simulação.
* Usados para categorizar e filtrar logs.
*/
public enum EventType {
VEHICLE_GENERATED("Vehicle Generated"),
VEHICLE_ARRIVED("Vehicle Arrived"),
VEHICLE_QUEUED("Vehicle Queued"),
VEHICLE_DEPARTED("Vehicle Departed"),
VEHICLE_EXITED("Vehicle Exited"),
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"),
SIMULATION_STARTED("Simulation Started"),
SIMULATION_STOPPED("Simulation Stopped"),
PROCESS_STARTED("Process Started"),
PROCESS_STOPPED("Process Stopped"),
STATS_UPDATE("Statistics Update"),
CONFIG_CHANGED("Configuration Changed"),
CONNECTION_ESTABLISHED("Connection Established"),
CONNECTION_LOST("Connection Lost"),
MESSAGE_SENT("Message Sent"),
MESSAGE_RECEIVED("Message Received"),
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,331 @@
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;
/**
* Rastreia e regista a viagem completa de veículos individuais.
*
* <p>Cria ficheiros de trace detalhados com:
* <ul>
* <li>Timestamps de todos os eventos
* <li>Localizações (interseções)
* <li>Tempos de espera em cada semáforo
* <li>Tempos de travessia
* <li>Tempo total no sistema
* </ul>
*/
public class VehicleTracer {
private static VehicleTracer instance;
private static final Object instanceLock = new Object();
private final Map<String, VehicleTrace> trackedVehicles;
private final SimpleDateFormat timestampFormat;
private final long simulationStartMillis;
private final String traceDirectory;
/** Construtor privado (singleton) */
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 ou cria a instância singleton */
public static VehicleTracer getInstance() {
if (instance == null) {
synchronized (instanceLock) {
if (instance == null) {
instance = new VehicleTracer("logs/traces");
}
}
}
return instance;
}
/** Inicializa com diretório de trace customizado */
public static void initialize(String traceDirectory) {
synchronized (instanceLock) {
if (instance != null) {
instance.shutdown();
}
instance = new VehicleTracer(traceDirectory);
}
}
/**
* Começa a rastrear um veículo específico.
* Cria ficheiro de trace para este veículo.
*/
public void startTracking(String vehicleId) {
if (trackedVehicles.containsKey(vehicleId)) {
return; // Already tracking
}
VehicleTrace trace = new VehicleTrace(vehicleId, traceDirectory);
trackedVehicles.put(vehicleId, trace);
trace.logEvent("TRACKING_STARTED", "", "Started tracking vehicle " + vehicleId);
}
/**
* Stops tracking a vehicle and closes its trace file.
*/
public void stopTracking(String vehicleId) {
VehicleTrace trace = trackedVehicles.remove(vehicleId);
if (trace != null) {
trace.logEvent("TRACKING_STOPPED", "", "Stopped tracking vehicle " + vehicleId);
trace.close();
}
}
/**
* Checks if a vehicle is being tracked.
*/
public boolean isTracking(String vehicleId) {
return trackedVehicles.containsKey(vehicleId);
}
/**
* Logs when a vehicle is generated.
*/
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()));
}
}
/**
* Logs when a vehicle arrives at an intersection.
*/
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));
}
}
/**
* Logs when a vehicle is queued at a traffic light.
*/
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));
}
}
/**
* Logs when a vehicle starts waiting at a red light.
*/
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));
}
}
/**
* Logs when a vehicle finishes waiting (light turns green).
*/
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));
}
}
/**
* Logs when a vehicle starts crossing an intersection.
*/
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));
}
}
/**
* Logs when a vehicle finishes crossing an intersection.
*/
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));
}
}
/**
* Logs when a vehicle departs from an intersection.
*/
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));
}
}
/**
* Logs when a vehicle exits the system.
*/
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()));
// Write summary
trace.writeSummary(vehicle, systemTime);
// Stop tracking and close file
stopTracking(vehicle.getId());
}
}
/**
* Shuts down the tracer and closes all trace files.
*/
public void shutdown() {
for (VehicleTrace trace : trackedVehicles.values()) {
trace.close();
}
trackedVehicles.clear();
}
/**
* Internal class to handle tracing for a single vehicle.
*/
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;
/**
* Represents an intersection in the traffic simulation.
* * An Intersection acts as a central hub. It does not control logic itself,
* but it *owns* and *manages* a set of {@link TrafficLight} objects.
* * Its primary responsibilities are:
* 1. Holding a {@link TrafficLight} for each direction ("North", "East", etc.).
* 2. Maintaining a {@code routing} table that maps a vehicle's *next*
* destination (e.g., "Cr3") to a specific *direction* at *this*
* intersection (e.g., "East").
* 3. Receiving incoming vehicles and placing them in the correct
* traffic light's queue based on the routing table.
* 4. Tracking aggregate statistics for all traffic passing through it.
* Representa uma interseção na simulação de tráfego.
*
* <p>Uma interseção funciona como um nó central da rede. Não controla lógica diretamente,
* mas gere um conjunto de semáforos ({@link TrafficLight}).</p>
*
* <p>Responsabilidades principais:</p>
* <ul>
* <li>Manter um {@link TrafficLight} para cada direção (Norte, Este, etc.)</li>
* <li>Gerir uma tabela de encaminhamento que mapeia destinos para direções</li>
* <li>Receber veículos e colocá-los na fila do semáforo correto</li>
* <li>Acompanhar estatísticas agregadas do tráfego</li>
* </ul>
*/
public class Intersection {
// --- Identity and configuration ---
/**
* Unique identifier for the intersection (e.g., "Cr1", "Cr2").
*/
/** Identificador único da interseção (ex: "Cr1", "Cr2") */
private final String id;
/**
* A map holding all traffic lights managed by this intersection.
* Key: Direction (String, e.g., "North", "East").
* Value: The {@link TrafficLight} object for that direction.
* Mapa com todos os semáforos desta interseção.
* Chave: Direção (String, ex: "Norte", "Este")
* Valor: Objeto {@link TrafficLight} correspondente
*/
private final Map<String, TrafficLight> trafficLights;
/**
* The routing table for this intersection.
* Key: The *next* destination ID (String, e.g., "Cr3", "S" for exit).
* Value: The *direction* (String, e.g., "East") a vehicle must take
* at *this* intersection to reach that destination.
* Tabela de encaminhamento da interseção.
* Chave: Próximo destino (String, ex: "Cr3", "S" para saída)
* Valor: Direção que o veículo deve tomar nesta interseção
*/
private final Map<String, String> routing;
// --- Statistics ---
/**
* Total number of vehicles that have been received by this intersection.
*/
/** Número total de veículos recebidos por esta interseção */
private int totalVehiclesReceived;
/**
* Total number of vehicles that have successfully passed through (sent from) this intersection.
*/
/** Número total de veículos que partiram desta interseção */
private int totalVehiclesSent;
/**
* A running average of the waiting time for vehicles at this intersection.
* Note: This calculation might be simplified.
*/
/** Média acumulada do tempo de espera dos veículos nesta interseção */
private double averageWaitingTime;
/**
* Constructs a new Intersection with a given ID.
* Initializes empty maps for traffic lights and routing.
* Cria uma nova interseção.
* Inicializa mapas vazios para semáforos e encaminhamento.
*
* @param id The unique identifier for this intersection (e.g., "Cr1").
* @param id identificador único da interseção (ex: "Cr1")
*/
public Intersection(String id) {
this.id = id;
@@ -76,42 +62,37 @@ public class Intersection {
}
/**
* Registers a new {@link TrafficLight} with this intersection.
* The light is mapped by its direction.
* Regista um novo semáforo nesta interseção.
* O semáforo é mapeado pela sua direção.
*
* @param trafficLight The {@link TrafficLight} object to add.
* @param trafficLight o semáforo a adicionar
*/
public void addTrafficLight(TrafficLight trafficLight) {
trafficLights.put(trafficLight.getDirection(), trafficLight);
}
/**
* Defines a routing rule for this intersection.
* * This method builds the routing table. For example, calling
* {@code configureRoute("Cr3", "East")} means "Any vehicle
* arriving here whose next destination is 'Cr3' should be sent to
* the 'East' traffic light queue."
* Define uma regra de encaminhamento para esta interseção.
*
* <p>Por exemplo, {@code configureRoute("Cr3", "Este")} significa:
* "Qualquer veículo que chegue aqui com destino 'Cr3' deve ser enviado
* para a fila do semáforo da direção Este."</p>
*
* @param nextDestination The ID of the *next* intersection or exit (e.g., "Cr3", "S").
* @param direction The direction (and thus, the traffic light)
* at *this* intersection to use (e.g., "East").
* @param nextDestination ID da próxima interseção ou saída (ex: "Cr3", "S")
* @param direction direção (e respetivo semáforo) a usar nesta interseção
*/
public void configureRoute(String nextDestination, String direction) {
routing.put(nextDestination, direction);
}
/**
* Accepts an incoming vehicle and places it in the correct queue.
* * This method:
* 1. Increments the {@link #totalVehiclesReceived} counter.
* 2. Advances the vehicle's route (since it just arrived here)
* 3. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
* 4. Uses the {@link #routing} map to find the correct *direction* for that destination.
* 5. Adds the vehicle to the queue of the {@link TrafficLight} for that direction.
*
* @param vehicle The {@link Vehicle} arriving at the intersection.
* Recebe um novo veículo e coloca-o na fila do semáforo apropriado.
* A direção é escolhida com base na tabela de encaminhamento.
*
* @param vehicle o veículo que está a chegar a esta interseção
* @param simulationTime o tempo de simulação atual (em segundos)
*/
public void receiveVehicle(Vehicle vehicle) {
public void receiveVehicle(Vehicle vehicle, double simulationTime) {
totalVehiclesReceived++;
// Note: Route advancement is handled by SimulationEngine.handleVehicleArrival()
@@ -130,7 +111,7 @@ public class Intersection {
if (direction != null && trafficLights.containsKey(direction)) {
// Found a valid route and light, add vehicle to the queue
trafficLights.get(direction).addVehicle(vehicle);
trafficLights.get(direction).addVehicle(vehicle, simulationTime);
} else {
// Routing error: No rule for this destination or no light for that direction
System.err.printf(
@@ -138,108 +119,100 @@ public class Intersection {
this.id, vehicle.getId(), nextDestination, direction
);
}
} /**
* Retorna a direção que um veículo deve tomar para alcançar um destino.
*
* @param destination o próximo destino (ex: "Cr3", "S")
* @return a direção (ex: "Este"), ou null se não houver rota configurada
*/
public String getDirectionForDestination(String destination) {
return routing.get(destination);
}
/**
* Returns the traffic light controlling the given direction.
* Retorna o semáforo que controla uma determinada direção.
*
* @param direction The direction (e.g., "North").
* @return The {@link TrafficLight} object, or null if no light exists
* for that direction.
* @param direction a direção (ex: "Norte")
* @return o objeto {@link TrafficLight}, ou null se não existir
*/
public TrafficLight getTrafficLight(String direction) {
return trafficLights.get(direction);
}
/**
* Returns a list of all traffic lights managed by this intersection.
* Retorna uma lista com todos os semáforos desta interseção.
*
* @return A new {@link List} containing all {@link TrafficLight} objects.
* @return uma nova {@link List} com todos os semáforos
*/
public List<TrafficLight> getTrafficLights() {
// Return a copy to prevent external modification of the internal map's values
return new ArrayList<>(trafficLights.values());
}
/**
* Returns the total number of vehicles currently queued across *all*
* traffic lights at this intersection.
* Retorna o número total de veículos em fila em todos os semáforos.
* Usa Java Stream API para somar os tamanhos de todas as filas.
*
* @return The sum of all queue sizes.
* @return a soma dos tamanhos de todas as filas
*/
public int getTotalQueueSize() {
// Uses Java Stream API:
// 1. trafficLights.values().stream() - Get a stream of TrafficLight objects
// 2. .mapToInt(TrafficLight::getQueueSize) - Convert each light to its queue size (an int)
// 3. .sum() - Sum all the integers
return trafficLights.values().stream()
.mapToInt(TrafficLight::getQueueSize)
.sum();
}
// --- Stats and getters ---
/**
* @return The unique ID of this intersection.
* @return o identificador único desta interseção
*/
public String getId() {
return id;
}
/**
* @return The total number of vehicles that have arrived at this intersection.
* @return o número total de veículos que chegaram a esta interseção
*/
public int getTotalVehiclesReceived() {
return totalVehiclesReceived;
}
/**
* @return The total number of vehicles that have successfully
* departed from this intersection.
* @return o número total de veículos que partiram desta interseção
*/
public int getTotalVehiclesSent() {
return totalVehiclesSent;
}
/**
* Increments the counter for vehicles that have successfully departed.
* This is typically called by the {@link sd.engine.SimulationEngine}
* after a vehicle finishes crossing.
* Incrementa o contador de veículos que partiram com sucesso.
* Tipicamente chamado após um veículo completar a travessia.
*/
public void incrementVehiclesSent() {
totalVehiclesSent++;
}
/**
* @return The running average of vehicle waiting time at this intersection.
* @return a média do tempo de espera dos veículos nesta interseção
*/
public double getAverageWaitingTime() {
return averageWaitingTime;
}
/**
* Updates the running average waiting time with a new sample (a new
* vehicle's wait time).
* * Uses an incremental/weighted average formula:
* NewAvg = (OldAvg * (N-1) + NewValue) / N
* where N is the total number of vehicles sent.
* Atualiza a média do tempo de espera com uma nova amostra.
* Usa a fórmula: Nova Média = (Média Antiga * (N-1) + Novo Valor) / N
*
* @param newTime The waiting time (in seconds) of the vehicle that just
* departed.
* @param newTime tempo de espera (em segundos) do veículo que acabou de partir
*/
public void updateAverageWaitingTime(double newTime) {
// Avoid division by zero if this is called before any vehicle is sent
if (totalVehiclesSent > 0) {
averageWaitingTime = (averageWaitingTime * (totalVehiclesSent - 1) + newTime)
/ totalVehiclesSent;
} else if (totalVehiclesSent == 1) {
// This is the first vehicle
averageWaitingTime = newTime;
}
}
/**
* @return A string summary of the intersection's current state.
* @return representação textual do estado atual da interseção
*/
@Override
public String toString() {

View File

@@ -1,55 +1,45 @@
package sd.model;
import java.io.Serializable;
import java.util.UUID;
import sd.protocol.MessageProtocol;
/**
* Represents a message exchanged between processes in the distributed simulation.
* Each message has a unique ID, a type, a sender, a destination, and a payload.
* This class implements {@link Serializable} to allow transmission over the network.
* Representa uma mensagem trocada entre processos na simulação distribuída.
*
* <p>Cada mensagem tem um ID único, tipo, remetente, destino e payload.
* Implementa {@link MessageProtocol} que estende Serializable para transmissão pela rede.</p>
*/
public class Message implements Serializable {
public class Message implements MessageProtocol {
private static final long serialVersionUID = 1L;
/**
* Unique identifier for this message.
*/
/** Identificador único desta mensagem */
private final String messageId;
/**
* The type of this message (e.g., VEHICLE_TRANSFER, STATS_UPDATE).
*/
/** Tipo desta mensagem (ex: VEHICLE_TRANSFER, STATS_UPDATE) */
private final MessageType type;
/**
* Identifier of the process that sent this message.
*/
/** Identificador do processo que enviou esta mensagem */
private final String senderId;
/**
* Identifier of the destination process. Can be null for broadcast messages.
*/
/** Identificador do processo de destino (pode ser null para broadcast) */
private final String destinationId;
/**
* The actual data being transmitted. Type depends on the message type.
*/
/** Dados a serem transmitidos (o tipo depende do tipo de mensagem) */
private final Object payload;
/**
* Timestamp when this message was created (simulation time or real time).
*/
/** Timestamp de criação da mensagem (tempo de simulação ou real) */
private final long timestamp;
/**
* Creates a new message with all parameters.
* Cria uma nova mensagem com todos os parâmetros.
*
* @param type The message type
* @param senderId The ID of the sending process
* @param destinationId The ID of the destination process (null for broadcast)
* @param payload The message payload
* @param timestamp The timestamp of message creation
* @param type tipo da mensagem
* @param senderId ID do processo remetente
* @param destinationId ID do processo de destino (null para broadcast)
* @param payload conteúdo da mensagem
* @param timestamp timestamp de criação da mensagem
*/
public Message(MessageType type, String senderId, String destinationId,
Object payload, long timestamp) {
@@ -62,23 +52,23 @@ public class Message implements Serializable {
}
/**
* Creates a new message with current system time as timestamp.
* Cria uma nova mensagem usando o tempo atual do sistema como timestamp.
*
* @param type The message type
* @param senderId The ID of the sending process
* @param destinationId The ID of the destination process
* @param payload The message payload
* @param type tipo da mensagem
* @param senderId ID do processo remetente
* @param destinationId ID do processo de destino
* @param payload conteúdo da mensagem
*/
public Message(MessageType type, String senderId, String destinationId, Object payload) {
this(type, senderId, destinationId, payload, System.currentTimeMillis());
}
/**
* Creates a broadcast message (no specific destination).
* Cria uma mensagem de broadcast (sem destino específico).
*
* @param type The message type
* @param senderId The ID of the sending process
* @param payload The message payload
* @param type tipo da mensagem
* @param senderId ID do processo remetente
* @param payload conteúdo da mensagem
*/
public Message(MessageType type, String senderId, Object payload) {
this(type, senderId, null, payload, System.currentTimeMillis());
@@ -132,6 +122,17 @@ public class Message implements Serializable {
return (T) payload;
}
// Impl MessageProtocol interface
@Override
public String getSourceNode() {
return senderId;
}
@Override
public String getDestinationNode() {
return destinationId;
}
@Override
public String toString() {
return String.format("Message[id=%s, type=%s, from=%s, to=%s, timestamp=%d]",

View File

@@ -1,81 +1,49 @@
package sd.model;
/**
* Enumeration representing all possible message types for distributed communication.
* These types are used for inter-process communication between different components
* of the distributed traffic simulation system.
* Enumeração que representa todos os tipos de mensagens possíveis para
* comunicação distribuída.
* Estes tipos são usados para a comunicação entre processos dos diferentes
* componentes
* do sistema de simulação de tráfego distribuído.
*/
public enum MessageType {
/**
* Message to transfer a vehicle between intersections or processes.
* Payload: Vehicle object with current state
* Mensagem para transferir um veículo entre interseções ou processos.
* Payload: Objeto Vehicle com o estado atual
*/
VEHICLE_TRANSFER,
/**
* Message to update statistics across the distributed system.
* Payload: Statistics data (waiting times, queue sizes, etc.)
* Mensagem para atualizar estatísticas em todo o sistema distribuído.
* Payload: Dados estatísticos (tempos de espera, tamanhos de fila, etc.)
*/
STATS_UPDATE,
/**
* Message to synchronize traffic light states between processes.
* Payload: TrafficLight state and timing information
* Mensagem para sincronizar a hora de início da simulação em todos os
* processos.
* Payload: Timestamp de início (long milissegundos)
*/
TRAFFIC_LIGHT_SYNC,
SIMULATION_START,
/**
* Heartbeat message to check if a process is alive.
* Payload: Process ID and timestamp
*/
HEARTBEAT,
/**
* Request to join the distributed simulation.
* Payload: Process information and capabilities
*/
JOIN_REQUEST,
/**
* Response to a join request.
* Payload: Acceptance status and configuration
*/
JOIN_RESPONSE,
/**
* Message to notify about a new vehicle generation.
* Payload: Vehicle generation parameters
* Mensagem para notificar sobre a geração de um novo veículo.
* Payload: Parâmetros de geração do veículo
*/
VEHICLE_SPAWN,
/**
* Message to request the current state of an intersection.
* Payload: Intersection ID
*/
STATE_REQUEST,
/**
* Response containing the current state of an intersection.
* Payload: Complete intersection state
*/
STATE_RESPONSE,
/**
* Message to signal shutdown of a process.
* Payload: Process ID and reason
* Mensagem para sinalizar o encerramento de um processo.
* Payload: ID do processo e motivo
*/
SHUTDOWN,
/**
* Acknowledgment message for reliable communication.
* Payload: Message ID being acknowledged
* Mensagem para alterar a política de roteamento durante a simulação.
* Payload: String com o nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
*/
ACK,
/**
* Error message to report problems in the distributed system.
* Payload: Error description and context
*/
ERROR
ROUTING_POLICY_CHANGE,
}

View File

@@ -1,315 +1,269 @@
package sd.model;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Represents a single traffic light controlling one direction at an intersection.
* * Each light maintains its own queue of {@link Vehicle} objects and
* alternates between {@link TrafficLightState#GREEN} and
* {@link TrafficLightState#RED} states.
* * This class is designed to be thread-safe for a potential concurrent
* simulation (though the current engine {@link sd.engine.SimulationEngine}
* is single-threaded). It uses a {@link ReentrantLock} to protect its
* internal state (the queue and the light state) from simultaneous access.
* * The {@link Condition} variables ({@code vehicleAdded}, {@code lightGreen})
* are included for a concurrent model where:
* - A "vehicle" thread might wait on {@code lightGreen} until the light changes.
* - A "controller" thread might wait on {@code vehicleAdded} to know when to
* process a queue.
* (Note: These Conditions are *not* used by the current discrete-event engine).
* Representa um semáforo numa interseção.
*
* <p>Cada semáforo controla uma direção específica e mantém uma fila de veículos à espera.
* Alterna entre os estados VERDE e VERMELHO de acordo com a temporização configurada.</p>
*
* <p><strong>Thread-safety:</strong> Usa locks para permitir acesso concorrente seguro entre
* a thread de processamento de eventos e as threads de I/O de rede.</p>
*/
public class TrafficLight {
// --- Identity and configuration ---
/**
* Unique identifier for the light (e.g., "Cr1-N").
*/
/** Identificador único do semáforo (ex: "Cr1-N") */
private final String id;
/**
* The direction this light controls (e.g., "North", "South").
*/
/** Direção que este semáforo controla (ex: "Norte", "Sul") */
private final String direction;
/**
* The current state of the light (GREEN or RED).
*/
/** Estado atual do semáforo (VERDE ou VERMELHO) */
private TrafficLightState state;
// --- Vehicle management ---
/**
* The queue of vehicles waiting at this light.
* {@link LinkedList} is used as it's a standard {@link Queue} implementation.
*/
/** Fila de veículos à espera neste semáforo */
private final Queue<Vehicle> queue;
// --- Synchronization primitives (for thread-safety) ---
/**
* A lock to protect all mutable state ({@link #queue} and {@link #state})
* from concurrent access. Any method reading or writing these fields
* *must* acquire this lock first.
* Lock para proteger o estado mutável ({@link #queue} e {@link #state})
* de acesso concorrente.
*/
private final Lock lock;
/**
* A condition variable for a potential concurrent model.
* It could be used to signal threads (e.g., a controller) that
* a new vehicle has been added to the queue.
* (Not used in the current discrete-event engine).
*/
/** Variável de condição para sinalizar adição de veículos (uso futuro) */
private final Condition vehicleAdded;
/**
* A condition variable for a potential concurrent model.
* It could be used to signal waiting vehicle threads that the
* light has just turned GREEN.
* (Not used in the current discrete-event engine).
*/
/** Variável de condição para sinalizar que o semáforo ficou verde (uso futuro) */
private final Condition lightGreen;
// --- Timing configuration ---
/**
* The duration (in seconds) this light stays GREEN.
*/
/** Duração (segundos) que o semáforo permanece VERDE */
private double greenTime;
/**
* The duration (in seconds) this light stays RED.
*/
/** Duração (segundos) que o semáforo permanece VERMELHO */
private double redTime;
// --- Statistics ---
/**
* Counter for the total number of vehicles that have
* been dequeued (processed) by this light.
*/
/** Número total de veículos processados por este semáforo */
private int totalVehiclesProcessed;
/**
* Constructs a new TrafficLight.
* Regista quando os veículos chegam ao semáforo para cálculo do tempo de espera.
* Mapeia ID do veículo para tempo de simulação de chegada (segundos).
*/
private final Map<String, Double> vehicleArrivalTimes;
/**
* Cria um novo semáforo.
*
* @param id The unique ID (e.g., "Cr1-N").
* @param direction The direction (e.g., "North").
* @param greenTime The duration of the GREEN state in seconds.
* @param redTime The duration of the RED state in seconds.
* @param id identificador único (ex: "Cr1-N")
* @param direction direção controlada (ex: "Norte")
* @param greenTime duração do estado VERDE em segundos
* @param redTime duração do estado VERMELHO em segundos
*/
public TrafficLight(String id, String direction, double greenTime, double redTime) {
this.id = id;
this.direction = direction;
this.state = TrafficLightState.RED; // All lights start RED
this.state = TrafficLightState.RED;
this.queue = new LinkedList<>();
// Initialize synchronization objects
this.lock = new ReentrantLock();
this.vehicleAdded = lock.newCondition();
this.lightGreen = lock.newCondition();
this.greenTime = greenTime;
this.redTime = redTime;
this.vehicleArrivalTimes = new HashMap<>();
this.totalVehiclesProcessed = 0;
}
/**
* Adds a vehicle to the *end* of the waiting queue.
* This method is thread-safe.
*
* @param vehicle The {@link Vehicle} to add.
* Coloca um veículo na fila deste semáforo.
*
* Registamos a hora de chegada para podermos calcular mais tarde quanto tempo o
* veículo esperou.
*
* @param vehicle O veículo que chega ao semáforo.
* @param simulationTime O tempo de simulação atual (em segundos).
*/
public void addVehicle(Vehicle vehicle) {
lock.lock(); // Acquire the lock
public void addVehicle(Vehicle vehicle, double simulationTime) {
lock.lock();
try {
queue.offer(vehicle); // Add vehicle to queue
vehicleAdded.signalAll(); // Signal (for concurrent models)
queue.offer(vehicle);
vehicleArrivalTimes.put(vehicle.getId(), simulationTime);
vehicleAdded.signalAll();
} finally {
lock.unlock(); // Always release the lock
lock.unlock();
}
}
/**
* Removes and returns the {@link Vehicle} from the *front* of the queue.
* * This only succeeds if:
* 1. The light's state is {@link TrafficLightState#GREEN}.
* 2. The queue is not empty.
* * If these conditions are not met, it returns {@code null}.
* This method is thread-safe.
*
* @return The {@link Vehicle} at the front of the queue, or {@code null}
* if the light is RED or the queue is empty.
* Remove um veículo da fila para travessia.
*
* <p>Só remove se:</p>
* <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() {
lock.lock(); // Acquire the lock
public Vehicle removeVehicle(double simulationTime) {
lock.lock();
try {
if (state == TrafficLightState.GREEN && !queue.isEmpty()) {
Vehicle vehicle = queue.poll(); // Remove vehicle from queue
Vehicle vehicle = queue.poll();
if (vehicle != null) {
totalVehiclesProcessed++;
Double arrivalTime = vehicleArrivalTimes.remove(vehicle.getId());
if (arrivalTime != null) {
double waitTimeSeconds = simulationTime - arrivalTime;
vehicle.addWaitingTime(waitTimeSeconds);
}
}
return vehicle;
}
return null; // Light is RED or queue is empty
return null;
} finally {
lock.unlock(); // Always release the lock
lock.unlock();
}
}
/**
* Changes the lights state (e.g., RED -> GREEN).
* If the new state is GREEN, it signals any waiting threads
* (for a potential concurrent model).
* This method is thread-safe.
*
* @param newState The {@link TrafficLightState} to set.
* Muda o estado do semáforo.
*
* @param newState novo estado (VERDE ou VERMELHO)
*/
public void changeState(TrafficLightState newState) {
lock.lock(); // Acquire the lock
lock.lock();
try {
this.state = newState;
if (newState == TrafficLightState.GREEN) {
lightGreen.signalAll(); // Signal (for concurrent models)
lightGreen.signalAll();
}
} finally {
lock.unlock(); // Always release the lock
lock.unlock();
}
}
/**
* Returns how many vehicles are currently in the queue.
* This method is thread-safe.
* * @return The size of the queue.
* Retorna quantos veículos estão atualmente na fila.
* Método thread-safe.
*
* @return tamanho da fila
*/
public int getQueueSize() {
lock.lock(); // Acquire the lock
lock.lock();
try {
return queue.size();
} finally {
lock.unlock(); // Always release the lock
lock.unlock();
}
}
/**
* Checks whether the queue is empty.
* This method is thread-safe.
* Verifica se a fila está vazia.
* Método thread-safe.
*
* @return {@code true} if the queue has no vehicles, {@code false} otherwise.
* @return {@code true} se não houver veículos, {@code false} caso contrário
*/
public boolean isQueueEmpty() {
lock.lock(); // Acquire the lock
lock.lock();
try {
return queue.isEmpty();
} finally {
lock.unlock(); // Always release the lock
lock.unlock();
}
}
// --- Getters & Setters ---
/**
* @return The unique ID of this light (e.g., "Cr1-N").
*/
/** @return identificador único do semáforo */
public String getId() {
return id;
}
/**
* @return The direction this light controls (e.g., "North").
*/
/** @return direção controlada por este semáforo */
public String getDirection() {
return direction;
}
/**
* Gets the current state of the light (GREEN or RED).
* This method is thread-safe.
* Obtém o estado atual do semáforo.
* Método thread-safe.
*
* @return The current {@link TrafficLightState}.
* @return estado atual (VERDE ou VERMELHO)
*/
public TrafficLightState getState() {
lock.lock(); // Acquire the lock
lock.lock();
try {
return state;
} finally {
lock.unlock(); // Always release the lock
lock.unlock();
}
}
/**
* @return The configured GREEN light duration in seconds.
*/
/** @return duração configurada do sinal verde em segundos */
public double getGreenTime() {
return greenTime;
}
/**
* Sets the GREEN light duration.
* @param greenTime The new duration in seconds.
* Define a duração do sinal verde.
*
* @param greenTime nova duração em segundos
*/
public void setGreenTime(double greenTime) {
this.greenTime = greenTime;
}
/**
* @return The configured RED light duration in seconds.
*/
/** @return duração configurada do sinal vermelho em segundos */
public double getRedTime() {
return redTime;
}
/**
* Sets the RED light duration.
* @param redTime The new duration in seconds.
* Define a duração do sinal vermelho.
*
* @param redTime nova duração em segundos
*/
public void setRedTime(double redTime) {
this.redTime = redTime;
}
/**
* @return The total number of vehicles processed (dequeued) by this light.
*/
/** @return número total de veículos processados por este semáforo */
public int getTotalVehiclesProcessed() {
// Note: This read is not locked, assuming it's okay
// for it to be "eventually consistent" for stats.
// For strict accuracy, it should also be locked.
return totalVehiclesProcessed;
}
/**
* @return The {@link Lock} object for advanced synchronization.
*/
/** @return objeto {@link Lock} para sincronização avançada */
public Lock getLock() {
return lock;
}
/**
* @return The {@link Condition} for vehicle additions.
*/
/** @return condição para adição de veículos */
public Condition getVehicleAdded() {
return vehicleAdded;
}
/**
* @return The {@link Condition} for the light turning green.
*/
/** @return condição para semáforo ficar verde */
public Condition getLightGreen() {
return lightGreen;
}
/**
* @return A string summary of the light's current state.
*/
/** @return representação textual do estado atual do semáforo */
@Override
public String toString() {
return String.format(
"TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}",
id, direction, getState(), getQueueSize() // Use getters for thread-safety
"TrafficLight{id='%s', direction='%s', state=%s, queueSize=%d}",
id, direction, getState(), getQueueSize()
);
}
}

View File

@@ -1,17 +1,13 @@
package sd.model;
/**
* Enumeration representing the two possible states of a {@link TrafficLight}.
* Estados possíveis de um semáforo ({@link TrafficLight}).
*/
public enum TrafficLightState {
/**
* The light is GREEN, allowing vehicles to pass (be dequeued).
*/
/** Sinal verde - veículos podem passar */
GREEN,
/**
* The light is RED, blocking vehicles (they remain in the queue).
*/
/** Sinal vermelho - veículos aguardam na fila */
RED
}

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
@@ -19,8 +18,8 @@ import sd.serialization.SerializerFactory;
/**
* Wrapper class that simplifies communication via Sockets.
* Includes connection retry logic for robustness.
* Simplifica comunicação via sockets.
* Inclui lógica de retry para robustez.
*/
public class SocketConnection implements Closeable {
@@ -29,22 +28,20 @@ public class SocketConnection implements Closeable {
private final InputStream inputStream;
private final MessageSerializer serializer;
// --- Configuration for Retry Logic ---
/** Maximum number of connection attempts. */
/** Número máximo de tentativas de ligação */
private static final int MAX_RETRIES = 5;
/** Delay between retry attempts in milliseconds. */
/** Atraso entre tentativas (milissegundos) */
private static final long RETRY_DELAY_MS = 1000;
/**
* Constructor for the "Client" (who initiates the connection).
* Tries to connect to a process that is already listening (Server).
* Includes retry logic in case of initial connection failure.
* Construtor do cliente que inicia a ligação.
* Tenta ligar a um servidor já em escuta, com retry.
*
* @param host The host address (e.g., "localhost" from your simulation.properties)
* @param port The port (e.g., 8001 from your simulation.properties)
* @throws IOException If connection fails after all retries.
* @throws UnknownHostException If the host is not found (this error usually doesn't need retry).
* @throws InterruptedException If the thread is interrupted while waiting between retries.
* @param host endereço do host (ex: "localhost")
* @param port número da porta
* @throws IOException se falhar após todas as tentativas
* @throws UnknownHostException se o host não for encontrado
* @throws InterruptedException se a thread for interrompida
*/
public SocketConnection(String host, int port) throws IOException, UnknownHostException, InterruptedException {
Socket tempSocket = null;
@@ -127,7 +124,7 @@ public class SocketConnection implements Closeable {
* @param message The "envelope" (which contains the Vehicle) to be sent.
* @throws IOException If writing to the stream fails or socket is not connected.
*/
public void sendMessage(MessageProtocol message) throws IOException {
public synchronized void sendMessage(MessageProtocol message) throws IOException {
if (socket == null || !socket.isConnected()) {
throw new IOException("Socket is not connected");
}
@@ -172,8 +169,8 @@ public class SocketConnection implements Closeable {
byte[] data = new byte[length];
dataIn.readFully(data);
// Deserialize do JSON
return serializer.deserialize(data, MessageProtocol.class);
// Deserialize do JSON - use concrete Message class, not interface
return serializer.deserialize(data, sd.model.Message.class);
} catch (SerializationException e) {
throw new IOException("Failed to deserialize message", e);

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,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

@@ -3,84 +3,82 @@ package sd.util;
import java.util.Random;
/**
* Utility class for generating random values used throughout the simulation.
* * Provides static methods for:
* - Generating exponentially distributed intervals (for Poisson processes).
* - Generating random integers and doubles in a range.
* - Making decisions based on probability.
* - Choosing random elements from an array.
* * It uses a single, static {@link Random} instance.
* Utilitário para gerar valores aleatórios usados na simulação.
*
* <p>Fornece métodos estáticos para:</p>
* <ul>
* <li>Gerar intervalos exponencialmente distribuídos (processos de Poisson)</li>
* <li>Gerar inteiros e doubles aleatórios num intervalo</li>
* <li>Tomar decisões baseadas em probabilidade</li>
* <li>Escolher elementos aleatórios de um array</li>
* </ul>
*
* <p>Usa uma única instância estática de {@link Random}.</p>
*/
public class RandomGenerator {
/**
* The single, shared Random instance for the entire simulation.
*/
/** Instância partilhada de Random para toda a simulação */
private static final Random random = new Random();
/**
* Returns a random time interval that follows an exponential distribution.
* * This is a key component for modeling a Poisson process, where the
* *inter-arrival times* (time between events) are exponentially distributed.
* The formula used is the inverse transform sampling method:
* {@code Time = -ln(1 - U) / λ}
* where U is a uniform random number [0, 1) and λ (lambda) is the
* average arrival rate.
* Retorna um intervalo de tempo que segue uma distribuição exponencial.
*
* <p>Componente essencial para modelar processos de Poisson, onde os
* tempos entre chegadas seguem uma distribuição exponencial.</p>
*
* <p>Fórmula: {@code Time = -ln(1 - U) / λ}<br>
* onde U é um número aleatório uniforme [0, 1) e λ (lambda) é a taxa média de chegada.</p>
*
* @param lambda The average arrival rate (λ) (e.g., 0.5 vehicles per second).
* @return The time interval (in seconds) until the next arrival.
* @param lambda taxa média de chegada λ (ex: 0.5 veículos por segundo)
* @return intervalo de tempo (segundos) até à próxima chegada
*/
public static double generateExponentialInterval(double lambda) {
// Math.log is the natural logarithm (ln)
// random.nextDouble() returns a value in [0.0, 1.0)
return Math.log(1 - random.nextDouble()) / -lambda;
}
/**
* Returns a random integer between {@code min} and {@code max}, inclusive.
* Retorna um inteiro aleatório entre {@code min} e {@code max}, inclusive.
*
* @param min The minimum possible value.
* @param max The maximum possible value.
* @return A random integer in the range [min, max].
* @param min valor mínimo possível
* @param max valor máximo possível
* @return inteiro aleatório no intervalo [min, max]
*/
public static int generateRandomInt(int min, int max) {
// random.nextInt(N) returns a value from 0 to N-1
// (max - min + 1) is the total number of integers in the range
// + min offsets the range
return random.nextInt(max - min + 1) + min;
}
/**
* Returns a random double between {@code min} (inclusive) and {@code max} (exclusive).
* Retorna um double aleatório entre {@code min} (inclusive) e {@code max} (exclusivo).
*
* @param min The minimum possible value.
* @param max The maximum possible value.
* @return A random double in the range [min, max).
* @param min valor mínimo possível
* @param max valor máximo possível
* @return double aleatório no intervalo [min, max)
*/
public static double generateRandomDouble(double min, double max) {
return min + (max - min) * random.nextDouble();
}
/**
* Returns {@code true} with a given probability.
* * This is useful for making weighted decisions. For example,
* {@code occursWithProbability(0.3)} will return {@code true}
* approximately 30% of the time.
* Retorna {@code true} com uma dada probabilidade.
*
* <p>Útil para tomar decisões ponderadas. Por exemplo,
* {@code occursWithProbability(0.3)} retorna {@code true}
* aproximadamente 30% das vezes.</p>
*
* @param probability A value between 0.0 (never) and 1.0 (always).
* @return {@code true} or {@code false}, based on the probability.
* @param probability valor entre 0.0 (nunca) e 1.0 (sempre)
* @return {@code true} ou {@code false}, baseado na probabilidade
*/
public static boolean occursWithProbability(double probability) {
return random.nextDouble() < probability;
}
/**
* Picks a random element from the given array.
* Escolhe um elemento aleatório do array fornecido.
*
* @param <T> The generic type of the array.
* @param array The array to choose from.
* @return A randomly selected element from the array.
* @throws IllegalArgumentException if the array is null or empty.
* @param <T> tipo genérico do array
* @param array array de onde escolher
* @return elemento selecionado aleatoriamente
* @throws IllegalArgumentException se o array for null ou vazio
*/
public static <T> T chooseRandom(T[] array) {
if (array == null || array.length == 0) {
@@ -90,12 +88,13 @@ public class RandomGenerator {
}
/**
* Sets the seed of the shared random number generator.
* This is extremely useful for debugging and testing, as it allows
* the simulation to be run multiple times with the *exact same*
* sequence of "random" events, making the results reproducible.
* Define a seed do gerador de números aleatórios partilhado.
*
* <p>Extremamente útil para debugging e testes, pois permite executar
* a simulação múltiplas vezes com a mesma sequência de eventos "aleatórios",
* tornando os resultados reproduzíveis.</p>
*
* @param seed The seed to use.
* @param seed seed a usar
*/
public static void setSeed(long seed) {
random.setSeed(seed);

View File

@@ -1,379 +0,0 @@
package sd.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import sd.config.SimulationConfig;
import sd.model.Intersection;
import sd.model.Vehicle;
import sd.model.VehicleType;
/**
* Collects, manages, and reports statistics throughout the simulation.
* * This class acts as the central bookkeeper for simulation metrics. It tracks:
* - Overall system statistics (total vehicles, completion time, wait time).
* - Per-vehicle-type statistics (counts, average wait time by type).
* - Per-intersection statistics (arrivals, departures).
* * It also maintains "in-flight" data, such as the arrival time of a
* vehicle at its *current* intersection, which is necessary to
* calculate waiting time when the vehicle later departs.
*/
public class StatisticsCollector {
// --- Vehicle tracking (for in-flight vehicles) ---
/**
* Tracks the simulation time when a vehicle arrives at its *current* intersection.
* This is used later to calculate waiting time (Depart_Time - Arrive_Time).
* Key: Vehicle ID (String)
* Value: Arrival Time (Double)
*/
private final Map<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 {@link sd.engine.SimulationEngine}
* during a {@code VEHICLE_GENERATION} event.
*
* @param vehicle The {@link Vehicle} that was just created.
* @param currentTime The simulation time of the event.
*/
public void recordVehicleGeneration(Vehicle vehicle, double currentTime) {
totalVehiclesGenerated++;
// Track by vehicle type
VehicleType type = vehicle.getType();
vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
// Initialize history tracking for this vehicle
vehicleIntersectionHistory.put(vehicle.getId(), new ArrayList<>());
}
/**
* Records that a vehicle has arrived at an intersection queue.
* This is called by the {@link sd.engine.SimulationEngine}
* during a {@code VEHICLE_ARRIVAL} event.
*
* @param vehicle The {@link Vehicle} that arrived.
* @param intersectionId The ID of the intersection it arrived at.
* @param currentTime The simulation time of the arrival.
*/
public void recordVehicleArrival(Vehicle vehicle, String intersectionId, double currentTime) {
// Store arrival time - this is the "start waiting" time
vehicleArrivalTimes.put(vehicle.getId(), currentTime);
// Track intersection history
List<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 {@link sd.engine.SimulationEngine}
* when a vehicle reaches destination "S".
*
* @param vehicle The {@link Vehicle} that is exiting.
* @param currentTime The simulation time of the exit.
*/
public void recordVehicleExit(Vehicle vehicle, double currentTime) {
totalVehiclesCompleted++;
// Calculate and aggregate total system time
double systemTime = vehicle.getTotalTravelTime(currentTime);
totalSystemTime += systemTime;
// Aggregate waiting time
double waitTime = vehicle.getTotalWaitingTime();
totalWaitingTime += waitTime;
// Aggregate waiting time by vehicle type
VehicleType type = vehicle.getType();
vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);
// Clean up tracking maps to save memory
vehicleArrivalTimes.remove(vehicle.getId());
vehicleIntersectionHistory.remove(vehicle.getId());
}
/**
* Gets the time a vehicle arrived at its *current* intersection.
* This is used by the {@link sd.engine.SimulationEngine} to calculate
* wait time just before the vehicle crosses.
*
* @param vehicle The {@link Vehicle} to check.
* @return The arrival time, or 0.0 if not found.
*/
public double getArrivalTime(Vehicle vehicle) {
return vehicleArrivalTimes.getOrDefault(vehicle.getId(), 0.0);
}
/**
* Prints a "snapshot" of the current simulation statistics.
* This is called periodically by the {@link sd.engine.SimulationEngine}
* during a {@code STATISTICS_UPDATE} event.
*
* @param intersections A map of all intersections (to get queue data).
* @param currentTime The current simulation time.
*/
public void printCurrentStatistics(Map<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 void recordDeparture() {
totalDepartures++;
}
public int getTotalArrivals() {
return totalArrivals;
}
public int getTotalDepartures() {
return totalDepartures;
}
}
// --- Public Getters for Final Statistics ---
/**
* @return Total vehicles generated during the simulation.
*/
public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated;
}
/**
* @return Total vehicles that completed their route.
*/
public int getTotalVehiclesCompleted() {
return totalVehiclesCompleted;
}
/**
* @return The sum of all travel times for *completed* vehicles.
*/
public double getTotalSystemTime() {
return totalSystemTime;
}
/**
* @return The sum of all waiting times for *completed* vehicles.
*/
public double getTotalWaitingTime() {
return totalWaitingTime;
}
/**
* @return The average travel time for *completed* vehicles.
*/
public double getAverageSystemTime() {
return totalVehiclesCompleted > 0 ? totalSystemTime / totalVehiclesCompleted : 0.0;
}
/**
* @return The average waiting time for *completed* vehicles.
*/
public double getAverageWaitingTime() {
return totalVehiclesCompleted > 0 ? totalWaitingTime / totalVehiclesCompleted : 0.0;
}
}

View File

@@ -1,145 +1,115 @@
package sd.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import sd.config.SimulationConfig;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.routing.RouteSelector;
/**
* Generates vehicles for the simulation.
* * This class is responsible for two key tasks:
* 1. Determining *when* the next vehicle should arrive, based on the
* arrival model (POISSON or FIXED) from the {@link SimulationConfig}.
* 2. Creating a new {@link Vehicle} object with a randomly selected
* type (e.g., BIKE, LIGHT) and a randomly selected route.
* * Routes are predefined and organized by entry point (E1, E2, E3).
* Gera veículos para a simulação.
*
* <p>Esta classe é responsável por duas tarefas principais:</p>
* <ol>
* <li>Determinar <em>quando</em> o próximo veículo deve chegar, baseado no
* modelo de chegada (POISSON ou FIXED) da {@link SimulationConfig}</li>
* <li>Criar um novo objeto {@link Vehicle} com tipo e rota selecionados pela
* política de roteamento configurada ({@link RouteSelector})</li>
* </ol>
*
* <p>As rotas são selecionadas usando uma política de roteamento que pode ser:
* aleatória, caminho mais curto, menor congestionamento, etc.</p>
*/
public class VehicleGenerator {
private final SimulationConfig config;
private final String arrivalModel;
private final double arrivalRate; // Lambda (λ) for POISSON
private final double fixedInterval; // Interval for FIXED
/** Lambda (λ) para modelo POISSON */
private final double arrivalRate;
/** Intervalo para modelo FIXED */
private final double fixedInterval;
// --- Predefined Routes ---
// These lists store all possible routes, grouped by where they start.
/** Routes starting from entry point E1. */
private final List<RouteWithProbability> e1Routes;
/** Routes starting from entry point E2. */
private final List<RouteWithProbability> e2Routes;
/** Routes starting from entry point E3. */
private final List<RouteWithProbability> e3Routes;
/** Política de roteamento usada para selecionar rotas */
private RouteSelector routeSelector;
/**
* Constructs a new VehicleGenerator.
* It reads the necessary configuration and initializes the
* predefined routes.
* Cria um novo gerador de veículos com a política de roteamento especificada.
* Lê a configuração necessária.
*
* @param config The {@link SimulationConfig} object.
* @param config objeto de {@link SimulationConfig}
* @param routeSelector política de roteamento a usar para selecionar rotas
*/
public VehicleGenerator(SimulationConfig config) {
public VehicleGenerator(SimulationConfig config, RouteSelector routeSelector) {
this.config = config;
this.routeSelector = routeSelector;
// Cache configuration values for performance
this.arrivalModel = config.getArrivalModel();
this.arrivalRate = config.getArrivalRate();
this.fixedInterval = config.getFixedArrivalInterval();
// Initialize route lists
this.e1Routes = new ArrayList<>();
this.e2Routes = new ArrayList<>();
this.e3Routes = new ArrayList<>();
initializePossibleRoutes();
}
/**
* Defines all possible routes that vehicles can take, organized by
* their entry point (E1, E2, E3). Each route is given a
* probability, which determines how often it's chosen.
*/
private void initializePossibleRoutes() {
// E1 routes (Starts at Cr1)
e1Routes.add(new RouteWithProbability(
Arrays.asList("Cr1", "Cr4", "Cr5", "S"), 0.34)); // E1 -> Cr1 -> Cr4 -> Cr5 -> Exit
e1Routes.add(new RouteWithProbability(
Arrays.asList("Cr1", "Cr2", "Cr5", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr5 -> Exit
e1Routes.add(new RouteWithProbability(
Arrays.asList("Cr1", "Cr2", "Cr3", "S"), 0.33)); // E1 -> Cr1 -> Cr2 -> Cr3 -> Exit
// E2 routes (Starts at Cr2)
e2Routes.add(new RouteWithProbability(
Arrays.asList("Cr2", "Cr5", "S"), 0.34)); // E2 -> Cr2 -> Cr5 -> Exit
e2Routes.add(new RouteWithProbability(
Arrays.asList("Cr2", "Cr3", "S"), 0.33)); // E2 -> Cr2 -> Cr3 -> Exit
e2Routes.add(new RouteWithProbability(
Arrays.asList("Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E2 -> Cr2 -> ... -> Exit
// E3 routes (Starts at Cr3)
e3Routes.add(new RouteWithProbability(
Arrays.asList("Cr3", "S"), 0.34)); // E3 -> Cr3 -> Exit
e3Routes.add(new RouteWithProbability(
Arrays.asList("Cr3", "Cr2", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> Cr2 -> Cr5 -> Exit
e3Routes.add(new RouteWithProbability(
Arrays.asList("Cr3", "Cr2", "Cr1", "Cr4", "Cr5", "S"), 0.33)); // E3 -> Cr3 -> ... -> Exit
}
/**
* Calculates the *absolute* time of the next vehicle arrival
* based on the configured model.
* * @param currentTime The current simulation time, used as the base.
* @return The absolute time (e.g., {@code currentTime + interval})
* when the next vehicle should be generated.
* Calcula o tempo <em>absoluto</em> da próxima chegada de veículo
* baseado no modelo configurado.
*
* @param currentTime tempo atual da simulação, usado como base
* @return tempo absoluto (ex: {@code currentTime + intervalo})
* em que o próximo veículo deve ser gerado
*/
public double getNextArrivalTime(double currentTime) {
if ("POISSON".equalsIgnoreCase(arrivalModel)) {
// For a Poisson process, the time *between* arrivals
// follows an exponential distribution.
double interval = RandomGenerator.generateExponentialInterval(arrivalRate);
return currentTime + interval;
} else {
// For a Fixed model, the interval is constant.
return currentTime + fixedInterval;
}
}
/**
* Generates a new {@link Vehicle} object.
* This involves:
* 1. Selecting a random {@link VehicleType} based on probabilities.
* 2. Selecting a random route (entry point + path) based on probabilities.
* Gera um novo objeto {@link Vehicle}.
*
* <p>Passos executados:</p>
* <ol>
* <li>Seleciona um {@link VehicleType} aleatório baseado em probabilidades</li>
* <li>Seleciona um ponto de entrada aleatório (E1, E2, E3)</li>
* <li>Usa a política de roteamento para escolher a rota</li>
* </ol>
*
* @param vehicleId The unique identifier for the new vehicle (e.g., "V123").
* @param entryTime The simulation time when this vehicle is being created.
* @return A new, configured {@link Vehicle} object.
* @param vehicleId identificador único do novo veículo (ex: "V123")
* @param entryTime tempo de simulação em que o veículo é criado
* @param queueSizes mapa com tamanho das filas (opcional, pode ser null)
* @return novo objeto {@link Vehicle} configurado
*/
public Vehicle generateVehicle(String vehicleId, double entryTime) {
public Vehicle generateVehicle(String vehicleId, double entryTime, Map<String, Integer> queueSizes) {
VehicleType type = selectVehicleType();
List<String> route = selectRandomRoute();
String entryPoint = selectRandomEntryPoint();
List<String> route = routeSelector.selectRoute(entryPoint, queueSizes);
return new Vehicle(vehicleId, type, entryTime, route);
}
/**
* Selects a {@link VehicleType} (BIKE, LIGHT, HEAVY) based on the
* probabilities defined in the {@link SimulationConfig}.
* * Uses a standard "cumulative probability" technique:
* 1. Get a random number {@code rand} from [0, 1).
* 2. If {@code rand < P(Bike)}, return BIKE.
* 3. Else if {@code rand < P(Bike) + P(Light)}, return LIGHT.
* 4. Else, return HEAVY.
* Seleciona um {@link VehicleType} (BIKE, LIGHT, HEAVY) baseado nas
* probabilidades definidas na {@link SimulationConfig}.
*
* <p>Usa técnica de "probabilidade cumulativa":</p>
* <ol>
* <li>Obtém número aleatório {@code rand} de [0, 1)</li>
* <li>Se {@code rand < P(Bike)}, retorna BIKE</li>
* <li>Senão se {@code rand < P(Bike) + P(Light)}, retorna LIGHT</li>
* <li>Caso contrário, retorna HEAVY</li>
* </ol>
*
* @return The selected {@link VehicleType}.
* @return tipo de veículo selecionado
*/
private VehicleType selectVehicleType() {
double bikeProbability = config.getBikeVehicleProbability();
double lightProbability = config.getLightVehicleProbability();
double heavyProbability = config.getHeavyVehicleProbability();
// Normalize probabilities in case they don't sum to exactly 1.0
double total = bikeProbability + lightProbability + heavyProbability;
if (total == 0) return VehicleType.LIGHT; // Avoid division by zero
bikeProbability /= total;
@@ -157,73 +127,46 @@ public class VehicleGenerator {
}
/**
* Selects a random route for a new vehicle.
* This is a two-step process:
* 1. Randomly select an entry point (E1, E2, or E3) with equal probability.
* 2. From the chosen entry point's list of routes, select one
* based on their defined probabilities (using cumulative probability).
* Seleciona aleatoriamente um ponto de entrada (E1, E2 ou E3).
* Cada ponto tem probabilidade igual (1/3).
*
* @return A {@link List} of strings representing the chosen route (e.g., ["Cr1", "Cr4", "S"]).
* @return ponto de entrada selecionado ("E1", "E2" ou "E3")
*/
private List<String> selectRandomRoute() {
// Step 1: Randomly select an entry point (E1, E2, or E3)
double entryRandom = Math.random();
List<RouteWithProbability> selectedRoutes;
private String selectRandomEntryPoint() {
double rand = Math.random();
if (entryRandom < 0.333) {
selectedRoutes = e1Routes;
} else if (entryRandom < 0.666) {
selectedRoutes = e2Routes;
if (rand < 0.333) {
return "E1";
} else if (rand < 0.666) {
return "E2";
} else {
selectedRoutes = e3Routes;
return "E3";
}
// Step 2: Select a route from the chosen list based on cumulative probabilities
double routeRand = Math.random();
double cumulative = 0.0;
for (RouteWithProbability routeWithProb : selectedRoutes) {
cumulative += routeWithProb.probability;
if (routeRand <= cumulative) {
// Return a *copy* of the route to prevent modification
return new ArrayList<>(routeWithProb.route);
}
}
// Fallback: This should only be reached if probabilities don't sum to 1
// (due to floating point errors)
return new ArrayList<>(selectedRoutes.get(0).route);
}
/**
* Altera dinamicamente o RouteSelector usado para gerar rotas.
* Permite mudar a política de roteamento durante a simulação.
*
* @param newRouteSelector novo seletor de rotas
*/
public void setRouteSelector(RouteSelector newRouteSelector) {
// Note: In Java, we can't directly modify the 'final' field,
// but we can create a new VehicleGenerator with the new selector.
// For this implementation, we'll need to remove 'final' from routeSelector.
// This is acceptable since we want dynamic policy changes.
throw new UnsupportedOperationException(
"VehicleGenerator is immutable. Use CoordinatorProcess.changeRoutingPolicy() instead."
);
}
/**
* @return A string providing information about the generator's configuration.
*/
public String getInfo() {
int totalRoutes = e1Routes.size() + e2Routes.size() + e3Routes.size();
return String.format(
"VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routes=%d (E1:%d, E2:%d, E3:%d)}",
arrivalModel, arrivalRate, fixedInterval, totalRoutes,
e1Routes.size(), e2Routes.size(), e3Routes.size()
"VehicleGenerator{model=%s, rate=%.2f, interval=%.2f, routeSelector=%s}",
arrivalModel, arrivalRate, fixedInterval, routeSelector.getClass().getSimpleName()
);
}
/**
* A private inner "struct-like" class to hold a route (a List of strings)
* and its associated selection probability.
*/
private static class RouteWithProbability {
final List<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

@@ -0,0 +1,46 @@
{
"intersections": [
{
"id": "Cr1",
"lights": ["East", "South"],
"routes": {
"Cr2": "East",
"Cr4": "South"
}
},
{
"id": "Cr2",
"lights": ["West", "East", "South"],
"routes": {
"Cr1": "West",
"Cr3": "East",
"Cr5": "South"
}
},
{
"id": "Cr3",
"lights": ["West", "South"],
"routes": {
"Cr2": "West",
"S": "South"
}
},
{
"id": "Cr4",
"lights": ["East", "North"],
"routes": {
"Cr1": "North",
"Cr5": "East"
}
},
{
"id": "Cr5",
"lights": ["East", "West", "North"],
"routes": {
"Cr2": "North",
"Cr4": "West",
"S": "East"
}
}
]
}

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 × 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 × 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 × 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 ===
# Total duration in seconds (3600 = 1 hour)
simulation.duration=60.0
simulation.duration=300
# Time scaling factor for visualization (real_seconds = sim_seconds * scale)
# 0 = instant (pure DES), 0.01 = 100x speed, 0.1 = 10x speed, 1.0 = real-time
simulation.time.scale=0.01
# Vehicle arrival model: FIXED or POISSON
simulation.arrival.model=POISSON
@@ -42,59 +46,44 @@ simulation.arrival.rate=0.5
# Fixed interval between arrivals (only used if model=FIXED)
simulation.arrival.fixed.interval=2.0
# Routing policy: RANDOM, SHORTEST_PATH, LEAST_CONGESTED
# RANDOM: selects routes with predefined probabilities (baseline)
# SHORTEST_PATH: always chooses the route with fewest intersections
# LEAST_CONGESTED: dynamically chooses routes to avoid congested areas
simulation.routing.policy=RANDOM
# === TRAFFIC LIGHT TIMINGS ===
# Format: trafficlight.<intersection>.<direction>.<state>=<seconds>
# Intersection 1
trafficlight.Cr1.North.green=30.0
trafficlight.Cr1.North.red=30.0
trafficlight.Cr1.South.green=30.0
trafficlight.Cr1.South.red=30.0
trafficlight.Cr1.East.green=30.0
trafficlight.Cr1.East.red=30.0
trafficlight.Cr1.West.green=30.0
trafficlight.Cr1.West.red=30.0
# Intersection 1 (Entry point - balanced)
trafficlight.Cr1.South.green=60.0
trafficlight.Cr1.South.red=5.0
trafficlight.Cr1.East.green=60.0
trafficlight.Cr1.East.red=5.0
# Intersection 2
trafficlight.Cr2.North.green=25.0
trafficlight.Cr2.North.red=35.0
trafficlight.Cr2.South.green=25.0
trafficlight.Cr2.South.red=35.0
trafficlight.Cr2.East.green=35.0
trafficlight.Cr2.East.red=25.0
trafficlight.Cr2.West.green=35.0
trafficlight.Cr2.West.red=25.0
# Intersection 2 (Main hub - shorter cycles, favor East-West)
trafficlight.Cr2.South.green=60.0
trafficlight.Cr2.South.red=5.0
trafficlight.Cr2.East.green=60.0
trafficlight.Cr2.East.red=5.0
trafficlight.Cr2.West.green=60.0
trafficlight.Cr2.West.red=5.0
# Intersection 3
trafficlight.Cr3.North.green=30.0
trafficlight.Cr3.North.red=30.0
trafficlight.Cr3.South.green=30.0
trafficlight.Cr3.South.red=30.0
trafficlight.Cr3.East.green=30.0
trafficlight.Cr3.East.red=30.0
trafficlight.Cr3.West.green=30.0
trafficlight.Cr3.West.red=30.0
# Intersection 3 (Path to exit - favor East)
trafficlight.Cr3.South.green=60.0
trafficlight.Cr3.South.red=5.0
trafficlight.Cr3.West.green=60.0
trafficlight.Cr3.West.red=5.0
# Intersection 4
trafficlight.Cr4.North.green=30.0
trafficlight.Cr4.North.red=30.0
trafficlight.Cr4.South.green=30.0
trafficlight.Cr4.South.red=30.0
trafficlight.Cr4.East.green=30.0
trafficlight.Cr4.East.red=30.0
trafficlight.Cr4.West.green=30.0
trafficlight.Cr4.West.red=30.0
# Intersection 4 (Favor East toward Cr5)
trafficlight.Cr4.East.green=60.0
trafficlight.Cr4.East.red=5.0
# Intersection 5 (Near exit - favor East)
trafficlight.Cr5.East.green=60.0
trafficlight.Cr5.East.red=5.0
# Intersection 5
trafficlight.Cr5.North.green=30.0
trafficlight.Cr5.North.red=30.0
trafficlight.Cr5.South.green=30.0
trafficlight.Cr5.South.red=30.0
trafficlight.Cr5.East.green=30.0
trafficlight.Cr5.East.red=30.0
trafficlight.Cr5.West.green=30.0
trafficlight.Cr5.West.red=30.0
# === VEHICLE CONFIGURATION ===
# Probability distribution for vehicle types (must sum to 1.0)
@@ -103,11 +92,19 @@ vehicle.probability.light=0.6
vehicle.probability.heavy=0.2
# Average crossing times (in seconds)
vehicle.crossing.time.bike=1.5
vehicle.crossing.time.bike=1.0
vehicle.crossing.time.light=2.0
vehicle.crossing.time.heavy=4.0
# Travel times between intersections (in seconds)
# Base time for light vehicles (cars)
vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time
vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=4.0
# === STATISTICS ===
# Interval between dashboard updates (seconds)
statistics.update.interval=10.0
statistics.update.interval=0.1

View File

@@ -1,473 +0,0 @@
import java.io.IOException;
import java.io.ObjectOutputStream;
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;
/**
* 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);
}
// cleanup after tests
@AfterEach
public void tearDown() {
if (intersectionProcess != null) {
intersectionProcess.shutdown();
}
}
// ==================== 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 seperate thread
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) {
// expected on shutdown
}
});
serverThread.start();
Thread.sleep(500); // wait for server to start
// try connecting to check if its running
try (Socket clientSocket = new Socket("localhost", 18001)) {
assertTrue(clientSocket.isConnected());
}
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);
// create test vehicle
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
try (Socket socket = new Socket("localhost", 18002)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
TestVehicleMessage message = new TestVehicleMessage("Cr1", "Cr2", vehicle);
out.writeObject(message);
out.flush();
Thread.sleep(1000); // wait for procesing
}
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();
Thread serverThread = new Thread(() -> {
try {
intersectionProcess.start();
} catch (IOException e) { }
});
serverThread.start();
Thread.sleep(500);
// verify server running
try (Socket socket = new Socket("localhost", 18001)) {
assertTrue(socket.isConnected());
}
intersectionProcess.shutdown();
serverThread.join(2000);
// after shutdown conection should fail
Thread.sleep(500);
Exception exception = assertThrows(IOException.class, () -> {
Socket socket = new Socket("localhost", 18001);
socket.close();
});
assertNotNull(exception);
}
@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 {
// setup 2 intersections
IntersectionProcess cr1 = new IntersectionProcess("Cr1", configFile.toString());
IntersectionProcess cr2 = new IntersectionProcess("Cr2", configFile.toString());
cr1.initialize();
cr2.initialize();
// start both
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(1000); // wait for servers
// send vehicle to Cr1 that goes to Cr2
java.util.List<String> route = Arrays.asList("Cr1", "Cr2", "S");
Vehicle vehicle = new Vehicle("V001", VehicleType.LIGHT, 0.0, route);
try (Socket socket = new Socket("localhost", 18001)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
TestVehicleMessage message = new TestVehicleMessage("Entry", "Cr1", vehicle);
out.writeObject(message);
out.flush();
Thread.sleep(2000); // time for processing
}
cr1.shutdown();
cr2.shutdown();
thread1.join(2000);
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,125 +0,0 @@
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import sd.config.SimulationConfig;
import sd.engine.SimulationEngine;
import sd.model.Event;
import sd.model.EventType;
import sd.model.Intersection;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.util.StatisticsCollector;
import sd.util.VehicleGenerator;
/**
* Basic tests for the simulation components.
*/
class SimulationTest {
@Test
void testConfigurationLoading() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
assertEquals(60.0, config.getSimulationDuration());
assertEquals("POISSON", config.getArrivalModel());
assertEquals(0.5, config.getArrivalRate());
assertEquals(10.0, config.getStatisticsUpdateInterval());
}
@Test
void testVehicleGeneration() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
VehicleGenerator generator = new VehicleGenerator(config);
Vehicle vehicle = generator.generateVehicle("TEST1", 0.0);
assertNotNull(vehicle);
assertEquals("TEST1", vehicle.getId());
assertNotNull(vehicle.getType());
assertNotNull(vehicle.getRoute());
assertTrue(!vehicle.getRoute().isEmpty());
}
@Test
void testEventOrdering() {
Event e1 = new Event(5.0, EventType.VEHICLE_ARRIVAL, null, "Cr1");
Event e2 = new Event(3.0, EventType.VEHICLE_ARRIVAL, null, "Cr2");
Event e3 = new Event(7.0, EventType.TRAFFIC_LIGHT_CHANGE, null, "Cr1");
assertTrue(e2.compareTo(e1) < 0); // e2 should come before e1
assertTrue(e1.compareTo(e3) < 0); // e1 should come before e3
}
@Test
void testIntersectionVehicleQueue() {
Intersection intersection = new Intersection("TestCr");
TrafficLight light = new TrafficLight("TestCr-N", "North", 30.0, 30.0);
intersection.addTrafficLight(light);
Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
java.util.Arrays.asList("TestCr", "S"));
intersection.configureRoute("S", "North");
// Advance route to next destination
v1.advanceRoute();
intersection.receiveVehicle(v1);
assertEquals(1, intersection.getTotalQueueSize());
assertEquals(1, intersection.getTotalVehiclesReceived());
}
@Test
void testTrafficLightStateChange() {
TrafficLight light = new TrafficLight("Test-Light", "North", 30.0, 30.0);
assertEquals(TrafficLightState.RED, light.getState());
light.changeState(TrafficLightState.GREEN);
assertEquals(TrafficLightState.GREEN, light.getState());
light.changeState(TrafficLightState.RED);
assertEquals(TrafficLightState.RED, light.getState());
}
@Test
void testSimulationEngineInitialization() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
SimulationEngine engine = new SimulationEngine(config);
engine.initialize();
assertNotNull(engine.getIntersections());
assertEquals(5, engine.getIntersections().size());
// Check that intersections have traffic lights
for (Intersection intersection : engine.getIntersections().values()) {
assertEquals(3, intersection.getTrafficLights().size()); // North, South, East, West
}
}
@Test
void testStatisticsCollector() throws IOException {
SimulationConfig config = new SimulationConfig("src/main/resources/simulation.properties");
StatisticsCollector collector = new StatisticsCollector(config);
Vehicle v1 = new Vehicle("V1", VehicleType.LIGHT, 0.0,
java.util.Arrays.asList("Cr1", "Cr2", "S"));
collector.recordVehicleGeneration(v1, 0.0);
assertEquals(1, collector.getTotalVehiclesGenerated());
collector.recordVehicleArrival(v1, "Cr1", 1.0);
collector.recordVehicleExit(v1, 10.0);
assertEquals(1, collector.getTotalVehiclesCompleted());
}
}

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 org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import sd.model.TrafficLight;
import sd.model.TrafficLightState;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* 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("\n✅ Traffic 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,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());
}
}

1055
main/testing.txt Normal file

File diff suppressed because it is too large Load Diff