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