Compare commits

..

41 Commits

Author SHA1 Message Date
HyungJune Kim 0a4900689e feat: CCTV 기능 개발 진행중
10 months ago
HyungJune Kim 1be29e82a0 fix: 온실가스 그래프 버그 수정
10 months ago
HyungJune Kim 2e56475725 feat: 에너지 -> 온실가스 변환식 및 데이터 출력
10 months ago
HyungJune Kim 6bc2fc30fb feat: 파이차트 생성 기능
10 months ago
HyungJune Kim ff95feeafc feat: 스택 영역 그래프 생성
10 months ago
HyungJune Kim 9a2bcb47bd feat: 에너지 화면 요약 데이터 출력
10 months ago
HyungJune Kim 11382dcea6 fix: 일부 디자인 수정
10 months ago
HyungJune Kim 9effacb0a5 feat: 에너지 데이터 추가 및 출력
10 months ago
HyungJune Kim e794514592 feat: Step 그래프 생성 기능 추가
10 months ago
HyungJune Kim fecca36925 feat: 산점도 그래프 생성 기능 추가
10 months ago
HyungJune Kim e4f72f965f feat: BoxPlot 생성 기능 추가
10 months ago
HyungJune Kim 41bff54c4f feat: 일반 라인 그래프 생성 추가
10 months ago
HyungJune Kim 4cbcc658b8 feat: 수조 라인차트 생성 기능 추가
10 months ago
HyungJune Kim 630570e506 feat: 그래프 설정 값 가져오기
10 months ago
HyungJune Kim 0edb2c9864 desgin: DataGrid 스타일
10 months ago
HyungJune Kim d2987be358 feat: 데이터/그래프 설정에 따라 설정 데이터 변경
10 months ago
HyungJune Kim d6391fbbc5 design: 에너지, 온실가스 화면 레이아웃
10 months ago
HyungJune Kim 3ac0255008 refactor: 그래프 종류 string -> enum
10 months ago
HyungJune Kim 11bbf7922f feat: 필터 탭 변경 시 DataGrid 변경
10 months ago
HyungJune Kim 92800c3171 feat: 수조 DataGrid 표시
10 months ago
HyungJune Kim b61a0024fc Merge "hhsun_work" to prototype
10 months ago
HyungJune Kim f3c5b5cd2c feat: DataGrid 동적 생성 기능 추가중
10 months ago
hhsung 48f2cd67e6 주석 복구
10 months ago
hhsung a94808b9ca data vo filed changed
10 months ago
HyungJune Kim aa65046922 feat: 필터 변경 시 그래프 종류 업데이트
10 months ago
HyungJune Kim ae31da2035 merge hhsung_work to prototype
10 months ago
HyungJune Kim fbcf495dce fix: field 수정
10 months ago
HyungJune Kim 49a0d14b6c design: 그래프 화면
10 months ago
hhsung 82d36c2b63 ef setting
10 months ago
HyungJune Kim 38458d2c30 feat: SegmentedControl 형태로 모니터링 필터버튼 추가
10 months ago
HyungJune Kim ea88b86186 staging
10 months ago
hhsung c007128dee EF framework 추가
10 months ago
hhsung 14a46fbf90 파일에서 임의의 vo 생성 로직 추가
10 months ago
hhsung 8a9310397b file 처리 클래스 작성
10 months ago
HyungJune Kim 2986bde22a Merge branch 'hhsung_work' into prototype
10 months ago
hhsung c52758fc50 임의의 데이터 클래스 및 샘플자료 생성함수 추가
10 months ago
HyungJune Kim acdfb8db7c fix: 그래프 화면 열고닫기 기능 MaterialDesign Drawer 방식 적용
10 months ago
HyungJune Kim 48cfad129e feat: 그래프 화면 열고닫기 기능 추가
10 months ago
HyungJune Kim 5b60525279 design: 라디오 버튼 스타일 변경
10 months ago
HyungJune Kim 99eb505d72 feat: view swap 기능 추가
11 months ago
HyungJune Kim 7daa875b05 feat: 파일 목록 시각화
11 months ago

@ -1,20 +1,26 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13 VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartAquaViewer", "SmartAquaViewer\SmartAquaViewer.csproj", "{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartAquaViewer", "SmartAquaViewer\SmartAquaViewer.csproj", "{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|Any CPU.Build.0 = Debug|Any CPU {B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|x64.ActiveCfg = Debug|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|x64.Build.0 = Debug|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.Build.0 = Release|Any CPU {B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.Build.0 = Release|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|x64.ActiveCfg = Release|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|x64.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

@ -2,8 +2,45 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SmartAquaViewer" xmlns:local="clr-namespace:SmartAquaViewer"
xmlns:view="clr-namespace:SmartAquaViewer.View"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/Generic.xaml"/>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign2.Defaults.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.DeepPurple.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Secondary/MaterialDesignColor.Lime.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Badged.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Card.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Chip.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Clock.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Flipper.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.FlipperClassic.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ColorPicker.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.RatingBar.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TimePicker.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Shadows.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.SmartHint.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Snackbar.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.AutoSuggestBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.SplitButton.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate DataType="{x:Type vm:MonitoringViewModel}">
<view:MonitoringView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:EnergyViewModel}">
<view:EnegyView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:GreenHouseGasViewModel}">
<view:GreenHouseView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:CCTVViewModel}">
<view:CCTVView/>
</DataTemplate>
</ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace SmartAquaViewer.Classes
{
public class EnumEqualsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value != null && parameter != null && value.Equals(parameter);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> (value is bool b && b) ? parameter! : Binding.DoNothing;
}
public class BoolToPowerConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b && b
? "On"
: "Off";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value as string;
}
}
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> !(value is bool b && b);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> !(value is bool b && b);
}
}

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows;
namespace SmartAquaViewer.Classes
{
public static class DataGridAutoBuilder
{
public static void BuildColumnsFromType<T>(DataGrid grid, IEnumerable<T> items,
Dictionary<string, string> headerMap = null, string dateFormat = "yyyy-MM-dd HH:mm:ss")
{
grid.Columns.Clear();
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var p in props)
{
DataGridColumn col = CreateColumnForProperty(p, headerMap, dateFormat);
if (col != null) grid.Columns.Add(col);
}
grid.ItemsSource = items;
}
private static DataGridColumn CreateColumnForProperty(PropertyInfo p,
Dictionary<string, string> headerMap, string dateFormat)
{
string header = headerMap != null && headerMap.TryGetValue(p.Name, out var h) ? h : p.Name;
var type = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType;
// bool → CheckBox
if (type == typeof(bool))
{
return new DataGridCheckBoxColumn
{
Header = header,
Binding = new Binding(p.Name) { Mode = BindingMode.TwoWay }
};
}
// enum → ComboBox(읽기/쓰기)
if (type.IsEnum)
{
return new DataGridComboBoxColumn
{
Header = header,
ItemsSource = Enum.GetValues(type),
SelectedItemBinding = new Binding(p.Name) { Mode = BindingMode.TwoWay }
};
}
// DateTime → 포맷
if (type == typeof(DateTime))
{
return new DataGridTextColumn
{
Header = header,
Binding = new Binding(p.Name)
{
Mode = BindingMode.TwoWay,
StringFormat = dateFormat
}
};
}
// 숫자 → 우측정렬
if (type == typeof(int) || type == typeof(long) ||
type == typeof(float) || type == typeof(double) || type == typeof(decimal))
{
var col = new DataGridTextColumn
{
Header = header,
Binding = new Binding(p.Name) { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged }
};
col.ElementStyle = new Style(typeof(TextBlock))
{
Setters = { new Setter(TextBlock.TextAlignmentProperty, TextAlignment.Right) }
};
return col;
}
// 기본(문자 등)
return new DataGridTextColumn
{
Header = header,
Binding = new Binding(p.Name) { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged }
};
}
}
}

@ -0,0 +1,50 @@
<UserControl x:Class="SmartAquaViewer.Controls.FFPlayerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" BorderThickness="1">
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="24"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Border Grid.RowSpan="2" x:Name="bdrNoSignalContainer">
<Border.BorderBrush>
<SolidColorBrush Color="#394861" Opacity="0.4"></SolidColorBrush>
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="#22262D" Opacity="0.4"></SolidColorBrush>
</Border.Background>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
</Grid>
</Border>
<Image x:Name="imgPlayer" Grid.RowSpan="2" Stretch="Fill"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Source="{Binding CurrentFrame}"/>
<Label HorizontalAlignment="Left" Grid.RowSpan="2" Margin="20, 5, 5, 0" x:Name="lblCCTVID"
FontFamily="Verna" FontSize="15" FontWeight='Bold' Foreground="White"
Content="{Binding CCTVInfo.DeviceId}"/>
<Grid x:Name="grdTopMenuBar" Grid.Row="0" Background="Black" Opacity="0.5" Visibility="Hidden" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" WindowChrome.IsHitTestVisibleInChrome="True" >
</Grid>
<!--<StackPanel Name="spTopMenuBar" Visibility="Hidden" Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right">
<Image Name="imgMaximize" Source="../Images/maximize.png" Width="24" Margin="1, 5, 0, 5" MouseLeftButtonUp="imgMaximize_MouseUp" Opacity="0.7" MouseEnter="imgTopmenu_MouseEnter" MouseLeave="imgTopmenu_MouseLeave"/>
</StackPanel>-->
<Grid Grid.Row="1">
<StackPanel VerticalAlignment="Bottom" Orientation="Vertical">
<Label HorizontalAlignment="Right" Margin="20, 5, 5, 0" x:Name="lblCCTVName"
FontFamily="Verna" FontSize="15" FontWeight='Bold' Foreground="White"
Content="{Binding CCTVInfo.DeviceName}"/>
</StackPanel>
</Grid>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// FFPlayerControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class FFPlayerControl : UserControl
{
public FFPlayerControl()
{
InitializeComponent();
}
}
}

@ -0,0 +1,16 @@
<UserControl x:Class="SmartAquaViewer.Controls.GraphControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
xmlns:oxy="http://oxyplot.org/wpf"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="1080">
<Border>
<Grid>
<oxy:PlotView Name="pvChart" Model="{Binding Model}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="0"/>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// GraphControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class GraphControl : UserControl
{
public GraphControl()
{
InitializeComponent();
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
}
}

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace SmartAquaViewer.Controls
{
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool>? _canExecute;
public RelayCommand(Action<T> execute, Func<T, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke((T)parameter!) ?? true;
}
public void Execute(object? parameter)
{
_execute((T)parameter!);
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}

@ -0,0 +1,31 @@
<UserControl x:Class="SmartAquaViewer.Controls.SegmentedControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid>
<StackPanel Orientation="Horizontal">
<RadioButton Name="rdbtnA" Content="수조" GroupName="SignalType" Tag="Tank"
Checked="RadioButton_Checked"
Style="{StaticResource ImageRadioButtonStyle}" Width="250" Height="50" IsChecked="True"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/SegmentedControl/select_btn_1.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/SegmentedControl/select_btn_1_press.png"/>
<RadioButton Name="rdbtnB" Content="여과시스템" GroupName="SignalType" Tag="Filter"
Checked="RadioButton_Checked"
Style="{StaticResource ImageRadioButtonStyle}" Width="250" Height="50"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/SegmentedControl/select_btn_2.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/SegmentedControl/select_btn_2_press.png"/>
<RadioButton Name="rdbtnC" Content="살균시스템" GroupName="SignalType" Tag="Sterilizer"
Checked="RadioButton_Checked"
Style="{StaticResource ImageRadioButtonStyle}" Width="250" Height="50"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/SegmentedControl/select_btn_4.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/SegmentedControl/select_btn_4_press.png"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// SegmentedControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class SegmentedControl : UserControl
{
public SegmentedControl()
{
InitializeComponent();
}
public string SelectedValue
{
get { return (string)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register(nameof(SelectedValue), typeof(string), typeof(SegmentedControl),
new PropertyMetadata(null));
public MonitorTab SelectedTab
{
get { return (MonitorTab)GetValue(SelectedTabProperty); }
set { SetValue(SelectedTabProperty, value); }
}
public static readonly DependencyProperty SelectedTabProperty =
DependencyProperty.Register(nameof(SelectedTab), typeof(MonitorTab), typeof(SegmentedControl),
new PropertyMetadata(null));
private void RadioButton_Checked(object sender, RoutedEventArgs e)
{
if (sender is RadioButton rb && rb.Tag != null)
{
SelectedTab = (MonitorTab)Enum.Parse(typeof(MonitorTab), rb.Tag.ToString());
}
}
}
}

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using SmartAquaViewer.DataAnalysis;
using System.Collections.Generic;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
namespace SmartAquaViewer.DataAnalisys
{
public class AppDbContext : DbContext
{
//dotnet tool install --global dotnet-ef
//dotnet ef migrations add InitialCreate --project SmartAquaViewer
//dotnet ef database update --project SmartAquaViewer
//dotnet ef migrations add AddMultipleTables --project SmartAquaViewer
//dotnet ef database update --project SmartAquaViewer
public DbSet<WaterQualityVO> WaterQualities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// MySQL 연결 문자열 (일반적인 형태)
var connectionString = "Server=192.168.10.143;Port=3306;Database=smart_aqua;User=root;Password=znqk123!;";
// MySQL Server Version 설정 (버전에 맞춰 변경)
var serverVersion = new MySqlServerVersion(new Version(8, 0, 36));
optionsBuilder.UseMySql(connectionString, serverVersion);
}
}
}

@ -0,0 +1,106 @@
using SmartAquaViewer.DataAnalysis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.DataAnalisys
{
internal class AquarDataControl
{
private AQUA_DATA_CONTROL_STATE state = AQUA_DATA_CONTROL_STATE.IDLE;
private readonly IWaterQuality iwaterQuality;
private CancellationTokenSource? cts;
AppDbContext db = new AppDbContext();
public AquarDataControl(IWaterQuality iwaterQuality)
{
this.iwaterQuality = iwaterQuality;
}
public AQUA_DATA_CONTROL_STATE GetState() => state;
/// <summary>
/// 파일 파싱 시작
/// </summary>
public void ParseFile(string filePath)
{
// --- 동기 처리 구간 ---
if (state != AQUA_DATA_CONTROL_STATE.IDLE &&
state != AQUA_DATA_CONTROL_STATE.PROCESS_COMPLETED)
{
iwaterQuality.OnError("illegal state error : " + state);
return;
}
state = AQUA_DATA_CONTROL_STATE.PROCESSING;
cts = new CancellationTokenSource();
// --- 비동기 처리 구간 ---
Task.Run(async () =>
{
try
{
var fileInfo = new FileInfo(filePath);
long totalBytes = fileInfo.Length;
long processedBytes = 0;
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (var reader = new StreamReader(fs))
{
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
cts.Token.ThrowIfCancellationRequested();
processedBytes += Encoding.UTF8.GetByteCount(line + Environment.NewLine);
double percent = (double)processedBytes / totalBytes * 100.0;
// TODO: 라인 파싱 로직
WaterQualityVO vo = new();
//vo.PH = 10.5;
vo.RecordedTime = DateTime.Now;
iwaterQuality.OnParsed(vo);
db.Add(vo);
iwaterQuality.OnProgress(percent);
}
}
state = AQUA_DATA_CONTROL_STATE.PROCESS_COMPLETED;
iwaterQuality.OnCompleted();
}
catch (OperationCanceledException)
{
state = AQUA_DATA_CONTROL_STATE.CANCELLED;
iwaterQuality.OnCancelled();
}
catch (Exception ex)
{
state = AQUA_DATA_CONTROL_STATE.ERROR;
iwaterQuality.OnError("파일 처리 중 오류 발생: " + ex.Message);
}
});
}
/// <summary>
/// 처리 중단 요청
/// </summary>
public void Cancel()
{
if (state == AQUA_DATA_CONTROL_STATE.PROCESSING && cts != null)
{
cts.Cancel();
}
}
}
}

@ -0,0 +1,29 @@
using SmartAquaViewer.DataAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.DataAnalisys
{
internal enum AQUA_DATA_CONTROL_STATE
{
IDLE,
PROCESSING,
PROCESS_COMPLETED,
CANCELLED,
ERROR
}
internal interface IWaterQuality
{
void OnParsed(WaterQualityVO vo);
void OnProgress(double percent);
void OnError(string message);
void OnCompleted();
void OnCancelled();
}
}

@ -0,0 +1,497 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace SmartAquaViewer.DataAnalysis
{
[Table("water_quality")]
public class WaterQualityVO
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// 측정 시각
/// </summary>
[Column("recorded_time")]
public DateTime RecordedTime { get; set; }
/// <summary>
/// 저수조
/// </summary>
public WaterTank Tank { get; set; } = new();
/// <summary>
/// 여과 시스템
/// </summary>
public FilteringSystem Filtering { get; set; } = new();
/// <summary>
/// 살균 시스템
/// </summary>
public SterilizingSystem Sterilizing { get; set; } = new();
/// <summary>
/// 총 전력 소비 (킬로와트, kW)
/// </summary>
public double TotalEnergy { get; set; }
/// <summary>
/// 총 온실가스 배출량 (톤, tCO₂)
/// </summary>
public double TotalGreenhouseGas { get; set; }
public WaterQualityVO() { }
public WaterQualityVO(DateTime RecordedTime, WaterTank tank, FilteringSystem filtering, SterilizingSystem sterilizing)
{
this.RecordedTime = RecordedTime;
Tank = tank;
Filtering = filtering;
Sterilizing = sterilizing;
}
/// <summary>
/// 샘플 데이터 리스트 생성
/// </summary>
public static List<WaterQualityVO> GetSampleData(DateTime start, DateTime end, int totalRowsCount)
{
var list = new List<WaterQualityVO>();
var rand = new Random();
double CalculateTotalEnergy(WaterQualityVO vo)
{
return vo.Filtering.SandFilterEnergy +
vo.Filtering.CirculationPumpEnergy +
vo.Filtering.HeatPumpEnergy +
vo.Filtering.AirBlowerEnergy +
vo.Sterilizing.OzoneGeneratorEnergy +
vo.Sterilizing.UVSterilizerEnergy +
vo.Sterilizing.OzoneDissolverEnergy +
vo.Sterilizing.ExcessOzoneDestroyerEnergy;
}
double ConvertEnergyToGHG(double energy)
{
// 2024년 배출계수 (kgCO₂/kWh)
const double EmissionFactor = 0.4747;
return (energy * EmissionFactor); // / 1000.0; // tCO₂
}
double CalculateTotalGreenhouseGas(WaterQualityVO vo)
{
return vo.Filtering.AirBlowerGreenhouseGas +
vo.Filtering.CirculationPumpGreenhouseGas +
vo.Filtering.HeatPumpGreenhouseGas +
vo.Filtering.SandFilterGreenhouseGas +
vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas +
vo.Sterilizing.OzoneDissolverGreenhouseGas +
vo.Sterilizing.OzoneGeneratorGreenhouseGas +
vo.Sterilizing.UVSterilizerGreenhouseGas;
}
if (totalRowsCount <= 0) return list;
double totalSeconds;
if (start == end)
{
totalSeconds = 24 * 60 * 60;
}
else
{
totalSeconds = (end - start).TotalSeconds;
if (totalSeconds < 0) throw new ArgumentException("end 날짜는 start 날짜보다 같거나 커야 합니다.");
}
double stepSeconds = totalSeconds / totalRowsCount;
for (int i = 0; i < totalRowsCount; i++)
{
DateTime ts = start.AddSeconds(stepSeconds * i);
var vo = new WaterQualityVO
{
RecordedTime = ts,
Tank = new WaterTank(
number: rand.Next(1, 4),
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),
sandFilterGreenhouseGas: 0, // 추후 계산
sumpPH: Math.Round(rand.NextDouble() * 2 + 6, 2),
sumpORP: Math.Round(rand.NextDouble() * 200 + 100, 2),
sumpWaterLevel: Math.Round(rand.NextDouble() * 2 + 1, 2),
sumpFlowRate: Math.Round(rand.NextDouble() * 5 + 1, 2),
sumpTemperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
circulationPumpPower: rand.Next(0, 2) == 1,
circulationPumpEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
circulationPumpGreenhouseGas: 0, // 추후 계산
inverterControllerStatus: rand.Next(0, 2) == 1 ? "Normal" : "Error",
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2),
heatPumpPower: rand.Next(0, 2) == 1,
heatPumpTemperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
heatPumpEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
heatPumpGreenhouseGas: 0, // 추후 계산
airBlowerPower: rand.Next(0, 2) == 1,
airBlowerEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
airBlowerGreenhouseGas: 0 // 추후 계산
),
Sterilizing = new SterilizingSystem(
ozoneGeneratorPower: rand.Next(0, 2) == 1,
ozoneGeneratorEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
ozoneGeneratorGreenhouseGas: 0, // 추후 계산
uvSterilizerId: "UV" + rand.Next(1, 100),
uvSterilizerPower: rand.Next(0, 2) == 1,
uvSterilizerEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
uvSterilizerGreenhouseGas: 0, // 추후 계산
ozoneDissolverPower: rand.Next(0, 2) == 1,
ozoneDissolverPressure: Math.Round(rand.NextDouble() * 100 + 50, 2),
ozoneDissolverEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
ozoneDissolverGreenhouseGas: 0, // 추후 계산
excessOzoneDestroyerPower: rand.Next(0, 2) == 1,
excessOzoneDestroyerEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
excessOzoneDestroyerGreenhouseGas: 0 // 추후 계산
),
};
vo.TotalEnergy = CalculateTotalEnergy(vo);
vo.Filtering.SandFilterGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.SandFilterEnergy);
vo.Filtering.CirculationPumpGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.CirculationPumpEnergy);
vo.Filtering.HeatPumpGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.HeatPumpEnergy);
vo.Filtering.AirBlowerGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.AirBlowerEnergy);
vo.Sterilizing.OzoneGeneratorGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.OzoneGeneratorEnergy);
vo.Sterilizing.UVSterilizerGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.UVSterilizerEnergy);
vo.Sterilizing.OzoneDissolverGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.OzoneDissolverEnergy);
vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.ExcessOzoneDestroyerEnergy);
vo.TotalGreenhouseGas = CalculateTotalGreenhouseGas(vo);
list.Add(vo);
}
return list;
}
}
[Owned]
public class WaterTank
{
/// <summary>
/// 저수조 번호
/// </summary>
[Column("tank_number")]
public int Number { get; set; }
/// <summary>
/// Dissolved Oxygen (mg/L)
/// </summary>
[Column("tank_do_value")]
public double DOValue { get; set; }
/// <summary>
/// pH (산도)
/// </summary>
[Column("tank_ph")]
public double PH { get; set; }
/// <summary>
/// 산화환원전위 (mV)
/// </summary>
[Column("tank_orp")]
public double ORP { get; set; }
/// <summary>
/// 수온 (°C)
/// </summary>
[Column("tank_temperature")]
public double Temperature { get; set; }
/// <summary>
/// 유량 (m³/s)
/// </summary>
[Column("tank_flow_rate")]
public double FlowRate { get; set; }
public WaterTank() { }
public WaterTank(int number, double doValue, double ph, double orp, double temperature, double flowRate)
{
Number = number;
DOValue = doValue;
PH = ph;
ORP = orp;
Temperature = temperature;
FlowRate = flowRate;
}
}
[Owned]
public class FilteringSystem
{
/// <summary>
/// 모래여과기 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_sand_filter_power")]
public bool SandFilterPower { get; set; }
/// <summary>
/// 모래여과기 전력 (kW)
/// </summary>
[Column("filter_sand_filter_energy")]
public double SandFilterEnergy { get; set; }
/// <summary>
/// 모래여과기 온실가스 (tCO₂)
/// </summary>
[Column("filter_sand_filter_greenhouse_gas")]
public double SandFilterGreenhouseGas { get; set; }
/// <summary>
/// 섬프탱크 pH (산도)
/// </summary>
[Column("filter_sump_ph")]
public double SumpPH { get; set; }
/// <summary>
/// 섬프탱크 산화환원전위 (mV)
/// </summary>
[Column("filter_sump_orp")]
public double SumpORP { get; set; }
/// <summary>
/// 섬프탱크 수위 (m)
/// </summary>
[Column("filter_sump_water_level")]
public double SumpWaterLevel { get; set; }
/// <summary>
/// 섬프탱크 유량 (m³/s)
/// </summary>
[Column("filter_sump_flow_rate")]
public double SumpFlowRate { get; set; }
/// <summary>
/// 섬프탱크 수온 (°C)
/// </summary>
[Column("filter_sump_temperature")]
public double SumpTemperature { get; set; }
/// <summary>
/// 순환펌프 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_circulation_pump_power")]
public bool CirculationPumpPower { get; set; }
/// <summary>
/// 순환펌프 전력 (kW)
/// </summary>
[Column("filter_circulation_pump_energy")]
public double CirculationPumpEnergy { get; set; }
/// <summary>
/// 순환펌프 온실가스 (tCO₂)
/// </summary>
[Column("filter_circulation_pump_greenhouse_gas")]
public double CirculationPumpGreenhouseGas { get; set; }
/// <summary>
/// 인버터 제어기 상태
/// </summary>
[Column("filter_inverter_controller_status")]
public string? InverterControllerStatus { get; set; }
/// <summary>
/// 순환펌프 유량 (m³/s)
/// </summary>
[Column("filter_flow_rate")]
public double FlowRate { get; set; }
/// <summary>
/// 히트펌프 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_heat_pump_power")]
public bool HeatPumpPower { get; set; }
/// <summary>
/// 히트펌프 온도 (°C)
/// </summary>
[Column("filter_heat_pump_temperature")]
public double HeatPumpTemperature { get; set; }
/// <summary>
/// 히트펌프 전력 (kW)
/// </summary>
[Column("filter_heat_pump_energy")]
public double HeatPumpEnergy { get; set; }
/// <summary>
/// 히트펌프 온실가스 (tCO₂)
/// </summary>
[Column("filter_heat_pump_greenhouse_gas")]
public double HeatPumpGreenhouseGas { get; set; }
/// <summary>
/// 에어브로와 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_air_blower_power")]
public bool AirBlowerPower { get; set; }
/// <summary>
/// 에어브로와 전력 (kW)
/// </summary>
[Column("filter_air_blower_energy")]
public double AirBlowerEnergy { get; set; }
/// <summary>
/// 에어브로와 온실가스 (tCO₂)
/// </summary>
[Column("filter_air_blower_greenhouse_gas")]
public double AirBlowerGreenhouseGas { get; set; }
public FilteringSystem() { }
public FilteringSystem(
bool sandFilterPower, double sandFilterEnergy, double sandFilterGreenhouseGas,
double sumpPH, double sumpORP, double sumpWaterLevel, double sumpFlowRate, double sumpTemperature,
bool circulationPumpPower, double circulationPumpEnergy, double circulationPumpGreenhouseGas, string? inverterControllerStatus, double flowRate,
bool heatPumpPower, double heatPumpTemperature, double heatPumpEnergy, double heatPumpGreenhouseGas,
bool airBlowerPower, double airBlowerEnergy, double airBlowerGreenhouseGas)
{
SandFilterPower = sandFilterPower;
SandFilterEnergy = sandFilterEnergy;
SandFilterGreenhouseGas = sandFilterGreenhouseGas;
SumpPH = sumpPH;
SumpORP = sumpORP;
SumpWaterLevel = sumpWaterLevel;
SumpFlowRate = sumpFlowRate;
SumpTemperature = sumpTemperature;
CirculationPumpPower = circulationPumpPower;
CirculationPumpEnergy = circulationPumpEnergy;
CirculationPumpGreenhouseGas = circulationPumpGreenhouseGas;
InverterControllerStatus = inverterControllerStatus;
FlowRate = flowRate;
HeatPumpPower = heatPumpPower;
HeatPumpTemperature = heatPumpTemperature;
HeatPumpEnergy = heatPumpEnergy;
HeatPumpGreenhouseGas = heatPumpGreenhouseGas;
AirBlowerPower = airBlowerPower;
AirBlowerEnergy = airBlowerEnergy;
AirBlowerGreenhouseGas = airBlowerGreenhouseGas;
}
}
[Owned]
public class SterilizingSystem
{
/// <summary>
/// 오존 발생기 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_ozone_generator_power")]
public bool OzoneGeneratorPower { get; set; }
/// <summary>
/// 오존 발생기 전력 (kW)
/// </summary>
[Column("ster_ozone_generator_energy")]
public double OzoneGeneratorEnergy { get; set; }
/// <summary>
/// 오존 발생기 온실가스 (tCO₂)
/// </summary>
[Column("ster_ozone_generator_greenhouse_gas")]
public double OzoneGeneratorGreenhouseGas { get; set; }
/// <summary>
/// 자외선 살균기 ID
/// </summary>
[Column("ster_uv_sterilizer_id")]
public string UVSterilizerId { get; set; }
/// <summary>
/// 자외선 살균기 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_uv_sterilizer_power")]
public bool UVSterilizerPower { get; set; }
/// <summary>
/// 자외선 살균기 전력 (kW)
/// </summary>
[Column("ster_uv_sterilizer_energy")]
public double UVSterilizerEnergy { get; set; }
/// <summary>
/// 자외선 살균기 온실가스 (tCO₂)
/// </summary>
[Column("ster_uv_sterilizer_greenhouse_gas")]
public double UVSterilizerGreenhouseGas { get; set; }
/// <summary>
/// 오존용해장치 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_ozone_dissolver_power")]
public bool OzoneDissolverPower { get; set; }
/// <summary>
/// 오존용해장치 압력 (kPa)
/// </summary>
[Column("ster_ozone_dissolver_pressure")]
public double OzoneDissolverPressure { get; set; }
/// <summary>
/// 오존용해장치 전력 (kW)
/// </summary>
[Column("ster_ozone_dissolver_energy")]
public double OzoneDissolverEnergy { get; set; }
/// <summary>
/// 오존용해장치 온실가스 (tCO₂)
/// </summary>
[Column("ster_ozone_dissolver_greenhouse_gas")]
public double OzoneDissolverGreenhouseGas { get; set; }
/// <summary>
/// 배오존장치 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_excess_ozone_destroyer_power")]
public bool ExcessOzoneDestroyerPower { get; set; }
/// <summary>
/// 배오존장치 전력 (kW)
/// </summary>
[Column("ster_excess_ozone_destroyer_energy")]
public double ExcessOzoneDestroyerEnergy { get; set; }
/// <summary>
/// 배오존장치 온실가스 (tCO₂)
/// </summary>
[Column("ster_excess_ozone_destroyer_greenhouse_gas")]
public double ExcessOzoneDestroyerGreenhouseGas { get; set; }
public SterilizingSystem() { }
public SterilizingSystem(
bool ozoneGeneratorPower, double ozoneGeneratorEnergy, double ozoneGeneratorGreenhouseGas,
string uvSterilizerId, bool uvSterilizerPower, double uvSterilizerEnergy, double uvSterilizerGreenhouseGas,
bool ozoneDissolverPower, double ozoneDissolverPressure, double ozoneDissolverEnergy, double ozoneDissolverGreenhouseGas,
bool excessOzoneDestroyerPower, double excessOzoneDestroyerEnergy, double excessOzoneDestroyerGreenhouseGas)
{
OzoneGeneratorPower = ozoneGeneratorPower;
OzoneGeneratorEnergy = ozoneGeneratorEnergy;
OzoneGeneratorGreenhouseGas = ozoneGeneratorGreenhouseGas;
UVSterilizerId = uvSterilizerId;
UVSterilizerPower = uvSterilizerPower;
UVSterilizerEnergy = uvSterilizerEnergy;
UVSterilizerGreenhouseGas = uvSterilizerGreenhouseGas;
OzoneDissolverPower = ozoneDissolverPower;
OzoneDissolverPressure = ozoneDissolverPressure;
OzoneDissolverEnergy = ozoneDissolverEnergy;
OzoneDissolverGreenhouseGas = ozoneDissolverGreenhouseGas;
ExcessOzoneDestroyerPower = excessOzoneDestroyerPower;
ExcessOzoneDestroyerEnergy = excessOzoneDestroyerEnergy;
ExcessOzoneDestroyerGreenhouseGas = excessOzoneDestroyerGreenhouseGas;
}
}
}

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SmartAquaViewer.Helper
{
public static class ComboBoxHelper
{
public static readonly DependencyProperty SelectFirstOnItemsChangeProperty =
DependencyProperty.RegisterAttached(
"SelectFirstOnItemsChange", typeof(bool), typeof(ComboBoxHelper),
new PropertyMetadata(false, OnSelectFirstChanged));
public static void SetSelectFirstOnItemsChange(DependencyObject obj, bool value)
=> obj.SetValue(SelectFirstOnItemsChangeProperty, value);
public static bool GetSelectFirstOnItemsChange(DependencyObject obj)
=> (bool)obj.GetValue(SelectFirstOnItemsChangeProperty);
private static void OnSelectFirstChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ComboBox cb) return;
void TrySelectFirst()
{
if (cb.Items.Count > 0 && cb.SelectedIndex < 0)
cb.SelectedIndex = 0;
}
cb.Loaded += (_, __) => TrySelectFirst();
void Hook()
{
if (cb.ItemsSource is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= OnItemsChanged;
incc.CollectionChanged += OnItemsChanged;
}
}
void OnItemsChanged(object? s, NotifyCollectionChangedEventArgs args) => TrySelectFirst();
// ItemsSource가 바뀌어도 다시 훅
cb.DataContextChanged += (_, __) => Hook();
Hook();
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
namespace SmartAquaViewer.Helper.FFHelper
{
internal static class Helper
{
public static unsafe string av_strerror(int error)
{
var bufferSize = 1024;
var buffer = stackalloc byte[bufferSize];
ffmpeg.av_strerror(error, buffer, (ulong)bufferSize);
var message = Marshal.PtrToStringAnsi((IntPtr)buffer);
return message;
}
public static int ThrowExceptionIfError(this int error)
{
if (error < 0) throw new ApplicationException(av_strerror(error));
return error;
}
}
}

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
using System.Windows;
namespace SmartAquaViewer.Helper.FFHelper
{
public sealed unsafe class StreamDecoder : IDisposable
{
public delegate void ThreadStoppedHandler(object obj);
public static event ThreadStoppedHandler hThreadStoppedHandler;
private readonly AVFormatContext* _pFormatContext;
public readonly AVFrame* _pFrame;
private readonly AVPacket* _pPacket;
AVCodec* codec;
public string CodecName { get; }
public Size FrameSize { get; }
public AVPixelFormat PixelFormat { get; }
public int videoStreamIndex { get; }
public AVCodecContext* _vcodecContext { get; }
public bool _isContextNotInitialized = false;
public double _fps;
public StreamDecoder(string url)
{
//Application.Current.Dispatcher.Invoke(() =>
//{
// ProgressWindow.GetInstance().Show();
//});
lock (this)
{
AVFormatContext* pFormatContext = null;
AVCodecContext* vcodecContext = null;
AVDictionary* options = null;
AVStream* videoStream = null;
try
{
ffmpeg.av_log_set_level(ffmpeg.AV_LOG_DEBUG);
_pFormatContext = ffmpeg.avformat_alloc_context();
if (_pFormatContext == null)
{
throw new InvalidOperationException("Failed to allocate format context.");
}
pFormatContext = _pFormatContext;
// Get the codec context
_vcodecContext = ffmpeg.avcodec_alloc_context3(codec);
if (_vcodecContext == null)
{
// Out of memory
ffmpeg.avformat_close_input(&pFormatContext);
}
ffmpeg.av_dict_set(&options, "stimeout", "600000000", 0); // 60 seconds timeout
ffmpeg.av_dict_set(&options, "reconnect", "1", 0);
ffmpeg.av_dict_set(&options, "reconnect_at_eof", "1", 0);
ffmpeg.av_dict_set(&options, "reconnect_streamed", "1", 0);
ffmpeg.av_dict_set(&options, "rtsp_transport", "tcp", 0);
int ret = ffmpeg.avformat_open_input(&pFormatContext, url, null, &options).ThrowExceptionIfError();
if (options != null)
{
ffmpeg.av_dict_free(&options);
}
ret = ffmpeg.avformat_find_stream_info(_pFormatContext, null).ThrowExceptionIfError();
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
if (pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
videoStream = pFormatContext->streams[i];
break;
}
}
if (videoStream == null)
{
Console.WriteLine("Failed to find video stream");
return;
}
AVRational frameRate = videoStream->avg_frame_rate;
_fps = (double)frameRate.num / frameRate.den;
videoStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);
int context = ffmpeg.avcodec_parameters_to_context(_vcodecContext, _pFormatContext->streams[videoStreamIndex]->codecpar).ThrowExceptionIfError();
if (context < 0)
{
//Log4NetManager.GetLog().Error("StreamDecoder() : avcodec_parameters_to_context Error");
return;
}
if (videoStreamIndex >= 0)
{
AVCodecContext* avctx = OpenStream(_vcodecContext);
if (avctx == null)
{
throw new InvalidOperationException("Failed to open codec context.");
}
FrameSize = new Size(avctx->width, avctx->height);
PixelFormat = avctx->pix_fmt;
}
else
{
throw new InvalidOperationException("No video stream found.");
}
_pPacket = ffmpeg.av_packet_alloc();
if (_pPacket == null)
{
throw new InvalidOperationException("Failed to allocate packet.");
}
_pFrame = ffmpeg.av_frame_alloc();
if (_pFrame == null)
{
throw new InvalidOperationException("Failed to allocate frame.");
}
}
catch (Exception ex)
{
_isContextNotInitialized = true;
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
MessageBox.Show("StreamDecoder() : " + ex.Message);
if (_pFrame != null)
{
ffmpeg.av_frame_unref(_pFrame);
ffmpeg.av_free(_pFrame);
}
if (_pPacket != null)
{
ffmpeg.av_packet_unref(_pPacket);
ffmpeg.av_free(_pPacket);
}
if (_vcodecContext != null)
{
vcodecContext = _vcodecContext;
ffmpeg.avcodec_close(vcodecContext);
ffmpeg.avcodec_free_context(&vcodecContext);
}
if (_pFormatContext != null)
{
ffmpeg.avformat_close_input(&pFormatContext);
ffmpeg.avformat_free_context(pFormatContext);
}
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
}
finally
{
if (options != null)
{
ffmpeg.av_dict_free(&options);
}
//Application.Current.Dispatcher.Invoke(() =>
//{
// ProgressWindow.GetInstance().Close();
//});
}
}
}
private AVCodecContext* OpenStream(AVCodecContext* avctx)
{
AVCodec* codec = ffmpeg.avcodec_find_decoder(avctx->codec_id);
if (codec == null) throw new InvalidOperationException("No codec could be found.");
avctx->codec_id = codec->id;
avctx->lowres = 0;
if (avctx->lowres > codec->max_lowres)
avctx->lowres = codec->max_lowres;
avctx->idct_algo = ffmpeg.FF_IDCT_AUTO;
avctx->error_concealment = 3;
ffmpeg.avcodec_open2(avctx, codec, null).ThrowExceptionIfError();
return avctx;
}
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
//public void Dispose()
//{
// ffmpeg.av_frame_unref(_pFrame);
// ffmpeg.av_free(_pFrame);
// ffmpeg.av_packet_unref(_pPacket);
// ffmpeg.av_free(_pPacket);
// ffmpeg.avcodec_close(_vcodecContext);
// //ffmpeg.avcodec_close(acodecContext);
// var pFormatContext = _pFormatContext;
// ffmpeg.avformat_close_input(&pFormatContext);
//}
public void Dispose()
{
lock (this)
{
if (_pFrame != null)
{
ffmpeg.av_frame_unref(_pFrame);
ffmpeg.av_free(_pFrame);
}
if (_pPacket != null)
{
ffmpeg.av_packet_unref(_pPacket);
ffmpeg.av_free(_pPacket);
}
if (_vcodecContext != null && !_isContextNotInitialized)
{
ffmpeg.avcodec_close(_vcodecContext);
}
//ffmpeg.avcodec_close(acodecContext);
if (_pFormatContext != null && !_isContextNotInitialized)
{
var pFormatContext = _pFormatContext;
ffmpeg.avformat_close_input(&pFormatContext);
ffmpeg.avformat_free_context(pFormatContext);
}
}
}
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
//public bool TryDecodeNextFrame(out AVFrame frame)
//{
// ffmpeg.av_frame_unref(_pFrame);
// int error = 0;
// do
// {
// try
// {
// do
// {
// error = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
// if (error == ffmpeg.AVERROR_EOF)
// {
// frame = *_pFrame;
// return false;
// }
// error.ThrowExceptionIfError();
// } while (_pPacket->stream_index != videoStreamIndex);
// ffmpeg.avcodec_send_packet(_vcodecContext, _pPacket).ThrowExceptionIfError();
// }
// catch (Exception ex)
// {
// Log4NetManager.GetLog().Error("TryDecodeNextFrame() : " + ex.Message);
// MessageBox.Show("TryDecodeNextFrame() : " + ex.Message);
// }
// finally
// {
// ffmpeg.av_packet_unref(_pPacket);
// }
// error = ffmpeg.avcodec_receive_frame(_vcodecContext, _pFrame);
// } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));
// error.ThrowExceptionIfError();
// frame = *_pFrame;
// return true;
//}
public bool TryDecodeNextFrame(out AVFrame frame)
{
ffmpeg.av_frame_unref(this._pFrame);
int error;
// repeated-try avcodec_receive_frame until enough packets haven been send
while (true)
{
// try read frame; maybe last avcodec_send_packet resulted in multiple frames to be read; will respond with EAGAIN if more packets are needed
error = ffmpeg.avcodec_receive_frame(this._vcodecContext, this._pFrame);
if (error != ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
frame = *this._pFrame;
if (error == ffmpeg.AVERROR_EOF)
return false;
error.ThrowExceptionIfError();
return true;
}
try
{
// feed all stream-matching packets to decoder
while (true)
{
error = ffmpeg.av_read_frame(this._pFormatContext, this._pPacket);
if (error == ffmpeg.AVERROR_EOF)
{
// no more packets to read -> trigger draining/flushing of remaining frames
ffmpeg.avcodec_send_packet(this._vcodecContext, null).ThrowExceptionIfError();
break; // don't throw error, just exit packet-feed-loop
}
error.ThrowExceptionIfError();
if (this._pPacket->stream_index == this.videoStreamIndex)
{
var sendPacketResult = ffmpeg.avcodec_send_packet(this._vcodecContext, this._pPacket);
if (sendPacketResult == 0)
break;
// no reason to abort/crash (it was just an invalid packet from demuxer) -> just retrieve and feed next packet; most of time the codec will resume without problems
var errorMsg = Helper.av_strerror(sendPacketResult);
Trace.TraceError(errorMsg);
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : av_strerror 오류 " + errorMsg);
}
// unref all packets, no matter if sent to avcodec or ignored
ffmpeg.av_packet_unref(this._pPacket);
}
}
catch (Exception ex)
{
Debug.WriteLine("TryDecodeNextFrame()" + ex.Message);
if (ex.Message.Equals("End of file"))
{
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : " + ex.Message);
}
//쓰레드가 망함,
}
finally
{
ffmpeg.av_packet_unref(this._pPacket);
}
}
}
public IReadOnlyDictionary<string, string> GetContextInfo()
{
AVDictionaryEntry* tag = null;
var result = new Dictionary<string, string>();
while ((tag = ffmpeg.av_dict_get(_pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
{
var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
result.Add(key, value);
}
return result;
}
}
}

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using FFmpeg.AutoGen;
namespace SmartAquaViewer.Helper.FFHelper
{
public sealed unsafe class VideoFrameConverter : IDisposable
{
private readonly IntPtr _convertedFrameBufferPtr;
private readonly System.Windows.Size _destinationSize;
private readonly byte_ptrArray4 _dstData;
private readonly int_array4 _dstLinesize;
private readonly SwsContext* _pConvertContext;
private bool _disposed = false;
public VideoFrameConverter(System.Windows.Size sourceSize, AVPixelFormat sourcePixelFormat,
System.Windows.Size destinationSize, AVPixelFormat destinationPixelFormat)
{
try
{
_destinationSize = destinationSize;
_pConvertContext = ffmpeg.sws_getContext(
(int)sourceSize.Width,
(int)sourceSize.Height,
sourcePixelFormat,
(int)destinationSize.Width,
(int)destinationSize.Height,
destinationPixelFormat,
ffmpeg.SWS_FAST_BILINEAR, null, null, null);
if (_pConvertContext == null)
throw new ApplicationException("Could not initialize the conversion context.");
var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixelFormat,
(int)destinationSize.Width, (int)destinationSize.Height, 1);
_convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
_dstData = new byte_ptrArray4();
_dstLinesize = new int_array4();
ffmpeg.av_image_fill_arrays(
ref _dstData,
ref _dstLinesize,
(byte*)_convertedFrameBufferPtr,
destinationPixelFormat,
(int)destinationSize.Width,
(int)destinationSize.Height, 1);
}
catch (Exception ex)
{
Debug.WriteLine("VideoFrameConverter() : " + ex.Message);
}
}
public void Dispose()
{
if (!_disposed)
{
if (_convertedFrameBufferPtr != IntPtr.Zero)
Marshal.FreeHGlobal(_convertedFrameBufferPtr);
if (_pConvertContext != null)
ffmpeg.sws_freeContext(_pConvertContext);
_disposed = true;
}
}
public AVFrame Convert(AVFrame sourceFrame)
{
ffmpeg.sws_scale(_pConvertContext,
sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, _dstData, _dstLinesize);
var data = new byte_ptrArray8();
data.UpdateFrom(_dstData);
var linesize = new int_array8();
linesize.UpdateFrom(_dstLinesize);
return new AVFrame
{
data = data,
linesize = linesize,
width = (int)_destinationSize.Width,
height = (int)_destinationSize.Height
};
}
public unsafe Bitmap DeepCopyFrame(AVFrame sourceFrame)
{
if (_disposed)
throw new ObjectDisposedException(nameof(VideoFrameConverter));
AVFrame* dstFramePtr = ffmpeg.av_frame_alloc();
if (dstFramePtr == null)
{
throw new Exception("Failed to allocate destination frame");
}
try
{
dstFramePtr->format = (int)AVPixelFormat.AV_PIX_FMT_BGR24;
dstFramePtr->width = sourceFrame.width;
dstFramePtr->height = sourceFrame.height;
int ret = ffmpeg.av_frame_get_buffer(dstFramePtr, 32);
if (ret < 0)
{
throw new Exception($"Failed to allocate buffer for destination frame: {Helper.av_strerror(ret)}");
}
ret = ffmpeg.av_frame_make_writable(dstFramePtr);
if (ret < 0)
{
throw new Exception($"Failed to make frame writable: {Helper.av_strerror(ret)}");
}
ffmpeg.sws_scale(_pConvertContext,
sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, dstFramePtr->data, dstFramePtr->linesize);
ret = ffmpeg.av_frame_copy_props(dstFramePtr, &sourceFrame);
if (ret < 0)
{
throw new Exception($"Failed to copy frame properties: {Helper.av_strerror(ret)}");
}
dstFramePtr->pts = sourceFrame.pts;
dstFramePtr->pkt_dts = sourceFrame.pkt_dts;
dstFramePtr->best_effort_timestamp = sourceFrame.best_effort_timestamp;
var dstDataArr = new byte_ptrArray8();
dstDataArr.UpdateFrom(dstFramePtr->data);
var dstLinesizeArr = new int_array8();
dstLinesizeArr.UpdateFrom(dstFramePtr->linesize);
AVFrame frame = new AVFrame
{
data = dstDataArr,
linesize = dstLinesizeArr,
width = dstFramePtr->width,
height = dstFramePtr->height,
format = (int)AVPixelFormat.AV_PIX_FMT_BGR24,
pts = dstFramePtr->pts,
pkt_dts = dstFramePtr->pkt_dts,
best_effort_timestamp = dstFramePtr->best_effort_timestamp
};
Bitmap bitmap = new Bitmap(
dstFramePtr->width,
dstFramePtr->height,
dstFramePtr->linesize[0],
System.Drawing.Imaging.PixelFormat.Format24bppRgb,
(IntPtr)dstFramePtr->data[0]
);
// Bitmap의 데이터를 따로 복사
Bitmap finalBitmap = new Bitmap(bitmap);
bitmap.Dispose(); // 중간 비트맵 해제
return finalBitmap; // 데이터 복사된 최종 비트맵 리턴
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error($"Exception during frame copy: {ex.Message}");
throw;
}
finally
{
// 포인터 유효성 검사 후 안전하게 해제
if (dstFramePtr != null)
{
ffmpeg.av_frame_unref(dstFramePtr); // 참조 해제
ffmpeg.av_frame_free(&dstFramePtr); // 메모리 해제
}
}
}
}
}

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows;
namespace SmartAquaViewer.Helper
{
public static class ImageButtonHelper
{
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.RegisterAttached(
"ImageSource",
typeof(ImageSource),
typeof(ImageButtonHelper),
new PropertyMetadata(null));
public static void SetImageSource(UIElement element, ImageSource value)
{
element.SetValue(ImageSourceProperty, value);
}
public static ImageSource GetImageSource(UIElement element)
{
return (ImageSource)element.GetValue(ImageSourceProperty);
}
}
}

@ -0,0 +1,150 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows;
using System.ComponentModel;
namespace SmartAquaViewer.Helper
{
public static class MultiSelectBehavior
{
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached(
"SelectedItems", typeof(IList), typeof(MultiSelectBehavior),
new PropertyMetadata(null, OnSelectedItemsChanged));
public static void SetSelectedItems(DependencyObject element, IList? value)
=> element.SetValue(SelectedItemsProperty, value);
public static IList? GetSelectedItems(DependencyObject element)
=> (IList?)element.GetValue(SelectedItemsProperty);
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListBox lb) return;
lb.SelectionChanged -= Lb_SelectionChanged;
lb.SelectionChanged += Lb_SelectionChanged;
if (e.NewValue is IList target)
{
target.Clear();
foreach (var item in lb.SelectedItems) target.Add(item);
}
}
private static void Lb_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (sender is not ListBox lb) return;
var list = GetSelectedItems(lb);
if (list != null)
{
foreach (var removed in e.RemovedItems) list.Remove(removed);
foreach (var added in e.AddedItems) list.Add(added);
}
// Dictionary 동기화도 같이 수행
var dict = GetSelectedDictionary(lb);
if (dict != null)
{
string? keyPath = GetKeyPath(lb);
string? valuePath = GetValuePath(lb);
foreach (var removed in e.RemovedItems)
{
var key = ResolvePath(removed, keyPath);
if (key != null && dict.Contains(key)) dict.Remove(key);
}
foreach (var added in e.AddedItems)
{
var key = ResolvePath(added, keyPath);
var value = ResolvePath(added, valuePath);
if (key != null) dict[key] = value;
}
}
}
// ===== IDictionary 버전 =====
public static readonly DependencyProperty SelectedDictionaryProperty =
DependencyProperty.RegisterAttached(
"SelectedDictionary", typeof(IDictionary), typeof(MultiSelectBehavior),
new PropertyMetadata(null, OnSelectedDictionaryChanged));
public static void SetSelectedDictionary(DependencyObject element, IDictionary? value)
=> element.SetValue(SelectedDictionaryProperty, value);
public static IDictionary? GetSelectedDictionary(DependencyObject element)
=> (IDictionary?)element.GetValue(SelectedDictionaryProperty);
// 선택된 항목에서 key/value를 뽑아낼 경로 (예: "Id", "Name", "User.Profile.Id")
public static readonly DependencyProperty KeyPathProperty =
DependencyProperty.RegisterAttached(
"KeyPath", typeof(string), typeof(MultiSelectBehavior), new PropertyMetadata(null));
public static void SetKeyPath(DependencyObject element, string? value)
=> element.SetValue(KeyPathProperty, value);
public static string? GetKeyPath(DependencyObject element)
=> (string?)element.GetValue(KeyPathProperty);
// 값 경로. 비우거나 "." 이면 아이템 자체를 값으로 사용
public static readonly DependencyProperty ValuePathProperty =
DependencyProperty.RegisterAttached(
"ValuePath", typeof(string), typeof(MultiSelectBehavior), new PropertyMetadata("."));
public static void SetValuePath(DependencyObject element, string? value)
=> element.SetValue(ValuePathProperty, value);
public static string? GetValuePath(DependencyObject element)
=> (string?)element.GetValue(ValuePathProperty);
private static void OnSelectedDictionaryChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListBox lb) return;
lb.SelectionChanged -= Lb_SelectionChanged;
lb.SelectionChanged += Lb_SelectionChanged;
if (e.NewValue is IDictionary dict)
{
dict.Clear();
string? keyPath = GetKeyPath(lb);
string? valuePath = GetValuePath(lb);
foreach (var item in lb.SelectedItems)
{
var key = ResolvePath(item, keyPath);
var value = ResolvePath(item, valuePath);
if (key != null) dict[key] = value;
}
}
}
// ===== 유틸 =====
private static object? ResolvePath(object? instance, string? path)
{
if (instance == null) return null;
if (string.IsNullOrWhiteSpace(path) || path == ".") return instance;
object? current = instance;
foreach (var segment in path.Split('.'))
{
if (current == null) return null;
// IDictionary인 중간 단계도 지원 (예: dict["Key"])
if (current is IDictionary dict)
{
if (dict.Contains(segment)) { current = dict[segment]; continue; }
return null;
}
var pd = TypeDescriptor.GetProperties(current)[segment];
if (pd == null) return null;
current = pd.GetValue(current);
}
return current;
}
}
}

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace SmartAquaViewer.Helper
{
public static class RadioButtonHelper
{
public static readonly DependencyProperty PressedImageProperty =
DependencyProperty.RegisterAttached(
"PressedImage",
typeof(ImageSource),
typeof(RadioButtonHelper),
new PropertyMetadata(null));
public static void SetPressedImage(DependencyObject element, ImageSource value)
=> element.SetValue(PressedImageProperty, value);
public static ImageSource GetPressedImage(DependencyObject element)
=> (ImageSource)element.GetValue(PressedImageProperty);
public static readonly DependencyProperty UnPressedImageProperty =
DependencyProperty.RegisterAttached(
"UnPressedImage",
typeof(ImageSource),
typeof(RadioButtonHelper),
new PropertyMetadata(null));
public static void SetUnPressedImage(DependencyObject element, ImageSource value)
=> element.SetValue(UnPressedImageProperty, value);
public static ImageSource GetUnPressedImage(DependencyObject element)
=> (ImageSource)element.GetValue(UnPressedImageProperty);
}
}

@ -4,9 +4,64 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SmartAquaViewer" xmlns:local="clr-namespace:SmartAquaViewer"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:view="clr-namespace:SmartAquaViewer.View"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
mc:Ignorable="d" mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"> Title="MainWindow" Height="1080" Width="1920" WindowStartupLocation="CenterScreen">
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid> <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="270"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.RowSpan="2"> <!--파일 리스트-->
<view:FileListView x:Name="fileListView"/>
</Grid>
<Grid Grid.Column="1"> <!--탭-->
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<RadioButton x:Name="rdbtnMonitoing" GroupName="contentSwap" Content="모니터링" Tag="monitoring"
Style="{StaticResource ImageRadioButtonStyle}" IsChecked="True" FontSize="30"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/tab_bg.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/tab_bg_off.png"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<RadioButton x:Name="rdbtnEnergy" GroupName="contentSwap" Content="에너지" Tag="energy"
Style="{StaticResource ImageRadioButtonStyle}" Grid.Column="1" FontSize="30"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/tab_bg.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/tab_bg_off.png"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<RadioButton x:Name="rdBtnGreenHouseGas" GroupName="contentSwap" Content="온실가스" Tag="greenHouseGas"
Style="{StaticResource ImageRadioButtonStyle}" Grid.Column="2" FontSize="30"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/tab_bg.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/tab_bg_off.png"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<RadioButton x:Name="rdbtnCCTV" GroupName="contentSwap" Content="CCTV" Tag="cctv"
Style="{StaticResource ImageRadioButtonStyle}" Grid.Column="3" FontSize="30"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/tab_bg.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/tab_bg_off.png"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="1"> <!--기능 화면-->
<ContentControl x:Name="contentControl" Content="{Binding SelectedViewModel}"/>
</Grid>
</Grid> </Grid>
</Window> </Window>

@ -8,6 +8,8 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer namespace SmartAquaViewer
{ {
@ -19,6 +21,13 @@ namespace SmartAquaViewer
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
} }
} }
} }

@ -0,0 +1,80 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartAquaViewer.DataAnalisys;
#nullable disable
namespace SmartAquaViewer.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250811050443_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("SmartAquaViewer.DataAnalisys.WaterQualityVO", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<double>("DO")
.HasColumnType("double");
b.Property<double>("ElectricalConductivity")
.HasColumnType("double");
b.Property<double>("FlowRate")
.HasColumnType("double");
b.Property<double>("ORP")
.HasColumnType("double");
b.Property<double>("PH")
.HasColumnType("double");
b.Property<double>("Salinity")
.HasColumnType("double");
b.Property<double>("TSS")
.HasColumnType("double");
b.Property<double>("Temperature")
.HasColumnType("double");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.Property<double>("TotalNitrogen")
.HasColumnType("double");
b.Property<double>("TotalPhosphorus")
.HasColumnType("double");
b.Property<double>("Turbidity")
.HasColumnType("double");
b.Property<double>("WaterLevel")
.HasColumnType("double");
b.Property<int>("WaterTankNum")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WaterQuality");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SmartAquaViewer.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "WaterQuality",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
WaterTankNum = table.Column<int>(type: "int", nullable: false),
DO = table.Column<double>(type: "double", nullable: false),
PH = table.Column<double>(type: "double", nullable: false),
Temperature = table.Column<double>(type: "double", nullable: false),
WaterLevel = table.Column<double>(type: "double", nullable: false),
FlowRate = table.Column<double>(type: "double", nullable: false),
ElectricalConductivity = table.Column<double>(type: "double", nullable: false),
Turbidity = table.Column<double>(type: "double", nullable: false),
Salinity = table.Column<double>(type: "double", nullable: false),
ORP = table.Column<double>(type: "double", nullable: false),
TSS = table.Column<double>(type: "double", nullable: false),
TotalNitrogen = table.Column<double>(type: "double", nullable: false),
TotalPhosphorus = table.Column<double>(type: "double", nullable: false),
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WaterQuality", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WaterQuality");
}
}
}

@ -0,0 +1,190 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartAquaViewer.DataAnalisys;
#nullable disable
namespace SmartAquaViewer.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250812043735_AddWaterQualityTable")]
partial class AddWaterQualityTable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("RecordedTime")
.HasColumnType("datetime(6)")
.HasColumnName("recorded_time");
b.HasKey("Id");
b.ToTable("water_quality");
});
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.OwnsOne("SmartAquaViewer.DataAnalysis.FilteringSystem", "Filtering", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("AirBlowerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_air_blower_power");
b1.Property<bool>("CirculationPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_circulation_pump_power");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("filter_flow_rate");
b1.Property<bool>("HeatPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_heat_pump_power");
b1.Property<double>("HeatPumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_heat_pump_temperature");
b1.Property<string>("InverterControllerStatus")
.HasColumnType("longtext")
.HasColumnName("filter_inverter_status");
b1.Property<bool>("SandFilterPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_sand_filter_power");
b1.Property<double>("SumpFlowRate")
.HasColumnType("double")
.HasColumnName("filter_sump_flow_rate");
b1.Property<double>("SumpORP")
.HasColumnType("double")
.HasColumnName("filter_sump_orp");
b1.Property<double>("SumpPH")
.HasColumnType("double")
.HasColumnName("filter_sump_ph");
b1.Property<double>("SumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_sump_temperature");
b1.Property<double>("SumpWaterLevel")
.HasColumnType("double")
.HasColumnName("filter_sump_water_level");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.SterilizingSystem", "Sterilizing", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("ExcessOzoneDestroyerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_excess_ozone_destroyer_power");
b1.Property<bool>("OzoneDissolverPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_dissolver_power");
b1.Property<double>("OzoneDissolverPressure")
.HasColumnType("double")
.HasColumnName("ster_ozone_dissolver_pressure");
b1.Property<bool>("OzoneGeneratorPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_generator_power");
b1.Property<string>("UVSterilizerId")
.IsRequired()
.HasColumnType("longtext")
.HasColumnName("ster_uv_sterilizer_id");
b1.Property<bool>("UVSterilizerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_uv_sterilizer_power");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.WaterTank", "Tank", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<double>("DOValue")
.HasColumnType("double")
.HasColumnName("tank_do_value");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("tank_flow_rate");
b1.Property<int>("Number")
.HasColumnType("int")
.HasColumnName("tank_number");
b1.Property<double>("ORP")
.HasColumnType("double")
.HasColumnName("tank_orp");
b1.Property<double>("PH")
.HasColumnType("double")
.HasColumnName("tank_ph");
b1.Property<double>("Temperature")
.HasColumnType("double")
.HasColumnName("tank_temperature");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.Navigation("Filtering")
.IsRequired();
b.Navigation("Sterilizing")
.IsRequired();
b.Navigation("Tank")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,93 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SmartAquaViewer.Migrations
{
/// <inheritdoc />
public partial class AddWaterQualityTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WaterQuality");
migrationBuilder.CreateTable(
name: "water_quality",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
recorded_time = table.Column<DateTime>(type: "datetime(6)", nullable: false),
tank_number = table.Column<int>(type: "int", nullable: false),
tank_do_value = table.Column<double>(type: "double", nullable: false),
tank_ph = table.Column<double>(type: "double", nullable: false),
tank_orp = table.Column<double>(type: "double", nullable: false),
tank_temperature = table.Column<double>(type: "double", nullable: false),
tank_flow_rate = table.Column<double>(type: "double", nullable: false),
filter_sand_filter_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
filter_sump_ph = table.Column<double>(type: "double", nullable: false),
filter_sump_orp = table.Column<double>(type: "double", nullable: false),
filter_sump_water_level = table.Column<double>(type: "double", nullable: false),
filter_sump_flow_rate = table.Column<double>(type: "double", nullable: false),
filter_sump_temperature = table.Column<double>(type: "double", nullable: false),
filter_circulation_pump_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
filter_inverter_status = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
filter_flow_rate = table.Column<double>(type: "double", nullable: false),
filter_heat_pump_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
filter_heat_pump_temperature = table.Column<double>(type: "double", nullable: false),
filter_air_blower_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_ozone_generator_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_uv_sterilizer_id = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ster_uv_sterilizer_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_ozone_dissolver_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_ozone_dissolver_pressure = table.Column<double>(type: "double", nullable: false),
ster_excess_ozone_destroyer_power = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_water_quality", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "water_quality");
migrationBuilder.CreateTable(
name: "WaterQuality",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
DO = table.Column<double>(type: "double", nullable: false),
ElectricalConductivity = table.Column<double>(type: "double", nullable: false),
FlowRate = table.Column<double>(type: "double", nullable: false),
ORP = table.Column<double>(type: "double", nullable: false),
PH = table.Column<double>(type: "double", nullable: false),
Salinity = table.Column<double>(type: "double", nullable: false),
TSS = table.Column<double>(type: "double", nullable: false),
Temperature = table.Column<double>(type: "double", nullable: false),
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TotalNitrogen = table.Column<double>(type: "double", nullable: false),
TotalPhosphorus = table.Column<double>(type: "double", nullable: false),
Turbidity = table.Column<double>(type: "double", nullable: false),
WaterLevel = table.Column<double>(type: "double", nullable: false),
WaterTankNum = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WaterQuality", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

@ -0,0 +1,187 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartAquaViewer.DataAnalisys;
#nullable disable
namespace SmartAquaViewer.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("RecordedTime")
.HasColumnType("datetime(6)")
.HasColumnName("recorded_time");
b.HasKey("Id");
b.ToTable("water_quality");
});
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.OwnsOne("SmartAquaViewer.DataAnalysis.FilteringSystem", "Filtering", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("AirBlowerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_air_blower_power");
b1.Property<bool>("CirculationPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_circulation_pump_power");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("filter_flow_rate");
b1.Property<bool>("HeatPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_heat_pump_power");
b1.Property<double>("HeatPumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_heat_pump_temperature");
b1.Property<string>("InverterControllerStatus")
.HasColumnType("longtext")
.HasColumnName("filter_inverter_status");
b1.Property<bool>("SandFilterPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_sand_filter_power");
b1.Property<double>("SumpFlowRate")
.HasColumnType("double")
.HasColumnName("filter_sump_flow_rate");
b1.Property<double>("SumpORP")
.HasColumnType("double")
.HasColumnName("filter_sump_orp");
b1.Property<double>("SumpPH")
.HasColumnType("double")
.HasColumnName("filter_sump_ph");
b1.Property<double>("SumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_sump_temperature");
b1.Property<double>("SumpWaterLevel")
.HasColumnType("double")
.HasColumnName("filter_sump_water_level");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.SterilizingSystem", "Sterilizing", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("ExcessOzoneDestroyerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_excess_ozone_destroyer_power");
b1.Property<bool>("OzoneDissolverPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_dissolver_power");
b1.Property<double>("OzoneDissolverPressure")
.HasColumnType("double")
.HasColumnName("ster_ozone_dissolver_pressure");
b1.Property<bool>("OzoneGeneratorPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_generator_power");
b1.Property<string>("UVSterilizerId")
.IsRequired()
.HasColumnType("longtext")
.HasColumnName("ster_uv_sterilizer_id");
b1.Property<bool>("UVSterilizerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_uv_sterilizer_power");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.WaterTank", "Tank", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<double>("DOValue")
.HasColumnType("double")
.HasColumnName("tank_do_value");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("tank_flow_rate");
b1.Property<int>("Number")
.HasColumnType("int")
.HasColumnName("tank_number");
b1.Property<double>("ORP")
.HasColumnType("double")
.HasColumnName("tank_orp");
b1.Property<double>("PH")
.HasColumnType("double")
.HasColumnName("tank_ph");
b1.Property<double>("Temperature")
.HasColumnType("double")
.HasColumnName("tank_temperature");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.Navigation("Filtering")
.IsRequired();
b.Navigation("Sterilizing")
.IsRequired();
b.Navigation("Tank")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public class CCTVInfo
{
public string? DeviceId { get; set; }
public string? DeviceName { get; set; }
public string? RtspUrl { get; set; }
public string? Status { get; set; }
}
}

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SmartAquaViewer.DataAnalysis;
namespace SmartAquaViewer.Model
{
public class Datas
{
public static List<WaterQualityVO> WaterQualityList { get; set; }
static Datas()
{
WaterQualityList = new List<WaterQualityVO>();
}
public static List<WaterQualityVO> GetWaterQualityVO()
{
return WaterQualityList;
}
public static void SetWaterQualityVO(List<WaterQualityVO> sampleData)
{
WaterQualityList = sampleData;
}
}
}

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public enum MonitorTab
{
Tank,
Filter,
Sterilizer
}
public enum GraphType
{
LINE,
BOX,
SCATTER,
STEP,
STACKAREA,
PIE
}
public enum StepFieldKind
{
Status, // 전원/상태
Sensor, // 센서 값
Energy, // 에너지 소비량
}
public enum PowerStatus
{
Off,
On
}
public enum DataType
{
Energy,
GreenhouseGas
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public class FileModel
{
public string Name { get; set; }
}
}

@ -0,0 +1,140 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
xmlns:model="clr-namespace:SmartAquaViewer.Model"
xmlns:da="clr-namespace:SmartAquaViewer.DataAnalysis">
<FontFamily x:Key="SCDream1">pack://application:,,,/Fonts/#S-Core Dream 1 Thin</FontFamily>
<FontFamily x:Key="SCDream2">pack://application:,,,/Fonts/#S-Core Dream 2 ExtraLight</FontFamily>
<FontFamily x:Key="SCDream3">pack://application:,,,/Fonts/#S-Core Dream 3 Light</FontFamily>
<FontFamily x:Key="SCDream4">pack://application:,,,/Fonts/#S-Core Dream 4 Regular</FontFamily>
<FontFamily x:Key="SCDream5">pack://application:,,,/Fonts/#S-Core Dream 5 Medium</FontFamily>
<FontFamily x:Key="SCDream6">pack://application:,,,/Fonts/#S-Core Dream 6 Bold</FontFamily>
<FontFamily x:Key="SCDream7">pack://application:,,,/Fonts/#S-Core Dream 7 ExtraBold</FontFamily>
<FontFamily x:Key="SCDream8">pack://application:,,,/Fonts/#S-Core Dream 8 Heavy</FontFamily>
<FontFamily x:Key="SCDream9">pack://application:,,,/Fonts/#S-Core Dream 9 Black</FontFamily>
<Style x:Key="RadioButtonStyle" TargetType="RadioButton">
<Setter Property="Background" Value="#2F2F44"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="32"/>
<Setter Property="Width" Value="300"/>
<Setter Property="Height" Value="60"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border Background="{TemplateBinding Background}" CornerRadius="10"
BorderThickness="1" BorderBrush="Black">
<TextBlock Text="{TemplateBinding Content}"
HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="32" Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#C2C2E6"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ImageRadioButtonStyle" TargetType="RadioButton">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Grid>
<Image x:Name="ButtonImage" Stretch="Fill"
Source="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helper:RadioButtonHelper.UnPressedImage)}"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.FontFamily="{StaticResource SCDream8}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="ButtonImage" Property="Source"
Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helper:RadioButtonHelper.PressedImage)}"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ImageButtonStyle" TargetType="Button">
<Setter Property="Width" Value="58"/>
<Setter Property="Height" Value="42"/>
<Setter Property="Margin" Value="2.5 0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Image x:Name="PART_Image"
Source="{TemplateBinding helper:ImageButtonHelper.ImageSource}"
Stretch="Uniform"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Image" Property="Opacity" Value="0.8"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="PART_Image" Property="Opacity" Value="0.6"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="PART_Image" Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ComboBoxStyle" TargetType="ComboBox">
<Setter Property="BorderBrush" Value="#404F63"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Background" Value="#323232"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style x:Key="DataGridStyle" TargetType="DataGrid">
<Setter Property="EnableRowVirtualization" Value="True"/>
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Style>
<Style x:Key="DataGridRowStyle" TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="#242424"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
<Style x:Key="DataGridColumnHeaderStyle" TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="#1F1F1F"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="8"/>
</Style>
<Style x:Key="DataGridElmenetStyle" TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</ResourceDictionary>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -6,6 +6,86 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Fonts\SCDream1.otf" />
<None Remove="Fonts\SCDream2.otf" />
<None Remove="Fonts\SCDream3.otf" />
<None Remove="Fonts\SCDream4.otf" />
<None Remove="Fonts\SCDream5.otf" />
<None Remove="Fonts\SCDream6.otf" />
<None Remove="Fonts\SCDream7.otf" />
<None Remove="Fonts\SCDream8.otf" />
<None Remove="Fonts\SCDream9.otf" />
<None Remove="Resources\Images\arrow_down.png" />
<None Remove="Resources\Images\arrow_up.png" />
<None Remove="Resources\Images\ListImage.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_1.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_1_press.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_2.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_2_press.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_3.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_3_press.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_4.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_4_press.png" />
<None Remove="Resources\Images\tab_bg.png" />
<None Remove="Resources\Images\tab_bg_off.png" />
<None Remove="Resources\Images\top_bg.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FFmpeg.AutoGen" Version="7.1.1" />
<PackageReference Include="MaterialDesignColors" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-rc.1.efcore.9.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
</ItemGroup>
<ItemGroup>
<Resource Include="Fonts\SCDream1.otf" />
<Resource Include="Fonts\SCDream2.otf" />
<Resource Include="Fonts\SCDream3.otf" />
<Resource Include="Fonts\SCDream4.otf" />
<Resource Include="Fonts\SCDream5.otf" />
<Resource Include="Fonts\SCDream6.otf" />
<Resource Include="Fonts\SCDream7.otf" />
<Resource Include="Fonts\SCDream8.otf" />
<Resource Include="Fonts\SCDream9.otf" />
<Resource Include="Resources\Images\arrow_down.png" />
<Resource Include="Resources\Images\arrow_up.png" />
<Resource Include="Resources\Images\ListImage.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_1.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_1_press.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_2.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_2_press.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_3.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_3_press.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_4.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_4_press.png" />
<Resource Include="Resources\Images\tab_bg.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\tab_bg_off.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\top_bg.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
</ItemGroup>
</Project> </Project>

@ -0,0 +1,23 @@
<UserControl x:Class="SmartAquaViewer.View.CCTVView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="#243851">
<ItemsControl ItemsSource="{Binding CCTVInfoList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid x:Name="ugrdFFPlayer" Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>

@ -0,0 +1,87 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// CCTVView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class CCTVView : UserControl
{
private Thread _videoThread;
private Thread _renderingThread;
private CancellationTokenSource _videoCancellationTokenSource;
private CancellationTokenSource _renderingCancellationTokenSource;
private bool _stopThread = false;
private ConcurrentQueue<Bitmap> _frameQueue = new ConcurrentQueue<Bitmap>();
private readonly object _lockObject = new object();
public CCTVView()
{
InitializeComponent();
}
public void StartMedia(string rtspURL)
{
ClosePlayer();
_stopThread = false;
_videoThread = new Thread(new ThreadStart(OpenMedia));
_renderingThread = new Thread(new ThreadStart(RenderImage));
_videoThread.Priority = ThreadPriority.Highest; // 우선순위 설정
_videoThread.Start();
_renderingThread.Start();
}
private void OpenMedia()
{
throw new NotImplementedException();
}
private void RenderImage()
{
throw new NotImplementedException();
}
public void ClosePlayer()
{
_stopThread = true;
lock (_lockObject)
{
if (_videoCancellationTokenSource != null)
{
_videoCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): videoThread 종료함");
}
if (_renderingCancellationTokenSource != null)
{
_renderingCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): RenderingThread 종료함");
}
}
}
}
}

@ -0,0 +1,335 @@
<UserControl x:Class="SmartAquaViewer.View.EnegyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<UserControl.Resources>
<classes:InverseBoolConverter x:Key="InverseBoolConverter"/>
</UserControl.Resources>
<Grid Background="#243851">
<Grid.RowDefinitions>
<RowDefinition Height="160"/>
<RowDefinition Height="350"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.ColumnSpan="2" Margin="20" BorderBrush="#3E4C60" BorderThickness="1" CornerRadius="10">
<Border.Background>
<ImageBrush ImageSource="/Resources/Images/top_bg.png" Stretch="Fill"/>
</Border.Background>
<UniformGrid Columns="9">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="총 소비 전력" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="모래여과기" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalSandFilterEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="순환펌프" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalCirculationPumpEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="히트펌프" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalHeatPumpEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="에어브로와" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalAirBlowerEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="오존발생기" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalOzoneGeneratorEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="자외선 살균기" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalUVSterilizerEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="오존용해장치" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalOzoneDissolverEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="배오존장치" HorizontalAlignment="Center"
FontSize="22" Foreground="White" FontFamily="{StaticResource SCDream5}"/>
<TextBlock Text="{Binding TotalExcessOzoneDestroyerEnergy, StringFormat=\{0:F2\}}" HorizontalAlignment="Center"
FontSize="26" Foreground="White" FontFamily="{StaticResource SCDream4}"/>
</StackPanel>
</UniformGrid>
</Border>
<ScrollViewer Grid.Row="1" Grid.RowSpan="2" Margin="20 0 20 20"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center">
<DataGrid Style="{StaticResource DataGridStyle}" ItemsSource="{Binding WaterQualityList}" 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="모래여과기" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.SandFilterEnergy}"/>
<DataGridTextColumn Header="순환펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.CirculationPumpEnergy}"/>
<DataGridTextColumn Header="히트펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.HeatPumpEnergy}"/>
<DataGridTextColumn Header="에어브로와" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.AirBlowerEnergy}"/>
<DataGridTextColumn Header="오존발생기" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneGeneratorEnergy}"/>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.UVSterilizerEnergy}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="자외선"/>
<TextBlock Text="살균기"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="오존용해장치" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneDissolverEnergy}"/>
<DataGridTextColumn Header="배오존장치" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.ExcessOzoneDestroyerEnergy}"/>
<DataGridTextColumn Header="총 전력" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding TotalEnergy, StringFormat=\{0:F2\}}"/>
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
<Border Grid.Row="1" Grid.Column="1" Margin="0 0 20 20" CornerRadius="10"
Background="#384659" BorderBrush="#404F63" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid Margin="15 15 15 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="40" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="15 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenStackArea" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenLineNStackArea" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenPie" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<StackPanel>
<StackPanel Style="{StaticResource VisibleWhenLineNStackArea}">
<Grid Margin="0 0 0 20" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<TextBlock Text="{Binding SelectedXField.Display}" VerticalAlignment="Center"
Margin="15 0 0 0" Grid.Column="1"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid>
<Grid Margin="0 0 0 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Top"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border Grid.Column="1" CornerRadius="10"
Background="White" Margin="15 0 0 15">
<ListBox ItemsSource="{Binding YFieldCandidates}"
DisplayMemberPath="Display"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</Grid>
</StackPanel>
<StackPanel Style="{StaticResource VisibleWhenPie}">
<Grid Margin="0 0 0 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드" VerticalAlignment="Top"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border Grid.Column="1" CornerRadius="10"
Background="White" Margin="15 0 0 15">
<ListBox ItemsSource="{Binding YFieldCandidates}"
DisplayMemberPath="Display"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0 0 0 15">
<RadioButton x:Name="rbStatus" Content="합계"
GroupName="pie" Margin="0 0 30 0"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Converter={StaticResource InverseBoolConverter}, Mode=TwoWay}"/>
<RadioButton x:Name="pie" Content="평균"
GroupName="pie" Grid.Column="1"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="15 0 0 0" Grid.Row="1">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
<CheckBox Content="도넛 모드" IsChecked="{Binding IsDonut}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="15 0" Grid.Row="1" HorizontalAlignment="Right">
<Button Content="그래프 생성" Style="{StaticResource MaterialDesignFlatLightBgButton}"
FontWeight="Bold" Command="{Binding DrawGraphCommand}"/>
</StackPanel>
</Grid>
</Border>
<Border Grid.Row="2" Grid.Column="1" Margin="0 0 20 20" CornerRadius="10"
Background="#384659" BorderBrush="#404F63" BorderThickness="1">
<control:GraphControl x:Name="graphControl"
Margin="10" DataContext="{Binding GraphControlVM}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</Grid>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// EnegyView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class EnegyView : UserControl
{
public EnegyView()
{
InitializeComponent();
}
}
}

@ -0,0 +1,54 @@
<UserControl x:Class="SmartAquaViewer.View.FileListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="1040" d:DesignWidth="270">
<UserControl.DataContext>
<vm:FileListViewModel/>
</UserControl.DataContext>
<Border Background="#1E2241">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<!-- Header Row -->
<RowDefinition Height="*"/>
<!-- File List Row -->
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Image Source="/Reources/Images/ListImage.png"
Width="35" Margin="20 0"/>
<TextBlock Text="파일 목록"
FontSize="20" FontWeight="Bold" Foreground="White"
VerticalAlignment="Center"/>
<Button Width="35" Height="35" Margin="50 0"
Background="#ffd663"
Command="{Binding OpenFileDialogCommand}">
<materialDesign:PackIcon Kind="FolderAdd"/>
</Button>
</StackPanel>
<ListView Grid.Row="1"
Margin="10"
ItemsSource="{Binding FileList}"
SelectedItem="{Binding SelectedFile}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
BorderThickness="0" Background="Transparent">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0 5">
<TextBlock Text="{Binding Name}" Margin="5"
FontSize="16" FontWeight="Medium" Foreground="White"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// FileListView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class FileListView : UserControl
{
public FileListView()
{
InitializeComponent();
}
}
}

@ -0,0 +1,269 @@
<UserControl x:Class="SmartAquaViewer.View.GreenHouseView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<UserControl.Resources>
<classes:InverseBoolConverter x:Key="InverseBoolConverter"/>
</UserControl.Resources>
<Grid Background="#243851">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.RowSpan="2" Margin="20"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Center">
<DataGrid Style="{StaticResource DataGridStyle}" Background="Transparent" ItemsSource="{Binding WaterQualityList}"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="모래여과기" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.SandFilterGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="순환펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.CirculationPumpGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="히트펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.HeatPumpGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="에어브로와" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.AirBlowerGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="오존발생기" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneGeneratorGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.UVSterilizerGreenhouseGas, StringFormat=\{0:F3\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="자외선"/>
<TextBlock Text="살균기"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="오존용해장치" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneDissolverGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="배오존장치" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.ExcessOzoneDestroyerGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="총 배출량" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding TotalGreenhouseGas, StringFormat=\{0:F3\}}"/>
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
<Border Grid.Row="0" Grid.Column="1" Margin="0 20 20 20" CornerRadius="10"
Background="#384659" BorderBrush="#404F63" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid Margin="15 15 15 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="40" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="15 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenStackArea" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenLineNStackArea" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenPie" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<StackPanel>
<StackPanel Style="{StaticResource VisibleWhenLineNStackArea}">
<Grid Margin="0 0 0 20" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<TextBlock Text="{Binding SelectedXField.Display}" VerticalAlignment="Center"
Margin="15 0 0 0" Grid.Column="1"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid>
<Grid Margin="0 0 0 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Top"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border Grid.Column="1" CornerRadius="10"
Background="White" Margin="15 0 0 15">
<ListBox ItemsSource="{Binding YFieldCandidates}"
DisplayMemberPath="Display"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</Grid>
</StackPanel>
<StackPanel Style="{StaticResource VisibleWhenPie}">
<Grid Margin="0 0 0 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드" VerticalAlignment="Top"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Border Grid.Column="1" CornerRadius="10"
Background="White" Margin="15 0 0 15">
<ListBox ItemsSource="{Binding YFieldCandidates}"
DisplayMemberPath="Display"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
Height="Auto" Background="White"
FontSize="16" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0 0 0 15">
<RadioButton x:Name="rbStatus" Content="합계"
GroupName="pie" Margin="0 0 30 0"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Converter={StaticResource InverseBoolConverter}, Mode=TwoWay}"/>
<RadioButton x:Name="pie" Content="평균"
GroupName="pie" Grid.Column="1"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="15 0 0 0" Grid.Row="1">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
<CheckBox Content="범례 표시" IsChecked="{Binding ShowLegends}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
<CheckBox Content="도넛 모드" IsChecked="{Binding IsDonut}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource MaterialDesignUserForegroundCheckBox}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="15 0" Grid.Row="1" HorizontalAlignment="Right">
<Button Content="그래프 생성" Style="{StaticResource MaterialDesignFlatLightBgButton}"
FontWeight="Bold" Command="{Binding DrawGraphCommand}"/>
</StackPanel>
</Grid>
</Border>
<Border Grid.Row="1" Grid.Column="1" Margin="0 0 20 20" CornerRadius="10"
Background="#384659" BorderBrush="#404F63" BorderThickness="1">
<control:GraphControl x:Name="graphControl"
Margin="10" DataContext="{Binding GraphControlVM}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</Grid>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// GreenHouseView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class GreenHouseView : UserControl
{
public GreenHouseView()
{
InitializeComponent();
}
}
}

@ -0,0 +1,506 @@
<UserControl x:Class="SmartAquaViewer.View.MonitoringView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:model="clr-namespace:SmartAquaViewer.Model"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<UserControl.Resources>
<classes:EnumEqualsConverter x:Key="EnumEqualsConverter"/>
<classes:BoolToPowerConverter x:Key="BoolToPowerConverter"/>
</UserControl.Resources>
<Border>
<md:DrawerHost BottomDrawerBackground="#122136" IsBottomDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Grid Background="#243851">
<Grid.RowDefinitions>
<RowDefinition Height="70"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.Background>
<ImageBrush ImageSource="/Resources/Images/top_bg.png" Stretch="Fill"/>
</Grid.Background>
<control:SegmentedControl x:Name="segmentedControl" Margin="20 10"
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
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>
</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 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>
</ScrollViewer>
<Grid Grid.Row="1" VerticalAlignment="Bottom">
<Button Name="btnVisibilityDown" Tag="down"
Style="{StaticResource ImageButtonStyle}" Height="33" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="{Binding BtnVisibilityDown}"
helper:ImageButtonHelper.ImageSource="/Resources/Images/arrow_down.png"/>
<Button Name="btnVisibilityUp" Tag="up"
Style="{StaticResource ImageButtonStyle}" Height="33" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="{Binding BtnVisibilityUp}"
helper:ImageButtonHelper.ImageSource="/Resources/Images/arrow_up.png"/>
</Grid>
</Grid>
<md:DrawerHost.BottomDrawerContent>
<Border Height="450" BorderThickness="0 3 0 0" BorderBrush="#455569">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="550"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Margin="20"
Background="#384659" BorderBrush="#404F63" BorderThickness="1" CornerRadius="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid Margin="15 15 15 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="40" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="15 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="40" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding XFieldCandidates}"
SelectedItem="{Binding SelectedXField}"
DisplayMemberPath="Display"/>
</Grid>
<Grid Margin="15 10 15 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<!-- 보이기 토글용 스타일 -->
<Style x:Key="VisibleWhenTrue" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STEP">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenTankNLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsTankAndLine}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenStep" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STEP">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenScatter" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="SCATTER">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenBox" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="BOX">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenTime" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedXField.Name}" Value="RecordedTime">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<!-- 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.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 Style="{StaticResource VisibleWhenLine}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center" Style="{StaticResource VisibleWhenLine}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField, Mode=TwoWay}"
Grid.Column="1"
DisplayMemberPath="Display" Margin="15 0 0 0"
Height="40" Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<!-- 옵션 -->
<StackPanel Orientation="Horizontal" Margin="0 15 0 0" Grid.Row="4">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" 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"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>-->
</StackPanel>
</StackPanel>
<!--STEP-->
<StackPanel Style="{StaticResource VisibleWhenStep}">
<Grid Margin="0 5 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드 종류" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Center">
<RadioButton x:Name="rbStatus" Content="전원/상태"
GroupName="strpPlot" Margin="0 0 30 0"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding SelectedKind, Mode=TwoWay,
Converter={StaticResource EnumEqualsConverter},
ConverterParameter={x:Static model:StepFieldKind.Status}}"/>
<RadioButton x:Name="rbValue" Content="센서 값"
GroupName="strpPlot" Grid.Column="1"
Foreground="White" FontSize="20"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding SelectedKind, Mode=TwoWay,
Converter={StaticResource EnumEqualsConverter},
ConverterParameter={x:Static model:StepFieldKind.Sensor}}"/>
</StackPanel>
</Grid>
<Grid Margin="0 5 0 0">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=rbStatus}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField, Mode=TwoWay}"
Grid.Column="1"
DisplayMemberPath="Display" Margin="15 0 0 0"
Height="40" Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=rbValue}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="Y축 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<!-- SelectedItems 바인딩을 위한 간단 Behavior는 아래 3) 참고 -->
<Border CornerRadius="10" Background="White" Margin="0 5">
<ListBox ItemsSource="{Binding YFieldCandidates}"
DisplayMemberPath="Display"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
Height="Auto" Background="White"
FontSize="14" FontWeight="Bold"
Style="{StaticResource MaterialDesignFilterChipListBox}"/>
</Border>
</StackPanel>
<!-- 옵션 -->
<StackPanel Orientation="Horizontal" Margin="0 15 0 0">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
</StackPanel>
</StackPanel>
<!-- SCATTER: 단일 Y + 옵션 -->
<StackPanel Style="{StaticResource VisibleWhenScatter}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center" Width="80"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField}"
DisplayMemberPath="Display" Height="40"
Grid.Column="1" Grid.ColumnSpan="3" Margin="15 0 0 0"
Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<Grid Margin="0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="마커 크기" Grid.Row="1" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Slider Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Margin="15 0 0 0"
Minimum="1" Maximum="15" Value="{Binding ScatterMarkerSize}" Width="280" IsSnapToTickEnabled="True" TickFrequency="1"/>
<TextBlock Text="{Binding ScatterMarkerSize}" Margin="15 0"
Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid>
<CheckBox Content="회귀선" IsChecked="{Binding ShowRegression}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="0" Grid.Row="1" Grid.Column="3"
VerticalContentAlignment="Center"
Style="{StaticResource MaterialDesignUserForegroundCheckBox}"/>
</StackPanel>
<!-- BOX: 값 필드 + 그룹 필드 + 옵션 -->
<StackPanel Style="{StaticResource VisibleWhenBox}">
<Grid Style="{StaticResource VisibleWhenTime}" Margin="0 10 0 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="시간 범위" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Slider Margin="15 0 0 0" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Left"
Minimum="1" Maximum="24" TickFrequency="1" IsSnapToTickEnabled="True"
Value="{Binding BoxTimeSpan}" Width="310"/>
<TextBlock Text="{Binding BoxTimeSpan}"
Margin="15 0" Grid.Column="2"
VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="18" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="값 필드" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField}"
DisplayMemberPath="Display" Height="40"
Grid.Column="1" Margin="15 0 0 0"
Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<Grid Margin="0 15 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="박스 너비" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Slider Margin="15 0 0 0" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Left"
Minimum="0.1" Maximum="1.0" TickFrequency="0.05" IsSnapToTickEnabled="True"
Value="{Binding BoxWidth}" Width="310"/>
<TextBlock Text="{Binding BoxWidth, StringFormat=F2}"
Margin="15 0" Grid.Column="2"
VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="18" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="15 0" Grid.Row="1" HorizontalAlignment="Right">
<Button Content="그래프 생성" Style="{StaticResource MaterialDesignFlatLightBgButton}"
FontWeight="Bold" Command="{Binding DrawGraphCommand}"/>
</StackPanel>
</Grid>
</Border>
<Border Grid.Column="1" Margin="0 20 20 20"
Background="#384659" BorderBrush="#404F63" BorderThickness="1" CornerRadius="10">
<control:GraphControl x:Name="graphControl"
Margin="10" DataContext="{Binding GraphControlVM}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</Grid>
</Border>
</md:DrawerHost.BottomDrawerContent>
</md:DrawerHost>
</Border>
</UserControl>

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Newtonsoft.Json.Linq;
using SmartAquaViewer.Classes;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
using SmartAquaViewer.ViewModel;
namespace SmartAquaViewer.View
{
/// <summary>
/// MonitoringView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MonitoringView : UserControl
{
private MonitoringViewModel? monitoringViewModel;
private readonly Dictionary<MonitorTab, ScrollViewer> _tabMap;
public MonitoringView()
{
InitializeComponent();
_tabMap = new Dictionary<MonitorTab, ScrollViewer>
{
{ MonitorTab.Tank, svTanks },
{ MonitorTab.Filter, svFilter },
{ MonitorTab.Sterilizer, svSterilizer }
};
Loaded += MonitoringView_Loaded;
}
private void MonitoringView_Loaded(object sender, RoutedEventArgs e)
{
monitoringViewModel = this.DataContext as MonitoringViewModel;
monitoringViewModel.PropertyChanged += VmOnPropertyChanged;
SetActiveTab(monitoringViewModel.SelectedTab); // 초기 반영
}
private void VmOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MonitoringViewModel.SelectedTab) && sender is MonitoringViewModel vm)
SetActiveTab(vm.SelectedTab);
}
private void SetActiveTab(MonitorTab tab)
{
// 전부 Collapsed
svTanks.Visibility = Visibility.Collapsed;
svFilter.Visibility = Visibility.Collapsed;
svSterilizer.Visibility = Visibility.Collapsed;
// 대상만 Visible
if (_tabMap.TryGetValue(tab, out var target))
target.Visibility = Visibility.Visible;
}
}
}

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class CCTVViewModel : INotifyPropertyChanged
{
public List<CCTVInfo> CCTVInfoList { get; } = new List<CCTVInfo>();
private int _columnCount;
public int ColumnCount
{
get => _columnCount;
set
{
if (_columnCount != value)
{
_columnCount = value;
OnPropertyChanged();
}
}
}
private int _rowCount;
public int RowCount
{
get => _rowCount;
set
{
if (_rowCount != value)
{
_rowCount = value;
OnPropertyChanged();
}
}
}
public CCTVViewModel()
{
ColumnCount = 4; // Default value
RowCount = 2; // Default value
CCTVInfoList = new List<CCTVInfo>()
{
new()
{
DeviceId = "000001",
DeviceName = "CCTV 1",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-211",
Status = "Active"
},
new()
{
DeviceId = "000002",
DeviceName = "CCTV 2",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-212",
Status = "Active"
},
new()
{
DeviceId = "000003",
DeviceName = "CCTV 3",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-211",
Status = "Active"
},
new()
{
DeviceId = "000004",
DeviceName = "CCTV 4",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-212",
Status = "Active"
},
new()
{
DeviceId = "000005",
DeviceName = "CCTV 5",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-211",
Status = "Active"
},
new()
{
DeviceId = "000006",
DeviceName = "CCTV 6",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-212",
Status = "Active"
}
};
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,366 @@
using System;
using System.Collections.Generic;
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;
namespace SmartAquaViewer.ViewModel
{
public class EnergyViewModel : INotifyPropertyChanged
{
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public List<WaterQualityVO> WaterQualityList { get; set; }
private double _totalEnergy;
public double TotalEnergy
{
get => _totalEnergy;
set
{
if (_totalEnergy != value)
{
_totalEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalSandFilterEnergy;
public double TotalSandFilterEnergy
{
get => _totalSandFilterEnergy;
set
{
if (_totalSandFilterEnergy != value)
{
_totalSandFilterEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalCirculationPumpEnergy;
public double TotalCirculationPumpEnergy
{
get => _totalCirculationPumpEnergy;
set
{
if (_totalCirculationPumpEnergy != value)
{
_totalCirculationPumpEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalHeatPumpEnergy;
public double TotalHeatPumpEnergy
{
get => _totalHeatPumpEnergy;
set
{
if (_totalHeatPumpEnergy != value)
{
_totalHeatPumpEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalAirBlowerEnergy;
public double TotalAirBlowerEnergy
{
get => _totalAirBlowerEnergy;
set
{
if (_totalAirBlowerEnergy != value)
{
_totalAirBlowerEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalOzoneGeneratorEnergy;
public double TotalOzoneGeneratorEnergy
{
get => _totalOzoneGeneratorEnergy;
set
{
if (_totalOzoneGeneratorEnergy != value)
{
_totalOzoneGeneratorEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalUVSterilizerEnergy;
public double TotalUVSterilizerEnergy
{
get => _totalUVSterilizerEnergy;
set
{
if (_totalUVSterilizerEnergy != value)
{
_totalUVSterilizerEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalOzoneDissolverEnergy;
public double TotalOzoneDissolverEnergy
{
get => _totalOzoneDissolverEnergy;
set
{
if (_totalOzoneDissolverEnergy != value)
{
_totalOzoneDissolverEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalExcessOzoneDestroyerEnergy;
public double TotalExcessOzoneDestroyerEnergy
{
get => _totalExcessOzoneDestroyerEnergy;
set
{
if (_totalExcessOzoneDestroyerEnergy != value)
{
_totalExcessOzoneDestroyerEnergy = value;
OnPropertyChanged();
}
}
}
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));
}
}
}
}
private StepFieldKind _selectedKind = StepFieldKind.Sensor; // 기본값은 센서
public StepFieldKind SelectedKind
{
get => _selectedKind;
set
{
if (_selectedKind != value)
{
_selectedKind = 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 _useAverage;
public bool UseAverage { get => _useAverage; set { _useAverage = value; OnPropertyChanged(); } }
private bool _showMarkers;
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
private bool _isDonut;
public bool IsDonut { get => _isDonut; set { _isDonut = value; OnPropertyChanged(); } }
public ICommand DrawGraphCommand { get; }
public EnergyViewModel()
{
WaterQualityList = Datas.GetWaterQualityVO();
GraphTypes = new ObservableCollection<GraphType>
{
GraphType.LINE,
GraphType.STACKAREA,
GraphType.PIE
};
TotalSandFilterEnergy = WaterQualityList.Sum(vo => vo.Filtering.SandFilterEnergy);
TotalCirculationPumpEnergy = WaterQualityList.Sum(vo => vo.Filtering.CirculationPumpEnergy);
TotalHeatPumpEnergy = WaterQualityList.Sum(vo => vo.Filtering.HeatPumpEnergy);
TotalAirBlowerEnergy = WaterQualityList.Sum(vo => vo.Filtering.AirBlowerEnergy);
TotalOzoneGeneratorEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.OzoneGeneratorEnergy);
TotalUVSterilizerEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.UVSterilizerEnergy);
TotalOzoneDissolverEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.OzoneDissolverEnergy);
TotalExcessOzoneDestroyerEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.ExcessOzoneDestroyerEnergy);
TotalEnergy = WaterQualityList.Sum(vo => vo.TotalEnergy);
SelectedKind = StepFieldKind.Energy; // 기본적으로 에너지 관련 필드만 표시
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
GraphControlVM.SetMultiLineGraph(WaterQualityList, SelectedYFields, DataType.Energy, ShowMarkers, ShowLegends);
break;
case GraphType.STACKAREA:
GraphControlVM.SetStackAreaPlot(WaterQualityList, SelectedYFields, DataType.Energy, ShowMarkers, ShowLegends);
break;
case GraphType.PIE:
GraphControlVM.SetPieChart(WaterQualityList, SelectedYFields, DataType.Energy, UseAverage, IsDonut);
break;
default:
break;
}
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime) });
AvailableFields.Add(new FieldItem { Name = "TotalEnergy", Display = "총 전력", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterEnergy", Display = "모래여과기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpEnergy", Display = "순환펌프", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpEnergy", Display = "히트펌프", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerEnergy", Display = "에어브로와", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorEnergy", Display = "오존발생기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.UVSterilizerEnergy", Display = "자외선 살균기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverEnergy", Display = "오존용해장치", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerEnergy", Display = "배오존장치", DataType = typeof(double), Kind = StepFieldKind.Energy });
}
private void RebuildFieldCandidates()
{
// 후보 초기화
XFieldCandidates.Clear();
YFieldCandidates.Clear();
// X축: 시간 우선
foreach (var f in AvailableFields)
{
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STACKAREA) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
if (SelectedGraphType is GraphType.LINE or GraphType.STACKAREA or GraphType.PIE)
{
// 수치형만 (LINE/STACKAREA/PIE 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
// Y축 후보: 수치형
foreach (var f in src)
{
if (SelectedGraphType is GraphType.STACKAREA or GraphType.PIE && f.Name.Equals("TotalEnergy")) continue;
YFieldCandidates.Add(f);
}
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();
SelectedYField = null;
switch (SelectedGraphType)
{
case GraphType.LINE:
//var def = YFieldCandidates.FirstOrDefault();
//if (def != null) SelectedYFields.Add(def);
break;
case GraphType.STACKAREA:
SelectedYField = YFieldCandidates.FirstOrDefault();
break;
case GraphType.PIE:
//SelectedYField = YFieldCandidates.FirstOrDefault();
break;
}
OnPropertyChanged(nameof(SelectedYFields));
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,218 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Threading;
using FFmpeg.AutoGen;
using SmartAquaViewer.Helper.FFHelper;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class FFPlayerViewModel : INotifyPropertyChanged
{
public CCTVInfo CCTVInfo { get; set; }
private ImageSource? _currentFrame;
public ImageSource? CurrentFrame
{
get => _currentFrame;
set { _currentFrame = value; OnPropertyChanged(); }
}
private readonly object _lockObject = new object();
private Thread _videoThread;
private Thread _renderingThread;
private CancellationTokenSource _videoCancellationTokenSource;
private CancellationTokenSource _renderingCancellationTokenSource;
private bool _stopThread = false;
private ConcurrentQueue<Bitmap> _frameQueue = new ConcurrentQueue<Bitmap>();
public FFPlayerViewModel(CCTVInfo cctvInfo)
{
CCTVInfo = cctvInfo;
}
public void StartMedia(string rtspURL)
{
ClosePlayer();
_stopThread = false;
_videoThread = new Thread(new ThreadStart(OpenMedia));
_renderingThread = new Thread(new ThreadStart(RenderImage));
_videoThread.Priority = ThreadPriority.Highest; // 우선순위 설정
_videoThread.Start();
_renderingThread.Start();
}
private unsafe void OpenMedia()
{
int failCount = 0;
int frameCount = 0;
_frameQueue = new ConcurrentQueue<Bitmap>();
_videoCancellationTokenSource = new CancellationTokenSource();
try
{
using (StreamDecoder sd = new StreamDecoder(CCTVInfo.RtspUrl))
{
using (var vfc = new VideoFrameConverter(sd.FrameSize, sd.PixelFormat, sd.FrameSize, AVPixelFormat.AV_PIX_FMT_BGR24))
{
while (!_videoCancellationTokenSource.Token.IsCancellationRequested)
{
bool decodeSuccess = sd.TryDecodeNextFrame(out var frame);
//if (!decodeSuccess)
//{
// failCount++;
// HandleDecodeFailure(ref failCount, maxFailCount);
// continue; // 다음 반복으로 이동
//}
// 디코딩 성공 시 프레임 처리
HandleDecodedFrame(frame, vfc, ref frameCount);
failCount = 0; // 실패 카운트 초기화
}
}
}
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error("OpenMedia() : " + ex.Message);
Debug.WriteLine("OpenMedia() : " + ex.Message.ToString());
}
finally
{
if (!_stopThread || !_videoCancellationTokenSource.Token.IsCancellationRequested)
{
Debug.WriteLine($"Restarting media");
StartMedia(CCTVInfo.RtspUrl);
}
}
}
private void HandleDecodedFrame(AVFrame frame, VideoFrameConverter vfc, ref int frameCount)
{
Bitmap convertedFrame = null;
try
{
convertedFrame = vfc.DeepCopyFrame(frame);
lock (_lockObject)
{
_frameQueue.Enqueue(convertedFrame);
frameCount++;
}
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error("HandleDecodedFrame() : " + ex.Message);
Debug.WriteLine("HandleDecodedFrame() : " + ex.Message);
convertedFrame?.Dispose();
}
}
private void RenderImage()
{
int dequeCount = 0;
_renderingCancellationTokenSource = new CancellationTokenSource();
try
{
while (!_renderingCancellationTokenSource.Token.IsCancellationRequested)
{
Bitmap bitmap = null;
lock (_lockObject)
{
if (_frameQueue.Count > 0)
{
_frameQueue.TryDequeue(out bitmap);
}
else
{
continue;
}
}
if (bitmap == null)
{
Thread.Sleep(10); // CPU 과부하 방지
continue;
}
try
{
//Dispatcher.BeginInvoke((Action)(() =>
//{
// try
// {
// DivideAndDisplayBitmap(bitmap);
// }
// finally
// {
// bitmap.Dispose(); // 자원 해제
// }
//}));
}
catch (ArgumentException ex)
{
Console.WriteLine("RenderImage() : " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("RenderImage() : " + ex.ToString());
}
_renderingCancellationTokenSource.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
Console.WriteLine("RenderImage() : Render loop canceled.");
}
finally
{
// 필요한 자원 정리
//ClearFrameQueue();
}
}
public void ClosePlayer()
{
_stopThread = true;
lock (_lockObject)
{
if (_videoCancellationTokenSource != null)
{
_videoCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): videoThread 종료함");
}
if (_renderingCancellationTokenSource != null)
{
_renderingCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): RenderingThread 종료함");
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
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 Microsoft.Win32;
using SmartAquaViewer.Controls;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class FileListViewModel : INotifyPropertyChanged
{
public ObservableCollection<FileModel> FileList { get; set; }
private FileModel? _selectedFile;
public FileModel SelectedFile
{
get => _selectedFile;
set
{
if (_selectedFile != value)
{
_selectedFile = value;
OnPropertyChanged();
}
}
}
public ICommand OpenFileDialogCommand { get; }
public FileListViewModel()
{
FileList = new ObservableCollection<FileModel>();
OpenFileDialogCommand = new RelayCommand(OpenFileList);
}
private void OpenFileList(object obj)
{
OpenFileDialog openFileDialog = new OpenFileDialog
{
Multiselect = true,
Filter = "Json Files (*.json)|*.json|All Files (*.*)|*.*"
};
if (openFileDialog.ShowDialog() == true)
{
FileList.Clear();
foreach (var filePath in openFileDialog.FileNames)
{
var fileName = System.IO.Path.GetFileName(filePath);
var fileModel = new FileModel { Name = fileName };
if (!FileList.Any(f => f.Name == fileModel.Name))
{
FileList.Add(fileModel);
}
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,827 @@
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 OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class GraphControlViewModel
{
public PlotModel Model { get; }
public GraphControlViewModel()
{
Model = new PlotModel();
InitializeGraph();
}
private void InitializeGraph()
{
Model.TextColor = OxyColors.Black;
}
public void SetTankLineGraph(Dictionary<int, ObservableCollection<WaterQualityVO>> collection,
FieldItem? xField, FieldItem? yField,
bool isMarker)
{
Model.Series.Clear();
Model.Axes.Clear();
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "시간",
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Minutes,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = yField.Display,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
foreach (var (tankNum, data) in collection.OrderBy(x => x.Key))
{
if (data == null || data.Count == 0) continue;
var series = new LineSeries()
{
Title = tankNum.ToString(),
MarkerType = isMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = isMarker ? 3 : 0
};
foreach (var r in data.OrderBy(r => r.RecordedTime))
{
var y = ResolveTank(r, yField.Name!);
if (!y.HasValue) continue;
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(r.RecordedTime),
y.Value));
}
if (series.Points.Count > 0)
{
// 트래커 포맷: 시간, 수조, 지표, 값
series.TrackerFormatString =
$"수조 {tankNum}\n시간: {{2:HH:mm}}\n{yField.Display}: {{4:0.###}}";
Model.Series.Add(series);
}
}
Model.InvalidatePlot(true);
}
public void SetMultiLineGraph(
List<WaterQualityVO> collection,
ObservableCollection<FieldItem> yFields,
DataType dataType,
bool showMarker, bool showLegend)
{
Model.Series.Clear();
Model.Axes.Clear();
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "시간",
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Minutes,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = dataType == DataType.Energy ? "전력 (kW)" : "온실가스 (tCO₂)",
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
foreach (var field in yFields)
{
var series = new LineSeries()
{
Title = field.Display,
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = showMarker ? 3 : 0
};
foreach (var r in collection.OrderBy(r => r.RecordedTime))
{
double? y = null;
double? v = ResolveGreenhouseGas(r, field.Name!);
y = dataType == DataType.Energy ? ResolveEnergyField(r, field.Name!) : v;
if (!y.HasValue) continue;
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(r.RecordedTime),
y.Value));
}
if (series.Points.Count > 0)
{
// 트래커 포맷: 시간, 수조, 지표, 값
series.TrackerFormatString =
$"시간: {{2:HH:mm}}\n{field.Display}: {{4:0.###}}";
Model.Series.Add(series);
}
}
Model.Legends.Clear();
Model.IsLegendVisible = showLegend;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = dataType == DataType.Energy ? "전력 소비량" : "온실가스 배출량",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true);
}
public void SetDefaultLineGraph(
List<WaterQualityVO> collection,
MonitorTab selectedTab,
FieldItem? xField, FieldItem? yField,
bool isMarker)
{
Model.Series.Clear();
Model.Axes.Clear();
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Minutes,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = yField.Display,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
var series = new LineSeries()
{
MarkerType = isMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = isMarker ? 3 : 0
};
foreach (var r in collection.OrderBy(r => r.RecordedTime))
{
double? y = null;
if (selectedTab.Equals(MonitorTab.Filter))
y = ResolveFilter(r, yField.Name!);
else if (selectedTab.Equals(MonitorTab.Sterilizer))
y = ResolveSterilizer(r, yField.Name!);
if (!y.HasValue) continue;
series.Points.Add(new DataPoint(
DateTimeAxis.ToDouble(r.RecordedTime),
y.Value));
if (series.Points.Count > 0)
{
// 트래커 포맷: 시간, 수조, 지표, 값
series.TrackerFormatString =
$"시간: {{2:HH:mm}}\n{yField.Display}: {{4:0.###}}";
}
}
Model.Series.Add(series);
Model.InvalidatePlot(true);
}
public void SetBoxPlot(
List<WaterQualityVO> rows,
FieldItem xAxisKind, // 시간 or 수조
FieldItem valueField, // 값 필드
double boxWidth, // 박스 너비
TimeSpan? timeBucket = null, // 시간 사용할 때 버킷(기본 1시간)
int maxCategories = 24 // X 카테고리 최대개수(너무 많을 때 최근 N개만)
)
{
Model.Series.Clear();
Model.Axes.Clear();
// 축
var xAxis = new CategoryAxis
{
Position = AxisPosition.Bottom,
GapWidth = 0.3,
IsPanEnabled = false,
IsZoomEnabled = false
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = valueField.Name,
MinorGridlineStyle = LineStyle.Dot,
MajorGridlineStyle = LineStyle.Solid
};
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
var series = new BoxPlotSeries()
{
BoxWidth = boxWidth
};
if (rows == null || rows.Count == 0) return;
var bucket = timeBucket ?? TimeSpan.FromHours(1);
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))
{
// 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;
// 2) 사분위수/중앙값 계산
double q1 = Percentile(values, 0.25);
double median = Percentile(values, 0.50);
double q3 = Percentile(values, 0.75);
double iqr = q3 - q1;
// 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();
// 4) 아이템 추가 (position = X축 인덱스)
string label = xAxisKind.Name switch
{
"수조" or "Tank" => $"수조 {key}",
"시간" or "RecordedTime" => FormatBucket(FloorToBucket(data.First().RecordedTime, bucket), bucket),
_ => key
};
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);
}
Model.Series.Add(series);
Model.IsLegendVisible = false;
Model.InvalidatePlot(true);
}
public void SetScatterPlot(
List<WaterQualityVO> rows,
FieldItem xAxisField,
FieldItem yAxisField,
double markerSize = 3,
bool showRegression = false)
{
Model.Series.Clear();
Model.Axes.Clear();
var xAxis = new LinearAxis
{
Position = AxisPosition.Bottom,
Title = xAxisField.Display,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = yAxisField.Display,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
var scatterSeries = new ScatterSeries
{
MarkerType = MarkerType.Circle,
MarkerSize = markerSize,
MarkerStroke = OxyColors.Black,
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));
}
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);
}
}
Model.InvalidatePlot(true);
}
public void SetStepPlot(
List<WaterQualityVO> rows,
MonitorTab selectedTab,
FieldItem xAxisField,
ObservableCollection<FieldItem> yAxisFields,
bool showMarker = false
)
{
Model.Series.Clear();
Model.Axes.Clear();
var xAxis = new CategoryAxis
{
Position = AxisPosition.Bottom,
Title = "시간",
GapWidth = 0.2
};
foreach (var r in rows)
{
xAxis.Labels.Add(r.RecordedTime.ToString("HH:mm:ss"));
}
Model.Axes.Add(xAxis);
Model.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "값"
});
foreach (var field in yAxisFields)
{
var series = new StairStepSeries
{
Title = field.Display,
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
MarkerSize = 3
};
int i = 0;
foreach (var r in rows.OrderBy(r => r.RecordedTime))
{
double? y = selectedTab.Equals(MonitorTab.Filter)
? ResolveFilter(r, field.Name!)
: ResolveSterilizer(r, field.Name!);
if (y.HasValue)
series.Points.Add(new DataPoint(i, y.Value));
i++;
}
Model.Series.Add(series);
}
Model.InvalidatePlot(true);
}
public void SetStatusSeriesStopPlot(List<WaterQualityVO> collection,
FieldItem yAxisField, bool showMarker = false)
{
Model.Series.Clear();
Model.Axes.Clear();
var series = new StairStepSeries
{
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
};
int i = 0;
foreach (var r in collection.OrderBy(r => r.RecordedTime))
{
string? rawValue = ResolveStatus(r, yAxisField.Name);
if (rawValue != null)
{
double y = MapDeviceStatus(rawValue);
series.Points.Add(new DataPoint(i, y));
}
else
{
double? uvPower = ResolveUvPowerPerId(r, yAxisField.Name);
if (uvPower.HasValue)
{
series.Points.Add(new DataPoint(i, uvPower.Value));
}
}
i++;
}
Model.Series.Add(series);
Model.InvalidatePlot(true);
}
public void SetStackAreaPlot(List<WaterQualityVO> collection, ObservableCollection<FieldItem> yFields,
DataType dataType,
bool showMarker, bool showLegends)
{
Model.Series.Clear();
Model.Axes.Clear();
// X축: 시간
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "시간",
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Minutes,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
// Y축: 전력
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = dataType == DataType.Energy ? "전력 (kW)" : "온실가스 (tCO₂)",
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.Dot
};
Model.Axes.Add(xAxis);
Model.Axes.Add(yAxis);
// 시간순 정렬
var records = collection.OrderBy(r => r.RecordedTime).ToList();
int n = records.Count;
// 누적 버퍼
double[] prevStack = new double[n];
foreach (var field in yFields)
{
var area = new AreaSeries
{
Title = field.Display,
StrokeThickness = 1,
MarkerType = showMarker ? MarkerType.Circle : MarkerType.None,
};
var upper = new List<DataPoint>();
var lower = new List<DataPoint>();
for (int i = 0; i < n; i++)
{
double? y = dataType == DataType.Energy ?
ResolveEnergyField(records[i], field.Name!) :
ResolveGreenhouseGas(records[i], field.Name!);
double value = y ?? 0;
double x = DateTimeAxis.ToDouble(records[i].RecordedTime);
// 현재 값 누적
double bottom = prevStack[i];
double top = bottom + value;
lower.Add(new DataPoint(x, bottom));
upper.Add(new DataPoint(x, top));
prevStack[i] = top;
}
area.Points.AddRange(upper); // 위쪽 경계
area.Points2.AddRange(lower); // 아래쪽 경계
Model.Series.Add(area);
}
// 레전드
Model.Legends.Clear();
Model.IsLegendVisible = showLegends;
Model.Legends.Add(new Legend
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.RightTop,
LegendOrientation = LegendOrientation.Vertical,
LegendTitle = dataType == DataType.Energy ? "전력 소비량" : "온실가스 배출량",
TextColor = OxyColors.Black
});
Model.InvalidatePlot(true);
}
public void SetPieChart(List<WaterQualityVO> collection, ObservableCollection<FieldItem> fields,
DataType dataType,
bool useAverage = false, bool donut = true,
double minLabelPercent = 0.03)
{
Model.Series.Clear();
Model.Axes.Clear();
if (collection == null || collection.Count == 0)
{
Model.InvalidatePlot(true);
return;
}
if (fields.Count == 0)
{
Model.InvalidatePlot(true);
return;
}
var agg = new List<(string name, double value)>();
foreach (var f in fields)
{
var values = collection.Select(r => (dataType == DataType.Energy ? ResolveEnergyField(r, f.Name!) : ResolveGreenhouseGas(r, f.Name!)) ?? 0.0);
double v = (useAverage ? (values.Any() ? values.Average() : 0.0) : values.Sum());
agg.Add((f.Display ?? f.Name!, v));
}
List<(string name, double value)> finalList;
finalList = agg;
double total = Math.Max(1e-9, finalList.Sum(x => x.value));
var ps = new PieSeries
{
AngleSpan = 360,
StartAngle = 0,
StrokeThickness = 0.5,
InsideLabelFormat = "{1}\n {0:F2}",
InsideLabelPosition = 0.8,
OutsideLabelFormat = null // 라벨은 내부만
};
if (donut) ps.InnerDiameter = 0.6; // 도넛 모드
foreach (var (name, value) in finalList)
{
var pct = value / total;
var label = (pct >= minLabelPercent) ? $"{name} ({pct:P0})" : ""; // 작은 조각 라벨 숨김
ps.Slices.Add(new PieSlice(name, value) { });
}
Model.Title = $"설비별 {(useAverage ? "" : "")} 소비 비중";
Model.Series.Add(ps);
Model.InvalidatePlot(true);
}
private DateTime FloorToBucket(DateTime dt, TimeSpan bucket)
{
long ticks = bucket.Ticks;
long floored = dt.Ticks - (dt.Ticks % ticks);
return new DateTime(floored, dt.Kind);
}
private double? ResolveTank(WaterQualityVO vo, 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,
_ => null
};
}
private double? ResolveFilter(WaterQualityVO vo, string fieldName)
{
return fieldName switch
{
"Filtering.SumpPH" => vo.Filtering.SumpPH,
"Filtering.SumpORP" => vo.Filtering.SumpORP,
"Filtering.SumpWaterLevel" => vo.Filtering.SumpWaterLevel,
"Filtering.SumpFlowRate" => vo.Filtering.SumpFlowRate,
"Filtering.SumpTemperature" => vo.Filtering.SumpTemperature,
"Filtering.FlowRate" => vo.Filtering.FlowRate,
"Filtering.HeatPumpTemperature" => vo.Filtering.HeatPumpTemperature,
_ => null
};
}
private double? ResolveSterilizer(WaterQualityVO vo, string fieldName)
{
return fieldName switch
{
"Sterilizing.OzoneDissolverPressure" => vo.Sterilizing.OzoneDissolverPressure,
_ => null
};
}
private string? ResolveStatus(WaterQualityVO vo, string field)
{
return field switch
{
"Filtering.SandFilterPower" => vo.Filtering.SandFilterPower.ToString(),
"Filtering.CirculationPumpPower" => vo.Filtering.CirculationPumpPower.ToString(),
"Filtering.InverterControllerStatus" => vo.Filtering.InverterControllerStatus,
"Filtering.HeatPumpPower" => vo.Filtering.HeatPumpPower.ToString(),
"Filtering.AirBlowerPower" => vo.Filtering.AirBlowerPower.ToString(),
"Sterilizing.OzoneGeneratorPower" => vo.Sterilizing.OzoneGeneratorPower.ToString(),
"Sterilizing.OzoneDissolverPower" => vo.Sterilizing.OzoneDissolverPower.ToString(),
"Sterilizing.ExcessOzoneDestroyerPower" => vo.Sterilizing.ExcessOzoneDestroyerPower.ToString(),
_ => null
};
}
private string ResolveTankOrTime(WaterQualityVO vo, string fieldName, TimeSpan? bucket = null)
{
return fieldName switch
{
"수조" or "Tank.Number" => vo.Tank.Number.ToString(),
"시간" or "RecordedTime" => FormatBucket(FloorToBucket(vo.RecordedTime, (TimeSpan)bucket), (TimeSpan)bucket),
_ => ""
};
}
private double? ResolveEnergyField(WaterQualityVO vo, string fieldName)
{
return fieldName switch
{
"Filtering.SandFilterEnergy" => vo.Filtering.SandFilterEnergy,
"Filtering.CirculationPumpEnergy" => vo.Filtering.CirculationPumpEnergy,
"Filtering.HeatPumpEnergy" => vo.Filtering.HeatPumpEnergy,
"Filtering.AirBlowerEnergy" => vo.Filtering.AirBlowerEnergy,
"Sterilizing.OzoneGeneratorEnergy" => vo.Sterilizing.OzoneGeneratorEnergy,
"Sterilizing.UVSterilizerEnergy" => vo.Sterilizing.UVSterilizerEnergy,
"Sterilizing.OzoneDissolverEnergy" => vo.Sterilizing.OzoneDissolverEnergy,
"Sterilizing.ExcessOzoneDestroyerEnergy" => vo.Sterilizing.ExcessOzoneDestroyerEnergy,
"TotalEnergy" => vo.TotalEnergy,
_ => null
};
}
private double? ResolveGreenhouseGas(WaterQualityVO vo, string fieldName)
{
return fieldName switch
{
"Filtering.SandFilterGreenhouseGas" => vo.Filtering.SandFilterGreenhouseGas,
"Filtering.CirculationPumpGreenhouseGas" => vo.Filtering.CirculationPumpGreenhouseGas,
"Filtering.HeatPumpGreenhouseGas" => vo.Filtering.HeatPumpGreenhouseGas,
"Filtering.AirBlowerGreenhouseGas" => vo.Filtering.AirBlowerGreenhouseGas,
"Sterilizing.OzoneGeneratorGreenhouseGas" => vo.Sterilizing.OzoneGeneratorGreenhouseGas,
"Sterilizing.UVSterilizerGreenhouseGas" => vo.Sterilizing.UVSterilizerGreenhouseGas,
"Sterilizing.OzoneDissolverGreenhouseGas" => vo.Sterilizing.OzoneDissolverGreenhouseGas,
"Sterilizing.ExcessOzoneDestroyerGreenhouseGas" => vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas,
"TotalGreenhouseGas" => vo.TotalGreenhouseGas,
_ => null
};
}
private double? ResolveUvPowerPerId(WaterQualityVO r, string fieldName)
{
// 케이스 A
if (fieldName.StartsWith("Sterilizing.UVSterilizerPower[id="))
{
var id = fieldName
.Replace("Sterilizing.UVSterilizerPower[id=", "")
.TrimEnd(']');
if (r?.Sterilizing?.UVSterilizerId == id)
return MapStatus(r.Sterilizing.UVSterilizerPower);
// 해당 시점에 이 ID의 레코드가 없으면 null (직전값 유지 로직에서 커버)
return null;
}
return null;
}
private static double MapStatus(object? v)
{
return v switch
{
bool b => b ? 1 : 0,
int i => i, // 이미 0/1/2로 들어온 경우
string s => s.Equals("True", StringComparison.OrdinalIgnoreCase) ? 1 :
s.Equals("False", StringComparison.OrdinalIgnoreCase) ? 0 :
s.Equals("Normal", StringComparison.OrdinalIgnoreCase) ? 1 :
s.Equals("Error", StringComparison.OrdinalIgnoreCase) ? 2 : -1,
_ => -1
};
}
private static string FormatBucket(DateTime t, TimeSpan bucket)
{
if (bucket >= TimeSpan.FromDays(7)) return t.ToString("yyyy-MM-dd");
if (bucket >= TimeSpan.FromDays(1)) return t.ToString("MM-dd");
return t.ToString("HH:00");
}
private double Percentile(IList<double> values, double p)
{
if (values == null || values.Count == 0) return double.NaN;
if (values.Count == 1) return (double)values[0];
double rank = p * (values.Count - 1);
int i = (int)Math.Floor(rank);
double frac = rank - i;
if (i >= values.Count - 1) return (double)values.Last();
return (double)(values[i] + (values[i + 1] - values[i]) * frac);
}
// 단순 선형회귀 계산
private (double Slope, double Intercept)? LinearRegression(List<DataPoint> points)
{
int n = points.Count;
if (n < 2) return null;
double avgX = points.Average(p => p.X);
double avgY = points.Average(p => p.Y);
double numerator = points.Sum(p => (p.X - avgX) * (p.Y - avgY));
double denominator = points.Sum(p => Math.Pow(p.X - avgX, 2));
if (denominator == 0) return null;
double slope = numerator / denominator;
double intercept = avgY - slope * avgX;
return (slope, intercept);
}
private double MapDeviceStatus(string status)
{
return status switch
{
"True" => 1, // 전원 ON
"False" => 0, // 전원 OFF
"Normal" => 1, // 정상
"Error" => 2, // 오류
_ => -1 // 알 수 없음
};
}
private class KeyComparer : IComparer<string>
{
public int Compare(string? a, string? b)
{
if (a == null || b == null) return Comparer<string>.Default.Compare(a, b);
if (a.StartsWith("수조 ") && b.StartsWith("수조 "))
{
if (int.TryParse(a.AsSpan(3), out var ai) && int.TryParse(b.AsSpan(3), out var bi))
return ai.CompareTo(bi);
}
return string.Compare(a, b, StringComparison.Ordinal);
}
}
}
}

@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
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 SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class GreenHouseGasViewModel : INotifyPropertyChanged
{
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public List<WaterQualityVO> WaterQualityList { get; set; }
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));
}
}
}
}
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 _useAverage;
public bool UseAverage { get => _useAverage; set { _useAverage = value; OnPropertyChanged(); } }
private bool _isDonut;
public bool IsDonut { get => _isDonut; set { _isDonut = value; OnPropertyChanged(); } }
public ICommand DrawGraphCommand { get; }
public GreenHouseGasViewModel()
{
WaterQualityList = Datas.GetWaterQualityVO();
GraphTypes = new ObservableCollection<GraphType>
{
GraphType.LINE,
GraphType.STACKAREA,
GraphType.PIE
};
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
GraphControlVM.SetMultiLineGraph(WaterQualityList, SelectedYFields, DataType.GreenhouseGas, ShowMarkers, ShowLegends);
break;
case GraphType.STACKAREA:
GraphControlVM.SetStackAreaPlot(WaterQualityList, SelectedYFields, DataType.GreenhouseGas, ShowMarkers, ShowLegends);
break;
case GraphType.PIE:
GraphControlVM.SetPieChart(WaterQualityList, SelectedYFields, DataType.GreenhouseGas, UseAverage, IsDonut);
break;
default:
break;
}
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime) });
AvailableFields.Add(new FieldItem { Name = "TotalGreenhouseGas", Display = "총 배출량", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterGreenhouseGas", Display = "모래여과기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpGreenhouseGas", Display = "순환펌프", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpGreenhouseGas", Display = "히트펌프", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerGreenhouseGas", Display = "에어브로와", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorGreenhouseGas", Display = "오존발생기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.UVSterilizerGreenhouseGas", Display = "자외선 살균기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverGreenhouseGas", Display = "오존용해장치", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerGreenhouseGas", Display = "배오존장치", DataType = typeof(double), Kind = StepFieldKind.Energy });
}
private void RebuildFieldCandidates()
{
// 후보 초기화
XFieldCandidates.Clear();
YFieldCandidates.Clear();
// X축: 시간 우선
foreach (var f in AvailableFields)
{
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STACKAREA) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
IEnumerable<FieldItem> src = AvailableFields;
if (SelectedGraphType is GraphType.LINE or GraphType.STACKAREA or GraphType.PIE)
{
// 수치형만 (LINE/STACKAREA/PIE 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
// Y축 후보: 수치형
foreach (var f in src)
{
if (SelectedGraphType is GraphType.STACKAREA or GraphType.PIE && f.Name.Equals("TotalEnergy")) continue;
YFieldCandidates.Add(f);
}
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();
SelectedYField = null;
switch (SelectedGraphType)
{
case GraphType.LINE:
//var def = YFieldCandidates.FirstOrDefault();
//if (def != null) SelectedYFields.Add(def);
break;
case GraphType.STACKAREA:
SelectedYField = YFieldCandidates.FirstOrDefault();
break;
case GraphType.PIE:
//SelectedYField = YFieldCandidates.FirstOrDefault();
break;
}
OnPropertyChanged(nameof(SelectedYFields));
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class MainViewModel : INotifyPropertyChanged
{
private object? _selectedViewModel;
public object? SelectedViewModel
{
get => _selectedViewModel;
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged(nameof(SelectedViewModel));
}
}
}
public ICommand SwapViewCommand { get; }
public MainViewModel()
{
SwapViewCommand = new RelayCommand(SwapView);
SelectedViewModel = new MonitoringViewModel(); // Default view
}
private void SwapView(object obj)
{
var tag = (string)obj;
switch (tag)
{
case "monitoring":
SelectedViewModel = new MonitoringViewModel();
break;
case "energy":
SelectedViewModel = new EnergyViewModel();
break;
case "greenHouseGas":
SelectedViewModel = new GreenHouseGasViewModel();
break;
case "cctv":
SelectedViewModel = new CCTVViewModel();
break;
default:
throw new ArgumentException("Unknown view tag", nameof(obj));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,481 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalisys;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
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 class MonitoringViewModel : INotifyPropertyChanged
{
#region Properties
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public List<WaterQualityVO> WaterQualityList { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; }
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);
}
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
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 _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 = WaterQualityVO.GetSampleData(new DateTime(2025, 8, 1), new DateTime(2025, 8, 1), 10);
Datas.SetWaterQualityVO(WaterQualityList);
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))
);
GraphTypes = [];
SelectedTab = MonitorTab.Tank; // Default system
SetGraphType();
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
}
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, SelectedTab, xField, yField, isMarker);
break;
case GraphType.BOX:
var xFieldBox = SelectedXField;
var dataFieldBox = SelectedYField;
var boxWidth = BoxWidth;
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
GraphControlVM.SetBoxPlot(WaterQualityList, xFieldBox, dataFieldBox, boxWidth, boxTimeSpan);
break;
case GraphType.SCATTER:
var xFieldScatter = SelectedXField;
var yFiledScatter = SelectedYField;
var markerSIze = ScatterMarkerSize;
var showRegression = ShowRegression;
GraphControlVM.SetScatterPlot(WaterQualityList, xFieldScatter, yFiledScatter, markerSIze, showRegression);
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, yFiledStep, showMarkerStep);
else
GraphControlVM.SetStepPlot(WaterQualityList, SelectedTab, xFieldStep, tFieldsStep, showMarkerStep);
break;
default:
break;
}
}
private void SetGraphData_Line_Tank(FieldItem? xField, FieldItem? yField, bool showMarkers)
{
if (SelectedTab != MonitorTab.Tank) return;
if (xField?.Name != "RecordedTime" || yField == null) return;
if (SelectedWaterTanks.Count == 0) return;
GraphControlVM.SetTankLineGraph(SelectedWaterTanks, xField, yField, showMarkers);
}
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;
}), System.Windows.Threading.DispatcherPriority.Background);
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime) });
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 });
}
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.SCATTER && f.Name.Equals("Tank.Number")) continue;
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STEP) break;
if (SelectedGraphType == GraphType.BOX && f.Name.Equals("Tank.Number")) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
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:
var def = YFieldCandidates.FirstOrDefault();
if (def != null) SelectedYFields.Add(def);
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));
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Loading…
Cancel
Save