forked from amkovkov/GranuSightSoftware2
938 lines
52 KiB
C#
938 lines
52 KiB
C#
using System.Data;
|
||
using System.Collections.ObjectModel;
|
||
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
using Avalonia.Threading;
|
||
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
|
||
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 SampleCalibrationViewModel : ViewModelBase
|
||
{
|
||
/// <summary>
|
||
/// Внутренняя модель элементов меню
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Внутренняя модель элементов выпадающего списка марок
|
||
/// </summary>
|
||
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;
|
||
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Логгер
|
||
/// </summary>
|
||
private readonly ILogger<SampleCalibrationViewModel> _logger;
|
||
/// <summary>
|
||
/// Контекст базы данных
|
||
/// </summary>
|
||
private readonly Context _context;
|
||
/// <summary>
|
||
/// Сервис для получения изображений с камеры
|
||
/// </summary>
|
||
private readonly IImageCapturerService _imageCapturer;
|
||
/// <summary>
|
||
/// ViewLocator для получения других View по их моделям
|
||
/// </summary>
|
||
private readonly DependencyInjectionViewLocator _viewLocator;
|
||
/// <summary>
|
||
/// Сервис навигации между экранами
|
||
/// </summary>
|
||
private readonly NavigationService _navigation;
|
||
|
||
/// <summary>
|
||
/// Задача съёмки изображений
|
||
/// </summary>
|
||
private Task? _сaptureImagesTask = null;
|
||
/// <summary>
|
||
/// Токен отмены задачи съёмки изображений
|
||
/// </summary>
|
||
private CancellationTokenSource? _captureImagesTaskCts = null;
|
||
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.BrandId"/> выбранного элемента
|
||
/// </summary>
|
||
private int _selectedBrandId = 0;
|
||
/// <summary>
|
||
/// Данные <see cref="SampleRecord.ImageId"/> выбранного элемента
|
||
/// </summary>
|
||
private string? _selectedImageData = null;
|
||
/// <summary>
|
||
/// Данные <see cref="SampleRecord.FeatureIds"/> выбранного элемента
|
||
/// </summary>
|
||
private List<float[]> _selectedFeaturesData = new();
|
||
|
||
/// <summary>
|
||
/// Меню открыто
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
|
||
/// <summary>
|
||
/// Элементы меню
|
||
/// </summary>
|
||
[ObservableProperty] public partial ObservableCollection<MenuItemViewModel> MenuItems { get; set; } = new();
|
||
/// <summary>
|
||
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
|
||
/// </summary>
|
||
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
|
||
|
||
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.Id"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.Uuid"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.SampleName"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial string SelectedSampleName { get; set; } = "";
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.SampleComment"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial string SelectedSampleComment { get; set; } = "";
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.SamplingDateTime"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial DateTime SelectedSamplingDateTime { get; set; } = DateTime.UtcNow;
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.SamplingPlace"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial string SelectedSamplingPlace { get; set; } = "";
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.MixtureActualRate"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial double SelectedMixtureActualRate { get; set; } = -1;
|
||
/// <summary>
|
||
/// <see cref="SampleRecord.MeasuredContent"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial double SelectedMeasuredContent { get; set; } = -1;
|
||
/// <summary>
|
||
/// Представление <see cref="SampleRecord.ImageId"/> выбранного элемента
|
||
/// </summary>
|
||
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
|
||
/// <summary>
|
||
/// Список элементов выпадающего списка марок
|
||
/// </summary>
|
||
[ObservableProperty] public partial ObservableCollection<BrandComboBoxItemViewModel> BrandComboBoxItems { get; set; } = new();
|
||
/// <summary>
|
||
/// Текущий выбранный элемент выпадающего списка марок. Равен -1 если элемент не выбран
|
||
/// </summary>
|
||
[ObservableProperty] public partial int BrandComboBoxSelectedIndex { get; set; } = -1;
|
||
|
||
/// <summary>
|
||
/// Текущий экран главной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0;
|
||
/// <summary>
|
||
/// Можно ли переходить на следующий экран главной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
|
||
/// <summary>
|
||
/// Можно ли переходить на предыдущий экран главной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = false;
|
||
/// <summary>
|
||
/// Происходит ли переход между экранами главной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
|
||
|
||
|
||
/// <summary>
|
||
/// Текущий экран информационной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
|
||
/// <summary>
|
||
/// Можно ли переходить на следующий экран информационной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
|
||
/// <summary>
|
||
/// Можно ли переходить на предыдущий экран информационной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
|
||
/// <summary>
|
||
/// Происходит ли переход между экранами информационной секции
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// Проводится ли съёмка изображений
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
|
||
/// <summary>
|
||
/// Прогресс съёмки изображений
|
||
/// </summary>
|
||
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
||
|
||
/// <summary>
|
||
/// Можно ли производить перемещение между экранами
|
||
/// </summary>
|
||
[ObservableProperty] public partial bool CanScroll { get; set; }
|
||
|
||
public SampleCalibrationViewModel(
|
||
ILogger<SampleCalibrationViewModel> logger,
|
||
Context context,
|
||
IImageCapturerService imageCapturer,
|
||
DependencyInjectionViewLocator viewLocator,
|
||
NavigationService navigation
|
||
)
|
||
{
|
||
// Устанавливаем readonly поля
|
||
_logger = logger;
|
||
_context = context;
|
||
_imageCapturer = imageCapturer;
|
||
_viewLocator = viewLocator;
|
||
_navigation = navigation;
|
||
_logger.LogInformation("Инициализация");
|
||
|
||
// Получаем записи меню из базы данных
|
||
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
|
||
if (MenuItems.Count() > 0) // Если записи есть,
|
||
MenuSelectedIndex = 0; // Выбираем первую запись
|
||
else // Если записей нет,
|
||
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
|
||
|
||
// Получаем записи выпадающего списка марок из базы данных
|
||
BrandComboBoxItems = new ObservableCollection<BrandComboBoxItemViewModel>(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName)));
|
||
// Подписываемся на изменения базы данных для отслеживания изменений списка марок
|
||
_context.ChangeTracker.StateChanged += (_, ea) =>
|
||
{
|
||
// Если запись не интересует нас, то ничего не делаем
|
||
if (ea.Entry.Entity is not BrandRecord brandRecord)
|
||
return;
|
||
|
||
// Если запись добавлена
|
||
if (ea.NewState is EntityState.Added)
|
||
{
|
||
// Добавляем запись
|
||
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;
|
||
}
|
||
// Обновляем элемент из выпадающего списка марок
|
||
editedComboBoxItem.BrandName = brandRecord.BrandName;
|
||
editedComboBoxItem.MixtureName = brandRecord.MixtureName;
|
||
}
|
||
};
|
||
|
||
// Подписываемся на изменения свойств для обновления CanScroll
|
||
PropertyChanged += (_, ea) =>
|
||
{
|
||
if (ea.PropertyName == nameof(MenuOpened) ||
|
||
ea.PropertyName == nameof(MainSectionIsScrolling) ||
|
||
ea.PropertyName == nameof(InfoSectionIsScrolling))
|
||
UpdateCanScroll();
|
||
};
|
||
|
||
_logger.LogInformation("Инициализировано");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается при изменении <see cref="MenuSelectedIndex"/>
|
||
/// </summary>
|
||
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.SampleRecords.Find(MenuItems[value].Id);
|
||
// Если запись найдена, то заполняем поля из базы данных
|
||
if (selectedRecord is not null)
|
||
{
|
||
// Заполняем простые поля
|
||
SelectedId = selectedRecord.Id;
|
||
SelectedUuid = selectedRecord.Uuid;
|
||
SelectedSampleName = selectedRecord.SampleName;
|
||
SelectedSampleComment = selectedRecord.SampleComment;
|
||
SelectedSamplingDateTime = selectedRecord.SamplingDateTime;
|
||
SelectedSamplingPlace = selectedRecord.SamplingPlace;
|
||
SelectedMixtureActualRate = selectedRecord.MixtureActualRate;
|
||
SelectedMeasuredContent = selectedRecord.MeasuredContent;
|
||
_selectedBrandId = selectedRecord.BrandId;
|
||
|
||
// Ищем изображение в базе данных
|
||
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
|
||
// Если изображение не найдено в базе данных, то логгируем как ошибку
|
||
if (imageRecord is null)
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
|
||
// В любом случае заполняем изображение, оно заполнится null если запись не найдена
|
||
_selectedImageData = imageRecord?.Base64;
|
||
SelectedImage.Data = _selectedImageData;
|
||
|
||
// Ищем марку в базе данных
|
||
var brandRecord = _context.BrandRecords.Find(_selectedBrandId);
|
||
// Если марка не найдена в базе данных
|
||
if (brandRecord is null)
|
||
{
|
||
// Логгируем как ошибку
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", _selectedBrandId, nameof(_context.BrandRecords));
|
||
// Устанавливаем, что элемент не выбран
|
||
BrandComboBoxSelectedIndex = -1;
|
||
}
|
||
// Если марка найдена в базе данных
|
||
else
|
||
{
|
||
// Ищем марку в выпадающем списке марок
|
||
var brandComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id);
|
||
// Если марка не найдена в выпадающем списке марок, то логгируем как ошибку
|
||
if (brandComboBoxItem is null)
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems));
|
||
// Если марка найдена в выпадающем списке марок, выбираем её
|
||
else
|
||
BrandComboBoxSelectedIndex = BrandComboBoxItems.IndexOf(brandComboBoxItem);
|
||
}
|
||
|
||
// Ищем все признаки в базе данных
|
||
_selectedFeaturesData = _context.FeatureRecords
|
||
.Where(r => r.SampleRecordId == SelectedId)
|
||
.Select(r => r.Values.ToArray())
|
||
.ToList();
|
||
|
||
return;
|
||
}
|
||
// Если значение не найдено в базе данных
|
||
else
|
||
{
|
||
// Логгируем как ошибку
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
|
||
var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
|
||
// Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
|
||
MenuSelectedIndex = -1;
|
||
// Удаляем несуществующий элемент из списка
|
||
MenuItems.RemoveAt(nonExistingIndex);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Если элемент не выбран, заполняем поля пустыми значениями
|
||
_logger.LogTrace("Выбран новый элемент");
|
||
SelectedId = 0;
|
||
SelectedUuid = Guid.NewGuid().ToString();
|
||
SelectedSampleName = "";
|
||
SelectedSampleComment = "";
|
||
SelectedSamplingDateTime = DateTime.UtcNow;
|
||
SelectedSamplingPlace = "";
|
||
SelectedMixtureActualRate = -1;
|
||
SelectedMeasuredContent = -1;
|
||
SelectedImage.Data = null;
|
||
BrandComboBoxSelectedIndex = -1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Вызывается при изменении <see cref="BrandComboBoxSelectedIndexC"/>
|
||
/// </summary>
|
||
partial void OnBrandComboBoxSelectedIndexChanged(int value)
|
||
{
|
||
if (value >= 0 && value < BrandComboBoxItems.Count())
|
||
_selectedBrandId = BrandComboBoxItems[value].Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновить значение <see cref="CanScroll"/>
|
||
/// </summary>
|
||
private void UpdateCanScroll()
|
||
{
|
||
CanScroll = !MenuOpened;
|
||
CanScroll &= !MainSectionIsScrolling;
|
||
CanScroll &= !InfoSectionIsScrolling;
|
||
CanScroll &= !ImagesIsCapturing;
|
||
}
|
||
|
||
// Команды управления меню
|
||
/// <summary>
|
||
/// Открыть меню
|
||
/// </summary>
|
||
[RelayCommand] private void OpenMenu() => MenuOpened = true;
|
||
/// <summary>
|
||
/// Закрыть меню
|
||
/// </summary>
|
||
[RelayCommand] private void CloseMenu() => MenuOpened = false;
|
||
/// <summary>
|
||
/// Переключить видимость меню
|
||
/// </summary>
|
||
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
|
||
|
||
/// <summary>
|
||
/// Переключить главную секцию информация <-> изображения
|
||
/// </summary>
|
||
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
||
/// <summary>
|
||
/// Переключить информационную секцию на следующую
|
||
/// </summary>
|
||
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
||
/// <summary>
|
||
/// Переключить информационную секцию на предыдущую
|
||
/// </summary>
|
||
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
||
|
||
/// <summary>
|
||
/// Создать новую запись
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void CreateRecord()
|
||
{
|
||
_logger.LogInformation("Создание новой записи");
|
||
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
|
||
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
|
||
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
|
||
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
|
||
if (MenuSelectedIndex != -1)
|
||
MenuSelectedIndex = -1;
|
||
else
|
||
OnMenuSelectedIndexChanged(-1);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void DeleteRecord()
|
||
{
|
||
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
|
||
try
|
||
{
|
||
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName);
|
||
// Если запись не выбрана, то просто создаём новую запись
|
||
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 = SelectedId;
|
||
var deletedRecord = _context.SampleRecords.Find(deletedRecordId);
|
||
// Создаём новую запись
|
||
CreateRecord();
|
||
|
||
// Удаляем запись из списка
|
||
MenuItems.RemoveAt(deletedMenuIndex);
|
||
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
|
||
if (deletedRecord is null)
|
||
{
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
|
||
return;
|
||
}
|
||
|
||
// Удаляем запись из базы данных и сохраняем изменения в базе данных
|
||
_context.SampleRecords.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, "Исключение при удалении записи");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void SaveRecord()
|
||
{
|
||
try
|
||
{
|
||
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName);
|
||
// Если запись не выбрана
|
||
if (MenuSelectedIndex < 0)
|
||
{
|
||
// Создаём запись изображения
|
||
_logger.LogTrace("Создаём новую запись изображения");
|
||
// Заполняем запись изображения
|
||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||
// Добавляем запись изображения в базу данных
|
||
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
|
||
// Сохраняем изменения в базе данных
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Запись изображения сохранена");
|
||
|
||
_logger.LogTrace("Создаём новую запись");
|
||
// Заполняем запись
|
||
var newRecord = new SampleRecord(
|
||
uuid: SelectedUuid,
|
||
sampleName: SelectedSampleName,
|
||
sampleComment: SelectedSampleComment,
|
||
samplingDateTime: SelectedSamplingDateTime,
|
||
brandId: _selectedBrandId,
|
||
samplingPlace: SelectedSamplingPlace,
|
||
mixtureActualRate: SelectedMixtureActualRate,
|
||
measuredContent: SelectedMeasuredContent,
|
||
imageId: attachedImageRecord.Entity.Id,
|
||
featureIds: []
|
||
);
|
||
// Добавляем запись в базу данных
|
||
var attachedRecord = _context.SampleRecords.Add(newRecord);
|
||
// Сохраняем изменения в базе данных
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Запись сохранена");
|
||
|
||
_logger.LogTrace("Создаём новые записи признаков");
|
||
// Заполняем записи признаков и добавляем их в базу данных
|
||
var attachedFeatureRecords = new List<FeatureRecord>();
|
||
foreach (var feature in _selectedFeaturesData)
|
||
{
|
||
var featureRecord = new FeatureRecord(
|
||
values: feature,
|
||
sampleRecordId: attachedRecord.Entity.Id
|
||
);
|
||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||
attachedFeatureRecords.Add(attachedFeatureRecord.Entity);
|
||
}
|
||
// Сохраняем изменения в базе данных
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Записи признаков сохранены");
|
||
|
||
_logger.LogTrace("Заполняем признаки в сохранённой записи");
|
||
// Добавляем идентификаторы признаков в запись
|
||
attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id));
|
||
// Сохраняем изменения в базе данных
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Сохранение записи завершено");
|
||
|
||
// Добавляем новый элемент в меню
|
||
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName));
|
||
// Выбираем последний элемент из меню
|
||
MenuSelectedIndex = MenuItems.Count() - 1;
|
||
}
|
||
// Если запись выбрана
|
||
else
|
||
{
|
||
// Находим запись в базе данных
|
||
var attachedRecord = _context.SampleRecords.Find(SelectedId);
|
||
// Если запись не найдена, то логгируем как ошибку
|
||
if (attachedRecord is null)
|
||
{
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SampleRecords));
|
||
return;
|
||
}
|
||
|
||
_logger.LogTrace("Записываем изменения в базу данных");
|
||
|
||
// Ищем запись изображения в базе данных
|
||
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId);
|
||
// Если запись не найдена
|
||
if (attachedImageRecord is null)
|
||
{
|
||
// Логгируем как ошибку
|
||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords));
|
||
_logger.LogTrace("Создаём новую запись изображения");
|
||
// Заполняем запись изображения
|
||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||
// Добавляем запись изображения в базу данных
|
||
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
|
||
// Сохраняем изменения в базе данных
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Запись изображения сохранена");
|
||
}
|
||
|
||
// Ищем записи признаков в базе данных
|
||
var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SampleRecordId == SelectedId);
|
||
// Если количество признаков в базе данных больше чем текущее
|
||
if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count())
|
||
{
|
||
// Находим количество признаков которое нужно удалить
|
||
var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count();
|
||
// Логгируем
|
||
_logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count());
|
||
// Удаляем необходимое количество признаков из конца
|
||
attachedFeatureRecords.TakeLast(featuresRecordsToRemove);
|
||
}
|
||
|
||
// Заполняем все имеющиеся признаки в базе данных новыми значениями
|
||
foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate())
|
||
attachedFeatureRecord.Values = _selectedFeaturesData[i];
|
||
|
||
// Если количество признаков в безе данных меньше чем текущее
|
||
if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count())
|
||
{
|
||
// Находим количество признаков которое нужно добавить
|
||
var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count();
|
||
// Логгируем
|
||
_logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count());
|
||
// Заполняем записи недостающих признаков и добавляем их в базу данных
|
||
for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++)
|
||
{
|
||
var featureRecord = new FeatureRecord(
|
||
values: _selectedFeaturesData[i],
|
||
sampleRecordId: attachedRecord.Id
|
||
);
|
||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||
attachedFeatureRecords.Append(attachedFeatureRecord.Entity);
|
||
}
|
||
}
|
||
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Записи признаков сохранена");
|
||
|
||
// Если в данных есть изменения, то записываем их
|
||
if (attachedRecord.SampleName != SelectedSampleName)
|
||
attachedRecord.SampleName = SelectedSampleName;
|
||
if (attachedRecord.SampleComment != SelectedSampleComment)
|
||
attachedRecord.SampleComment = SelectedSampleComment;
|
||
if (attachedRecord.SamplingDateTime != SelectedSamplingDateTime)
|
||
attachedRecord.SamplingDateTime = SelectedSamplingDateTime;
|
||
if (attachedRecord.BrandId != _selectedBrandId)
|
||
attachedRecord.BrandId = _selectedBrandId;
|
||
if (attachedRecord.SamplingPlace != SelectedSamplingPlace)
|
||
attachedRecord.SamplingPlace = SelectedSamplingPlace;
|
||
if (attachedRecord.MixtureActualRate != SelectedMixtureActualRate)
|
||
attachedRecord.MixtureActualRate = SelectedMixtureActualRate;
|
||
if (attachedRecord.MeasuredContent != SelectedMeasuredContent)
|
||
attachedRecord.MeasuredContent = SelectedMeasuredContent;
|
||
if (attachedRecord.ImageId != attachedImageRecord.Id)
|
||
attachedRecord.ImageId = attachedImageRecord.Id;
|
||
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
|
||
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
|
||
|
||
// Сохраняем изменения в базе данных
|
||
_context.SaveChanges();
|
||
_logger.LogTrace("Запись сохранена");
|
||
|
||
// Записываем изменения в меню
|
||
_logger.LogTrace("Записываем изменения в меню");
|
||
MenuItems[MenuSelectedIndex].SampleName = SelectedSampleName;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Исключение при сохранении записи");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Снять изображения
|
||
/// </summary>
|
||
[RelayCommand]
|
||
private void CaptureImage()
|
||
{
|
||
// Если съёмка уже начата, то логгируем как ошибку
|
||
if (_сaptureImagesTask is not null)
|
||
{
|
||
_logger.LogError("Задача съёмки уже запущена");
|
||
return;
|
||
}
|
||
|
||
// Создаём токен отмены задачи
|
||
_captureImagesTaskCts = new CancellationTokenSource();
|
||
var cancellationToken = _captureImagesTaskCts.Token;
|
||
|
||
// Обновляем данные в интерфейсе
|
||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||
|
||
// Запускаем задачу получения изображений
|
||
_сaptureImagesTask = Task.Run(async () =>
|
||
{
|
||
// Список полученных изображений
|
||
var imagesData = new List<Task<ImageData>>();
|
||
|
||
// Обновляем данные в интерфейсе
|
||
// 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-calibration-sample-{visible}-{uv365}-{uv254}.png");
|
||
image.ImWrite(path);
|
||
|
||
// Освобождаем память изображения
|
||
image.Dispose();
|
||
|
||
// Добавляем данные изображения в список
|
||
return new ImageData(path, visible, uv365, uv254);
|
||
}, cancellationToken);
|
||
// Добавляем задачу сохранения изображения в список
|
||
imagesData.Add(imagesDataTask);
|
||
}, cancellationToken);
|
||
|
||
// Создаём задачу обработки изображений,
|
||
// эта задача начнётся только после того как завершится
|
||
// задача съёмки изображений и все задачи сохранения изображений
|
||
var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
|
||
{
|
||
// Выбрасываем исключение, если задача отменена
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
|
||
// Обновляем данные в интерфейсе
|
||
// 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 result = AnalyzerService.GetFeatureVectors(imagesData.Select(i => i.Result));
|
||
// Если вектор признаков не вычислен, то логгируем как ошибку
|
||
if (result is null)
|
||
{
|
||
_logger.LogError("Не удалось вычислить вектор признаков");
|
||
return;
|
||
}
|
||
|
||
// Распаковываем результат
|
||
var (avgImage, roi, features) = result.Value;
|
||
|
||
// Сохраняем вектор признаков
|
||
_selectedFeaturesData = features;
|
||
|
||
// Обрабатываем среднее изображение
|
||
avgImage = new OpenCvSharp.Mat(avgImage, roi);
|
||
OpenCvSharp.Cv2.Resize(
|
||
avgImage,
|
||
avgImage,
|
||
new OpenCvSharp.Size(
|
||
AnalyzerService.ANALYSIS_IMAGE_SIZE,
|
||
AnalyzerService.ANALYSIS_IMAGE_SIZE
|
||
)
|
||
);
|
||
|
||
// Кодируем среднее изображение
|
||
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
|
||
_selectedImageData = Convert.ToBase64String(avgData);
|
||
|
||
// Очищаем память среднего изображения
|
||
avgImage.Dispose();
|
||
|
||
// Обновляем данные в интерфейсе
|
||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData);
|
||
}, 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;
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отменить задачу съёмки изображения
|
||
/// </summary>
|
||
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
||
|
||
}
|