1 Commits
v0.2.2 ... old

Author SHA1 Message Date
Leandro Afonso
70296cf56f old files 2025-10-13 20:54:15 +01:00
62 changed files with 3881 additions and 3580 deletions

View File

@@ -1,61 +0,0 @@
name: Java CI with Maven
on:
push:
branches: [ "main" ]
tags:
- 'v*.*.*'
pull_request:
branches: [ "main" ]
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/**
publish-release:
runs-on: ubuntu-latest
needs: [build]
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download built JAR
uses: actions/download-artifact@v4
with:
name: package
path: main/target/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: main/target/*.jar

50
.gitignore vendored
View File

@@ -1,50 +0,0 @@
# Compiled class files
*.class
# Log files
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# VS Code settings
.vscode/
# Eclipse files
*.pydevproject
.project
.classpath
.cproject
.settings/
bin/
tmp/
# IntelliJ IDEA files
*.iml
.idea/
out/
# Mac system files
.DS_Store
# Windows system files
Thumbs.db
# Maven
target/
# Gradle
.gradle/
build/
# Other
*.swp
*.pdf

View File

@@ -1,27 +0,0 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" version="28.2.7">
<diagram name="Página-1" id="B1_hHcevBzWlEwI7FSV6">
<mxGraphModel dx="778" dy="476" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="vcp7vux32DhQR4tKQhnF-8" value="Dashboard" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=#C73500;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;fillColor=#fa6800;shape=mxgraph.mscae.oms.dashboard;fontColor=#000000;" vertex="1" parent="1">
<mxGeometry x="389" y="230" width="50" height="41" as="geometry" />
</mxCell>
<mxCell id="vcp7vux32DhQR4tKQhnF-12" value="Semaforo.java" style="shape=image;html=1;verticalAlign=top;verticalLabelPosition=bottom;labelBackgroundColor=#ffffff;imageAspect=0;aspect=fixed;image=https://icons.diagrams.net/icon-cache1/Strabo-2829/traffic_light-1068.png" vertex="1" parent="1">
<mxGeometry x="230" y="350" width="53" height="53" as="geometry" />
</mxCell>
<mxCell id="vcp7vux32DhQR4tKQhnF-13" value="" style="endArrow=classic;startArrow=classic;html=1;rounded=0;movable=1;resizable=1;rotatable=1;deletable=1;editable=1;locked=0;connectable=1;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="310" y="330" as="sourcePoint" />
<mxPoint x="360" y="280" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="vcp7vux32DhQR4tKQhnF-14" value="CruzamentoServer.java" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=1;points=[];movable=1;rotatable=1;deletable=1;editable=1;locked=0;connectable=1;" vertex="1" connectable="0" parent="vcp7vux32DhQR4tKQhnF-13">
<mxGeometry x="-0.3933" relative="1" as="geometry">
<mxPoint x="25" y="25" as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

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

@@ -1,172 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<mxfile host="app.diagrams.net" agent="Gemini" version="28.2.7">
<diagram name="Arquitetura-Sistema-Trafego" id="L-jWkP8vD7q_2fM6N-yC">
<mxGraphModel dx="1434" dy="746" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="E1-process" value="Processo Gerador E1" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="140" y="100" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="E2-process" value="Processo Gerador E2" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="430" y="100" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="E3-process" value="Processo Gerador E3" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="720" y="100" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="Cr1-process" value="Processo Cruzamento (Cr1)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="140" y="240" width="140" height="100" as="geometry" />
</mxCell>
<mxCell id="Cr1-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr1-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr1-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr1-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr2-process" value="Processo Cruzamento (Cr2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="430" y="240" width="140" height="140" as="geometry" />
</mxCell>
<mxCell id="Cr2-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr2-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr2-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr2-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr2-thread3" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr2-process">
<mxGeometry x="20" y="100" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr3-process" value="Processo Cruzamento (Cr3)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="720" y="240" width="140" height="140" as="geometry" />
</mxCell>
<mxCell id="Cr3-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr3-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr3-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr3-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr3-thread3" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr3-process">
<mxGeometry x="20" y="100" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr4-process" value="Processo Cruzamento (Cr4)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="140" y="460" width="140" height="100" as="geometry" />
</mxCell>
<mxCell id="Cr4-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr4-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr4-thread-peao" value="Thread Semáforo (Peões)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr4-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr5-process" value="Processo Cruzamento (Cr5)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;verticalAlign=top;spacingTop=5;" vertex="1" parent="1">
<mxGeometry x="430" y="460" width="140" height="100" as="geometry" />
</mxCell>
<mxCell id="Cr5-thread1" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr5-process">
<mxGeometry x="20" y="30" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="Cr5-thread2" value="Thread Semáforo" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="Cr5-process">
<mxGeometry x="20" y="65" width="100" height="30" as="geometry" />
</mxCell>
<mxCell id="S-process" value="Processo Saída (S)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="720" y="460" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="dashboard-server" value="Servidor Dashboard" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="430" y="640" width="140" height="80" as="geometry" />
</mxCell>
<mxCell id="arrow-E1-Cr1" value="Fluxo Veículos&lt;br&gt;(Sockets/Middleware)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;fontSize=10;align=left;verticalAlign=bottom;" edge="1" parent="1" source="E1-process" target="Cr1-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-E2-Cr2" value="Fluxo Veículos&lt;br&gt;(Sockets/Middleware)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;fontSize=10;align=left;verticalAlign=bottom;" edge="1" parent="1" source="E2-process" target="Cr2-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-E3-Cr3" value="Fluxo Veículos&lt;br&gt;(Sockets/Middleware)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;fontSize=10;align=left;verticalAlign=bottom;" edge="1" parent="1" source="E3-process" target="Cr3-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr1-Cr4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr1-process" target="Cr4-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr2-Cr5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr2-process" target="Cr5-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr4-Cr5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr4-process" target="Cr5-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr5-S" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr5-process" target="S-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr3-S" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr3-process" target="S-process">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="arrow-Cr1-Cr2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr1-process" target="Cr2-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="280" y="290" />
<mxPoint x="430" y="290" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow-Cr2-Cr1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr2-process" target="Cr1-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="430" y="310" />
<mxPoint x="280" y="310" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow-Cr2-Cr3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr2-process" target="Cr3-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="570" y="290" />
<mxPoint x="720" y="290" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="arrow-Cr3-Cr2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;" edge="1" parent="1" source="Cr3-process" target="Cr2-process">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="720" y="310" />
<mxPoint x="570" y="310" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr1-Dash" value="Envio de Estatísticas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="Cr1-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="210" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr2-Dash" value="Envio de Estatísticas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="Cr2-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="stats-Cr3-Dash" value="Envio de Estatísticas" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="Cr3-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="790" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr4-Dash" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;" edge="1" parent="1" source="Cr4-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="210" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="stats-Cr5-Dash" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;" edge="1" parent="1" source="Cr5-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="stats-S-Dash" value="Estatísticas Globais" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=classic;endFill=1;dashed=1;strokeColor=#9673a6;fontSize=10;verticalAlign=bottom;" edge="1" parent="1" source="S-process" target="dashboard-server">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="790" y="680" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="legend" value="Legenda simplificada (removida tabela)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#999999;" vertex="1" parent="1">
</mxCell>
<mxCell id="title" value="Diagrama de Arquitetura - Simulador de Tráfego Distribuído" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=18;fontStyle=1" vertex="1" parent="1">
<mxGeometry x="290" y="40" width="420" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -11,17 +11,6 @@
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target> <maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<dependencies>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project> </project>

View File

@@ -0,0 +1,147 @@
package client;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.structs.NetworkManager;
import client.handlers.UnicastHandler;
import client.handlers.MulticastHandler;
import client.handlers.BroadcastHandler;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Client application that manages network communications and thread handling.
* Created by: 0x1eo
* Last modified: 2024-12-12
*/
public class Client implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(Client.class.getName());
public static final String SERVER_ADDRESS = "localhost";
public static final String BROADCAST_ADDRESS = "255.255.255.255";
public static final int SERVER_PORT = 7500;
public static int CLIENT_PORT = 7501; // Made non-final to allow dynamic assignment
public static final int MULTICAST_PORT = 7502;
public static final int BUFFER_SIZE = 1024;
private final ExecutorService executorService;
private final NetworkManager networkManager;
private static Client instance;
private Client() {
this.executorService = Executors.newFixedThreadPool(3);
this.networkManager = NetworkManager.getInstance();
}
public static synchronized Client getInstance() {
if (instance == null) {
instance = new Client();
}
return instance;
}
/**
* Initializes and starts the client application.
*/
public void start() {
LOGGER.info("Initializing client application...");
try {
networkManager.initializePrimaryConnection();
setupShutdownHook();
initializeAuthenticatedState();
startInputLoop();
LOGGER.info("Client initialization completed successfully");
} catch (Exception e) {
LOGGER.severe("Failed to initialize client: " + e.getMessage());
close();
}
}
/**
* Initializes authenticated state and starts network handlers.
*/
public void initializeAuthenticatedState() {
try {
NetworkManager networkManager = NetworkManager.getInstance();
networkManager.initializeAuthenticatedConnections();
if (networkManager.isAuthenticated()) {
startNetworkHandlers();
LOGGER.info("Authenticated state initialized successfully on port " + CLIENT_PORT);
} else {
LOGGER.severe("Authentication failed");
close();
}
} catch (NetworkManager.NetworkInitializationException e) {
LOGGER.log(Level.SEVERE, "Failed to initialize authenticated state", e);
close();
}
}
private void startNetworkHandlers() {
executorService.execute(new UnicastHandler());
executorService.execute(new MulticastHandler());
executorService.execute(new BroadcastHandler());
}
private void setupShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LOGGER.info("Shutdown hook triggered");
close();
}));
}
@Override
public void close() {
LOGGER.info("Initiating client shutdown sequence...");
shutdownExecutors();
networkManager.close();
LOGGER.info("Client shutdown completed");
System.exit(0);
}
private void shutdownExecutors() {
try {
UnicastHandler.getExecutorService().shutdown();
executorService.shutdown();
// Wait for termination
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
LOGGER.warning("Executor service shutdown interrupted: " + e.getMessage());
Thread.currentThread().interrupt();
} catch (Exception e) {
LOGGER.severe("Error during executor service shutdown: " + e.getMessage());
}
}
private void startInputLoop() {
Thread inputThread = new Thread(() -> {
try (Scanner scanner = new Scanner(System.in)) {
while (!Thread.currentThread().isInterrupted()) {
String input = scanner.nextLine();
String response = InputCommandRouter.processInput(ConnType.UNICAST, input);
if (response != null) {
networkManager.getUnicastOut().println(response);
}
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Error in input loop", e);
}
});
inputThread.setName("InputProcessor");
inputThread.start();
}
public static void main(String[] args) {
Client client = Client.getInstance();
client.start();
}
}

View File

@@ -0,0 +1,164 @@
package client.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.Client;
import client.structs.NetworkManager;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Handles broadcast communication across the network.
* Receives and processes broadcast packets, excluding packets from the local host.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class BroadcastHandler implements Runnable, AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(BroadcastHandler.class.getName());
private static final int SOCKET_TIMEOUT_MS = 5000; // 5 seconds
private static final long TIMEOUT_LOG_INTERVAL = 300000; // Log timeouts every 5 minutes
private static final long HEARTBEAT_INTERVAL = 60000; // 1 minute
private final NetworkManager networkManager;
private volatile boolean isRunning;
private DatagramSocket socket;
private long lastTimeoutLog;
private long lastHeartbeat;
public BroadcastHandler() {
this.networkManager = NetworkManager.getInstance();
this.isRunning = true;
this.lastTimeoutLog = System.currentTimeMillis();
this.lastHeartbeat = System.currentTimeMillis();
}
@Override
public void run() {
try {
initializeSocket();
LOGGER.info("Broadcast handler started successfully on port " + socket.getLocalPort());
processBroadcastMessages();
} catch (IOException e) {
if (isRunning) {
LOGGER.log(Level.SEVERE, "Fatal error in broadcast handler", e);
}
} finally {
close();
}
}
private void initializeSocket() throws IOException {
this.socket = networkManager.getBroadcastSocket();
if (socket == null) {
throw new IOException("Failed to initialize broadcast socket");
}
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
LOGGER.fine("Broadcast socket timeout set to " + SOCKET_TIMEOUT_MS + "ms");
}
private void processBroadcastMessages() throws IOException {
byte[] buffer = new byte[Client.BUFFER_SIZE];
InetAddress localhost = InetAddress.getLocalHost();
while (isRunning) {
checkHeartbeat();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
receiveAndProcessPacket(packet, localhost);
} catch (IOException e) {
handleReceiveException(e);
}
}
}
private void handleReceiveException(IOException e) {
if (!isRunning) return;
if (e instanceof java.net.SocketTimeoutException) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastTimeoutLog > TIMEOUT_LOG_INTERVAL) {
LOGGER.fine("No broadcast messages received in the last " +
(SOCKET_TIMEOUT_MS / 1000) + " seconds");
lastTimeoutLog = currentTime;
}
} else {
LOGGER.log(Level.WARNING, "Error receiving broadcast packet", e);
}
}
private void checkHeartbeat() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastHeartbeat > HEARTBEAT_INTERVAL) {
if (socket != null && !socket.isClosed()) {
LOGGER.fine("Broadcast connection alive - listening for messages");
}
lastHeartbeat = currentTime;
}
}
private void receiveAndProcessPacket(DatagramPacket packet, InetAddress localhost) throws IOException {
socket.receive(packet);
if (packet.getAddress().equals(localhost)) {
return; // Skip localhost packets
}
String input = extractMessage(packet);
String output = processMessage(input);
if (output != null) {
sendResponse(output, packet.getAddress(), packet.getPort());
}
}
private String extractMessage(DatagramPacket packet) {
return new String(
packet.getData(),
0,
packet.getLength(),
StandardCharsets.UTF_8
).trim();
}
private String processMessage(String input) {
try {
return InputCommandRouter.processInput(ConnType.BROADCAST, input);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing message", e);
return null;
}
}
private void sendResponse(String message, InetAddress address, int port) {
try {
byte[] responseData = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket response = new DatagramPacket(
responseData,
responseData.length,
address,
port
);
socket.send(response);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to send broadcast response", e);
}
}
@Override
public void close() {
isRunning = false;
if (socket != null && !socket.isClosed()) {
socket.close();
LOGGER.info("Broadcast handler closed");
}
}
}

View File

@@ -0,0 +1,163 @@
package client.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.Client;
import client.structs.NetworkManager;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Handles multicast communication between network nodes.
* Receives and processes multicast packets, excluding packets from the local host.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class MulticastHandler implements Runnable, AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(MulticastHandler.class.getName());
private static final int SOCKET_TIMEOUT_MS = 5000; // 5 seconds
private static final long TIMEOUT_LOG_INTERVAL = 300000; // Log timeouts every 5 minutes
private static final long HEARTBEAT_INTERVAL = 60000; // 1 minute
private final NetworkManager networkManager;
private volatile boolean isRunning;
private DatagramSocket socket;
private long lastTimeoutLog;
private long lastHeartbeat;
public MulticastHandler() {
this.networkManager = NetworkManager.getInstance();
this.isRunning = true;
this.lastTimeoutLog = System.currentTimeMillis();
this.lastHeartbeat = System.currentTimeMillis();
}
@Override
public void run() {
try {
initializeSocket();
LOGGER.info("Multicast handler started successfully on port " + socket.getLocalPort());
processMulticastMessages();
} catch (IOException e) {
if (isRunning) {
LOGGER.log(Level.SEVERE, "Fatal error in multicast handler", e);
}
} finally {
close();
}
}
private void initializeSocket() throws IOException {
this.socket = networkManager.getMulticastSocket();
if (socket == null) {
throw new IOException("Failed to initialize multicast socket");
}
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
LOGGER.fine("Multicast socket timeout set to " + SOCKET_TIMEOUT_MS + "ms");
}
private void processMulticastMessages() throws IOException {
byte[] buffer = new byte[Client.BUFFER_SIZE];
InetAddress localhost = InetAddress.getLocalHost();
while (isRunning) {
checkHeartbeat();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
receiveAndProcessPacket(packet, localhost);
} catch (IOException e) {
handleReceiveException(e);
}
}
}
private void handleReceiveException(IOException e) {
if (!isRunning) return;
if (e instanceof java.net.SocketTimeoutException) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastTimeoutLog > TIMEOUT_LOG_INTERVAL) {
LOGGER.fine("No multicast messages received in the last " +
(SOCKET_TIMEOUT_MS / 1000) + " seconds");
lastTimeoutLog = currentTime;
}
} else {
LOGGER.log(Level.WARNING, "Error receiving multicast packet", e);
}
}
private void checkHeartbeat() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastHeartbeat > HEARTBEAT_INTERVAL) {
if (socket != null && !socket.isClosed()) {
LOGGER.fine("Multicast connection alive - listening for messages");
}
lastHeartbeat = currentTime;
}
}
private void receiveAndProcessPacket(DatagramPacket packet, InetAddress localhost) throws IOException {
socket.receive(packet);
if (packet.getAddress().equals(localhost)) {
return; // Skip localhost packets
}
String input = extractMessage(packet);
String output = processMessage(input);
if (output != null) {
sendResponse(output, packet.getAddress(), packet.getPort());
}
}
private String extractMessage(DatagramPacket packet) {
return new String(
packet.getData(),
0,
packet.getLength(),
StandardCharsets.UTF_8
).trim();
}
private String processMessage(String input) {
try {
return InputCommandRouter.processInput(ConnType.MULTICAST, input);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing message", e);
return null;
}
}
private void sendResponse(String message, InetAddress address, int port) {
try {
byte[] responseData = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket response = new DatagramPacket(
responseData,
responseData.length,
address,
port
);
socket.send(response);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to send multicast response", e);
}
}
@Override
public void close() {
isRunning = false;
if (socket != null && !socket.isClosed()) {
socket.close();
LOGGER.info("Multicast handler closed");
}
}
}

View File

@@ -0,0 +1,143 @@
package client.handlers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.structs.NetworkManager;
import client.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Handles unicast (point-to-point) connections between clients.
* Manages incoming connections and processes their messages in separate threads.
*
* @author 0x1eo
* @since 2024-12-12
*/
public class UnicastHandler implements Runnable, AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(UnicastHandler.class.getName());
private static final int MAX_THREADS = 50;
private static final int SOCKET_TIMEOUT_MS = 30000; // 30 seconds
private static final int SHUTDOWN_TIMEOUT_SECONDS = 5;
private static ExecutorService connectionPool = null;
private final NetworkManager networkManager;
private volatile boolean isRunning;
/**
* Creates a new UnicastHandler with a fixed thread pool.
*/
public UnicastHandler() {
connectionPool = Executors.newFixedThreadPool(MAX_THREADS);
this.networkManager = NetworkManager.getInstance();
this.isRunning = true;
}
@Override
public void run() {
ServerSocket serverSocket = networkManager.getServerSocket();
if (serverSocket == null) {
LOGGER.severe("Server socket is null. Cannot start UnicastHandler.");
return;
}
while (isRunning) {
try {
Socket clientSocket = serverSocket.accept();
configureSocket(clientSocket);
connectionPool.execute(new ClientHandler(clientSocket));
} catch (IOException e) {
if (isRunning) {
LOGGER.log(Level.SEVERE, "Error accepting connection", e);
}
}
}
}
private void configureSocket(Socket socket) throws IOException {
socket.setSoTimeout(SOCKET_TIMEOUT_MS);
socket.setKeepAlive(true);
}
@Override
public void close() {
isRunning = false;
shutdownConnectionPool();
}
private void shutdownConnectionPool() {
connectionPool.shutdown();
try {
if (!connectionPool.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
connectionPool.shutdownNow();
}
} catch (InterruptedException e) {
connectionPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* Gets the executor service managing client connections.
*
* @return the executor service
*/
public static ExecutorService getExecutorService() {
return connectionPool;
}
/**
* Handles individual client connections in separate threads.
*/
private static class ClientHandler implements Runnable {
private final Socket clientSocket;
private final String clientInfo;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
this.clientInfo = String.format("%s:%d",
socket.getInetAddress().getHostAddress(),
socket.getPort());
}
@Override
public void run() {
LOGGER.info(() -> "New connection established with " + clientInfo);
try (Socket socket = clientSocket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
handleClientCommunication(in, out);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error handling client " + clientInfo, e);
} finally {
LOGGER.info(() -> "Connection closed with " + clientInfo);
}
}
private void handleClientCommunication(BufferedReader in, PrintWriter out) throws IOException {
String input;
while ((input = in.readLine()) != null) {
try {
String response = InputCommandRouter.processInput(ConnType.UNICAST, input);
if (response != null) {
out.println(response);
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing message from " + clientInfo, e);
}
}
}
}
}

View File

@@ -0,0 +1,222 @@
package client.structs;
import java.io.*;
import java.net.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import client.Client;
/**
* Network connection manager for the emergency communication client.
* Implements thread-safe singleton pattern and manages all network resources.
*
* Features:
* - Connection retry mechanism
* - Multiple communication channels (Unicast, Multicast, Broadcast)
* - Automatic resource cleanup
* - Connection state monitoring
*
* @author 0x1eo
* @since 2024-12-13
*/
public class NetworkManager implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(NetworkManager.class.getName());
private static volatile NetworkManager instance;
private static final Object LOCK = new Object();
private static final int MAX_CONNECTION_RETRIES = 3;
private static final int RETRY_DELAY_MS = 2000;
private static final int SOCKET_TIMEOUT_MS = 5000;
private final AtomicBoolean isAuthenticated = new AtomicBoolean(false);
private volatile String username;
private final SocketContainer sockets;
private static final int PORT_RANGE_START = 7501;
private static final int PORT_RANGE_END = 7600;
private static final int MAX_PORT_ATTEMPTS = 10;
private NetworkManager() {
this.sockets = new SocketContainer();
}
public static NetworkManager getInstance() {
NetworkManager result = instance;
if (result == null) {
synchronized (LOCK) {
result = instance;
if (result == null) {
instance = result = new NetworkManager();
}
}
}
return result;
}
/**
* Initializes primary connection with retry mechanism.
*
* @throws NetworkInitializationException if connection fails after retries
*/
public void initializePrimaryConnection() {
for (int attempt = 1; attempt <= MAX_CONNECTION_RETRIES; attempt++) {
try {
establishPrimaryConnection();
LOGGER.info("Primary connection initialized successfully on attempt " + attempt);
return;
} catch (IOException e) {
handleConnectionRetry(attempt, e);
}
}
throw new NetworkInitializationException("Failed to initialize after " + MAX_CONNECTION_RETRIES + " attempts", null);
}
private void establishPrimaryConnection() throws IOException {
Socket unicastSocket = new Socket(Client.SERVER_ADDRESS, Client.SERVER_PORT);
unicastSocket.setSoTimeout(SOCKET_TIMEOUT_MS);
sockets.unicastSocket = unicastSocket;
sockets.unicastIn = new BufferedReader(
new InputStreamReader(unicastSocket.getInputStream())
);
sockets.unicastOut = new PrintWriter(
unicastSocket.getOutputStream(),
true
);
}
private void handleConnectionRetry(int attempt, IOException e) {
LOGGER.log(Level.WARNING,
String.format("Connection attempt %d/%d failed", attempt, MAX_CONNECTION_RETRIES),
e
);
if (attempt < MAX_CONNECTION_RETRIES) {
try {
Thread.sleep(RETRY_DELAY_MS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new NetworkInitializationException("Connection retry interrupted", ie);
}
}
}
/**
* Initializes authenticated connections for multicast and broadcast.
*
* @throws NetworkInitializationException if initialization fails
*/
public void initializeAuthenticatedConnections() {
for (int port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
try {
setupAuthenticatedSockets(port);
isAuthenticated.set(true);
LOGGER.info(String.format(
"Authenticated connections initialized for user %s on port %d",
username, port
));
return;
} catch (IOException e) {
if (port >= PORT_RANGE_END) {
isAuthenticated.set(false);
throw new NetworkInitializationException(
"Failed to find available ports in range " +
PORT_RANGE_START + "-" + PORT_RANGE_END,
e
);
}
LOGGER.warning(String.format(
"Port %d in use, trying next port", port
));
}
}
}
private void setupAuthenticatedSockets(int basePort) throws IOException {
// Server socket uses the base port
sockets.serverSocket = new ServerSocket(basePort);
// Multicast socket uses the multicast port
sockets.multicastSocket = new MulticastSocket(Client.MULTICAST_PORT);
// Broadcast socket uses base port + 1 to avoid conflict
sockets.broadcastSocket = createBroadcastSocket(basePort + 1);
// Update the client port in the Client class
Client.CLIENT_PORT = basePort;
}
private DatagramSocket createBroadcastSocket(int port) throws IOException {
DatagramSocket socket = new DatagramSocket(
port,
InetAddress.getByName(Client.BROADCAST_ADDRESS)
);
socket.setBroadcast(true);
return socket;
}
@Override
public void close() {
sockets.closeAll();
isAuthenticated.set(false);
LOGGER.info("Network manager closed successfully");
}
// Thread-safe getters
public boolean isAuthenticated() { return isAuthenticated.get(); }
public String getUsername() { return username; }
public Socket getUnicastSocket() { return sockets.unicastSocket; }
public BufferedReader getUnicastIn() { return sockets.unicastIn; }
public PrintWriter getUnicastOut() { return sockets.unicastOut; }
public ServerSocket getServerSocket() { return sockets.serverSocket; }
public MulticastSocket getMulticastSocket() { return sockets.multicastSocket; }
public DatagramSocket getBroadcastSocket() { return sockets.broadcastSocket; }
// Thread-safe setter
public void setUsername(String username) {
this.username = username;
LOGGER.info("Username set to: " + username);
}
/**
* Thread-safe container for network resources.
*/
private static class SocketContainer {
private volatile Socket unicastSocket;
private volatile BufferedReader unicastIn;
private volatile PrintWriter unicastOut;
private volatile ServerSocket serverSocket;
private volatile MulticastSocket multicastSocket;
private volatile DatagramSocket broadcastSocket;
private void closeAll() {
closeQuietly(broadcastSocket, "BroadcastSocket");
closeQuietly(multicastSocket, "MulticastSocket");
closeQuietly(serverSocket, "ServerSocket");
closeQuietly(unicastSocket, "UnicastSocket");
closeQuietly(unicastOut, "UnicastWriter");
closeQuietly(unicastIn, "UnicastReader");
}
private void closeQuietly(AutoCloseable resource, String resourceName) {
if (resource != null) {
try {
resource.close();
LOGGER.fine(resourceName + " closed successfully");
} catch (Exception e) {
LOGGER.warning(resourceName + " close failed: " + e.getMessage());
}
}
}
}
/**
* Custom exception for network initialization failures.
*/
public static class NetworkInitializationException extends RuntimeException {
public NetworkInitializationException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,18 @@
package client.structs;
public class TerminalMessageHandler {
public static void displayMessage(String from, String to, String content, String date) {
System.out.printf("[%s] %s -> %s: %s%n", date, from, to, content);
}
public static void displayRequest(String from, String to, String content, String date, String accepter) {
String accepterStatus = accepter.isEmpty() ? "PENDING" : "ACCEPTED by " + accepter;
System.out.printf("[%s] REQUEST from %s to %s: %s (%s)%n", date, from, to, content, accepterStatus);
}
public static boolean promptRequestAcceptance(String from, String to, String content) {
System.out.printf("Accept request from %s to %s: %s%n", from, to, content);
System.out.print("Accept? (y/n): ");
return System.console().readLine().trim().toLowerCase().startsWith("y");
}
}

View File

@@ -0,0 +1,162 @@
package client.utils;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import client.structs.NetworkManager;
import client.structs.TerminalMessageHandler;
/**
* Handles chat message operations including creation, receiving, and processing of messages and requests.
*
* @author 0x1eo
* @since 2024-12-12
*/
public class ChatMessageHandler {
private static final Logger LOGGER = Logger.getLogger(ChatMessageHandler.class.getName());
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
private static final NetworkManager networkManager = NetworkManager.getInstance();
private ChatMessageHandler() {} // Prevent instantiation
/**
* Message types supported by the chat system.
*/
public enum MessageType {
MESSAGE("message"),
REQUEST("request"),
JOIN_GROUP("joinGroup");
private final String command;
MessageType(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}
/**
* Creates a basic message structure with common fields.
*/
private static JSONObject createBaseMessage(MessageType type, String destination, String content) {
JSONObject json = new JSONObject();
json.put("command", type.getCommand());
json.put("from", networkManager.getUsername());
json.put("to", destination);
json.put("content", content);
json.put("date", LocalDateTime.now().format(DATE_FORMATTER));
return json;
}
public static JSONObject createMessage(String destination, String content) {
return createBaseMessage(MessageType.MESSAGE, destination, content);
}
public static JSONObject createRequest(String destination, String content) {
JSONObject json = createBaseMessage(MessageType.REQUEST, destination, content);
json.put("accepter", "");
return json;
}
/**
* Validates if a JSON object contains all required fields.
*/
private static boolean validateMessageFields(JSONObject json, String... requiredFields) {
for (String field : requiredFields) {
if (!json.has(field)) {
LOGGER.warning("Missing required field: " + field);
return false;
}
}
return true;
}
public static void receiveMessage(JSONObject json) {
if (!validateMessageFields(json, "from", "to", "content", "date")) {
return;
}
TerminalMessageHandler.displayMessage(
json.getString("from"),
json.getString("to"),
json.getString("content"),
json.getString("date")
);
}
public static void receiveRequest(JSONObject json) {
if (!validateMessageFields(json, "from", "to", "content", "date", "accepter")) {
return;
}
TerminalMessageHandler.displayRequest(
json.getString("from"),
json.getString("to"),
json.getString("content"),
json.getString("date"),
json.getString("accepter")
);
}
public static String handleAnswerRequest(JSONObject json) {
if (!validateMessageFields(json, "from", "to", "content")) {
return null;
}
boolean accepted = TerminalMessageHandler.promptRequestAcceptance(
json.getString("from"),
json.getString("to"),
json.getString("content")
);
return new JSONObject().put("response", accepted ? "YES" : "NO").toString();
}
public static void processEvents(JSONObject json) {
if (!json.has("events")) {
LOGGER.warning("No events field in JSON");
return;
}
JSONArray events = json.getJSONArray("events");
events.forEach(event -> {
JSONObject eventObj = (JSONObject) event;
if (!eventObj.has("command")) {
LOGGER.warning("Event missing command field");
return;
}
String command = eventObj.getString("command");
try {
switch (MessageType.valueOf(command.toUpperCase())) {
case MESSAGE -> receiveMessage(eventObj);
case REQUEST -> receiveRequest(eventObj);
default -> LOGGER.warning("Unknown command: " + command);
}
} catch (IllegalArgumentException e) {
LOGGER.warning("Invalid command type: " + command);
}
});
}
public static void announceJoinGroup(String ip) {
try {
JSONObject json = new JSONObject()
.put("command", MessageType.JOIN_GROUP.getCommand())
.put("group", ip)
.put("username", networkManager.getUsername());
networkManager.getUnicastOut().println(json);
} catch (JSONException e) {
LOGGER.severe("Failed to announce group join: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,157 @@
package client.utils;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONObject;
import shared.enums.ConnType;
import client.structs.NetworkManager;
import shared.enums.Hierarchy;
/**
* Client-side command router that processes and formats commands according to server protocol.
*
* @author 0x1eo
* @since 2024-12-13 16:41:03
*/
public class InputCommandRouter {
private static final Logger LOGGER = Logger.getLogger(InputCommandRouter.class.getName());
private static final NetworkManager networkManager = NetworkManager.getInstance();
/**
* Processes input and formats it according to server protocol.
*
* @param connType The type of connection (UNICAST, MULTICAST, BROADCAST)
* @param input The user's input string
* @return Formatted JSON string or null if no response needed
*/
public static String processInput(ConnType connType, String input) {
if (input == null || input.trim().isEmpty()) {
return null;
}
String trimmedInput = input.trim();
// Handle client-side commands
if (trimmedInput.startsWith("{command:")) {
return handleCommand(connType, trimmedInput);
}
// Handle messages and requests
return handleMessage(connType, trimmedInput);
}
private static String handleCommand(ConnType connType, String command) {
if (connType != ConnType.UNICAST) {
return null;
}
// Split by : but keep quotes intact
String[] parts = command.substring(9, command.length() - 1).split(":");
String cmd = parts[0].toLowerCase();
switch (cmd) {
case "help":
displayHelp();
return null;
case "register":
if (parts.length != 4) {
System.out.println("Invalid register format. Use: {command:register:username:name:password}");
return null;
}
JSONObject registerJson = new JSONObject()
.put("command", "register")
.put("username", parts[1])
.put("name", parts[2])
.put("password", parts[3])
.put("role", Hierarchy.LOW.name());
// Debug log
LOGGER.info("Sending registration JSON: " + registerJson.toString());
return registerJson.toString();
default:
LOGGER.warning("Unknown command: " + command);
System.out.println("Unknown command. Type {command:help} for available commands.");
return null;
}
}
private static void displayHelp() {
System.out.println("\nAvailable Commands:");
System.out.println("------------------");
System.out.println("{command:register:username:name:password} - Register with the server");
System.out.println("Example: {command:register:leo:Leandro:0808wq21}");
System.out.println("{command:help} - Display this help message");
System.out.println("\nMessage Formats:");
System.out.println("---------------");
System.out.println("Regular message: text");
System.out.println("Direct message: @username: message");
System.out.println("Group message: #groupname: message");
System.out.println("Request: !request @username: content");
System.out.println();
}
private static String createRegistrationJson() {
JSONObject json = new JSONObject()
.put("command", "register")
.put("username", networkManager.getUsername())
.put("name", networkManager.getUsername()) // Using username as name for now
.put("password", "default") // Should be handled properly in production
.put("role", Hierarchy.LOW.name()); // Using LOW as the default hierarchy level
return json.toString();
}
private static String createLoginJson() {
JSONObject json = new JSONObject()
.put("command", "login")
.put("username", networkManager.getUsername())
.put("password", "default"); // Should be handled properly in production
return json.toString();
}
private static String handleMessage(ConnType connType, String input) {
try {
JSONObject messageJson;
if (input.startsWith("!request")) {
// Handle request
String[] parts = input.substring(9).split(":", 2);
if (parts.length != 2 || !parts[0].startsWith("@")) {
System.out.println("Invalid request format. Use: !request @username: content");
return null;
}
String destination = parts[0].substring(1).trim();
messageJson = ChatMessageHandler.createRequest(destination, parts[1].trim());
} else if (input.startsWith("@")) {
// Handle direct message
String[] parts = input.substring(1).split(":", 2);
if (parts.length != 2) {
System.out.println("Invalid direct message format. Use: @username: message");
return null;
}
messageJson = ChatMessageHandler.createMessage(parts[0].trim(), parts[1].trim());
} else if (input.startsWith("#")) {
// Handle group message
String[] parts = input.substring(1).split(":", 2);
if (parts.length != 2) {
System.out.println("Invalid group message format. Use: #groupname: message");
return null;
}
String groupName = parts[0].trim();
ChatMessageHandler.announceJoinGroup(groupName); // Join group first
messageJson = ChatMessageHandler.createMessage(groupName, parts[1].trim());
} else {
// Handle broadcast message
messageJson = ChatMessageHandler.createMessage("", input);
}
return messageJson.toString();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to process message: " + e.getMessage());
return null;
}
}
}

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

@@ -1,260 +0,0 @@
package sd.config;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 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.
*/
public class SimulationConfig {
/**
* Holds all properties loaded from the file.
*/
private final Properties properties;
/**
* Constructs a new SimulationConfig object by loading properties
* from the specified file path.
*
* @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.
*/
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
}
} 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);
}
InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
if (resourceStream == null) {
//como último recurso, tentar com um leading slash
resourceStream = SimulationConfig.class.getResourceAsStream('/' + resourcePath);
}
if (resourceStream != null) {
try (InputStream input = resourceStream) {
properties.load(input);
return;
}
}
}
if (lastException != null) throw lastException;
}
// --- 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").
*/
public String getIntersectionHost(String intersectionId) {
return properties.getProperty("intersection." + intersectionId + ".host", "localhost");
}
/**
* Gets the port number for a specific intersection.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @return The port number.
*/
public int getIntersectionPort(String intersectionId) {
return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0"));
}
/**
* Gets the host address for the dashboard server.
* @return The dashboard host.
*/
public String getDashboardHost() {
return properties.getProperty("dashboard.host", "localhost");
}
/**
* Gets the port number for the dashboard server.
* @return The dashboard port.
*/
public int getDashboardPort() {
return Integer.parseInt(properties.getProperty("dashboard.port", "9000"));
}
/**
* Gets the host address for the exit node.
* @return The exit node host.
*/
public String getExitHost() {
return properties.getProperty("exit.host", "localhost");
}
/**
* Gets the port number for the exit node.
* @return The exit node port.
*/
public int getExitPort() {
return Integer.parseInt(properties.getProperty("exit.port", "9001"));
}
// --- Simulation configurations ---
/**
* 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"));
}
/**
* Gets the vehicle arrival model ("POISSON" or "FIXED").
* @return The arrival model as a string.
*/
public String getArrivalModel() {
return properties.getProperty("simulation.arrival.model", "POISSON");
}
/**
* Gets the average arrival rate (lambda) for the POISSON model.
* This represents the average number of vehicles arriving per second.
* @return The arrival rate.
*/
public double getArrivalRate() {
return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5"));
}
/**
* Gets the fixed time interval between vehicle arrivals for the FIXED model.
* @return The fixed interval in seconds.
*/
public double getFixedArrivalInterval() {
return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0"));
}
// --- Traffic light configurations ---
/**
* Gets the duration of the GREEN light state for a specific traffic light.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @param direction The direction of the light (e.g., "North").
* @return The green light time in seconds.
*/
public double getTrafficLightGreenTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".green";
return Double.parseDouble(properties.getProperty(key, "30.0"));
}
/**
* Gets the duration of the RED light state for a specific traffic light.
* @param intersectionId The ID of the intersection (e.g., "Cr1").
* @param direction The direction of the light (e.g., "North").
* @return The red light time in seconds.
*/
public double getTrafficLightRedTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".red";
return Double.parseDouble(properties.getProperty(key, "30.0"));
}
// --- Vehicle configurations ---
/**
* 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() {
return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7"));
}
/**
* Gets the average time it takes a LIGHT vehicle to cross an intersection.
* @return The crossing time in seconds.
*/
public double getLightVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0"));
}
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type BIKE.
* @return The probability for BIKE vehicles.
*/
public double getBikeVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0"));
}
/**
* Gets the average time it takes a BIKE vehicle to cross an intersection.
* @return The crossing time in seconds.
*/
public double getBikeVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5"));
}
/**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type HEAVY.
* @return The probability for HEAVY vehicles.
*/
public double getHeavyVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0"));
}
/**
* Gets the average time it takes a HEAVY vehicle to cross an intersection.
* @return The crossing time in seconds.
*/
public double getHeavyVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "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"));
}
// --- Generic getters ---
/**
* Generic method to get any property as a string, with a default value.
* @param key The property key.
* @param defaultValue The value to return if the key is not found.
* @return The property value or the default.
*/
public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
/**
* Generic method to get any property as a string.
* @param key The property key.
* @return The property value, or null if not found.
*/
public String getProperty(String key) {
return properties.getProperty(key);
}
}

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

@@ -1,243 +0,0 @@
package sd.model;
import java.util.ArrayList;
import java.util.HashMap;
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.
*/
public class Intersection {
// --- Identity and configuration ---
/**
* Unique identifier for the intersection (e.g., "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.
*/
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.
*/
private final Map<String, String> routing;
// --- Statistics ---
/**
* Total number of vehicles that have been received by this intersection.
*/
private int totalVehiclesReceived;
/**
* Total number of vehicles that have successfully passed through (sent from) this intersection.
*/
private int totalVehiclesSent;
/**
* A running average of the waiting time for vehicles at this intersection.
* Note: This calculation might be simplified.
*/
private double averageWaitingTime;
/**
* Constructs a new Intersection with a given ID.
* Initializes empty maps for traffic lights and routing.
*
* @param id The unique identifier for this intersection (e.g., "Cr1").
*/
public Intersection(String id) {
this.id = id;
this.trafficLights = new HashMap<>();
this.routing = new HashMap<>();
this.totalVehiclesReceived = 0;
this.totalVehiclesSent = 0;
this.averageWaitingTime = 0.0;
}
/**
* Registers a new {@link TrafficLight} with this intersection.
* The light is mapped by its direction.
*
* @param trafficLight The {@link TrafficLight} object to add.
*/
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."
*
* @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").
*/
public void configureRoute(String nextDestination, String direction) {
routing.put(nextDestination, direction);
}
/**
* Accepts an incoming vehicle and places it in the correct queue.
* * This method:
* 1. Increments the {@link #totalVehiclesReceived} counter.
* 2. Gets the vehicle's *next* destination (from {@link Vehicle#getCurrentDestination()}).
* 3. Uses the {@link #routing} map to find the correct *direction* for that destination.
* 4. Adds the vehicle to the queue of the {@link TrafficLight} for that direction.
*
* @param vehicle The {@link Vehicle} arriving at the intersection.
*/
public void receiveVehicle(Vehicle vehicle) {
totalVehiclesReceived++;
String nextDestination = vehicle.getCurrentDestination();
String direction = routing.get(nextDestination);
if (direction != null && trafficLights.containsKey(direction)) {
// Found a valid route and light, add vehicle to the queue
trafficLights.get(direction).addVehicle(vehicle);
} else {
// Routing error: No rule for this destination or no light for that direction
System.err.printf(
"Routing error at %s: could not place vehicle %s (destination: %s, found direction: %s)%n",
this.id, vehicle.getId(), nextDestination, direction
);
}
}
/**
* Returns the traffic light controlling the given direction.
*
* @param direction The direction (e.g., "North").
* @return The {@link TrafficLight} object, or null if no light exists
* for that direction.
*/
public TrafficLight getTrafficLight(String direction) {
return trafficLights.get(direction);
}
/**
* Returns a list of all traffic lights managed by this intersection.
*
* @return A new {@link List} containing all {@link TrafficLight} objects.
*/
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.
*
* @return The sum of all queue sizes.
*/
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.
*/
public String getId() {
return id;
}
/**
* @return The total number of vehicles that have arrived at this intersection.
*/
public int getTotalVehiclesReceived() {
return totalVehiclesReceived;
}
/**
* @return The total number of vehicles that have successfully
* departed from this intersection.
*/
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.
*/
public void incrementVehiclesSent() {
totalVehiclesSent++;
}
/**
* @return The running average of vehicle waiting time at this intersection.
*/
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.
*
* @param newTime The waiting time (in seconds) of the vehicle that just
* departed.
*/
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.
*/
@Override
public String toString() {
return String.format(
"Intersection{id='%s', lights=%d, queues=%d, received=%d, sent=%d}",
id,
trafficLights.size(),
getTotalQueueSize(),
totalVehiclesReceived,
totalVehiclesSent
);
}
}

View File

@@ -1,315 +0,0 @@
package sd.model;
import java.util.LinkedList;
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).
*/
public class TrafficLight {
// --- Identity and configuration ---
/**
* Unique identifier for the light (e.g., "Cr1-N").
*/
private final String id;
/**
* The direction this light controls (e.g., "North", "South").
*/
private final String direction;
/**
* The current state of the light (GREEN or RED).
*/
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.
*/
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.
*/
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).
*/
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).
*/
private final Condition lightGreen;
// --- Timing configuration ---
/**
* The duration (in seconds) this light stays GREEN.
*/
private double greenTime;
/**
* The duration (in seconds) this light stays RED.
*/
private double redTime;
// --- Statistics ---
/**
* Counter for the total number of vehicles that have
* been dequeued (processed) by this light.
*/
private int totalVehiclesProcessed;
/**
* Constructs a new TrafficLight.
*
* @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.
*/
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.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.totalVehiclesProcessed = 0;
}
/**
* Adds a vehicle to the *end* of the waiting queue.
* This method is thread-safe.
*
* @param vehicle The {@link Vehicle} to add.
*/
public void addVehicle(Vehicle vehicle) {
lock.lock(); // Acquire the lock
try {
queue.offer(vehicle); // Add vehicle to queue
vehicleAdded.signalAll(); // Signal (for concurrent models)
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* 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.
*/
public Vehicle removeVehicle() {
lock.lock(); // Acquire the lock
try {
if (state == TrafficLightState.GREEN && !queue.isEmpty()) {
Vehicle vehicle = queue.poll(); // Remove vehicle from queue
if (vehicle != null) {
totalVehiclesProcessed++;
}
return vehicle;
}
return null; // Light is RED or queue is empty
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* 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.
*/
public void changeState(TrafficLightState newState) {
lock.lock(); // Acquire the lock
try {
this.state = newState;
if (newState == TrafficLightState.GREEN) {
lightGreen.signalAll(); // Signal (for concurrent models)
}
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* Returns how many vehicles are currently in the queue.
* This method is thread-safe.
* * @return The size of the queue.
*/
public int getQueueSize() {
lock.lock(); // Acquire the lock
try {
return queue.size();
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* Checks whether the queue is empty.
* This method is thread-safe.
*
* @return {@code true} if the queue has no vehicles, {@code false} otherwise.
*/
public boolean isQueueEmpty() {
lock.lock(); // Acquire the lock
try {
return queue.isEmpty();
} finally {
lock.unlock(); // Always release the lock
}
}
// --- Getters & Setters ---
/**
* @return The unique ID of this light (e.g., "Cr1-N").
*/
public String getId() {
return id;
}
/**
* @return The direction this light controls (e.g., "North").
*/
public String getDirection() {
return direction;
}
/**
* Gets the current state of the light (GREEN or RED).
* This method is thread-safe.
*
* @return The current {@link TrafficLightState}.
*/
public TrafficLightState getState() {
lock.lock(); // Acquire the lock
try {
return state;
} finally {
lock.unlock(); // Always release the lock
}
}
/**
* @return The configured GREEN light duration in seconds.
*/
public double getGreenTime() {
return greenTime;
}
/**
* Sets the GREEN light duration.
* @param greenTime The new duration in seconds.
*/
public void setGreenTime(double greenTime) {
this.greenTime = greenTime;
}
/**
* @return The configured RED light duration in seconds.
*/
public double getRedTime() {
return redTime;
}
/**
* Sets the RED light duration.
* @param redTime The new duration in seconds.
*/
public void setRedTime(double redTime) {
this.redTime = redTime;
}
/**
* @return The total number of vehicles processed (dequeued) by this light.
*/
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.
*/
public Lock getLock() {
return lock;
}
/**
* @return The {@link Condition} for vehicle additions.
*/
public Condition getVehicleAdded() {
return vehicleAdded;
}
/**
* @return The {@link Condition} for the light turning green.
*/
public Condition getLightGreen() {
return lightGreen;
}
/**
* @return A string summary of the light's current state.
*/
@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
);
}
}

View File

@@ -1,17 +0,0 @@
package sd.model;
/**
* Enumeration representing the two possible states of a {@link TrafficLight}.
*/
public enum TrafficLightState {
/**
* The light is GREEN, allowing vehicles to pass (be dequeued).
*/
GREEN,
/**
* The light is RED, blocking vehicles (they remain in the queue).
*/
RED
}

View File

@@ -1,218 +0,0 @@
package sd.model;
import java.io.Serializable;
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).
*/
public class Vehicle implements Serializable {
private static final long serialVersionUID = 1L;
// --- Identity and configuration ---
/**
* Unique identifier for the vehicle (e.g., "V1", "V2").
*/
private final String id;
/**
* The type of vehicle (BIKE, LIGHT, HEAVY).
*/
private final VehicleType type;
/**
* The simulation time (in seconds) when the vehicle was generated.
*/
private final double entryTime;
/**
* The complete, ordered list of destinations (intersection IDs and the
* final exit "S"). Example: ["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*).
*/
private int currentRouteIndex;
// --- Metrics ---
/**
* The total accumulated time (in seconds) this vehicle has spent
* waiting at red lights.
*/
private double totalWaitingTime;
/**
* The total accumulated time (in seconds) this vehicle has spent
* actively crossing intersections.
*/
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"]).
*/
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.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.
*/
public boolean advanceRoute() {
currentRouteIndex++;
return currentRouteIndex < route.size();
}
/**
* Gets the current destination (the next intersection or exit) that
* the vehicle is heading towards.
*
* @return The ID of the current destination (e.g., "Cr1"), or
* {@code null} if the route is complete.
*/
public String getCurrentDestination() {
return (currentRouteIndex < route.size()) ? route.get(currentRouteIndex) : null;
}
/**
* Checks if the vehicle has completed its entire route.
*
* @return {@code true} if the route index is at or past the end
* of the route list, {@code false} otherwise.
*/
public boolean hasReachedEnd() {
return currentRouteIndex >= route.size();
}
// --- Getters and metrics management ---
/**
* @return The vehicle's unique ID.
*/
public String getId() {
return id;
}
/**
* @return The vehicle's {@link VehicleType}.
*/
public VehicleType getType() {
return type;
}
/**
* @return The simulation time when the vehicle entered the system.
*/
public double getEntryTime() {
return entryTime;
}
/**
* @return A *copy* of the vehicle's complete route.
*/
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.
*/
public int getCurrentRouteIndex() {
return currentRouteIndex;
}
/**
* @return The total accumulated waiting time in seconds.
*/
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.
*
* @param time The duration (in seconds) to add.
*/
public void addWaitingTime(double time) {
totalWaitingTime += time;
}
/**
* @return The total accumulated crossing time in seconds.
*/
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.
*
* @param time The duration (in seconds) to add.
*/
public void addCrossingTime(double time) {
totalCrossingTime += time;
}
/**
* Calculates the vehicle's total time spent in the system so far.
* This is a "live" calculation.
*
* @param currentTime The current simulation time.
* @return The total elapsed time (in seconds) since the vehicle
* was generated ({@code currentTime - entryTime}).
*/
public double getTotalTravelTime(double currentTime) {
return currentTime - entryTime;
}
/**
* @return A string summary of the vehicle's current state.
*/
@Override
public String toString() {
return String.format(
"Vehicle{id='%s', type=%s, next='%s', route=%s}",
id, type, getCurrentDestination(), route
);
}
}

View File

@@ -1,27 +0,0 @@
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}.
*/
public enum VehicleType {
/**
* A bike or motorcycle.
* Typically has a short crossing time.
*/
BIKE,
/**
* A standard light vehicle, such as a car.
* This is usually the most common type.
*/
LIGHT,
/**
* A heavy vehicle, such as a truck or bus.
* Typically has a long crossing time.
*/
HEAVY
}

View File

@@ -1,103 +0,0 @@
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.
*/
public class RandomGenerator {
/**
* The single, shared Random instance for the entire simulation.
*/
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.
*
* @param lambda The average arrival rate (λ) (e.g., 0.5 vehicles per second).
* @return The time interval (in seconds) until the next arrival.
*/
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.
*
* @param min The minimum possible value.
* @param max The maximum possible value.
* @return A random integer in the range [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).
*
* @param min The minimum possible value.
* @param max The maximum possible value.
* @return A random double in the range [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.
*
* @param probability A value between 0.0 (never) and 1.0 (always).
* @return {@code true} or {@code false}, based on the probability.
*/
public static boolean occursWithProbability(double probability) {
return random.nextDouble() < probability;
}
/**
* Picks a random element from the given array.
*
* @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.
*/
public static <T> T chooseRandom(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array cannot be null or empty.");
}
return array[random.nextInt(array.length)];
}
/**
* 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.
*
* @param seed The seed to use.
*/
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,229 +0,0 @@
package sd.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import sd.config.SimulationConfig;
import sd.model.Vehicle;
import sd.model.VehicleType;
/**
* 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).
*/
public class VehicleGenerator {
private final SimulationConfig config;
private final String arrivalModel;
private final double arrivalRate; // Lambda (λ) for POISSON
private final double fixedInterval; // Interval for FIXED
// --- Predefined Routes ---
// These lists store all possible routes, grouped by where they start.
/** 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;
/**
* Constructs a new VehicleGenerator.
* It reads the necessary configuration and initializes the
* predefined routes.
*
* @param config The {@link SimulationConfig} object.
*/
public VehicleGenerator(SimulationConfig config) {
this.config = config;
// 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.
*/
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.
*
* @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.
*/
public Vehicle generateVehicle(String vehicleId, double entryTime) {
VehicleType type = selectVehicleType();
List<String> route = selectRandomRoute();
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.
*
* @return The selected {@link VehicleType}.
*/
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;
lightProbability /= total;
double rand = Math.random();
if (rand < bikeProbability) {
return VehicleType.BIKE;
} else if (rand < bikeProbability + lightProbability) {
return VehicleType.LIGHT;
} else {
return VehicleType.HEAVY;
}
}
/**
* 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).
*
* @return A {@link List} of strings representing the chosen route (e.g., ["Cr1", "Cr4", "S"]).
*/
private List<String> selectRandomRoute() {
// Step 1: Randomly select an entry point (E1, E2, or E3)
double entryRandom = Math.random();
List<RouteWithProbability> selectedRoutes;
if (entryRandom < 0.333) {
selectedRoutes = e1Routes;
} else if (entryRandom < 0.666) {
selectedRoutes = e2Routes;
} else {
selectedRoutes = e3Routes;
}
// Step 2: Select a route from the chosen list based on cumulative probabilities
double routeRand = Math.random();
double cumulative = 0.0;
for (RouteWithProbability routeWithProb : selectedRoutes) {
cumulative += routeWithProb.probability;
if (routeRand <= cumulative) {
// Return a *copy* of the route to prevent modification
return new ArrayList<>(routeWithProb.route);
}
}
// Fallback: This should only be reached if probabilities don't sum to 1
// (due to floating point errors)
return new ArrayList<>(selectedRoutes.get(0).route);
}
/**
* @return A string providing information about the generator's configuration.
*/
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()
);
}
/**
* 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,95 @@
package server;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import server.structs.SystemStateManager;
import server.handlers.ActiveUsersHandler;
import server.handlers.BroadcastHandler;
import server.handlers.UnicastHandler;
import server.handlers.DataPersistence;
import server.handlers.ThingHandler;
import server.handlers.MulticastHandler;
//import server.handlers.RequestsStatsThread;
/**
* The Server class represents the main server application.
* It handles direct connections, broadcasts, multicasts, events, active users, request statistics,
* data persistence, and provides methods for starting and closing the server.
*/
public class Server {
private static final Logger logger = Logger.getLogger(Server.class.getName());
private static final ExecutorService executorService = Executors.newFixedThreadPool(100);
public static final int BUFFER_SIZE = 1024;
public static final int SERVER_PORT = 7500;
public static final int USER_PORT = 7501;
public static final int MULTICAST_PORT = 7502;
public static final String BROADCAST_ADDRESS = "255.255.255.255";
/**
* The main method of the Server class.
* It loads the shared data, creates sockets, and starts various threads for server operations.
*
* @param args The command line arguments.
*/
public static void main(String[] args) {
try {
SystemStateManager.loadData();
} catch (Exception ignored) {
}
executorService.execute(() -> handleUnicast(SERVER_PORT));
try {
SystemStateManager.setMulticastSocket(new MulticastSocket(MULTICAST_PORT));
DatagramSocket broadcastSocket = new DatagramSocket(USER_PORT, InetAddress.getByName(BROADCAST_ADDRESS));
broadcastSocket.setBroadcast(true);
SystemStateManager.setBroadcastSocket(broadcastSocket);
} catch (IOException io) {
logger.severe("Error Creating Sockets! " + io.getMessage());
close();
}
executorService.execute(new BroadcastHandler());
executorService.execute(new MulticastHandler());
executorService.execute(new ThingHandler());
executorService.execute(new ActiveUsersHandler());
//executorService.execute(new RequestsStats());
executorService.execute(new DataPersistence());
}
/**
* Handles direct connections on the specified port.
*
* @param port The port number for direct connections.
*/
public static void handleUnicast(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
executorService.execute(new UnicastHandler(serverSocket.accept()));
}
} catch (Exception e) {
logger.severe("Error Handling Unicast Connection! " + e.getMessage());
close();
}
}
/**
* Closes the server by shutting down the executor service, saving the shared data, and exiting the application.
*/
public static void close() {
try {
executorService.shutdown();
SystemStateManager.saveData();
System.exit(0);
} catch (Exception ignored) {
}
}
}

View File

@@ -0,0 +1,195 @@
package server.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.Server;
import server.structs.SystemStateManager;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
import server.utils.MessageProtocolHandler;
import server.utils.UserHandler;
import shared.enums.ConnType;
/**
* Handles asynchronous request acceptance in the emergency communication system.
* Manages the workflow of request acceptance based on connection type and user hierarchy.
*
* Features:
* - Multi-mode communication support (unicast, multicast, broadcast)
* - Hierarchy-based request handling
* - Asynchronous operation
* - Request acknowledgment tracking
*
* @author 0x1eo
* @since 2024-12-13
*/
public class AcceptRequestHandler implements Runnable {
private static final Logger logger = Logger.getLogger(AcceptRequestHandler.class.getName());
private static final String MULTICAST_ADDRESS_PATTERN =
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
private final ConnType connectionType;
private final Request request;
/**
* Creates a new request acceptance handler.
*
* @param connectionType the type of network connection to use
* @param request the request to be processed
*/
public AcceptRequestHandler(ConnType connectionType, Request request) {
this.connectionType = connectionType;
this.request = request;
}
@Override
public void run() {
try {
JSONObject requestJson = createRequestJson();
handleRequestByConnectionType(requestJson);
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to send request answer", e);
} catch (JSONException e) {
logger.log(Level.SEVERE, "Failed to create request JSON", e);
}
}
private JSONObject createRequestJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("command", "requestAnswer");
json.put("from", request.getSender());
json.put("content", request.getMessage());
json.put("to", determineReceiver());
return json;
}
private String determineReceiver() {
Object receiver = request.getReceiver();
if (receiver instanceof User) {
return ((User) receiver).getUsername();
}
if (receiver instanceof String) {
String receiverStr = (String) receiver;
if ("broadcast".equals(receiverStr) || receiverStr.matches(MULTICAST_ADDRESS_PATTERN)) {
return receiverStr;
}
logger.severe("Invalid receiver string format");
throw new IllegalStateException("Invalid receiver format");
}
logger.severe("Invalid receiver type");
throw new IllegalStateException("Invalid receiver type");
}
private void handleRequestByConnectionType(JSONObject requestJson) throws IOException, JSONException {
switch (connectionType) {
case UNICAST:
handleUnicastRequest(requestJson);
break;
case MULTICAST:
handleMulticastRequest(requestJson);
break;
case BROADCAST:
handleBroadcastRequest(requestJson);
break;
default:
logger.warning("Unsupported connection type: " + connectionType);
}
}
private void handleUnicastRequest(JSONObject requestJson) throws IOException, JSONException {
User receiver = request.getReceiver();
String response = UserHandler.sendAndReceiveSomething(receiver, requestJson.toString());
if (response != null && new JSONObject(response).getString("response").equals("YES")) {
request.setTruster(receiver);
notifyUsers(receiver);
}
}
private void handleMulticastRequest(JSONObject requestJson) throws IOException, JSONException {
User group = request.getReceiver(); // Now correctly returns a User
List<User> eligibleUsers = getEligibleUsers(
new ArrayList<>(SystemStateManager.getUsersFromGroup(group.getUsername())), // Assuming you want to get users from the group name
request.getSender()
);
for (User user : eligibleUsers) {
if (tryAcceptRequest(user, requestJson)) {
sendMulticastNotification(group.toString());
break;
}
}
}
private void handleBroadcastRequest(JSONObject requestJson) throws IOException, JSONException {
List<User> eligibleUsers = getEligibleUsers(
new ArrayList<>(SystemStateManager.getUsers()),
request.getSender()
);
for (User user : eligibleUsers) {
if (tryAcceptRequest(user, requestJson)) {
sendBroadcastNotification();
break;
}
}
}
private List<User> getEligibleUsers(List<User> users, User sender) {
users.remove(sender);
users.removeIf(user -> !SystemStateManager.getOnlineUsers().contains(user));
users.removeIf(user -> !user.getHierarchy().isHigherThan(sender.getHierarchy()));
users.sort((u1, u2) -> u2.getHierarchy().getValue() - u1.getHierarchy().getValue());
return users;
}
private boolean tryAcceptRequest(User user, JSONObject requestJson) throws IOException, JSONException {
String response = UserHandler.sendAndReceiveSomething(user, requestJson.toString());
if (response != null && new JSONObject(response).getString("response").equals("YES")) {
request.setTruster(user);
return true;
}
return false;
}
private void notifyUsers(User receiver) throws IOException {
UserHandler.sendSomething(request.getSender(),
MessageProtocolHandler.notificationToJson(request).toString());
UserHandler.sendSomething(receiver,
MessageProtocolHandler.notificationToJson(request).toString());
}
private void sendMulticastNotification(String group) throws IOException {
String eventJson = MessageProtocolHandler.notificationToJson(request).toString();
DatagramPacket packet = new DatagramPacket(
eventJson.getBytes(),
eventJson.length(),
InetAddress.getByName(group),
Server.MULTICAST_PORT
);
SystemStateManager.getMulticastSocket().send(packet);
}
private void sendBroadcastNotification() throws IOException {
String eventJson = MessageProtocolHandler.notificationToJson(request).toString();
DatagramPacket packet = new DatagramPacket(
eventJson.getBytes(),
eventJson.length(),
InetAddress.getByName(Server.BROADCAST_ADDRESS),
Server.USER_PORT
);
SystemStateManager.getBroadcastSocket().send(packet);
}
}

View File

@@ -0,0 +1,61 @@
package server.handlers;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.intfaces.User;
import server.utils.UserHandler;
/**
* This class represents a thread that periodically checks the number of online users and sends a message to the user with the highest role.
*/
public class ActiveUsersHandler implements Runnable {
private static final Logger logger = Logger.getLogger(ActiveUsersHandler.class.getName());
/**
* The run method of the ActiveUsersThread class.
* This method is executed when the thread starts.
* It periodically checks the number of online users and sends a message to the user with the highest role.
*/
@Override
public void run() {
// Number of Online Users Only for the highest role
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
List<User> onlineUsers = SystemStateManager.getOnlineUsers();
logger.info("Number of Online Users: " + onlineUsers.size());
if (onlineUsers.size() == 0) {
continue;
}
User highestRoleUser = SystemStateManager.getHighestHierarchyUser(onlineUsers);
if (highestRoleUser == null) {
logger.severe("Highest Role User is null!");
continue;
}
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("command", "message");
jsonObject.put("from", "server");
jsonObject.put("to", highestRoleUser.getUsername());
jsonObject.put("content", "Number of Online Users: " + onlineUsers.size());
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm");
jsonObject.put("date", sdf.format(new Date()));
UserHandler.sendSomething(highestRoleUser, jsonObject.toString());
} catch (IOException | JSONException ignored) {
logger.severe("Error Sending Active Users! " + ignored.getMessage());
}
}
}
}

View File

@@ -0,0 +1,57 @@
package server.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.logging.Logger;
import server.Server;
import server.structs.SystemStateManager;
import server.utils.InputCommandRouter;
import server.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* The BroadcastThread class represents a thread that handles broadcasting messages to all connected clients.
*/
public class BroadcastHandler implements Runnable {
private static final Logger logger = Logger.getLogger(BroadcastHandler.class.getName());
/**
* Constructs a new BroadcastThread.
*/
public BroadcastHandler() {}
/**
* Runs the broadcast thread.
*/
@Override
public void run() {
try (
DatagramSocket broadcastSocket = SystemStateManager.getBroadcastSocket();
) {
while (true) {
byte[] buffer = new byte[Server.BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
broadcastSocket.receive(packet);
if (packet.getAddress().equals(InetAddress.getLocalHost())) {
continue;
}
String input = new String(packet.getData());
String output = InputCommandRouter.processInput(ConnType.BROADCAST, packet, input);
if (output == null) {
continue;
}
DatagramPacket response = new DatagramPacket(output.getBytes(), output.length(), packet.getAddress(), packet.getPort());
try {
broadcastSocket.send(response);
} catch (IOException io) {
logger.severe("Error Sending Broadcast Response: " + io.getMessage());
}
}
} catch (IOException io) {
logger.severe("Error Handling Broadcast Connection! " + io.getMessage());
}
}
}

View File

@@ -0,0 +1,30 @@
package server.handlers;
import java.util.logging.Logger;
import server.structs.SystemStateManager;
/**
* This class represents a thread responsible for persisting data at regular intervals.
*/
public class DataPersistence implements Runnable {
private static final Logger logger = Logger.getLogger(DataPersistence.class.getName());
/**
* The run method of the DataPersistenceThread.
* This method is responsible for saving data at regular intervals.
*/
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
SystemStateManager.saveData();
logger.info("Data Saved");
} catch (Exception e) {
logger.severe("Error Saving Data! " + e.getMessage());
}
}
}
}

View File

@@ -0,0 +1,57 @@
package server.handlers;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.intfaces.Notification;
import server.structs.intfaces.User;
import server.utils.MessageProtocolHandler;
import server.utils.UserHandler;
/**
* This class represents a thread that retrieves and sends message history for a user.
*/
public class MessageHistoryHandler implements Runnable {
private static final Logger logger = Logger.getLogger(MessageHistoryHandler.class.getName());
private User user;
/**
* Constructs a new MessageHistoryThread object.
*
* @param user the user for whom the message history will be retrieved and sent
*/
public MessageHistoryHandler(User user) {
this.user = user;
}
/**
* Runs the thread, retrieving and sending the message history for the user.
*/
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
List<? extends Notification> notifications = SystemStateManager.getUserNotifications(user);
if (notifications.isEmpty()) {
return;
}
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("command", "history");
jsonObject.put("notifications", MessageProtocolHandler.notificationsToJson(notifications));
UserHandler.sendSomething(user, jsonObject.toString());
} catch (JSONException | IOException error) {
logger.severe("Error Sending Message History! " + error.getMessage());
}
}
}

View File

@@ -0,0 +1,58 @@
package server.handlers;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.logging.Logger;
import server.Server;
import server.structs.SystemStateManager;
import server.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* MulticastThread class.
* This class is responsible for handling the multicast connection.
* It receives multicast packets, processes them, and sends responses back to the clients.
*/
public class MulticastHandler implements Runnable {
private static final Logger logger = Logger.getLogger(MulticastHandler.class.getName());
/**
* MulticastThread constructor.
*/
public MulticastHandler() {}
/**
* Runs the multicast thread.
* It continuously receives multicast packets, processes them, and sends responses back to the clients.
*/
@Override
public void run() {
try (
MulticastSocket multicastSocket = SystemStateManager.getMulticastSocket()) {
while (true) {
byte[] buffer = new byte[Server.BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
multicastSocket.receive(packet);
if (packet.getAddress().equals(InetAddress.getLocalHost())) {
continue;
}
String input = new String(packet.getData());
String output = InputCommandRouter.processInput(ConnType.MULTICAST, packet, input);
if (output == null) {
continue;
}
DatagramPacket response = new DatagramPacket(output.getBytes(), output.length(), packet.getAddress(), packet.getPort());
try {
multicastSocket.send(response);
} catch (IOException io) {
logger.severe("Error sending Multicast Response: " + io.getMessage());
}
}
} catch (IOException io) {
logger.severe("Error Handling Multicast Connection! " + io.getMessage());
}
}
}

View File

@@ -0,0 +1,119 @@
package server.handlers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.List;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.Server;
import server.structs.SystemStateManager;
import server.structs.intfaces.Notification;
import server.structs.intfaces.Message;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
import server.utils.MessageProtocolHandler;
/**
* Handles the delivery of notifications to users in a server-side communication system.
*
* This class is a runnable thread responsible for continuously processing and delivering
* notifications from the system state manager to their intended recipients. It manages
* different types of notifications such as messages and requests, ensuring they are
* transmitted to the appropriate users via network sockets.
*
* Key responsibilities:
* <ul>
* <li>Periodically retrieves pending notifications from the system state manager</li>
* <li>Validates and delivers notifications to their intended users</li>
* <li>Handles socket connections and communication protocols</li>
* <li>Manages error scenarios such as closed sockets or failed JSON conversions</li>
* </ul>
*
* The handler operates in an infinite loop, sleeping briefly between notification checks
* to prevent excessive CPU usage. It supports different notification types and logs
* critical events for monitoring and debugging purposes.
*
* @author 0x1eo
* @since 2024-12-13
* @see SystemStateManager
* @see Notification
* @see Message
* @see Request
*/
public class ThingHandler implements Runnable {
private static final Logger logger = Logger.getLogger(ThingHandler.class.getName());
/**
* The run method is the entry point for the thread.
* It continuously checks for events in the shared object and delivers them to the appropriate users.
*/
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
List<? extends Notification> notifications = SystemStateManager.getNotificationsToDeliver();
logger.info("Notifications to deliver: " + notifications.size());
for (int i = 0; i < notifications.size(); i++) {
Notification notification = notifications.get(i);
Object receiver = notification.getReceiver();
if (receiver instanceof User) {
User user = ((User) receiver);
logger.info("Event to deliver to " + user.getUsername());
Socket socket = SystemStateManager.getUserSocket(user);
if (socket == null || socket.isClosed() || !socket.isConnected()) {
logger.severe("User Socket is Null or Closed");
SystemStateManager.removeNotificationDelivered(notification);
continue;
}
try (
Socket newSocket = new Socket(socket.getInetAddress(), Server.USER_PORT);
BufferedReader in = new BufferedReader(new InputStreamReader(newSocket.getInputStream()));
PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true)) {
if (notification instanceof Message) {
Message message = ((Message) notification);
JSONObject json = MessageProtocolHandler.notificationToJson(message);
if (json == null) {
logger.severe("Event to JSON returned null!");
SystemStateManager.removeNotificationDelivered(notification);
continue;
}
out.println(MessageProtocolHandler.notificationToJson(message).toString());
logger.info("Message delivered to " + user.getUsername());
} else if (notification instanceof Request) {
Request request = ((Request) notification);
JSONObject json = MessageProtocolHandler.notificationToJson(request);
if (json == null) {
logger.severe("Event to JSON returned null!");
SystemStateManager.removeNotificationDelivered(notification);
continue;
}
out.println(MessageProtocolHandler.notificationToJson(request).toString());
logger.info("Request delivered to " + user.getUsername());
}
SystemStateManager.removeNotificationDelivered(notification);
} catch (IOException io) {
SystemStateManager.removeUserSocket(user);
} catch (JSONException json) {
SystemStateManager.removeNotificationDelivered(notification);
}
} else {
logger.severe("Receiver is not a user!");
SystemStateManager.removeNotificationDelivered(notification);
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
package server.handlers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.logging.Logger;
import server.utils.InputCommandRouter;
import shared.enums.ConnType;
/**
* Represents a thread that handles direct connections with clients.
*/
public class UnicastHandler implements Runnable {
private static final Logger logger = Logger.getLogger(UnicastHandler.class.getName());
private Socket socket;
/**
* Constructs a DirectThread object with the specified socket.
*
* @param socket the socket representing the client connection
*/
public UnicastHandler(Socket socket) {
this.socket = socket;
}
/**
* Runs the thread and handles the communication with the client.
*/
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);) {
try {
while (true) {
String input = in.readLine();
if (input == null) {
in.close();
out.close();
if (!socket.isClosed()) socket.close();
return;
}
String output = InputCommandRouter.processInput(ConnType.UNICAST, socket, input);
if (output == null) {
continue;
}
out.println(output);
}
} catch (IOException io) {
logger.severe("Error Handling Direct Message! " + io.getMessage());
}
} catch (IOException io) {
logger.severe("Error Handling Direct Connection! " + io.getMessage());
}
}
}

View File

@@ -0,0 +1,453 @@
package server.structs;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.stream.Collectors;
import server.structs.intfaces.*;
import shared.enums.Hierarchy;
/**
* Manages network communications and user data for the emergency chat server.
* Implements thread-safe operations for managing users, connections, and messages.
*
* @author 0x1eo
* @since 2024-12-12
*/
public class SystemStateManager {
// Maps usernames to their corresponding User objects for quick lookup
private static final Map<String, User> users = new HashMap<>();
// Maintains active socket connections for online users
private static final Map<User, Socket> userSockets = new HashMap<>();
// Stores pending notifications for each user in priority order
private static final Map<User, TreeSet<Notification>> userNotifications = new HashMap<>();
// Queue of notifications pending delivery to users
private static final List<Notification> notificationsToDeliver = new ArrayList<>();
// Maps multicast group addresses to their member users
private static final Map<String, List<User>> groups = new HashMap<>();
// Socket for handling multicast communication
private static MulticastSocket multicastSocket;
// Socket for handling broadcast messages
private static DatagramSocket broadcastSocket;
// Private constructor to prevent instantiation
private SystemStateManager() {
throw new AssertionError("Utility class - do not instantiate");
}
//#region User Management
/**
* Adds a new user to the system.
*
* @param user the user to be added
* @throws IllegalArgumentException if user is null or if username already exists
*/
public static void addUser(User user) {
validateNotNull("User", user);
synchronized (users) {
if (users.containsKey(user.getUsername())) {
throw new IllegalArgumentException("User already exists!");
}
users.put(user.getUsername(), user);
}
}
/**
* Retrieves a user by their username.
*
* @param username the username to look up
* @return the User object if found, null otherwise
* @throws IllegalArgumentException if username is null or empty
*/
public static User getUser(String username) {
validateNotEmpty("Username", username);
synchronized (users) {
return users.get(username);
}
}
/**
* Returns a list of all registered users in the system.
*
* @return new ArrayList containing all users
*/
public static List<User> getUsers() {
synchronized (users) {
return new ArrayList<>(users.values());
}
}
/**
* Gets the user with the highest hierarchy level from a list of users.
*
* @param userList list of users to check
* @return user with highest hierarchy level, or null if list is empty
*/
public static User getHighestHierarchyUser(List<User> userList) {
if (userList == null || userList.isEmpty()) {
return null;
}
return userList.stream()
.max((u1, u2) -> {
Hierarchy h1 = u1.getHierarchy();
Hierarchy h2 = u2.getHierarchy();
return Integer.compare(h1.getValue(), h2.getValue());
})
.orElse(null);
}
//#endregion
//#region Socket Management
/**
* Associates a socket connection with a user.
*
* @param user the user to associate the socket with
* @param socket the socket connection
* @throws IllegalArgumentException if either parameter is null
*/
public static void addUserSocket(User user, Socket socket) {
validateNotNull("User", user);
validateNotNull("Socket", socket);
synchronized (userSockets) {
userSockets.put(user, socket);
}
}
/**
* Retrieves the active socket connection for a user.
*
* @param user the user whose socket to retrieve
* @return the Socket object if found, null otherwise
* @throws IllegalArgumentException if user is null
*/
public static Socket getUserSocket(User user) {
validateNotNull("User", user);
synchronized (userSockets) {
return userSockets.get(user);
}
}
/**
* Returns a list of currently online users.
* A user is considered online if they have an active socket connection.
*
* @return list of users with active socket connections
*/
public static List<User> getOnlineUsers() {
synchronized (userSockets) {
return userSockets.entrySet().stream()
.filter(entry -> isSocketActive(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
}
/**
* Checks if a socket connection is active and valid.
*
* @param socket the socket to check
* @return true if socket is active and operational
*/
private static boolean isSocketActive(Socket socket) {
return socket != null &&
!socket.isClosed() &&
socket.isConnected() &&
!socket.isInputShutdown() &&
!socket.isOutputShutdown();
}
/**
* Removes a user's socket connection.
*
* @param user the user whose socket to remove
* @throws IllegalArgumentException if user is null
*/
public static void removeUserSocket(User user) {
validateNotNull("User", user);
synchronized (userSockets) {
userSockets.remove(user);
}
}
//#endregion
//#region Notification Management
/**
* Adds a notification for a specific user.
*
* @param user the target user
* @param notification the notification to add
* @throws IllegalArgumentException if either parameter is null
*/
public static void addUserNotification(User user, Notification notification) {
validateNotNull("User", user);
validateNotNull("Notification", notification);
synchronized (userNotifications) {
userNotifications.computeIfAbsent(user, k -> new TreeSet<>()).add(notification);
}
}
/**
* Retrieves all notifications for a user.
*
* @param user the user whose notifications to retrieve
* @return list of notifications, empty list if none found
* @throws IllegalArgumentException if user is null
*/
public static List<Notification> getUserNotifications(User user) {
validateNotNull("User", user);
synchronized (userNotifications) {
TreeSet<Notification> notifications = userNotifications.get(user);
return notifications != null ? new ArrayList<>(notifications) : new ArrayList<>();
}
}
/**
* Gets all pending requests in the system.
*
* @return list of all Request objects
*/
public static List<Request> getRequests() {
synchronized (userNotifications) {
return userNotifications.values().stream()
.flatMap(Collection::stream)
.filter(notification -> notification instanceof Request)
.map(notification -> (Request) notification)
.collect(Collectors.toList());
}
}
/**
* Filters a collection of requests to return only accepted ones.
*
* @param requests collection of requests to filter
* @return list of requests that have been accepted
*/
public static List<Request> getAcceptedRequests(Collection<Request> requests) {
return requests.stream()
.filter(request -> request.getTruster() != null)
.collect(Collectors.toList());
}
/**
* Adds a notification to the delivery queue.
*
* @param notification the notification to queue
* @throws IllegalArgumentException if notification is null
*/
public static void addNotificationToDeliver(Notification notification) {
validateNotNull("Notification", notification);
synchronized (notificationsToDeliver) {
notificationsToDeliver.add(notification);
}
}
/**
* Returns all notifications pending delivery.
*
* @return list of queued notifications
*/
public static List<Notification> getNotificationsToDeliver() {
synchronized (notificationsToDeliver) {
return new ArrayList<>(notificationsToDeliver);
}
}
/**
* Removes a delivered notification from the queue.
*
* @param notification the notification to remove
* @throws IllegalArgumentException if notification is null
*/
public static void removeNotificationDelivered(Notification notification) {
validateNotNull("Notification", notification);
synchronized (notificationsToDeliver) {
notificationsToDeliver.remove(notification);
}
}
//#endregion
//#region Group Management
/**
* Adds a user to a multicast group.
*
* @param group the multicast group address
* @param user the user to add
* @throws IllegalArgumentException if group is invalid or user is null
*/
public static void addUserToGroup(String group, User user) {
validateNotEmpty("Group", group);
validateNotNull("User", user);
validateMulticastAddress(group);
synchronized (groups) {
groups.computeIfAbsent(group, k -> new ArrayList<>()).add(user);
}
}
/**
* Gets all users in a specific group.
*
* @param group the group address
* @return list of users in the group
* @throws IllegalArgumentException if group is null or empty
*/
public static List<User> getUsersFromGroup(String group) {
validateNotEmpty("Group", group);
synchronized (groups) {
List<User> groupUsers = groups.get(group);
return groupUsers != null ? new ArrayList<>(groupUsers) : new ArrayList<>();
}
}
//#endregion
//#region Socket Getters/Setters
/**
* Gets the system's multicast socket.
*
* @return the MulticastSocket instance
*/
public static MulticastSocket getMulticastSocket() {
return multicastSocket;
}
/**
* Sets the system's multicast socket.
*
* @param socket the MulticastSocket to use
*/
public static void setMulticastSocket(MulticastSocket socket) {
multicastSocket = socket;
}
/**
* Gets the system's broadcast socket.
*
* @return the DatagramSocket instance
*/
public static DatagramSocket getBroadcastSocket() {
return broadcastSocket;
}
/**
* Sets the system's broadcast socket.
*
* @param socket the DatagramSocket to use
*/
public static void setBroadcastSocket(DatagramSocket socket) {
broadcastSocket = socket;
}
//#endregion
//#region Data Persistence
/**
* Gets a map of all data structures for persistence.
*
* @return map of structure names to their objects
*/
private static Map<String, Object> getDataStructures() {
Map<String, Object> structures = new HashMap<>();
structures.put("users.bin", users);
structures.put("userNotifications.bin", userNotifications);
structures.put("notificationsToDeliver.bin", notificationsToDeliver);
structures.put("groups.bin", groups);
return structures;
}
/**
* Saves all system state to persistent storage.
*
* @throws IOException if an I/O error occurs during saving
*/
public static void saveData() throws IOException {
for (Map.Entry<String, Object> entry : getDataStructures().entrySet()) {
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("./" + entry.getKey()))) {
out.writeObject(entry.getValue());
}
}
}
/**
* Loads system state from persistent storage.
*
* @throws IOException if an I/O error occurs during loading
* @throws ClassNotFoundException if a serialized class cannot be found
*/
public static void loadData() throws IOException, ClassNotFoundException {
for (Map.Entry<String, Object> entry : getDataStructures().entrySet()) {
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream(entry.getKey()))) {
Object data = in.readObject();
loadDataStructure(entry.getKey(), data);
}
}
}
/**
* Loads a specific data structure from serialized data.
*
* @param key the identifier for the data structure
* @param data the serialized data to load
*/
private static void loadDataStructure(String key, Object data) {
switch (key) {
case "users.bin":
users.putAll((Map<String, User>) data);
break;
case "userNotifications.bin":
userNotifications.putAll((Map<User, TreeSet<Notification>>) data);
break;
case "notificationsToDeliver.bin":
notificationsToDeliver.addAll((List<Notification>) data);
break;
case "groups.bin":
groups.putAll((Map<String, List<User>>) data);
break;
}
}
//#endregion
//#region Validation Helpers
/**
* Validates that an object is not null.
*
* @param field name of the field being validated
* @param value the value to check
* @throws IllegalArgumentException if value is null
*/
private static void validateNotNull(String field, Object value) {
if (value == null) {
throw new IllegalArgumentException(field + " cannot be null!");
}
}
/**
* Validates that a string is not null or empty.
*
* @param field name of the field being validated
* @param value the string to check
* @throws IllegalArgumentException if value is null or empty
*/
private static void validateNotEmpty(String field, String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " cannot be null or empty!");
}
}
/**
* Validates that a string represents a valid multicast address.
*
* @param address the address to validate
* @throws IllegalArgumentException if address format is invalid
*/
private static void validateMulticastAddress(String address) {
if (!address.matches("^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
throw new IllegalArgumentException("Invalid multicast address format!");
}
}
//#endregion
}

View File

@@ -0,0 +1,28 @@
package server.structs.abstractions;
import server.structs.intfaces.Message;
import server.structs.intfaces.User;
/**
* Abstract implementation of Message interface for the emergency communication system.
* Represents the base message type that can be exchanged between users.
*
* @author 0x1eo
* @since 2024-12-13
* @see Message
* @see AbstractNotification
*/
public abstract class AbstractMessage extends AbstractNotification implements Message {
/**
* Creates a new message.
*
* @param sender the user sending the message
* @param receiver the recipient (user, broadcast, or multicast group)
* @param content the message content
* @throws IllegalArgumentException if sender or content is null
*/
protected AbstractMessage(User sender, Object receiver, String content) {
super(sender, receiver, content);
}
}

View File

@@ -0,0 +1,111 @@
package server.structs.abstractions;
import java.time.Instant;
import java.util.Objects;
import server.structs.intfaces.Notification;
import server.structs.intfaces.User;
/**
* Abstract base implementation for notifications in the emergency communication system.
* Provides common functionality for all types of communications including
* messages, requests, and alerts.
*
* Features:
* - Timestamp-based ordering
* - Sender and receiver tracking
* - Message content storage
* - Thread-safe immutable timestamp
*
* @author 0x1eo
* @since 2024-12-13 02:47:23
* @see Notification
*/
public abstract class AbstractNotification implements Notification {
protected User sender;
protected User receiver;
protected String message;
protected final Instant timestamp;
/**
* Creates a new notification.
*
* @param sender the sender's identifier object
* @param receiver the recipient's identifier object
* @param message the notification message content
* @throws IllegalArgumentException if sender, receiver or message is null
*/
protected AbstractNotification(Object sender, Object receiver, String message) {
setSender(sender);
setReceiver(receiver);
setMessage(message);
this.timestamp = Instant.now();
}
@Override
public User getSender() {
return sender;
}
@Override
public void setSender(Object sender) {
if (sender == null) {
throw new IllegalArgumentException("Sender cannot be null");
}
this.sender = (User)sender;
}
@Override
public User getReceiver() { return receiver; }
@Override
public void setReceiver(Object receiver) {
if (receiver == null) {
throw new IllegalArgumentException("Receiver cannot be null");
}
this.receiver = (User)receiver;
}
@Override
public String getMessage() { return message; }
@Override
public void setMessage(String message) {
if (message == null) {
throw new IllegalArgumentException("Message cannot be null");
}
this.message = message;
}
@Override
public Instant getTimestamp() { return timestamp; }
@Override
public void setTimestamp(Instant timestamp) {
throw new UnsupportedOperationException("Timestamp cannot be modified after creation");
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Notification)) return false;
Notification other = (Notification) obj;
return Objects.equals(sender, other.getSender()) &&
Objects.equals(receiver, other.getReceiver()) &&
Objects.equals(message, other.getMessage()) &&
Objects.equals(timestamp, other.getTimestamp());
}
@Override
public int hashCode() { return Objects.hash(sender, receiver, message, timestamp); }
@Override
public String toString() {
return String.format("Notification[from=%s, to=%s, message='%s', time=%s]",
sender,
receiver,
message,
timestamp);
}
}

View File

@@ -0,0 +1,88 @@
package server.structs.abstractions;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
/**
* Abstract base implementation for requests in the emergency communication system.
* Extends AbstractNotification to provide request-specific functionality including
* request author tracking.
*
* Features:
* - Request author (truster) tracking
* - Inherits notification base features
* - Supports emergency communication protocol
* - Immutable creation timestamp
*
* @author 0x1eo
* @since 2024-12-13
* @see Request
* @see AbstractNotification
* @see User
*/
public abstract class AbstractRequest extends AbstractNotification implements Request {
protected User truster;
/**
* Creates a new request in the emergency system.
*
* @param sender the user initiating the request
* @param receiver the intended recipient
* @param content the request content/message
* @throws IllegalArgumentException if any parameter is null, or if receiver is empty
*/
protected AbstractRequest(User sender, String receiver, String content) {
super(sender.getUsername(), receiver, content);
setTruster(sender); // Initialize truster with the sender
}
/**
* Gets the author (requesting user) of this request.
*
* @return the user who authored this request
*/
@Override
public User getTruster() {
return truster;
}
/**
* Sets the author (requesting user) of this request.
*
* @param author the user who authored this request
* @throws IllegalArgumentException if author is null
*/
@Override
public void setTruster(User author) {
if (author == null) {
throw new IllegalArgumentException("Request author cannot be null");
}
this.truster = author;
}
@Override
public String toString() {
return String.format("Request[from=%s, to=%s, content='%s', author=%s, time=%s]",
getSender(),
getReceiver(),
getMessage(),
truster.getUsername(),
getTimestamp());
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) return false;
if (!(obj instanceof Request)) return false;
Request other = (Request) obj;
return truster.equals(other.getTruster());
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + truster.hashCode();
return result;
}
}

View File

@@ -0,0 +1,125 @@
package server.structs.abstractions;
import java.util.Objects;
import server.structs.intfaces.User;
import shared.enums.Hierarchy;
/**
* Abstract base implementation of the User interface that represents
* a user in the emergency communication system with hierarchical privileges.
*
* Features:
* - Username and full name management
* - Secure password storage
* - Hierarchical role-based access
* - Natural ordering based on hierarchy
*
* @author 0x1eo
* @since 2024-12-13
* @see User
* @see Hierarchy
*/
public abstract class AbstractUser implements User {
private String username;
private String name;
private String password;
private Hierarchy hierarchy;
/**
* Creates a new user with the specified credentials and hierarchy level.
*
* @param username unique identifier for the user
* @param name full name of the user
* @param password user's authentication credential
* @param hierarchy user's position in the system hierarchy
* @throws IllegalArgumentException if any parameter is null or empty strings
*/
protected AbstractUser(String username, String name, String password, Hierarchy hierarchy) {
setUsername(username);
setName(name);
setPassword(password);
setHierarchy(hierarchy);
}
@Override
public String getUsername() {
return username;
}
@Override
public void setUsername(String username) {
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be null or empty");
}
this.username = username.trim();
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
this.name = name.trim();
}
@Override
public String getPassword() {
return password;
}
@Override
public void setPassword(String password) {
if (password == null || password.trim().isEmpty()) {
throw new IllegalArgumentException("Password cannot be null or empty");
}
this.password = password;
}
@Override
public Hierarchy getHierarchy() {
return hierarchy;
}
@Override
public void setHierarchy(Hierarchy hierarchy) {
if (hierarchy == null) {
throw new IllegalArgumentException("Hierarchy cannot be null");
}
this.hierarchy = hierarchy;
}
@Override
public int compareTo(User other) {
return this.hierarchy.getValue() - other.getHierarchy().getValue();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User)) return false;
User other = (User) obj;
return Objects.equals(username, other.getUsername()) &&
Objects.equals(name, other.getName()) &&
Objects.equals(password, other.getPassword()) &&
Objects.equals(hierarchy, other.getHierarchy());
}
@Override
public int hashCode() {
return Objects.hash(username, name, password, hierarchy);
}
@Override
public String toString() {
return String.format("User[username='%s', name='%s', hierarchy=%s]",
username,
name,
hierarchy.getDisplayName());
}
}

View File

@@ -0,0 +1,46 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractMessage;
import server.structs.intfaces.Message;
import server.structs.intfaces.User;
/**
* Standard message implementation for the emergency communication system.
* Provides a concrete implementation for direct, broadcast, and group messages.
*
* Features:
* - Direct user-to-user messaging
* - Broadcast messaging support
* - Multicast group communication
* - Emergency notifications
* - Timestamp-based ordering
*
* @author 0x1eo
* @since 2024-12-13 03:42:45 UTC
* @see Message
* @see AbstractMessage
* @see User
*/
public class StandardMessage extends AbstractMessage {
/**
* Creates a new message in the emergency system.
*
* @param sender the user initiating the message
* @param receiver the recipient (user, broadcast address, or multicast group)
* @param content the message content
* @throws IllegalArgumentException if sender or content is null
*/
public StandardMessage(User sender, Object receiver, String content) {
super(sender, receiver, content);
}
@Override
public String toString() {
return String.format("Message[from=%s, to=%s, content='%s', time=%s]",
getSender(),
getReceiver(),
getMessage(),
getTimestamp());
}
}

View File

@@ -0,0 +1,44 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractNotification;
import server.structs.intfaces.User;
import server.utils.UserHandler;
/**
* Standard notification implementation for the emergency communication system.
* Provides a concrete implementation of AbstractNotification for general-purpose
* system notifications.
*
* Features:
* - Direct messaging support
* - Broadcast capability
* - Multicast group messaging
* - System alerts and announcements
*
* @author 0x1eo
* @since 2024-12-13 03:41:39 UTC
* @see AbstractNotification
*/
public class StandardNotification extends AbstractNotification {
/**
* Creates a new standard notification.
*
* @param sender the sender's identifier (user, system, or service)
* @param receiver the recipient's identifier (user, broadcast, or multicast group)
* @param message the notification content
* @throws IllegalArgumentException if any parameter is null
*/
public StandardNotification(Object sender, Object receiver, String message) {
super(sender, receiver, message);
}
public String convertToUser(Object obj) {
if (obj instanceof String) {
// Use your user lookup mechanism
return UserHandler.findUser(String.valueOf(obj));
}
throw new IllegalArgumentException("Cannot convert to User: " + obj);
}
}

View File

@@ -0,0 +1,17 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractRequest;
import server.structs.intfaces.User;
/**
* Standard request implementation for the emergency communication system.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class StandardRequest extends AbstractRequest {
public StandardRequest(User sender, String receiver, String content) {
super(sender, receiver, content);
}
}

View File

@@ -0,0 +1,17 @@
package server.structs.implementations;
import server.structs.abstractions.AbstractUser;
import shared.enums.Hierarchy;
/**
* Standard user implementation for the emergency communication system.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class StandardUser extends AbstractUser {
public StandardUser(String username, String name, String password, Hierarchy hierarchy) {
super(username, name, password, hierarchy);
}
}

View File

@@ -0,0 +1,15 @@
package server.structs.intfaces;
/**
* Represents a message in the communication system.
* Extends the base Notification interface to provide message-specific functionality.
* This interface serves as a marker for distinguishing message types from other notifications.
*
* @author 0x1eo
* @since 2024-12-12
* @see Notification
*/
public interface Message extends Notification {
// Marker interface - no additional methods required
// Implementation classes should provide message-specific functionality
}

View File

@@ -0,0 +1,86 @@
package server.structs.intfaces;
import java.io.Serializable;
import java.time.Instant;
/**
* Represents a notification in the communication system.
* Provides methods for managing notification metadata and content.
* Implements Serializable for network transmission and Comparable for ordering.
*
* @author 0x1eo
* @since 2024-12-12
*/
public interface Notification extends Serializable, Comparable<Notification> {
/**
* Gets the sender of the notification.
*
* @return the sender's identifier
*/
User getSender();
/**
* Sets the sender of the notification.
*
* @param sender the sender's identifier
* @throws IllegalArgumentException if sender is null or empty
*/
void setSender(Object sender);
/**
* Gets the receiver of the notification.
*
* @return the receiver's identifier
*/
User getReceiver();
/**
* Sets the receiver of the notification.
*
* @param receiver the receiver's identifier
* @throws IllegalArgumentException if receiver is null or empty
*/
void setReceiver(Object receiver);
/**
* Gets the content of the notification.
*
* @return the notification message content
*/
String getMessage();
/**
* Sets the content of the notification.
*
* @param message the notification message content
* @throws IllegalArgumentException if message is null
*/
void setMessage(String message);
/**
* Gets the timestamp of the notification.
*
* @return the instant when the notification was created
*/
Instant getTimestamp();
/**
* Sets the timestamp of the notification.
*
* @param timestamp the instant when the notification was created
* @throws IllegalArgumentException if timestamp is null
*/
void setTimestamp(Instant timestamp);
/**
* Provides a default natural ordering for notifications based on timestamp.
*
* @param other the notification to compare with
* @return negative if this is earlier, zero if same time, positive if later
*/
@Override
default int compareTo(Notification other) {
return this.getTimestamp().compareTo(other.getTimestamp());
}
}

View File

@@ -0,0 +1,29 @@
package server.structs.intfaces;
/**
* Represents a request in the communication system.
* Extends the Notification interface to add request-specific functionality.
* A request is a special type of notification that includes an author (requesting user).
*
* @author 0x1eo
* @since 2024-12-12
* @see Notification
* @see User
*/
public interface Request extends Notification {
/**
* Gets the author (requesting user) of this request.
*
* @return the user who authored this request
*/
User getTruster();
/**
* Sets the author (requesting user) of this request.
*
* @param author the user who authored this request
* @throws IllegalArgumentException if author is null
*/
void setTruster(User author);
}

View File

@@ -0,0 +1,101 @@
package server.structs.intfaces;
import java.io.Serializable;
import shared.enums.Hierarchy;
/**
* Represents a user in the system with their credentials and permissions.
* Implements Serializable for network transmission and Comparable for user ordering.
*
* @author 0x1eo
* @since 2024-12-12
*/
public interface User extends Serializable, Comparable<User> {
/**
* Gets the user's unique username.
*
* @return the username
*/
String getUsername();
/**
* Sets the user's username.
*
* @param username the username to set
* @throws IllegalArgumentException if username is null or empty
*/
void setUsername(String username); // Fixed method name from setUserName
/**
* Gets the user's password (hashed).
*
* @return the hashed password
*/
String getPassword();
/**
* Sets the user's password.
* Implementation should ensure the password is properly hashed before storage.
*
* @param password the password to set
* @throws IllegalArgumentException if password is null or empty
*/
void setPassword(String password);
/**
* Gets the user's display name.
*
* @return the user's full name
*/
String getName();
/**
* Sets the user's display name.
*
* @param name the name to set
* @throws IllegalArgumentException if name is null or empty
*/
void setName(String name);
/**
* Gets the user's hierarchy level in the system.
*
* @return the user's hierarchy level
*/
Hierarchy getHierarchy();
/**
* Sets the user's hierarchy level.
*
* @param hierarchy the hierarchy level to set
* @throws IllegalArgumentException if hierarchy is null
*/
void setHierarchy(Hierarchy hierarchy);
/**
* Provides a default natural ordering for users based on username.
*
* @param other the user to compare with
* @return negative if this username comes before, zero if equal, positive if after
*/
@Override
default int compareTo(User other) {
return this.getUsername().compareToIgnoreCase(other.getUsername());
}
/**
* Checks if the user has at least the specified hierarchy level.
*
* @param minimumHierarchy the minimum required hierarchy level
* @return true if user's hierarchy is at least the specified level
* @throws IllegalArgumentException if minimumHierarchy is null
*/
default boolean hasMinimumHierarchy(Hierarchy minimumHierarchy) {
if (minimumHierarchy == null) {
throw new IllegalArgumentException("Minimum hierarchy cannot be null");
}
return this.getHierarchy().ordinal() >= minimumHierarchy.ordinal();
}
}

View File

@@ -0,0 +1,90 @@
package server.utils;
import java.net.Socket;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.intfaces.User;
import shared.enums.ConnType;
/**
* This class represents the protocol used for processing input in the server.
*/
public class InputCommandRouter {
private static final Logger logger = Logger.getLogger(InputCommandRouter.class.getName());
/**
* Processes the input based on the given connection type, socket packet, and input string.
*
* @param connType The type of connection (DIRECT or INDIRECT).
* @param socketPacket The socket packet object.
* @param input The input string to be processed.
* @return The response as a JSON string.
*/
public static String processInput(ConnType connType, Object socketPacket, String input) {
JSONObject response = new JSONObject();
try {
JSONObject json = new JSONObject(input);
if (!json.has("command")) {
response.put("response", "Invalid command!");
return response.toString();
}
// Register the user's socket if it is not already registered
Socket socket;
if (connType == ConnType.UNICAST) {
if (json.has("username") || json.has("from")) {
User User = null;
if (json.has("username")) {
User = SystemStateManager.getUser(json.getString("username"));
}
if (json.has("from")) {
User = SystemStateManager.getUser(json.getString("from"));
}
if (User != null) {
if (socketPacket instanceof Socket) {
socket = (Socket) socketPacket;
if (socket != SystemStateManager.getUserSocket(User)) {
SystemStateManager.addUserSocket(User, socket);
}
}
}
}
}
// Process the input based on the command
switch (json.getString("command")) {
case "register":
if (connType != ConnType.UNICAST) {
return null;
}
socket = (Socket) socketPacket;
return UserHandler.register(json, socketPacket);
case "login":
if (connType != ConnType.UNICAST) {
return null;
}
socket = (Socket) socketPacket;
return UserHandler.login(json, socketPacket);
case "message":
MessageProtocolHandler.receiveMessage(connType, json);
return null;
case "request":
MessageProtocolHandler.receiveRequest(connType, json, socketPacket);
return null;
case "joinGroup":
UserHandler.joinGroup(json);
return null;
default:
logger.severe("Invalid command received! " + input);
return null;
}
} catch (JSONException e) {
logger.severe("Invalid JSON received! " + e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,260 @@
package server.utils;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import server.structs.SystemStateManager;
import server.structs.implementations.StandardMessage;
import server.structs.implementations.StandardRequest;
import server.structs.intfaces.Notification;
import server.structs.intfaces.Message;
import server.structs.intfaces.Request;
import server.structs.intfaces.User;
import server.handlers.AcceptRequestHandler;
import shared.enums.ConnType;
public class MessageProtocolHandler {
private static final Logger logger = Logger.getLogger(MessageProtocolHandler.class.getName());
private static final ExecutorService executorService = Executors.newFixedThreadPool(50);
/**
* Returns the ExecutorService used by the EventsHandler.
*
* @return the ExecutorService used by the EventsHandler
*/
public static ExecutorService getExecutorService() {
return executorService;
}
/**
* Converts an event to a JSON object.
*
* @param notification The event to convert.
* @return The JSON object representation of the event.
*/
public static <N extends Notification> JSONObject notificationToJson(N notification) throws JSONException {
JSONObject json = new JSONObject();
json.put("from", notification.getSender());
Object receiver = notification.getReceiver();
if (receiver instanceof User) {
json.put("to", ((User) receiver).getUsername());
} else if (receiver instanceof String) {
String receiverString = (String) receiver;
if (receiverString.equals("broadcast")) {
json.put("to", "broadcast");
} else if (receiverString.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
json.put("to", receiverString);
}
} else {
logger.severe("Invalid receiver type!");
}
json.put("content", notification.getMessage());
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm");
json.put("date", sdf.format(notification.getTimestamp()));
if (notification instanceof Message) {
json.put("command", "message");
return json;
} else if (notification instanceof Request) {
json.put("command", "request");
Request request = (Request) notification;
if (request.getTruster() != null) {
json.put("accepter", request.getTruster().getUsername());
} else {
json.put("accepter", "");
}
return json;
}
throw new JSONException("Invalid event type");
}
/**
* Converts a collection of events to a JSON array.
*
* @param notifications The collection of events to convert.
* @return The JSON array representation of the events.
* @throws JSONException If an error occurs while converting the events to JSON.
*/
public static JSONArray notificationsToJson(Collection<? extends Notification> notifications) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (Notification notification : notifications) {
jsonArray.put(notificationToJson(notification));
}
return jsonArray;
}
/**
* Converts a JSON object to a message.
*
* @param json The JSON object to convert.
* @return The message representation of the JSON object.
*/
public static Message messageFromJson(JSONObject json) {
try {
if (!json.has("from") || !json.has("to") || !json.has("content")) {
logger.severe("Invalid message received! (field missing)");
return null;
}
if (json.getString("from").equals("server")) {
return null;
}
User from = SystemStateManager.getUser(json.getString("from"));
if (from == null) {
logger.severe("Invalid message received! (User from)");
return null;
}
String to = json.getString("to");
String content = json.getString("content");
Message message = new StandardMessage(from, null, content);
if (to.equals("broadcast")) {
message.setReceiver(to);
} else if (to.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
message.setReceiver(to);
} else {
message.setReceiver(SystemStateManager.getUser(to));
}
if (message.getReceiver() == null) {
logger.severe("Invalid message received! (User to)");
return null;
}
return message;
} catch (JSONException ignored) {
logger.severe("Invalid message received! (JSONException)");
return null;
}
}
/**
* Converts a JSON object to a request.
*
* @param json The JSON object to convert.
* @return The request representation of the JSON object.
*/
public static Request requestFromJson(JSONObject json) {
try {
if (!json.has("from") || !json.has("to") || !json.has("content")) {
logger.severe("Invalid request received! (field missing)");
return null;
}
User from = SystemStateManager.getUser(json.getString("from"));
if (from == null) {
logger.severe("Invalid request received! (User from)");
return null;
}
String to = json.getString("to");
String content = json.getString("content");
Request request = new StandardRequest(from, null, content);
if (to.equals("broadcast")) {
request.setReceiver(to);
} else if (to.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
request.setReceiver(to);
} else {
request.setReceiver(SystemStateManager.getUser(to));
}
if (request.getReceiver() == null) {
logger.severe("Invalid request received! (User to)");
return null;
}
return request;
} catch (JSONException ignored) {
logger.severe("Invalid request received! (JSONException)");
return null;
}
}
/// ! Protocol methods
/**
* Receives a message and processes it based on the connection type and the message content.
*
* @param connType the type of connection (DIRECT or BROADCAST)
* @param json the JSON object containing the message data
* @return always returns null
*/
public static String receiveMessage(ConnType connType, JSONObject json) {
Message message = messageFromJson(json);
if (message != null) {
Object Receiver = message.getReceiver();
if (Receiver instanceof User) {
SystemStateManager.addUserNotification((User) message.getReceiver(), message);
} else if (Receiver instanceof String) {
String receiverString = (String) Receiver;
if (receiverString.equals("broadcast")) {
Collection<User> users = SystemStateManager.getUsers();
for (User user : users) {
SystemStateManager.addUserNotification(user, message);
}
} else if (receiverString.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
Collection<User> users = SystemStateManager.getUsersFromGroup(receiverString);
for (User user : users) {
SystemStateManager.addUserNotification(user, message);
}
} else {
User user = SystemStateManager.getUser(receiverString);
if (user != null) {
SystemStateManager.addUserNotification(user, message);
}
}
}
SystemStateManager.addUserNotification(message.getSender(), message);
if (connType == ConnType.UNICAST) {
SystemStateManager.addNotificationToDeliver(message);
}
}
return null;
}
/**
* Receives a request and processes it based on the connection type, JSON data, and socket packet.
*
* @param connType The type of connection (DIRECT or INDIRECT).
* @param json The JSON object containing the request data.
* @param socketPacket The socket packet associated with the request.
* @return The response string.
*/
public static String receiveRequest(ConnType connType, JSONObject json, Object socketPacket) {
Request request = requestFromJson(json);
if (request != null) {
Object Receiver = request.getReceiver();
if (Receiver instanceof User) {
SystemStateManager.addUserNotification(request.getReceiver(), request);
} else if (Receiver instanceof String) {
String receiverString = (String) Receiver;
if (receiverString.equals("broadcast")) {
Collection<User> users = SystemStateManager.getUsers();
for (User user : users) {
SystemStateManager.addUserNotification(user, request);
}
} else if (receiverString.matches(
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {
Collection<User> users = SystemStateManager.getUsersFromGroup(receiverString);
for (User user : users) {
SystemStateManager.addUserNotification(user, request);
}
} else {
User user = SystemStateManager.getUser(receiverString);
if (user != null) {
SystemStateManager.addUserNotification(user, request);
}
}
}
SystemStateManager.addUserNotification(request.getSender(), request);
if (connType == ConnType.UNICAST) {
SystemStateManager.addNotificationToDeliver(request);
}
executorService.execute(new AcceptRequestHandler(connType, request));
}
return null;
}
}

View File

@@ -0,0 +1,193 @@
package server.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import server.Server;
import server.handlers.MessageHistoryHandler;
import server.structs.SystemStateManager;
import server.structs.intfaces.User;
import server.structs.implementations.StandardUser; // New concrete implementation
import shared.enums.Hierarchy;
/**
* Handles user registration, authentication, and communication in the emergency system.
* Manages user sessions and group memberships.
*
* @author 0x1eo
* @since 2024-12-13
*/
public class UserHandler {
private static final Logger logger = Logger.getLogger(UserHandler.class.getName());
private static final String MULTICAST_GROUP_PATTERN =
"^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
+ "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
// Prevent instantiation
private UserHandler() {}
/**
* Registers a new user in the system.
*
* @param json The registration details
* @param socketPacket The connection socket
* @return JSON response indicating success or failure
* @throws JSONException If registration data is malformed
*/
public static String register(JSONObject json, Object socketPacket) throws JSONException {
try {
String username = json.getString("username");
if (SystemStateManager.getUser(username) != null) {
logger.info("Registration failed: User already exists - " + username);
return createErrorResponse("User already exists!");
}
User user = createUser(json);
Socket socket = (Socket) socketPacket;
SystemStateManager.addUser(user);
SystemStateManager.addUserSocket(user, socket);
return createSuccessResponse();
} catch (IllegalArgumentException e) {
logger.log(Level.WARNING, "Registration failed: Invalid role", e);
return createErrorResponse("Invalid role!");
} catch (Exception e) {
logger.log(Level.SEVERE, "Registration failed: Unexpected error", e);
return e.getMessage();
}
}
/**
* Creates a new user instance from JSON data.
*/
private static User createUser(JSONObject json) throws JSONException {
return new StandardUser(
json.getString("username"),
json.getString("name"),
json.getString("password"),
Hierarchy.valueOf(json.getString("role").toUpperCase())
);
}
/**
* Authenticates a user and establishes their session.
*/
public static String login(JSONObject json, Object socketPacket) throws JSONException {
User user = SystemStateManager.getUser(json.getString("username"));
if (user == null) {
logger.info("Login failed: Invalid username");
return createErrorResponse("Invalid username!");
}
if (!user.getPassword().equals(json.getString("password"))) {
logger.info("Login failed: Invalid password for user " + user.getUsername());
return createErrorResponse("Invalid password!");
}
establishUserSession(user, (Socket) socketPacket);
return createSuccessResponse();
}
/**
* Sets up user session and starts message history handler.
*/
private static void establishUserSession(User user, Socket socket) {
SystemStateManager.addUserSocket(user, socket);
new Thread(new MessageHistoryHandler(user)).start();
}
/**
* Sends data to a user through their socket connection.
*/
public static void sendSomething(User user, String data) throws IOException {
try (Socket newSocket = createUserSocket(user);
PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true)) {
out.println(data);
}
}
/**
* Sends data and waits for a response.
*/
public static String sendAndReceiveSomething(User user, String data) throws IOException {
try (Socket newSocket = createUserSocket(user);
PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(newSocket.getInputStream()))) {
out.println(data);
return in.readLine();
}
}
private static Socket createUserSocket(User user) throws IOException {
Socket userSocket = SystemStateManager.getUserSocket(user);
return new Socket(userSocket.getInetAddress(), Server.USER_PORT);
}
/**
* Adds a user to a multicast group.
*/
public static String joinGroup(JSONObject json) throws JSONException {
if (!isValidGroupRequest(json)) {
return null;
}
User user = SystemStateManager.getUser(json.getString("username"));
if (user == null) {
logger.info("Group join failed: Invalid username");
return null;
}
try {
String group = json.getString("group");
SystemStateManager.getMulticastSocket().joinGroup(InetAddress.getByName(group));
SystemStateManager.addUserToGroup(group, user);
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to join multicast group", e);
}
return null;
}
private static boolean isValidGroupRequest(JSONObject json) {
if (!json.has("group") || !json.has("username")) {
logger.info("Group join failed: Missing required fields");
return false;
}
String group = json.getString("group");
if (!group.matches(MULTICAST_GROUP_PATTERN)) {
logger.info("Group join failed: Invalid group address - " + group);
return false;
}
return true;
}
private static String createSuccessResponse() {
return new JSONObject().put("response", "OK").toString();
}
private static String createErrorResponse(String message) {
return new JSONObject().put("response", message).toString();
}
public static String findUser(String username) {
User user = SystemStateManager.getUser(username);
if (user == null) {
return null;
}
return user.getUsername();
}
}

View File

@@ -0,0 +1,72 @@
package shared.enums;
import java.util.Arrays;
import java.util.Optional;
/**
* Represents different types of network connections used in the application.
*
* @author 0x1eo
* @since 2024-12-12
*/
public enum ConnType {
UNICAST("Unicast", "Point-to-point connection between two nodes"),
MULTICAST("Multicast", "One-to-many connection to a specific group of nodes"),
BROADCAST("Broadcast", "One-to-all connection reaching all nodes in the network");
private final String displayName;
private final String description;
ConnType(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
/**
* Gets the user-friendly display name of the connection type.
*
* @return the display name
*/
public String getDisplayName() {
return displayName;
}
/**
* Gets the description of the connection type.
*
* @return the description
*/
public String getDescription() {
return description;
}
/**
* Safely converts a string to a ConnType.
*
* @param value the string value to convert
* @return an Optional containing the ConnType if valid, empty Optional otherwise
*/
public static Optional<ConnType> fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return Optional.empty();
}
return Arrays.stream(values())
.filter(connType -> connType.name().equalsIgnoreCase(value.trim()))
.findFirst();
}
/**
* Checks if the connection type is suitable for group communication.
*
* @return true if the connection type supports group communication
*/
public boolean isGroupCapable() {
return this == MULTICAST || this == BROADCAST;
}
@Override
public String toString() {
return String.format("%s (%s)", displayName, description);
}
}

View File

@@ -0,0 +1,108 @@
package shared.enums;
import java.util.Arrays;
import java.util.Optional;
/**
* Represents the priority levels in the system's hierarchy.
* Used for tasks and user permissions.
*
* @author 0x1eo
* @since 2024-12-12
*/
public enum Hierarchy {
LOW(0, "Low Priority"),
MEDIUM(1, "Medium Priority"),
HIGH(2, "High Priority");
private final int value;
private final String displayName;
Hierarchy(int value, String displayName) {
this.value = value;
this.displayName = displayName;
}
/**
* Gets the numeric value associated with this hierarchy level.
*
* @return the numeric value of the hierarchy level
*/
public int getValue() {
return value;
}
/**
* Gets the display name of this hierarchy level.
*
* @return the user-friendly name of the hierarchy level
*/
public String getDisplayName() {
return displayName;
}
/**
* Finds a Hierarchy enum by its numeric value.
*
* @param value the numeric value to look up
* @return an Optional containing the Hierarchy if found, empty Optional otherwise
*/
public static Optional<Hierarchy> fromValue(int value) {
return Arrays.stream(values())
.filter(h -> h.value == value)
.findFirst();
}
/**
* Finds a Hierarchy enum by its name (case-insensitive).
*
* @param name the name to look up
* @return an Optional containing the Hierarchy if found, empty Optional otherwise
*/
public static Optional<Hierarchy> fromString(String name) {
if (name == null || name.trim().isEmpty()) {
return Optional.empty();
}
return Arrays.stream(values())
.filter(h -> h.name().equalsIgnoreCase(name.trim()))
.findFirst();
}
/**
* Gets all hierarchy values as strings.
*
* @return array of hierarchy names
*/
public static String[] getAllNames() {
return Arrays.stream(values())
.map(Hierarchy::name)
.toArray(String[]::new);
}
/**
* Gets all hierarchy display names.
*
* @return array of user-friendly hierarchy names
*/
public static String[] getAllDisplayNames() {
return Arrays.stream(values())
.map(Hierarchy::getDisplayName)
.toArray(String[]::new);
}
/**
* Compares this hierarchy level with another.
*
* @param other the hierarchy level to compare with
* @return true if this level is higher than the other
*/
public boolean isHigherThan(Hierarchy other) {
return this.value > other.value;
}
@Override
public String toString() {
return displayName;
}
}

View File

@@ -0,0 +1,90 @@
package shared.enums;
import java.util.Arrays;
import java.util.Optional;
/**
* Represents different types of message receivers in the communication system.
*
* @author 0x1eo
* @since 2024-12-12
*/
public enum RecvType {
USER("Single User", "Direct message to a specific user"),
GROUP("Group", "Message to a defined group of users"),
BROADCAST("Broadcast", "Message to all users in the network");
private final String displayName;
private final String description;
RecvType(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
/**
* Gets the user-friendly display name of the receiver type.
*
* @return the display name
*/
public String getDisplayName() {
return displayName;
}
/**
* Gets the description of the receiver type.
*
* @return the description
*/
public String getDescription() {
return description;
}
/**
* Determines if this receiver type supports multiple recipients.
*
* @return true if the receiver type supports multiple recipients
*/
public boolean isMultiReceiver() {
return this == GROUP || this == BROADCAST;
}
/**
* Safely converts a string to a RecvType.
*
* @param value the string value to convert
* @return an Optional containing the RecvType if valid, empty Optional otherwise
*/
public static Optional<RecvType> fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return Optional.empty();
}
return Arrays.stream(values())
.filter(type -> type.name().equalsIgnoreCase(value.trim()))
.findFirst();
}
/**
* Gets the appropriate receiver type for a given number of recipients.
*
* @param recipientCount the number of recipients
* @return the appropriate receiver type
*/
public static RecvType forRecipientCount(int recipientCount) {
if (recipientCount <= 0) {
throw new IllegalArgumentException("Recipient count must be positive");
}
return switch (recipientCount) {
case 1 -> USER;
case Integer.MAX_VALUE -> BROADCAST;
default -> GROUP;
};
}
@Override
public String toString() {
return String.format("%s (%s)", displayName, description);
}
}

View File

@@ -1,113 +0,0 @@
# =========================================================
# Traffic Simulation Configuration
# ---------------------------------------------------------
# All parameters controlling network layout, timing,
# and simulation behavior.
# =========================================================
# === 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 (3600 = 1 hour)
simulation.duration=60.0
# Vehicle arrival model: FIXED or POISSON
simulation.arrival.model=POISSON
# λ (lambda): average arrival rate (vehicles per second)
simulation.arrival.rate=0.5
# Fixed interval between arrivals (only used if model=FIXED)
simulation.arrival.fixed.interval=2.0
# === 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 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 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 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 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)
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.5
vehicle.crossing.time.light=2.0
vehicle.crossing.time.heavy=4.0
# === STATISTICS ===
# Interval between dashboard updates (seconds)
statistics.update.interval=10.0

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.