1288 lines
44 KiB
C#
1288 lines
44 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.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
|
|
{
|
|
// Referencia al objeto de simulación hidráulica específico
|
|
private simHydraulicTank SimHydraulicTank;
|
|
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 _maxLevelM = 2.0; // m
|
|
private double _minLevelM = 0.1; // m
|
|
private double _crossSectionalArea = 1.0; // m²
|
|
private double _currentVolumeL = 1000.0; // L
|
|
private double _maxVolumeL = 2000.0; // L
|
|
private double _inletFlow = 0.0; // L/min
|
|
private double _outletFlow = 0.0; // L/min
|
|
private double _currentPressure = 1.013; // bar
|
|
private bool _isFixedPressure = true;
|
|
private HydraulicTankType _tankType = HydraulicTankType.Intermediate;
|
|
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
|
|
|
|
[JsonIgnore]
|
|
private object _levelLock = new object();
|
|
|
|
|
|
// Enums
|
|
public enum HydraulicTankType
|
|
{
|
|
[Description("Tanque de succión (inicio del sistema)")]
|
|
Suction,
|
|
[Description("Tanque intermedio (buffer)")]
|
|
Intermediate,
|
|
[Description("Tanque de almacenamiento")]
|
|
Storage,
|
|
[Description("Tanque de proceso")]
|
|
Process
|
|
}
|
|
|
|
|
|
// 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 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()
|
|
{
|
|
// Se llama cuando inicia la simulación - crear objeto hidráulico si no existe
|
|
if (SimHydraulicTank == null)
|
|
{
|
|
// Convertir bar a Pa para el sistema hidráulico interno
|
|
var pressurePa = TankPressure * 100000.0;
|
|
SimHydraulicTank = hydraulicSimulationManager.AddTank(pressurePa, CurrentLevelM, MaxLevelM, MinLevelM, CrossSectionalArea, IsFixedPressure);
|
|
SimHydraulicTank.SimObjectType = "HydraulicTank";
|
|
SimHydraulicTank.WpfObject = this;
|
|
SimHydraulicTank.Nombre = Nombre;
|
|
}
|
|
else
|
|
{
|
|
// Actualizar propiedades si el objeto ya existe (convertir bar a Pa)
|
|
SimHydraulicTank.TankPressure = TankPressure * 100000.0;
|
|
SimHydraulicTank.CurrentLevel = CurrentLevelM;
|
|
SimHydraulicTank.MaxLevel = MaxLevelM;
|
|
SimHydraulicTank.MinLevel = MinLevelM;
|
|
SimHydraulicTank.CrossSectionalArea = CrossSectionalArea;
|
|
SimHydraulicTank.IsFixedPressure = IsFixedPressure;
|
|
}
|
|
}
|
|
|
|
public override void UpdateControl(int elapsedMilliseconds)
|
|
{
|
|
// Actualizar propiedades desde la simulación hidráulica
|
|
if (SimHydraulicTank != null)
|
|
{
|
|
// Convertir de m³/s a L/min
|
|
InletFlow = SimHydraulicTank.InletFlow * 60000.0;
|
|
OutletFlow = SimHydraulicTank.OutletFlow * 60000.0;
|
|
|
|
// Convertir de Pa a bar
|
|
CurrentPressure = SimHydraulicTank.CurrentPressure / 100000.0;
|
|
|
|
// Actualizar nivel del tanque basado en el balance de flujo
|
|
double deltaTime = elapsedMilliseconds / 60000.0; // Convertir a minutos
|
|
if (deltaTime > 0)
|
|
{
|
|
UpdateLevelFromFlowBalance(deltaTime);
|
|
}
|
|
}
|
|
|
|
// Actualizar el color según el fluido actual
|
|
UpdateMixingState();
|
|
}
|
|
|
|
|
|
|
|
// Properties - Configuration
|
|
|
|
[Category("🏗️ Configuración del Tanque")]
|
|
[DisplayName("Tipo de tanque")]
|
|
[Description("Función del tanque en el sistema hidráulico")]
|
|
public HydraulicTankType TankType
|
|
{
|
|
get => _tankType;
|
|
set
|
|
{
|
|
if (SetProperty(ref _tankType, value))
|
|
{
|
|
UpdateTankConfiguration();
|
|
InvalidateHydraulicNetwork();
|
|
}
|
|
}
|
|
}
|
|
|
|
[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.2, value)))
|
|
{
|
|
if (_currentLevelM > _maxLevelM)
|
|
CurrentLevelM = _maxLevelM;
|
|
SafeUpdateVolumeCalculations();
|
|
}
|
|
}
|
|
}
|
|
|
|
[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, Math.Min(value, _maxLevelM - 0.1))))
|
|
{
|
|
if (_currentLevelM < _minLevelM)
|
|
CurrentLevelM = _minLevelM;
|
|
SafeUpdateVolumeCalculations();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
{
|
|
if (SetProperty(ref _tankPressure, Math.Max(0, value)))
|
|
{
|
|
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(_minLevelM, Math.Min(_maxLevelM, 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 = _minLevelM + (clampedVolume / 1000.0) / _crossSectionalArea;
|
|
_currentLevelM = Math.Max(_minLevelM, Math.Min(_maxLevelM, _currentLevelM));
|
|
OnPropertyChanged(nameof(CurrentLevelM));
|
|
OnPropertyChanged(nameof(FillPercentage));
|
|
UpdateMixingState();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Category("📊 Estado Actual")]
|
|
[DisplayName("Porcentaje llenado")]
|
|
[Description("Porcentaje de llenado del tanque")]
|
|
[JsonIgnore]
|
|
public double FillPercentage => (_currentLevelM - _minLevelM) / (_maxLevelM - _minLevelM) * 100.0;
|
|
|
|
[Category("📊 Estado Actual")]
|
|
[DisplayName("Flujo entrada")]
|
|
[Description("Flujo de entrada actual (L/min)")]
|
|
[JsonIgnore]
|
|
public double InletFlow
|
|
{
|
|
get => _inletFlow;
|
|
private set => SetProperty(ref _inletFlow, value);
|
|
}
|
|
|
|
[Category("📊 Estado Actual")]
|
|
[DisplayName("Flujo salida")]
|
|
[Description("Flujo de salida actual (L/min)")]
|
|
[JsonIgnore]
|
|
public double OutletFlow
|
|
{
|
|
get => _outletFlow;
|
|
private set => SetProperty(ref _outletFlow, value);
|
|
}
|
|
|
|
[Category("📊 Estado Actual")]
|
|
[DisplayName("Balance flujo")]
|
|
[Description("Balance de flujo (entrada - salida) (L/min)")]
|
|
[JsonIgnore]
|
|
public double FlowBalance => InletFlow - OutletFlow;
|
|
|
|
[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);
|
|
}
|
|
|
|
|
|
|
|
// 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 bar a Pa para el sistema hidráulico interno
|
|
var pressurePa = TankPressure * 100000.0;
|
|
nodes.Add(new HydraulicNodeDefinition(Nombre, true, pressurePa, GetTankDescription()));
|
|
//Debug.WriteLine($"Tanque {Nombre}: Nodo de presión fija creado - {TankPressure:F2} bar ({pressurePa:F0} Pa)");
|
|
}
|
|
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)
|
|
{
|
|
var deltaTime = GetDeltaTime();
|
|
|
|
// Obtener flujos conectados
|
|
UpdateFlowsFromConnectedPipes(flows);
|
|
|
|
// Actualizar presión (convertir de Pa a bar)
|
|
if (pressures.ContainsKey(Nombre))
|
|
{
|
|
CurrentPressure = pressures[Nombre] / 100000.0; // Pa a bar
|
|
}
|
|
|
|
// Actualizar nivel basado en balance de flujo
|
|
UpdateLevelFromFlowBalance(deltaTime);
|
|
|
|
if (VerboseLogging())
|
|
{
|
|
Debug.WriteLine($"Tanque {Nombre}: Nivel={CurrentLevelM:F2}m ({FillPercentage:F1}%), " +
|
|
$"Flujo_entrada={InletFlow:F2}L/min, Flujo_salida={OutletFlow:F2}L/min, " +
|
|
$"Balance={FlowBalance:F2}L/min, Presión={CurrentPressure:F2}bar, Fluido={CurrentFluidDescription}");
|
|
}
|
|
}
|
|
|
|
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>
|
|
/// Indicador del tipo de tanque
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string TankTypeIndicator
|
|
{
|
|
get
|
|
{
|
|
return TankType switch
|
|
{
|
|
HydraulicTankType.Suction => "S",
|
|
HydraulicTankType.Intermediate => "I",
|
|
HydraulicTankType.Storage => "A",
|
|
HydraulicTankType.Process => "P",
|
|
_ => "T"
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
/// Posición Y de la línea de nivel medio (50%)
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public double MidLevelY
|
|
{
|
|
get
|
|
{
|
|
var tankHeight = Tamano * 100; // Convertir a píxeles aproximados
|
|
return tankHeight * 0.5; // 50% de altura
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Posición Y de la línea de nivel mínimo
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public double MinLevelY
|
|
{
|
|
get
|
|
{
|
|
var tankHeight = Tamano * 100; // Convertir a píxeles aproximados
|
|
var minPercentage = (MinLevelM - MinLevelM) / (MaxLevelM - MinLevelM) * 100;
|
|
return tankHeight - (tankHeight * (minPercentage + 10) / 100); // 10% del nivel mínimo
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
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 - _minLevelM) * _crossSectionalArea * 1000.0; // convertir m³ a L
|
|
_maxVolumeL = (_maxLevelM - _minLevelM) * _crossSectionalArea * 1000.0;
|
|
|
|
// 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)
|
|
{
|
|
CurrentPressure = TankPressure;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine($"Error in SafeUpdateTankPressure: {ex.Message}");
|
|
// Usar presión atmosférica por defecto
|
|
_currentPressure = 1.013;
|
|
}
|
|
}
|
|
|
|
private void UpdateTankConfiguration()
|
|
{
|
|
// Configuración automática según el tipo de tanque
|
|
switch (TankType)
|
|
{
|
|
case HydraulicTankType.Suction:
|
|
// Tanques de succión típicamente a presión atmosférica
|
|
if (TankPressure < 0.5) // Si no se ha configurado manualmente
|
|
TankPressure = 1.013; // 1 atm
|
|
IsFixedPressure = true;
|
|
break;
|
|
|
|
case HydraulicTankType.Intermediate:
|
|
case HydraulicTankType.Storage:
|
|
case HydraulicTankType.Process:
|
|
// Estos pueden tener presión variable o fija según el proceso
|
|
break;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
Debug.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)
|
|
{
|
|
InletFlow = 0.0;
|
|
OutletFlow = 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 si esta rama conecta este tanque
|
|
if (branchName.Contains("Pump") && HasPumpConnection(branchName, flowValueLmin))
|
|
{
|
|
continue; // Ya manejado en HasPumpConnection
|
|
}
|
|
|
|
// Buscar otras conexiones directas al tanque por nombre
|
|
if (branchName.Contains(Nombre))
|
|
{
|
|
if (flowValueLmin > 0)
|
|
{
|
|
// Flujo positivo hacia o desde el tanque
|
|
if (IsFlowTowardsTank(branchName))
|
|
{
|
|
InletFlow += flowValueLmin;
|
|
}
|
|
else
|
|
{
|
|
OutletFlow += flowValueLmin;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool HasPumpConnection(string branchName, double flowValueLmin)
|
|
{
|
|
if (_mainViewModel == null) return false;
|
|
|
|
// Buscar si hay una bomba conectada que esté generando este flujo
|
|
var connectedPipes = _mainViewModel.ObjetosSimulables
|
|
.OfType<osHydPipe>()
|
|
.Where(pipe => pipe.Id_ComponenteA == Nombre || pipe.Id_ComponenteB == Nombre)
|
|
.ToList();
|
|
|
|
foreach (var pipe in connectedPipes)
|
|
{
|
|
if (pipe.Id_ComponenteA == Nombre)
|
|
{
|
|
// Esta conexión sale del tanque
|
|
if (branchName.Contains(pipe.Id_ComponenteB) && branchName.Contains("Pump"))
|
|
{
|
|
OutletFlow += Math.Abs(flowValueLmin);
|
|
return true;
|
|
}
|
|
}
|
|
else if (pipe.Id_ComponenteB == Nombre)
|
|
{
|
|
// Esta conexión llega al tanque
|
|
if (branchName.Contains(pipe.Id_ComponenteA) && branchName.Contains("Pump"))
|
|
{
|
|
InletFlow += Math.Abs(flowValueLmin);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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 TankType switch
|
|
{
|
|
HydraulicTankType.Suction => "Tanque de succión",
|
|
HydraulicTankType.Intermediate => "Tanque intermedio",
|
|
HydraulicTankType.Storage => "Tanque de almacenamiento",
|
|
HydraulicTankType.Process => "Tanque de proceso",
|
|
_ => "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
|
|
}
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
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()
|
|
{
|
|
// Remover objeto hidráulico de la simulación
|
|
if (SimHydraulicTank != null)
|
|
{
|
|
hydraulicSimulationManager.Remove(SimHydraulicTank);
|
|
SimHydraulicTank = null;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|