using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Windows.Media;
using CtrEditor.HydraulicSimulator;
using CtrEditor.HydraulicSimulator.TSNet.Components;
using CtrEditor.ObjetosSim;
using CtrEditor.FuncionesBase;
using HydraulicSimulator.Models;
using Newtonsoft.Json;
using Xceed.Wpf.Toolkit.PropertyGrid.Attributes;
using CommunityToolkit.Mvvm.ComponentModel;
using LibS7Adv;
namespace CtrEditor.ObjetosSim
{
///
/// Tubería hidráulica que conecta componentes del sistema hidráulico
///
public partial class osHydPipe : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase
{
// TSNet Integration
[JsonIgnore]
public TSNetPipeAdapter TSNetAdapter { get; private set; }
// Properties
private double _length = 1.0; // metros
private double _diameter = 0.05; // metros (50mm)
private double _roughness = 4.5e-5; // metros - rugosidad del acero comercial
private double _currentFlow = 0.0;
private double _pressureDrop = 0.0;
// 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;
[Category("🔧 Tubería Hidráulica")]
[Description("Longitud de la tubería en metros")]
[Name("Longitud (m)")]
public double Length
{
get => _length;
set => SetProperty(ref _length, Math.Max(0.1, value));
}
[Category("🔧 Tubería Hidráulica")]
[Description("Diámetro interno de la tubería en metros")]
[Name("Diámetro (m)")]
public double Diameter
{
get => _diameter;
set => SetProperty(ref _diameter, Math.Max(0.001, value));
}
[Category("🔧 Tubería Hidráulica")]
[Description("Rugosidad del material en metros")]
[Name("Rugosidad (m)")]
public double Roughness
{
get => _roughness;
set => SetProperty(ref _roughness, Math.Max(1e-6, value));
}
[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 = "";
[Category("📊 Estado")]
[Description("Flujo actual a través de la tubería en m³/s")]
[Name("Flujo Actual (m³/s)")]
[ReadOnly(true)]
public double CurrentFlow
{
get => _currentFlow;
set => SetProperty(ref _currentFlow, value);
}
[Category("📊 Estado")]
[Description("Pérdida de presión en la tubería en Pa")]
[Name("Pérdida Presión (Pa)")]
[ReadOnly(true)]
public double PressureDrop
{
get => _pressureDrop;
set => SetProperty(ref _pressureDrop, value);
}
// Propiedades adicionales para información de fluido
private FluidProperties _currentFluid = new FluidProperties(FluidType.Air);
[Category("🧪 Fluido Actual")]
[DisplayName("Tipo de fluido")]
[Description("Tipo de fluido que atraviesa la tubería")]
[ReadOnly(true)]
public FluidType CurrentFluidType
{
get => _currentFluid.Type;
private set
{
if (_currentFluid.Type != value)
{
_currentFluid.Type = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Actual")]
[DisplayName("Descripción")]
[Description("Descripción completa del fluido actual")]
[ReadOnly(true)]
public string CurrentFluidDescription => _currentFluid.Description;
[Category("🧪 Fluido Actual")]
[DisplayName("Temperatura (°C)")]
[Description("Temperatura del fluido en grados Celsius")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidTemperature => _currentFluid.Temperature;
[Category("🧪 Fluido Actual")]
[DisplayName("Brix (%)")]
[Description("Concentración en grados Brix (para jarabes)")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidBrix => _currentFluid.Brix;
[Category("🧪 Fluido Actual")]
[DisplayName("Concentración (%)")]
[Description("Concentración en porcentaje (para químicos)")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidConcentration => _currentFluid.Concentration;
[Category("🧪 Fluido Actual")]
[DisplayName("Densidad (kg/L)")]
[Description("Densidad del fluido en kg/L")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidDensity => _currentFluid.Density;
[Category("🧪 Fluido Actual")]
[DisplayName("Viscosidad (cP)")]
[Description("Viscosidad dinámica en centipoise")]
[ReadOnly(true)]
[JsonProperty]
public double CurrentFluidViscosity => _currentFluid.Viscosity;
[Category("🧪 Fluido Actual")]
[DisplayName("Velocidad (m/s)")]
[Description("Velocidad del fluido en la tubería")]
[ReadOnly(true)]
[JsonProperty]
public double FluidVelocity
{
get
{
if (Diameter <= 0) return 0;
var area = Math.PI * Math.Pow(Diameter / 2, 2); // m²
return Math.Abs(CurrentFlow) / area; // m/s
}
}
[Category("🧪 Fluido Actual")]
[DisplayName("Número de Reynolds")]
[Description("Número de Reynolds (Re = ρ × v × D / μ) - Caracteriza el régimen de flujo")]
[ReadOnly(true)]
[JsonProperty]
public double ReynoldsNumber
{
get
{
if (CurrentFluidViscosity <= 0 || Diameter <= 0) return 0;
// Re = (ρ × v × D) / μ
// ρ: densidad (kg/m³) - convertir de kg/L
// v: velocidad (m/s)
// D: diámetro (m)
// μ: viscosidad dinámica (Pa·s) - convertir de cP
var density_kg_m3 = CurrentFluidDensity * 1000; // kg/L → kg/m³
var viscosity_pa_s = CurrentFluidViscosity * 0.001; // cP → Pa·s
return (density_kg_m3 * FluidVelocity * Diameter) / viscosity_pa_s;
}
}
[Category("🧪 Fluido Actual")]
[DisplayName("Régimen de Flujo")]
[Description("Tipo de régimen basado en Reynolds (Laminar < 2300 < Turbulento)")]
[ReadOnly(true)]
[JsonProperty]
public string FlowRegime
{
get
{
var re = ReynoldsNumber;
return re switch
{
< 2300 => $"Laminar (Re = {re:F0})",
> 4000 => $"Turbulento (Re = {re:F0})",
_ => $"Transición (Re = {re:F0})"
};
}
}
// Método para exponer propiedades detalladas en serialización JSON
[JsonProperty("DetailedFluidProperties")]
[Browsable(false)]
public Dictionary DetailedFluidProperties => new Dictionary
{
["CurrentFluidTemperature"] = CurrentFluidTemperature,
["CurrentFluidBrix"] = CurrentFluidBrix,
["CurrentFluidConcentration"] = CurrentFluidConcentration,
["CurrentFluidDensity"] = CurrentFluidDensity,
["CurrentFluidViscosity"] = CurrentFluidViscosity,
["FluidVelocity"] = FluidVelocity,
["ReynoldsNumber"] = ReynoldsNumber,
["FlowRegime"] = FlowRegime
};
[Category("📊 Estado")]
[DisplayName("Flujo (L/min)")]
[Description("Flujo actual en litros por minuto")]
[ReadOnly(true)]
public double CurrentFlowLMin => Math.Abs(CurrentFlow) * 60000.0; // Conversión de m³/s a L/min
[Category("📊 Estado")]
[DisplayName("Dirección")]
[Description("Dirección del flujo")]
[ReadOnly(true)]
public string FlowDirection => CurrentFlow > 0 ? $"{Id_ComponenteA} → {Id_ComponenteB}" :
CurrentFlow < 0 ? $"{Id_ComponenteB} → {Id_ComponenteA}" : "Sin flujo";
[JsonIgnore]
public SolidColorBrush FluidColor
{
get
{
try
{
var colorHex = _currentFluid.Color;
return new SolidColorBrush((Color)ColorConverter.ConvertFromString(colorHex));
}
catch
{
return Brushes.Gray;
}
}
}
private string pipeId = "";
public string PipeId
{
get => pipeId;
set => SetProperty(ref pipeId, value);
}
// Component References
[JsonIgnore]
private IHydraulicComponent ComponenteA = null;
[JsonIgnore]
private IHydraulicComponent ComponenteB = null;
[JsonIgnore]
private PropertyChangedEventHandler componenteAPropertyChangedHandler;
[JsonIgnore]
private PropertyChangedEventHandler componenteBPropertyChangedHandler;
[JsonIgnore]
public Action ActualizarTamaño { get; set; }
// IHydraulicPipe Implementation
// Ya implementadas las propiedades Length, Diameter, Roughness arriba
// IHydraulicComponent Implementation
public bool HasHydraulicComponents => true;
public List GetHydraulicNodes()
{
var nodes = new List();
// Las tuberías conectan componentes existentes, no crean nodos propios
// Los nodos son creados por los componentes conectados (tanques, bombas, etc.)
// La tubería solo actúa como elemento que conecta nodos existentes
return nodes;
}
public List GetHydraulicElements()
{
var elements = new List();
// Solo crear elemento si ambos componentes están conectados
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
// Obtener los nombres de nodos correctos para cada componente
string fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true); // true = es el nodo origen
string toNodeName = GetNodeNameForComponent(Id_ComponenteB, false); // false = es el nodo destino
if (!string.IsNullOrEmpty(fromNodeName) && !string.IsNullOrEmpty(toNodeName))
{
// Crear el elemento Pipe según la documentación
var pipeElement = new Pipe(Length, Diameter, Roughness);
elements.Add(new HydraulicElementDefinition(
name: $"{Nombre}_Pipe",
fromNode: fromNodeName,
toNode: toNodeName,
element: pipeElement,
description: $"Tubería {Nombre} - L:{Length:F2}m, D:{Diameter*1000:F0}mm ({fromNodeName} → {toNodeName})"
));
}
}
return elements;
}
///
/// Obtiene el nombre del nodo correcto para un componente dado
///
/// Nombre del componente
/// True si es el nodo origen de la tubería, False si es destino
/// Nombre del nodo o string vacío si no se encuentra
private string GetNodeNameForComponent(string componentName, bool isSource)
{
if (string.IsNullOrEmpty(componentName) || _mainViewModel == null)
return "";
// Buscar el componente hidráulico
var component = _mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == componentName) as IHydraulicComponent;
if (component == null)
return "";
// Obtener los nodos que crea este componente
var nodes = component.GetHydraulicNodes();
if (nodes == null || !nodes.Any())
return "";
// Determinar qué nodo usar según el tipo de componente
var componentType = component.GetType();
// Para tanques: siempre usar su único nodo
if (componentType.Name.Contains("Tank"))
{
return nodes.FirstOrDefault()?.Name ?? "";
}
// Para bombas: usar nodo de entrada o salida según corresponda
if (componentType.Name.Contains("Pump"))
{
if (isSource)
{
// Si la tubería sale desde este componente, usar el nodo de salida de la bomba
return nodes.FirstOrDefault(n => n.Name.EndsWith("_Out"))?.Name ?? "";
}
else
{
// Si la tubería llega a este componente, usar el nodo de entrada de la bomba
return nodes.FirstOrDefault(n => n.Name.EndsWith("_In"))?.Name ?? "";
}
}
// Para otros tipos de componentes, usar el primer nodo disponible
return nodes.FirstOrDefault()?.Name ?? "";
}
public void UpdateHydraulicProperties()
{
// Actualizar propiedades antes de la simulación si es necesario
// Por ejemplo, cambios dinámicos en diámetro o rugosidad
}
public void ApplyHydraulicResults(Dictionary flows, Dictionary pressures)
{
try
{
// Aplicar resultados a través del TSNet Adapter
if (TSNetAdapter?.Results != null)
{
// Buscar flujo usando el nombre del elemento de TSNet
string pipeElementName = $"{Nombre}_Pipe";
if (flows.ContainsKey(pipeElementName))
{
var flowM3s = flows[pipeElementName];
CurrentFlow = flowM3s;
TSNetAdapter.Results.CalculatedFlowM3s = flowM3s;
Debug.WriteLine($"Pipe {Nombre}: TSNet Flow={flowM3s:F6} m³/s ({CurrentFlowLMin:F2} L/min)");
}
else
{
// Intentar claves alternativas basadas en componentes conectados
var alternativeKeys = new[]
{
$"{Id_ComponenteA}_{Id_ComponenteB}_Pipe",
$"Pipe_{Id_ComponenteA}_{Id_ComponenteB}",
TSNetAdapter.PipeId,
$"Branch_{Nombre}"
};
bool flowFound = false;
foreach (string altKey in alternativeKeys)
{
if (flows.ContainsKey(altKey))
{
var flowM3s = flows[altKey];
CurrentFlow = flowM3s;
TSNetAdapter.Results.CalculatedFlowM3s = flowM3s;
flowFound = true;
break;
}
}
if (!flowFound)
{
CurrentFlow = 0.0;
TSNetAdapter.Results.CalculatedFlowM3s = 0.0;
}
}
// Calcular pérdida de presión entre nodos conectados
if (!string.IsNullOrEmpty(Id_ComponenteA) && !string.IsNullOrEmpty(Id_ComponenteB))
{
var fromNodeName = GetNodeNameForComponent(Id_ComponenteA, true);
var toNodeName = GetNodeNameForComponent(Id_ComponenteB, false);
if (pressures.ContainsKey(fromNodeName) && pressures.ContainsKey(toNodeName))
{
var pressureA = pressures[fromNodeName];
var pressureB = pressures[toNodeName];
PressureDrop = pressureA - pressureB;
TSNetAdapter.Results.PressureDropBar = PressureDrop;
Debug.WriteLine($"Pipe {Nombre}: Pressure drop={PressureDrop:F0} Pa ({fromNodeName} → {toNodeName})");
}
}
// Actualizar resultados del adapter
TSNetAdapter.Results.Timestamp = DateTime.Now;
TSNetAdapter.Results.FlowStatus = $"Flow: {CurrentFlowLMin:F1}L/min, Drop: {PressureDrop:F1}Pa";
// Actualizar fluido desde componente fuente
UpdateFluidFromSource();
}
// Notificar cambios para UI
OnPropertyChanged(nameof(CurrentFlow));
OnPropertyChanged(nameof(CurrentFlowLMin));
OnPropertyChanged(nameof(PressureDrop));
OnPropertyChanged(nameof(FlowDirection));
OnPropertyChanged(nameof(FluidVelocity));
OnPropertyChanged(nameof(ReynoldsNumber));
OnPropertyChanged(nameof(FlowRegime));
// Debug periódico
if (Environment.TickCount % 2000 < 50) // Cada ~2 segundos
{
Debug.WriteLine($"Pipe {Nombre}: Flow={CurrentFlowLMin:F1}L/min, Drop={PressureDrop:F1}Pa, {FlowDirection}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error en Pipe {Nombre} ApplyHydraulicResults: {ex.Message}");
}
}
// IHydraulicFlowReceiver Implementation
public void SetFlow(double flow)
{
CurrentFlow = flow;
}
public double GetFlow()
{
return CurrentFlow;
}
// IHydraulicPressureReceiver Implementation
public void SetPressure(double pressure)
{
// Para tuberías, la presión se maneja a través de los nodos
}
public double GetPressure()
{
return PressureDrop;
}
// Connection Management
partial void OnId_ComponenteAChanged(string value)
{
if (ComponenteA != null && componenteAPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteA).PropertyChanged -= componenteAPropertyChangedHandler;
if (_mainViewModel != null && !string.IsNullOrEmpty(value))
{
ComponenteA = (IHydraulicComponent)_mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == value);
if (ComponenteA != null)
{
componenteAPropertyChangedHandler = (sender, e) =>
{
if (e.PropertyName == nameof(osBase.Nombre))
{
Id_ComponenteA = ((osBase)sender).Nombre;
}
};
((INotifyPropertyChanged)ComponenteA).PropertyChanged += componenteAPropertyChangedHandler;
}
}
}
partial void OnId_ComponenteBChanged(string value)
{
if (ComponenteB != null && componenteBPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteB).PropertyChanged -= componenteBPropertyChangedHandler;
if (_mainViewModel != null && !string.IsNullOrEmpty(value))
{
ComponenteB = (IHydraulicComponent)_mainViewModel.ObjetosSimulables
.FirstOrDefault(s => s is IHydraulicComponent comp &&
((osBase)comp).Nombre == value);
if (ComponenteB != null)
{
componenteBPropertyChangedHandler = (sender, e) =>
{
if (e.PropertyName == nameof(osBase.Nombre))
{
Id_ComponenteB = ((osBase)sender).Nombre;
}
};
((INotifyPropertyChanged)ComponenteB).PropertyChanged += componenteBPropertyChangedHandler;
}
}
}
// osBase Implementation
public static string NombreCategoria() => "Componentes Hidráulicos";
public static string NombreClase() => "Tubería Hidráulica";
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 AltoChanged(float value)
{
ActualizarGeometrias();
}
public override void AnchoChanged(float value)
{
ActualizarGeometrias();
}
public void Start()
{
// Las tuberías no participan en la simulación física Bepu
// Solo en el sistema hidráulico
}
public override void UpdateGeometryStart()
{
// En el nuevo sistema unificado, el HydraulicSimulationManager se encarga
// de registrar automáticamente los objetos que implementan IHydraulicComponent
// No necesitamos crear objetos simHydraulic* separados
Debug.WriteLine($"[DEBUG] {Nombre}: UpdateGeometryStart() - ComponenteA: '{Id_ComponenteA}', ComponenteB: '{Id_ComponenteB}'");
ActualizarGeometrias();
}
public override void UpdateGeometryStep()
{
// Los objetos hidráulicos actualizan sus resultados
// a través de ApplyHydraulicResults() desde HydraulicSimulationManager
}
// Método para actualizar fluido desde componente fuente
public void UpdateFluidFromSource()
{
if (_mainViewModel?.ObjetosSimulables == null) return;
// Determinar componente fuente basado en dirección del flujo
string sourceComponentName = CurrentFlow >= 0 ? Id_ComponenteA : Id_ComponenteB;
var sourceComponent = _mainViewModel.ObjetosSimulables
.OfType()
.FirstOrDefault(c => c is osBase osb && osb.Nombre == sourceComponentName);
if (sourceComponent is osHydTank tank)
{
var sourceFluid = tank.CurrentOutputFluid;
if (sourceFluid != null)
{
_currentFluid = sourceFluid.Clone();
OnPropertyChanged(nameof(CurrentFluidType));
OnPropertyChanged(nameof(CurrentFluidDescription));
OnPropertyChanged(nameof(FluidColor));
}
}
// Actualizar color visual
if (Math.Abs(CurrentFlow) > 1e-6) // Hay flujo
{
ColorButton_oculto = FluidColor;
}
else
{
ColorButton_oculto = Brushes.Gray; // Sin flujo
}
}
public override void UpdateControl(int elapsedMilliseconds)
{
// En el nuevo sistema TSNet, los valores provienen exclusivamente de TSNet
// No realizamos cálculos internos - solo actualizamos propiedades de UI
try
{
// Los resultados de TSNet ya están aplicados en ApplyHydraulicResults()
// Solo actualizamos propiedades derivadas y visuales
// Actualizar propiedades del fluido cada ciclo
UpdateFluidFromSource();
// Actualizar propiedades de UI calculadas
OnPropertyChanged(nameof(CurrentFlowLMin));
OnPropertyChanged(nameof(FlowDirection));
OnPropertyChanged(nameof(FluidVelocity));
OnPropertyChanged(nameof(ReynoldsNumber));
OnPropertyChanged(nameof(FlowRegime));
// Actualizar color visual basado en fluido
UpdatePipeColorFromFluid();
// Debug periódico cada 5 segundos
if (Environment.TickCount % 5000 < elapsedMilliseconds)
{
Debug.WriteLine($"Pipe {Nombre}: Flow={CurrentFlowLMin:F1}L/min, Drop={PressureDrop:F1}Pa, {FlowDirection}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in Pipe {Nombre} UpdateControl: {ex.Message}");
}
}
public override void ucLoaded()
{
// En el nuevo sistema unificado, no necesitamos crear objetos separados
base.ucLoaded();
}
public override void ucUnLoaded()
{
// En el nuevo sistema unificado, el HydraulicSimulationManager
// maneja automáticamente el registro/desregistro de objetos
base.ucUnLoaded();
}
public override void UpdatePLC(PLCViewModel plc, int elapsedMilliseconds)
{
// Las tuberías no tienen control PLC directo
}
private void ActualizarGeometrias()
{
try
{
// Actualizar geometría visual
if (this.ActualizarTamaño != null)
this.ActualizarTamaño(Ancho, Alto);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error actualizando geometría: {ex.Message}");
}
}
public void Inicializar(int valorInicial)
{
PipeId = $"Pipe_{Nombre}_{valorInicial}";
OnId_ComponenteAChanged(Id_ComponenteA);
OnId_ComponenteBChanged(Id_ComponenteB);
}
public void Disposing()
{
if (ComponenteA != null && componenteAPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteA).PropertyChanged -= componenteAPropertyChangedHandler;
if (ComponenteB != null && componenteBPropertyChangedHandler != null)
((INotifyPropertyChanged)ComponenteB).PropertyChanged -= componenteBPropertyChangedHandler;
}
///
/// Actualiza el color de la tubería basado en el fluido que transporta
///
private void UpdatePipeColorFromFluid()
{
try
{
if (Math.Abs(CurrentFlow) < 1e-6)
{
ColorButton_oculto = Brushes.Gray; // Sin flujo
return;
}
// Color basado en el fluido actual
var fluidColor = FluidColor;
if (fluidColor != null)
{
ColorButton_oculto = fluidColor;
}
else
{
// Color por defecto según dirección de flujo
ColorButton_oculto = CurrentFlow > 0 ? Brushes.Blue : Brushes.Red;
}
}
catch
{
ColorButton_oculto = Brushes.Gray; // Color por defecto en caso de error
}
}
// Constructor
public osHydPipe()
{
PipeId = Guid.NewGuid().ToString();
IsVisFilter = true; // Asegurar que el componente hidráulico sea visible en filtros
// No inicializar TSNet Adapter aquí - se creará cuando se registre con TSNetSimulationManager
// TSNetAdapter = new TSNetPipeAdapter(this);
}
}
}