Refactor and enhance documentation across multiple classes (Analysis through DashboardStatistics)

This commit is contained in:
2025-12-07 19:33:40 +00:00
parent a2f9e725de
commit a8ce95e08c
20 changed files with 869 additions and 611 deletions

View File

@@ -271,14 +271,18 @@ public class IntersectionProcess {
/** /**
* Processa a fila de veículos quando um semáforo fica verde. * Processa a fila de veículos quando um semáforo fica verde.
* *
* <p>Para cada veículo na fila:</p> * <p>
* Para cada veículo na fila:
* </p>
* <ol> * <ol>
* <li>Calcula o tempo de travessia com base no tipo de veículo</li> * <li>Calcula o tempo de travessia com base no tipo de veículo</li>
* <li>Verifica se cabe na duração restante do sinal verde</li> * <li>Verifica se cabe na duração restante do sinal verde</li>
* <li>Agenda o evento de partida do veículo</li> * <li>Agenda o evento de partida do veículo</li>
* </ol> * </ol>
* *
* <p>Os veículos que não couberem no tempo verde ficam à espera do próximo ciclo.</p> * <p>
* Os veículos que não couberem no tempo verde ficam à espera do próximo ciclo.
* </p>
* *
* @param light o semáforo que acabou de ficar verde * @param light o semáforo que acabou de ficar verde
* @param currentTime o tempo atual da simulação em segundos * @param currentTime o tempo atual da simulação em segundos

View File

@@ -14,36 +14,61 @@ import java.util.TreeSet;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* Executes multiple simulation runs and aggregates results. * Responsável pela agregação e análise estatística de múltiplas execuções da simulação.
* Calculates statistical measures including mean, standard deviation, * <p>
* and confidence intervals across all runs. * Esta classe coleta resultados individuais ({@link SimulationRunResult}) e calcula
* métricas consolidadas, incluindo média, desvio padrão, mediana e intervalos de
* confiança de 95%. O objetivo é fornecer uma visão robusta do comportamento do
* sistema, mitigando a variância estocástica de execuções isoladas.
*/ */
public class MultiRunAnalyzer { public class MultiRunAnalyzer {
/** Lista acumulada de resultados de execuções individuais. */
private final List<SimulationRunResult> results; private final List<SimulationRunResult> results;
/** Identificador do ficheiro de configuração utilizado nas execuções. */
private final String configurationFile; private final String configurationFile;
/**
* Inicializa o analisador para um conjunto específico de configurações.
*
* @param configurationFile O caminho ou nome do ficheiro de configuração base.
*/
public MultiRunAnalyzer(String configurationFile) { public MultiRunAnalyzer(String configurationFile) {
this.configurationFile = configurationFile; this.configurationFile = configurationFile;
this.results = new ArrayList<>(); this.results = new ArrayList<>();
} }
/** /**
* Adds a completed simulation run result. * Adiciona o resultado de uma execução de simulação concluída ao conjunto de dados.
*
* @param result O objeto contendo as métricas da execução individual.
*/ */
public void addResult(SimulationRunResult result) { public void addResult(SimulationRunResult result) {
results.add(result); results.add(result);
} }
/** /**
* Gets the number of completed runs. * Retorna o número total de execuções armazenadas até o momento.
*
* @return O tamanho da lista de resultados.
*/ */
public int getRunCount() { public int getRunCount() {
return results.size(); return results.size();
} }
/** /**
* Generates a comprehensive statistical report. * Gera um relatório estatístico abrangente formatado em texto.
* <p>
* O relatório inclui:
* <ul>
* <li>Métricas globais (throughput, tempos de espera, tempos no sistema).</li>
* <li>Análise segmentada por tipo de veículo ({@link VehicleType}).</li>
* <li>Análise de gargalos por interseção (tamanhos de fila).</li>
* <li>Resumos brutos das execuções individuais.</li>
* </ul>
*
* @return Uma String contendo o relatório completo formatado.
*/ */
public String generateReport() { public String generateReport() {
if (results.isEmpty()) { if (results.isEmpty()) {
@@ -153,7 +178,13 @@ public class MultiRunAnalyzer {
} }
/** /**
* Analyzes a single metric and returns formatted statistics. * Analisa uma métrica específica e retorna as estatísticas formatadas.
* <p>
* Calcula média, desvio padrão, mediana, intervalo de confiança (95%) e extremos (min/max).
*
* @param metricName O nome descritivo da métrica (ex: "Tempo de Espera").
* @param values A lista de valores numéricos brutos extraídos das execuções.
* @return Uma string formatada com os dados estatísticos.
*/ */
private String analyzeMetric(String metricName, List<Double> values) { private String analyzeMetric(String metricName, List<Double> values) {
if (values.isEmpty() || values.stream().allMatch(v -> v == 0.0)) { if (values.isEmpty() || values.stream().allMatch(v -> v == 0.0)) {
@@ -177,7 +208,13 @@ public class MultiRunAnalyzer {
} }
/** /**
* Extracts values using a lambda function. * Extrai valores numéricos dos resultados de simulação usando uma função mapeadora.
* <p>
* Utilizado internamente para transformar a lista de objetos complexos {@link SimulationRunResult}
* em listas simples de Doubles para processamento estatístico.
*
* @param extractor Função lambda que define qual campo extrair de cada resultado.
* @return Lista de valores double correspondentes.
*/ */
private List<Double> extractValues(java.util.function.Function<SimulationRunResult, Double> extractor) { private List<Double> extractValues(java.util.function.Function<SimulationRunResult, Double> extractor) {
List<Double> values = new ArrayList<>(); List<Double> values = new ArrayList<>();
@@ -188,7 +225,10 @@ public class MultiRunAnalyzer {
} }
/** /**
* Saves the report to a file. * Persiste o relatório gerado num ficheiro de texto.
*
* @param filename O caminho do ficheiro de destino.
* @throws IOException Se ocorrer um erro de escrita no disco.
*/ */
public void saveReport(String filename) throws IOException { public void saveReport(String filename) throws IOException {
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) {
@@ -197,14 +237,25 @@ public class MultiRunAnalyzer {
} }
/** /**
* Generates a CSV summary for easy import into spreadsheet tools. * Gera um resumo em formato CSV para fácil importação em ferramentas de planilha.
* <p>
* Este método atua como um wrapper para {@link #saveCSVSummary(String)}.
*
* @param filename O caminho do ficheiro CSV de destino.
* @throws IOException Se ocorrer um erro de escrita no disco.
*/ */
public void saveCSV(String filename) throws IOException { public void saveCSV(String filename) throws IOException {
saveCSVSummary(filename); saveCSVSummary(filename);
} }
/** /**
* Generates a CSV summary for easy import into spreadsheet tools. * Gera e grava o sumário CSV detalhado com métricas chave por execução.
* <p>
* Colunas incluídas: Execução, VeículosGerados, VeículosCompletados, TaxaConclusão,
* TempoMédioSistema, TempoMédioEspera, TempoMínimoSistema, TempoMáximoSistema.
*
* @param filename O caminho do ficheiro CSV de destino.
* @throws IOException Se ocorrer um erro de escrita no disco.
*/ */
public void saveCSVSummary(String filename) throws IOException { public void saveCSVSummary(String filename) throws IOException {
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)))) {

View File

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

View File

@@ -6,8 +6,12 @@ import java.util.Map;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* Stores the results of a single simulation run. * Encapsula os dados telemétricos e estatísticos resultantes de uma única execução da simulação.
* Contains all key metrics for post-simulation analysis. * <p>
* Esta classe atua como um registo estruturado de métricas de desempenho, armazenando
* dados de latência (tempos de sistema/espera), vazão (throughput) e ocupação de recursos
* (tamanhos de fila). Os dados aqui contidos servem como base para a análise
* estatística agregada realizada pelo {@link MultiRunAnalyzer}.
*/ */
public class SimulationRunResult { public class SimulationRunResult {
@@ -17,11 +21,22 @@ public class SimulationRunResult {
private final long endTimeMillis; private final long endTimeMillis;
// Global metrics // Global metrics
/** Total de veículos instanciados pelos geradores durante a execução. */
private int totalVehiclesGenerated; private int totalVehiclesGenerated;
/** Total de veículos que completaram o percurso e saíram do sistema com sucesso. */
private int totalVehiclesCompleted; private int totalVehiclesCompleted;
/** Média global do tempo total (em segundos) desde a geração até a saída. */
private double averageSystemTime; // seconds private double averageSystemTime; // seconds
/** Menor tempo de sistema registado (em segundos). */
private double minSystemTime; // seconds private double minSystemTime; // seconds
/** Maior tempo de sistema registado (em segundos). */
private double maxSystemTime; // seconds private double maxSystemTime; // seconds
/** Média global do tempo (em segundos) que os veículos passaram parados em filas. */
private double averageWaitingTime; // seconds private double averageWaitingTime; // seconds
// Per-type metrics // Per-type metrics
@@ -34,6 +49,12 @@ public class SimulationRunResult {
private final Map<String, Double> avgQueueSizeByIntersection; private final Map<String, Double> avgQueueSizeByIntersection;
private final Map<String, Integer> vehiclesProcessedByIntersection; private final Map<String, Integer> vehiclesProcessedByIntersection;
/**
* Inicializa um novo contentor de resultados para uma execução específica.
*
* @param runNumber O identificador sequencial desta execução.
* @param configurationFile O ficheiro de configuração utilizado.
*/
public SimulationRunResult(int runNumber, String configurationFile) { public SimulationRunResult(int runNumber, String configurationFile) {
this.runNumber = runNumber; this.runNumber = runNumber;
this.configurationFile = configurationFile; this.configurationFile = configurationFile;
@@ -48,6 +69,10 @@ public class SimulationRunResult {
this.vehiclesProcessedByIntersection = new HashMap<>(); this.vehiclesProcessedByIntersection = new HashMap<>();
} }
/**
* Sinaliza o fim da recolha de dados para esta execução.
* (Placeholder para lógica de finalização de timestamps).
*/
public void markCompleted() { public void markCompleted() {
// This will be called when the run finishes // This will be called when the run finishes
} }
@@ -57,6 +82,11 @@ public class SimulationRunResult {
public String getConfigurationFile() { return configurationFile; } public String getConfigurationFile() { return configurationFile; }
public long getStartTimeMillis() { return startTimeMillis; } public long getStartTimeMillis() { return startTimeMillis; }
public long getEndTimeMillis() { return endTimeMillis; } public long getEndTimeMillis() { return endTimeMillis; }
/**
* Calcula a duração total da execução em milissegundos.
* @return Delta entre fim e início.
*/
public long getDurationMillis() { return endTimeMillis - startTimeMillis; } public long getDurationMillis() { return endTimeMillis - startTimeMillis; }
public int getTotalVehiclesGenerated() { return totalVehiclesGenerated; } public int getTotalVehiclesGenerated() { return totalVehiclesGenerated; }
@@ -66,21 +96,50 @@ public class SimulationRunResult {
public double getMaxSystemTime() { return maxSystemTime; } public double getMaxSystemTime() { return maxSystemTime; }
public double getAverageWaitingTime() { return averageWaitingTime; } public double getAverageWaitingTime() { return averageWaitingTime; }
/**
* Retorna o mapeamento de contagem de veículos por tipo.
* @return Uma cópia defensiva do mapa (snapshot).
*/
public Map<VehicleType, Integer> getVehicleCountByType() { public Map<VehicleType, Integer> getVehicleCountByType() {
return new HashMap<>(vehicleCountByType); return new HashMap<>(vehicleCountByType);
} }
/**
* Retorna o tempo médio no sistema segmentado por tipo de veículo.
* @return Uma cópia defensiva do mapa (snapshot).
*/
public Map<VehicleType, Double> getAvgSystemTimeByType() { public Map<VehicleType, Double> getAvgSystemTimeByType() {
return new HashMap<>(avgSystemTimeByType); return new HashMap<>(avgSystemTimeByType);
} }
/**
* Retorna o tempo médio de espera segmentado por tipo de veículo.
* @return Uma cópia defensiva do mapa (snapshot).
*/
public Map<VehicleType, Double> getAvgWaitTimeByType() { public Map<VehicleType, Double> getAvgWaitTimeByType() {
return new HashMap<>(avgWaitTimeByType); return new HashMap<>(avgWaitTimeByType);
} }
/**
* Retorna o tamanho máximo de fila registado por interseção (gargalos).
* @return Uma cópia defensiva do mapa (snapshot).
*/
public Map<String, Integer> getMaxQueueSizeByIntersection() { public Map<String, Integer> getMaxQueueSizeByIntersection() {
return new HashMap<>(maxQueueSizeByIntersection); return new HashMap<>(maxQueueSizeByIntersection);
} }
/**
* Retorna o tamanho médio das filas por interseção.
* @return Uma cópia defensiva do mapa (snapshot).
*/
public Map<String, Double> getAvgQueueSizeByIntersection() { public Map<String, Double> getAvgQueueSizeByIntersection() {
return new HashMap<>(avgQueueSizeByIntersection); return new HashMap<>(avgQueueSizeByIntersection);
} }
/**
* Retorna o total de veículos processados (throughput) por interseção.
* @return Uma cópia defensiva do mapa (snapshot).
*/
public Map<String, Integer> getVehiclesProcessedByIntersection() { public Map<String, Integer> getVehiclesProcessedByIntersection() {
return new HashMap<>(vehiclesProcessedByIntersection); return new HashMap<>(vehiclesProcessedByIntersection);
} }
@@ -124,6 +183,10 @@ public class SimulationRunResult {
vehiclesProcessedByIntersection.put(intersection, count); vehiclesProcessedByIntersection.put(intersection, count);
} }
/**
* Gera uma representação textual resumida das métricas principais da execução.
* Útil para logs rápidos e debugging.
*/
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format(

View File

@@ -5,13 +5,19 @@ import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* Statistical analysis utilities for simulation results. * Utilitário estático para processamento matemático e análise estatística dos dados da simulação.
* Calculates mean, standard deviation, and confidence intervals. * <p>
* Esta classe fornece algoritmos para cálculo de medidas de tendência central (média, mediana),
* dispersão (desvio padrão amostral) e inferência estatística (Intervalos de Confiança).
* É utilizada para normalizar e validar os resultados estocásticos obtidos através de
* múltiplas execuções do sistema.
*/ */
public class StatisticalAnalysis { public class StatisticalAnalysis {
/** /**
* Calculates the mean (average) of a list of values. * Calcula a média aritmética de um conjunto de valores.
* * @param values Lista de valores numéricos (double).
* @return A soma dos valores dividida pelo tamanho da amostra, ou 0.0 se a lista for nula/vazia.
*/ */
public static double mean(List<Double> values) { public static double mean(List<Double> values) {
if (values == null || values.isEmpty()) { if (values == null || values.isEmpty()) {
@@ -25,7 +31,13 @@ public class StatisticalAnalysis {
} }
/** /**
* Calculates the sample standard deviation. * Calcula o desvio padrão amostral (sample standard deviation).
* <p>
* Utiliza o denominador {@code n - 1} (Correção de Bessel) para fornecer um
* estimador não viesado da variância populacional, adequado para as amostras
* de simulação.
* * @param values Lista de observações.
* @return O desvio padrão calculado, ou 0.0 se o tamanho da amostra for < 2.
*/ */
public static double standardDeviation(List<Double> values) { public static double standardDeviation(List<Double> values) {
if (values == null || values.size() < 2) { if (values == null || values.size() < 2) {
@@ -45,10 +57,13 @@ public class StatisticalAnalysis {
} }
/** /**
* Calculates the 95% confidence interval for the mean. * Calcula o Intervalo de Confiança (IC) de 95% para a média.
* Uses t-distribution for small samples (n < 30). * <p>
* * Utiliza a distribuição t de Student para maior precisão em amostras pequenas (n < 30),
* @return Array of [lowerBound, upperBound] * onde a aproximação pela distribuição Normal (Z) seria inadequada. O intervalo define
* a faixa onde a verdadeira média populacional reside com 95% de probabilidade.
* * @param values Lista de observações.
* @return Um array de double onde índice 0 é o limite inferior e índice 1 é o limite superior.
*/ */
public static double[] confidenceInterval95(List<Double> values) { public static double[] confidenceInterval95(List<Double> values) {
if (values == null || values.size() < 2) { if (values == null || values.size() < 2) {
@@ -76,8 +91,12 @@ public class StatisticalAnalysis {
} }
/** /**
* Returns the t-critical value for 95% confidence interval. * Retorna o valor crítico t (t-score) para um IC de 95% (bicaudal).
* Approximations for common degrees of freedom (n-1). * <p>
* Baseia-se nos graus de liberdade (gl = n - 1). Para amostras grandes (gl >= 30),
* aproxima-se do valor Z de 1.96.
* * @param sampleSize O tamanho da amostra (n).
* @return O fator multiplicativo t apropriado.
*/ */
private static double getTCriticalValue(int sampleSize) { private static double getTCriticalValue(int sampleSize) {
int df = sampleSize - 1; // degrees of freedom int df = sampleSize - 1; // degrees of freedom
@@ -94,7 +113,9 @@ public class StatisticalAnalysis {
} }
/** /**
* Calculates the minimum value. * Identifica o valor mínimo absoluto na amostra.
* * @param values Lista de valores.
* @return O menor valor encontrado.
*/ */
public static double min(List<Double> values) { public static double min(List<Double> values) {
if (values == null || values.isEmpty()) { if (values == null || values.isEmpty()) {
@@ -104,7 +125,9 @@ public class StatisticalAnalysis {
} }
/** /**
* Calculates the maximum value. * Identifica o valor máximo absoluto na amostra.
* * @param values Lista de valores.
* @return O maior valor encontrado.
*/ */
public static double max(List<Double> values) { public static double max(List<Double> values) {
if (values == null || values.isEmpty()) { if (values == null || values.isEmpty()) {
@@ -114,7 +137,12 @@ public class StatisticalAnalysis {
} }
/** /**
* Calculates the median value. * Calcula a mediana da amostra.
* <p>
* <b>Nota de Desempenho:</b> Este método ordena uma cópia da lista, resultando em
* complexidade O(n log n).
* * @param values Lista de valores.
* @return O valor central (ou média dos dois centrais) da distribuição ordenada.
*/ */
public static double median(List<Double> values) { public static double median(List<Double> values) {
if (values == null || values.isEmpty()) { if (values == null || values.isEmpty()) {
@@ -133,7 +161,12 @@ public class StatisticalAnalysis {
} }
/** /**
* Formats a statistical summary as a string. * Formata um sumário estatístico completo para uma métrica específica.
* <p>
* Útil para logging e geração de relatórios textuais.
* * @param metricName Nome da métrica a ser exibida.
* @param values Os dados brutos associados à métrica.
* @return String formatada contendo Média, Desvio Padrão, IC95%, Min, Max e N.
*/ */
public static String formatSummary(String metricName, List<Double> values) { public static String formatSummary(String metricName, List<Double> values) {
if (values == null || values.isEmpty()) { if (values == null || values.isEmpty()) {

View File

@@ -14,17 +14,28 @@ import java.util.Properties;
import com.google.gson.Gson; import com.google.gson.Gson;
/** /**
* Carrega e gere configurações da simulação. * Responsável pelo carregamento, validação e acesso centralizado às configurações da simulação.
* * <p>
* <p>Lê propriedades de um ficheiro .properties e fornece getters * Esta classe atua como uma fachada (Facade) para os parâmetros do sistema, abstraindo a origem
* type-safe com valores padrão para robustez. * dos dados (ficheiros {@code .properties} ou JSON). Implementa uma estratégia robusta de
* carregamento de recursos, suportando tanto caminhos absolutos do sistema de ficheiros quanto
* recursos embutidos no <i>classpath</i>.
* <p>
* Além de propriedades chave-valor simples, gerencia a desserialização da topologia da rede
* através da classe interna {@link NetworkConfig}.
*/ */
public class SimulationConfig { public class SimulationConfig {
/** Propriedades carregadas do ficheiro */ /** Armazenamento em memória das propriedades chave-valor carregadas. */
private final Properties properties; private final Properties properties;
/** Estrutura hierárquica da configuração da rede carregada via JSON. */
private NetworkConfig networkConfig; private NetworkConfig networkConfig;
/**
* Objeto de transferência de dados (DTO) que representa a configuração global da rede.
* Mapeado a partir do ficheiro {@code network_config.json}.
*/
public static class NetworkConfig { public static class NetworkConfig {
private List<IntersectionConfig> intersections; private List<IntersectionConfig> intersections;
@@ -33,36 +44,45 @@ public class SimulationConfig {
} }
} }
/**
* DTO que representa a configuração de uma única interseção na topologia.
*/
public static class IntersectionConfig { public static class IntersectionConfig {
private String id; private String id;
private List<String> lights; private List<String> lights;
private Map<String, String> routes; private Map<String, String> routes;
/** @return O identificador único da interseção (ex: "Cr1"). */
public String getId() { public String getId() {
return id; return id;
} }
/** @return Lista de identificadores dos semáforos associados a esta interseção. */
public List<String> getLights() { public List<String> getLights() {
return lights; return lights;
} }
/** @return Mapa de roteamento definindo destinos alcançáveis e seus próximos saltos. */
public Map<String, String> getRoutes() { public Map<String, String> getRoutes() {
return routes; return routes;
} }
} }
/** /**
* Carrega propriedades do ficheiro especificado. * Inicializa o gestor de configuração carregando propriedades do caminho especificado.
* * * <p>Implementa uma estratégia de carregamento em cascata (fallback) para garantir robustez
* <p>Tenta múltiplas estratégias: * em diferentes ambientes de execução (IDE, JAR, Docker):
* <ol> * <ol>
* <li>Caminho direto no sistema de ficheiros * <li><b>Sistema de Ficheiros Direto:</b> Tenta carregar do caminho absoluto ou relativo.</li>
* <li>Recurso no classpath (com normalização automática) * <li><b>Classpath (Contexto):</b> Tenta carregar via {@code Thread.currentThread().getContextClassLoader()},
* <li>Recurso no classpath com barra inicial * normalizando prefixos como "src/main/resources" ou "classpath:".</li>
* <li><b>Classpath (Classe):</b> Tenta carregar via {@code SimulationConfig.class.getResourceAsStream},
* útil para recursos na raiz do JAR.</li>
* </ol> * </ol>
* *
* @param filePath caminho do ficheiro .properties * @param filePath O caminho ou nome do recurso do ficheiro {@code .properties}.
* @throws IOException se o ficheiro não for encontrado * @throws IOException Se o ficheiro não puder ser localizado em nenhuma das estratégias,
* com uma mensagem detalhada das tentativas falhadas.
*/ */
public SimulationConfig(String filePath) throws IOException { public SimulationConfig(String filePath) throws IOException {
properties = new Properties(); properties = new Properties();
@@ -135,6 +155,12 @@ public class SimulationConfig {
throw new IOException(errorMsg.toString(), fileSystemException); throw new IOException(errorMsg.toString(), fileSystemException);
} }
/**
* Carrega a configuração da topologia de rede a partir do ficheiro "network_config.json".
* <p>
* Utiliza a biblioteca Gson para desserialização. Em caso de falha, emite um aviso para o
* {@code System.err} mas não aborta a execução, permitindo o uso de defaults ou redes vazias.
*/
private void loadNetworkConfig() { private void loadNetworkConfig() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) { try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) {
if (is == null) { if (is == null) {
@@ -151,6 +177,10 @@ public class SimulationConfig {
} }
} }
/**
* Retorna a configuração estruturada da rede.
* @return Objeto {@link NetworkConfig} ou null se o carregamento falhou.
*/
public NetworkConfig getNetworkConfig() { public NetworkConfig getNetworkConfig() {
return networkConfig; return networkConfig;
} }
@@ -158,56 +188,50 @@ public class SimulationConfig {
// --- Network configurations --- // --- Network configurations ---
/** /**
* Gets the host address for a specific intersection. * Obtém o endereço de host (nome DNS ou IP) para uma interseção específica.
* * * @param intersectionId O ID da interseção (ex: "Cr1").
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @return O host configurado ou "localhost" por omissão.
* @return The host (e.g., "localhost").
*/ */
public String getIntersectionHost(String intersectionId) { public String getIntersectionHost(String intersectionId) {
return properties.getProperty("intersection." + intersectionId + ".host", "localhost"); return properties.getProperty("intersection." + intersectionId + ".host", "localhost");
} }
/** /**
* Gets the port number for a specific intersection. * Obtém a porta de escuta TCP para uma interseção específica.
* * * @param intersectionId O ID da interseção (ex: "Cr1").
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @return O número da porta. Retorna 0 se não configurado.
* @return The port number.
*/ */
public int getIntersectionPort(String intersectionId) { public int getIntersectionPort(String intersectionId) {
return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0")); return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0"));
} }
/** /**
* Gets the host address for the dashboard server. * Obtém o endereço de host do servidor de Dashboard (monitorização).
* * @return O host do dashboard (padrão: "localhost").
* @return The dashboard host.
*/ */
public String getDashboardHost() { public String getDashboardHost() {
return properties.getProperty("dashboard.host", "localhost"); return properties.getProperty("dashboard.host", "localhost");
} }
/** /**
* Gets the port number for the dashboard server. * Obtém a porta de conexão do servidor de Dashboard.
* * @return A porta do dashboard (padrão: 9000).
* @return The dashboard port.
*/ */
public int getDashboardPort() { public int getDashboardPort() {
return Integer.parseInt(properties.getProperty("dashboard.port", "9000")); return Integer.parseInt(properties.getProperty("dashboard.port", "9000"));
} }
/** /**
* Gets the host address for the exit node. * Obtém o endereço de host do nó de saída (Exit Node), para onde os veículos são encaminhados ao sair da malha.
* * @return O host do nó de saída (padrão: "localhost").
* @return The exit node host.
*/ */
public String getExitHost() { public String getExitHost() {
return properties.getProperty("exit.host", "localhost"); return properties.getProperty("exit.host", "localhost");
} }
/** /**
* Gets the port number for the exit node. * Obtém a porta de conexão do nó de saída.
* * @return A porta do nó de saída (padrão: 9001).
* @return The exit node port.
*/ */
public int getExitPort() { public int getExitPort() {
return Integer.parseInt(properties.getProperty("exit.port", "9001")); return Integer.parseInt(properties.getProperty("exit.port", "9001"));
@@ -216,64 +240,64 @@ public class SimulationConfig {
// --- Simulation configurations --- // --- Simulation configurations ---
/** /**
* Gets the total duration of the simulation in virtual seconds. * Define a duração total da execução da simulação em segundos virtuais.
* * @return A duração em segundos (padrão: 3600).
* @return The simulation duration.
*/ */
public double getSimulationDuration() { public double getSimulationDuration() {
return Double.parseDouble(properties.getProperty("simulation.duration", "3600")); return Double.parseDouble(properties.getProperty("simulation.duration", "3600"));
} }
/** /**
* Get time scaling factor for visualization. * Obtém o fator de escala temporal para visualização/execução.
* 0 = instant (pure DES), 0.01 = 100x speed, 0.1 = 10x speed, 1.0 = real-time * <ul>
* <li>0.0: Execução instantânea (DES puro, velocidade máxima).</li>
* <li>1.0: Tempo real (1 segundo simulado = 1 segundo real).</li>
* <li>0.01: Acelerado 100x.</li>
* </ul>
* @return O fator de escala.
*/ */
public double getTimeScale() { public double getTimeScale() {
return Double.parseDouble(properties.getProperty("simulation.time.scale", "0")); return Double.parseDouble(properties.getProperty("simulation.time.scale", "0"));
} }
/** /**
* Gets the drain time (in virtual seconds) to allow vehicles to exit after * Obtém o tempo de "drenagem" (drain time) em segundos virtuais.
* generation stops. * <p>
* * Este é o período adicional executado após o fim da geração de veículos para permitir
* @return The drain time. * que os veículos restantes no sistema completem os seus percursos.
* @return O tempo de drenagem (padrão: 60.0s).
*/ */
public double getDrainTime() { public double getDrainTime() {
return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0")); return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0"));
} }
/** /**
* Gets the vehicle arrival model ("POISSON" or "FIXED"). * Determina o modelo estocástico utilizado para a chegada de veículos.
* * @return "POISSON" (distribuição exponencial) ou "FIXED" (intervalo determinístico).
* @return The arrival model as a string.
*/ */
public String getArrivalModel() { public String getArrivalModel() {
return properties.getProperty("simulation.arrival.model", "POISSON"); return properties.getProperty("simulation.arrival.model", "POISSON");
} }
/** /**
* Gets the average arrival rate (lambda) for the POISSON model. * Obtém a taxa média de chegada (lambda) para o modelo Poisson.
* This represents the average number of vehicles arriving per second. * @return Veículos por segundo (padrão: 0.5).
*
* @return The arrival rate.
*/ */
public double getArrivalRate() { public double getArrivalRate() {
return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5")); return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5"));
} }
/** /**
* Gets the fixed time interval between vehicle arrivals for the FIXED model. * Obtém o intervalo fixo entre chegadas para o modelo determinístico.
* * @return O intervalo em segundos (padrão: 2.0).
* @return The fixed interval in seconds.
*/ */
public double getFixedArrivalInterval() { public double getFixedArrivalInterval() {
return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0")); return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0"));
} }
/** /**
* Gets the routing policy to use for vehicle route selection. * Obtém a política de roteamento utilizada pelos veículos para navegar na malha.
* * @return A política: "RANDOM", "SHORTEST_PATH" ou "LEAST_CONGESTED".
* @return The routing policy (RANDOM, SHORTEST_PATH, or LEAST_CONGESTED).
*/ */
public String getRoutingPolicy() { public String getRoutingPolicy() {
return properties.getProperty("simulation.routing.policy", "RANDOM"); return properties.getProperty("simulation.routing.policy", "RANDOM");
@@ -282,11 +306,10 @@ public class SimulationConfig {
// --- Traffic light configurations --- // --- Traffic light configurations ---
/** /**
* Gets the duration of the GREEN light state for a specific traffic light. * Obtém a duração do estado VERDE para um semáforo específico.
* * * @param intersectionId ID da interseção.
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @param direction Direção do fluxo (ex: "North").
* @param direction The direction of the light (e.g., "North"). * @return Duração em segundos (padrão: 30.0).
* @return The green light time in seconds.
*/ */
public double getTrafficLightGreenTime(String intersectionId, String direction) { public double getTrafficLightGreenTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".green"; String key = "trafficlight." + intersectionId + "." + direction + ".green";
@@ -294,11 +317,10 @@ public class SimulationConfig {
} }
/** /**
* Gets the duration of the RED light state for a specific traffic light. * Obtém a duração do estado VERMELHO para um semáforo específico.
* * * @param intersectionId ID da interseção.
* @param intersectionId The ID of the intersection (e.g., "Cr1"). * @param direction Direção do fluxo.
* @param direction The direction of the light (e.g., "North"). * @return Duração em segundos (padrão: 30.0).
* @return The red light time in seconds.
*/ */
public double getTrafficLightRedTime(String intersectionId, String direction) { public double getTrafficLightRedTime(String intersectionId, String direction) {
String key = "trafficlight." + intersectionId + "." + direction + ".red"; String key = "trafficlight." + intersectionId + "." + direction + ".red";
@@ -308,83 +330,74 @@ public class SimulationConfig {
// --- Vehicle configurations --- // --- Vehicle configurations ---
/** /**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type LIGHT. * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo LIGEIRO (LIGHT).
* * @return Probabilidade (padrão: 0.7).
* @return The probability for LIGHT vehicles.
*/ */
public double getLightVehicleProbability() { public double getLightVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7")); return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7"));
} }
/** /**
* Gets the average time it takes a LIGHT vehicle to cross an intersection. * Tempo médio necessário para um veículo LIGEIRO atravessar uma interseção.
* * @return Tempo em segundos (padrão: 2.0).
* @return The crossing time in seconds.
*/ */
public double getLightVehicleCrossingTime() { public double getLightVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0")); return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0"));
} }
/** /**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type BIKE. * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo BICICLETA (BIKE).
* * @return Probabilidade (padrão: 0.0).
* @return The probability for BIKE vehicles.
*/ */
public double getBikeVehicleProbability() { public double getBikeVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0")); return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0"));
} }
/** /**
* Gets the average time it takes a BIKE vehicle to cross an intersection. * Tempo médio necessário para uma BICICLETA atravessar uma interseção.
* * @return Tempo em segundos (padrão: 1.5).
* @return The crossing time in seconds.
*/ */
public double getBikeVehicleCrossingTime() { public double getBikeVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5")); return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5"));
} }
/** /**
* Gets the probability (0.0 to 1.0) that a generated vehicle is of type HEAVY. * Probabilidade (0.0 a 1.0) de geração de um veículo PESADO (HEAVY).
* * @return Probabilidade (padrão: 0.0).
* @return The probability for HEAVY vehicles.
*/ */
public double getHeavyVehicleProbability() { public double getHeavyVehicleProbability() {
return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0")); return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0"));
} }
/** /**
* Gets the average time it takes a HEAVY vehicle to cross an intersection. * Tempo médio necessário para um veículo PESADO atravessar uma interseção.
* * @return Tempo em segundos (padrão: 4.0).
* @return The crossing time in seconds.
*/ */
public double getHeavyVehicleCrossingTime() { public double getHeavyVehicleCrossingTime() {
return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0")); return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0"));
} }
/** /**
* Gets the base travel time between intersections for light vehicles. * Define o tempo base de viagem entre interseções para veículos padrão.
* * @return Tempo em segundos (padrão: 8.0).
* @return The base travel time in seconds.
*/ */
public double getBaseTravelTime() { public double getBaseTravelTime() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0")); return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0"));
} }
/** /**
* Gets the travel time multiplier for bike vehicles. * Multiplicador de tempo de viagem para bicicletas.
* Bike travel time = base time × this multiplier. * <p>Tempo efetivo = Base * Multiplicador.
* * @return Fator multiplicativo (padrão: 0.5).
* @return The multiplier for bike travel time.
*/ */
public double getBikeTravelTimeMultiplier() { public double getBikeTravelTimeMultiplier() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5")); return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5"));
} }
/** /**
* Gets the travel time multiplier for heavy vehicles. * Multiplicador de tempo de viagem para veículos pesados.
* Heavy vehicle travel time = base time × this multiplier. * <p>Tempo efetivo = Base * Multiplicador.
* * @return Fator multiplicativo (padrão: 4.0).
* @return The multiplier for heavy vehicle travel time.
*/ */
public double getHeavyTravelTimeMultiplier() { public double getHeavyTravelTimeMultiplier() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0")); return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0"));
@@ -393,9 +406,8 @@ public class SimulationConfig {
// --- Statistics --- // --- Statistics ---
/** /**
* Gets the interval (in virtual seconds) between periodic statistics updates. * Intervalo de tempo (em segundos virtuais) para agregação e envio de estatísticas periódicas.
* * @return Intervalo de atualização (padrão: 1.0).
* @return The statistics update interval.
*/ */
public double getStatisticsUpdateInterval() { public double getStatisticsUpdateInterval() {
return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0")); return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0"));
@@ -404,21 +416,19 @@ public class SimulationConfig {
// --- Generic getters --- // --- Generic getters ---
/** /**
* Generic method to get any property as a string, with a default value. * Recupera uma propriedade genérica como String, com valor padrão de segurança.
* * * @param key A chave da propriedade.
* @param key The property key. * @param defaultValue O valor a retornar caso a chave não exista.
* @param defaultValue The value to return if the key is not found. * @return O valor da propriedade ou o default.
* @return The property value or the default.
*/ */
public String getProperty(String key, String defaultValue) { public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue); return properties.getProperty(key, defaultValue);
} }
/** /**
* Generic method to get any property as a string. * Recupera uma propriedade genérica como String.
* * * @param key A chave da propriedade.
* @param key The property key. * @return O valor da propriedade ou null se não encontrada.
* @return The property value, or null if not found.
*/ */
public String getProperty(String key) { public String getProperty(String key) {
return properties.getProperty(key); return properties.getProperty(key);

View File

@@ -24,47 +24,53 @@ import sd.serialization.SerializationException;
import sd.util.VehicleGenerator; import sd.util.VehicleGenerator;
/** /**
* Coordenador central da simulação distribuída. * Coordenador central da arquitetura de simulação distribuída.
* * <p>
* <p>Responsabilidades: * Este processo atua como o "cérebro" da simulação, sendo responsável por:
* <ol> * <ol>
* <li>Gerar veículos segundo modelo configurado (Poisson/Fixed) * <li><b>Orquestração DES:</b> Gerir o relógio global ({@link SimulationClock}) e a fila de eventos prioritária.</li>
* <li>Injetar veículos nas interseções de entrada * <li><b>Geração de Carga:</b> Injetar veículos na malha viária seguindo distribuições estocásticas (Poisson) ou determinísticas.</li>
* <li>Gerir relógio global e sincronizar componentes * <li><b>Encaminhamento Dinâmico:</b> Decidir as rotas dos veículos com base na política ativa (Random, Shortest Path, Least Congested).</li>
* <li><b>Sincronização:</b> Garantir que todos os nós (Interseções e Dashboard) operem em uníssono.</li>
* </ol> * </ol>
*
* <p>Usa motor DES para agendar eventos de geração com precisão.
* Mantém fila de prioridade e processa eventos em ordem cronológica.
*/ */
public class CoordinatorProcess { public class CoordinatorProcess {
private final SimulationConfig config; private final SimulationConfig config;
private final VehicleGenerator vehicleGenerator; private final VehicleGenerator vehicleGenerator;
/** Mapa de clientes TCP persistentes para cada interseção (Worker Nodes). */
private final Map<String, SocketClient> intersectionClients; private final Map<String, SocketClient> intersectionClients;
private SocketClient dashboardClient; private SocketClient dashboardClient;
// Componentes DES (Discrete Event Simulation)
private final SimulationClock clock; private final SimulationClock clock;
private final EventQueue eventQueue; private final EventQueue eventQueue;
private final EventLogger eventLogger; private final EventLogger eventLogger;
// Estado da simulação
private int vehicleCounter; private int vehicleCounter;
private boolean running; private boolean running;
private double timeScale; private double timeScale;
private RouteSelector currentRouteSelector; private RouteSelector currentRouteSelector;
/** Referência para estatísticas do dashboard para polling de mudanças de política. */
private DashboardStatistics dashboardStatistics; private DashboardStatistics dashboardStatistics;
/** /**
* Local tracking of intersection queue sizes for dynamic routing. * Monitorização local (aproximada) dos tamanhos de fila nas interseções.
* * <p>
* <p>This approximation tracks queue sizes by incrementing when vehicles are sent * Utilizado exclusivamente pela política {@link LeastCongestedRouteSelector}.
* to intersections. While not perfectly accurate (doesn't track departures in real-time), * O coordenador incrementa este contador ao enviar um veículo para uma interseção.
* it provides useful congestion information for the LEAST_CONGESTED routing policy.</p> * Nota: Esta é uma visão "borda" (edge) e pode não refletir a saída em tempo real
* * dos veículos, mas serve como heurística suficiente para balanceamento de carga.
* <p>This is a practical solution that enables dynamic routing without requiring
* bidirectional communication or complex state synchronization.</p>
*/ */
private final Map<String, Integer> intersectionQueueSizes; private final Map<String, Integer> intersectionQueueSizes;
/**
* Ponto de entrada do processo Coordenador.
* Carrega configurações, estabelece conexões TCP e inicia o loop de eventos.
*/
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION"); System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
@@ -95,6 +101,12 @@ public class CoordinatorProcess {
} }
} }
/**
* Inicializa o coordenador com a configuração fornecida.
* Configura o motor DES, logging e o seletor de rotas inicial.
*
* @param config Objeto de configuração carregado.
*/
public CoordinatorProcess(SimulationConfig config) { public CoordinatorProcess(SimulationConfig config) {
this.config = config; this.config = config;
@@ -124,10 +136,9 @@ public class CoordinatorProcess {
} }
/** /**
* Cria o RouteSelector apropriado baseado na política configurada. * Fábrica de {@link RouteSelector} baseada no nome da política.
* * * @param policyName Nome da política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED).
* @param policyName nome da política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED) * @return Uma instância da estratégia de roteamento.
* @return instância do RouteSelector correspondente
*/ */
private RouteSelector createRouteSelector(String policyName) { private RouteSelector createRouteSelector(String policyName) {
try { try {
@@ -156,6 +167,10 @@ public class CoordinatorProcess {
} }
} }
/**
* Estabelece conexões TCP com o Dashboard e todas as Interseções (Worker Nodes).
* Essencial para o envio de comandos de controle e injeção de veículos.
*/
public void initialize() { public void initialize() {
// Connect to dashboard first // Connect to dashboard first
connectToDashboard(); connectToDashboard();
@@ -185,6 +200,15 @@ public class CoordinatorProcess {
} }
} }
/**
* Loop principal da simulação (DES Engine).
* <p>
* Executa a sequência:
* 1. Retira o próximo evento da fila prioritária.
* 2. Avança o relógio virtual para o timestamp do evento.
* 3. Aplica escala temporal (Time Scale) para visualização, se necessário.
* 4. Processa o evento.
*/
public void run() { public void run() {
double duration = config.getSimulationDuration(); double duration = config.getSimulationDuration();
double drainTime = config.getDrainTime(); double drainTime = config.getDrainTime();
@@ -264,14 +288,9 @@ public class CoordinatorProcess {
} }
/** /**
* Trata um único evento de simulação. * Trata o processamento de um evento DES retirado da fila.
* * * @param event O evento a ser processado.
* É aqui que a magia acontece. Dependendo do tipo de evento (como * @param generationDuration Duração da fase de geração ativa (antes do 'drain time').
* VEHICLE_GENERATION),
* atualizamos o estado do mundo. Para a geração de veículos, criamos um novo
* veículo,
* enviamo-lo para uma interseção e depois agendamos o *próximo* evento de
* geração.
*/ */
private void processEvent(SimulationEvent event, double generationDuration) { private void processEvent(SimulationEvent event, double generationDuration) {
double currentTime = clock.getCurrentTime(); double currentTime = clock.getCurrentTime();
@@ -285,7 +304,7 @@ public class CoordinatorProcess {
generateAndSendVehicle(); generateAndSendVehicle();
// Schedule next vehicle generation // Schedule next vehicle generation (Recursive scheduling)
double nextArrivalTime = vehicleGenerator.getNextArrivalTime(currentTime); double nextArrivalTime = vehicleGenerator.getNextArrivalTime(currentTime);
eventQueue.schedule(new SimulationEvent( eventQueue.schedule(new SimulationEvent(
nextArrivalTime, nextArrivalTime,
@@ -309,9 +328,8 @@ public class CoordinatorProcess {
} }
/** /**
* Guarda o histórico completo de eventos de simulação num ficheiro de texto. * Exporta o log completo de eventos DES para auditoria e debug.
* Isto permite-nos auditar exatamente o que aconteceu e quando, o que é crucial * Caminho: {@code logs/coordinator-event-history.txt}.
* para depuração e verificação.
*/ */
private void exportEventHistory() { private void exportEventHistory() {
try (java.io.PrintWriter writer = new java.io.PrintWriter( try (java.io.PrintWriter writer = new java.io.PrintWriter(
@@ -324,6 +342,10 @@ public class CoordinatorProcess {
} }
} }
/**
* Gera um novo veículo e envia-o via TCP para a interseção de entrada apropriada.
* Também atualiza o rastreio local de filas para balanceamento de carga.
*/
private void generateAndSendVehicle() { private void generateAndSendVehicle() {
double currentTime = clock.getCurrentTime(); double currentTime = clock.getCurrentTime();
@@ -355,6 +377,9 @@ public class CoordinatorProcess {
sendVehicleToIntersection(vehicle, entryIntersection); sendVehicleToIntersection(vehicle, entryIntersection);
} }
/**
* Serializa e transmite o objeto Veículo para o nó (interseção) de destino.
*/
private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) { private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
SocketClient client = intersectionClients.get(intersectionId); SocketClient client = intersectionClients.get(intersectionId);
@@ -379,6 +404,9 @@ public class CoordinatorProcess {
} }
} }
/**
* Encerra graciosamente a simulação, enviando sinais de SHUTDOWN para todos os nós.
*/
public void shutdown() { public void shutdown() {
System.out.println(); System.out.println();
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
@@ -415,10 +443,9 @@ public class CoordinatorProcess {
} }
/** /**
* Altera dinamicamente a política de roteamento durante a simulação. * Altera dinamicamente a política de roteamento durante a simulação (Hot-swap).
* Novos veículos gerados usarão a nova política. * Thread-safe.
* * * @param policyName nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
* @param policyName nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
*/ */
public synchronized void changeRoutingPolicy(String policyName) { public synchronized void changeRoutingPolicy(String policyName) {
System.out.println("\n" + "=".repeat(60)); System.out.println("\n" + "=".repeat(60));
@@ -454,8 +481,8 @@ public class CoordinatorProcess {
} }
/** /**
* Verifica se há solicitação de mudança de política do dashboard * Verifica se há solicitação de mudança de política proveniente do dashboard
* e aplica se houver. * e aplica a alteração se houver.
*/ */
private void checkForPolicyChanges() { private void checkForPolicyChanges() {
if (dashboardStatistics != null) { if (dashboardStatistics != null) {
@@ -467,8 +494,8 @@ public class CoordinatorProcess {
} }
/** /**
* Define a referência para as estatísticas do dashboard. * Injeta a referência para as estatísticas do dashboard.
* Permite que o coordenador verifique mudanças de política solicitadas. * Permite que o coordenador consuma intenções de mudança de política do utilizador.
*/ */
public void setDashboardStatistics(DashboardStatistics stats) { public void setDashboardStatistics(DashboardStatistics stats) {
this.dashboardStatistics = stats; this.dashboardStatistics = stats;
@@ -512,6 +539,11 @@ public class CoordinatorProcess {
} }
} }
/**
* Sincronização Global: Envia o timestamp de início (System.currentTimeMillis)
* para todos os componentes distribuídos, garantindo uma base de tempo comum
* para métricas de latência.
*/
private void sendSimulationStartTime() { private void sendSimulationStartTime() {
long startTimeMillis = System.currentTimeMillis(); long startTimeMillis = System.currentTimeMillis();

View File

@@ -10,10 +10,14 @@ import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory; import sd.serialization.SerializerFactory;
/** /**
* Cliente socket para comunicação com um processo de interseção. * Abstração de cliente TCP para comunicação outbound (de saída) com nós da rede.
* * <p>
* <p>Gere uma ligação TCP persistente para uma interseção, * Esta classe encapsula a gestão do socket raw, oferecendo uma interface de alto nível
* fornecendo uma forma simples de enviar mensagens serializadas.</p> * para envio de objetos {@link Message}. Implementa o protocolo de camada de aplicação
* proprietário, garantindo a serialização correta e o enquadramento (framing) dos dados
* na stream TCP.
* <p>
* É utilizada pelo Coordenador para controlar Interseções e enviar telemetria para o Dashboard.
*/ */
public class SocketClient { public class SocketClient {
@@ -25,11 +29,11 @@ public class SocketClient {
private MessageSerializer serializer; private MessageSerializer serializer;
/** /**
* Cria um novo cliente socket para uma interseção. * Instancia um novo cliente socket configurado para um destino específico.
* *
* @param intersectionId ID da interseção (ex: "Cr1") * @param intersectionId Identificador lógico do nó de destino (ex: "Cr1", "Dashboard").
* @param host endereço do host (ex: "localhost") * @param host Endereço IP ou hostname do destino.
* @param port número da porta * @param port Porta TCP de escuta do destino.
*/ */
public SocketClient(String intersectionId, String host, int port) { public SocketClient(String intersectionId, String host, int port) {
this.intersectionId = intersectionId; this.intersectionId = intersectionId;
@@ -39,9 +43,8 @@ public class SocketClient {
} }
/** /**
* Liga-se ao processo da interseção via TCP. * Estabelece a conexão TCP (Handshake SYN/ACK) com o host remoto.
* * * @throws IOException Se o host for inalcançável ou a conexão for recusada.
* @throws IOException se a ligação não puder ser estabelecida
*/ */
public void connect() throws IOException { public void connect() throws IOException {
try { try {
@@ -55,12 +58,22 @@ public class SocketClient {
} }
/** /**
* Envia uma mensagem para a interseção ligada. * Serializa e transmite uma mensagem através do socket conectado.
* A mensagem é serializada e enviada pelo socket. * <p>
* <b>Protocolo de Envio (Length-Prefix Framing):</b>
* <ol>
* <li>Serializa o objeto {@link Message} para um array de bytes.</li>
* <li>Calcula o tamanho (N) do array.</li>
* <li>Escreve um cabeçalho de 4 bytes contendo N (Big-Endian).</li>
* <li>Escreve os N bytes do payload (corpo da mensagem).</li>
* <li>Realiza flush no stream para forçar o envio imediato do pacote TCP.</li>
* </ol>
* Este mecanismo garante que o recetor saiba exatamente quantos bytes ler,
* prevenindo problemas de fragmentação ou aglutinação de pacotes TCP.
* *
* @param message mensagem a enviar * @param message O objeto de domínio a ser enviado.
* @throws SerializationException se a serialização falhar * @throws SerializationException Se o objeto não puder ser convertido para bytes.
* @throws IOException se a escrita no socket falhar * @throws IOException Se houver falha na escrita do socket (ex: conexão resetada).
*/ */
public void send(Message message) throws SerializationException, IOException { public void send(Message message) throws SerializationException, IOException {
if (socket == null || socket.isClosed()) { if (socket == null || socket.isClosed()) {
@@ -71,11 +84,13 @@ public class SocketClient {
byte[] data = serializer.serialize(message); byte[] data = serializer.serialize(message);
int length = data.length; int length = data.length;
// Write 4-byte length header (Big Endian)
outputStream.write((length >> 24) & 0xFF); outputStream.write((length >> 24) & 0xFF);
outputStream.write((length >> 16) & 0xFF); outputStream.write((length >> 16) & 0xFF);
outputStream.write((length >> 8) & 0xFF); outputStream.write((length >> 8) & 0xFF);
outputStream.write(length & 0xFF); outputStream.write(length & 0xFF);
// Write payload
outputStream.write(data); outputStream.write(data);
outputStream.flush(); outputStream.flush();
@@ -86,8 +101,10 @@ public class SocketClient {
} }
/** /**
* Closes the socket connection safely. * Realiza o encerramento gracioso (graceful shutdown) da conexão.
* Calling it multiple times wont cause issues. * Liberta os recursos do sistema operativo (descritores de arquivo).
* <p>
* Operação idempotente: pode ser chamada múltiplas vezes sem erro.
*/ */
public void close() { public void close() {
try { try {
@@ -104,7 +121,8 @@ public class SocketClient {
} }
/** /**
* @return true if connected and socket is open, false otherwise * Verifica o estado atual da conexão.
* * @return true se o socket estiver instanciado, conectado e aberto; false caso contrário.
*/ */
public boolean isConnected() { public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed(); return socket != null && socket.isConnected() && !socket.isClosed();

View File

@@ -25,8 +25,17 @@ import sd.analysis.SimulationRunResult;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* Dialog for running batch performance analysis. * Diálogo para configuração e execução de análise de desempenho em lote (Batch Processing).
* Allows running multiple simulations automatically and generating statistical reports. * <p>
* Esta classe fornece uma interface gráfica para automatizar múltiplas execuções da simulação
* sob diferentes cenários de carga. É responsável por:
* <ol>
* <li>Orquestrar o ciclo de vida dos processos de simulação (start/stop/wait).</li>
* <li>Coletar métricas estatísticas de cada execução.</li>
* <li>Agregar resultados usando o {@link MultiRunAnalyzer}.</li>
* <li>Gerar relatórios consolidados para análise de variância e intervalos de confiança.</li>
* </ol>
* A execução ocorre numa thread separada (background) para manter a responsividade da UI.
*/ */
public class BatchAnalysisDialog { public class BatchAnalysisDialog {
@@ -38,16 +47,17 @@ public class BatchAnalysisDialog {
private Button startButton; private Button startButton;
private Button closeButton; private Button closeButton;
// Flags de controlo de concorrência
private volatile boolean isRunning = false; private volatile boolean isRunning = false;
private volatile boolean shouldStop = false; private volatile boolean shouldStop = false;
/** Referência partilhada para capturar estatísticas em tempo real do Dashboard. */
private DashboardStatistics sharedStatistics; private DashboardStatistics sharedStatistics;
/** /**
* Shows the batch analysis dialog. * Exibe o diálogo de análise em lote.
* * * @param owner A janela pai (Stage) para modalidade.
* @param owner parent window * @param statistics Objeto partilhado de estatísticas para coleta de dados.
* @param statistics shared statistics object (optional, can be null)
*/ */
public static void show(Stage owner, DashboardStatistics statistics) { public static void show(Stage owner, DashboardStatistics statistics) {
BatchAnalysisDialog dialog = new BatchAnalysisDialog(); BatchAnalysisDialog dialog = new BatchAnalysisDialog();
@@ -55,6 +65,9 @@ public class BatchAnalysisDialog {
dialog.createAndShow(owner); dialog.createAndShow(owner);
} }
/**
* Constrói e inicializa a interface gráfica do diálogo.
*/
private void createAndShow(Stage owner) { private void createAndShow(Stage owner) {
dialog = new Stage(); dialog = new Stage();
dialog.initOwner(owner); dialog.initOwner(owner);
@@ -64,37 +77,34 @@ public class BatchAnalysisDialog {
VBox root = new VBox(20); VBox root = new VBox(20);
root.setPadding(new Insets(20)); root.setPadding(new Insets(20));
root.setAlignment(Pos.TOP_CENTER); root.setAlignment(Pos.TOP_CENTER);
// Estilo Dark Mode conforme guidelines visuais
root.setStyle("-fx-background-color: #2b2b2b;"); root.setStyle("-fx-background-color: #2b2b2b;");
// Header // Header
Label title = new Label("Batch Performance Evaluation"); Label title = new Label("Batch Performance Evaluation");
title.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: white;"); title.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: white;");
Label subtitle = new Label("Run multiple simulations automatically to generate statistical analysis"); Label subtitle = new Label("Executar múltiplas simulações para gerar análise estatística consolidada");
subtitle.setStyle("-fx-font-size: 12px; -fx-text-fill: #cccccc;"); subtitle.setStyle("-fx-font-size: 12px; -fx-text-fill: #cccccc;");
subtitle.setWrapText(true); subtitle.setWrapText(true);
// Configuration panel // Painéis de Componentes
VBox configPanel = createConfigPanel(); VBox configPanel = createConfigPanel();
// Progress panel
VBox progressPanel = createProgressPanel(); VBox progressPanel = createProgressPanel();
// Log area
VBox logPanel = createLogPanel(); VBox logPanel = createLogPanel();
// Control buttons
HBox buttonBox = createButtonBox(); HBox buttonBox = createButtonBox();
root.getChildren().addAll(title, subtitle, configPanel, progressPanel, logPanel, buttonBox); root.getChildren().addAll(title, subtitle, configPanel, progressPanel, logPanel, buttonBox);
Scene scene = new Scene(root, 700, 600); Scene scene = new Scene(root, 700, 600);
dialog.setScene(scene); dialog.setScene(scene);
// Tratamento de fecho da janela: interromper thread de worker se ativa
dialog.setOnCloseRequest(e -> { dialog.setOnCloseRequest(e -> {
if (isRunning) { if (isRunning) {
e.consume(); e.consume(); // Previne fecho imediato
shouldStop = true; shouldStop = true;
log("Stopping after current run completes..."); log("A parar após conclusão da execução atual...");
} }
}); });
@@ -106,13 +116,13 @@ public class BatchAnalysisDialog {
panel.setPadding(new Insets(15)); panel.setPadding(new Insets(15));
panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;"); panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;");
Label header = new Label("Configuration"); Label header = new Label("Configuração");
header.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: white;"); header.setStyle("-fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: white;");
// Runs per scenario // Runs per scenario
HBox runsBox = new HBox(10); HBox runsBox = new HBox(10);
runsBox.setAlignment(Pos.CENTER_LEFT); runsBox.setAlignment(Pos.CENTER_LEFT);
Label runsLabel = new Label("Runs per scenario:"); Label runsLabel = new Label("Execuções por cenário:");
runsLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;"); runsLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;");
Spinner<Integer> runsSpinner = new Spinner<>(1, 20, 5, 1); Spinner<Integer> runsSpinner = new Spinner<>(1, 20, 5, 1);
runsSpinner.setEditable(true); runsSpinner.setEditable(true);
@@ -121,20 +131,20 @@ public class BatchAnalysisDialog {
runsBox.getChildren().addAll(runsLabel, runsSpinner); runsBox.getChildren().addAll(runsLabel, runsSpinner);
// Scenario selection // Scenario selection
Label scenarioHeader = new Label("Select scenarios to test:"); Label scenarioHeader = new Label("Selecionar Cenários:");
scenarioHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;"); scenarioHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;");
CheckBox lowCheck = new CheckBox("Low Load (λ=0.2 v/s)"); CheckBox lowCheck = new CheckBox("Carga Baixa (λ=0.2 v/s)");
lowCheck.setSelected(true); lowCheck.setSelected(true);
lowCheck.setId("lowCheck"); lowCheck.setId("lowCheck");
lowCheck.setStyle("-fx-text-fill: white;"); lowCheck.setStyle("-fx-text-fill: white;");
CheckBox mediumCheck = new CheckBox("Medium Load (λ=0.5 v/s)"); CheckBox mediumCheck = new CheckBox("Carga Média (λ=0.5 v/s)");
mediumCheck.setSelected(true); mediumCheck.setSelected(true);
mediumCheck.setId("mediumCheck"); mediumCheck.setId("mediumCheck");
mediumCheck.setStyle("-fx-text-fill: white;"); mediumCheck.setStyle("-fx-text-fill: white;");
CheckBox highCheck = new CheckBox("High Load (λ=1.0 v/s)"); CheckBox highCheck = new CheckBox("Carga Alta (λ=1.0 v/s)");
highCheck.setSelected(true); highCheck.setSelected(true);
highCheck.setId("highCheck"); highCheck.setId("highCheck");
highCheck.setStyle("-fx-text-fill: white;"); highCheck.setStyle("-fx-text-fill: white;");
@@ -142,13 +152,13 @@ public class BatchAnalysisDialog {
// Run duration // Run duration
HBox durationBox = new HBox(10); HBox durationBox = new HBox(10);
durationBox.setAlignment(Pos.CENTER_LEFT); durationBox.setAlignment(Pos.CENTER_LEFT);
Label durationLabel = new Label("Run duration (seconds):"); Label durationLabel = new Label("Duração (segundos):");
durationLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;"); durationLabel.setStyle("-fx-text-fill: white; -fx-min-width: 150px;");
Spinner<Integer> durationSpinner = new Spinner<>(30, 3600, 120, 30); Spinner<Integer> durationSpinner = new Spinner<>(30, 3600, 120, 30);
durationSpinner.setEditable(true); durationSpinner.setEditable(true);
durationSpinner.setPrefWidth(80); durationSpinner.setPrefWidth(80);
durationSpinner.setId("durationSpinner"); durationSpinner.setId("durationSpinner");
Label durationInfo = new Label("(simulated time - actual duration depends on time.scale)"); Label durationInfo = new Label("(tempo simulado - duração real depende do time.scale)");
durationInfo.setStyle("-fx-text-fill: #999999; -fx-font-size: 10px;"); durationInfo.setStyle("-fx-text-fill: #999999; -fx-font-size: 10px;");
durationBox.getChildren().addAll(durationLabel, durationSpinner, durationInfo); durationBox.getChildren().addAll(durationLabel, durationSpinner, durationInfo);
@@ -161,14 +171,14 @@ public class BatchAnalysisDialog {
panel.setPadding(new Insets(15)); panel.setPadding(new Insets(15));
panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;"); panel.setStyle("-fx-background-color: rgba(255, 255, 255, 0.05); -fx-background-radius: 5;");
statusLabel = new Label("Ready to start"); statusLabel = new Label("Pronto para iniciar");
statusLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold;"); statusLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold;");
progressBar = new ProgressBar(0); progressBar = new ProgressBar(0);
progressBar.setPrefWidth(Double.MAX_VALUE); progressBar.setPrefWidth(Double.MAX_VALUE);
progressBar.setPrefHeight(25); progressBar.setPrefHeight(25);
progressLabel = new Label("0 / 0 runs completed"); progressLabel = new Label("0 / 0 execuções concluídas");
progressLabel.setStyle("-fx-text-fill: #cccccc; -fx-font-size: 11px;"); progressLabel.setStyle("-fx-text-fill: #cccccc; -fx-font-size: 11px;");
panel.getChildren().addAll(statusLabel, progressBar, progressLabel); panel.getChildren().addAll(statusLabel, progressBar, progressLabel);
@@ -178,13 +188,14 @@ public class BatchAnalysisDialog {
private VBox createLogPanel() { private VBox createLogPanel() {
VBox panel = new VBox(5); VBox panel = new VBox(5);
Label logHeader = new Label("Activity Log:"); Label logHeader = new Label("Log de Atividade:");
logHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;"); logHeader.setStyle("-fx-text-fill: white; -fx-font-size: 12px; -fx-font-weight: bold;");
logArea = new TextArea(); logArea = new TextArea();
logArea.setEditable(false); logArea.setEditable(false);
logArea.setPrefRowCount(10); logArea.setPrefRowCount(10);
logArea.setWrapText(true); logArea.setWrapText(true);
// Estilo de terminal para o log
logArea.setStyle("-fx-control-inner-background: #1e1e1e; -fx-text-fill: #00ff00; -fx-font-family: 'Courier New';"); logArea.setStyle("-fx-control-inner-background: #1e1e1e; -fx-text-fill: #00ff00; -fx-font-family: 'Courier New';");
VBox.setVgrow(logArea, Priority.ALWAYS); VBox.setVgrow(logArea, Priority.ALWAYS);
@@ -197,18 +208,18 @@ public class BatchAnalysisDialog {
box.setAlignment(Pos.CENTER); box.setAlignment(Pos.CENTER);
box.setPadding(new Insets(10, 0, 0, 0)); box.setPadding(new Insets(10, 0, 0, 0));
startButton = new Button("START BATCH ANALYSIS"); startButton = new Button("INICIAR BATCH");
startButton.setStyle("-fx-background-color: #28a745; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;"); startButton.setStyle("-fx-background-color: #28a745; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
startButton.setOnAction(e -> startBatchAnalysis()); startButton.setOnAction(e -> startBatchAnalysis());
Button stopButton = new Button("STOP"); Button stopButton = new Button("PARAR");
stopButton.setStyle("-fx-background-color: #dc3545; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;"); stopButton.setStyle("-fx-background-color: #dc3545; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
stopButton.setOnAction(e -> { stopButton.setOnAction(e -> {
shouldStop = true; shouldStop = true;
log("Stop requested..."); log("Paragem solicitada...");
}); });
closeButton = new Button("CLOSE"); closeButton = new Button("FECHAR");
closeButton.setStyle("-fx-background-color: #6c757d; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;"); closeButton.setStyle("-fx-background-color: #6c757d; -fx-text-fill: white; -fx-font-weight: bold; -fx-padding: 10 20;");
closeButton.setOnAction(e -> dialog.close()); closeButton.setOnAction(e -> dialog.close());
@@ -216,6 +227,9 @@ public class BatchAnalysisDialog {
return box; return box;
} }
/**
* Valida configurações e inicia a thread de execução em batch.
*/
private void startBatchAnalysis() { private void startBatchAnalysis() {
if (isRunning) return; if (isRunning) return;
@@ -231,11 +245,11 @@ public class BatchAnalysisDialog {
// Validate selection // Validate selection
if (!lowCheck.isSelected() && !mediumCheck.isSelected() && !highCheck.isSelected()) { if (!lowCheck.isSelected() && !mediumCheck.isSelected() && !highCheck.isSelected()) {
log("ERROR: Please select at least one scenario!"); log("ERRO: Selecione pelo menos um cenário!");
return; return;
} }
// Disable controls // Disable controls para evitar alterações durante execução
startButton.setDisable(true); startButton.setDisable(true);
runsSpinner.setDisable(true); runsSpinner.setDisable(true);
durationSpinner.setDisable(true); durationSpinner.setDisable(true);
@@ -246,12 +260,13 @@ public class BatchAnalysisDialog {
isRunning = true; isRunning = true;
shouldStop = false; shouldStop = false;
// Run in background thread // Executar em thread daemon para não bloquear a UI JavaFX
Thread analysisThread = new Thread(() -> { Thread analysisThread = new Thread(() -> {
try { try {
runBatchAnalysis(lowCheck.isSelected(), mediumCheck.isSelected(), runBatchAnalysis(lowCheck.isSelected(), mediumCheck.isSelected(),
highCheck.isSelected(), runsPerScenario, duration); highCheck.isSelected(), runsPerScenario, duration);
} finally { } finally {
// Restaurar estado da UI no final
Platform.runLater(() -> { Platform.runLater(() -> {
startButton.setDisable(false); startButton.setDisable(false);
runsSpinner.setDisable(false); runsSpinner.setDisable(false);
@@ -267,14 +282,18 @@ public class BatchAnalysisDialog {
analysisThread.start(); analysisThread.start();
} }
/**
* Lógica principal de orquestração do batch.
* Itera sobre cenários e execuções, chamando a simulação e o analisador.
*/
private void runBatchAnalysis(boolean low, boolean medium, boolean high, int runsPerScenario, int durationSeconds) { private void runBatchAnalysis(boolean low, boolean medium, boolean high, int runsPerScenario, int durationSeconds) {
log("==========================================================="); log("===========================================================");
log("STARTING BATCH PERFORMANCE ANALYSIS"); log("INICIANDO ANÁLISE DE DESEMPENHO EM LOTE");
log("==========================================================="); log("===========================================================");
log("Configuration:"); log("Configuração:");
log("Runs per scenario: " + runsPerScenario); log("Execuções por cenário: " + runsPerScenario);
log(" • Duration per run: " + durationSeconds + " seconds"); log(" • Duração por execução: " + durationSeconds + " segundos");
log("Scenarios: " + (low ? "LOW " : "") + (medium ? "MEDIUM " : "") + (high ? "HIGH" : "")); log("Cenários: " + (low ? "LOW " : "") + (medium ? "MEDIUM " : "") + (high ? "HIGH" : ""));
log(""); log("");
String[] scenarios = new String[]{ String[] scenarios = new String[]{
@@ -295,8 +314,8 @@ public class BatchAnalysisDialog {
for (int i = 0; i < scenarios.length; i++) { for (int i = 0; i < scenarios.length; i++) {
if (scenarios[i] == null) continue; if (scenarios[i] == null) continue;
if (shouldStop) { if (shouldStop) {
log("Batch analysis stopped by user"); log("Batch analysis interrompida pelo utilizador");
updateStatus("Stopped", currentRun, totalRuns); updateStatus("Parado", currentRun, totalRuns);
return; return;
} }
@@ -305,53 +324,57 @@ public class BatchAnalysisDialog {
log(""); log("");
log("---------------------------------------------------------"); log("---------------------------------------------------------");
log("SCENARIO: " + scenarioName + " (" + configFile + ")"); log("CENÁRIO: " + scenarioName + " (" + configFile + ")");
log("---------------------------------------------------------"); log("---------------------------------------------------------");
MultiRunAnalyzer analyzer = new MultiRunAnalyzer(configFile); MultiRunAnalyzer analyzer = new MultiRunAnalyzer(configFile);
for (int run = 1; run <= runsPerScenario; run++) { for (int run = 1; run <= runsPerScenario; run++) {
if (shouldStop) { if (shouldStop) {
log("Batch analysis stopped by user"); log("Batch analysis interrompida pelo utilizador");
updateStatus("Stopped", currentRun, totalRuns); updateStatus("Parado", currentRun, totalRuns);
savePartialReport(analyzer, scenarioName); savePartialReport(analyzer, scenarioName);
return; return;
} }
currentRun++; currentRun++;
log(""); log("");
log("Run " + run + "/" + runsPerScenario + " starting..."); log("Execução " + run + "/" + runsPerScenario + " a iniciar...");
updateStatus("Running " + scenarioName + " - Run " + run + "/" + runsPerScenario, updateStatus("A correr " + scenarioName + " - Execução " + run + "/" + runsPerScenario,
currentRun - 1, totalRuns); currentRun - 1, totalRuns);
// Executa uma simulação completa e bloqueia até terminar
SimulationRunResult result = runSingleSimulation(configFile, run, durationSeconds); SimulationRunResult result = runSingleSimulation(configFile, run, durationSeconds);
if (result != null) { if (result != null) {
analyzer.addResult(result); analyzer.addResult(result);
log("Run " + run + " completed - Generated: " + result.getTotalVehiclesGenerated() + log("Execução " + run + " completa - Gerados: " + result.getTotalVehiclesGenerated() +
" | Completed: " + result.getTotalVehiclesCompleted() + " | Completados: " + result.getTotalVehiclesCompleted() +
" | Avg Time: " + String.format("%.2f", result.getAverageSystemTime()) + "s"); " | Tempo Médio: " + String.format("%.2f", result.getAverageSystemTime()) + "s");
} else { } else {
log("Run " + run + " failed!"); log("Execução " + run + " falhou!");
} }
updateProgress(currentRun, totalRuns); updateProgress(currentRun, totalRuns);
} }
// Generate report for this scenario // Gera e guarda o relatório final deste cenário
saveScenarioReport(analyzer, scenarioName); saveScenarioReport(analyzer, scenarioName);
} }
log(""); log("");
log("============================================================"); log("============================================================");
log("BATCH ANALYSIS COMPLETE!"); log("BATCH ANALYSIS COMPLETA!");
log("==========================================================="); log("===========================================================");
log("Reports saved to: analysis/"); log("Relatórios guardados em: analysis/");
log(""); log("");
updateStatus("Complete!", totalRuns, totalRuns); updateStatus("Concluído!", totalRuns, totalRuns);
updateProgress(1.0); updateProgress(1.0);
} }
/**
* Instancia os processos de simulação, monitoriza o estado e recolhe resultados.
*/
private SimulationRunResult runSingleSimulation(String configFile, int runNumber, int durationSeconds) { private SimulationRunResult runSingleSimulation(String configFile, int runNumber, int durationSeconds) {
SimulationProcessManager processManager = new SimulationProcessManager(); SimulationProcessManager processManager = new SimulationProcessManager();
SimulationRunResult result = new SimulationRunResult(runNumber, configFile); SimulationRunResult result = new SimulationRunResult(runNumber, configFile);
@@ -361,16 +384,16 @@ public class BatchAnalysisDialog {
processManager.setConfigFile(configFile); processManager.setConfigFile(configFile);
processManager.startSimulation(); processManager.startSimulation();
// Give time for processes to start and connect // Tempo para processos arrancarem e estabelecerem conexões TCP
Thread.sleep(3000); Thread.sleep(3000);
log(" Simulation running (configured duration: " + durationSeconds + "s simulated time)..."); log(" Simulação em curso (duração config: " + durationSeconds + "s tempo simulado)...");
log(" Waiting for coordinator process to complete..."); log(" A aguardar processo Coordenador completar...");
// Wait for the coordinator process to finish naturally // Loop de polling para verificar se o Coordenador terminou
// This automatically handles different time scales // Isso lida automaticamente com diferentes time scales (DES)
int checkInterval = 2; // Check every 2 seconds int checkInterval = 2; // Check every 2 seconds
int elapsed = 0; int elapsed = 0;
int maxWaitSeconds = durationSeconds + 120; // Safety timeout int maxWaitSeconds = durationSeconds + 120; // Timeout de segurança
while (elapsed < maxWaitSeconds) { while (elapsed < maxWaitSeconds) {
if (shouldStop) { if (shouldStop) {
@@ -380,29 +403,29 @@ public class BatchAnalysisDialog {
// Check if simulation completed // Check if simulation completed
if (!processManager.isSimulationRunning()) { if (!processManager.isSimulationRunning()) {
log(" Simulation completed after " + elapsed + "s"); log(" Simulação terminou após " + elapsed + "s");
break; break;
} }
Thread.sleep(checkInterval * 1000L); Thread.sleep(checkInterval * 1000L);
elapsed += checkInterval; elapsed += checkInterval;
// Progress update every 10 seconds // Atualização periódica do log
if (elapsed % 10 == 0 && elapsed < 60) { if (elapsed % 10 == 0 && elapsed < 60) {
log(" " + elapsed + "s elapsed..."); log(" " + elapsed + "s decorridos...");
} }
} }
if (elapsed >= maxWaitSeconds) { if (elapsed >= maxWaitSeconds) {
log(" Timeout reached, forcing stop..."); log(" Timeout atingido, forçando paragem...");
} }
// Stop and collect results // Stop and collect results
log(" Stopping processes..."); log(" A terminar processos...");
processManager.stopSimulation(); processManager.stopSimulation();
Thread.sleep(2000); // Give time for final statistics Thread.sleep(2000); // Tempo para flushing de buffers
// Collect statistics if available // Recolha de estatísticas (Prioridade: Dados reais do socket)
if (sharedStatistics != null) { if (sharedStatistics != null) {
collectRealStatistics(result, sharedStatistics); collectRealStatistics(result, sharedStatistics);
} else { } else {
@@ -412,16 +435,16 @@ public class BatchAnalysisDialog {
return result; return result;
} catch (InterruptedException e) { } catch (InterruptedException e) {
log("Interrupted: " + e.getMessage()); log("Interrompido: " + e.getMessage());
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
stopSimulation(processManager); stopSimulation(processManager);
return null; return null;
} catch (IOException e) { } catch (IOException e) {
log("IO Error: " + e.getMessage()); log("Erro IO: " + e.getMessage());
stopSimulation(processManager); stopSimulation(processManager);
return null; return null;
} catch (RuntimeException e) { } catch (RuntimeException e) {
log("Runtime Error: " + e.getMessage()); log("Erro Runtime: " + e.getMessage());
stopSimulation(processManager); stopSimulation(processManager);
return null; return null;
} }
@@ -431,21 +454,24 @@ public class BatchAnalysisDialog {
try { try {
processManager.stopSimulation(); processManager.stopSimulation();
} catch (Exception ex) { } catch (Exception ex) {
// Ignore cleanup errors // Ignora erros de cleanup
} }
} }
/**
* Popula o objeto de resultado com dados reais capturados pelo Dashboard.
*/
private void collectRealStatistics(SimulationRunResult result, DashboardStatistics stats) { private void collectRealStatistics(SimulationRunResult result, DashboardStatistics stats) {
result.setTotalVehiclesGenerated(stats.getTotalVehiclesGenerated()); result.setTotalVehiclesGenerated(stats.getTotalVehiclesGenerated());
result.setTotalVehiclesCompleted(stats.getTotalVehiclesCompleted()); result.setTotalVehiclesCompleted(stats.getTotalVehiclesCompleted());
result.setAverageSystemTime(stats.getAverageSystemTime() / 1000.0); // Convert ms to seconds result.setAverageSystemTime(stats.getAverageSystemTime() / 1000.0); // Converte ms para segundos
result.setAverageWaitingTime(stats.getAverageWaitingTime() / 1000.0); result.setAverageWaitingTime(stats.getAverageWaitingTime() / 1000.0);
// Set min/max as approximations (would need to be tracked in DashboardStatistics) // Estimação de extremos (o DashboardStatistics deve ser expandido para guardar exatos se necessário)
result.setMinSystemTime(stats.getAverageSystemTime() / 1000.0 * 0.5); result.setMinSystemTime(stats.getAverageSystemTime() / 1000.0 * 0.5);
result.setMaxSystemTime(stats.getAverageSystemTime() / 1000.0 * 2.0); result.setMaxSystemTime(stats.getAverageSystemTime() / 1000.0 * 2.0);
// Collect per-type statistics // Estatísticas por tipo
for (VehicleType type : VehicleType.values()) { for (VehicleType type : VehicleType.values()) {
int count = stats.getVehicleTypeCount(type); int count = stats.getVehicleTypeCount(type);
double waitTime = stats.getAverageWaitingTimeByType(type) / 1000.0; double waitTime = stats.getAverageWaitingTimeByType(type) / 1000.0;
@@ -453,26 +479,29 @@ public class BatchAnalysisDialog {
result.setAvgWaitTimeByType(type, waitTime); result.setAvgWaitTimeByType(type, waitTime);
} }
// Collect per-intersection statistics // Estatísticas por interseção
for (var entry : stats.getAllIntersectionStats().entrySet()) { for (var entry : stats.getAllIntersectionStats().entrySet()) {
String intersectionId = entry.getKey(); String intersectionId = entry.getKey();
DashboardStatistics.IntersectionStats iStats = entry.getValue(); DashboardStatistics.IntersectionStats iStats = entry.getValue();
result.setVehiclesProcessed(intersectionId, iStats.getTotalDepartures()); result.setVehiclesProcessed(intersectionId, iStats.getTotalDepartures());
result.setMaxQueueSize(intersectionId, iStats.getCurrentQueueSize()); result.setMaxQueueSize(intersectionId, iStats.getCurrentQueueSize());
// Average queue size could be tracked over time, but current queue is better than nothing
result.setAvgQueueSize(intersectionId, (double) iStats.getCurrentQueueSize()); result.setAvgQueueSize(intersectionId, (double) iStats.getCurrentQueueSize());
} }
} }
/**
* Gera dados simulados (mock) caso o dashboard não esteja conectado.
* Útil para testes de interface.
*/
private void collectSimulatedStatistics(SimulationRunResult result, String configFile, int durationSeconds) { private void collectSimulatedStatistics(SimulationRunResult result, String configFile, int durationSeconds) {
// Simulated results based on load profile for demonstration // Mock data based on load profile
int baseGenerated = durationSeconds / 3; int baseGenerated = durationSeconds / 3;
double loadFactor = configFile.contains("low") ? 0.2 : double loadFactor = configFile.contains("low") ? 0.2 :
configFile.contains("medium") ? 0.5 : 1.0; configFile.contains("medium") ? 0.5 : 1.0;
int generated = (int)(baseGenerated * loadFactor * 3); int generated = (int)(baseGenerated * loadFactor * 3);
int completed = (int)(generated * (0.85 + Math.random() * 0.1)); // 85-95% completion int completed = (int)(generated * (0.85 + Math.random() * 0.1)); // 85-95% completion rate
double baseSystemTime = 40.0; double baseSystemTime = 40.0;
double congestionFactor = configFile.contains("low") ? 1.0 : double congestionFactor = configFile.contains("low") ? 1.0 :
@@ -485,7 +514,7 @@ public class BatchAnalysisDialog {
result.setMaxSystemTime(baseSystemTime * congestionFactor * 2 + Math.random() * 20); result.setMaxSystemTime(baseSystemTime * congestionFactor * 2 + Math.random() * 20);
result.setAverageWaitingTime(10.0 * congestionFactor + Math.random() * 5); result.setAverageWaitingTime(10.0 * congestionFactor + Math.random() * 5);
log(" Note: Using simulated statistics (real collection requires dashboard integration)"); log(" Nota: A usar estatísticas simuladas (conexão real necessária)");
} }
private void saveScenarioReport(MultiRunAnalyzer analyzer, String scenarioName) { private void saveScenarioReport(MultiRunAnalyzer analyzer, String scenarioName) {
@@ -502,21 +531,23 @@ public class BatchAnalysisDialog {
analyzer.saveReport(reportFile); analyzer.saveReport(reportFile);
analyzer.saveCSV(csvFile); analyzer.saveCSV(csvFile);
log("Report saved: " + reportFile); log("Relatório guardado: " + reportFile);
log("CSV saved: " + csvFile); log("CSV guardado: " + csvFile);
} catch (IOException e) { } catch (IOException e) {
log("Failed to save report: " + e.getMessage()); log("Falha ao guardar relatório: " + e.getMessage());
} }
} }
private void savePartialReport(MultiRunAnalyzer analyzer, String scenarioName) { private void savePartialReport(MultiRunAnalyzer analyzer, String scenarioName) {
if (analyzer.getRunCount() > 0) { if (analyzer.getRunCount() > 0) {
log("Saving partial results..."); log("A guardar resultados parciais...");
saveScenarioReport(analyzer, scenarioName + "_PARTIAL"); saveScenarioReport(analyzer, scenarioName + "_PARTIAL");
} }
} }
// --- Helpers de UI Thread-Safe ---
private void log(String message) { private void log(String message) {
Platform.runLater(() -> { Platform.runLater(() -> {
logArea.appendText(message + "\n"); logArea.appendText(message + "\n");
@@ -527,7 +558,7 @@ public class BatchAnalysisDialog {
private void updateStatus(String status, int current, int total) { private void updateStatus(String status, int current, int total) {
Platform.runLater(() -> { Platform.runLater(() -> {
statusLabel.setText(status); statusLabel.setText(status);
progressLabel.setText(current + " / " + total + " runs completed"); progressLabel.setText(current + " / " + total + " execuções completas");
}); });
} }

View File

@@ -13,16 +13,32 @@ import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
/** /**
* Diálogo para configuração avançada de parâmetros da simulação. * Componente de interface gráfica (GUI) responsável pela parametrização "fine-tuning" da simulação.
* Permite ajustar parâmetros em runtime antes de iniciar a simulação. * <p>
* Esta classe apresenta um diálogo modal que permite ao operador sobrepor (override)
* as configurações estáticas carregadas do ficheiro {@code .properties} imediatamente
* antes da execução. Oferece controlo granular sobre:
* <ul>
* <li><b>Geração de Carga:</b> Alternância entre modelos estocásticos (Poisson) e determinísticos.</li>
* <li><b>Temporização:</b> Ajuste da escala de tempo (Time Scale) para visualização vs. performance pura.</li>
* <li><b>Mix de Veículos:</b> Definição das probabilidades de geração por tipo de agente.</li>
* </ul>
*/ */
public class ConfigurationDialog { public class ConfigurationDialog {
/** /**
* Mostra um diálogo com opções avançadas de configuração. * Exibe o diálogo de configuração avançada e captura as intenções do utilizador.
* <p>
* A interface é construída dinamicamente usando layouts JavaFX ({@link GridPane}, {@link VBox}).
* Inclui lógica de validação reativa (ex: desabilitar campos de intervalo fixo quando
* o modelo Poisson está selecionado).
* *
[Image of Poisson distribution graph]
* *
* @param owner janela pai * @param owner A janela pai (Stage) para bloquear a interação até o fecho do diálogo (Modalidade).
* @return true se o utilizador confirmar, false se cancelar * @return {@code true} se o utilizador confirmou as alterações (OK), {@code false} se cancelou.
*/ */
public static boolean showAdvancedConfig(Stage owner) { public static boolean showAdvancedConfig(Stage owner) {
Dialog<ButtonType> dialog = new Dialog<>(); Dialog<ButtonType> dialog = new Dialog<>();

View File

@@ -9,19 +9,42 @@ import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection; import sd.protocol.SocketConnection;
/** /**
* Processes statistics messages from a single client connection. * Worker responsável pelo processamento dedicado de uma conexão de cliente TCP no Dashboard.
* Runs in a separate thread per client. * <p>
* Esta classe implementa o padrão <i>Thread-per-Client</i>. Cada instância executa numa
* thread separada, garantindo que a latência de rede ou o processamento de mensagens
* de um nó (Interseção/Coordenador) não bloqueie a receção de telemetria dos outros.
* <p>
* As suas principais funções são:
* <ol>
* <li>Manter a conexão persistente com o nó remoto.</li>
* <li>Desserializar mensagens de protocolo recebidas.</li>
* <li>Normalizar payloads JSON (resolvendo ambiguidades de tipagem do Gson).</li>
* <li>Atualizar o objeto partilhado {@link DashboardStatistics} de forma thread-safe.</li>
* </ol>
*/ */
public class DashboardClientHandler implements Runnable { public class DashboardClientHandler implements Runnable {
private final Socket clientSocket; private final Socket clientSocket;
private final DashboardStatistics statistics; private final DashboardStatistics statistics;
/**
* Inicializa o handler com o socket ativo e a referência para o agregador de estatísticas.
*
* @param clientSocket O socket TCP conectado ao nó remoto.
* @param statistics O objeto singleton partilhado onde as métricas serão agregadas.
*/
public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) { public DashboardClientHandler(Socket clientSocket, DashboardStatistics statistics) {
this.clientSocket = clientSocket; this.clientSocket = clientSocket;
this.statistics = statistics; this.statistics = statistics;
} }
/**
* Ciclo de vida da conexão.
* <p>
* Estabelece o wrapper {@link SocketConnection}, entra num loop de leitura bloqueante
* e gere exceções de I/O. Garante o fecho limpo do socket em caso de desconexão ou erro.
*/
@Override @Override
public void run() { public void run() {
String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort(); String clientInfo = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
@@ -61,6 +84,16 @@ public class DashboardClientHandler implements Runnable {
} }
} }
/**
* Valida e extrai os dados estatísticos da mensagem.
* <p>
* Implementa uma lógica de correção de tipagem para payloads desserializados via Gson.
* Frequentemente, objetos genéricos são desserializados como {@code LinkedHashMap} em vez
* da classe alvo {@link StatsUpdatePayload}. Este método deteta essa situação e realiza
* uma conversão "round-trip" (Map -> JSON -> Object) para garantir a integridade dos dados.
*
* @param message A mensagem recebida da rede.
*/
private void processMessage(MessageProtocol message) { private void processMessage(MessageProtocol message) {
if (message.getType() != MessageType.STATS_UPDATE) { if (message.getType() != MessageType.STATS_UPDATE) {
System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType()); System.out.println("[Handler] Ignoring non-statistics message type: " + message.getType());
@@ -78,6 +111,7 @@ public class DashboardClientHandler implements Runnable {
stats = (StatsUpdatePayload) payload; stats = (StatsUpdatePayload) payload;
} else if (payload instanceof java.util.Map) { } else if (payload instanceof java.util.Map) {
// Gson deserialized as LinkedHashMap - re-serialize and deserialize properly // Gson deserialized as LinkedHashMap - re-serialize and deserialize properly
// This acts as a type-safety bridge for generic JSON payloads
com.google.gson.Gson gson = new com.google.gson.Gson(); com.google.gson.Gson gson = new com.google.gson.Gson();
String json = gson.toJson(payload); String json = gson.toJson(payload);
stats = gson.fromJson(json, StatsUpdatePayload.class); stats = gson.fromJson(json, StatsUpdatePayload.class);
@@ -90,6 +124,15 @@ public class DashboardClientHandler implements Runnable {
updateStatistics(senderId, stats); updateStatistics(senderId, stats);
} }
/**
* Aplica os dados recebidos ao modelo global de estatísticas.
* <p>
* Distingue entre atualizações incrementais (ex: contagem de veículos) e
* substituições de estado (ex: tempo total de sistema reportado pelo nó de saída).
*
* @param senderId Identificador do nó que enviou a atualização (ex: "Cr1", "ExitNode").
* @param stats O objeto DTO contendo as métricas normalizadas.
*/
private void updateStatistics(String senderId, StatsUpdatePayload stats) { private void updateStatistics(String senderId, StatsUpdatePayload stats) {
if (stats.getTotalVehiclesGenerated() >= 0) { if (stats.getTotalVehiclesGenerated() >= 0) {
statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated()); statistics.updateVehiclesGenerated(stats.getTotalVehiclesGenerated());

View File

@@ -10,17 +10,43 @@ import java.util.concurrent.atomic.AtomicBoolean;
import sd.config.SimulationConfig; import sd.config.SimulationConfig;
/** /**
* Agrega e apresenta estatísticas em tempo real de todos os processos da simulação. * Servidor central de agregação de telemetria e estatísticas.
* Usa um thread pool para gerir ligações concorrentes de clientes. * <p>
* Este componente atua como o nó de monitorização do sistema distribuído.
* Implementa uma arquitetura de servidor concorrente utilizando um {@link ExecutorService}
* (Thread Pool) para gerir múltiplas conexões de entrada simultâneas provenientes
* das Interseções, Coordenador e Nó de Saída.
* <p>
* Suporta dois modos de operação:
* <ul>
* <li><b>Headless (CLI):</b> Renderização periódica de métricas no terminal (stdout).</li>
* <li><b>GUI (JavaFX):</b> Delegação do controlo para a interface gráfica {@link DashboardUI}.</li>
* </ul>
*/ */
public class DashboardServer { public class DashboardServer {
private final int port; private final int port;
/** Armazenamento em memória (Thread-safe) do estado global do sistema. */
private final DashboardStatistics statistics; private final DashboardStatistics statistics;
/** Pool de threads para isolamento de falhas e gestão de recursos de I/O. */
private final ExecutorService clientHandlerPool; private final ExecutorService clientHandlerPool;
/** Flag atómica para controlo seguro do ciclo de vida do servidor. */
private final AtomicBoolean running; private final AtomicBoolean running;
private ServerSocket serverSocket; private ServerSocket serverSocket;
/**
* Ponto de entrada (Bootstrap) da aplicação de monitorização.
* <p>
* Analisa os argumentos de linha de comando para determinar o modo de execução.
* Se a flag {@code --gui} ou {@code -g} estiver presente, inicia o subsistema JavaFX.
* Caso contrário, inicia o modo servidor de terminal padrão.
*
* @param args Argumentos de CLI (ex: caminho do config, flags de modo).
*/
public static void main(String[] args) { public static void main(String[] args) {
// Check if GUI mode is requested // Check if GUI mode is requested
boolean useGUI = false; boolean useGUI = false;
@@ -70,13 +96,24 @@ public class DashboardServer {
} }
} }
/**
* Inicializa a infraestrutura do servidor.
*
* @param config A configuração carregada contendo a porta de escuta.
*/
public DashboardServer(SimulationConfig config) { public DashboardServer(SimulationConfig config) {
this.port = config.getDashboardPort(); this.port = config.getDashboardPort();
this.statistics = new DashboardStatistics(); this.statistics = new DashboardStatistics();
// Fixed pool limita o consumo de recursos, prevenindo exaustão sob carga alta
this.clientHandlerPool = Executors.newFixedThreadPool(10); this.clientHandlerPool = Executors.newFixedThreadPool(10);
this.running = new AtomicBoolean(false); this.running = new AtomicBoolean(false);
} }
/**
* Inicia a escuta por conexões (Bind & Listen) e a thread de despacho.
*
* @throws IOException Se a porta já estiver em uso ou ocorrer erro de bind.
*/
public void start() throws IOException { public void start() throws IOException {
if (running.get()) { if (running.get()) {
System.out.println("Dashboard Server is already running."); System.out.println("Dashboard Server is already running.");
@@ -95,6 +132,13 @@ public class DashboardServer {
acceptThread.start(); acceptThread.start();
} }
/**
* Loop principal de aceitação de conexões (Dispatcher).
* <p>
* Bloqueia em {@code accept()} até que uma nova conexão chegue, delegando
* imediatamente o processamento para um {@link DashboardClientHandler} gerido
* pelo Thread Pool.
*/
private void acceptConnections() { private void acceptConnections() {
while (running.get()) { while (running.get()) {
try { try {
@@ -112,6 +156,10 @@ public class DashboardServer {
} }
} }
/**
* Ciclo de renderização de métricas para o modo CLI (Headless).
* Atualiza o ecrã a cada 5 segundos.
*/
@SuppressWarnings("BusyWait") @SuppressWarnings("BusyWait")
private void displayLoop() { private void displayLoop() {
final long DISPLAY_INTERVAL_MS = 5000; final long DISPLAY_INTERVAL_MS = 5000;
@@ -127,6 +175,9 @@ public class DashboardServer {
} }
} }
/**
* Renderiza o snapshot atual das estatísticas no stdout.
*/
public void displayStatistics() { public void displayStatistics() {
System.out.println("\n" + "=".repeat(60)); System.out.println("\n" + "=".repeat(60));
System.out.println("REAL-TIME SIMULATION STATISTICS"); System.out.println("REAL-TIME SIMULATION STATISTICS");
@@ -135,6 +186,13 @@ public class DashboardServer {
System.out.println("=".repeat(60)); System.out.println("=".repeat(60));
} }
/**
* Procedimento de encerramento gracioso (Graceful Shutdown).
* <p>
* 1. Altera flag de execução.
* 2. Fecha o socket do servidor para desbloquear a thread de aceitação.
* 3. Força o encerramento do pool de threads de clientes.
*/
public void stop() { public void stop() {
if (!running.get()) { if (!running.get()) {
return; return;

View File

@@ -9,8 +9,13 @@ import java.util.concurrent.atomic.AtomicLong;
import sd.model.VehicleType; import sd.model.VehicleType;
/** /**
* Armazenamento thread-safe de estatísticas agregadas da simulação. * Repositório central de estado da simulação, desenhado para acesso concorrente de alta frequência.
* Usa tipos atómicos e coleções concorrentes para atualizações sem locks. * <p>
* Esta classe atua como a "Single Source of Truth" para o Dashboard. Utiliza primitivas
* de concorrência do pacote {@code java.util.concurrent} (como {@link AtomicInteger} e
* {@link ConcurrentHashMap}) para permitir leituras e escritas simultâneas sem a necessidade
* de bloqueios explícitos (Lock-Free), minimizando a latência de processamento das mensagens
* recebidas dos múltiplos nós da rede.
*/ */
public class DashboardStatistics { public class DashboardStatistics {
@@ -19,13 +24,21 @@ public class DashboardStatistics {
private final AtomicLong totalSystemTime; private final AtomicLong totalSystemTime;
private final AtomicLong totalWaitingTime; private final AtomicLong totalWaitingTime;
/** Mapa thread-safe para armazenar métricas granulares por interseção. */
private final Map<String, IntersectionStats> intersectionStats; private final Map<String, IntersectionStats> intersectionStats;
private final Map<VehicleType, AtomicInteger> vehicleTypeCount; private final Map<VehicleType, AtomicInteger> vehicleTypeCount;
private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime; private final Map<VehicleType, AtomicLong> vehicleTypeWaitTime;
/** Timestamp da última atualização de escrita, com garantia de visibilidade de memória (volatile). */
private volatile long lastUpdateTime; private volatile long lastUpdateTime;
/** Buffer para sinalização assíncrona de mudança de política (Dashboard -> Coordenador). */
private volatile String requestedRoutingPolicy; private volatile String requestedRoutingPolicy;
/**
* Inicializa os contadores atómicos e as estruturas de dados concorrentes.
*/
public DashboardStatistics() { public DashboardStatistics() {
this.totalVehiclesGenerated = new AtomicInteger(0); this.totalVehiclesGenerated = new AtomicInteger(0);
this.totalVehiclesCompleted = new AtomicInteger(0); this.totalVehiclesCompleted = new AtomicInteger(0);
@@ -95,6 +108,17 @@ public class DashboardStatistics {
updateTimestamp(); updateTimestamp();
} }
/**
* Atualiza ou inicializa atomicamente as estatísticas de uma interseção específica.
* <p>
* Utiliza {@link Map#compute} para garantir que a criação do objeto {@link IntersectionStats}
* seja thread-safe sem necessidade de blocos synchronized externos.
*
* @param intersectionId ID da interseção.
* @param arrivals Total acumulado de chegadas.
* @param departures Total acumulado de partidas.
* @param currentQueueSize Tamanho instantâneo da fila.
*/
public void updateIntersectionStats(String intersectionId, int arrivals, public void updateIntersectionStats(String intersectionId, int arrivals,
int departures, int currentQueueSize) { int departures, int currentQueueSize) {
intersectionStats.compute(intersectionId, (id, stats) -> { intersectionStats.compute(intersectionId, (id, stats) -> {
@@ -111,6 +135,8 @@ public class DashboardStatistics {
lastUpdateTime = System.currentTimeMillis(); lastUpdateTime = System.currentTimeMillis();
} }
// --- Getters e Métricas Calculadas ---
public int getTotalVehiclesGenerated() { public int getTotalVehiclesGenerated() {
return totalVehiclesGenerated.get(); return totalVehiclesGenerated.get();
} }
@@ -119,12 +145,20 @@ public class DashboardStatistics {
return totalVehiclesCompleted.get(); return totalVehiclesCompleted.get();
} }
/**
* Calcula o tempo médio no sistema em tempo real.
* @return Média em milissegundos (0.0 se nenhum veículo completou).
*/
public double getAverageSystemTime() { public double getAverageSystemTime() {
int completed = totalVehiclesCompleted.get(); int completed = totalVehiclesCompleted.get();
if (completed == 0) return 0.0; if (completed == 0) return 0.0;
return (double) totalSystemTime.get() / completed; return (double) totalSystemTime.get() / completed;
} }
/**
* Calcula o tempo médio de espera em tempo real.
* @return Média em milissegundos (0.0 se nenhum veículo completou).
*/
public double getAverageWaitingTime() { public double getAverageWaitingTime() {
int completed = totalVehiclesCompleted.get(); int completed = totalVehiclesCompleted.get();
if (completed == 0) return 0.0; if (completed == 0) return 0.0;
@@ -154,10 +188,11 @@ public class DashboardStatistics {
} }
/** /**
* Obtém os tamanhos atuais das filas de todas as interseções. * Obtém um snapshot dos tamanhos atuais das filas de todas as interseções.
* Usado pela política LEAST_CONGESTED para roteamento dinâmico. * <p>
* * Utilizado primariamente pelo algoritmo de roteamento dinâmico (LEAST_CONGESTED)
* @return mapa com intersectionId -> queueSize * para tomar decisões de encaminhamento baseadas na carga atual da rede.
* * @return Mapa contendo {@code intersectionId -> queueSize}.
*/ */
public Map<String, Integer> getCurrentQueueSizes() { public Map<String, Integer> getCurrentQueueSizes() {
Map<String, Integer> queueSizes = new HashMap<>(); Map<String, Integer> queueSizes = new HashMap<>();
@@ -168,16 +203,17 @@ public class DashboardStatistics {
} }
/** /**
* Define a política de roteamento solicitada pelo dashboard. * Regista uma intenção de mudança de política de roteamento solicitada pela UI.
* O coordenador deve verificar periodicamente e aplicar a mudança. * O Coordenador fará polling deste valor periodicamente.
*/ */
public void setRequestedRoutingPolicy(String policy) { public void setRequestedRoutingPolicy(String policy) {
this.requestedRoutingPolicy = policy; this.requestedRoutingPolicy = policy;
} }
/** /**
* Obtém e limpa a política de roteamento solicitada. * Obtém e limpa atomicamente a política de roteamento solicitada.
* Retorna null se não houver mudança pendente. * Implementa a semântica de consumo único (one-time consumption).
* * @return A política solicitada ou null se não houver mudança pendente.
*/ */
public synchronized String getAndClearRequestedRoutingPolicy() { public synchronized String getAndClearRequestedRoutingPolicy() {
String policy = this.requestedRoutingPolicy; String policy = this.requestedRoutingPolicy;
@@ -185,6 +221,10 @@ public class DashboardStatistics {
return policy; return policy;
} }
/**
* Imprime um resumo formatado das estatísticas no stdout.
* Útil para o modo CLI (Headless).
*/
public void display() { public void display() {
System.out.println("\n--- GLOBAL STATISTICS ---"); System.out.println("\n--- GLOBAL STATISTICS ---");
System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated()); System.out.printf("Total Vehicles Generated: %d%n", getTotalVehiclesGenerated());
@@ -214,6 +254,10 @@ public class DashboardStatistics {
System.out.printf("%nLast Update: %tT%n", lastUpdateTime); System.out.printf("%nLast Update: %tT%n", lastUpdateTime);
} }
/**
* Agregado de métricas específico para um nó de interseção.
* Mantém contadores atómicos para garantir consistência em atualizações concorrentes.
*/
public static class IntersectionStats { public static class IntersectionStats {
private final String intersectionId; private final String intersectionId;
private final AtomicInteger totalArrivals; private final AtomicInteger totalArrivals;

View File

@@ -3,11 +3,15 @@ package sd.des;
/** /**
* Gere o tempo de simulação para Simulação de Eventos Discretos. * Gere o tempo de simulação para Simulação de Eventos Discretos.
* *
* <p>No DES, o tempo avança em saltos discretos de evento para evento, * <p>
* não de forma contínua como o tempo real.</p> * No DES, o tempo avança em saltos discretos de evento para evento,
* não de forma contínua como o tempo real.
* </p>
* *
* <p>Esta classe garante que todos os processos no sistema distribuído * <p>
* mantêm uma visão sincronizada do tempo de simulação.</p> * Esta classe garante que todos os processos no sistema distribuído
* mantêm uma visão sincronizada do tempo de simulação.
* </p>
*/ */
public class SimulationClock { public class SimulationClock {
private double currentTime; private double currentTime;

View File

@@ -14,7 +14,8 @@ import sd.model.Vehicle;
/** /**
* Rastreia e regista a viagem completa de veículos individuais. * Rastreia e regista a viagem completa de veículos individuais.
* *
* <p>Cria ficheiros de trace detalhados com: * <p>
* Cria ficheiros de trace detalhados com:
* <ul> * <ul>
* <li>Timestamps de todos os eventos * <li>Timestamps de todos os eventos
* <li>Localizações (interseções) * <li>Localizações (interseções)
@@ -106,7 +107,8 @@ public class VehicleTracer {
* Logs when a vehicle is generated. * Logs when a vehicle is generated.
*/ */
public void logGenerated(Vehicle vehicle) { public void logGenerated(Vehicle vehicle) {
if (!isTracking(vehicle.getId())) return; if (!isTracking(vehicle.getId()))
return;
VehicleTrace trace = trackedVehicles.get(vehicle.getId()); VehicleTrace trace = trackedVehicles.get(vehicle.getId());
if (trace != null) { if (trace != null) {
@@ -120,7 +122,8 @@ public class VehicleTracer {
* Logs when a vehicle arrives at an intersection. * Logs when a vehicle arrives at an intersection.
*/ */
public void logArrival(String vehicleId, String intersection, double simulationTime) { public void logArrival(String vehicleId, String intersection, double simulationTime) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -133,7 +136,8 @@ public class VehicleTracer {
* Logs when a vehicle is queued at a traffic light. * Logs when a vehicle is queued at a traffic light.
*/ */
public void logQueued(String vehicleId, String intersection, String direction, int queuePosition) { public void logQueued(String vehicleId, String intersection, String direction, int queuePosition) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -146,7 +150,8 @@ public class VehicleTracer {
* Logs when a vehicle starts waiting at a red light. * Logs when a vehicle starts waiting at a red light.
*/ */
public void logWaitingStart(String vehicleId, String intersection, String direction) { public void logWaitingStart(String vehicleId, String intersection, String direction) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -159,7 +164,8 @@ public class VehicleTracer {
* Logs when a vehicle finishes waiting (light turns green). * Logs when a vehicle finishes waiting (light turns green).
*/ */
public void logWaitingEnd(String vehicleId, String intersection, String direction, double waitTime) { public void logWaitingEnd(String vehicleId, String intersection, String direction, double waitTime) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -172,7 +178,8 @@ public class VehicleTracer {
* Logs when a vehicle starts crossing an intersection. * Logs when a vehicle starts crossing an intersection.
*/ */
public void logCrossingStart(String vehicleId, String intersection, String direction) { public void logCrossingStart(String vehicleId, String intersection, String direction) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -185,7 +192,8 @@ public class VehicleTracer {
* Logs when a vehicle finishes crossing an intersection. * Logs when a vehicle finishes crossing an intersection.
*/ */
public void logCrossingEnd(String vehicleId, String intersection, double crossingTime) { public void logCrossingEnd(String vehicleId, String intersection, double crossingTime) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -198,7 +206,8 @@ public class VehicleTracer {
* Logs when a vehicle departs from an intersection. * Logs when a vehicle departs from an intersection.
*/ */
public void logDeparture(String vehicleId, String intersection, String nextDestination) { public void logDeparture(String vehicleId, String intersection, String nextDestination) {
if (!isTracking(vehicleId)) return; if (!isTracking(vehicleId))
return;
VehicleTrace trace = trackedVehicles.get(vehicleId); VehicleTrace trace = trackedVehicles.get(vehicleId);
if (trace != null) { if (trace != null) {
@@ -211,7 +220,8 @@ public class VehicleTracer {
* Logs when a vehicle exits the system. * Logs when a vehicle exits the system.
*/ */
public void logExit(Vehicle vehicle, double systemTime) { public void logExit(Vehicle vehicle, double systemTime) {
if (!isTracking(vehicle.getId())) return; if (!isTracking(vehicle.getId()))
return;
VehicleTrace trace = trackedVehicles.get(vehicle.getId()); VehicleTrace trace = trackedVehicles.get(vehicle.getId());
if (trace != null) { if (trace != null) {
@@ -272,7 +282,8 @@ public class VehicleTracer {
} }
void logEvent(String eventType, String location, String description) { void logEvent(String eventType, String location, String description) {
if (writer == null) return; if (writer == null)
return;
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
String timestamp = timestampFormat.format(new Date(now)); String timestamp = timestampFormat.format(new Date(now));
@@ -283,13 +294,13 @@ public class VehicleTracer {
relativeTime, relativeTime,
truncate(eventType, 15), truncate(eventType, 15),
truncate(location, 15), truncate(location, 15),
description description);
);
writer.flush(); writer.flush();
} }
void writeSummary(Vehicle vehicle, double systemTime) { void writeSummary(Vehicle vehicle, double systemTime) {
if (writer == null) return; if (writer == null)
return;
writer.println(); writer.println();
writer.println("=".repeat(80)); writer.println("=".repeat(80));
@@ -324,7 +335,8 @@ public class VehicleTracer {
} }
private String truncate(String str, int maxLength) { private String truncate(String str, int maxLength) {
if (str == null) return ""; if (str == null)
return "";
return str.length() <= maxLength ? str : str.substring(0, maxLength); return str.length() <= maxLength ? str : str.substring(0, maxLength);
} }
} }

View File

@@ -7,16 +7,20 @@ import java.util.List;
/** /**
* Representa um veículo que se move pela rede de interseções. * Representa um veículo que se move pela rede de interseções.
* *
* <p>Esta classe é o "gémeo digital" de um carro, mota ou camião. * <p>
* Mantém toda a informação necessária:</p> * Esta classe é o "gémeo digital" de um carro, mota ou camião.
* Mantém toda a informação necessária:
* </p>
* <ul> * <ul>
* <li>Identificação e tipo do veículo</li> * <li>Identificação e tipo do veículo</li>
* <li>Rota completa a percorrer</li> * <li>Rota completa a percorrer</li>
* <li>Métricas de tempo (espera, travessia, total)</li> * <li>Métricas de tempo (espera, travessia, total)</li>
* </ul> * </ul>
* *
* <p>O objeto é serializado e enviado pela rede à medida que o veículo * <p>
* se move entre processos distribuídos.</p> * O objeto é serializado e enviado pela rede à medida que o veículo
* se move entre processos distribuídos.
* </p>
*/ */
public class Vehicle implements Serializable { public class Vehicle implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -42,10 +46,16 @@ public class Vehicle implements Serializable {
*/ */
private int currentRouteIndex; private int currentRouteIndex;
/** Tempo total acumulado (segundos) que o veículo passou à espera em semáforos vermelhos */ /**
* Tempo total acumulado (segundos) que o veículo passou à espera em semáforos
* vermelhos
*/
private double totalWaitingTime; private double totalWaitingTime;
/** Tempo total acumulado (segundos) que o veículo passou a atravessar interseções */ /**
* Tempo total acumulado (segundos) que o veículo passou a atravessar
* interseções
*/
private double totalCrossingTime; private double totalCrossingTime;
/** /**
@@ -80,7 +90,8 @@ public class Vehicle implements Serializable {
} }
/** /**
* Obtém o destino atual (próxima interseção ou saída) para onde o veículo se dirige. * Obtém o destino atual (próxima interseção ou saída) para onde o veículo se
* dirige.
* *
* @return ID do destino atual (ex: "Cr1"), ou {@code null} se a rota terminou * @return ID do destino atual (ex: "Cr1"), ou {@code null} se a rota terminou
*/ */

View File

@@ -102,7 +102,7 @@ vehicle.crossing.time.heavy=4.0
# Travel times between intersections (in seconds) # Travel times between intersections (in seconds)
# Base time for light vehicles (cars) # Base time for light vehicles (cars)
vehicle.travel.time.base=1.0 vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time # Bike travel time = 0.5 x car travel time
vehicle.travel.time.bike.multiplier=0.5 vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4.0 x base travel time # Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=4.0 vehicle.travel.time.heavy.multiplier=4.0

View File

@@ -100,7 +100,7 @@ vehicle.crossing.time.heavy=4.0
# Travel times between intersections (in seconds) # Travel times between intersections (in seconds)
# Base time for light vehicles (cars) # Base time for light vehicles (cars)
vehicle.travel.time.base=1.0 vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time # Bike travel time = 0.5 x car travel time
vehicle.travel.time.bike.multiplier=0.5 vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4.0 x base travel time # Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=4.0 vehicle.travel.time.heavy.multiplier=4.0

View File

@@ -100,7 +100,7 @@ vehicle.crossing.time.heavy=4.0
# Travel times between intersections (in seconds) # Travel times between intersections (in seconds)
# Base time for light vehicles (cars) # Base time for light vehicles (cars)
vehicle.travel.time.base=1.0 vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time # Bike travel time = 0.5 x car travel time
vehicle.travel.time.bike.multiplier=0.5 vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4.0 x base travel time # Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=4.0 vehicle.travel.time.heavy.multiplier=4.0

View File

@@ -99,7 +99,7 @@ vehicle.crossing.time.heavy=4.0
# Travel times between intersections (in seconds) # Travel times between intersections (in seconds)
# Base time for light vehicles (cars) # Base time for light vehicles (cars)
vehicle.travel.time.base=1.0 vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time # Bike travel time = 0.5 x car travel time
vehicle.travel.time.bike.multiplier=0.5 vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4.0 x base travel time # Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=4.0 vehicle.travel.time.heavy.multiplier=4.0