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.Calibration; using GSS2.Core.Hardware; using GSS2.UI.Core.ViewModels; namespace GSS2.ViewModels.AmineContent; public partial class SeparatorCalibrationViewModel : ViewModelBase { private readonly ILogger _logger; private readonly AmineContentCalibrationContext _context; private readonly AmineContentImageCapturerService _imageCapturer; private readonly ImageStorageService _imageStorage; private CancellationTokenSource? _captureImagesTaskCts = null; [ObservableProperty] public partial bool MenuOpened { get; set; } = false; [ObservableProperty] public partial ObservableCollection Records { get; set; } [ObservableProperty] public partial SeparatorRecord? SelectedRecord { get; set; } [ObservableProperty] public partial SeparatorRecord EditingRecord { get; set; } [ObservableProperty] public partial int MainSectionsCurrentIndex { get; set; } = 0; [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true; [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true; [ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null; [ObservableProperty] public partial ObservableCollection Images { get; set; } = new(); [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; [ObservableProperty] public partial int ImagesSectionCurrentIndex { get; set; } = 0; [ObservableProperty] public partial bool ImagesSectionCanScrollForward { get; set; } = false; [ObservableProperty] public partial bool ImagesSectionCanScrollBackward { get; set; } = false; [ObservableProperty] public partial bool ImagesSectionIsScrolling { get; set; } = true; public SeparatorCalibrationViewModel( ILogger logger, AmineContentCalibrationContext context, AmineContentImageCapturerService imageCapturer, ImageStorageService imageStorage ) { _logger = logger; _context = context; _imageCapturer = imageCapturer; _imageStorage = imageStorage; _logger.LogInformation("Инициализация"); // Получаем записи из базы данных Records = new ObservableCollection(_context.SeparatorRecords.Include(r => r.Images)); // Берём первую запись SelectedRecord = Records.FirstOrDefault(); // Если записей нет, то создаём пустую // на самом деле это излишне, потому что при изменении SelectedRecord // вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений if (SelectedRecord is null) EditingRecord = new() { Type = "", Batch = "", Images = new() }; else EditingRecord = CopyRecord(SelectedRecord); _logger.LogInformation("Инициализировано"); } /// /// Вызывается при изменении .
/// Копирует в или издаёт пустую . ///
partial void OnSelectedRecordChanged(SeparatorRecord? value) { // Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись) // то создаём пустую запись // TODO: Подумать над тем, чтобы сразу удалять несохранённые изображения из хранилища if (SelectedRecord is null) { EditingRecord = new() { Type = "", Batch = "", Images = new() }; } else { // Иначе копируем запись, отвязывая её от базы данных EditingRecord = CopyRecord(SelectedRecord); } } /// /// Вызывается при изменении .
/// Заполняет изображениями из . ///
partial void OnEditingRecordChanged(SeparatorRecord value) { var images = value.Images // Для каждого изображения .Select(i => { // Находим путь через ImageStorageService var path = _imageStorage.GetFullPath(i.ImagePath, "separator"); // Если изображения не найдено, возвращаем null if (path is null) return null; // Создаём AsyncImageRecordViewModel return new AsyncImageRecordViewModel(path, i); }) // Оставляем только не null значения .OfType(); // Создаём новую коллекцию изображений // TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой Images.Clear(); Images = new(images); } // Просто команды управления меню [RelayCommand] private void OpenMenu() => MenuOpened = true; [RelayCommand] private void CloseMenu() => MenuOpened = false; [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; // Команда создания новой записи // вызывает цепочку действий по изменению EditingRecord и Images [RelayCommand] private void CreateRecord() { // Из-за того что, если создана новая запись, то SelectedRecord == null // при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется, // потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null, // поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged if (SelectedRecord is not null) SelectedRecord = null; else OnSelectedRecordChanged(null); } // Команда удаления записи, создаёт новую запись и удаляет старую из списка и базы данных [RelayCommand] private void DeleteRecord() { // Сохраняем текущую запись var deletedRecord = SelectedRecord; // Создаём новую запись CreateRecord(); // Если запись на была сохранена в базу данных, то ничего не делаем if (deletedRecord is null) return; // Если запись была в без данных, то // она была и в Records, удаляем её из Records Records.Remove(deletedRecord); // Удаляем запись из базы данных и сохраняем изменения // TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления, // но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске, // тогда удалённые записи будут заполнять диск // TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища _context.SeparatorRecords.Remove(deletedRecord); _context.SaveChanges(); } // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной [RelayCommand] private void SaveRecord() { // Если это новая запись if (SelectedRecord is null) { // Если это новая запись, то копируем её var newRecord = CopyRecord(EditingRecord); // Для каждого изображения в записи for (int i = 0; i < newRecord.Images.Count(); i++) { var image = newRecord.Images[i]; // Если изображение есть в базе данных, то пропускаем его if (_context.Entry(image).State is not EntityState.Detached) continue; // Если изображения нет в базе данных, то добавляем его в базу var attachedImage = _context.ImageRecords.Add(image); // Удаляем отвязанное от базы данных изображение из записи newRecord.Images.RemoveAt(i); // Добавляем привязанное изображение к записи newRecord.Images.Insert(i, attachedImage.Entity); } // Если записи нет в базе данных if (_context.Entry(newRecord).State is EntityState.Detached) { // То добавляем её в базу var attachedRecord = _context.SeparatorRecords.Add(newRecord); // Заменяем текущую запись на привязанную к базе данных newRecord = attachedRecord.Entity; } // Сохраняем изменения в базе данных _context.SaveChanges(); // Добавляем запись в список Records.Add(newRecord); // Выбираем запись SelectedRecord = newRecord; } // Если эта запись есть в базе else { // Переносим данные из редактируемой записи в выбранную SelectedRecord.Type = EditingRecord.Type; SelectedRecord.Batch = EditingRecord.Batch; // Находим изображения которые нужно удалить - // те которые есть в выбранной записи, но нет в редактируемой var imagesToRemove = SelectedRecord.Images .Where(e => EditingRecord.Images.All(i => i.Id != e.Id)) .ToList(); // Удаляем старые изображения // TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища foreach (var image in imagesToRemove) SelectedRecord.Images.Remove(image); // Для каждого изображения в редактируемой записи foreach (var editedImage in EditingRecord.Images) { // Находим изображение в выбранной записи (оно будет привязано к базе данных) var existing = SelectedRecord.Images .FirstOrDefault(i => i.Id == editedImage.Id); // Если изображение не найдено if (existing is null) { // Копируем изображение в выбранную запись SelectedRecord.Images.Add(CopyRecord(editedImage)); } // Если изображение найдено else { // Изменяем данные изображения на данные изображения из редактируемой записи existing.ImagePath = editedImage.ImagePath; existing.VisibleIntensity = editedImage.VisibleIntensity; existing.Uv365Intensity = editedImage.Uv365Intensity; existing.Uv254Intensity = editedImage.Uv254Intensity; } } // Для каждого изображения в выбранной записи for (int i = 0; i < SelectedRecord.Images.Count(); i++) { var image = SelectedRecord.Images[i]; // Если изображение есть в базе данных, то пропускаем его if (_context.Entry(image).State is not EntityState.Detached) continue; // Если изображения нет в базе данных, то добавляем его в базу var attachedImage = _context.ImageRecords.Add(image); // Удаляем отвязанное от базы данных изображение из записи SelectedRecord.Images.RemoveAt(i); // Добавляем привязанное изображение к записи SelectedRecord.Images.Insert(i, attachedImage.Entity); } // Если записи нет в базе данных if (_context.Entry(SelectedRecord).State is EntityState.Detached) { // То добавляем её в базу var attachedRecord = _context.SeparatorRecords.Add(SelectedRecord); // Находим текущую запись в списке var index = Records.IndexOf(SelectedRecord); // Заменяем её в списке на привязанную к базе данных Records[index] = attachedRecord.Entity; // Заменяем выбранную запись на привязанную SelectedRecord = attachedRecord.Entity; } // Сохраняем изменения в базе данных _context.SaveChanges(); } } // Команда изменения основной секции (информация <-> изображения) [RelayCommand] private void SwitchMainSection() => MainSectionsCurrentIndex = MainSectionsCurrentIndex == 0 ? 1 : 0; // Команда навигации по изображениям [RelayCommand] private void ImagesSectionPrevious() => ImagesSectionCurrentIndex--; [RelayCommand] private void ImagesSectionNext() => ImagesSectionCurrentIndex++; // Команда съёмки изображений [RelayCommand] private void CaptureImage() { // Если съёмка уже начата, то ничего не делаем if (CaptureImagesTask is not null) return; // Создаём токен отмены задачи _captureImagesTaskCts = new CancellationTokenSource(); var cancellationToken = _captureImagesTaskCts.Token; // Запускаем получение изображений асинхронно CaptureImagesTask = Task.Run(async () => { // Список полученных изображений var imageRecords = new List(); // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); // Получаем изображения var task = _imageCapturer.CaptureImages(async (data) => { // Распаковываем данные var (i, visible, uv365, uv254, image) = data; // Выбрасываем исключение если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Обновляем данные в интерфейсе // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {AmineContentImageCapturerService.IlluminatorIntensities.Count()}"); _ = Task.Run(async () => { // Кодируем изображение в png var bytes = image.ImEncode(".png"); // Сохраняем изображение в ImageStorageService string fileName; using (var stream = new MemoryStream(bytes)) fileName = await _imageStorage.SaveAsync(stream, ".png", "separator", cancellationToken); // Освобождаем память изображения image.Release(); image = null; bytes = null; // Добавляем изображение в список полученных imageRecords.Add( new ImageRecord() { ImagePath = fileName, VisibleIntensity = visible, Uv365Intensity = uv365, Uv254Intensity = uv254, } ); }); }, cancellationToken); _ = task.ContinueWith(t => { if (t.Exception is not null) { _logger.LogError(t.Exception, "Исключение во время получения изображений"); foreach (var ex in t.Exception.InnerExceptions ?? []) _logger.LogError(ex, "Исключение во время получения изображений"); } var images = imageRecords // Для каждого изображения .Select(i => { // Находим путь через ImageStorageService var path = _imageStorage.GetFullPath(i.ImagePath, "separator"); // Если изображения не найдено, возвращаем null if (path is null) return null; // Создаём AsyncImageRecordViewModel return new AsyncImageRecordViewModel(path, i); }) // Оставляем только не null значения .OfType(); // Обновляем данные в редактируемой коллекции // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { EditingRecord.Images = imageRecords; // Создаём новую коллекцию изображений // TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой Images = new(images); }); // Сбрасываем состояние задачи съёмки изображений // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { ImagesIsCapturing = false; ImagesCapturingProgress = ""; CaptureImagesTask = null; }); }); }); } // Команда отмены задачи съёмки изображений [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); /// /// Копирует , отвязывая её от базы данных /// /// Скопированная private static SeparatorRecord CopyRecord(SeparatorRecord source) { return new SeparatorRecord { Id = source.Id, Type = source.Type, Batch = source.Batch, Images = source.Images .Select(CopyRecord) .ToList() }; } /// /// Копирует , отвязывая её от базы данных /// /// Скопированная private static ImageRecord CopyRecord(ImageRecord source) { return new ImageRecord { Id = source.Id, ImagePath = source.ImagePath, VisibleIntensity = source.VisibleIntensity, Uv365Intensity = source.Uv365Intensity, Uv254Intensity = source.Uv254Intensity }; } }