using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HydraulicSimulator.Models;
namespace CtrEditor.HydraulicSimulator.TSNet
{
///
/// Generador de archivos INP para TSNet basado en la red hidráulica
/// Convierte el modelo interno a formato EPANET INP compatible con TSNet
///
public class TSNetINPGenerator
{
private readonly HydraulicNetwork _network;
private readonly TSNetConfiguration _config;
private readonly TSNetSimulationManager _simulationManager;
public TSNetINPGenerator(HydraulicNetwork network, TSNetConfiguration config, TSNetSimulationManager simulationManager = null)
{
_network = network ?? throw new ArgumentNullException(nameof(network));
_config = config ?? throw new ArgumentNullException(nameof(config));
_simulationManager = simulationManager; // Puede ser null para compatibilidad hacia atrás
}
///
/// Genera el archivo INP completo
///
public async Task GenerateAsync(string filePath)
{
var content = new StringBuilder();
// Header
content.AppendLine("[TITLE]");
content.AppendLine("TSNet Hydraulic Network");
content.AppendLine($"Generated on {DateTime.Now}");
content.AppendLine("CtrEditor TSNet Integration");
content.AppendLine();
// Junctions (nodos libres)
GenerateJunctions(content);
// Reservoirs (nodos de presión fija)
GenerateReservoirs(content);
// Tanks (tanques)
GenerateTanks(content);
// Pipes (tuberías)
GeneratePipes(content);
// Pumps (bombas)
GeneratePumps(content);
// Valves (válvulas)
GenerateValves(content);
// Patterns (patrones de demanda)
GeneratePatterns(content);
// Curves (curvas de bombas)
GenerateCurves(content);
// Quality
GenerateQuality(content);
// Options
GenerateOptions(content);
// Times
GenerateTimes(content);
// Coordinates (opcional, para visualización)
GenerateCoordinates(content);
// End
content.AppendLine("[END]");
// Escribir archivo
await File.WriteAllTextAsync(filePath, content.ToString());
}
private void GenerateJunctions(StringBuilder content)
{
content.AppendLine("[JUNCTIONS]");
content.AppendLine(";ID \tElev \tDemand \tPattern ");
foreach (var node in _network.Nodes.Values.Where(n => !n.FixedP && !IsTank(n)))
{
var elevation = GetNodeElevation(node);
var demand = GetNodeDemand(node);
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t{demand.ToString("F2", CultureInfo.InvariantCulture)} \t;");
}
content.AppendLine();
}
private void GenerateReservoirs(StringBuilder content)
{
content.AppendLine("[RESERVOIRS]");
content.AppendLine(";ID \tHead \tPattern ");
foreach (var node in _network.Nodes.Values.Where(n => n.FixedP && !IsTank(n)))
{
var head = PressureToHead(node.P);
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{head.ToString("F2", CultureInfo.InvariantCulture)} \t;");
}
content.AppendLine();
}
private void GenerateTanks(StringBuilder content)
{
content.AppendLine("[TANKS]");
content.AppendLine(";ID \tElevation \tInitLevel \tMinLevel \tMaxLevel \tDiameter \tMinVol \tVolCurve");
// Usar configuración real de los TSNetTankAdapter en lugar de valores dummy
var tankNodes = _network.Nodes.Values.Where(IsTank);
foreach (var node in tankNodes)
{
var elevation = GetNodeElevation(node);
var sanitizedName = SanitizeNodeName(node.Name);
// Buscar el adapter correspondiente en el simulation manager
var tankAdapter = _simulationManager?.GetTankAdapterByNodeName(node.Name);
if (tankAdapter?.Configuration != null)
{
// Usar configuración real del tanque
var config = tankAdapter.Configuration;
// Debug: Log de generación INP
System.Diagnostics.Debug.WriteLine($"INPGenerator: Generando tanque {node.Name}");
System.Diagnostics.Debug.WriteLine($" config.InitialLevelM: {config.InitialLevelM}");
System.Diagnostics.Debug.WriteLine($" config.MaxLevelM: {config.MaxLevelM}");
System.Diagnostics.Debug.WriteLine($" config.DiameterM: {config.DiameterM}");
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t{config.InitialLevelM.ToString("F2", CultureInfo.InvariantCulture)} \t{config.MinLevelM.ToString("F2", CultureInfo.InvariantCulture)} \t{config.MaxLevelM.ToString("F2", CultureInfo.InvariantCulture)} \t{config.DiameterM.ToString("F2", CultureInfo.InvariantCulture)} \t0 \t");
}
else
{
// Fallback a valores por defecto si no hay configuración
System.Diagnostics.Debug.WriteLine($"INPGenerator: WARNING - No se encontró configuración para tanque {node.Name}, usando valores por defecto");
content.AppendLine($" {sanitizedName,-15}\t{elevation.ToString("F2", CultureInfo.InvariantCulture)} \t1.0 \t0.0 \t2.0 \t1.0 \t0 \t");
}
}
content.AppendLine();
}
private void GeneratePipes(StringBuilder content)
{
content.AppendLine("[PIPES]");
content.AppendLine(";ID \tNode1 \tNode2 \tLength \tDiameter \tRoughness \tMinorLoss \tStatus");
int pipeId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType())
{
var id = $"PIPE{pipeId++}";
var length = element.L; // Usar L en lugar de Length
var diameter = element.D * 1000; // Usar D en lugar de Diameter, convertir a mm
var roughness = element.Rough * 1000; // Usar Rough en lugar de Roughness, convertir a mm
var sanitizedN1 = SanitizeNodeName(branch.N1);
var sanitizedN2 = SanitizeNodeName(branch.N2);
content.AppendLine($" {id,-15}\t{sanitizedN1,-15}\t{sanitizedN2,-15}\t{length.ToString("F2", CultureInfo.InvariantCulture)} \t{diameter.ToString("F1", CultureInfo.InvariantCulture)} \t{roughness.ToString("F4", CultureInfo.InvariantCulture)} \t0 \tOpen");
}
}
content.AppendLine();
}
private void GeneratePumps(StringBuilder content)
{
content.AppendLine("[PUMPS]");
content.AppendLine(";ID \tNode1 \tNode2 \tParameters");
int pumpId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType())
{
var id = $"PUMP{pumpId}";
var sanitizedN1 = SanitizeNodeName(branch.N1);
var sanitizedN2 = SanitizeNodeName(branch.N2);
content.AppendLine($" {id,-15}\t{sanitizedN1,-15}\t{sanitizedN2,-15}\tHEAD CURVE{pumpId}");
pumpId++;
}
}
content.AppendLine();
}
private void GenerateValves(StringBuilder content)
{
content.AppendLine("[VALVES]");
content.AppendLine(";ID \tNode1 \tNode2 \tDiameter \tType\tSetting \tMinorLoss ");
int valveId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType())
{
var id = $"VALVE{valveId++}";
var diameter = EstimateDiameter(element);
content.AppendLine($" {id,-15}\t{branch.N1,-15}\t{branch.N2,-15}\t{diameter:F1} \tFCV \t{element.KvFull:F2} \t0 ");
}
}
content.AppendLine();
}
private void GeneratePatterns(StringBuilder content)
{
content.AppendLine("[PATTERNS]");
content.AppendLine(";ID \tMultipliers");
content.AppendLine(" 1 \t1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0");
content.AppendLine(" 1 \t1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0");
content.AppendLine(" 1 \t1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0");
content.AppendLine();
}
private void GenerateCurves(StringBuilder content)
{
content.AppendLine("[CURVES]");
content.AppendLine(";ID \tX-Value \tY-Value");
// Generar curvas para bombas
int curveId = 1;
foreach (var branch in _network.Branches)
{
foreach (var element in branch.Elements.OfType())
{
content.AppendLine($";PUMP CURVE {curveId}");
// Puntos de la curva H-Q (simplificada)
var maxHead = element.H0;
var maxFlow = element.H0 / 10; // Estimación simple
content.AppendLine($" CURVE{curveId} \t0 \t{maxHead.ToString("F2", CultureInfo.InvariantCulture)}");
content.AppendLine($" CURVE{curveId} \t{(maxFlow/2).ToString("F2", CultureInfo.InvariantCulture)} \t{(maxHead*0.8).ToString("F2", CultureInfo.InvariantCulture)}");
content.AppendLine($" CURVE{curveId} \t{maxFlow.ToString("F2", CultureInfo.InvariantCulture)} \t{(maxHead*0.5).ToString("F2", CultureInfo.InvariantCulture)}");
curveId++;
}
}
content.AppendLine();
}
private void GenerateQuality(StringBuilder content)
{
content.AppendLine("[QUALITY]");
content.AppendLine(";Node \tInitQual");
foreach (var node in _network.Nodes.Values)
{
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t0.0");
}
content.AppendLine();
}
private void GenerateOptions(StringBuilder content)
{
content.AppendLine("[OPTIONS]");
content.AppendLine(" Units \tLPS"); // Litros por segundo
content.AppendLine(" Headloss \tD-W"); // Darcy-Weisbach
content.AppendLine(" Specific Gravity\t1.0");
content.AppendLine($" Viscosity \t{_network.Fluid.Mu.ToString("E2", CultureInfo.InvariantCulture)}");
content.AppendLine(" Trials \t40");
content.AppendLine(" Accuracy \t0.001");
content.AppendLine(" CHECKFREQ \t2");
content.AppendLine(" MAXCHECK \t10");
content.AppendLine(" DAMPLIMIT \t0");
content.AppendLine(" Unbalanced \tContinue 10");
content.AppendLine(" Pattern \t1");
content.AppendLine(" Demand Multiplier\t1.0");
content.AppendLine(" Emitter Exponent\t0.5");
content.AppendLine(" Quality \tNone mg/L");
content.AppendLine(" Diffusivity \t1.0");
content.AppendLine(" Tolerance \t0.01");
content.AppendLine();
}
private void GenerateTimes(StringBuilder content)
{
content.AppendLine("[TIMES]");
// Convert duration from seconds to H:MM:SS format
var durationTime = TimeSpan.FromSeconds(_config.Duration);
var durationStr = $"{(int)durationTime.TotalHours}:{durationTime.Minutes:00}:{durationTime.Seconds:00}";
// Convert timestep from seconds to H:MM:SS format
var timestepTime = TimeSpan.FromSeconds(_config.TimeStep);
var timestepStr = $"{(int)timestepTime.TotalHours}:{timestepTime.Minutes:00}:{timestepTime.Seconds:00}";
content.AppendLine($" Duration \t{durationStr}");
content.AppendLine($" Hydraulic Timestep\t{timestepStr}");
content.AppendLine(" Quality Timestep\t0:05:00");
content.AppendLine(" Pattern Timestep\t1:00:00");
content.AppendLine(" Pattern Start \t0:00:00");
content.AppendLine(" Report Timestep \t1:00:00");
content.AppendLine(" Report Start \t0:00:00");
content.AppendLine(" Start ClockTime \t12:00:00 AM");
content.AppendLine(" Statistic \tNone");
content.AppendLine();
}
private void GenerateCoordinates(StringBuilder content)
{
content.AppendLine("[COORDINATES]");
content.AppendLine(";Node \tX-Coord \tY-Coord");
// Generar coordenadas simples en grid
int x = 0, y = 0;
foreach (var node in _network.Nodes.Values)
{
var sanitizedName = SanitizeNodeName(node.Name);
content.AppendLine($" {sanitizedName,-15}\t{x.ToString("F2", CultureInfo.InvariantCulture)} \t{y.ToString("F2", CultureInfo.InvariantCulture)}");
x += 1000;
if (x > 5000)
{
x = 0;
y += 1000;
}
}
content.AppendLine();
}
#region Helper Methods
private bool IsTank(Node node)
{
// Determinar si un nodo es un tanque basado en su nombre
return node.Name.ToLower().Contains("tank") || node.Name.ToLower().Contains("tanque");
}
private double GetNodeElevation(Node node)
{
// Por defecto usar elevación de 0, será mejorado con datos reales
return 0.0;
}
private double GetNodeDemand(Node node)
{
// Por defecto sin demanda, será mejorado con datos reales
return 0.0;
}
private double PressureToHead(double pressurePa)
{
// Convertir presión en Pa a altura en metros
// H = P / (ρ * g)
var density = _network.Fluid.Rho; // Usar Rho en lugar de Density
var gravity = 9.81;
return pressurePa / (density * gravity);
}
private double EstimateDiameter(ValveKv valve)
{
// Estimación del diámetro basado en Kv
// Kv = 0.865 * A * sqrt(2*g*H) donde A = π*D²/4
// Esta es una aproximación, se mejorará con datos reales
return Math.Sqrt(valve.KvFull / 100.0) * 0.1; // en metros
}
///
/// Sanitiza nombres de nodos para compatibilidad con EPANET INP
///
private string SanitizeNodeName(string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
return "UnknownNode";
// Reemplazar espacios y caracteres especiales con guiones bajos
var sanitized = nodeName
.Replace(" ", "_")
.Replace("á", "a")
.Replace("é", "e")
.Replace("í", "i")
.Replace("ó", "o")
.Replace("ú", "u")
.Replace("ü", "u")
.Replace("ñ", "n")
.Replace("Á", "A")
.Replace("É", "E")
.Replace("Í", "I")
.Replace("Ó", "O")
.Replace("Ú", "U")
.Replace("Ü", "U")
.Replace("Ñ", "N");
// Remover caracteres no válidos para EPANET
var validChars = sanitized.Where(c => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray();
var result = new string(validChars);
// Asegurar que empiece con una letra
if (!string.IsNullOrEmpty(result) && !char.IsLetter(result[0]))
{
result = "Node_" + result;
}
return string.IsNullOrEmpty(result) ? "UnknownNode" : result;
}
#endregion
}
}