CtrEditor/ObjetosSim/HydraulicComponents/osHydTank.cs

1392 lines
48 KiB
C#

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
{
/// <summary>
/// Tanque hidráulico con gestión dinámica de nivel y presión configurable
/// </summary>
public partial class osHydTank : osBase, IHydraulicComponent, IHydraulicFlowReceiver, IHydraulicPressureReceiver, IosBase
{
public static string NombreCategoria() => "Componentes Hidráulicos";
public static string NombreClase() => "Tanque Hidráulico";
// Private Fields
private double _tankPressure = 1.013; // bar (1 atm por defecto)
private double _currentLevelM = 1.0; // m
private double _crossSectionalArea = 1.0; // m²
private double _currentVolumeL = 1000.0; // L
private double _maxVolumeL = 2000.0; // L
private double _netFlow = 0.0; // L/min (positivo = entrada, negativo = salida)
private double _currentPressure = 1.013; // bar
private bool _isFixedPressure = true;
private double _lastUpdateTime = 0.0;
// Propiedades de fluido
private FluidProperties _primaryFluid = new FluidProperties(FluidType.Water, 20.0);
private FluidProperties _secondaryFluid = new FluidProperties(FluidType.Syrup, 20.0, 65.0);
private double _primaryVolumeL = 800.0; // L
private double _secondaryVolumeL = 200.0; // L
private double _mixingVolumeL = 100.0; // L de transición para mezcla
private MixingState _mixingState = MixingState.PrimaryOnly;
private double _currentMixRatio = 0.0; // 0 = solo primario, 1 = solo secundario
// Tank configuration fields
private double _maxLevelM = 2.0; // m - máximo nivel del tanque
private double _minLevelM = 0.0; // m - mínimo nivel del tanque
private string _tankType = "Standard"; // Tipo de tanque
// TSNet Integration
[JsonIgnore]
public TSNetTankAdapter? TSNetAdapter { get; private set; }
[JsonIgnore]
private object _levelLock = new object();
// Properties
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Tamaño visual del tanque")]
[property: Name("Tamaño")]
public float tamano;
// Propiedades visuales específicas del tanque
[ObservableProperty]
[property: Category("🎨 Apariencia")]
[property: Description("Color visual del tanque")]
[property: Name("Color")]
private Brush colorButton_oculto = Brushes.LightBlue;
// Constructor
public osHydTank()
{
// Inicializar el lock primero para thread safety
EnsureLockInitialized();
try
{
Nombre = "Tanque Hidráulico";
Tamano = 1.0f; // Usar un tamaño mayor para mayor visibilidad
// Inicializar dimensiones usando las propiedades de osBase
Ancho = 0.30f;
Alto = 0.40f;
Angulo = 0f;
// Asegurar que el movimiento esté habilitado
Lock_movement = false;
// No inicializar TSNet Adapter aquí - se creará cuando se registre con TSNetSimulationManager
// TSNetAdapter = new TSNetTankAdapter(this);
// No cargar imagen aquí - se carga en ucLoaded()
// para evitar problemas de serialización
// Cálculos seguros
SafeUpdateVolumeCalculations();
IsVisFilter = true; // Asegurar que el componente hidráulico sea visible en filtros
SafeUpdateTankPressure();
// Inicializar estado de mezcla
UpdateMixingState();
// Debug: Confirmar que el constructor se ejecuta
Debug.WriteLine($"osHydTank Constructor: Nombre='{Nombre}', Tamaño={Tamano}, ZIndex={zIndex_fromFrames}, IsVisFilter={IsVisFilter}, Lock_movement={Lock_movement}");
}
catch (Exception ex)
{
Debug.WriteLine($"Error in osHydTank constructor: {ex.Message}");
// Inicializar valores por defecto mínimos
InitializeDefaults();
}
}
// Constructor y Métodos Base
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)
{
// Método básico de movimiento como otros objetos
// Debug para verificar que se llama
Debug.WriteLine($"osHydTank.OnMove: LeftPixels={LeftPixels}, TopPixels={TopPixels}, Left={Left}, Top={Top}");
}
public override void OnResize(float Delta_Width, float Delta_Height)
{
Tamano += Delta_Width + Delta_Height;
}
public override void UpdateGeometryStart()
{
// En el nuevo sistema unificado, no necesitamos crear objetos hidráulicos
// El componente se registra automáticamente como IHydraulicComponent
}
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
// Los resultados de TSNet ya están aplicados en ApplyHydraulicResults()
// Solo actualizamos propiedades derivadas y visuales
try
{
// Actualizar propiedades de UI y mezcla
UpdateMixingState();
// Actualizar propiedades calculadas para UI
OnPropertyChanged(nameof(FillPercentage));
OnPropertyChanged(nameof(FlowBalance));
OnPropertyChanged(nameof(CurrentPressurePa));
// Actualizar color visual basado en estado
UpdateTankColorFromFluid();
// Debug periódico cada 5 segundos
if (Environment.TickCount % 5000 < elapsedMilliseconds)
{
Debug.WriteLine($"Tank {Nombre}: Level={CurrentLevelM:F2}m, Volume={CurrentVolumeL:F0}L, Flow={NetFlow:F1}L/min, Pressure={CurrentPressure:F2}bar");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in Tank {Nombre} UpdateControl: {ex.Message}");
}
}
// Properties - Configuration
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Área sección transversal")]
[Description("Área de la sección transversal del tanque (m²)")]
public double CrossSectionalArea
{
get => _crossSectionalArea;
set
{
if (SetProperty(ref _crossSectionalArea, Math.Max(0.1, value)))
{
SafeUpdateVolumeCalculations();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Volumen máximo")]
[Description("Volumen máximo del tanque (L)")]
public double MaxVolumeL
{
get => _maxVolumeL;
set
{
if (SetProperty(ref _maxVolumeL, Math.Max(10, value)))
{
if (_currentVolumeL > _maxVolumeL)
CurrentVolumeL = _maxVolumeL;
SafeUpdateVolumeCalculations();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Nivel máximo")]
[Description("Nivel máximo permitido en el tanque (m)")]
public double MaxLevelM
{
get => _maxLevelM;
set
{
if (SetProperty(ref _maxLevelM, Math.Max(0.1, value)))
{
// Asegurar que el nivel mínimo no sea mayor que el máximo
if (_minLevelM >= _maxLevelM)
MinLevelM = _maxLevelM * 0.1; // 10% del máximo como mínimo
SafeUpdateVolumeCalculations();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Nivel mínimo")]
[Description("Nivel mínimo permitido en el tanque (m)")]
public double MinLevelM
{
get => _minLevelM;
set
{
if (SetProperty(ref _minLevelM, Math.Max(0.0, value)))
{
// Asegurar que el nivel mínimo no sea mayor que el máximo
if (_minLevelM >= _maxLevelM)
MaxLevelM = _minLevelM + 0.1; // Al menos 10cm más que el mínimo
SafeUpdateVolumeCalculations();
InvalidateHydraulicNetwork();
}
}
}
[Category("🏗️ Configuración del Tanque")]
[DisplayName("Tipo de tanque")]
[Description("Tipo de tanque (Standard, Storage, Process, etc.)")]
public string TankType
{
get => _tankType;
set => SetProperty(ref _tankType, value ?? "Standard");
}
// Properties - Pressure
[Category("🔧 Sistema de Presión")]
[DisplayName("Presión fija")]
[Description("Si está habilitado, la presión se mantiene constante")]
public bool IsFixedPressure
{
get => _isFixedPressure;
set
{
if (SetProperty(ref _isFixedPressure, value))
{
SafeUpdateTankPressure();
InvalidateHydraulicNetwork();
}
}
}
[Category("🔧 Sistema de Presión")]
[DisplayName("Presión del tanque")]
[Description("Presión mantenida en el tanque (bar). En sistemas reales controlada por PID")]
public double TankPressure
{
get => _tankPressure;
set
{
// Asegurar que el valor esté en bar y sea razonable
var valueInBar = value > 1000 ? value / 100000.0 : value; // Si viene en Pa, convertir a bar
valueInBar = Math.Max(0.1, Math.Min(10.0, valueInBar)); // Limitar entre 0.1 y 10 bar
if (SetProperty(ref _tankPressure, valueInBar))
{
SafeUpdateTankPressure();
InvalidateHydraulicNetwork();
}
}
}
// Properties - Current State
[Category("📊 Estado Actual")]
[DisplayName("Nivel actual")]
[Description("Nivel actual de líquido en el tanque (m)")]
public double CurrentLevelM
{
get => _currentLevelM;
set
{
EnsureLockInitialized();
lock (_levelLock)
{
var clampedLevel = Math.Max(0.0, value);
if (SetProperty(ref _currentLevelM, clampedLevel))
{
SafeUpdateVolumeCalculations();
OnPropertyChanged(nameof(FillPercentage));
UpdateMixingState();
}
}
}
}
[Category("📊 Estado Actual")]
[DisplayName("Volumen actual")]
[Description("Volumen actual de líquido en el tanque (L)")]
public double CurrentVolumeL
{
get => _currentVolumeL;
set
{
EnsureLockInitialized();
lock (_levelLock)
{
var clampedVolume = Math.Max(0, Math.Min(_maxVolumeL, value));
if (SetProperty(ref _currentVolumeL, clampedVolume))
{
// Actualizar nivel basado en volumen
_currentLevelM = (clampedVolume / 1000.0) / _crossSectionalArea;
_currentLevelM = Math.Max(0.0, _currentLevelM);
OnPropertyChanged(nameof(CurrentLevelM));
OnPropertyChanged(nameof(FillPercentage));
UpdateMixingState();
}
}
}
}
[Category("📊 Estado Actual")]
[DisplayName("Porcentaje llenado")]
[Description("Porcentaje de llenado del tanque")]
[JsonIgnore]
public double FillPercentage => MaxLevelM > 0 ? (_currentLevelM / MaxLevelM) * 100.0 : 0.0;
[Category("📊 Estado Actual")]
[DisplayName("Flujo neto")]
[Description("Flujo neto del tanque (L/min) - positivo=entrada, negativo=salida")]
[JsonIgnore]
public double NetFlow
{
get => _netFlow;
private set => SetProperty(ref _netFlow, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Balance flujo")]
[Description("Balance de flujo neto (L/min)")]
[JsonIgnore]
public double FlowBalance => NetFlow;
[Category("📊 Estado Actual")]
[DisplayName("Presión actual")]
[Description("Presión actual en el tanque (bar)")]
[JsonIgnore]
public double CurrentPressure
{
get => _currentPressure;
private set => SetProperty(ref _currentPressure, value);
}
[Category("📊 Estado Actual")]
[DisplayName("Presión (Pa)")]
[Description("Presión actual en Pascal")]
[JsonIgnore]
public double CurrentPressurePa => CurrentPressure * 100000.0;
// Properties - Fluids and Mixing
[Category("🧪 Fluido Primario")]
[DisplayName("Tipo de fluido")]
[Description("Tipo del fluido primario en el tanque")]
public FluidType PrimaryFluidType
{
get => _primaryFluid.Type;
set
{
if (_primaryFluid.Type != value)
{
_primaryFluid.Type = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
InvalidateHydraulicNetwork();
}
}
}
[Category("🧪 Fluido Primario")]
[DisplayName("Temperatura")]
[Description("Temperatura del fluido primario (°C)")]
public double PrimaryTemperature
{
get => _primaryFluid.Temperature;
set
{
if (Math.Abs(_primaryFluid.Temperature - value) > 0.01)
{
_primaryFluid.Temperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Primario")]
[DisplayName("Brix")]
[Description("Grados Brix para jarabe (% sacarosa)")]
public double PrimaryBrix
{
get => _primaryFluid.Brix;
set
{
var clampedValue = Math.Max(0, Math.Min(80, value));
if (Math.Abs(_primaryFluid.Brix - clampedValue) > 0.01)
{
_primaryFluid.Brix = clampedValue;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Primario")]
[DisplayName("Concentración")]
[Description("Concentración para soda cáustica (%)")]
public double PrimaryConcentration
{
get => _primaryFluid.Concentration;
set
{
var clampedValue = Math.Max(0, Math.Min(50, value));
if (Math.Abs(_primaryFluid.Concentration - clampedValue) > 0.01)
{
_primaryFluid.Concentration = clampedValue;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Primario")]
[DisplayName("Volumen")]
[Description("Volumen del fluido primario (L)")]
public double PrimaryVolumeL
{
get => _primaryVolumeL;
set
{
if (SetProperty(ref _primaryVolumeL, Math.Max(0, value)))
{
UpdateMixingState();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Secundario")]
[DisplayName("Tipo de fluido")]
[Description("Tipo del fluido secundario en el tanque")]
public FluidType SecondaryFluidType
{
get => _secondaryFluid.Type;
set
{
if (_secondaryFluid.Type != value)
{
_secondaryFluid.Type = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
InvalidateHydraulicNetwork();
}
}
}
[Category("🧪 Fluido Secundario")]
[DisplayName("Temperatura")]
[Description("Temperatura del fluido secundario (°C)")]
public double SecondaryTemperature
{
get => _secondaryFluid.Temperature;
set
{
if (Math.Abs(_secondaryFluid.Temperature - value) > 0.01)
{
_secondaryFluid.Temperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Secundario")]
[DisplayName("Brix")]
[Description("Grados Brix para jarabe (% sacarosa)")]
public double SecondaryBrix
{
get => _secondaryFluid.Brix;
set
{
var clampedValue = Math.Max(0, Math.Min(80, value));
if (Math.Abs(_secondaryFluid.Brix - clampedValue) > 0.01)
{
_secondaryFluid.Brix = clampedValue;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Secundario")]
[DisplayName("Concentración")]
[Description("Concentración para soda cáustica (%)")]
public double SecondaryConcentration
{
get => _secondaryFluid.Concentration;
set
{
var clampedValue = Math.Max(0, Math.Min(50, value));
if (Math.Abs(_secondaryFluid.Concentration - clampedValue) > 0.01)
{
_secondaryFluid.Concentration = clampedValue;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🧪 Fluido Secundario")]
[DisplayName("Volumen")]
[Description("Volumen del fluido secundario (L)")]
public double SecondaryVolumeL
{
get => _secondaryVolumeL;
set
{
if (SetProperty(ref _secondaryVolumeL, Math.Max(0, value)))
{
UpdateMixingState();
OnPropertyChanged(nameof(CurrentFluidDescription));
}
}
}
[Category("🔀 Control de Mezcla")]
[DisplayName("Volumen de mezcla")]
[Description("Volumen de transición durante la mezcla (L)")]
public double MixingVolumeL
{
get => _mixingVolumeL;
set => SetProperty(ref _mixingVolumeL, Math.Max(0, value));
}
[Category("🔀 Control de Mezcla")]
[DisplayName("Estado de mezcla")]
[Description("Estado actual del proceso de mezcla")]
[JsonIgnore]
public MixingState MixingState
{
get => _mixingState;
private set => SetProperty(ref _mixingState, value);
}
[Category("🔀 Control de Mezcla")]
[DisplayName("Ratio de mezcla")]
[Description("Ratio actual de mezcla (0=primario, 1=secundario)")]
[JsonIgnore]
public double CurrentMixRatio
{
get => _currentMixRatio;
private set => SetProperty(ref _currentMixRatio, value);
}
// IHydraulicComponent Implementation
[JsonIgnore]
public bool HasHydraulicComponents => true;
public List<HydraulicNodeDefinition> GetHydraulicNodes()
{
var nodes = new List<HydraulicNodeDefinition>();
// El tanque siempre es un nodo de presión fija si está configurado así
if (IsFixedPressure)
{
// Convertir TankPressure de bar a Pa para el solver
var pressurePa = TankPressure * 100000.0; // bar a Pa
nodes.Add(new HydraulicNodeDefinition(Nombre, true, pressurePa, GetTankDescription()));
//Debug.WriteLine($"Tanque {Nombre}: Nodo de presión fija creado - {pressurePa:F0} Pa ({TankPressure:F2} bar)");
}
else
{
nodes.Add(new HydraulicNodeDefinition(Nombre, false, null, GetTankDescription()));
//Debug.WriteLine($"Tanque {Nombre}: Nodo de presión libre creado");
}
return nodes;
}
public void ApplyHydraulicResults(Dictionary<string, double> flows, Dictionary<string, double> pressures)
{
try
{
// Aplicar resultados a través del TSNet Adapter
if (TSNetAdapter?.Results != null)
{
// Actualizar presión desde TSNet
if (pressures.ContainsKey(TSNetAdapter.TankId))
{
var pressurePa = pressures[TSNetAdapter.TankId];
CurrentPressure = pressurePa / 100000.0; // Convertir Pa a bar
TSNetAdapter.Results.CalculatedPressureBar = CurrentPressure;
Debug.WriteLine($"Tank {Nombre}: Presión TSNet={pressurePa:F0} Pa = {CurrentPressure:F3} bar");
}
// Calcular flujo neto desde pipes conectadas
var netFlowM3s = CalculateNetFlowFromConnectedPipes(flows);
NetFlow = netFlowM3s * 60000.0; // Convertir m³/s a L/min
TSNetAdapter.Results.NetFlowM3s = NetFlow;
// Actualizar nivel basado en balance de flujo solo si hay cambio
if (Math.Abs(NetFlow) > 0.001) // Umbral mínimo de 0.001 L/min
{
var deltaTime = GetDeltaTime();
UpdateLevelFromFlowBalance(deltaTime);
}
// Actualizar resultados del adapter
TSNetAdapter.Results.CalculatedLevelM = CurrentLevelM;
TSNetAdapter.Results.CalculatedVolumeL = CurrentVolumeL;
TSNetAdapter.Results.Timestamp = DateTime.Now;
TSNetAdapter.Results.Status = $"Level: {CurrentLevelM:F2}m, Flow: {NetFlow:F1}L/min";
}
// Notificar cambios para UI
OnPropertyChanged(nameof(CurrentLevelM));
OnPropertyChanged(nameof(CurrentVolumeL));
OnPropertyChanged(nameof(FillPercentage));
OnPropertyChanged(nameof(NetFlow));
OnPropertyChanged(nameof(CurrentPressure));
// Debug periódico
if (Environment.TickCount % 2000 < 50) // Cada ~2 segundos
{
Debug.WriteLine($"Tank {Nombre}: Level={CurrentLevelM:F2}m, NetFlow={NetFlow:F1}L/min, Pressure={CurrentPressure:F3}bar");
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error en Tank {Nombre} ApplyHydraulicResults: {ex.Message}");
}
}
public List<HydraulicElementDefinition> GetHydraulicElements()
{
// Los tanques no generan elementos hidráulicos, solo nodos
return new List<HydraulicElementDefinition>();
}
public void UpdateHydraulicProperties()
{
// Actualizar propiedades antes de la simulación
SafeUpdateTankPressure();
SafeUpdateVolumeCalculations();
}
// UI Properties
/// <summary>
/// Propiedades del fluido actual que está saliendo del tanque
/// </summary>
[JsonIgnore]
public FluidProperties CurrentOutputFluid
{
get
{
return MixingState switch
{
MixingState.Empty => new FluidProperties(FluidType.Air),
MixingState.PrimaryOnly => _primaryFluid.Clone(),
MixingState.SecondaryOnly => _secondaryFluid.Clone(),
MixingState.EmptyingSecondary => _secondaryFluid.Clone(),
MixingState.Mixing => _primaryFluid.MixWith(_secondaryFluid, CurrentMixRatio),
MixingState.EmptyingPrimary => _primaryFluid.Clone(),
_ => new FluidProperties(FluidType.Air)
};
}
}
/// <summary>
/// Descripción del fluido actual
/// </summary>
[JsonIgnore]
public string CurrentFluidDescription => CurrentOutputFluid.Description;
/// <summary>
/// Color del nivel de líquido basado en el fluido actual
/// </summary>
[JsonIgnore]
public SolidColorBrush LevelColor
{
get
{
var fluid = CurrentOutputFluid;
var colorHex = fluid.Color;
// Convertir hex a color y ajustar según nivel
var percentage = FillPercentage;
var color = (Color)System.Windows.Media.ColorConverter.ConvertFromString(colorHex);
// Oscurecer si nivel es bajo
if (percentage < 20)
{
color = Color.FromRgb((byte)(color.R * 0.7), (byte)(color.G * 0.7), (byte)(color.B * 0.7));
}
return new SolidColorBrush(color);
}
}
/// <summary>
/// Color del borde del nivel de líquido
/// </summary>
[JsonIgnore]
public SolidColorBrush LevelBorderColor
{
get
{
var percentage = FillPercentage;
if (percentage < 20) return new SolidColorBrush(Colors.DarkRed);
if (percentage < 50) return new SolidColorBrush(Colors.DarkOrange);
return new SolidColorBrush(Colors.DarkBlue);
}
}
/// <summary>
/// Color del balance de flujo
/// </summary>
[JsonIgnore]
public SolidColorBrush FlowBalanceColor
{
get
{
var balance = FlowBalance;
if (Math.Abs(balance) < 0.1) return new SolidColorBrush(Colors.Gray);
return balance > 0 ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red);
}
}
/// <summary>
/// Indica si el tanque está vacío (contiene aire)
/// </summary>
[JsonIgnore]
public bool IsEmpty => CurrentVolumeL < 1.0; // Menos de 1L considerado vacío
/// <summary>
/// Densidad del fluido actual
/// </summary>
[JsonIgnore]
public double CurrentFluidDensity => CurrentOutputFluid.Density;
/// <summary>
/// Viscosidad del fluido actual
/// </summary>
[JsonIgnore]
public double CurrentFluidViscosity => CurrentOutputFluid.Viscosity;
/// <summary>
/// Porcentaje de altura para la sección secundaria (abajo) - para display visual
/// </summary>
[JsonIgnore]
public double SecondaryDisplayPercentage
{
get
{
var totalVolume = _primaryVolumeL + _secondaryVolumeL + _mixingVolumeL;
if (totalVolume == 0) return 0;
return (_secondaryVolumeL / totalVolume) * FillPercentage / 100.0;
}
}
/// <summary>
/// Porcentaje de altura para la sección de mezcla (medio) - para display visual
/// </summary>
[JsonIgnore]
public double MixDisplayPercentage
{
get
{
var totalVolume = _primaryVolumeL + _secondaryVolumeL + _mixingVolumeL;
if (totalVolume == 0) return 0;
return (_mixingVolumeL / totalVolume) * FillPercentage / 100.0;
}
}
/// <summary>
/// Porcentaje de altura para la sección primaria (arriba) - para display visual
/// </summary>
[JsonIgnore]
public double PrimaryDisplayPercentage
{
get
{
var totalVolume = _primaryVolumeL + _secondaryVolumeL + _mixingVolumeL;
if (totalVolume == 0) return 0;
return (_primaryVolumeL / totalVolume) * FillPercentage / 100.0;
}
}
/// <summary>
/// Color para la sección secundaria
/// </summary>
[JsonIgnore]
public SolidColorBrush SecondaryLevelColor
{
get
{
var colorHex = _secondaryFluid.Color;
var color = (Color)System.Windows.Media.ColorConverter.ConvertFromString(colorHex);
return new SolidColorBrush(color);
}
}
/// <summary>
/// Color para la sección de mezcla
/// </summary>
[JsonIgnore]
public SolidColorBrush MixLevelColor
{
get
{
var mixedFluid = _primaryFluid.MixWith(_secondaryFluid, 0.5); // Mezcla 50-50
var colorHex = mixedFluid.Color;
var color = (Color)System.Windows.Media.ColorConverter.ConvertFromString(colorHex);
return new SolidColorBrush(color);
}
}
/// <summary>
/// Color para la sección primaria
/// </summary>
[JsonIgnore]
public SolidColorBrush PrimaryLevelColor
{
get
{
var colorHex = _primaryFluid.Color;
var color = (Color)System.Windows.Media.ColorConverter.ConvertFromString(colorHex);
return new SolidColorBrush(color);
}
}
// Private Methods
private void InitializeDefaults()
{
// Valores mínimos para que el objeto sea funcional
try
{
if (string.IsNullOrEmpty(nombre))
nombre = "Tanque Hidráulico";
if (Tamano <= 0)
Tamano = 1.0f;
if (Ancho <= 0)
Ancho = 0.30f;
if (Alto <= 0)
Alto = 0.40f;
// Inicializar configuraciones del tanque
if (_maxLevelM <= 0)
_maxLevelM = 2.0;
if (_minLevelM < 0 || _minLevelM >= _maxLevelM)
_minLevelM = 0.0;
if (string.IsNullOrEmpty(_tankType))
_tankType = "Standard";
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
Debug.WriteLine("osHydTank: Initialized with default values");
}
catch (Exception ex)
{
Debug.WriteLine($"Error in InitializeDefaults: {ex.Message}");
}
}
private void SafeUpdateVolumeCalculations()
{
try
{
// Calcular volumen total actual basado en nivel
_currentVolumeL = _currentLevelM * _crossSectionalArea * 1000.0; // convertir m³ a L
_maxVolumeL = 2.0 * _crossSectionalArea * 1000.0; // Assuming 2m as max height
// Asegurar que los volúmenes de fluidos no excedan el volumen total
var totalFluidVolume = _primaryVolumeL + _secondaryVolumeL;
if (totalFluidVolume > _currentVolumeL)
{
var ratio = _currentVolumeL / totalFluidVolume;
_primaryVolumeL *= ratio;
_secondaryVolumeL *= ratio;
}
}
catch (Exception ex)
{
Debug.WriteLine($"Error in SafeUpdateVolumeCalculations: {ex.Message}");
// Usar valores por defecto seguros
_currentVolumeL = 1000.0;
_maxVolumeL = 2000.0;
}
}
private void SafeUpdateTankPressure()
{
try
{
if (IsFixedPressure)
{
// TankPressure está en bar, CurrentPressure también debe estar en bar
CurrentPressure = TankPressure;
}
else
{
// Calcular presión basada solo en volumen primario (para cálculos hidráulicos)
var primaryLevel = PrimaryVolumeL / (CrossSectionalArea * 1000.0); // Nivel equivalente del volumen primario
var hydrostaticPressure = primaryLevel * 9.81 * CurrentFluidDensity / 100000.0; // Convertir a bar
CurrentPressure = 1.013 + hydrostaticPressure; // Presión atmosférica + hidráulica (todo en bar)
}
// Debug para verificar las unidades
Debug.WriteLine($"Tanque {Nombre}: TankPressure={TankPressure:F3} bar, CurrentPressure={CurrentPressure:F3} bar");
}
catch (Exception ex)
{
Debug.WriteLine($"Error in SafeUpdateTankPressure: {ex.Message}");
// Usar presión atmosférica por defecto (en bar)
_currentPressure = 1.013;
}
}
private void UpdateMixingState()
{
var totalVolume = _primaryVolumeL + _secondaryVolumeL;
if (totalVolume < 1.0) // Menos de 1L considerado vacío
{
MixingState = MixingState.Empty;
CurrentMixRatio = 0.0;
return;
}
if (_secondaryVolumeL <= 0)
{
MixingState = MixingState.PrimaryOnly;
CurrentMixRatio = 0.0;
}
else if (_primaryVolumeL <= 0)
{
MixingState = MixingState.SecondaryOnly;
CurrentMixRatio = 1.0;
}
else
{
// Determinar estado de mezcla basado en volúmenes
var secondaryRatio = _secondaryVolumeL / totalVolume;
if (_secondaryVolumeL <= _mixingVolumeL && _primaryVolumeL > _mixingVolumeL)
{
// Vaciando secundario
MixingState = MixingState.EmptyingSecondary;
CurrentMixRatio = secondaryRatio;
}
else if (_primaryVolumeL <= _mixingVolumeL && _secondaryVolumeL > _mixingVolumeL)
{
// Vaciando primario
MixingState = MixingState.EmptyingPrimary;
CurrentMixRatio = secondaryRatio;
}
else if (_secondaryVolumeL <= _mixingVolumeL && _primaryVolumeL <= _mixingVolumeL)
{
// Mezclando ambos
MixingState = MixingState.Mixing;
CurrentMixRatio = secondaryRatio;
}
else
{
// Determinar cual predomina
if (secondaryRatio > 0.7)
{
MixingState = MixingState.SecondaryOnly;
CurrentMixRatio = 1.0;
}
else if (secondaryRatio < 0.3)
{
MixingState = MixingState.PrimaryOnly;
CurrentMixRatio = 0.0;
}
else
{
MixingState = MixingState.Mixing;
CurrentMixRatio = secondaryRatio;
}
}
}
}
private void UpdateLevelFromFlowBalance(double deltaTime)
{
if (deltaTime <= 0 || Math.Abs(deltaTime) > 1.0) // Evitar deltas muy grandes
return;
// Balance de masa: cambio_volumen = (flujo_entrada - flujo_salida) * tiempo
var volumeChangeL = FlowBalance * deltaTime; // L/min * min = L
// Actualizar volumen total
var newVolumeL = CurrentVolumeL + volumeChangeL;
CurrentVolumeL = Math.Max(0, Math.Min(_maxVolumeL, newVolumeL));
// Actualizar volúmenes de fluidos según el estado de mezcla
UpdateFluidVolumesFromFlow(volumeChangeL);
// Debug para verificar cambios
if (VerboseLogging() && Math.Abs(volumeChangeL) > 0.1)
{
Trace.WriteLine($"Tanque {Nombre}: Δt={deltaTime:F3}min, ΔVolumen={volumeChangeL:F2}L, " +
$"Volumen={CurrentVolumeL:F1}L, Estado={MixingState}, Fluido={CurrentFluidDescription}");
}
}
private void UpdateFluidVolumesFromFlow(double volumeChangeL)
{
if (Math.Abs(volumeChangeL) < 0.01) return; // Cambio muy pequeño
if (volumeChangeL > 0)
{
// Flujo de entrada - agregar al fluido primario por defecto
_primaryVolumeL += volumeChangeL;
}
else
{
// Flujo de salida - quitar según el estado de mezcla
var volumeToRemove = Math.Abs(volumeChangeL);
switch (MixingState)
{
case MixingState.SecondaryOnly:
case MixingState.EmptyingSecondary:
_secondaryVolumeL = Math.Max(0, _secondaryVolumeL - volumeToRemove);
break;
case MixingState.PrimaryOnly:
case MixingState.EmptyingPrimary:
_primaryVolumeL = Math.Max(0, _primaryVolumeL - volumeToRemove);
break;
case MixingState.Mixing:
// Quitar proporcionalmente
var totalVolume = _primaryVolumeL + _secondaryVolumeL;
if (totalVolume > 0)
{
var primaryRatio = _primaryVolumeL / totalVolume;
var secondaryRatio = _secondaryVolumeL / totalVolume;
_primaryVolumeL = Math.Max(0, _primaryVolumeL - volumeToRemove * primaryRatio);
_secondaryVolumeL = Math.Max(0, _secondaryVolumeL - volumeToRemove * secondaryRatio);
}
break;
}
}
UpdateMixingState();
}
private void UpdateFlowsFromConnectedPipes(Dictionary<string, double> flows)
{
NetFlow = 0.0;
// Buscar flujos en las ramas que involucren este tanque
foreach (var flow in flows)
{
var branchName = flow.Key;
var flowValueM3s = flow.Value; // El flujo viene en m³/s
var flowValueLmin = flowValueM3s * 60000.0; // Convertir a L/min
// Buscar conexiones que involucren este tanque
if (branchName.Contains(Nombre))
{
// Determinar dirección del flujo basado en el nombre de la rama
if (IsFlowTowardsTank(branchName))
{
NetFlow += flowValueLmin; // Flujo hacia el tanque (positivo)
}
else
{
NetFlow -= flowValueLmin; // Flujo desde el tanque (negativo)
}
}
}
}
private double GetDeltaTime()
{
var currentTime = Environment.TickCount64 / 60000.0; // convertir a minutos
var deltaTime = _lastUpdateTime > 0 ? currentTime - _lastUpdateTime : 0.0;
_lastUpdateTime = currentTime;
// Limitar delta time para evitar saltos grandes
return Math.Min(deltaTime, 0.1); // máximo 0.1 minutos (6 segundos)
}
private bool VerboseLogging()
{
return _mainViewModel?.hydraulicSimulationManager?.VerboseOutput ?? false;
}
private void InvalidateHydraulicNetwork()
{
_mainViewModel?.hydraulicSimulationManager?.InvalidateNetwork();
}
private string GetTankDescription()
{
return "Tanque hidráulico";
}
private bool IsFlowTowardsTank(string branchName)
{
// Determinar la dirección del flujo basándose en el nombre de la rama
return branchName.EndsWith($" -> {Nombre}") || branchName.EndsWith($"_{Nombre}");
}
// IHydraulicFlowReceiver Implementation
public void SetFlow(double flow)
{
// Este método puede ser llamado por tuberías conectadas
// La lógica de flujo se maneja en ApplyHydraulicResults
}
public double GetFlow()
{
return FlowBalance;
}
// IHydraulicPressureReceiver Implementation
public void SetPressure(double pressure)
{
if (!IsFixedPressure)
{
CurrentPressure = pressure;
}
}
public double GetPressure()
{
return CurrentPressure;
}
// ZIndex Implementation
public int zIndex_fromFrames { get; set; } = 2;
public ZIndexEnum ZIndex_Base()
{
return ZIndexEnum.Estaticos;
}
// IosBase Implementation
public void Start()
{
// Inicialización del tanque para la simulación
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
}
public void Inicializar(int valorInicial)
{
// Inicialización con valor inicial
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
}
// Disposing no longer needed for connections since we removed them
public void Disposing()
{
// Cleanup if needed for fluids
}
/// <summary>
/// Calcula el flujo neto desde pipes conectadas usando resultados de TSNet
/// </summary>
private double CalculateNetFlowFromConnectedPipes(Dictionary<string, double> flows)
{
double netFlow = 0.0;
if (_mainViewModel?.ObjetosSimulables == null) return netFlow;
try
{
// Buscar todas las pipes conectadas a este tanque
var connectedPipes = _mainViewModel.ObjetosSimulables
.OfType<osHydPipe>()
.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.Nombre}_Pipe";
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;
}
}
/// <summary>
/// Actualiza el color del tanque basado en el fluido actual
/// </summary>
private void UpdateTankColorFromFluid()
{
try
{
var currentFluid = CurrentOutputFluid;
if (currentFluid != null)
{
var colorHex = currentFluid.Color;
var color = (Color)ColorConverter.ConvertFromString(colorHex);
ColorButton_oculto = new SolidColorBrush(color);
}
else
{
ColorButton_oculto = Brushes.LightBlue; // Color por defecto
}
}
catch
{
ColorButton_oculto = Brushes.LightBlue; // Color por defecto en caso de error
}
}
// Serialization Support
/// <summary>
/// Se llama antes de la serialización para asegurar que el lock esté disponible
/// </summary>
private void EnsureLockInitialized()
{
if (_levelLock == null)
_levelLock = new object();
}
// osBase Overrides
public override void ucLoaded()
{
// El UserControl ya se ha cargado y podemos obtener las coordenadas para
// crear el objeto de simulacion
base.ucLoaded();
// Reinicializar TSNet Adapter si es necesario
if (TSNetAdapter == null)
{
TSNetAdapter = new TSNetTankAdapter(this);
Debug.WriteLine($"Tank {Nombre}: TSNetAdapter inicializado en ucLoaded");
}
Debug.WriteLine($"osHydTank.ucLoaded(): Componente hidráulico cargado correctamente");
// Inicialización específica del tanque hidráulico
SafeUpdateVolumeCalculations();
SafeUpdateTankPressure();
// Debug: Confirmar que el UserControl se está cargando
Debug.WriteLine($"osHydTank.ucLoaded(): Tanque '{Nombre}' cargado - Tamaño: {Tamano}, ZIndex: {zIndex_fromFrames}");
}
public override void ucUnLoaded()
{
// En el nuevo sistema unificado, el HydraulicSimulationManager
// maneja automáticamente el registro/desregistro de objetos
base.ucUnLoaded();
}
}
}