You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SmartAquaViewer/SmartAquaViewer/ViewModel/MonitoringViewModel.cs

547 lines
22 KiB

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
/// <summary>
/// 데이터에서 추출한 필드 정보
/// </summary>
public sealed class FieldItem
{
public string? Name { get; init; } // 바인딩 경로 키 (예: "Tank.DOValue")
public string? Display { get; init; } // UI 표시명 (예: "DO (mg/L)")
public Type? DataType { get; init; } // typeof(double), typeof(DateTime) 등
public StepFieldKind Kind { get; init; }
public override string? ToString() => Display;
}
/// <summary>
/// 특정 시점에 기록된 수조 데이터 묶음
/// </summary>
public class TanksByTime
{
public DateTime RecordedTime { get; }
public List<WaterTank> Tanks { get; } = new();
public TanksByTime(DateTime time, List<WaterTank> tanks)
{
RecordedTime = time;
Tanks = tanks;
}
}
public class MonitoringViewModel : PagingViewModelBase<WaterQualityVO>
{
#region Properties
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; set; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
public ObservableCollection<TanksByTime> TanksByTimes { get; } = new();
public PagingViewModelBase<TanksByTime> TanksPager { get; } = new();
private MonitorTab _selectedTab;
public MonitorTab SelectedTab
{
get => _selectedTab;
set
{
if (_selectedTab != value)
{
_selectedTab = value;
OnPropertyChanged();
RebuildAvailableFields(); // 탭에 맞춰 필드 목록 재구성
SetGraphType();
OnPropertyChanged(nameof(IsTankAndLine));
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
RebuildFieldCandidates();
}), DispatcherPriority.Background);
OnSystemChanged?.Invoke(SelectedTab);
}
}
}
private int _selectedGraphIndex = -1;
public int SelectedGraphIndex
{
get => _selectedGraphIndex;
set
{
if (_selectedGraphIndex != value)
{
_selectedGraphIndex = value;
OnPropertyChanged();
// 인덱스가 바뀌면 enum도 맞춰준다
if (value >= 0 && value < GraphTypes.Count)
SelectedGraphType = GraphTypes[value];
}
}
}
private GraphType _selectedGraphType;
public GraphType SelectedGraphType
{
get => _selectedGraphType;
set
{
if (_selectedGraphType != value)
{
_selectedGraphType = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowXSelector));
RebuildFieldCandidates();
var idx = GraphTypes.IndexOf(value);
if (idx != -1 && idx != _selectedGraphIndex)
{
_selectedGraphIndex = idx;
OnPropertyChanged(nameof(SelectedGraphIndex));
OnPropertyChanged(nameof(IsTankAndLine));
}
}
}
}
private StepFieldKind _selectedKind = StepFieldKind.Sensor; // 기본값은 센서
public StepFieldKind SelectedKind
{
get => _selectedKind;
set
{
if (_selectedKind != value)
{
_selectedKind = value;
OnPropertyChanged();
// 라디오 변경 시 Y 후보 재구성
RebuildFieldCandidates();
}
}
}
public bool IsTankAndLine
{
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
}
private bool _isOpenMode;
public bool IsOpenMode
{
get => _isOpenMode;
set
{
if (_isOpenMode != value)
{
_isOpenMode = value;
OnPropertyChanged();
BtnVisibilityDown = _isOpenMode ? Visibility.Visible : Visibility.Collapsed;
BtnVisibilityUp = _isOpenMode ? Visibility.Collapsed : Visibility.Visible;
}
}
}
private Visibility _btnVisibilityDown;
public Visibility BtnVisibilityDown
{
get => _btnVisibilityDown;
set
{
if (_btnVisibilityDown != value)
{
_btnVisibilityDown = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityUp;
public Visibility BtnVisibilityUp
{
get => _btnVisibilityUp;
set
{
if (_btnVisibilityUp != value)
{
_btnVisibilityUp = value;
OnPropertyChanged();
}
}
}
public bool ShowXSelector => SelectedGraphType == GraphType.SCATTER;
// [필드 후보 목록] 탭/시스템에 따라 달라짐
public ObservableCollection<FieldItem> AvailableFields { get; } = new();
// [X축 후보/선택]
public ObservableCollection<FieldItem> XFieldCandidates { get; } = new();
private FieldItem? _selectedXField;
public FieldItem? SelectedXField
{
get => _selectedXField;
set { if (_selectedXField != value) { _selectedXField = value; OnPropertyChanged(); } }
}
// [Y축 후보/선택] — Line/Step: 다중, Scatter/Box: 단일
public ObservableCollection<FieldItem> YFieldCandidates { get; } = new();
// 다중 선택(Y)용
public ObservableCollection<FieldItem> SelectedYFields { get; } = new();
// 단일 선택(Y)용
private FieldItem? _selectedYField;
public FieldItem? SelectedYField
{
get => _selectedYField;
set { if (_selectedYField != value) { _selectedYField = value; OnPropertyChanged(); } }
}
// [옵션] 예시 — 필요하면 추가
private bool _showMarkers; // Line
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
private bool _useSmoothing; // Line
public bool UseSmoothing { get => _useSmoothing; set { _useSmoothing = value; OnPropertyChanged(); } }
private double _scatterMarkerSize = 3; // Scatter
public double ScatterMarkerSize { get => _scatterMarkerSize; set { _scatterMarkerSize = value; OnPropertyChanged(); } }
private bool _showRegression; // Scatter
public bool ShowRegression { get => _showRegression; set { _showRegression = value; OnPropertyChanged(); } }
private double _boxWidth = 0.3; // Box
public double BoxWidth { get => _boxWidth; set { _boxWidth = value; OnPropertyChanged(); } }
private int _boxTimeSpan = 6; // Box
public int BoxTimeSpan
{
get => _boxTimeSpan;
set
{
if (_boxTimeSpan != value)
{
_boxTimeSpan = value;
OnPropertyChanged();
}
}
}
#endregion
public ICommand ChangeDrawerStatusCommand { get; }
public ICommand DrawGraphCommand { get; }
public delegate void SystemChangedEventHandler(MonitorTab selectedTab);
public event SystemChangedEventHandler OnSystemChanged;
public MonitoringViewModel()
{
IsOpenMode = true;
BtnVisibilityUp = Visibility.Collapsed;
WaterQualityList = Datas.Instance.WaterQualityView;
((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnWaterQualityChanged;
RebuildAllGroups();
GraphTypes = [];
SelectedTab = MonitorTab.Tank; // Default system
SetGraphType();
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
}
private void OnWaterQualityChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildAllGroups();
private void RebuildAllGroups()
{
var grouped = WaterQualityList
.GroupBy(x => x.Tank.Number) // 또는 x.Tank.Num
.OrderBy(g => g.Key)
.ToDictionary(
g => g.Key,
g => new ObservableCollection<WaterQualityVO>(
g.OrderBy(r => r.RecordedTime))
);
TankGroups = grouped;
OnPropertyChanged(nameof(TankGroups));
TanksPager.Items.Clear();
foreach (var w in WaterQualityList)
TanksPager.Items.Add(new TanksByTime(w.RecordedTime, w.Tanks));
var ordered = TanksPager.Items.OrderBy(t => t.RecordedTime).ToList();
TanksPager.Items.Clear();
foreach (var t in ordered) TanksPager.Items.Add(t);
// TankGroups를 깔끔하게 다시 구성
this.Items.Clear();
foreach (var w in WaterQualityList)
this.Items.Add(w);
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
if (SelectedTab.Equals(MonitorTab.Tank))
SetGraphData_Line_Tank();
else
GraphControlVM.SetDefaultLineGraph(
WaterQualityList.ToList(), SelectedTab, SelectedXField, SelectedYField, ShowMarkers);
break;
case GraphType.BOX:
var xFieldBox = SelectedXField;
SetGraphData_Box_Tank();
break;
case GraphType.SCATTER:
SetGraphData_Scatter_Tank();
break;
case GraphType.STEP:
var xFieldStep = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
var tFieldsStep = SelectedYFields;
var yFiledStep = SelectedYField;
var showMarkerStep = ShowMarkers;
if (SelectedKind.Equals(StepFieldKind.Status))
GraphControlVM.SetStatusSeriesStopPlot(WaterQualityList.ToList(), SelectedYField, SelectedYFields, ShowMarkers, ShowLegends);
else
GraphControlVM.SetStepPlot(WaterQualityList.ToList(), SelectedTab, xFieldStep, SelectedYFields, ShowMarkers, ShowLegends);
break;
default:
break;
}
}
private void SetGraphData_Line_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetTankLineGraph(WaterQualityList, keys, SelectedXField, SelectedYField, ShowMarkers, ShowLegends);
}
private void SetGraphData_Box_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField!, SelectedYField, BoxWidth, boxTimeSpan);
}
private void SetGraphData_Scatter_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetScatterPlot(WaterQualityList, SelectedXField!, SelectedYField, keys, ScatterMarkerSize, ShowRegression, ShowLegends);
}
private void SetGraphType()
{
GraphTypes.Clear();
switch (SelectedTab)
{
case MonitorTab.Tank:
GraphTypes.Add(GraphType.LINE);
GraphTypes.Add(GraphType.BOX);
GraphTypes.Add(GraphType.SCATTER);
break;
case MonitorTab.Filter:
case MonitorTab.Sterilizer:
GraphTypes.Add(GraphType.LINE);
GraphTypes.Add(GraphType.STEP);
break;
default:
break;
}
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
SelectedGraphIndex = -1;
SelectedGraphIndex = GraphTypes.Count > 0 ? 0 : -1;
}), DispatcherPriority.Background);
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime), Kind = StepFieldKind.Time });
if (SelectedTab == MonitorTab.Tank)
{
AvailableFields.Add(new FieldItem { Name = "Number", Display = "수조", DataType = typeof(int), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "DOValue", Display = "DO (mg/L)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "PH", Display = "pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "ORP", Display = "ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Temperature", Display = "온도 (℃)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
}
else if (SelectedTab == MonitorTab.Filter)
{
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterPower", Display = "모래여과기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpPower", Display = "순환펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.InverterControllerStatus", Display = "인버터 제어기 상태", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpPower", Display = "히트펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerPower", Display = "에어브로와 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
}
else // Sterilizer
{
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPressure", Display = "용해장치 압력 (kPa)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorPower", Display = "오존 발생기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPower", Display = "오존용해장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerPower", Display = "배오존장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AddUvPowerFieldsPerId(WaterQualityList);
}
}
// rows: 현재 그리려는 데이터(필터링/정렬 반영된)
private void AddUvPowerFieldsPerId(IEnumerable<WaterQualityVO> rows)
{
// 케이스 A: 한 행이 UV 한 대의 상태를 담는 스키마
var idsA = rows
.Select(r => r?.Sterilizing?.UVSterilizerId)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct()
.ToList();
foreach (var id in idsA)
{
// Name 규칙: "Sterilizing.UVSterilizerPower[id=XXX]"
AvailableFields.Add(new FieldItem
{
Name = $"Sterilizing.UVSterilizerPower[id={id}]",
Display = $"자외선 살균기 {id} 전원",
DataType = typeof(int),
Kind = StepFieldKind.Status
});
}
}
// 그래프 타입이 바뀔 때 후보/기본 선택 재구성
private void RebuildFieldCandidates()
{
// 후보 초기화
XFieldCandidates.Clear();
YFieldCandidates.Clear();
// X축: 시간 우선
foreach (var f in AvailableFields)
{
if ((SelectedGraphType == GraphType.LINE
|| SelectedGraphType == GraphType.STEP
|| SelectedGraphType == GraphType.SCATTER)
&& f.Name!.Equals("Number")) continue;
if (SelectedGraphType == GraphType.LINE
&& (f.Kind.Equals(StepFieldKind.Status)
|| f.Name!.Equals("Filtering.InverterControllerStatus"))) continue;
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.STEP || SelectedGraphType == GraphType.BOX) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
if (SelectedGraphType != GraphType.STEP) SelectedKind = StepFieldKind.Sensor;
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
if (SelectedGraphType is GraphType.LINE or GraphType.SCATTER or GraphType.BOX)
{
// 수치형만 (LINE/SCATTER/BOX는 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
else if (SelectedGraphType == GraphType.STEP)
{
// STEP은 상태 전환에 잘 맞음: int/bool 위주
src = src.Where(f => f.DataType == typeof(int) || f.DataType == typeof(bool) || f.DataType == typeof(double));
// (상태가 double로 들어오는 경우도 있을 수 있어 double 허용)
}
// Y축 후보: 수치형
foreach (var f in src) YFieldCandidates.Add(f);
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();
SelectedYField = null;
switch (SelectedGraphType)
{
case GraphType.LINE:
SelectedYField = YFieldCandidates.FirstOrDefault();
ShowMarkers = false;
UseSmoothing = false;
break;
case GraphType.STEP:
ShowMarkers = false;
UseSmoothing = false;
break;
case GraphType.SCATTER:
SelectedYField = YFieldCandidates.FirstOrDefault();
ScatterMarkerSize = 3;
ShowRegression = false;
break;
case GraphType.BOX:
SelectedYField = YFieldCandidates.FirstOrDefault();
BoxWidth = 0.3;
break;
}
OnPropertyChanged(nameof(SelectedYFields));
}
protected new void OnPropertyChanged([CallerMemberName] string? name = null)
=> base.OnPropertyChanged(name);
}
}