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
{
///
/// Внутренняя модель элементов меню
///
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 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
{
///
///
///
[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 double MixtureActualRate { get; set; } = -1;
///
///
///
[ObservableProperty] public partial double MeasuredContent { get; set; } = -1;
///
/// Представление
///
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
///
/// Данные
///
[ObservableProperty] public partial List Features { get; set; } = new();
}
///
/// Логгер
///
private readonly ILogger _logger;
///
/// Контекст базы данных
///
private readonly Context _context;
///
/// Сервис для получения изображений с камеры
///
private readonly IImageCapturerService _imageCapturer;
///
/// 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 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;
public SampleCalibrationViewModel(
ILogger logger,
Context context,
IImageCapturerService imageCapturer,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
)
{
// Устанавливаем readonly поля
_logger = logger;
_context = context;
_imageCapturer = imageCapturer;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация");
// Получаем записи выпадающего списка марок из базы данных
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(() =>
{
// Если запись не интересует нас, то ничего не делаем
if (ea.Entry.Entity is not BrandRecord brandRecord)
return;
// Если запись добавлена
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;
}
// Обновляем элемент из выпадающего списка марок
editedComboBoxItem.BrandName = brandRecord.BrandName;
editedComboBoxItem.MixtureName = brandRecord.MixtureName;
}
});
};
// Получаем записи меню из базы данных
MenuItems = new ObservableCollection(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
if (MenuItems.Count() > 0) // Если записи есть,
MenuSelectedIndex = 0; // Выбираем первую запись
else // Если записей нет,
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
// Подписываемся на изменения свойств для обновления CanScroll
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling) ||
ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
};
_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.SampleRecords.Find(MenuItems[value].Id);
// Если значение не найдено в базе данных
if (selectedRecord is null)
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.SampleRecords));
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.MixtureActualRate = selectedRecord.MixtureActualRate;
Record.MeasuredContent = selectedRecord.MeasuredContent;
if (selectedRecord.Features.Count() > 0)
Record.Features = selectedRecord.Features.Chunk(AnalyzerService.FEATURES_LENGTH).Select(v => v.ToArray()).ToList();
else
Record.Features = new List();
// Ищем изображение в базе данных
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 brandRecord = _context.BrandRecords.Find(selectedRecord.BrandId);
// Если марка не найдена в базе данных
if (brandRecord is null)
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.BrandId, 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);
}
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.MixtureActualRate = -1;
Record.MeasuredContent = -1;
Record.Image.Data = "";
Record.Features = new List();
BrandComboBoxSelectedIndex = -1;
}
///
/// Обновить значение
///
private void UpdateCanInteract()
{
CanInteract = !MenuOpened;
CanInteract &= !MainSectionIsScrolling;
CanInteract &= !InfoSectionIsScrolling;
}
// Команды управления меню
///
/// Открыть меню
///
[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 CreateRecord()
{
_logger.LogInformation("Создание новой записи");
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
// поэтому если Record.Record != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
if (MenuSelectedIndex != -1)
MenuSelectedIndex = -1;
else
OnMenuSelectedIndexChanged(-1);
}
///
/// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана
///
[RelayCommand]
private void DeleteRecord()
{
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try
{
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.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.SampleRecords.Find(deletedRecordId);
// Создаём новую запись
CreateRecord();
// Удаляем запись из списка
MenuItems.RemoveAt(deletedMenuIndex);
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
if (deletedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SampleRecords));
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, "Исключение при удалении записи");
}
}
///
/// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи
///
[RelayCommand]
private void SaveRecord()
{
try
{
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.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;
}
// Определяем 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;
_logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Заполняем запись
var newRecord = new SampleRecord(
uuid: Record.Uuid,
createDateTime: dateTime,
editDateTime: dateTime,
sampleName: Record.SampleName,
sampleComment: Record.SampleComment,
samplingDateTime: Record.SamplingDateTime,
brandId: selectedBrandId,
samplingPlace: Record.SamplingPlace,
mixtureActualRate: Record.MixtureActualRate,
measuredContent: Record.MeasuredContent,
imageId: imageId,
features: Record.Features
);
// Добавляем запись в базу данных
var attachedRecord = _context.SampleRecords.Add(newRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись сохранена");
// Добавляем новый элемент в меню
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName));
// Выбираем последний элемент из меню
MenuSelectedIndex = MenuItems.Count() - 1;
}
// Если запись выбрана
else
{
// Находим запись в базе данных
var attachedRecord = _context.SampleRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SampleRecords));
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;
}
}
// Определяем 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.BrandId = selectedBrandId;
attachedRecord.SamplingPlace = Record.SamplingPlace;
attachedRecord.MixtureActualRate = Record.MixtureActualRate;
attachedRecord.MeasuredContent = Record.MeasuredContent;
attachedRecord.Features = Record.Features.SelectMany(fs => fs).ToList();
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись сохранена");
// Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].SampleName = Record.SampleName;
}
}
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-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;
// Сохраняем вектор признаков
Record.Features = 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);
// Очищаем память среднего изображения
avgImage.Dispose();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
}, 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();
}