forked from amkovkov/GranuSightSoftware2
658 lines
36 KiB
C#
658 lines
36 KiB
C#
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;
|
|
using System.Diagnostics;
|
|
|
|
namespace GSS2.ViewModels.AmineContent;
|
|
|
|
public partial class SampleCalibrationViewModel : ViewModelBase
|
|
{
|
|
private readonly ILogger<SampleCalibrationViewModel> _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<CalibrationRecord> Records { get; set; }
|
|
|
|
[ObservableProperty] public partial CalibrationRecord? SelectedRecord { get; set; }
|
|
[ObservableProperty] public partial CalibrationRecord EditingRecord { get; set; }
|
|
|
|
[ObservableProperty] public partial int MainSectionCurrentIndex { 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 int InfoSectionCurrentIndex { get; set; } = 0;
|
|
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
|
|
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true;
|
|
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true;
|
|
|
|
[ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null;
|
|
|
|
[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;
|
|
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
|
|
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
|
|
|
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> SeparatorImages { get; set; } = new();
|
|
[ObservableProperty] public partial int SeparatorImagesSectionCurrentIndex { get; set; } = 0;
|
|
[ObservableProperty] public partial bool SeparatorImagesSectionCanScrollForward { get; set; } = false;
|
|
[ObservableProperty] public partial bool SeparatorImagesSectionCanScrollBackward { get; set; } = false;
|
|
[ObservableProperty] public partial bool SeparatorImagesSectionIsScrolling { get; set; } = true;
|
|
|
|
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> SampleImages { get; set; } = new();
|
|
[ObservableProperty] public partial int SampleImagesSectionCurrentIndex { get; set; } = 0;
|
|
[ObservableProperty] public partial bool SampleImagesSectionCanScrollForward { get; set; } = false;
|
|
[ObservableProperty] public partial bool SampleImagesSectionCanScrollBackward { get; set; } = false;
|
|
[ObservableProperty] public partial bool SampleImagesSectionIsScrolling { get; set; } = true;
|
|
|
|
|
|
public SampleCalibrationViewModel(
|
|
ILogger<SampleCalibrationViewModel> logger,
|
|
AmineContentCalibrationContext context,
|
|
AmineContentImageCapturerService imageCapturer,
|
|
ImageStorageService imageStorage
|
|
)
|
|
{
|
|
_logger = logger;
|
|
_context = context;
|
|
_imageCapturer = imageCapturer;
|
|
_imageStorage = imageStorage;
|
|
_logger.LogInformation("Инициализация");
|
|
|
|
// Получаем записи из базы данных
|
|
Records = new ObservableCollection<CalibrationRecord>(
|
|
_context.CalibrationRecords
|
|
.Include(r => r.SampleImages)
|
|
.Include(r => r.SeparatorImages)
|
|
);
|
|
|
|
// Берём первую запись
|
|
SelectedRecord = Records.FirstOrDefault();
|
|
// Если записей нет, то создаём пустую
|
|
// на самом деле это излишне, потому что при изменении SelectedRecord
|
|
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
|
|
if (SelectedRecord is null)
|
|
EditingRecord = new()
|
|
{
|
|
SampleName = "",
|
|
SampleComment = "",
|
|
SampleBrand = "",
|
|
SampleMass = -1.0,
|
|
SamplingDateTime = DateTime.Now,
|
|
SamplingPlace = "",
|
|
MixtureName = "",
|
|
MixtureAmineContent = -1.0,
|
|
MixtureNormalRate = -1.0,
|
|
MixtureActualRate = -1.0,
|
|
MeasuredContent = -1.0,
|
|
SampleImages = new(),
|
|
SeparatorImages = new()
|
|
};
|
|
else
|
|
EditingRecord = CopyRecord(SelectedRecord);
|
|
|
|
_logger.LogInformation("Инициализировано");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
|
|
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
|
|
/// </summary>
|
|
partial void OnSelectedRecordChanged(CalibrationRecord? value)
|
|
{
|
|
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
|
|
// то создаём пустую запись
|
|
// TODO: Подумать над тем, чтобы сразу удалять несохранённые изображения из хранилища
|
|
if (SelectedRecord is null)
|
|
{
|
|
EditingRecord = new()
|
|
{
|
|
SampleName = "",
|
|
SampleComment = "",
|
|
SampleBrand = "",
|
|
SampleMass = -1.0,
|
|
SamplingDateTime = DateTime.Now,
|
|
SamplingPlace = "",
|
|
MixtureName = "",
|
|
MixtureAmineContent = -1.0,
|
|
MixtureNormalRate = -1.0,
|
|
MixtureActualRate = -1.0,
|
|
MeasuredContent = -1.0,
|
|
SampleImages = new(),
|
|
SeparatorImages = new()
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// Иначе копируем запись, отвязывая её от базы данных
|
|
EditingRecord = CopyRecord(SelectedRecord);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
|
|
/// Заполняет <see cref="Images"/> изображениями из <see cref="EditingRecord"/>.
|
|
/// </summary>
|
|
partial void OnEditingRecordChanged(CalibrationRecord value)
|
|
{
|
|
// Обновляем изображения сепаратора
|
|
{
|
|
var images = value.SeparatorImages
|
|
// Для каждого изображения
|
|
.Select(i =>
|
|
{
|
|
// Находим путь через ImageStorageService
|
|
string? path;
|
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
|
if (ImagesSectionCurrentIndex == 0)
|
|
path = _imageStorage.GetFullPath(i.ImagePath, "separator");
|
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
|
else if (ImagesSectionCurrentIndex == 1)
|
|
path = _imageStorage.GetFullPath(i.ImagePath, "sample");
|
|
else
|
|
throw new UnreachableException();
|
|
|
|
// Если изображения не найдено, возвращаем null
|
|
if (path is null)
|
|
return null;
|
|
// Создаём AsyncImageRecordViewModel
|
|
return new AsyncImageRecordViewModel(path, i);
|
|
})
|
|
// Оставляем только не null значения
|
|
.OfType<AsyncImageRecordViewModel>();
|
|
|
|
// Создаём новую коллекцию изображений
|
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
|
SeparatorImages.Clear();
|
|
SeparatorImages = new(images);
|
|
}
|
|
// Обновляем изображения пробы
|
|
{
|
|
var images = value.SampleImages
|
|
// Для каждого изображения
|
|
.Select(i =>
|
|
{
|
|
// Находим путь через ImageStorageService
|
|
var path = _imageStorage.GetFullPath(i.ImagePath, "sample");
|
|
// Если изображения не найдено, возвращаем null
|
|
if (path is null)
|
|
return null;
|
|
// Создаём AsyncImageRecordViewModel
|
|
return new AsyncImageRecordViewModel(path, i);
|
|
})
|
|
// Оставляем только не null значения
|
|
.OfType<AsyncImageRecordViewModel>();
|
|
|
|
// Создаём новую коллекцию изображений
|
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
|
SampleImages.Clear();
|
|
SampleImages = 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.CalibrationRecords.Remove(deletedRecord);
|
|
_context.SaveChanges();
|
|
}
|
|
|
|
// Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
|
|
[RelayCommand]
|
|
private void SaveRecord()
|
|
{
|
|
// Если это новая запись
|
|
if (SelectedRecord is null)
|
|
{
|
|
// Если это новая запись, то копируем её
|
|
var newRecord = CopyRecord(EditingRecord);
|
|
|
|
// Для каждого изображения сепаратора в записи
|
|
for (int i = 0; i < newRecord.SeparatorImages.Count(); i++)
|
|
{
|
|
var image = newRecord.SeparatorImages[i];
|
|
// Если изображение есть в базе данных, то пропускаем его
|
|
if (_context.Entry(image).State is not EntityState.Detached)
|
|
continue;
|
|
|
|
// Если изображения нет в базе данных, то добавляем его в базу
|
|
var attachedImage = _context.ImageRecords.Add(image);
|
|
// Удаляем отвязанное от базы данных изображение из записи
|
|
newRecord.SeparatorImages.RemoveAt(i);
|
|
// Добавляем привязанное изображение к записи
|
|
newRecord.SeparatorImages.Insert(i, attachedImage.Entity);
|
|
}
|
|
// Для каждого изображения пробы в записи
|
|
for (int i = 0; i < newRecord.SampleImages.Count(); i++)
|
|
{
|
|
var image = newRecord.SampleImages[i];
|
|
// Если изображение есть в базе данных, то пропускаем его
|
|
if (_context.Entry(image).State is not EntityState.Detached)
|
|
continue;
|
|
|
|
// Если изображения нет в базе данных, то добавляем его в базу
|
|
var attachedImage = _context.ImageRecords.Add(image);
|
|
// Удаляем отвязанное от базы данных изображение из записи
|
|
newRecord.SampleImages.RemoveAt(i);
|
|
// Добавляем привязанное изображение к записи
|
|
newRecord.SampleImages.Insert(i, attachedImage.Entity);
|
|
}
|
|
// Если записи нет в базе данных
|
|
if (_context.Entry(newRecord).State is EntityState.Detached)
|
|
{
|
|
// То добавляем её в базу
|
|
var attachedRecord = _context.CalibrationRecords.Add(newRecord);
|
|
// Заменяем текущую запись на привязанную к базе данных
|
|
newRecord = attachedRecord.Entity;
|
|
}
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
// Добавляем запись в список
|
|
Records.Add(newRecord);
|
|
// Выбираем запись
|
|
SelectedRecord = newRecord;
|
|
}
|
|
// Если эта запись есть в базе
|
|
else
|
|
{
|
|
// Переносим данные из редактируемой записи в выбранную
|
|
SelectedRecord.SampleName = EditingRecord.SampleName;
|
|
SelectedRecord.SampleComment = EditingRecord.SampleComment;
|
|
SelectedRecord.SampleBrand = EditingRecord.SampleBrand;
|
|
SelectedRecord.SampleMass = EditingRecord.SampleMass;
|
|
SelectedRecord.SamplingDateTime = EditingRecord.SamplingDateTime;
|
|
SelectedRecord.SamplingPlace = EditingRecord.SamplingPlace;
|
|
SelectedRecord.MixtureName = EditingRecord.MixtureName;
|
|
SelectedRecord.MixtureAmineContent = EditingRecord.MixtureAmineContent;
|
|
SelectedRecord.MixtureNormalRate = EditingRecord.MixtureNormalRate;
|
|
SelectedRecord.MixtureActualRate = EditingRecord.MixtureActualRate;
|
|
SelectedRecord.MeasuredContent = EditingRecord.MeasuredContent;
|
|
|
|
// Удаляем старые изображения сепаратора в записи
|
|
{
|
|
// Находим изображения которые нужно удалить -
|
|
// те которые есть в выбранной записи, но нет в редактируемой
|
|
var imagesToRemove = SelectedRecord.SeparatorImages
|
|
.Where(e => EditingRecord.SeparatorImages.All(i => i.Id != e.Id))
|
|
.ToList();
|
|
|
|
// Удаляем старые изображения
|
|
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
|
foreach (var image in imagesToRemove)
|
|
SelectedRecord.SeparatorImages.Remove(image);
|
|
|
|
// Для каждого изображения в редактируемой записи
|
|
foreach (var editedImage in EditingRecord.SeparatorImages)
|
|
{
|
|
// Находим изображение в выбранной записи (оно будет привязано к базе данных)
|
|
var existing = SelectedRecord.SeparatorImages
|
|
.FirstOrDefault(i => i.Id == editedImage.Id);
|
|
|
|
// Если изображение не найдено
|
|
if (existing is null)
|
|
{
|
|
// Копируем изображение в выбранную запись
|
|
SelectedRecord.SeparatorImages.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.SeparatorImages.Count(); i++)
|
|
{
|
|
var image = SelectedRecord.SeparatorImages[i];
|
|
// Если изображение есть в базе данных, то пропускаем его
|
|
if (_context.Entry(image).State is not EntityState.Detached)
|
|
continue;
|
|
|
|
// Если изображения нет в базе данных, то добавляем его в базу
|
|
var attachedImage = _context.ImageRecords.Add(image);
|
|
// Удаляем отвязанное от базы данных изображение из записи
|
|
SelectedRecord.SeparatorImages.RemoveAt(i);
|
|
// Добавляем привязанное изображение к записи
|
|
SelectedRecord.SeparatorImages.Insert(i, attachedImage.Entity);
|
|
}
|
|
}
|
|
// Удаляем старые изображения пробы в записи
|
|
{
|
|
// Находим изображения которые нужно удалить -
|
|
// те которые есть в выбранной записи, но нет в редактируемой
|
|
var imagesToRemove = SelectedRecord.SampleImages
|
|
.Where(e => EditingRecord.SampleImages.All(i => i.Id != e.Id))
|
|
.ToList();
|
|
|
|
// Удаляем старые изображения
|
|
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
|
|
foreach (var image in imagesToRemove)
|
|
SelectedRecord.SampleImages.Remove(image);
|
|
|
|
// Для каждого изображения в редактируемой записи
|
|
foreach (var editedImage in EditingRecord.SampleImages)
|
|
{
|
|
// Находим изображение в выбранной записи (оно будет привязано к базе данных)
|
|
var existing = SelectedRecord.SampleImages
|
|
.FirstOrDefault(i => i.Id == editedImage.Id);
|
|
|
|
// Если изображение не найдено
|
|
if (existing is null)
|
|
{
|
|
// Копируем изображение в выбранную запись
|
|
SelectedRecord.SampleImages.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.SampleImages.Count(); i++)
|
|
{
|
|
var image = SelectedRecord.SampleImages[i];
|
|
// Если изображение есть в базе данных, то пропускаем его
|
|
if (_context.Entry(image).State is not EntityState.Detached)
|
|
continue;
|
|
|
|
// Если изображения нет в базе данных, то добавляем его в базу
|
|
var attachedImage = _context.ImageRecords.Add(image);
|
|
// Удаляем отвязанное от базы данных изображение из записи
|
|
SelectedRecord.SampleImages.RemoveAt(i);
|
|
// Добавляем привязанное изображение к записи
|
|
SelectedRecord.SampleImages.Insert(i, attachedImage.Entity);
|
|
}
|
|
}
|
|
|
|
// Если записи нет в базе данных
|
|
if (_context.Entry(SelectedRecord).State is EntityState.Detached)
|
|
{
|
|
// То добавляем её в базу
|
|
var attachedRecord = _context.CalibrationRecords.Add(SelectedRecord);
|
|
// Находим текущую запись в списке
|
|
var index = Records.IndexOf(SelectedRecord);
|
|
// Заменяем её в списке на привязанную к базе данных
|
|
Records[index] = attachedRecord.Entity;
|
|
// Заменяем выбранную запись на привязанную
|
|
SelectedRecord = attachedRecord.Entity;
|
|
}
|
|
|
|
// Сохраняем изменения в базе данных
|
|
_context.SaveChanges();
|
|
}
|
|
}
|
|
|
|
// Команда изменения основной секции (информация <-> изображения)
|
|
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
|
|
|
// Команда навигации по информации
|
|
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
|
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
|
|
|
// Команда навигации по информации
|
|
[RelayCommand] private void ImagesSectionSwitch() => ImagesSectionCurrentIndex = ImagesSectionCurrentIndex == 0 ? 1 : 0;
|
|
|
|
// Команда навигации по изображениям сепаратора
|
|
[RelayCommand] private void SeparatorImagesSectionPrevious() => SeparatorImagesSectionCurrentIndex--;
|
|
[RelayCommand] private void SeparatorImagesSectionNext() => SeparatorImagesSectionCurrentIndex++;
|
|
|
|
// Команда навигации по изображениям пробы
|
|
[RelayCommand] private void SampleImagesSectionPrevious() => SampleImagesSectionCurrentIndex--;
|
|
[RelayCommand] private void SampleImagesSectionNext() => SampleImagesSectionCurrentIndex++;
|
|
|
|
// Команда съёмки изображений
|
|
[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<ImageRecord>();
|
|
|
|
// Обновляем данные в интерфейсе
|
|
// 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;
|
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
|
if (ImagesSectionCurrentIndex == 0)
|
|
using (var stream = new MemoryStream(bytes))
|
|
fileName = await _imageStorage.SaveAsync(stream, ".png", "separator", cancellationToken);
|
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
|
else if (ImagesSectionCurrentIndex == 1)
|
|
using (var stream = new MemoryStream(bytes))
|
|
fileName = await _imageStorage.SaveAsync(stream, ".png", "sample", cancellationToken);
|
|
else
|
|
throw new UnreachableException();
|
|
|
|
// Освобождаем память изображения
|
|
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
|
|
string? path;
|
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
|
if (ImagesSectionCurrentIndex == 0)
|
|
path = _imageStorage.GetFullPath(i.ImagePath, "separator");
|
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
|
else if (ImagesSectionCurrentIndex == 1)
|
|
path = _imageStorage.GetFullPath(i.ImagePath, "sample");
|
|
else
|
|
throw new UnreachableException();
|
|
// Если изображения не найдено, возвращаем null
|
|
if (path is null)
|
|
return null;
|
|
|
|
// Создаём AsyncImageRecordViewModel
|
|
return new AsyncImageRecordViewModel(path, i);
|
|
})
|
|
// Оставляем только не null значения
|
|
.OfType<AsyncImageRecordViewModel>();
|
|
|
|
// Обновляем данные в редактируемой коллекции
|
|
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
|
|
if (ImagesSectionCurrentIndex == 0)
|
|
{
|
|
EditingRecord.SeparatorImages = imageRecords;
|
|
// Создаём новую коллекцию изображений
|
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
|
SeparatorImages = new(images);
|
|
}
|
|
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
|
|
else if (ImagesSectionCurrentIndex == 1)
|
|
{
|
|
EditingRecord.SampleImages = imageRecords;
|
|
// Создаём новую коллекцию изображений
|
|
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
|
|
SampleImages = new(images);
|
|
}
|
|
else
|
|
throw new UnreachableException();
|
|
});
|
|
|
|
// Сбрасываем состояние задачи съёмки изображений
|
|
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
|
|
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
|
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
ImagesIsCapturing = false;
|
|
ImagesCapturingProgress = "";
|
|
CaptureImagesTask = null;
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Команда отмены задачи съёмки изображений
|
|
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
|
|
|
/// <summary>
|
|
/// Копирует <see cref="CalibrationRecord"/>, отвязывая её от базы данных
|
|
/// </summary>
|
|
/// <returns>Скопированная <see cref="CalibrationRecord"/></returns>
|
|
private static CalibrationRecord CopyRecord(CalibrationRecord source)
|
|
{
|
|
return new CalibrationRecord
|
|
{
|
|
Id = source.Id,
|
|
SampleName = source.SampleName,
|
|
SampleComment = source.SampleComment,
|
|
SampleBrand = source.SampleBrand,
|
|
SampleMass = source.SampleMass,
|
|
SamplingDateTime = source.SamplingDateTime,
|
|
SamplingPlace = source.SamplingPlace,
|
|
MixtureName = source.MixtureName,
|
|
MixtureAmineContent = source.MixtureAmineContent,
|
|
MixtureNormalRate = source.MixtureNormalRate,
|
|
MixtureActualRate = source.MixtureActualRate,
|
|
MeasuredContent = source.MeasuredContent,
|
|
SampleImages = source.SampleImages
|
|
.Select(CopyRecord)
|
|
.ToList(),
|
|
SeparatorImages = source.SeparatorImages
|
|
.Select(CopyRecord)
|
|
.ToList()
|
|
};
|
|
}
|
|
/// <summary>
|
|
/// Копирует <see cref="ImageRecord"/>, отвязывая её от базы данных
|
|
/// </summary>
|
|
/// <returns>Скопированная <see cref="ImageRecord"/></returns>
|
|
private static ImageRecord CopyRecord(ImageRecord source)
|
|
{
|
|
return new ImageRecord
|
|
{
|
|
Id = source.Id,
|
|
ImagePath = source.ImagePath,
|
|
VisibleIntensity = source.VisibleIntensity,
|
|
Uv365Intensity = source.Uv365Intensity,
|
|
Uv254Intensity = source.Uv254Intensity
|
|
};
|
|
}
|
|
}
|