fix: 상자그림 생성 기능 수정

prototype
HyungJune Kim 10 months ago
parent 8858bafd8b
commit 9a3ae0cda2

@ -22,6 +22,8 @@ namespace SmartAquaViewer.DataAnalysis
/// </summary>
public WaterTank Tank { get; set; } = new();
public List<WaterTank> Tanks { get; set; } = new();
/// <summary>
/// 여과 시스템
/// </summary>
@ -111,7 +113,6 @@ namespace SmartAquaViewer.DataAnalysis
{
DateTime ts = start.AddSeconds(stepSeconds * i);
var vo = new WaterQualityVO
{
RecordedTime = ts,
@ -123,6 +124,30 @@ namespace SmartAquaViewer.DataAnalysis
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)
),
Tanks = new List<WaterTank>()
{
new WaterTank(
number: 1,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
new WaterTank(
number: 2,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
new WaterTank(
number: 3,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
},
Filtering = new FilteringSystem(
sandFilterPower: rand.Next(0, 2) == 1,
sandFilterEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),

@ -32,119 +32,133 @@
SelectedTab="{Binding SelectedTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<ScrollViewer x:Name="svTanks" Grid.Row="1" Margin="20 20 20 40"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center">
<ItemsControl ItemsSource="{Binding TankGroups}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="20 0" HorizontalAlignment="Center" >
<TextBlock Text="{Binding Key, StringFormat=수조 {0}}"
FontSize="20" FontWeight="Bold" Foreground="White"
Margin="0 0 0 10"/>
<!-- Value(= ObservableCollection<WaterQualityVO>)로 DataGrid -->
<DataGrid ItemsSource="{Binding Value}"
Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<!-- 측정 시각 -->
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<!-- Tank 값들 -->
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tank.DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="pH" Binding="{Binding Tank.PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tank.ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tank.Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tank.FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ScrollViewer x:Name="svFilter" Grid.Row="1" Margin="20 20 20 40"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center" Visibility="Collapsed">
<DataGrid ItemsSource="{Binding WaterQualityList}" Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
<DataGrid x:Name="dgTanks" ItemsSource="{Binding TanksByTimes}"
Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
Grid.Row="1" Margin="20 20 20 40"
ColumnWidth="*"
Background="Transparent"
HorizontalAlignment="Stretch">
<DataGrid.Columns>
<!-- 측정 시각 -->
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="모래여과기 전원" Binding="{Binding Filtering.SandFilterPower, Converter={StaticResource BoolToPowerConverter}}"
<!-- Tank 값들 -->
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[0].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 pH" Binding="{Binding Filtering.SumpPH}"
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tanks[0].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 ORP(mV)" Binding="{Binding Filtering.SumpORP, StringFormat=\{0:F2\}}"
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[0].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 수위(m)" Binding="{Binding Filtering.SumpWaterLevel, StringFormat=\{0:F0\}}"
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tanks[0].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 유량(m³/s)" Binding="{Binding Filtering.SumpFlowRate, StringFormat=\{0:F1\}}"
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tanks[0].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 수온(°C)" Binding="{Binding Filtering.SumpTemperature, StringFormat=\{0:F2\}}"
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tanks[0].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 전원" Binding="{Binding Filtering.CirculationPumpPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Width="5"/>
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[1].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="인버터 제어기 상태" Binding="{Binding Filtering.InverterControllerStatus}"
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tanks[1].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 유량(m³/s)" Binding="{Binding Filtering.FlowRate, StringFormat=\{0:F2\}}"
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[1].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 전원" Binding="{Binding Filtering.HeatPumpPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tanks[1].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 온도(°C)" Binding="{Binding Filtering.HeatPumpTemperature, StringFormat=\{0:F2\}}"
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tanks[1].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="에어브로와 전원" Binding="{Binding Filtering.AirBlowerPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tanks[1].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
<ScrollViewer x:Name="svSterilizer" Grid.Row="1" Margin="20 20 20 40"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center" Visibility="Collapsed">
<DataGrid ItemsSource="{Binding WaterQualityList}" Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Width="5"/>
<DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[2].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 ID" Binding="{Binding Sterilizing.UVSterilizerId}"
<DataGridTextColumn Header="DO(mg/L)" Binding="{Binding Tanks[2].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[2].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Header="ORP(mV)" Binding="{Binding Tanks[2].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 압력(kPa)" Binding="{Binding Sterilizing.OzoneDissolverPressure, StringFormat=\{0:F1\}}"
<DataGridTextColumn Header="온도(℃)" Binding="{Binding Tanks[2].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower, Converter={StaticResource BoolToPowerConverter}}"
<DataGridTextColumn Header="유량(m³/s)" Binding="{Binding Tanks[2].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
</DataGrid.Columns>
</DataGrid>
<DataGrid ItemsSource="{Binding WaterQualityList}" x:Name="dgFilter"
Style="{StaticResource DataGridStyle}" ColumnWidth="*"
Grid.Row="1" Margin="20 20 20 40"
Background="Transparent"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="모래여과기 전원" Binding="{Binding Filtering.SandFilterPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 pH" Binding="{Binding Filtering.SumpPH}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 ORP(mV)" Binding="{Binding Filtering.SumpORP, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 수위(m)" Binding="{Binding Filtering.SumpWaterLevel, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 유량(m³/s)" Binding="{Binding Filtering.SumpFlowRate, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="섬프탱크 수온(°C)" Binding="{Binding Filtering.SumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 전원" Binding="{Binding Filtering.CirculationPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="인버터 제어기 상태" Binding="{Binding Filtering.InverterControllerStatus}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="순환펌프 유량(m³/s)" Binding="{Binding Filtering.FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 전원" Binding="{Binding Filtering.HeatPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="히트펌프 온도(°C)" Binding="{Binding Filtering.HeatPumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="에어브로와 전원" Binding="{Binding Filtering.AirBlowerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
<DataGrid ItemsSource="{Binding WaterQualityList}" x:Name="dgSterilizer"
Style="{StaticResource DataGridStyle}" ColumnWidth="*"
Grid.Row="1" Margin="20 20 20 40"
Background="Transparent"
VerticalScrollBarVisibility="Auto"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 ID" Binding="{Binding Sterilizing.UVSterilizerId}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 압력(kPa)" Binding="{Binding Sterilizing.OzoneDissolverPressure, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
<Grid Grid.Row="1" VerticalAlignment="Bottom">
<Button Name="btnVisibilityDown" Tag="down"
@ -202,7 +216,24 @@
DisplayMemberPath="Display"/>
</Grid>
<Grid Margin="15 10 15 0">
<StackPanel Margin="15 15 15 0">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border CornerRadius="10" Background="White" Margin="0 5 0 10">
<ListBox ItemsSource="{Binding TankGroups}"
DisplayMemberPath="Key"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedDictionary="{Binding SelectedWaterTanks, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</StackPanel>
<Grid Margin="15 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
@ -264,23 +295,6 @@
<!-- LINE -->
<StackPanel Style="{StaticResource VisibleWhenLine}">
<StackPanel Style="{StaticResource VisibleWhenTankNLine}">
<TextBlock Text="수조 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border CornerRadius="10" Background="White" Margin="0 5 0 10">
<ListBox ItemsSource="{Binding TankGroups}"
DisplayMemberPath="Key"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedWaterTanks, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</StackPanel>
<Grid Style="{StaticResource VisibleWhenLine}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
@ -301,6 +315,11 @@
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<!--<CheckBox Content="스무딩" IsChecked="{Binding UseSmoothing}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
@ -389,6 +408,10 @@
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
</StackPanel>
</StackPanel>

@ -28,17 +28,17 @@ namespace SmartAquaViewer.View
{
private MonitoringViewModel? monitoringViewModel;
private readonly Dictionary<MonitorTab, ScrollViewer> _tabMap;
private readonly Dictionary<MonitorTab, UIElement> _tabMap;
public MonitoringView()
{
InitializeComponent();
_tabMap = new Dictionary<MonitorTab, ScrollViewer>
_tabMap = new Dictionary<MonitorTab, UIElement>
{
{ MonitorTab.Tank, svTanks },
{ MonitorTab.Filter, svFilter },
{ MonitorTab.Sterilizer, svSterilizer }
{ MonitorTab.Tank, dgTanks },
{ MonitorTab.Filter, dgFilter },
{ MonitorTab.Sterilizer, dgSterilizer }
};
Loaded += MonitoringView_Loaded;
@ -60,9 +60,9 @@ namespace SmartAquaViewer.View
private void SetActiveTab(MonitorTab tab)
{
// 전부 Collapsed
svTanks.Visibility = Visibility.Collapsed;
svFilter.Visibility = Visibility.Collapsed;
svSterilizer.Visibility = Visibility.Collapsed;
dgTanks.Visibility = Visibility.Collapsed;
dgFilter.Visibility = Visibility.Collapsed;
dgSterilizer.Visibility = Visibility.Collapsed;
// 대상만 Visible
if (_tabMap.TryGetValue(tab, out var target))

@ -1,13 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using OxyPlot.Axes;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;

@ -1,11 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.VisualBasic;
using System.Collections.ObjectModel;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
@ -30,9 +23,10 @@ namespace SmartAquaViewer.ViewModel
Model.TextColor = OxyColors.Black;
}
public void SetTankLineGraph(Dictionary<int, ObservableCollection<WaterQualityVO>> collection,
public void SetTankLineGraph(
Dictionary<int, ObservableCollection<WaterQualityVO>> collection,
FieldItem? xField, FieldItem? yField,
bool isMarker)
bool isMarker, bool showLegends)
{
Model.Series.Clear();
Model.Axes.Clear();
@ -57,9 +51,10 @@ namespace SmartAquaViewer.ViewModel
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
foreach (var (tankNum, data) in collection.OrderBy(x => x.Key))
foreach (var (tankNum, datas) in collection.OrderBy(x => x.Key))
{
if (data == null || data.Count == 0) continue;
if (datas == null || datas.Count == 0)
continue;
var series = new LineSeries()
{
@ -68,13 +63,14 @@ namespace SmartAquaViewer.ViewModel
MarkerSize = isMarker ? 3 : 0
};
foreach (var r in data.OrderBy(r => r.RecordedTime))
foreach (var data in datas.OrderBy(d => d.RecordedTime))
{
var y = ResolveTank(r, yField.Name!);
var tank = data.Tanks.Find(t => t.Number.Equals(tankNum));
var y = ResolveTank(tank, yField.Name!);
if (!y.HasValue) continue;
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(r.RecordedTime),
DateTimeAxis.ToDouble(data.RecordedTime), // 여기서 recordedTime 사용
y.Value));
}
@ -87,6 +83,16 @@ namespace SmartAquaViewer.ViewModel
}
}
Model.Legends.Clear();
Model.IsLegendVisible = showLegends;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = "수조",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true);
}
@ -219,7 +225,8 @@ namespace SmartAquaViewer.ViewModel
}
public void SetBoxPlot(
List<WaterQualityVO> rows,
ReadOnlyObservableCollection<WaterQualityVO> collection,
List<int>? selectedTankNums,
FieldItem xAxisKind, // 시간 or 수조
FieldItem valueField, // 값 필드
double boxWidth, // 박스 너비
@ -248,62 +255,77 @@ namespace SmartAquaViewer.ViewModel
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
var series = new BoxPlotSeries()
{
BoxWidth = boxWidth
};
var timeBuckets = collection
.Select(w => FloorToBucket(w.RecordedTime, (TimeSpan)timeBucket))
.Distinct()
.OrderBy(t => t)
.ToList();
if (rows == null || rows.Count == 0) return;
// X축 라벨
xAxis.Labels.AddRange(timeBuckets.Select(t => t.ToString("MM-dd HH:mm")));
var bucket = timeBucket ?? TimeSpan.FromHours(1);
var allTankIds = collection.SelectMany(w => w.Tanks).Select(t => t.Number).Distinct().OrderBy(id => id).ToList();
var tankIds = (selectedTankNums == null || !selectedTankNums.Any())
? allTankIds
: allTankIds.Where(id => selectedTankNums.Contains(id)).ToList();
var col = rows
.OrderBy(r => r.RecordedTime)
.GroupBy(g => ResolveTankOrTime(g, xAxisKind.Name, bucket))
.ToDictionary(g => g.Key, g => g.ToList());
foreach(var (key, data) in col.OrderBy(x => x.Key))
if (tankIds.Count == 0 || timeBuckets.Count == 0)
{
// 1) 수조/시간별 값 리스트
var values = data
.Select(d => ResolveTank(d, valueField.Name!))
.OfType<double>() // object→double
.Where(v => !double.IsNaN(v) && !double.IsInfinity(v))
.OrderBy(v => v)
.ToList();
if (values.Count == 0) continue;
Model.InvalidatePlot(true);
return;
}
// 2) 사분위수/중앙값 계산
double q1 = Percentile(values, 0.25);
double median = Percentile(values, 0.50);
double q3 = Percentile(values, 0.75);
double iqr = q3 - q1;
// 3) 팔레트
var colors = OxyPalettes.HueDistinct(tankIds.Count).Colors;
// 3) 수염(윗/아랫 경계)과 이상치(Tukey 1.5*IQR)
double lowerFence = q1 - 1.5 * iqr;
double upperFence = q3 + 1.5 * iqr;
var inliers = values.Where(v => v >= lowerFence && v <= upperFence).ToList();
var outliers = values.Where(v => v < lowerFence || v > upperFence).ToList();
double lowerWhisker = inliers.Count > 0 ? inliers.First() : values.First();
double upperWhisker = inliers.Count > 0 ? inliers.Last() : values.Last();
for (int k = 0; k < tankIds.Count; k++)
{
int tankId = tankIds[k];
// 4) 아이템 추가 (position = X축 인덱스)
string label = xAxisKind.Name switch
var series = new BoxPlotSeries
{
"수조" or "Tank" => $"수조 {key}",
"시간" or "RecordedTime" => FormatBucket(FloorToBucket(data.First().RecordedTime, bucket), bucket),
_ => key
Title = $"Tank {tankId}",
BoxWidth = boxWidth,
Fill = OxyColor.FromAColor(160, colors[k]),
Stroke = colors[k],
StrokeThickness = 1
};
xAxis.Labels.Add(label);
int position = xAxis.Labels.Count - 1;
var item = new BoxPlotItem(position, lowerWhisker, q1, median, q3, upperWhisker);
foreach (var o in outliers) item.Outliers.Add(o);
series.Items.Add(item);
for (int ti = 0; ti < timeBuckets.Count; ti++)
{
var bucketTime = timeBuckets[ti];
// 해당 시간 버킷 + 해당 수조 + 선택 필드 값 모으기
var values = collection
.Where(w => FloorToBucket(w.RecordedTime, (TimeSpan)timeBucket!) == bucketTime)
.SelectMany(w => w.Tanks)
.Where(t => t.Number == tankId)
.Select(t => ResolveTank(t, valueField.Name!)) // 선택 필드 동적 접근
.Where(v => v.HasValue)
.Select(v => v.Value)
.ToList();
if (values.Count < 1) continue;
var item = CreateBoxPlotItem(ti, values);
if (item != null) series.Items.Add(item);
}
if (series.Items.Count > 0)
Model.Series.Add(series);
}
Model.Series.Add(series);
Model.IsLegendVisible = false;
Model.Legends.Clear();
Model.IsLegendVisible = true;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = "수조",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true);
}
@ -343,42 +365,42 @@ namespace SmartAquaViewer.ViewModel
MarkerFill = OxyColors.DeepSkyBlue
};
foreach (var row in rows)
{
double x = ResolveTank(row, xAxisField.Name) ?? double.NaN;
double y = ResolveTank(row, yAxisField.Name) ?? double.NaN;
scatterSeries.Points.Add(new ScatterPoint(x, y));
}
//foreach (var row in rows)
//{
// double x = ResolveTank(row, xAxisField.Name) ?? double.NaN;
// double y = ResolveTank(row, yAxisField.Name) ?? double.NaN;
// scatterSeries.Points.Add(new ScatterPoint(x, y));
//}
Model.Series.Add(scatterSeries);
if (showRegression && rows.Count > 1)
{
var points = rows.Select(r
=> new DataPoint(
ResolveTank(r, xAxisField.Name) ?? double.NaN,
ResolveTank(r, yAxisField.Name) ?? double.NaN))
.ToList();
var regression = LinearRegression(points);
if (regression != null)
{
var lineSeries = new LineSeries
{
Title = "Regression",
Color = OxyColors.Red,
StrokeThickness = 2
};
// 최소/최대 구간으로 선 그리기
double minX = points.Min(p => p.X);
double maxX = points.Max(p => p.X);
lineSeries.Points.Add(new DataPoint(minX, regression.Value.Intercept + regression.Value.Slope * minX));
lineSeries.Points.Add(new DataPoint(maxX, regression.Value.Intercept + regression.Value.Slope * maxX));
Model.Series.Add(lineSeries);
}
}
//if (showRegression && rows.Count > 1)
//{
// var points = rows.Select(r
// => new DataPoint(
// ResolveTank(r, xAxisField.Name) ?? double.NaN,
// ResolveTank(r, yAxisField.Name) ?? double.NaN))
// .ToList();
// var regression = LinearRegression(points);
// if (regression != null)
// {
// var lineSeries = new LineSeries
// {
// Title = "Regression",
// Color = OxyColors.Red,
// StrokeThickness = 2
// };
// // 최소/최대 구간으로 선 그리기
// double minX = points.Min(p => p.X);
// double maxX = points.Max(p => p.X);
// lineSeries.Points.Add(new DataPoint(minX, regression.Value.Intercept + regression.Value.Slope * minX));
// lineSeries.Points.Add(new DataPoint(maxX, regression.Value.Intercept + regression.Value.Slope * maxX));
// Model.Series.Add(lineSeries);
// }
//}
Model.InvalidatePlot(true);
}
@ -619,6 +641,25 @@ namespace SmartAquaViewer.ViewModel
Model.InvalidatePlot(true);
}
private BoxPlotItem CreateBoxPlotItem(int xIndex, List<double> values)
{
values.Sort();
int n = values.Count;
if (n == 0) return null;
double q1 = values[(int)(0.25 * (n - 1))];
double q3 = values[(int)(0.75 * (n - 1))];
double median = values[(int)(0.5 * (n - 1))];
double iqr = q3 - q1;
double lower = values.Where(v => v >= q1 - 1.5 * iqr).DefaultIfEmpty(q1).Min();
double upper = values.Where(v => v <= q3 + 1.5 * iqr).DefaultIfEmpty(q3).Max();
return new BoxPlotItem(xIndex, lower, q1, median, q3, upper)
{
Outliers = values.Where(v => v < lower || v > upper).ToList()
};
}
private DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
{
long ticks = bucket.Ticks;
@ -626,16 +667,15 @@ namespace SmartAquaViewer.ViewModel
return new DateTime(floored, dt.Kind);
}
private double? ResolveTank(WaterQualityVO vo, string fieldName)
private double? ResolveTank(WaterTank tank, string fieldName)
{
return fieldName switch
{
"RecordedTime" => DateTimeAxis.ToDouble(vo.RecordedTime),
"Tank.DOValue" => vo.Tank.DOValue,
"Tank.PH" => vo.Tank.PH,
"Tank.ORP" => vo.Tank.ORP,
"Tank.Temperature" => vo.Tank.Temperature,
"Tank.FlowRate" => vo.Tank.FlowRate,
"DOValue" => tank.DOValue,
"PH" => tank.PH,
"ORP" => tank.ORP,
"Temperature" => tank.Temperature,
"FlowRate" => tank.FlowRate,
_ => null
};
}
@ -680,13 +720,18 @@ namespace SmartAquaViewer.ViewModel
};
}
private string ResolveTankOrTime(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null)
private string ResolveTankOrTime(WaterQualityVO data, string xAxisKind, TimeSpan bucket)
{
return fieldName switch
int? tankId = data.Tanks?.FirstOrDefault().Number; // 분리된 구조이므로 항상 하나만 존재해야 함
return xAxisKind switch
{
"수조" or "Tank.Number" => vo.Tank.Number.ToString(),
"시간" or "RecordedTime" => FormatBucket(FloorToBucket(vo.RecordedTime, (TimeSpan)bucket), (TimeSpan)bucket),
_ => ""
"수조" or "Tank" => tankId?.ToString() ?? "Unknown",
"시간" or "RecordedTime" =>
$"{FloorToBucket(data.RecordedTime, bucket):yyyy-MM-dd HH:mm} | Tank {tankId}",
_ => "Unknown"
};
}

@ -39,15 +39,15 @@ namespace SmartAquaViewer.ViewModel
SelectedViewModel = new MonitoringViewModel(); // Default view
//더미데이터 생성 및 파일로 저장
//for (int i = 1; i <= 10; i++)
//{
// DateTime date = new(2025, 8, i);
// var dataList = WaterQualityVO.GetSampleData(date, date, 24);
// var jsonStr = JsonConvert.SerializeObject(dataList, Formatting.Indented);
for (int i = 1; i <= 10; i++)
{
DateTime date = new(2025, 8, i);
var dataList = WaterQualityVO.GetSampleData(date, date, 24);
var jsonStr = JsonConvert.SerializeObject(dataList, Formatting.Indented);
// string fileName = date.ToString("yyyy-MM-dd") + ".json";
// File.WriteAllText(fileName, jsonStr);
//}
string fileName = date.ToString("yyyy-MM-dd") + ".json";
File.WriteAllText(fileName, jsonStr);
}
}
private void SwapView(object obj)

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
@ -25,6 +26,18 @@ namespace SmartAquaViewer.ViewModel
public StepFieldKind Kind { get; init; }
}
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 : INotifyPropertyChanged
{
#region Properties
@ -33,7 +46,13 @@ namespace SmartAquaViewer.ViewModel
public ObservableCollection<GraphType> GraphTypes { get; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; set; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
public ObservableCollection<TanksByTime> TanksByTimes { get; } = new();
private ObservableCollection<int> SelectedTankNumbers { get; } = new();
private MonitorTab _selectedTab;
public MonitorTab SelectedTab
@ -122,8 +141,6 @@ namespace SmartAquaViewer.ViewModel
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
}
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
private bool _isOpenMode;
public bool IsOpenMode
{
@ -201,6 +218,9 @@ namespace SmartAquaViewer.ViewModel
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(); } }
@ -246,15 +266,10 @@ namespace SmartAquaViewer.ViewModel
//Datas.Instance.SetWaterQualityVO(WaterQualityList);
WaterQualityList = Datas.Instance.WaterQualityView;
TankGroups = 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))
);
((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnWaterQualityChanged;
RebuildAllGroups();
GraphTypes = [];
SelectedTab = MonitorTab.Tank; // Default system
SetGraphType();
@ -266,23 +281,48 @@ namespace SmartAquaViewer.ViewModel
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));
// TankGroups를 깔끔하게 다시 구성
TanksByTimes.Clear();
foreach (var w in WaterQualityList)
{
var tbt = new TanksByTime(w.RecordedTime, w.Tanks);
TanksByTimes.Add(tbt);
}
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
var xField = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
var yField = SelectedYField;
var isMarker = ShowMarkers;
if (SelectedTab.Equals(MonitorTab.Tank)) SetGraphData_Line_Tank(xField, yField, isMarker);
else GraphControlVM.SetDefaultLineGraph(WaterQualityList.ToList(), SelectedTab, xField, yField, isMarker);
if (SelectedTab.Equals(MonitorTab.Tank))
SetGraphData_Line_Tank();
else
GraphControlVM.SetDefaultLineGraph(
WaterQualityList.ToList(), SelectedTab, SelectedXField, SelectedYField, ShowMarkers);
break;
case GraphType.BOX:
var xFieldBox = SelectedXField;
var dataFieldBox = SelectedYField;
var boxWidth = BoxWidth;
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
GraphControlVM.SetBoxPlot(WaterQualityList.ToList(), xFieldBox, dataFieldBox, boxWidth, boxTimeSpan);
SetGraphData_Box_Tank();
break;
case GraphType.SCATTER:
var xFieldScatter = SelectedXField;
@ -306,13 +346,36 @@ namespace SmartAquaViewer.ViewModel
}
}
private void SetGraphData_Line_Tank(FieldItem? xField, FieldItem? yField, bool showMarkers)
private void SetGraphData_Line_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (xField?.Name != "RecordedTime" || yField == null) return;
if (SelectedWaterTanks.Count == 0) return;
if (SelectedYField == null) return;
var keys = SelectedWaterTanks.Keys.ToList();
var selectedTanks = WaterQualityList
.SelectMany(wq => wq.Tanks.Select(tank => new { Tank = tank, VO = wq }))
.Where(x => keys.Contains(x.Tank.Number))
.GroupBy(x => x.Tank.Number)
.ToDictionary(
g => g.Key,
g => new ObservableCollection<WaterQualityVO>(
g.Select(x => x.VO).OrderBy(vo => vo.RecordedTime))
);
GraphControlVM.SetTankLineGraph(selectedTanks, 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.SetTankLineGraph(SelectedWaterTanks, xField, yField, showMarkers);
GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField, SelectedYField, BoxWidth, boxTimeSpan);
}
private void SetGraphType()
@ -351,12 +414,12 @@ namespace SmartAquaViewer.ViewModel
if (SelectedTab == MonitorTab.Tank)
{
AvailableFields.Add(new FieldItem { Name = "Tank.Number", Display = "수조", DataType = typeof(int), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.DOValue", Display = "DO (mg/L)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.PH", Display = "pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.ORP", Display = "ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.Temperature", Display = "온도 (℃)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Tank.FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
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)
{
@ -418,10 +481,12 @@ namespace SmartAquaViewer.ViewModel
// X축: 시간 우선
foreach (var f in AvailableFields)
{
if (SelectedGraphType == GraphType.SCATTER && f.Name.Equals("Tank.Number")) continue;
if ((SelectedGraphType == GraphType.LINE
|| SelectedGraphType == GraphType.STEP
|| SelectedGraphType == GraphType.SCATTER)
&& f.Name.Equals("Number")) continue;
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STEP) break;
if (SelectedGraphType == GraphType.BOX && f.Name.Equals("Tank.Number")) break;
if (SelectedGraphType == GraphType.STEP || SelectedGraphType == GraphType.BOX) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();

Loading…
Cancel
Save