using System.Data; using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Avalonia; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using SkiaSharp; using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.Painting.Effects; using UnitsNet; using MathNet.Numerics.Statistics; using GSS2.Core.Hardware; using GSS2.Core.Analysis.AmineContent; using GSS2.Core.Analysis.AmineContent.Database; using GSS2.Core.Extensions; using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.Services; namespace GSS2.ViewModels.AmineContent; public partial class AnalysisViewModel : ViewModelBase { /// /// Внутренняя модель элементов меню /// public partial class MenuItemViewModel : ViewModelBase { [ObservableProperty] public partial int Id { get; set; } [ObservableProperty] public partial string SampleName { get; set; } public MenuItemViewModel(int id, string sampleName) { Id = id; SampleName = sampleName; } } /// /// Внутренняя модель элементов выпадающего списка сепараторов /// public partial class SeparatorComboBoxItemViewModel : ViewModelBase { [ObservableProperty] public partial int Id { get; set; } [ObservableProperty] public partial string Type { get; set; } public SeparatorComboBoxItemViewModel(int id, string type) { Id = id; Type = type; } } /// /// Внутренняя модель элементов выпадающего списка марок /// public partial class BrandComboBoxItemViewModel : ViewModelBase { [ObservableProperty] public partial int Id { get; set; } [ObservableProperty] public partial string BrandName { get; set; } [ObservableProperty] public partial string MixtureName { get; set; } public BrandComboBoxItemViewModel(int id, string brandName, string mixtureName) { Id = id; BrandName = brandName; MixtureName = mixtureName; } } /// /// Внутренняя модель записи /// public partial class RecordViewModel : ViewModelBase { public const double UNPROCESSED_THRESHOLD = 0.6; public const double HOTSPOT_THRESHOLD = 1.1; public const double EPSILON = 1e-10; public const double UNPROCESSED_FRACTION_THRESHOLD = 50; /// /// Модель гистограммы /// public partial class HistogramViewModel : ViewModelBase { /// /// Оси абсцисс /// [ObservableProperty] public partial Axis[] XAxis { get; set; } = [new Axis()]; /// /// Оси ординат /// [ObservableProperty] public partial Axis[] YAxis { get; set; } = [new Axis()]; /// /// Серии (значения) /// [ObservableProperty] public partial ISeries[] Series { get; set; } = []; /// /// Очистить гистограмму /// public void Clear() { XAxis = [new Axis()]; YAxis = [new Axis()]; Series = Array.Empty(); } /// /// Заполнить гистограмму /// /// Значения /// Левая граница оси абсцисс /// Правая граница оси абсцисс /// Текст слева /// Текст снизу /// Текст легенды public void Set(IEnumerable values, double? min = null, double? max = null, string leftText = "", string bottomText = "", string legend = "") { if (values is null || values.Count() == 0) { Clear(); return; } var vals = values.Select(v => (double)v).ToArray(); double minVal = min ?? vals.Min(); double maxVal = max ?? vals.Max(); int binCount = 20; double binSize = (maxVal - minVal) / binCount; var binsPDF = new int[binCount]; var binsCDF = new float[binCount]; foreach (var v in vals) { int index = (int)((v - minVal) / binSize); if (index >= binCount) index = binCount - 1; if (index < 0) index = 0; binsPDF[index]++; for (int i = index; i < binCount; i++) binsCDF[i] += 100.0f / values.Count(); } // Подписи для оси X var labels = new string[binCount]; for (int i = 0; i < binCount; i++) { double from = minVal + i * binSize; double to = from + binSize; labels[i] = $"{from:F2}"; } Series = [ new ColumnSeries { Values = binsPDF, Name = legend, Fill = new SolidColorPaint(SKColors.White), ScalesYAt = 0 } ]; XAxis = [ new Axis { Labels = labels, LabelsRotation = 45, TextSize = 14, Name = bottomText, NameTextSize = 14, NamePaint=new SolidColorPaint(SKColors.White), SeparatorsPaint = new SolidColorPaint(SKColors.White), LabelsPaint = new SolidColorPaint(SKColors.White), TicksPaint = new SolidColorPaint(SKColors.White), SubticksPaint = new SolidColorPaint(SKColors.White) } ]; YAxis = [ new Axis { TextSize = 14, MinLimit = 0, Name=leftText, NameTextSize = 14, NamePaint=new SolidColorPaint(SKColors.White), Position = LiveChartsCore.Measure.AxisPosition.Start, SeparatorsPaint = new SolidColorPaint(SKColors.White), LabelsPaint = new SolidColorPaint(SKColors.White), TicksPaint = new SolidColorPaint(SKColors.White), SubticksPaint = new SolidColorPaint(SKColors.White) } ]; } /// /// Заполнить гистограмму /// /// Значения /// Левая граница оси абсцисс /// Правая граница оси абсцисс /// Текст слева /// Текст снизу /// Текст легенды public void Set(IEnumerable values, double? min = null, double? max = null, string leftText = "", string bottomText = "", string legend = "") => Set(values.Select(v => (float)v), min, max, leftText, bottomText, legend); } #region Основные свойства /// /// /// [ObservableProperty] public partial int Id { get; set; } = 0; /// /// /// [ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString(); /// /// /// [ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow; /// /// /// [ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow; /// /// /// [ObservableProperty] public partial string SampleName { get; set; } = ""; /// /// /// [ObservableProperty] public partial string SampleComment { get; set; } = ""; /// /// /// [ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow; /// /// /// [ObservableProperty] public partial string SamplingPlace { get; set; } = ""; /// /// /// [ObservableProperty] public partial string SeparatorType { get; set; } = ""; /// /// /// [ObservableProperty] public partial string BrandName { get; set; } = ""; /// /// /// [ObservableProperty] public partial string MixtureName { get; set; } = ""; /// /// /// [ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1; /// /// Представление /// [ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new(); /// /// Представление /// [ObservableProperty] public partial AsyncImageRecordViewModel MaskImage { get; set; } = new(); /// /// Представление /// [ObservableProperty] public partial AsyncImageRecordViewModel ResultImage { get; set; } = new(); /// /// /// [ObservableProperty] public partial int ParticlesCount { get; set; } = -1; /// /// Данные /// [ObservableProperty] public partial List Values { get; set; } = new(); /// /// Данные /// [ObservableProperty] public partial List Xs { get; set; } = new(); /// /// Данные /// [ObservableProperty] public partial List Ys { get; set; } = new(); #endregion #region Общие свойства /// /// Средняя нормированная обработанная площадь, % /// [ObservableProperty] public partial double MeanProcessedArea { get; set; } = new(); /// /// Средняя нормированная площадь хотспотов, % /// [ObservableProperty] public partial double MeanHotspotArea { get; set; } = new(); /// /// Средний интегральный оптический показатель /// [ObservableProperty] public partial double MeanOpticalIntegral { get; set; } = new(); /// /// Средняя внутригранульная неоднородность /// [ObservableProperty] public partial double MeanInnerHeterogeneity { get; set; } = new(); /// /// Среднее внутригранульное среднеквадратичное отклонение /// [ObservableProperty] public partial double MeanInnerStdDev { get; set; } = new(); /// /// 50-й процентиль нормированной обработанной площади (медиана), % /// [ObservableProperty] public partial double ProcessedAreaP50 { get; set; } = new(); /// /// 10-й процентиль нормированной обработанной площади, % /// [ObservableProperty] public partial double ProcessedAreaP10 { get; set; } = new(); /// /// 90-й процентиль нормированной обработанной площади, % /// [ObservableProperty] public partial double ProcessedAreaP90 { get; set; } = new(); /// /// Ширина центрального диапазона распределения нормированной обработанной площади, п.п. /// [ObservableProperty] public partial double ProcessedAreaDeltaP90P10 { get; set; } = new(); /// /// Среднее квадратичное отклонение нормированной обработанной площади /// [ObservableProperty] public partial double ProcessedAreaStdDev { get; set; } = new(); /// /// Межгранульная неоднородность /// [ObservableProperty] public partial double InterHeterogeneity { get; set; } = new(); /// /// Доля необработанных гранул, тех гранул нормированная НЕобработанная площадь которых больше % /// [ObservableProperty] public partial double UnprocessedFraction { get; set; } = new(); /// /// Нормализованные значения, где
/// от 0 до 1 - необработанный пиксель (в изначальных значениях от 0 до * x)
/// от 1 до 2 - обработанный пиксель (в изначальных значениях от * x до * x)
/// от 2 до 3 - хотспот (в изначальных значениях от * x до максимального значения) ///
public List NormalizedValues { get; set; } = new(); /// /// Нормированные обработанные площади /// public List ProcessedAreas { get; set; } = new(); /// /// Нормированные необработанные площади /// public List UnprocessedAreas { get; set; } = new(); /// /// Нормированные площади хотспотов /// public List HotspotAreas { get; set; } = new(); /// /// Интегральные оптические показатели /// public List OpticalIntegrals { get; set; } = new(); /// /// Внутригранульные среднеквадратичные отклонения /// public List InnerStdDevs { get; set; } = new(); /// /// Внутригранульные неоднородности /// public List InnerHeterogeneities { get; set; } = new(); /// /// Гистограмма нормированных обработанных площадей /// [ObservableProperty] public partial HistogramViewModel ProcessedAreasHistogram { get; set; } = new(); /// /// Гистограмма нормированных необработанных площадей /// [ObservableProperty] public partial HistogramViewModel UnprocessedAreasHistogram { get; set; } = new(); /// /// Гистограмма нормированных площадей хотспотов /// [ObservableProperty] public partial HistogramViewModel HotspotAreasHistogram { get; set; } = new(); /// /// Гистограмма интегральных оптических показателей /// [ObservableProperty] public partial HistogramViewModel OpticalIntegralsHistogram { get; set; } = new(); /// /// Гистограмма внутригранульных среднеквадратичных отклонений /// [ObservableProperty] public partial HistogramViewModel InnerStdDevsHistogram { get; set; } = new(); /// /// Гистограмма внутригранульных неоднородностей /// [ObservableProperty] public partial HistogramViewModel InnerHeterogeneitiesHistogram { get; set; } = new(); #endregion #region Свойства отдельных гранул /// /// Индекс выбранной гранулы /// [ObservableProperty] public partial int ParticleIndex { get; set; } = 0; /// /// Выбрано ли изображение /// [ObservableProperty] public partial bool ParticleImageIsVisible { get; set; } = true; /// /// Выбрано ли изображение /// [ObservableProperty] public partial bool ParticleMaskImageIsVisible { get; set; } = false; /// /// Выбрано ли изображение /// [ObservableProperty] public partial bool ParticleResultImageIsVisible { get; set; } = false; /// /// Масштаб изображений , и /// [ObservableProperty] public partial float ParticleImagesZoom { get; set; } = 1; /// /// Смещение по X изображений , и /// [ObservableProperty] public partial float ParticleImagesPanX { get; set; } = 0; /// /// Смещение по Y изображений , и /// [ObservableProperty] public partial float ParticleImagesPanY { get; set; } = 0; /// /// Ширина изображений , и /// [ObservableProperty] public partial Rect ParticleImagesBounds { get; set; } = new Rect(); /// /// /// Нормированная обработанная площадь выбранной гранулы /// [ObservableProperty] public partial double ParticleProcessedArea { get; set; } = new(); /// /// Нормированная необработанная площадь выбранной гранулы /// [ObservableProperty] public partial double ParticleUnprocessedArea { get; set; } = new(); /// /// Нормированная площадь хотспотов выбранной гранулы /// [ObservableProperty] public partial double ParticleHotspotArea { get; set; } = new(); /// /// Интегральный оптический показатель выбранной гранулы /// [ObservableProperty] public partial double ParticleOpticalIntegral { get; set; } = new(); /// /// Внутригранульное среднеквадратичное отклонение выбранной гранулы /// [ObservableProperty] public partial double ParticleInnerStdDev { get; set; } = new(); /// /// Внутригранульная неоднородность выбранной гранулы /// [ObservableProperty] public partial double ParticleInnerHeterogeneity { get; set; } = new(); /// /// Гистограмма нормированных значений выбранной гранулы, где
/// от 0 до 1 - необработанный пиксель (в изначальных значениях от 0 до * x)
/// от 1 до 2 - обработанный пиксель (в изначальных значениях от * x до * x)
/// от 2 до 3 - хотспот (в изначальных значениях от * x до максимального значения) ///
[ObservableProperty] public partial HistogramViewModel ParticleValuesHistogram { get; set; } = new(); #endregion /// /// Пересчитать вычисляемые свойства /// public void CalculateProperties() { // Очищаем списки NormalizedValues.Clear(); ProcessedAreas.Clear(); UnprocessedAreas.Clear(); HotspotAreas.Clear(); OpticalIntegrals.Clear(); InnerStdDevs.Clear(); InnerHeterogeneities.Clear(); ( ParticlesCount, _, NormalizedValues, ProcessedAreas, UnprocessedAreas, HotspotAreas, OpticalIntegrals, InnerStdDevs, InnerHeterogeneities, MeanProcessedArea, ProcessedAreaP10, ProcessedAreaP50, ProcessedAreaP90, ProcessedAreaStdDev, InterHeterogeneity, ProcessedAreaDeltaP90P10, UnprocessedFraction, MeanInnerHeterogeneity, MeanInnerStdDev, MeanHotspotArea, MeanOpticalIntegral ) = ResultsPostProcessor.PostProcess(Values, MixtureNormalRate); // Обновляем гистограммы ProcessedAreasHistogram.Set(ProcessedAreas, 0, 100, "Количество гранул, шт", "Нормированная обработанная площадь, %"); UnprocessedAreasHistogram.Set(UnprocessedAreas, 0, 100, "Количество гранул, шт", "Нормированная необработанная площадь, %"); HotspotAreasHistogram.Set(HotspotAreas, 0, 100, "Количество гранул, шт", "Нормированная горячая площадь, %"); OpticalIntegralsHistogram.Set(OpticalIntegrals, 0, 3, "Количество гранул, шт", "Оптический интегральный показатель, %"); InnerStdDevsHistogram.Set(InnerStdDevs, InnerStdDevs.Min(), InnerStdDevs.Max(), "Количество гранул, шт", "Внутригранульное среднеквадратичное отклонение"); InnerHeterogeneitiesHistogram.Set(InnerHeterogeneities, InnerHeterogeneities.Min(), InnerHeterogeneities.Max(), "Количество гранул, шт", "Внутригранульная однородность"); // Сбрасываем индекс выбранной гранулы ParticleIndex = 0; ZoomPanOnParticle(); } /// /// Переключится на следующую гранулу /// [RelayCommand] public void NextParticle() { if (ParticlesCount == 0) return; ParticleIndex = (ParticleIndex + 1) % ParticlesCount; } /// /// Переключится на предыдущую гранулу /// [RelayCommand] public void PreviousParticle() { if (ParticlesCount == 0) return; // ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount; } /// /// Переключить на изображение /// [RelayCommand] private void SwitchToImage() { ParticleImageIsVisible = true; ParticleMaskImageIsVisible = false; ParticleResultImageIsVisible = false; } /// /// Переключить на изображение /// [RelayCommand] private void SwitchToMaskImage() { ParticleImageIsVisible = false; ParticleMaskImageIsVisible = true; ParticleResultImageIsVisible = false; } /// /// Переключить на изображение /// [RelayCommand] private void SwitchToResultImage() { ParticleImageIsVisible = false; ParticleMaskImageIsVisible = false; ParticleResultImageIsVisible = true; } /// /// Вызывается при изменении /// partial void OnParticleIndexChanged(int value) { // Если количество частиц меньше или равно 0 if (ParticlesCount <= 0) { // Сбрасываем общие значения ParticleProcessedArea = -1; ParticleUnprocessedArea = -1; ParticleHotspotArea = -1; ParticleOpticalIntegral = -1; ParticleInnerStdDev = -1; ParticleInnerHeterogeneity = -1; ParticleValuesHistogram.Clear(); return; } // Заполняем общие значения из списков ParticleProcessedArea = ProcessedAreas[value]; ParticleUnprocessedArea = UnprocessedAreas[value]; ParticleHotspotArea = HotspotAreas[value]; ParticleOpticalIntegral = OpticalIntegrals[value]; ParticleInnerStdDev = InnerStdDevs[value]; ParticleInnerHeterogeneity = InnerHeterogeneities[value]; // Обновляем гистограмму ParticleValuesHistogram.Set(NormalizedValues[value], 0, 3, "Количество, шт", "Значение"); // Обновляем изображение ZoomPanOnParticle(); } partial void OnParticleImagesBoundsChanged(Rect value) { ZoomPanOnParticle(); } private void ZoomPanOnParticle() { if (ParticleIndex < 0 || ParticleIndex >= Xs.Count() || ParticleIndex >= Ys.Count()) return; if (ParticleImagesBounds.Width == 0 || ParticleImagesBounds.Height == 0) return; var minX = Xs[ParticleIndex].Min(); var maxX = Xs[ParticleIndex].Max(); var minY = Ys[ParticleIndex].Min(); var maxY = Ys[ParticleIndex].Max(); ParticleImagesZoom = MathF.Max((float)ParticleImagesBounds.Width, (float)ParticleImagesBounds.Height) * MathF.Max(maxX - minX, maxY - minY) / AnalyzerService.ANALYSIS_IMAGE_SIZE; ParticleImagesPanX = -((maxX + minX) / 2 / AnalyzerService.ANALYSIS_IMAGE_SIZE - 0.5f) * (float)ParticleImagesBounds.Width * ParticleImagesZoom; ParticleImagesPanY = -((maxY + minY) / 2 / AnalyzerService.ANALYSIS_IMAGE_SIZE - 0.5f) * (float)ParticleImagesBounds.Height * ParticleImagesZoom; } } public event EventHandler? UpdateCharts; /// /// Логгер /// private readonly ILogger _logger; /// /// Контекст базы данных /// private readonly Context _context; /// /// Сервис для получения изображений с камеры /// private readonly IImageCapturerService _imageCapturer; /// /// Сервис анализатора /// private readonly AnalyzerService _analyzer; /// /// Сервис управления сигнальными светодиодами /// private readonly LightsService _lights; /// /// ViewLocator для получения других View по их моделям /// private readonly DependencyInjectionViewLocator _viewLocator; /// /// Сервис навигации между экранами /// private readonly NavigationService _navigation; /// /// Задача съёмки изображений /// private Task? _сaptureImagesTask = null; /// /// Токен отмены задачи съёмки изображений /// private CancellationTokenSource? _captureImagesTaskCts = null; /// /// Меню открыто /// [ObservableProperty] public partial bool MenuOpened { get; set; } = false; /// /// Элементы меню /// [ObservableProperty] public partial ObservableCollection MenuItems { get; set; } = new(); /// /// Текущий выбранный элемент меню. Равен -1 если элемент не выбран /// [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; /// /// Запись /// [ObservableProperty] public partial RecordViewModel Record { get; set; } = new(); /// /// Выбрано ли изображение /// [ObservableProperty] public partial bool ImageIsVisible { get; set; } = true; /// /// Выбрано ли изображение /// [ObservableProperty] public partial bool MaskImageIsVisible { get; set; } = false; /// /// Выбрано ли изображение /// [ObservableProperty] public partial bool ResultImageIsVisible { get; set; } = false; /// /// Масштаб изображений , и /// [ObservableProperty] public partial float ImagesZoom { get; set; } = 1; /// /// Смещение по X изображений , и /// [ObservableProperty] public partial float ImagesPanX { get; set; } = 0; /// /// Смещение по Y изображений , и /// [ObservableProperty] public partial float ImagesPanY { get; set; } = 0; /// /// Список элементов выпадающего списка сепараторов /// [ObservableProperty] public partial ObservableCollection SeparatorComboBoxItems { get; set; } = new(); /// /// Текущий выбранный элемент выпадающего списка сепараторов. Равен -1 если элемент не выбран /// [ObservableProperty] public partial int SeparatorComboBoxSelectedIndex { get; set; } = -1; /// /// Список элементов выпадающего списка марок /// [ObservableProperty] public partial ObservableCollection BrandComboBoxItems { get; set; } = new(); /// /// Текущий выбранный элемент выпадающего списка марок. Равен -1 если элемент не выбран /// [ObservableProperty] public partial int BrandComboBoxSelectedIndex { get; set; } = -1; /// /// Текущий экран главной секции /// [ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0; /// /// Можно ли переходить на следующий экран главной секции /// [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; /// /// Можно ли переходить на предыдущий экран главной секции /// [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = false; /// /// Происходит ли переход между экранами главной секции /// [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; /// /// Текущий экран информационной секции /// [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; /// /// Можно ли переходить на следующий экран информационной секции /// [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; /// /// Можно ли переходить на предыдущий экран информационной секции /// [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false; /// /// Происходит ли переход между экранами информационной секции /// [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false; /// /// Проводится ли съёмка изображений /// [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; /// /// Прогресс съёмки изображений /// [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; /// /// Можно ли взаимодействовать с объектом /// [ObservableProperty] public partial bool CanInteract { get; set; } = true; /// /// Можно ли снять изображение /// [ObservableProperty] public partial bool CanCapture { get; set; } = true; /// /// Создаётся ли новая запись /// [ObservableProperty] public partial bool IsNewRecord { get; set; } = true; public AnalysisViewModel( ILogger logger, Context context, IImageCapturerService imageCapturer, AnalyzerService analyzer, LightsService lights, DependencyInjectionViewLocator viewLocator, NavigationService navigation ) { // Устанавливаем readonly поля _logger = logger; _context = context; _imageCapturer = imageCapturer; _analyzer = analyzer; _lights = lights; _viewLocator = viewLocator; _navigation = navigation; _logger.LogInformation("Инициализация"); // Получаем записи выпадающего списка сепараторов из базы данных SeparatorComboBoxItems = new ObservableCollection(_context.SeparatorRecords.Select(r => new SeparatorComboBoxItemViewModel(r.Id, r.Type))); // Получаем записи выпадающего списка марок из базы данных BrandComboBoxItems = new ObservableCollection(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName))); // Подписываемся на изменения базы данных для отслеживания изменений списка сепараторов и марок _context.ChangeTracker.StateChanged += (_, ea) => { // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { // Если запись является SeparatorRecord if (ea.Entry.Entity is SeparatorRecord separatorRecord) { // Если запись добавлена if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged) { // Добавляем запись SeparatorComboBoxItems.Add(new SeparatorComboBoxItemViewModel(separatorRecord.Id, separatorRecord.Type)); } // Если запись удалена else if (ea.NewState is EntityState.Deleted) { // Ищем удалённую запись в выпадающем списке сепараторов var deletedComboBoxItem = SeparatorComboBoxItems.FirstOrDefault(r => r.Id == separatorRecord.Id); // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку if (deletedComboBoxItem is null) { _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems)); return; } // Берём индекс удалённого элемента в выпадающем списке марок var deletedComboBoxIndex = SeparatorComboBoxItems.IndexOf(deletedComboBoxItem); // Если текущий индекс равен удаляемому, то устанавливаем, что элемент не выбран if (SeparatorComboBoxSelectedIndex == deletedComboBoxIndex) SeparatorComboBoxSelectedIndex = -1; // Удаляем элемент из выпадающего списка марок SeparatorComboBoxItems.RemoveAt(deletedComboBoxIndex); } // Если запись изменилась else if (ea.NewState is EntityState.Modified) { // Ищем изменённую запись в выпадающем списке марок var editedComboBoxItem = SeparatorComboBoxItems.FirstOrDefault(r => r.Id == separatorRecord.Id); // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку if (editedComboBoxItem is null) { _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems)); return; } // Берём индекс изменённого элемента в выпадающем списке марок var editedComboBoxIndex = SeparatorComboBoxItems.IndexOf(editedComboBoxItem); // Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи if (SeparatorComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord) Record.SeparatorType = separatorRecord.Type; // Обновляем элемент из выпадающего списка марок editedComboBoxItem.Type = separatorRecord.Type; } } // Если запись является BrandRecord if (ea.Entry.Entity is BrandRecord brandRecord) { // Если запись добавлена if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged) { // Добавляем запись BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName)); } // Если запись удалена else if (ea.NewState is EntityState.Deleted) { // Ищем удалённую запись в выпадающем списке марок var deletedComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id); // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку if (deletedComboBoxItem is null) { _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems)); return; } // Берём индекс удалённого элемента в выпадающем списке марок var deletedComboBoxIndex = BrandComboBoxItems.IndexOf(deletedComboBoxItem); // Если текущий индекс равен удаляемому, то устанавливаем, что элемент не выбран if (BrandComboBoxSelectedIndex == deletedComboBoxIndex) BrandComboBoxSelectedIndex = -1; // Удаляем элемент из выпадающего списка марок BrandComboBoxItems.RemoveAt(deletedComboBoxIndex); } // Если запись изменилась else if (ea.NewState is EntityState.Modified) { // Ищем изменённую запись в выпадающем списке марок var editedComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id); // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку if (editedComboBoxItem is null) { _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems)); return; } // Берём индекс изменённого элемента в выпадающем списке марок var editedComboBoxIndex = BrandComboBoxItems.IndexOf(editedComboBoxItem); // Если текущий индекс равен изменённому и это новая записть, то обновляем данные в записи if (BrandComboBoxSelectedIndex == editedComboBoxIndex && IsNewRecord) { Record.BrandName = brandRecord.BrandName; Record.MixtureName = brandRecord.MixtureName; Record.MixtureNormalRate = brandRecord.MixtureNormalRate; } // Обновляем элемент из выпадающего списка марок editedComboBoxItem.BrandName = brandRecord.BrandName; editedComboBoxItem.MixtureName = brandRecord.MixtureName; } } }); }; // Получаем записи меню из базы данных MenuItems = new ObservableCollection(_context.ResultRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName))); if (MenuItems.Count() > 0) // Если записи есть, MenuSelectedIndex = 0; // Выбираем первую запись else // Если записей нет, MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана // Подписываемся на изменения свойств для обновления CanInteract и обновления графиков PropertyChanged += (_, ea) => { if (ea.PropertyName == nameof(MenuOpened) || ea.PropertyName == nameof(MainSectionIsScrolling) || ea.PropertyName == nameof(InfoSectionIsScrolling)) UpdateCanInteract(); if (ea.PropertyName == nameof(BrandComboBoxSelectedIndex) || ea.PropertyName == nameof(SeparatorComboBoxSelectedIndex)) UpdateCanCapture(); if (ea.PropertyName == nameof(MenuSelectedIndex) || ea.PropertyName == nameof(InfoSectionCurrentIndex)) UpdateCharts?.Invoke(this, EventArgs.Empty); }; _logger.LogInformation("Инициализировано"); } /// /// Вызывается при изменении /// partial void OnMenuSelectedIndexChanged(int value) { // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку if (value >= MenuItems.Count()) _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count()); // Если значение меньше 0, то значит элемент меню не выбран if (value >= 0 && value < MenuItems.Count()) { _logger.LogTrace("Выбран элемент из списка: {}", value); // Находим запись в базе данных var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id); // Если запись не найдена if (selectedRecord is null) { // Логгируем как ошибку _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.ResultRecords)); var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями MenuSelectedIndex = -1; // Удаляем несуществующий элемент из списка MenuItems.RemoveAt(nonExistingIndex); return; } // Если запись найдена, то заполняем поля из базы данных // Заполняем простые поля Record.Id = selectedRecord.Id; Record.Uuid = selectedRecord.Uuid; Record.CreateDateTime = selectedRecord.CreateDateTime; Record.EditDateTime = selectedRecord.EditDateTime; Record.SampleName = selectedRecord.SampleName; Record.SampleComment = selectedRecord.SampleComment; Record.SamplingDateTime = selectedRecord.SamplingDateTime; Record.SamplingPlace = selectedRecord.SamplingPlace; Record.SeparatorType = selectedRecord.SeparatorType; Record.BrandName = selectedRecord.BrandName; Record.MixtureName = selectedRecord.MixtureName; Record.MixtureNormalRate = selectedRecord.MixtureNormalRate; Record.ParticlesCount = selectedRecord.ParticlesCount; Record.Values = selectedRecord.ValueData.Chunk(selectedRecord.ValueCount).Select(v => v.ToArray()).ToList(); Record.Xs = selectedRecord.XData.Chunk(selectedRecord.XCount).Select(v => v.ToArray()).ToList(); Record.Ys = selectedRecord.YData.Chunk(selectedRecord.YCount).Select(v => v.ToArray()).ToList(); // Ищем изображение в базе данных var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); // Если изображение не найдено в базе данных, то логгируем как ошибку if (imageRecord is null) _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); // В любом случае заполняем изображение Record.Image.Data = imageRecord?.Base64 ?? ""; // Ищем изображение маски в базе данных var maskImageRecord = _context.ImageRecords.Find(selectedRecord.MaskImageId); // Если изображение не найдено в базе данных, то логгируем как ошибку if (maskImageRecord is null) _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords)); // В любом случае заполняем изображение Record.MaskImage.Data = maskImageRecord?.Base64 ?? ""; // Ищем изображение результата в базе данных var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId); // Если изображение не найдено в базе данных, то логгируем как ошибку if (resultImageRecord is null) _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords)); // В любом случае заполняем изображение Record.ResultImage.Data = resultImageRecord?.Base64 ?? ""; // Сбрасываем приближение изображений ImagesPanX = 0; ImagesPanY = 0; ImagesZoom = 1; // Устанавливаем, что запись выбрана из базы данных IsNewRecord = false; // Вычисляем дополнительные свойства Record.CalculateProperties(); CanCapture = false; return; } // Если элемент не выбран, заполняем поля пустыми значениями _logger.LogTrace("Выбран новый элемент"); var dateTime = DateTime.UtcNow; Record.Id = 0; Record.Uuid = Guid.NewGuid().ToString(); Record.CreateDateTime = dateTime; Record.EditDateTime = dateTime; Record.SampleName = ""; Record.SampleComment = ""; Record.SamplingDateTime = dateTime; Record.SamplingPlace = ""; Record.SeparatorType = ""; Record.BrandName = ""; Record.MixtureName = ""; Record.MixtureNormalRate = -1; Record.Image.Data = ""; Record.MaskImage.Data = ""; Record.ResultImage.Data = ""; Record.Values = new List(); Record.Xs = new List(); Record.Ys = new List(); // Сбрасываем приближение изображений ImagesPanX = 0; ImagesPanY = 0; ImagesZoom = 1; // Устанавливаем, что запись не выбрана из базы данных IsNewRecord = true; // Вычисляем дополнительные свойства Record.CalculateProperties(); BrandComboBoxSelectedIndex = -1; SeparatorComboBoxSelectedIndex = -1; } /// /// Вызывается при изменении /// partial void OnBrandComboBoxSelectedIndexChanged(int value) { if (!IsNewRecord) return; // Если значение выходит за верхнюю границу списка элементов выпадающего списка марок, то логгируем как ошибку if (value >= BrandComboBoxItems.Count()) _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count()); // Если значение меньше 0, то значит элемент выпадающего списка марок не выбран if (value < 0 || value >= BrandComboBoxItems.Count()) { // Логгируем как ошибку _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[value].Id, nameof(_context.BrandRecords)); // Сохраняем несуществующий индекс var nonExistingIndex = BrandComboBoxSelectedIndex; // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями BrandComboBoxSelectedIndex = -1; // Удаляем несуществующий элемент из списка BrandComboBoxItems.RemoveAt(nonExistingIndex); return; } // Находим запись в базе данных var selectedRecord = _context.BrandRecords.Find(BrandComboBoxItems[value].Id); // Если запись найдена, то заполняем поля из базы данных if (selectedRecord is not null) { Record.BrandName = selectedRecord.BrandName; Record.MixtureName = selectedRecord.MixtureName; Record.MixtureNormalRate = selectedRecord.MixtureNormalRate; return; } // Если элемент не выбран, заполняем поля пустыми значениями Record.BrandName = ""; Record.MixtureName = ""; Record.MixtureNormalRate = -1; } /// /// Вызывается при изменении /// partial void OnSeparatorComboBoxSelectedIndexChanged(int value) { if (!IsNewRecord) return; // Если значение выходит за верхнюю границу списка элементов выпадающего списка сепараторов, то логгируем как ошибку if (value >= SeparatorComboBoxItems.Count()) _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(SeparatorComboBoxSelectedIndex), SeparatorComboBoxSelectedIndex, nameof(SeparatorComboBoxItems), SeparatorComboBoxItems.Count()); // Если значение меньше 0, то значит элемент выпадающего списка сепараторов не выбран if (value < 0 || value >= SeparatorComboBoxItems.Count()) { // Логгируем как ошибку _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[value].Id, nameof(_context.SeparatorRecords)); // Сохраняем несуществующий индекс var nonExistingIndex = SeparatorComboBoxSelectedIndex; // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями SeparatorComboBoxSelectedIndex = -1; // Удаляем несуществующий элемент из списка SeparatorComboBoxItems.RemoveAt(nonExistingIndex); return; } // Находим запись в базе данных var selectedRecord = _context.SeparatorRecords.Find(SeparatorComboBoxItems[value].Id); // Если запись найдена, то заполняем поля из базы данных if (selectedRecord is not null) { Record.SeparatorType = selectedRecord.Type; return; } // Если элемент не выбран, заполняем поля пустыми значениями Record.SeparatorType = ""; } /// /// Обновить значение /// private void UpdateCanInteract() { CanInteract = !MenuOpened; CanInteract &= !MainSectionIsScrolling; CanInteract &= !InfoSectionIsScrolling; } /// /// Обновить значение /// private void UpdateCanCapture() { CanCapture = BrandComboBoxSelectedIndex >= 0; CanCapture &= SeparatorComboBoxSelectedIndex >= 0; } // Команды управления меню /// /// Открыть меню /// [RelayCommand] private void OpenMenu() => MenuOpened = true; /// /// Закрыть меню /// [RelayCommand] private void CloseMenu() => MenuOpened = false; /// /// Переключить видимость меню /// [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; /// /// Переключить главную секцию информация <-> изображения /// [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; /// /// Переключить информационную секцию на следующую /// [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--; /// /// Переключить информационную секцию на предыдущую /// [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++; /// /// Переключить на изображение /// [RelayCommand] private void SwitchToImage() { ImageIsVisible = true; MaskImageIsVisible = false; ResultImageIsVisible = false; } /// /// Переключить на изображение /// [RelayCommand] private void SwitchToMaskImage() { ImageIsVisible = false; MaskImageIsVisible = true; ResultImageIsVisible = false; } /// /// Переключить на изображение /// [RelayCommand] private void SwitchToResultImage() { ImageIsVisible = false; MaskImageIsVisible = false; ResultImageIsVisible = true; } /// /// Создать новую запись /// [RelayCommand] private void CreateRecord() { _logger.LogInformation("Создание новой записи"); // Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и // при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется, // потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1, // поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged if (MenuSelectedIndex != -1) MenuSelectedIndex = -1; else OnMenuSelectedIndexChanged(-1); } /// /// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана /// [RelayCommand] private void DeleteRecord() { try { _logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(ResultRecord.Id), Record.Id, nameof(ResultRecord.SampleName), Record.SampleName); // Если запись не выбрана, то просто создаём новую запись if (MenuSelectedIndex < 0) { _logger.LogTrace("Запись не выбрана, создаём новую"); CreateRecord(); return; } // Создаём модель для экрана подтверждения var confirmViewModel = new DeleteConfirmViewModel(_navigation) { Question = "Вы уверены, что хотите удалить запись?" }; // Создаём коллбек который вызовется при экрана подтверждения void OnConfirmCloses(object? sender, bool result) { // Если пользователь отменил удаление, то выходим из коллбека if (!result) { _logger.LogTrace("Пользователь отменил удаление"); return; } // Если запись не выбрана, то просто создаём новую запись if (MenuSelectedIndex < 0) { _logger.LogTrace("Запись не выбрана, создаём новую"); CreateRecord(); return; } // Сохраняем индекс удалённый записи и находим её в базе данных var deletedMenuIndex = MenuSelectedIndex; var deletedRecordId = Record.Id; var deletedRecord = _context.ResultRecords.Find(deletedRecordId); // Создаём новую запись CreateRecord(); // Удаляем запись из списка MenuItems.RemoveAt(deletedMenuIndex); // Если удалённая запись не найдена в базе данных, то логгируем как ошибку if (deletedRecord is null) { _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.ResultRecords)); return; } // Удаляем запись из базы данных и сохраняем изменения в базе данных _context.ResultRecords.Remove(deletedRecord); _context.SaveChanges(); _logger.LogTrace("Запись удалена"); // Отписываемся от события закрытия экрана подтверждения confirmViewModel.Closes -= OnConfirmCloses; } // Создаём View экрана подтверждения var confirmView = _viewLocator.Build(confirmViewModel); // Если View не найден, то логгируем как ошибку if (confirmView is null) { _logger.LogError("Не удалось найти View для экрана подтверждения"); return; } _logger.LogTrace("Задаём вопрос"); // Подписываемся на событие закрытия экрана подтверждения confirmViewModel.Closes += OnConfirmCloses; // Преходим на экран подтверждения, при закрытии он сам вернёт экран обратно на текущий _navigation.NavigateTo(confirmView); } catch (Exception ex) { _logger.LogError(ex, "Исключение при удалении записи"); } } /// /// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи /// [RelayCommand] private void SaveRecord() { try { _logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(ResultRecord.Id), Record.Id, nameof(ResultRecord.SampleName), Record.SampleName); // Если запись не выбрана if (MenuSelectedIndex < 0) { // Создаём запись изображения var imageId = 0; if (!string.IsNullOrEmpty(Record.Image.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения var imageRecord = new ImageRecord(base64: Record.Image.Data); // Добавляем запись изображения в базу данных var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись изображения сохранена"); imageId = attachedImageRecord.Entity.Id; } // Создаём запись маски var maskImageId = 0; if (!string.IsNullOrEmpty(Record.Image.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения var imageRecord = new ImageRecord(base64: Record.MaskImage.Data); // Добавляем запись изображения в базу данных var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись изображения сохранена"); maskImageId = attachedImageRecord.Entity.Id; } // Создаём запись изображения результата var resultImageId = 0; if (!string.IsNullOrEmpty(Record.Image.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения var imageRecord = new ImageRecord(base64: Record.ResultImage.Data); // Добавляем запись изображения в базу данных var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись изображения сохранена"); resultImageId = attachedImageRecord.Entity.Id; } _logger.LogTrace("Создаём новую запись"); var dateTime = DateTime.UtcNow; // Заполняем запись var newRecord = new ResultRecord( uuid: Record.Uuid, createDateTime: dateTime, editDateTime: dateTime, sampleName: Record.SampleName, sampleComment: Record.SampleComment, samplingDateTime: Record.SamplingDateTime, samplingPlace: Record.SamplingPlace, separatorType: Record.SeparatorType, brandName: Record.BrandName, mixtureName: Record.MixtureName, mixtureNormalRate: Record.MixtureNormalRate, imageId: imageId, maskImageId: maskImageId, resultImageId: resultImageId, particlesCount: Record.ParticlesCount, values: Record.Values, xs: Record.Xs, ys: Record.Ys ); // Добавляем запись в базу данных var attachedRecord = _context.ResultRecords.Add(newRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись сохранена"); // Добавляем новый элемент в меню MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName)); // Выбираем последний элемент из меню MenuSelectedIndex = MenuItems.Count() - 1; } // Если запись выбрана else { // Находим запись в базе данных var attachedRecord = _context.ResultRecords.Find(Record.Id); // Если запись не найдена, то логгируем как ошибку if (attachedRecord is null) { _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.ResultRecords)); return; } _logger.LogTrace("Записываем изменения в базу данных"); // Обновляем изображение { // Ищем запись изображения в базе данных var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId); // Если запись изображения не найдена if (attachedImageRecord is null) { // Если изображение есть if (!string.IsNullOrEmpty(Record.Image.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения var imageRecord = new ImageRecord(base64: Record.Image.Data); // Добавляем запись изображения в базу данных attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись изображения сохранена"); attachedRecord.ImageId = attachedImageRecord.Id; } } // Если запись изображения найдена else { // Если изображение есть if (!string.IsNullOrEmpty(Record.Image.Data)) { _logger.LogTrace("Записываем изменения в запись изображения"); // Вносим изменения attachedImageRecord.Base64 = Record.Image.Data; // Сохраняем изменения в базе данных _context.SaveChanges(); } // Если изображения нет else { // Удаляем изображение из базы данных _context.ImageRecords.Remove(attachedImageRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); attachedRecord.ImageId = 0; } } } // Обновляем маску { // Ищем запись маски в базе данных var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.MaskImageId); // Если запись изображения не найдена if (attachedImageRecord is null) { // Если изображение есть if (!string.IsNullOrEmpty(Record.MaskImage.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения var imageRecord = new ImageRecord(base64: Record.MaskImage.Data); // Добавляем запись изображения в базу данных attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись изображения сохранена"); attachedRecord.MaskImageId = attachedImageRecord.Id; } } // Если запись изображения найдена else { // Если изображение есть if (!string.IsNullOrEmpty(Record.MaskImage.Data)) { _logger.LogTrace("Записываем изменения в запись изображения"); // Вносим изменения attachedImageRecord.Base64 = Record.MaskImage.Data; // Сохраняем изменения в базе данных _context.SaveChanges(); } // Если изображения нет else { // Удаляем изображение из базы данных _context.ImageRecords.Remove(attachedImageRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); attachedRecord.MaskImageId = 0; } } } // Обновляем изображение результата { // Ищем запись маски в базе данных var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ResultImageId); // Если запись изображения не найдена if (attachedImageRecord is null) { // Если изображение есть if (!string.IsNullOrEmpty(Record.ResultImage.Data)) { _logger.LogTrace("Создаём новую запись изображения"); // Заполняем запись изображения var imageRecord = new ImageRecord(base64: Record.ResultImage.Data); // Добавляем запись изображения в базу данных attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись изображения сохранена"); attachedRecord.ResultImageId = attachedImageRecord.Id; } } // Если запись изображения найдена else { // Если изображение есть if (!string.IsNullOrEmpty(Record.ResultImage.Data)) { _logger.LogTrace("Записываем изменения в запись изображения"); // Вносим изменения attachedImageRecord.Base64 = Record.ResultImage.Data; // Сохраняем изменения в базе данных _context.SaveChanges(); } // Если изображения нет else { // Удаляем изображение из базы данных _context.ImageRecords.Remove(attachedImageRecord); // Сохраняем изменения в базе данных _context.SaveChanges(); attachedRecord.ResultImageId = 0; } } } // Определяем Id выбранной марки var selectedBrandId = 0; // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count()) _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count()); // Если значение больше 0, то достаём Id из списка элементов else if (BrandComboBoxSelectedIndex >= 0) selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id; // Записываем изменения attachedRecord.EditDateTime = DateTime.UtcNow; attachedRecord.SampleName = Record.SampleName; attachedRecord.SampleComment = Record.SampleComment; attachedRecord.SamplingDateTime = Record.SamplingDateTime; attachedRecord.SamplingPlace = Record.SamplingPlace; attachedRecord.SeparatorType = Record.SeparatorType; attachedRecord.BrandName = Record.BrandName; attachedRecord.MixtureName = Record.MixtureName; attachedRecord.MixtureNormalRate = Record.MixtureNormalRate; attachedRecord.ParticlesCount = Record.ParticlesCount; attachedRecord.ValueCount = Record.Values.Select(fs => fs.Count()).ToList(); attachedRecord.ValueData = Record.Values.SelectMany(fs => fs).ToList(); attachedRecord.XCount = Record.Xs.Select(fs => fs.Count()).ToList(); attachedRecord.XData = Record.Xs.SelectMany(fs => fs).ToList(); attachedRecord.YCount = Record.Ys.Select(fs => fs.Count()).ToList(); attachedRecord.YData = Record.Ys.SelectMany(fs => fs).ToList(); // Сохраняем изменения в базе данных _context.SaveChanges(); _logger.LogTrace("Запись сохранена"); // Записываем изменения в меню _logger.LogTrace("Записываем изменения в меню"); MenuItems[MenuSelectedIndex].SampleName = Record.SampleName; // Устанавливаем, что запись выбрана из базы данных IsNewRecord = false; } } catch (Exception ex) { _logger.LogError(ex, "Исключение при сохранении записи"); } } /// /// Снять изображения /// [RelayCommand] private void CaptureImage() { // Если съёмка уже начата, то логгируем как ошибку if (_сaptureImagesTask is not null) { _logger.LogError("Задача съёмки уже запущена"); return; } // Создаём токен отмены задачи _captureImagesTaskCts = new CancellationTokenSource(); var cancellationToken = _captureImagesTaskCts.Token; ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}"; // Запускаем получение изображений асинхронно _сaptureImagesTask = Task.Run(async () => { // Список полученных изображений var imagesData = new List>(); // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); // Создаём задачу получения изображений var captureTask = _imageCapturer.CaptureImages(async (data) => { // Распаковываем данные var (i, visible, uv365, uv254, image) = data; // Выбрасываем исключение, если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}"); // Создаём задачу сохранения изображения var imagesDataTask = Task.Run(() => { // Выбрасываем исключение, если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Сохраняем PNG изображение во временную папку var path = Path.Join(Path.GetTempPath(), $"gss2-analysis-{visible}-{uv365}-{uv254}.png"); image.ImWrite(path); // Освобождаем память изображения image.Dispose(); // Добавляем данные изображения в список return new ImageData(path, visible, uv365, uv254); }, cancellationToken); // Добавляем задачу сохранения изображения в список imagesData.Add(imagesDataTask); }, cancellationToken); // var captureTask = Task.Run(() => { // foreach (var (visible, uv365, uv254) in ImageCapturerService.IlluminatorIntensities) // { // var imagesDataTask = Task.Run(() => // { // var path = Path.Join(Path.GetTempPath(), $"gss2-analysis-{visible}-{uv365}-{uv254}.png"); // return new ImageData(path, visible, uv365, uv254); // }, cancellationToken); // imagesData.Add(imagesDataTask); // } // }); // Создаём задачу обработки изображений, // эта задача начнётся только после того как завершится // задача съёмки изображений и все задачи сохранения изображений var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t => { // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений"); // Если во время выполнения задачи съёмки изображений произошло исключение, // то логгируем все исключения и выходим if (captureTask.Exception is not null) { _logger.LogError(captureTask.Exception, "Исключение во время получения изображения"); foreach (var ex in captureTask.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время получения изображения"); return; } // Если во время выполнения задач сохранения изображений произошло исключение, // то логгируем все исключения и выходим bool anyImageDataException = false; foreach (var imageData in imagesData) { if (imageData.Exception is not null) { _logger.LogError(imageData.Exception, "Исключение во время сохранения изображения"); foreach (var ex in imageData.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время сохранения изображения"); anyImageDataException = true; } } if (anyImageDataException) return; // Если во время выполнения произошло исключение, // то логгируем все исключения и выходим if (t.Exception is not null) { _logger.LogError(t.Exception, "Исключение во время обработки изображений"); foreach (var ex in t.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время обработки изображений"); return; } // Инициализируем анализатор var brand = _context.BrandRecords.Find(BrandComboBoxItems[BrandComboBoxSelectedIndex].Id); var separator = _context.SeparatorRecords.Find(SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id); if (brand is null) { _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[BrandComboBoxSelectedIndex].Id, nameof(_context.BrandRecords)); return; } if (brand.MixtureNormalRate <= 0) { _logger.LogError("Норма расхода выбранной марки меньше или равена 0"); return; } if (separator is null) { _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id, nameof(_context.SeparatorRecords)); return; } _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей"); await _analyzer.Initialize(brand, separator); // Проверяем что анализатор инициализирован if (!_analyzer.Initialized) { _logger.LogError("Ошибка при инициализации анализатора"); return; } // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Анализ"); // Проводим анализ var result = await _analyzer.Analyze(imagesData.Select(i => i.Result)); // Если анализ проведён if (result is null) { _logger.LogError("Ошибка при выполнении анализа"); // Если была ошибка, то мигаем светодиодами красным _ = Task.Run(async () => { await Task.Delay(1000); _lights.Flash(1, 1, System.Drawing.Color.Red); await Task.Delay(3000); _lights.Disconnect(); }); return; } // Распаковываем результат var (avgImage, _, mask, contours, resultImage) = result.Value; try { Record.ParticlesCount = contours.Count(); // Кодируем среднее изображение OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); Record.Image.Data = Convert.ToBase64String(avgData); // Освобождаем память среднего изображения avgImage.Dispose(); // Создаём маску контура var contourMask = new OpenCvSharp.Mat( new OpenCvSharp.Size( AnalyzerService.ANALYSIS_IMAGE_SIZE, AnalyzerService.ANALYSIS_IMAGE_SIZE ), OpenCvSharp.MatType.CV_8UC1, new OpenCvSharp.Scalar(0) ); var values = new List(); // Проходим по каждому контуру foreach (var contour in contours) { // Обнуляем маску контура contourMask.SetTo(new OpenCvSharp.Scalar(0)); // Заполняем контур на маске OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1); // Чуть-чуть обрезаем его по краям OpenCvSharp.Cv2.Erode(contourMask, contourMask, OpenCvSharp.Cv2.GetStructuringElement(OpenCvSharp.MorphShapes.Ellipse, new OpenCvSharp.Size(5, 5))); // Находим прямоугольник ограничивающий конур var bbox = OpenCvSharp.Cv2.BoundingRect(contour); // Проходимся по каждому пикселю в прямоугольнике и если он попадает в маску, то сохраняем значение результата var contourValues = new List(); for (int i = bbox.Top; i < bbox.Bottom; i++) for (int j = bbox.Left; j < bbox.Right; j++) if (contourMask.Get(i, j) > 127) contourValues.Add(resultImage.Get(i, j)); // Сохраняем полученные значения values.Add(contourValues.ToArray()); } Record.Values = values; // Освобождаем память маски контура contourMask.Dispose(); // Преобразуем конура в массивы координат Record.Xs = contours.Select(c => c.Select(p => (float)p.X).ToArray()).ToList(); Record.Ys = contours.Select(c => c.Select(p => (float)p.Y).ToArray()).ToList(); // Вычисляем пороговые значения var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * brand.MixtureNormalRate; var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * brand.MixtureNormalRate; // Выичсляем максимальное значение OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max); // Если максимальное значние слишком мало, то устанавливаем его чуть больше hotspotsThreshold, чтобы не возникло деления на ноль и отрицательных значений if (max <= hotspotsThreshold) max = hotspotsThreshold + 0.001; // Создаём цветное изображение результата var coloredResultImage = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC3, OpenCvSharp.Scalar.All(0)); // Раскрашиваем диапазон 0...unprocessedThreshold -> чёрный...белый ApplyGradientToRange(resultImage, 0, unprocessedThreshold, OpenCvSharp.Scalar.Black, OpenCvSharp.Scalar.White, coloredResultImage); // Раскрашиваем диапазон unprocessedThreshold...hotspotsThreshold -> белый...зелёный ApplyGradientToRange(resultImage, unprocessedThreshold, hotspotsThreshold, OpenCvSharp.Scalar.White, OpenCvSharp.Scalar.Green, coloredResultImage); // Раскрашиваем диапазон hotspotsThreshold...max -> зелёный...красный ApplyGradientToRange(resultImage, hotspotsThreshold, max, OpenCvSharp.Scalar.Green, OpenCvSharp.Scalar.Red, coloredResultImage); // Устанавливаем в 0 всё что не попадает в маску var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0)); OpenCvSharp.Cv2.BitwiseNot(mask, invMask); coloredResultImage.SetTo(OpenCvSharp.Scalar.All(0), invMask); // Освобождаем память, оригинальное изображение результата можно удалить invMask.Dispose(); resultImage.Dispose(); // Кодируем маску OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData); Record.MaskImage.Data = Convert.ToBase64String(maskData); // Освобождаем память маски mask.Release(); // Кодируем изображение результата OpenCvSharp.Cv2.ImEncode(".png", coloredResultImage, out var resultData); Record.ResultImage.Data = Convert.ToBase64String(resultData); // Освобождаем память изображения результата coloredResultImage.Release(); // Пересчитываем значения свойств Record.CalculateProperties(); // Если ошибок не было, то мигаем светодиодами зелёным _ = Task.Run(async () => { await Task.Delay(1000); _lights.Flash(1, 1, System.Drawing.Color.Green); await Task.Delay(3000); _lights.Disconnect(); }); } catch (Exception ex) { _logger.LogError(ex, "Исключение во время анализа"); // Если была ошибка, то мигаем светодиодами красным _ = Task.Run(async () => { await Task.Delay(1000); _lights.Flash(1, 1, System.Drawing.Color.Red); await Task.Delay(3000); _lights.Disconnect(); }); } }, cancellationToken); // Создаём задачу которая выполнится после завершения всех других _ = Task.WhenAll([captureTask, .. imagesData, processTask]).ContinueWith(t => { // Если во время выполнения задачи съёмки изображений произошло исключение, // то логгируем все исключения и выходим if (captureTask.Exception is not null) { _logger.LogError(captureTask.Exception, "Исключение во время получения изображения"); foreach (var ex in captureTask.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время получения изображения"); return; } // Если во время выполнения задач сохранения изображений произошло исключение, // то логгируем все исключения и выходим bool anyImageDataException = false; foreach (var imageData in imagesData) { if (imageData.Exception is not null) { _logger.LogError(imageData.Exception, "Исключение во время сохранения изображения"); foreach (var ex in imageData.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время сохранения изображения"); anyImageDataException = true; } } if (anyImageDataException) return; // Если во время выполнения задачи обработки изображений произошло исключение, // то логгируем все исключения и выходим if (processTask.Exception is not null) { _logger.LogError(processTask.Exception, "Исключение во время обработки изображений"); foreach (var ex in processTask.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время обработки изображений"); return; } // Если во время выполнения произошло исключение, // то логгируем все исключения и выходим if (t.Exception is not null) { _logger.LogError(t.Exception, "Исключение во время обработки изображений"); foreach (var ex in t.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время обработки изображений"); return; } // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { // Сбрасываем состояние задачи съёмки изображений ImagesIsCapturing = false; ImagesCapturingProgress = ""; _сaptureImagesTask = null; }); }); }); } /// /// Отменить задачу съёмки изображения /// [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); /// /// Раскрасить диапазон в градиент /// public static void ApplyGradientToRange(OpenCvSharp.Mat source, double minVal, double maxVal, OpenCvSharp.Scalar minColor, OpenCvSharp.Scalar maxColor, OpenCvSharp.Mat destination) { // Проверяем что все типы данных if (source.Type() != OpenCvSharp.MatType.CV_32FC1) throw new ArgumentException("Исходное изображение должно быть типа CV_32FC1", nameof(source)); if (destination.Type() != OpenCvSharp.MatType.CV_8UC3) throw new ArgumentException("Выходное изображение должно быть типа CV_8UC3", nameof(destination)); if (source.Size() != destination.Size()) throw new ArgumentException("Размеры исходного и выходного изображений должны совпадать"); if (maxVal <= minVal) throw new ArgumentException("Максимальное значение диапазона должно быть больше минимального."); // Создаем маску для целевого диапазона значений using var mask = new OpenCvSharp.Mat(); OpenCvSharp.Cv2.InRange(source, OpenCvSharp.Scalar.All(minVal), OpenCvSharp.Scalar.All(maxVal), mask); // Нормализуем значения из диапазона minVal...maxVal в 0.0...1.0 using var normalized = new OpenCvSharp.Mat(); OpenCvSharp.Cv2.Subtract(source, OpenCvSharp.Scalar.All(minVal), normalized); normalized.ConvertTo(normalized, OpenCvSharp.MatType.CV_32FC1, 1.0 / (maxVal - minVal)); // Чтобы избежать выхода за границы, ограничиваем диапазон от 0.0...1.0 OpenCvSharp.Cv2.Threshold(normalized, normalized, 1.0, 1.0, OpenCvSharp.ThresholdTypes.Trunc); OpenCvSharp.Cv2.Threshold(normalized, normalized, 0.0, 0.0, OpenCvSharp.ThresholdTypes.Tozero); // Вычисляем градиент для каждого из трех каналов (BGR) using var bChannel = new OpenCvSharp.Mat(); using var gChannel = new OpenCvSharp.Mat(); using var rChannel = new OpenCvSharp.Mat(); double diffB = maxColor.Val0 - minColor.Val0; double diffG = maxColor.Val1 - minColor.Val1; double diffR = maxColor.Val2 - minColor.Val2; normalized.ConvertTo(bChannel, OpenCvSharp.MatType.CV_8UC1, diffB, minColor.Val0); normalized.ConvertTo(gChannel, OpenCvSharp.MatType.CV_8UC1, diffG, minColor.Val1); normalized.ConvertTo(rChannel, OpenCvSharp.MatType.CV_8UC1, diffR, minColor.Val2); // Объеденяем каналы в одно изображение using var gradientColor = new OpenCvSharp.Mat(); OpenCvSharp.Cv2.Merge(new[] { bChannel, gChannel, rChannel }, gradientColor); // Копируем полученный градиент в итоговое изображение по маске gradientColor.CopyTo(destination, mask); } }