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 HydraulicSimulator.Models; namespace CtrEditor.ObjetosSim { /// /// Tubería hidráulica simplificada para integración con TSNet /// public partial class osHydPipe : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase { public static string NombreCategoria() => "Componentes Hidráulicos"; public static string NombreClase() => "Tubería Hidráulica"; // Private fields para TSNet private double _length = 1.0; // m - longitud de la tubería private double _diameter = 0.05; // m - diámetro interno private double _roughness = 4.5e-5; // m - rugosidad del material // Estado actual (desde TSNet) private double _currentFlow = 0.0; // m³/s - flujo actual private double _pressureDrop = 0.0; // Pa - caída de presión private double _velocity = 0.0; // m/s - velocidad del fluido // TSNet Integration [JsonIgnore] public TSNetPipeAdapter? TSNetAdapter { get; private set; } // Propiedades visuales [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Ancho visual de la tubería en metros")] [property: Name("Ancho")] private float ancho = 1.0f; [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Alto visual de la tubería en metros")] [property: Name("Alto")] private float alto = 0.05f; [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Color visual de la tubería")] [property: Name("Color")] private Brush colorButton_oculto = Brushes.Gray; [ObservableProperty] [property: Category("🎨 Apariencia")] [property: Description("Ángulo de rotación de la tubería en grados")] [property: Name("Ángulo")] private float angulo = 0f; // Propiedades de conexión [ObservableProperty] [property: Category("🔗 Conexiones")] [property: Description("Primer componente hidráulico conectado")] [property: Name("Componente A")] [property: ItemsSource(typeof(osBaseItemsSource))] private string id_ComponenteA = ""; [ObservableProperty] [property: Category("🔗 Conexiones")] [property: Description("Segundo componente hidráulico conectado")] [property: Name("Componente B")] [property: ItemsSource(typeof(osBaseItemsSource))] private string id_ComponenteB = ""; // Constructor public osHydPipe() { try { Nombre = "Tubería Hidráulica"; Ancho = 1.0f; Alto = 0.05f; Angulo = 0f; Lock_movement = false; Debug.WriteLine($"osHydPipe Constructor completado correctamente"); } catch (Exception ex) { Debug.WriteLine($"Error en constructor osHydPipe: {ex.Message}"); Nombre = "Tubería Hidráulica"; Ancho = 1.0f; Alto = 0.05f; } } // Propiedades de configuración de la tubería [Category("🔧 Tubería Hidráulica")] [DisplayName("Longitud")] [Description("Longitud de la tubería en metros")] public double Length { get => _length; set { if (SetProperty(ref _length, Math.Max(0.1, value))) { InvalidateHydraulicNetwork(); } } } [Category("🔧 Tubería Hidráulica")] [DisplayName("Diámetro")] [Description("Diámetro interno de la tubería en metros")] public double Diameter { get => _diameter; set { if (SetProperty(ref _diameter, Math.Max(0.001, value))) { InvalidateHydraulicNetwork(); } } } [Category("🔧 Tubería Hidráulica")] [DisplayName("Rugosidad")] [Description("Rugosidad del material en metros")] public double Roughness { get => _roughness; set { if (SetProperty(ref _roughness, Math.Max(1e-6, value))) { InvalidateHydraulicNetwork(); } } } // Estado actual (desde TSNet) [Category("📊 Estado Actual")] [DisplayName("Flujo actual")] [Description("Flujo actual a través de la tubería (m³/s)")] [ReadOnly(true)] public double CurrentFlow { get => _currentFlow; private set => SetProperty(ref _currentFlow, value); } [Category("📊 Estado Actual")] [DisplayName("Caída de presión")] [Description("Caída de presión en la tubería (Pa)")] [ReadOnly(true)] public double PressureDrop { get => _pressureDrop; private set => SetProperty(ref _pressureDrop, value); } [Category("📊 Estado Actual")] [DisplayName("Velocidad")] [Description("Velocidad del fluido en la tubería (m/s)")] [ReadOnly(true)] public double FluidVelocity { get => _velocity; private set => SetProperty(ref _velocity, value); } [Category("📊 Estado Actual")] [DisplayName("Flujo (L/min)")] [Description("Flujo actual en L/min")] [ReadOnly(true)] public double CurrentFlowLMin => Math.Abs(CurrentFlow) * 60000.0; // m³/s a L/min [Category("📊 Estado Actual")] [DisplayName("Caída presión (bar)")] [Description("Caída de presión en bar")] [ReadOnly(true)] public double PressureDropBar => PressureDrop / 100000.0; // Pa a bar [Category("📊 Estado Actual")] [DisplayName("Tiene flujo")] [Description("Indica si hay flujo en la tubería")] [ReadOnly(true)] public bool HasFlow => Math.Abs(CurrentFlow) > 1e-6; // 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) { Ancho += Delta_Width; Alto += 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(CurrentFlowLMin)); OnPropertyChanged(nameof(PressureDropBar)); OnPropertyChanged(nameof(HasFlow)); UpdatePipeColor(); } private void UpdatePipeColor() { // Cambiar color según el flujo if (HasFlow) { var flowPercent = Math.Min(1.0, CurrentFlowLMin / 100.0); // normalizar a 100 L/min var intensity = (byte)(255 * flowPercent); ColorButton_oculto = new SolidColorBrush(Color.FromRgb(intensity, (byte)(255 - intensity), 0)); } else { ColorButton_oculto = 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) { // La caída de presión se calcula en TSNet } public void ApplyHydraulicFlow(double flowM3s) { CurrentFlow = flowM3s; // Calcular velocidad if (Diameter > 0) { var area = Math.PI * Math.Pow(Diameter / 2, 2); // m² FluidVelocity = Math.Abs(CurrentFlow) / area; // m/s } } public void UpdateHydraulicState(double timeSeconds) { // TSNet maneja el estado } // Métodos para TSNet public void UpdateFromTSNetResults(double flow, double pressureDrop) { CurrentFlow = flow; PressureDrop = pressureDrop; // Calcular velocidad if (Diameter > 0) { var area = Math.PI * Math.Pow(Diameter / 2, 2); // m² FluidVelocity = Math.Abs(CurrentFlow) / area; // m/s } } // Crear adapter para TSNet public TSNetPipeAdapter CreateTSNetAdapter() { TSNetAdapter = new TSNetPipeAdapter(this); return TSNetAdapter; } // Implementación de IHydraulicComponent [JsonIgnore] public bool HasHydraulicComponents => true; public List GetHydraulicNodes() { // Las tuberías no crean nodos propios, se conectan entre nodos existentes return new List(); } public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures) { try { // Buscar resultados de esta tubería en TSNet var pipeElementName = $"PIPE_{Nombre}"; if (flows.ContainsKey(pipeElementName)) { var pipeFlow = flows[pipeElementName]; ApplyHydraulicFlow(pipeFlow); } // Calcular caída de presión si tenemos datos de los nodos conectados if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB)) { if (pressures.ContainsKey(Id_ComponenteA) && pressures.ContainsKey(Id_ComponenteB)) { var pressureA = pressures[Id_ComponenteA]; var pressureB = pressures[Id_ComponenteB]; PressureDrop = Math.Abs(pressureA - pressureB); } } } catch (Exception ex) { Debug.WriteLine($"Error en Pipe {Nombre} ApplyHydraulicResults: {ex.Message}"); } } public List GetHydraulicElements() { var elements = new List(); // Verificar que la tubería esté conectada a dos componentes if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB)) { try { // Resolver los nombres de los componentes conectados var nodeNameA = ResolveComponentNodeName(Id_ComponenteA); var nodeNameB = ResolveComponentNodeName(Id_ComponenteB); if (!string.IsNullOrEmpty(nodeNameA) && !string.IsNullOrEmpty(nodeNameB)) { // Crear elemento pipe con propiedades correctas var pipeElement = new Pipe(Length, Diameter, Roughness); elements.Add(new HydraulicElementDefinition( $"PIPE_{Nombre}", nodeNameA, nodeNameB, pipeElement, $"Tubería - L:{Length:F1}m, D:{Diameter*1000:F0}mm, Rough:{Roughness*1000:F2}mm" )); if (VerboseOutput) { Debug.WriteLine($"Pipe {Nombre}: Conectando nodos '{nodeNameA}' -> '{nodeNameB}'"); } } else { Debug.WriteLine($"Warning: Pipe {Nombre} no pudo resolver nombres de nodos. " + $"ComponenteA='{Id_ComponenteA}', ComponenteB='{Id_ComponenteB}'"); } } catch (Exception ex) { Debug.WriteLine($"Error en GetHydraulicElements para pipe {Nombre}: {ex.Message}"); } } return elements; } public void UpdateHydraulicProperties() { // Actualizar propiedades antes de la simulación // Validar conexiones if (string.IsNullOrEmpty(Id_ComponenteA) || string.IsNullOrEmpty(Id_ComponenteB)) { Debug.WriteLine($"Warning: Tubería {Nombre} no está completamente conectada"); } } /// /// Resuelve el ID de un componente al nombre del nodo hidráulico correspondiente /// THREAD-SAFE: No accede a objetos WPF desde background threads /// private string ResolveComponentNodeName(string componentId) { try { // Debug: Log the original componentId Debug.WriteLine($"Pipe {Nombre}: Resolviendo componentId '{componentId}'"); // Primero intentar usar el ID directamente como nombre (para compatibilidad) if (!string.IsNullOrEmpty(componentId)) { // Para evitar problemas de threading, usar el Dispatcher para acceder a objetos UI MainViewModel mainViewModel = null; // Solo acceder a la UI desde el thread principal if (Application.Current?.Dispatcher?.CheckAccess() == true) { mainViewModel = Application.Current?.MainWindow?.DataContext as MainViewModel; } else { // Si estamos en un background thread, usar Invoke para acceder de forma segura Application.Current?.Dispatcher?.Invoke(() => { mainViewModel = Application.Current?.MainWindow?.DataContext as MainViewModel; }); } if (mainViewModel?.ObjetosSimulables != null) { List hydraulicComponents = null; // Obtener componentes de forma thread-safe if (Application.Current?.Dispatcher?.CheckAccess() == true) { hydraulicComponents = mainViewModel.ObjetosSimulables .OfType() .Where(comp => comp is osBase) .Cast() .ToList(); } else { Application.Current?.Dispatcher?.Invoke(() => { hydraulicComponents = mainViewModel.ObjetosSimulables .OfType() .Where(comp => comp is osBase) .Cast() .ToList(); }); } if (hydraulicComponents != null) { // Debug: Log available components Debug.WriteLine($"Pipe {Nombre}: Componentes hidráulicos disponibles:"); foreach (var comp in hydraulicComponents) { Debug.WriteLine($" - Nombre: '{comp.Nombre}', ID: '{comp.Id?.Value}'"); } // Buscar por nombre primero (caso más común) var componentByName = hydraulicComponents .FirstOrDefault(comp => comp.Nombre == componentId); if (componentByName != null) { Debug.WriteLine($"Pipe {Nombre}: Encontrado componente por nombre: '{componentByName.Nombre}'"); // Verificar si es una bomba (requiere resolución especial de nodos) if (componentByName is osHydPump) { var sanitizedName = SanitizeNodeName(componentByName.Nombre); // Determinar si esta tubería está conectada a la entrada o salida de la bomba // Basándonos en si la bomba es origen (ComponenteA) o destino (ComponenteB) de esta tubería if (componentId == Id_ComponenteA) { // La bomba es ComponenteA (origen de esta tubería) // Por lo tanto, conectamos desde la salida de la bomba (NODE_B) Debug.WriteLine($"Pipe {Nombre}: Bomba como origen - conectando desde salida: NODE_B_{sanitizedName}"); return $"NODE_B_{sanitizedName}"; } else if (componentId == Id_ComponenteB) { // La bomba es ComponenteB (destino de esta tubería) // Por lo tanto, conectamos hacia la entrada de la bomba (NODE_A) Debug.WriteLine($"Pipe {Nombre}: Bomba como destino - conectando hacia entrada: NODE_A_{sanitizedName}"); return $"NODE_A_{sanitizedName}"; } else { // Fallback: usar salida por defecto Debug.WriteLine($"Pipe {Nombre}: Usando salida por defecto de bomba: NODE_B_{sanitizedName}"); return $"NODE_B_{sanitizedName}"; } } else { // Para tanques y otras componentes, usar el nombre sanitizado return SanitizeNodeName(componentByName.Nombre); } } // Si no se encuentra por nombre, buscar por ID único (convertir string a int) if (int.TryParse(componentId, out int idValue)) { var componentById = hydraulicComponents .FirstOrDefault(comp => comp.Id?.Value == idValue); if (componentById != null) { Debug.WriteLine($"Pipe {Nombre}: Encontrado componente por ID {idValue}: '{componentById.Nombre}'"); // Verificar si es una bomba (requiere resolución especial de nodos) if (componentById is osHydPump) { var sanitizedName = SanitizeNodeName(componentById.Nombre); // Determinar si esta tubería está conectada a la entrada o salida de la bomba if (componentId == Id_ComponenteA) { Debug.WriteLine($"Pipe {Nombre}: Bomba como origen (ID) - conectando desde salida: NODE_B_{sanitizedName}"); return $"NODE_B_{sanitizedName}"; } else if (componentId == Id_ComponenteB) { Debug.WriteLine($"Pipe {Nombre}: Bomba como destino (ID) - conectando hacia entrada: NODE_A_{sanitizedName}"); return $"NODE_A_{sanitizedName}"; } else { Debug.WriteLine($"Pipe {Nombre}: Usando salida por defecto de bomba (ID): NODE_B_{sanitizedName}"); return $"NODE_B_{sanitizedName}"; } } else { return SanitizeNodeName(componentById.Nombre); } } } } // Último intento: usar el componentId directamente si es un nombre válido Debug.WriteLine($"Pipe {Nombre}: No se encontró componente, usando '{componentId}' directamente"); return SanitizeNodeName(componentId); } else { Debug.WriteLine($"Pipe {Nombre}: MainViewModel o ObjetosSimulables es null"); } } else { Debug.WriteLine($"Pipe {Nombre}: componentId está vacío"); } } catch (Exception ex) { Debug.WriteLine($"Error resolviendo componente '{componentId}': {ex.Message}"); } return string.Empty; } /// /// Sanitiza nombres de nodos para compatibilidad con EPANET INP /// private string SanitizeNodeName(string nodeName) { if (string.IsNullOrEmpty(nodeName)) return string.Empty; // 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(); return new string(validChars); } private bool VerboseOutput => true; // Habilitar salida detallada para debugging // Implementación de IHydraulicFlowReceiver public void SetFlow(double flow) { ApplyHydraulicFlow(flow); } public double GetFlow() { return CurrentFlow; } // Implementación de IHydraulicPressureReceiver public void SetPressure(double pressure) { // Las tuberías no tienen presión propia, solo caída de presión } public double GetPressure() { return PressureDrop; } // Implementación de IosBase public void Start() { // Inicialización de la tubería 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; } = 1; public ZIndexEnum ZIndex_Base() { return ZIndexEnum.Estaticos; } public override void CopyFrom(osBase source) { if (source is osHydPipe sourcePipe) { Length = sourcePipe.Length; Diameter = sourcePipe.Diameter; Roughness = sourcePipe.Roughness; Id_ComponenteA = sourcePipe.Id_ComponenteA; Id_ComponenteB = sourcePipe.Id_ComponenteB; } base.CopyFrom(source); } } }