using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Media; using CtrEditor.HydraulicSimulator; using CtrEditor.HydraulicSimulator.TSNet.Components; using CtrEditor.ObjetosSim; using CtrEditor.FuncionesBase; using Newtonsoft.Json; using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; using CommunityToolkit.Mvvm.ComponentModel; using LibS7Adv; namespace CtrEditor.ObjetosSim { /// /// Tanque hidráulico simplificado para integración con TSNet /// public partial class osHydTank : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase { public static string NombreCategoria() => "Componentes Hidráulicos"; public static string NombreClase() => "Tanque Hidráulico"; // Tipos de fluido disponibles public enum FluidTypeId { Water = 0, SyrupBrix40 = 1 } // Private Fields básicos para TSNet private double _elevation = 0.0; // m - elevación del fondo del tanque private double _initialLevel = 1.0; // m - nivel inicial private double _minLevel = 0.0; // m - nivel mínimo private double _maxLevel = 2.0; // m - nivel máximo private double _diameter = 1.0; // m - diámetro del tanque private FluidTypeId _fluidType = FluidTypeId.Water; // Estado actual del tanque (desde TSNet) private double _currentLevel = 1.0; // m private double _currentPressure = 1.013; // bar private double _currentFlow = 0.0; // m³/s // TSNet Integration [JsonIgnore] public TSNetTankAdapter? TSNetAdapter { get; private set; } // Propiedades visuales [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Tamaño visual del tanque")] [property: Name("Tamaño")] public float tamano; [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Color visual del tanque")] [property: Name("Color")] private Brush colorButton_oculto = Brushes.LightBlue; // Constructor public osHydTank() { try { Nombre = "Tanque Hidráulico"; Tamano = 1.0f; Ancho = 0.30f; Alto = 0.40f; Angulo = 0f; Lock_movement = false; Debug.WriteLine($"osHydTank Constructor completado correctamente"); } catch (Exception ex) { Debug.WriteLine($"Error en constructor osHydTank: {ex.Message}"); Nombre = "Tanque Hidráulico"; Tamano = 1.0f; } } // Propiedades de configuración del tanque [Category("🏗️ Configuración del Tanque")] [DisplayName("Elevación")] [Description("Elevación del fondo del tanque (m)")] public double Elevation { get => _elevation; set { if (SetProperty(ref _elevation, value)) { InvalidateHydraulicNetwork(); } } } [Category("🏗️ Configuración del Tanque")] [DisplayName("Nivel inicial")] [Description("Nivel inicial del agua en el tanque (m)")] public double InitialLevel { get => _initialLevel; set { if (SetProperty(ref _initialLevel, Math.Max(0, value))) { InvalidateHydraulicNetwork(); } } } [Category("🏗️ Configuración del Tanque")] [DisplayName("Nivel mínimo")] [Description("Nivel mínimo del tanque (m)")] public double MinLevel { get => _minLevel; set { if (SetProperty(ref _minLevel, Math.Max(0, value))) { InvalidateHydraulicNetwork(); } } } [Category("🏗️ Configuración del Tanque")] [DisplayName("Nivel máximo")] [Description("Nivel máximo del tanque (m)")] public double MaxLevel { get => _maxLevel; set { if (SetProperty(ref _maxLevel, Math.Max(_minLevel + 0.1, value))) { InvalidateHydraulicNetwork(); } } } [Category("🏗️ Configuración del Tanque")] [DisplayName("Diámetro")] [Description("Diámetro del tanque (m)")] public double Diameter { get => _diameter; set { if (SetProperty(ref _diameter, Math.Max(0.1, value))) { InvalidateHydraulicNetwork(); } } } [Category("🧪 Fluido")] [DisplayName("Tipo de fluido")] [Description("Tipo de fluido en el tanque")] public FluidTypeId FluidType { get => _fluidType; set { if (SetProperty(ref _fluidType, value)) { UpdateTankColorFromFluid(); OnPropertyChanged(nameof(FluidDescription)); } } } [Category("🧪 Fluido")] [DisplayName("Descripción del fluido")] [Description("Descripción del tipo de fluido")] [ReadOnly(true)] public string FluidDescription { get { return FluidType switch { FluidTypeId.Water => "Agua", FluidTypeId.SyrupBrix40 => "Jarabe Brix 40%", _ => "Desconocido" }; } } // Estado actual (desde TSNet) [Category("📊 Estado Actual")] [DisplayName("Nivel actual")] [Description("Nivel actual del agua en el tanque (m)")] [ReadOnly(true)] public double CurrentLevel { get => _currentLevel; private set => SetProperty(ref _currentLevel, value); } [Category("📊 Estado Actual")] [DisplayName("Presión actual")] [Description("Presión actual en el tanque (bar)")] [ReadOnly(true)] public double CurrentPressure { get => _currentPressure; private set => SetProperty(ref _currentPressure, value); } [Category("📊 Estado Actual")] [DisplayName("Flujo actual")] [Description("Flujo actual hacia/desde el tanque (m³/s)")] [ReadOnly(true)] public double CurrentFlow { get => _currentFlow; private set => SetProperty(ref _currentFlow, value); } [Category("📊 Estado Actual")] [DisplayName("Volumen actual")] [Description("Volumen actual en el tanque (L)")] [ReadOnly(true)] public double CurrentVolume { get { var area = Math.PI * Math.Pow(_diameter / 2, 2); // m² return _currentLevel * area * 1000; // L } } [Category("📊 Estado Actual")] [DisplayName("Porcentaje de llenado")] [Description("Porcentaje de llenado del tanque")] [ReadOnly(true)] public double FillPercentage { get { if (_maxLevel <= _minLevel) return 0; return Math.Max(0, Math.Min(100, (_currentLevel - _minLevel) / (_maxLevel - _minLevel) * 100)); } } /// /// Nivel actual en metros (alias para compatibilidad) /// [Category("📊 Estado Actual")] [DisplayName("Nivel actual (m)")] [Description("Nivel actual del líquido en metros")] public double CurrentLevelM { get => _currentLevel; set => SetProperty(ref _currentLevel, value); } /// /// Balance de flujo neto del tanque /// [Category("📊 Estado Actual")] [DisplayName("Balance de flujo")] [Description("Balance neto de flujo del tanque (positivo = entrada, negativo = salida)")] [ReadOnly(true)] public double FlowBalance => _currentFlow; /// /// Tipo de tanque para clasificación /// [Category("📊 Estado Actual")] [DisplayName("Tipo de tanque")] [Description("Tipo de tanque para clasificación")] [ReadOnly(true)] public string TankType => "Simplified"; /// /// Descripción actual del fluido /// [Category("📊 Estado Actual")] [DisplayName("Descripción fluido actual")] [Description("Descripción detallada del fluido actual")] [ReadOnly(true)] public string CurrentFluidDescription => FluidDescription; /// /// Nivel máximo en metros (alias para compatibilidad) /// [Category("📏 Dimensiones")] [DisplayName("Nivel máximo (m)")] [Description("Nivel máximo del tanque en metros")] public double MaxLevelM { get => _maxLevel; set => SetProperty(ref _maxLevel, value); } /// /// Nivel mínimo en metros (alias para compatibilidad) /// [Category("📏 Dimensiones")] [DisplayName("Nivel mínimo (m)")] [Description("Nivel mínimo del tanque en metros")] public double MinLevelM { get => _minLevel; set => SetProperty(ref _minLevel, value); } /// /// Área de sección transversal del tanque /// [Category("📏 Dimensiones")] [DisplayName("Área sección transversal")] [Description("Área de sección transversal del tanque (m²)")] public double CrossSectionalArea { get => Math.PI * Math.Pow(_diameter / 2.0, 2); set => _diameter = Math.Sqrt(value / Math.PI) * 2.0; // Calcular diámetro desde área } /// /// Presión del tanque /// [Category("📊 Estado Actual")] [DisplayName("Presión del tanque")] [Description("Presión actual del tanque (Pa)")] public double TankPressure { get; set; } = 101325.0; // 1 atm por defecto /// /// Si el tanque tiene presión fija /// [Category("⚙️ Configuración")] [DisplayName("Presión fija")] [Description("Indica si el tanque mantiene presión fija")] public bool IsFixedPressure { get; set; } = false; // Métodos básicos private string nombre = NombreClase(); [Category("🏷️ Identificación")] [Description("Nombre identificativo del objeto")] [Name("Nombre")] public override string Nombre { get => nombre; set => SetProperty(ref nombre, value); } public override void OnMove(float LeftPixels, float TopPixels) { // Movimiento básico } public override void OnResize(float Delta_Width, float Delta_Height) { Tamano += Delta_Width + Delta_Height; } public override void UpdateGeometryStart() { // El componente se registra automáticamente como IHydraulicComponent } public override void UpdateControl(int elapsedMilliseconds) { // Los valores provienen de TSNet OnPropertyChanged(nameof(CurrentVolume)); OnPropertyChanged(nameof(FillPercentage)); UpdateTankColorFromFluid(); } private void UpdateTankColorFromFluid() { ColorButton_oculto = FluidType switch { FluidTypeId.Water => Brushes.LightBlue, FluidTypeId.SyrupBrix40 => Brushes.Orange, _ => Brushes.Gray }; } private void InvalidateHydraulicNetwork() { // Invalidar la red hidráulica para que se recalcule var mainViewModel = Application.Current?.MainWindow?.DataContext as MainViewModel; mainViewModel?.InvalidateHydraulicNetwork(); } // Implementación de interfaces hidráulicas public void ApplyHydraulicPressure(double pressurePa) { CurrentPressure = pressurePa / 100000.0; // Pa a bar } public void ApplyHydraulicFlow(double flowM3s) { CurrentFlow = flowM3s; } public void UpdateHydraulicState(double timeSeconds) { // TSNet maneja el estado } // Métodos para TSNet public void UpdateFromTSNetResults(double level, double pressure, double flow) { CurrentLevel = Math.Max(_minLevel, Math.Min(_maxLevel, level)); CurrentPressure = pressure / 100000.0; // Pa a bar CurrentFlow = flow; } // Crear nodo para TSNet public TSNetTankAdapter CreateTSNetAdapter() { TSNetAdapter = new TSNetTankAdapter(this); return TSNetAdapter; } // Implementación de IHydraulicComponent [JsonIgnore] public bool HasHydraulicComponents => true; public List GetHydraulicNodes() { var nodes = new List(); // Sanitizar el nombre para compatibilidad con EPANET var sanitizedName = SanitizeNodeName(Nombre); // Tanque como nodo libre por defecto nodes.Add(new HydraulicNodeDefinition(sanitizedName, false, null, $"Tanque hidráulico - {FluidDescription}")); return nodes; } /// /// Sanitiza nombres de nodos para compatibilidad con EPANET INP /// private string SanitizeNodeName(string nodeName) { if (string.IsNullOrEmpty(nodeName)) return "UnknownTank"; // 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 = "Tank_" + result; } return string.IsNullOrEmpty(result) ? "UnknownTank" : result; } public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) { try { // Sanitizar el nombre para compatibilidad con EPANET var sanitizedName = SanitizeNodeName(Nombre); // Actualizar presión desde TSNet if (pressures.ContainsKey(sanitizedName)) { var pressurePa = pressures[sanitizedName]; CurrentPressure = pressurePa / 100000.0; // Convertir Pa a bar } // Calcular flujo neto desde pipes conectadas var netFlowM3s = CalculateNetFlowFromConnectedPipes(flows); CurrentFlow = netFlowM3s; // Actualizar nivel basado en balance de flujo UpdateLevelFromFlowBalance(netFlowM3s); } catch (Exception ex) { Debug.WriteLine($"Error en Tank {Nombre} ApplyHydraulicResults: {ex.Message}"); } } public List GetHydraulicElements() { // Los tanques no generan elementos hidráulicos, solo nodos return new List(); } public void UpdateHydraulicProperties() { // Actualizar propiedades antes de la simulación } // Implementación de IHydraulicFlowReceiver public void SetFlow(double flow) { CurrentFlow = flow; } public double GetFlow() { return CurrentFlow; } // Implementación de IHydraulicPressureReceiver public void SetPressure(double pressure) { CurrentPressure = pressure / 100000.0; // Pa a bar } public double GetPressure() { return CurrentPressure * 100000.0; // bar a Pa } // Implementación de IosBase public void Start() { // Inicialización del tanque para la simulación } public void Inicializar(int valorInicial) { // Inicialización con valor inicial } public void Disposing() { // Limpieza si es necesaria } public int zIndex_fromFrames { get; set; } = 2; public ZIndexEnum ZIndex_Base() { return ZIndexEnum.Estaticos; } public override void CopyFrom(osBase source) { if (source is osHydTank sourceTank) { Elevation = sourceTank.Elevation; InitialLevel = sourceTank.InitialLevel; MinLevel = sourceTank.MinLevel; MaxLevel = sourceTank.MaxLevel; Diameter = sourceTank.Diameter; FluidType = sourceTank.FluidType; } base.CopyFrom(source); } // Métodos privados auxiliares private double CalculateNetFlowFromConnectedPipes(Dictionary flows) { double netFlow = 0.0; var mainViewModel = Application.Current?.MainWindow?.DataContext as MainViewModel; if (mainViewModel?.ObjetosSimulables == null) return netFlow; try { // Buscar todas las pipes conectadas a este tanque var connectedPipes = mainViewModel.ObjetosSimulables .OfType() .Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre) .ToList(); foreach (var pipe in connectedPipes) { // Buscar el flujo de la pipe en los resultados de TSNet var pipeElementName = $"PIPE_{pipe.Nombre}"; if (flows.ContainsKey(pipeElementName)) { var pipeFlow = flows[pipeElementName]; // m³/s // Determinar dirección: positivo si entra al tanque, negativo si sale if (pipe.Id_ComponenteB == Nombre) { // La pipe va hacia este tanque netFlow += pipeFlow; } else if (pipe.Id_ComponenteA == Nombre) { // La pipe sale desde este tanque netFlow -= pipeFlow; } } } return netFlow; } catch (Exception ex) { Debug.WriteLine($"Error calculating net flow for tank {Nombre}: {ex.Message}"); return 0.0; } } private void UpdateLevelFromFlowBalance(double netFlowM3s) { if (Math.Abs(netFlowM3s) < 1e-6) return; // Flujo muy pequeño // Calcular área transversal var area = Math.PI * Math.Pow(_diameter / 2, 2); // m² // Tiempo de simulación (asumimos 1 segundo por simplicidad) var deltaTime = 1.0; // segundos // Cambio de nivel = flujo volumétrico * tiempo / área var deltaLevel = (netFlowM3s * deltaTime) / area; // m // Actualizar nivel actual var newLevel = _currentLevel + deltaLevel; CurrentLevel = Math.Max(_minLevel, Math.Min(_maxLevel, newLevel)); } } }