comprehensive update

1) remove unused components: camera view (due it causes segmentation faults), compute resources and all related, illuminator controller (due in useless without camera view), image storage service, temperature and humidity service
2) database schema - reduce tables references, features storing in records themselves in compressed form, add records creation and editing date and time, add separator comment column
3) analysis - rework of pipeline and ui, now database storing only raw data and all display values calculated from it
4) lights service - add reconnection if disconnected
5) add width and height command line arguments
6) fix some typos and other issues
This commit is contained in:
2026-06-29 15:13:29 +03:00
parent caee9fafd5
commit 72ae26eee7
74 changed files with 5219 additions and 4248 deletions
File diff suppressed because it is too large Load Diff
@@ -31,6 +31,45 @@ public partial class BrandCalibrationViewModel : ViewModelBase
}
}
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
/// <summary>
/// <see cref="BrandRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="BrandRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="BrandRecord.BrandName"/>
/// </summary>
[ObservableProperty] public partial string BrandName { get; set; } = "";
/// <summary>
/// <see cref="BrandRecord.MixtureName"/>
/// </summary>
[ObservableProperty] public partial string MixtureName { get; set; } = "";
/// <summary>
/// <see cref="BrandRecord.MixtureNormalRate"/>
/// </summary>
[ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1;
/// <summary>
/// <see cref="BrandRecord.Comment"/>
/// </summary>
[ObservableProperty] public partial string Comment { get; set; } = "";
}
/// <summary>
/// Логгер
/// </summary>
@@ -60,30 +99,33 @@ public partial class BrandCalibrationViewModel : ViewModelBase
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
/// </summary>
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary>
/// <see cref="BrandRecord.Id"/> выбранного элемента
/// Запись
/// </summary>
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <summary>
/// <see cref="BrandRecord.Uuid"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
/// Текущий экран информационной секции
/// </summary>
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
/// <summary>
/// <see cref="BrandRecord.BrandName"/> выбранного элемента
/// Можно ли переходить на следующий экран информационной секции
/// </summary>
[ObservableProperty] public partial string SelectedBrandName { get; set; } = "";
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
/// <summary>
/// <see cref="BrandRecord.MixtureName"/> выбранного элемента
/// Можно ли переходить на предыдущий экран информационной секции
/// </summary>
[ObservableProperty] public partial string SelectedMixtureName { get; set; } = "";
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
/// <summary>
/// <see cref="BrandRecord.MixtureNormalRate"/> выбранного элемента
/// Происходит ли переход между экранами информационной секции
/// </summary>
[ObservableProperty] public partial double SelectedMixtureNormalRate { get; set; } = -1;
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
/// <summary>
/// Можно ли производить перемещение между экранами
/// Можно ли взаимодействовать с объектом
/// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; } = true;
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
public BrandCalibrationViewModel(
ILogger<BrandCalibrationViewModel> logger,
@@ -109,8 +151,9 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Подписываемся на изменения свойств для обновления CanScroll
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(MenuOpened))
UpdateCanScroll();
if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
};
_logger.LogInformation("Инициализировано");
@@ -133,11 +176,14 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Если запись найдена, то заполняем поля из базы данных
if (selectedRecord is not null)
{
SelectedId = selectedRecord.Id;
SelectedUuid = selectedRecord.Uuid;
SelectedBrandName = selectedRecord.BrandName;
SelectedMixtureName = selectedRecord.MixtureName;
SelectedMixtureNormalRate = selectedRecord.MixtureNormalRate;
Record.Id = selectedRecord.Id;
Record.Uuid = selectedRecord.Uuid;
Record.CreateDateTime = selectedRecord.CreateDateTime;
Record.EditDateTime = selectedRecord.EditDateTime;
Record.BrandName = selectedRecord.BrandName;
Record.MixtureName = selectedRecord.MixtureName;
Record.MixtureNormalRate = selectedRecord.MixtureNormalRate;
Record.Comment = selectedRecord.Comment;
return;
}
// Если значение не найдено в базе данных
@@ -156,19 +202,24 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент");
SelectedId = 0;
SelectedUuid = Guid.NewGuid().ToString();
SelectedBrandName = "";
SelectedMixtureName = "";
SelectedMixtureNormalRate = -1;
var dateTime = DateTime.UtcNow;
Record.Id = 0;
Record.Uuid = Guid.NewGuid().ToString();
Record.CreateDateTime = dateTime;
Record.EditDateTime = dateTime;
Record.BrandName = "";
Record.MixtureName = "";
Record.MixtureNormalRate = -1;
Record.Comment = "";
}
/// <summary>
/// Обновить значение <see cref="CanScroll"/>
/// Обновить значение <see cref="CanInteract"/>
/// </summary>
private void UpdateCanScroll()
private void UpdateCanInteract()
{
CanScroll = !MenuOpened;
CanInteract = !MenuOpened;
CanInteract &= !InfoSectionIsScrolling;
}
@@ -185,6 +236,15 @@ public partial class BrandCalibrationViewModel : ViewModelBase
/// Переключить видимость меню
/// </summary>
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
/// <summary>
/// Переключить информационную секцию на следующую
/// </summary>
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
/// <summary>
/// Переключить информационную секцию на предыдущую
/// </summary>
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
/// <summary>
/// Создать новую запись
@@ -212,7 +272,7 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try
{
_logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName);
_logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
// Если запись не выбрана, то просто создаём новую запись
if (MenuSelectedIndex < 0)
{
@@ -247,7 +307,7 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Сохраняем индекс удалённый записи и находим её в базе данных
var deletedMenuIndex = MenuSelectedIndex;
var deletedRecordId = SelectedId;
var deletedRecordId = Record.Id;
var deletedRecord = _context.BrandRecords.Find(deletedRecordId);
// Создаём новую запись
CreateRecord();
@@ -299,17 +359,21 @@ public partial class BrandCalibrationViewModel : ViewModelBase
{
try
{
_logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName);
_logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
// Если запись не выбрана
if (MenuSelectedIndex < 0)
{
_logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Создаём новую запись
var newRecord = new BrandRecord(
uuid: SelectedUuid,
brandName: SelectedBrandName,
mixtureName: SelectedMixtureName,
mixtureNormalRate: SelectedMixtureNormalRate
uuid: Record.Uuid,
createDateTime: dateTime,
editDateTime: dateTime,
brandName: Record.BrandName,
mixtureName: Record.MixtureName,
mixtureNormalRate: Record.MixtureNormalRate,
comment: Record.Comment
);
// Добавляем запись в базу данных
@@ -328,22 +392,22 @@ public partial class BrandCalibrationViewModel : ViewModelBase
else
{
// Находим запись в базе данных
var attachedRecord = _context.BrandRecords.Find(SelectedId);
var attachedRecord = _context.BrandRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.BrandRecords));
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.BrandRecords));
return;
}
_logger.LogTrace("Записываем изменения в базу данных");
// Если в данных есть изменения, то записываем их
if (attachedRecord.BrandName != SelectedBrandName)
attachedRecord.BrandName = SelectedBrandName;
if (attachedRecord.MixtureName != SelectedMixtureName)
attachedRecord.MixtureName = SelectedMixtureName;
if (attachedRecord.MixtureNormalRate != SelectedMixtureNormalRate)
attachedRecord.MixtureNormalRate = SelectedMixtureNormalRate;
// Записываем изменения
attachedRecord.EditDateTime = DateTime.UtcNow;
attachedRecord.BrandName = Record.BrandName;
attachedRecord.MixtureName = Record.MixtureName;
attachedRecord.MixtureNormalRate = Record.MixtureNormalRate;
attachedRecord.Comment = Record.Comment;
// Сохраняем изменения в базе данных
_context.SaveChanges();
@@ -351,8 +415,8 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].BrandName = SelectedBrandName;
MenuItems[MenuSelectedIndex].MixtureName = SelectedMixtureName;
MenuItems[MenuSelectedIndex].BrandName = Record.BrandName;
MenuItems[MenuSelectedIndex].MixtureName = Record.MixtureName;
}
}
catch (Exception ex)
@@ -53,6 +53,63 @@ public partial class SampleCalibrationViewModel : ViewModelBase
}
}
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
/// <summary>
/// <see cref="SampleRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="SampleRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.SampleName"/>
/// </summary>
[ObservableProperty] public partial string SampleName { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.SampleComment"/>
/// </summary>
[ObservableProperty] public partial string SampleComment { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.SamplingDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.SamplingPlace"/>
/// </summary>
[ObservableProperty] public partial string SamplingPlace { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.MixtureActualRate"/>
/// </summary>
[ObservableProperty] public partial double MixtureActualRate { get; set; } = -1;
/// <summary>
/// <see cref="SampleRecord.MeasuredContent"/>
/// </summary>
[ObservableProperty] public partial double MeasuredContent { get; set; } = -1;
/// <summary>
/// Представление <see cref="SampleRecord.ImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
/// <summary>
/// Данные <see cref="SampleRecord.FeatureIds"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Features { get; set; } = new();
}
/// <summary>
/// Логгер
/// </summary>
@@ -83,19 +140,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// </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>
@@ -109,43 +153,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// </summary>
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary>
/// Запись
/// </summary>
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <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>
@@ -172,7 +184,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// </summary>
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
/// <summary>
/// Текущий экран информационной секции
/// </summary>
@@ -200,9 +211,9 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
/// <summary>
/// Можно ли производить перемещение между экранами
/// Можно ли взаимодействовать с объектом
/// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; }
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
public SampleCalibrationViewModel(
ILogger<SampleCalibrationViewModel> logger,
@@ -220,13 +231,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_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)));
// Подписываемся на изменения базы данных для отслеживания изменений списка марок
@@ -242,7 +246,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
return;
// Если запись добавлена
if (ea.NewState is EntityState.Added)
if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged)
{
// Добавляем запись
BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName));
@@ -284,13 +288,20 @@ public partial class SampleCalibrationViewModel : ViewModelBase
});
};
// Получаем записи меню из базы данных
MenuItems = new ObservableCollection<MenuItemViewModel>(_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))
UpdateCanScroll();
UpdateCanInteract();
};
_logger.LogInformation("Инициализировано");
@@ -314,32 +325,36 @@ public partial class SampleCalibrationViewModel : ViewModelBase
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;
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<float[]>();
// Ищем изображение в базе данных
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;
// В любом случае заполняем изображение
Record.Image.Data = imageRecord?.Base64 ?? "";
// Ищем марку в базе данных
var brandRecord = _context.BrandRecords.Find(_selectedBrandId);
var brandRecord = _context.BrandRecords.Find(selectedRecord.BrandId);
// Если марка не найдена в базе данных
if (brandRecord is null)
{
// Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", _selectedBrandId, nameof(_context.BrandRecords));
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.BrandId, nameof(_context.BrandRecords));
// Устанавливаем, что элемент не выбран
BrandComboBoxSelectedIndex = -1;
}
@@ -356,12 +371,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
BrandComboBoxSelectedIndex = BrandComboBoxItems.IndexOf(brandComboBoxItem);
}
// Ищем все признаки в базе данных
_selectedFeaturesData = _context.FeatureRecords
.Where(r => r.SampleRecordId == SelectedId)
.Select(r => r.Values.ToArray())
.ToList();
return;
}
// Если значение не найдено в базе данных
@@ -380,36 +389,30 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент");
SelectedId = 0;
SelectedUuid = Guid.NewGuid().ToString();
SelectedSampleName = "";
SelectedSampleComment = "";
SelectedSamplingDateTime = DateTime.UtcNow;
SelectedSamplingPlace = "";
SelectedMixtureActualRate = -1;
SelectedMeasuredContent = -1;
SelectedImage.Data = null;
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<float[]>();
BrandComboBoxSelectedIndex = -1;
}
/// <summary>
/// Вызывается при изменении <see cref="BrandComboBoxSelectedIndexC"/>
/// Обновить значение <see cref="CanInteract"/>
/// </summary>
partial void OnBrandComboBoxSelectedIndexChanged(int value)
private void UpdateCanInteract()
{
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;
CanInteract = !MenuOpened;
CanInteract &= !MainSectionIsScrolling;
CanInteract &= !InfoSectionIsScrolling;
}
// Команды управления меню
@@ -449,7 +452,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
// поэтому если Record.Record != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
if (MenuSelectedIndex != -1)
MenuSelectedIndex = -1;
else
@@ -465,7 +468,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try
{
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName);
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.SampleName), Record.SampleName);
// Если запись не выбрана, то просто создаём новую запись
if (MenuSelectedIndex < 0)
{
@@ -500,7 +503,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Сохраняем индекс удалённый записи и находим её в базе данных
var deletedMenuIndex = MenuSelectedIndex;
var deletedRecordId = SelectedId;
var deletedRecordId = Record.Id;
var deletedRecord = _context.SampleRecords.Find(deletedRecordId);
// Создаём новую запись
CreateRecord();
@@ -510,7 +513,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
if (deletedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SampleRecords));
return;
}
@@ -552,33 +555,50 @@ public partial class SampleCalibrationViewModel : ViewModelBase
{
try
{
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName);
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.SampleName), Record.SampleName);
// Если запись не выбрана
if (MenuSelectedIndex < 0)
{
// Создаём запись изображения
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
// Добавляем запись изображения в базу данных
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
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: SelectedUuid,
sampleName: SelectedSampleName,
sampleComment: SelectedSampleComment,
samplingDateTime: SelectedSamplingDateTime,
brandId: _selectedBrandId,
samplingPlace: SelectedSamplingPlace,
mixtureActualRate: SelectedMixtureActualRate,
measuredContent: SelectedMeasuredContent,
imageId: attachedImageRecord.Entity.Id,
featureIds: []
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);
@@ -586,29 +606,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_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));
// Выбираем последний элемент из меню
@@ -618,11 +615,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase
else
{
// Находим запись в базе данных
var attachedRecord = _context.SampleRecords.Find(SelectedId);
var attachedRecord = _context.SampleRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SampleRecords));
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SampleRecords));
return;
}
@@ -630,79 +627,65 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Ищем запись изображения в базе данных
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++)
// Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data))
{
var featureRecord = new FeatureRecord(
values: _selectedFeaturesData[i],
sampleRecordId: attachedRecord.Id
);
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
attachedFeatureRecords.Append(attachedFeatureRecord.Entity);
_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;
}
}
_context.SaveChanges();
_logger.LogTrace("Записи признаков сохранена");
// Определяем 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;
// Если в данных есть изменения, то записываем их
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();
// Записываем изменения
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();
@@ -710,7 +693,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].SampleName = SelectedSampleName;
MenuItems[MenuSelectedIndex].SampleName = Record.SampleName;
}
}
catch (Exception ex)
@@ -736,10 +719,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}";
// Запускаем задачу получения изображений
_сaptureImagesTask = Task.Run(async () =>
@@ -748,7 +728,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
@@ -761,7 +741,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
@@ -793,7 +773,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
@@ -846,7 +826,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
var (avgImage, roi, features) = result.Value;
// Сохраняем вектор признаков
_selectedFeaturesData = features;
Record.Features = features;
// Обрабатываем среднее изображение
avgImage = new OpenCvSharp.Mat(avgImage, roi);
@@ -861,15 +841,14 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Кодируем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
_selectedImageData = Convert.ToBase64String(avgData);
// Очищаем память среднего изображения
avgImage.Dispose();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData);
_ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
}, cancellationToken);
// Создаём задачу которая выполнится после завершения всех других
@@ -922,7 +901,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
}
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() =>
{
@@ -35,6 +35,45 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
}
}
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
/// <summary>
/// <see cref="SeparatorRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="SeparatorRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SeparatorRecord.Type"/>
/// </summary>
[ObservableProperty] public partial string Type { get; set; } = "";
/// <summary>
/// <see cref="SeparatorRecord.Comment"/>
/// </summary>
[ObservableProperty] public partial string Comment { get; set; } = "";
/// <summary>
/// Представление <see cref="SeparatorRecord.ImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
/// <summary>
/// Данные <see cref="SeparatorRecord.Features"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Features { get; set; } = new();
}
/// <summary>
/// Логгер
/// </summary>
@@ -65,15 +104,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// </summary>
private CancellationTokenSource? _captureImagesTaskCts = null;
/// <summary>
/// Данные <see cref="SeparatorRecord.ImageId"/> выбранного элемента
/// </summary>
private string? _selectedImageData = null;
/// <summary>
/// Данные <see cref="SeparatorRecord.FeatureIds"/> выбранного элемента
/// </summary>
private List<float[]> _selectedFeaturesData = new();
/// <summary>
/// Меню открыто
/// </summary>
@@ -88,21 +118,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary>
/// <see cref="SeparatorRecord.Id"/> выбранного элемента
/// Запись
/// </summary>
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
/// <summary>
/// <see cref="SeparatorRecord.Uuid"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SeparatorRecord.Type"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedType { get; set; } = "";
/// <summary>
/// Представление <see cref="SeparatorRecord.ImageId"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <summary>
/// Текущий экран главной секции
@@ -112,6 +130,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// Можно ли переходить на следующий экран главной секции
/// </summary>
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
/// <summary>
/// Можно ли переходить на предыдущий экран главной секции
/// </summary>
@@ -120,6 +139,22 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// Происходит ли переход между экранами главной секции
/// </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>
/// Проводится ли съёмка изображений
@@ -131,9 +166,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
/// <summary>
/// Можно ли производить перемещение между экранами
/// Можно ли взаимодействовать с объектом
/// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; }
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
public SeparatorCalibrationViewModel(
ILogger<SeparatorCalibrationViewModel> logger,
@@ -162,8 +197,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling))
UpdateCanScroll();
ea.PropertyName == nameof(MainSectionIsScrolling) ||
ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
};
_logger.LogInformation("Инициализировано");
@@ -187,24 +223,26 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
if (selectedRecord is not null)
{
// Заполняем простые поля
SelectedId = selectedRecord.Id;
SelectedUuid = selectedRecord.Uuid;
SelectedType = selectedRecord.Type;
Record.Id = selectedRecord.Id;
Record.Uuid = selectedRecord.Uuid;
Record.CreateDateTime = selectedRecord.CreateDateTime;
Record.EditDateTime = selectedRecord.EditDateTime;
Record.Type = selectedRecord.Type;
Record.Comment = selectedRecord.Comment;
if (selectedRecord.Features.Count() > 0)
Record.Features = selectedRecord.Features.Chunk(AnalyzerService.FEATURES_LENGTH).Select(v => v.ToArray()).ToList();
else
Record.Features = new List<float[]>();
// Ищем изображение в базе данных
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;
// В любом случае заполняем изображение
Record.Image.Data = imageRecord?.Base64 ?? "";
// Ищем все признаки в базе данных
_selectedFeaturesData = _context.FeatureRecords
.Where(r => r.SeparatorRecordId == SelectedId)
.Select(r => r.Values.ToArray())
.ToList();
return;
}
@@ -224,20 +262,25 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент");
SelectedId = 0;
SelectedUuid = Guid.NewGuid().ToString();
SelectedType = "";
SelectedImage.Data = null;
var dateTime = DateTime.UtcNow;
Record.Id = 0;
Record.Uuid = Guid.NewGuid().ToString();
Record.CreateDateTime = dateTime;
Record.EditDateTime = dateTime;
Record.Type = "";
Record.Comment = "";
Record.Image.Data = "";
Record.Features = new List<float[]>();
}
/// <summary>
/// Обновить значение <see cref="CanScroll"/>
/// Обновить значение <see cref="CanInteract"/>
/// </summary>
private void UpdateCanScroll()
private void UpdateCanInteract()
{
CanScroll = !MenuOpened;
CanScroll &= !MainSectionIsScrolling;
CanScroll &= !ImagesIsCapturing;
CanInteract = !MenuOpened;
CanInteract &= !MainSectionIsScrolling;
CanInteract &= !InfoSectionIsScrolling;
}
// Команды управления меню
@@ -258,7 +301,15 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// Переключить главную секцию информация <-> изображения
/// </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>
@@ -285,7 +336,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try
{
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), Record.Id, nameof(SeparatorRecord.Type), Record.Type);
// Если запись не выбрана, то просто создаём новую запись
if (MenuSelectedIndex < 0)
{
@@ -320,7 +371,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Сохраняем индекс удалённый записи и находим её в базе данных
var deletedMenuIndex = MenuSelectedIndex;
var deletedRecordId = SelectedId;
var deletedRecordId = Record.Id;
var deletedRecord = _context.SeparatorRecords.Find(deletedRecordId);
// Создаём новую запись
CreateRecord();
@@ -330,7 +381,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
if (deletedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SeparatorRecords));
return;
}
@@ -372,27 +423,36 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
{
try
{
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), Record.Id, nameof(SeparatorRecord.Type), Record.Type);
// Если запись не выбрана
if (MenuSelectedIndex < 0)
{
// Создаём запись изображения
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
// Добавляем запись изображения в базу данных
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
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;
}
_logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Заполняем запись
var newRecord = new SeparatorRecord(
uuid: SelectedUuid,
type: SelectedType,
imageId: attachedImageRecord.Entity.Id,
featureIds: []
uuid: Record.Uuid,
createDateTime: dateTime,
editDateTime: dateTime,
type: Record.Type,
comment: Record.Comment,
imageId: imageId,
features: Record.Features
);
// Добавляем запись в базу данных
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
@@ -400,29 +460,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
_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.Type));
// Выбираем последний элемент из меню
@@ -432,11 +469,11 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
else
{
// Находим запись в базе данных
var attachedRecord = _context.SeparatorRecords.Find(SelectedId);
var attachedRecord = _context.SeparatorRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null)
{
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SeparatorRecords));
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SeparatorRecords));
return;
}
@@ -449,62 +486,48 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
{
// Логгируем как ошибку
_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.SeparatorRecordId == 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++)
// Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data))
{
var featureRecord = new FeatureRecord(
values: _selectedFeaturesData[i],
sampleRecordId: attachedRecord.Id
);
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
attachedFeatureRecords.Append(attachedFeatureRecord.Entity);
_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;
}
}
_context.SaveChanges();
_logger.LogTrace("Записи признаков сохранена");
// Если в данных есть изменения, то записываем их
if (attachedRecord.Type != SelectedType)
attachedRecord.Type = SelectedType;
if (attachedRecord.ImageId != attachedImageRecord.Id)
attachedRecord.ImageId = attachedImageRecord.Id;
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
// Записываем изменения
attachedRecord.EditDateTime = DateTime.UtcNow;
attachedRecord.Type = Record.Type;
attachedRecord.Comment = Record.Comment;
attachedRecord.Features = Record.Features.SelectMany(fs => fs).ToList();
// Сохраняем изменения в базе данных
_context.SaveChanges();
@@ -512,7 +535,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].Type = SelectedType;
MenuItems[MenuSelectedIndex].Type = Record.Type;
}
}
catch (Exception ex)
@@ -538,10 +561,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
_captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}";
// Запускаем получение изображений асинхронно
_сaptureImagesTask = Task.Run(async () =>
@@ -550,7 +570,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
@@ -563,7 +583,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
@@ -595,7 +615,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
@@ -648,7 +668,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
var (avgImage, roi, features) = result.Value;
// Сохраняем вектор признаков
_selectedFeaturesData = features;
Record.Features = features;
// Обрабатываем среднее изображение
avgImage = new OpenCvSharp.Mat(avgImage, roi);
@@ -663,15 +683,14 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Кодируем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
_selectedImageData = Convert.ToBase64String(avgData);
// Очищаем память среднего изображения
avgImage.Dispose();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData);
_ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
}, cancellationToken);
// Создаём задачу которая выполнится после завершения всех других
@@ -724,7 +743,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
}
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() =>
{
@@ -737,6 +756,8 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
});
}
// Команда отмены задачи съёмки изображений
/// <summary>
/// Отменить задачу съёмки изображения
/// </summary>
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
}