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
+59
View File
@@ -3,10 +3,69 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:GSS2"
xmlns:converters="using:GSS2.UI.Core.Converters"
RequestedThemeVariant="Dark">
<Application.Resources>
<converters:NotEqualsConverter x:Key="NotEqualsConverter" />
<converters:EqualsConverter x:Key="EqualsConverter" />
<converters:GreaterThanConverter x:Key="GreaterThanConverter" />
<converters:GreaterThanOrEqualConverter x:Key="GreaterThanOrEqualConverter" />
<converters:LessThanConverter x:Key="LessThanConverter" />
<converters:LessThanOrEqualConverter x:Key="LessThanOrEqualConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
<SolidColorBrush x:Key="MenuBackground" Color="#00357a" />
<SolidColorBrush x:Key="MenuBorder" Color="#0865b2" />
<SolidColorBrush x:Key="Overlay" Color="#0865b23f" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonDangerBackground" Color="#b20808" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
</Application.Resources>
<Application.Styles>
<FluentTheme />
<Style Selector="Button">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource ButtonDangerBackground}" />
</Style>
<Style Selector="Button.vertical">
<Setter Property="MinWidth" Value="40" />
</Style>
<Style Selector="TextBox">
<Setter Property="FontSize" Value="20"/>
</Style>
<Style Selector="TextBox.singleline">
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="TextBox.multiline">
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="AcceptsReturn" Value="True" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="20"/>
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.mini">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="0,0,0,0" />
</Style>
</Application.Styles>
</Application>
+8 -2
View File
@@ -18,6 +18,8 @@ public partial class Application : Avalonia.Application
{
private readonly IServiceProvider _services;
private readonly bool _fullscreen;
private readonly double _width;
private readonly double _height;
private readonly ILogger<Application> _logger;
private readonly DependencyInjectionViewLocator _viewLocator;
@@ -25,10 +27,12 @@ public partial class Application : Avalonia.Application
public NavigationService? Navigation { get; private set; } = null;
public Application(IServiceProvider services, bool fullscreen = false)
public Application(IServiceProvider services, bool fullscreen = false, int width = 0, int height = 0)
{
_services = services;
_fullscreen = fullscreen;
_width = width <= 0 ? double.NaN : width;
_height = height <= 0 ? double.NaN : height;
// Создаём AvaloniaLogSink, который перенаправляет логи из Avalonia в хост
var loggerFactory = _services.GetRequiredService<ILoggerFactory>();
@@ -124,7 +128,9 @@ public partial class Application : Avalonia.Application
{
WindowDecorations = _fullscreen ? Avalonia.Controls.WindowDecorations.None : Avalonia.Controls.WindowDecorations.Full,
Topmost = _fullscreen,
WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal
WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal,
Width = _width,
Height = _height
},
};
desktop.MainWindow = mainWindow;
+23 -7
View File
@@ -28,6 +28,8 @@ public static class UI
public static readonly Option<RenderingModes> RenderingMode;
public static readonly Option<bool> HideConsole;
public static readonly Option<bool> Mock;
public static readonly Option<int> Width;
public static readonly Option<int> Height;
static UI()
{
@@ -56,6 +58,16 @@ public static class UI
Description = "Запуск в фиктивном режиме без инициализации и использования периферии",
DefaultValueFactory = (result) => false
};
Width = new("--width")
{
Description = "Ширина окна, если меньше или равна 0, то устанавливается ширина по-умолчанию",
DefaultValueFactory = (result) => 0
};
Height = new("--height")
{
Description = "Высота окна, если меньше или равна 0, то устанавливается высота по-умолчанию",
DefaultValueFactory = (result) => 0
};
// Изменяем описания стандартных опций
Helper.TranslateDefaultOptionDescriptions(RootCommand);
@@ -65,12 +77,16 @@ public static class UI
RootCommand.Add(RenderingMode);
RootCommand.Add(HideConsole);
RootCommand.Add(Mock);
RootCommand.Add(Width);
RootCommand.Add(Height);
RootCommand.SetAction(CommandAction);
Command.Add(Fullscreen);
Command.Add(RenderingMode);
Command.Add(HideConsole);
Command.Add(Mock);
Command.Add(Width);
Command.Add(Height);
Command.SetAction(CommandAction);
}
@@ -92,6 +108,8 @@ public static class UI
var renderingMode = result.GetValue(RenderingMode);
var hideConsole = result.GetValue(HideConsole);
var mock = result.GetValue(Mock);
var width = result.GetValue(Width);
var height = result.GetValue(Height);
// Если выбран режим рендеринга DRM, то приложение будет рисоваться вместе с консолью,
// поэтому если не перехватить консоль, то вывод в неё будет рисоваться вместе с интерфейсом
@@ -105,7 +123,7 @@ public static class UI
_ = host.RunAsync();
// Собираем графическое приложение
var ui = BuildApplication(host, fullscreen);
var ui = BuildApplication(host, fullscreen, width, height);
switch (renderingMode)
{
case RenderingModes.X11:
@@ -160,15 +178,13 @@ public static class UI
builder.Services.AddAmineContentContext(builder.Configuration);
// Добавляем сервисы аппаратной части и прочее
builder.Services.AddLightsService("Hardware:Lights");
if (!mock)
{
builder.Services.AddLightsService("Hardware:Lights");
builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
// builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
builder.Services.AddIlluminatorService("Hardware:Illuminator");
builder.Services.AddCameraService("Hardware:Camera", true);
builder.Services.AddComputeResourcesService();
}
builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images"));
builder.Services.AddAmineContentImageCapturerService(mock);
builder.Services.AddAmineContentAnalyzerService();
@@ -193,8 +209,8 @@ public static class UI
return host;
}
private static AppBuilder BuildApplication(IHost host, bool fullscreen) =>
AppBuilder.Configure(() => new Application(host.Services, fullscreen))
private static AppBuilder BuildApplication(IHost host, bool fullscreen, int width, int height) =>
AppBuilder.Configure(() => new Application(host.Services, fullscreen, width, height))
.UsePlatformDetect()
.UseSkia()
.LogToTrace()
+6 -8
View File
@@ -27,10 +27,6 @@ public static class DependencyInjectionHelper
.AddSingleton<AmineContentView>()
// Core
.AddSingleton<GSS2.UI.Core.ViewModels.MainWindowViewModel>()
.AddSingleton<GSS2.UI.Core.ViewModels.CameraViewModel>()
.AddSingleton<GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel>()
.AddSingleton<GSS2.UI.Core.Views.CameraView>()
.AddSingleton<GSS2.UI.Core.Views.IlluminatorControllerView>()
// База
.AddSingleton<ViewModels.DeleteConfirmViewModel>()
.AddSingleton<ViewModels.NumericKeyboardViewModel>()
@@ -38,16 +34,18 @@ public static class DependencyInjectionHelper
.AddSingleton<Views.NumericKeyboardView>()
// Общие
.AddSingleton<ViewModels.Common.SystemViewModel>()
.AddSingleton<ViewModels.Common.CameraViewModel>()
.AddSingleton<Views.Common.SystemView>()
.AddSingleton<Views.Common.CameraView>()
// .AddSingleton<ViewModels.Common.CameraViewModel>()
// .AddSingleton<Views.Common.SystemView>()
// .AddSingleton<Views.Common.CameraView>()
// AmineContent
.AddSingleton<ViewModels.AmineContent.StartViewModel>()
.AddSingleton<ViewModels.AmineContent.AnalysisViewModel>()
.AddSingleton<ViewModels.AmineContent.SampleCalibrationViewModel>()
.AddSingleton<ViewModels.AmineContent.SeparatorCalibrationViewModel>()
.AddSingleton<ViewModels.AmineContent.BrandCalibrationViewModel>()
.AddSingleton<Views.AmineContent.StartView>()
.AddSingleton<Views.AmineContent.AnalysisView>()
.AddSingleton<Views.AmineContent.SampleCalibrationView>()
.AddSingleton<Views.AmineContent.SeparatorCalibrationView>();
.AddSingleton<Views.AmineContent.SeparatorCalibrationView>()
.AddSingleton<Views.AmineContent.BrandCalibrationView>();
}
+6 -11
View File
@@ -4,15 +4,7 @@ public partial class Program
{
// Аргументы командной строки
private static List<string> _args = new List<string>();
public static string[] Args
{
get
{
var copy = new string[_args.Count()];
_args.CopyTo(copy);
return copy;
}
}
public static string[] Args => _args.ToArray();
/// <summary>
/// Точка входа
@@ -60,8 +52,11 @@ public partial class Program
catch (Exception ex)
{
Console.Error.WriteLine("Ошибка при выполнении программы:");
Console.Write(ex.Message);
Console.Write(ex.StackTrace);
Console.Error.WriteLine(ex.Message);
if (ex.InnerException is not null)
Console.Error.WriteLine($"Внутреннее исключение: {ex.InnerException}");
Console.Error.WriteLine(ex.StackTrace);
Environment.Exit(1);
return 1;
}
}
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();
}
+37 -7
View File
@@ -7,6 +7,8 @@ using GSS2.ViewModels.AmineContent;
using ViewModelBase = GSS2.UI.Core.ViewModels.ViewModelBase;
using System.ComponentModel;
namespace GSS2.ViewModels;
public partial class AmineContentViewModel : ViewModelBase
@@ -17,26 +19,54 @@ public partial class AmineContentViewModel : ViewModelBase
[ObservableProperty] public partial SampleCalibrationViewModel SampleCalibration { get; set; }
[ObservableProperty] public partial SeparatorCalibrationViewModel SeparatorCalibration { get; set; }
// [ObservableProperty] public partial CameraViewModel Camera { get; set; }
[ObservableProperty] public partial SystemViewModel System { get; set; }
[ObservableProperty] public partial BrandCalibrationViewModel BrandCalibration { get; set; }
// [ObservableProperty] public partial SystemViewModel System { get; set; }
[ObservableProperty] public partial int CurrentIndex { get; set; }
[ObservableProperty] public partial bool IsScrolling { get; set; }
[ObservableProperty] public partial int CurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool IsScrolling { get; set; } = false;
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
public AmineContentViewModel(
NavigationService navigationService,
AnalysisViewModel analysis,
SampleCalibrationViewModel sampleCalibration,
SeparatorCalibrationViewModel separatorCalibration,
// CameraViewModel camera,
SystemViewModel system
BrandCalibrationViewModel brandCalibration
)
{
_navigationService = navigationService;
Analysis = analysis;
SampleCalibration = sampleCalibration;
SeparatorCalibration = separatorCalibration;
// Camera = camera;
System = system;
BrandCalibration = brandCalibration;
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(IsScrolling))
UpdateCanInteract();
};
Analysis.PropertyChanged += SubViewModelPropertyChanged;
SampleCalibration.PropertyChanged += SubViewModelPropertyChanged;
SeparatorCalibration.PropertyChanged += SubViewModelPropertyChanged;
BrandCalibration.PropertyChanged += SubViewModelPropertyChanged;
}
private void SubViewModelPropertyChanged(object? sender, PropertyChangedEventArgs ea)
{
if (ea.PropertyName == nameof(Analysis.CanInteract) ||
ea.PropertyName == nameof(Analysis.ImagesIsCapturing))
UpdateCanInteract();
}
private void UpdateCanInteract()
{
CanInteract = !IsScrolling;
CanInteract &= Analysis.CanInteract;
CanInteract &= SampleCalibration.CanInteract;
CanInteract &= SeparatorCalibration.CanInteract;
CanInteract &= BrandCalibration.CanInteract;
CanInteract &= !Analysis.ImagesIsCapturing;
CanInteract &= !SampleCalibration.ImagesIsCapturing;
CanInteract &= !SeparatorCalibration.ImagesIsCapturing;
}
[RelayCommand]
-20
View File
@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using GSS2.UI.Core.ViewModels;
namespace GSS2.ViewModels.Common;
public partial class CameraViewModel : ViewModelBase
{
[ObservableProperty] public partial GSS2.UI.Core.ViewModels.CameraViewModel Camera { get; set; }
[ObservableProperty] public partial GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel Illuminator { get; set; }
public CameraViewModel(
GSS2.UI.Core.ViewModels.CameraViewModel camera,
GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel illuminator
)
{
Camera = camera;
Illuminator = illuminator;
}
}
File diff suppressed because it is too large Load Diff
@@ -16,8 +16,12 @@ public partial class AnalysisView : UserControl
private void OnChartsUpdate(object? sender, EventArgs ea)
{
MeanValuesChart.CoreChart.Update();
VariancesChart.CoreChart.Update();
QualitiesChart.CoreChart.Update();
ProcessedAreasHistogram.CoreChart.Update();
UnprocessedAreasHistogram.CoreChart.Update();
HotspotAreasHistogram.CoreChart.Update();
OpticalIntegralsHistogram.CoreChart.Update();
InnerStdDevsHistogram.CoreChart.Update();
InnerHeterogeneitiesHistogram.CoreChart.Update();
ParticleValuesHistogram.CoreChart.Update();
}
}
+188 -100
View File
@@ -14,7 +14,7 @@
<Grid Background="{StaticResource MainBackground}">
<!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}">
<Grid IsEnabled="{Binding CanInteract}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
@@ -40,12 +40,14 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
<ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Информация -->
<RowDefinition Height="*" /> <!-- Информация -->
<RowDefinition Height="Auto" /> <!-- Кнопки -->
</Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing>
@@ -55,115 +57,201 @@
Classes="vertical"
Command="{Binding ToggleMenuCommand}"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="3"
VerticalAlignment="Stretch"
>
<TextBlock Text="☰" />
<TextBlock Text="☰"/>
</Button>
<!-- Информация -->
<Grid Grid.Column="1">
<core:SectionPanel
CanScrollBackward="{Binding InfoSectionCanScrollBackward}"
CanScrollForward="{Binding InfoSectionCanScrollForward}"
ClipToBounds="True"
CurrentIndex="{Binding InfoSectionCurrentIndex}"
Cyclic="True"
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="0"
IsScrolling="{Binding InfoSectionIsScrolling}"
Orientation="Horizontal"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Id / Uuid / BrandName / MixtureName / MixtureNormalRate -->
<Grid>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- BrandName (подпись) | MixtureName (подпись) -->
<RowDefinition Height="Auto" /> <!-- BrandName (поле ) | MixtureName (поле ) -->
<RowDefinition Height="Auto" /> <!-- MixtureNormalRate (подпись) -->
<RowDefinition Height="Auto" /> <!-- MixtureNormalRate (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.RowSpacing>5</Grid.RowSpacing>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- BrandName (подпись) -->
<RowDefinition Height="Auto" /> <!-- BrandName (поле ) -->
<RowDefinition Height="Auto" /> <!-- MixtureName (подпись) | MixtureNormalRate (подпись) -->
<RowDefinition Height="Auto" /> <!-- MixtureName (поле ) | MixtureNormalRate (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<!-- Id (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="0"
Text="ИД:"
/>
<!-- Id (поле) -->
<TextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="ИД"
Text="{Binding SelectedId}"
/>
<!-- Uuid (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="1"
Grid.Row="0"
Text="Уникальный ИД:"
/>
<!-- Uuid (поле) -->
<TextBox
Classes="singleline"
Grid.Column="1"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="Уникальный ИД"
Text="{Binding SelectedUuid}"
/>
<!-- BrandName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="2"
Text="Марка продукта:"
/>
<!-- BrandName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="3"
PlaceholderText="Марка продукта"
Text="{Binding SelectedBrandName}"
/>
<!-- MixtureName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="1"
Grid.Row="2"
Text="Марка смеси:"
/>
<!-- MixtureName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="1"
Grid.Row="3"
PlaceholderText="Марка смеси"
Text="{Binding SelectedMixtureName}"
/>
<!-- MixtureNormalRate (подпись) -->
<TextBlock
Classes="mini" Text="Норма расхода смеси (кг/т | гр/кг):"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
/>
<!-- MixtureNormalRate (поле) -->
<controls:TouchNumericUpDown
Classes="singleline"
FormatString="0.0000"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="5"
Increment="0.0001"
PlaceholderText="Норма расхода смеси (кг/т | гр/кг)"
Value="{Binding SelectedMixtureNormalRate}"
/>
<Grid.RowSpacing>5</Grid.RowSpacing>
</Grid>
<!-- Id (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="0"
Text="ИД:"
/>
<!-- Id (поле) -->
<TextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="ИД"
Text="{Binding Record.Id}"
/>
<!-- Uuid (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="1"
Grid.Row="0"
Text="Уникальный ИД:"
/>
<!-- Uuid (поле) -->
<TextBox
Classes="singleline"
Grid.Column="1"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="Уникальный ИД"
Text="{Binding Record.Uuid}"
/>
<!-- BrandName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="2"
Text="Марка продукта:"
/>
<!-- BrandName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="3"
PlaceholderText="Марка продукта"
Text="{Binding Record.BrandName}"
/>
<!-- MixtureName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="4"
Text="Марка смеси:"
/>
<!-- MixtureName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="5"
PlaceholderText="Марка смеси"
Text="{Binding Record.MixtureName}"
/>
<!-- MixtureNormalRate (подпись) -->
<TextBlock
Classes="mini" Text="Норма расхода смеси (кг/т | гр/кг):"
Grid.Column="1"
Grid.Row="4"
/>
<!-- MixtureNormalRate (поле) -->
<controls:TouchNumericUpDown
Classes="singleline"
FormatString="0.0000"
Grid.Column="1"
Grid.Row="5"
Increment="0.0001"
PlaceholderText="Норма расхода смеси (кг/т | гр/кг)"
Value="{Binding Record.MixtureNormalRate}"
/>
</Grid>
<!-- Comment -->
<Grid>
<Grid.RowSpacing>5</Grid.RowSpacing>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Comment (подпись) -->
<RowDefinition Height="*" /> <!-- Comment (поле ) -->
</Grid.RowDefinitions>
<!-- Comment (подпись) -->
<TextBlock
Classes="mini"
Grid.Row="0"
Text="Дополнительная информация:"
/>
<!-- Comment (поле) -->
<controls:TouchTextBox
Classes="multiline"
Grid.Row="1"
PlaceholderText="Дополнительная информация"
Text="{Binding Record.Comment}"
/>
</Grid>
</core:SectionPanel>
<!-- Кнопка влево -->
<Button
Command="{Binding InfoSectionPreviousCommand}"
Grid.Column="1"
Grid.Row="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="InfoSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
<TextBlock Text="◀"/>
</Button>
<!-- Кнопка вправо -->
<Button
Command="{Binding InfoSectionNextCommand}"
Grid.Column="2"
Grid.Row="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="InfoSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
<TextBlock Text="▶"/>
</Button>
</Grid>
@@ -14,7 +14,7 @@
<Grid Background="{StaticResource MainBackground}">
<!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}">
<Grid IsEnabled="{Binding CanInteract}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
@@ -51,8 +51,8 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
<ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
@@ -101,12 +101,12 @@
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- SampleName (подпись) | BrandComboBox (подпись) -->
<RowDefinition Height="Auto" /> <!-- SampleName (поле ) | BrandComboBox (поле ) -->
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (подпись) | SamplingPlace (подпись) -->
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (поле ) | SamplingPlace (поле ) -->
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (подпись) | SamplingPlace (подпись) -->
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (поле ) | SamplingPlace (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
@@ -126,7 +126,7 @@
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="ИД"
Text="{Binding SelectedId}"
Text="{Binding Record.Id}"
/>
<!-- Uuid (подпись) -->
<TextBlock
@@ -142,7 +142,7 @@
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="Уникальный ИД"
Text="{Binding SelectedUuid}"
Text="{Binding Record.Uuid}"
/>
<!-- SampleName (подпись) -->
<TextBlock
@@ -156,7 +156,7 @@
Grid.Row="3"
Grid.Column="0"
Classes="singleline"
Text="{Binding SelectedSampleName}"
Text="{Binding Record.SampleName}"
PlaceholderText="Название пробы"
/>
<!-- BrandComboBox (подпись) -->
@@ -180,7 +180,7 @@
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SampleCalibrationViewModel+BrandComboBoxItemViewModel">
<TextBlock
HorizontalAlignment="Center"
HorizontalAlignment="Left"
VerticalAlignment="Center"
>
<Run Text="{Binding Id, StringFormat='[{0}] '}" />
@@ -204,7 +204,7 @@
Grid.Column="0"
Grid.Row="5"
PlaceholderText="Дата и время отбора пробы"
Text="{Binding SelectedSamplingDateTime, Converter={StaticResource DateTimeConverter}}"
Text="{Binding Record.SamplingDateTime, Converter={StaticResource DateTimeConverter}}"
/>
<!-- SamplingPlace (подпись) -->
<TextBlock
@@ -219,7 +219,7 @@
Grid.Column="1"
Grid.Row="5"
PlaceholderText="Место отбора пробы"
Text="{Binding SelectedSamplingPlace}"
Text="{Binding Record.SamplingPlace}"
/>
</Grid>
@@ -258,7 +258,7 @@
Grid.Row="1"
Increment="0.0001"
PlaceholderText="Фактический расход смеси (кг/т | гр/кг)"
Value="{Binding SelectedMixtureActualRate}"
Value="{Binding Record.MixtureActualRate}"
/>
<!-- MeasuredContent (подпись) -->
<TextBlock
@@ -274,7 +274,7 @@
Grid.Row="1"
Increment="0.0001"
PlaceholderText="Измеренное содержание смеси (кг/т | гр/кг)"
Value="{Binding SelectedMeasuredContent}"
Value="{Binding Record.MeasuredContent}"
/>
<!-- SampleComment (подпись) -->
<TextBlock
@@ -290,7 +290,7 @@
Grid.ColumnSpan="2"
Grid.Row="3"
PlaceholderText="Дополнительная информация о пробе"
Text="{Binding SelectedSampleComment}"
Text="{Binding Record.SampleComment}"
/>
</Grid>
@@ -363,15 +363,14 @@
<core:ZoomPanImage
Background="Black"
HorizontalAlignment="Stretch"
ImageSource="{Binding SelectedImage.Image}"
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
ImageSource="{Binding Record.Image.Image}"
VerticalAlignment="Stretch"
>
<core:ZoomPanImage.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
@@ -379,11 +378,11 @@
</core:ZoomPanImage>
<!-- Оверлей "Изображение загружается" -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedImage.IsLoading" />
<Binding Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
@@ -402,8 +401,8 @@
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
@@ -417,8 +416,8 @@
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
@@ -433,18 +432,20 @@
<!-- Кнопка "Получить изображения" -->
<Button
Command="{Binding CaptureImageCommand}"
Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}"
IsVisible="{Binding !ImagesIsCapturing}"
>
<TextBlock Text="Получить изображения"/>
</Button>
<!-- Кнопка "Отменить съёмку" -->
<Button
Command="{Binding CancelImageCapturingCommand}"
Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}"
IsVisible="{Binding ImagesIsCapturing}"
>
<TextBlock Text="Отменить съёмку"/>
</Button>
@@ -14,7 +14,7 @@
<Grid Background="{StaticResource MainBackground}">
<!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}">
<Grid IsEnabled="{Binding CanInteract}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
@@ -80,7 +80,8 @@
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- Type (подпись) -->
<RowDefinition Height="Auto" /> <!-- Type (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
<RowDefinition Height="Auto" /> <!-- Comment (подпись) -->
<RowDefinition Height="*" /> <!-- Comment (поле ) -->
</Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing>
@@ -99,7 +100,7 @@
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="ИД"
Text="{Binding SelectedId}"
Text="{Binding Record.Id}"
/>
<!-- Uuid (подпись) -->
<TextBlock
@@ -115,7 +116,7 @@
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="Уникальный ИД"
Text="{Binding SelectedUuid}"
Text="{Binding Record.Uuid}"
/>
<!-- Type (подпись) -->
<TextBlock
@@ -131,25 +132,27 @@
Grid.ColumnSpan="2"
Grid.Row="3"
PlaceholderText="Тип"
Text="{Binding SelectedType}"
Text="{Binding Record.Type}"
/>
</Grid>
<!-- Comment (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Text="Дополнительная информация:"
/>
<!-- Comment (поле) -->
<controls:TouchTextBox
Classes="multiline"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="5"
PlaceholderText="Дополнительная информация"
Text="{Binding Record.Comment}"
/>
<!-- Информация -->
<Grid Grid.Column="1">
<!-- ИД (подпись) -->
<!-- <TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД:" /> -->
<!-- ИД (поле) -->
<!-- <TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Id}" PlaceholderText="ИД" IsReadOnly="True" /> -->
<!-- Тип (подпись) -->
<!-- <TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" /> -->
<!-- Тип (поле) -->
<!-- <controls:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" /> -->
<!-- Партия (подпись) -->
<!-- <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" /> -->
<!-- Партия (поле) -->
<!-- <controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" /> -->
</Grid>
</Grid>
@@ -166,25 +169,19 @@
<!-- Изображение -->
<Grid>
<!-- Изображение -->
<Grid>
</Grid>
<!-- Изображение -->
<core:ZoomPanImage
Background="Black"
HorizontalAlignment="Stretch"
ImageSource="{Binding SelectedImage.Image}"
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
ImageSource="{Binding Record.Image.Image}"
VerticalAlignment="Stretch"
>
<core:ZoomPanImage.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</core:ZoomPanImage.IsVisible>
@@ -196,7 +193,7 @@
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedImage.IsLoading" />
<Binding Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
@@ -215,8 +212,8 @@
>
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
@@ -230,8 +227,8 @@
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
@@ -246,18 +243,20 @@
<!-- Кнопка "Получить изображения" -->
<Button
Command="{Binding CaptureImageCommand}"
Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}"
IsVisible="{Binding !ImagesIsCapturing}"
>
<TextBlock Text="Получить изображения"/>
</Button>
<!-- Кнопка "Отменить съёмку" -->
<Button
Command="{Binding CancelImageCapturingCommand}"
Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}"
IsVisible="{Binding ImagesIsCapturing}"
>
<TextBlock Text="Отменить съёмку"/>
</Button>
+29 -33
View File
@@ -36,31 +36,30 @@
<Border Background="{StaticResource MainBackground}" BorderThickness="0">
<Grid Margin="5" RowSpacing="5">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Меню -->
<RowDefinition Height="*"/> <!-- Основной контент -->
</Grid.RowDefinitions>
<!-- Меню -->
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
<Grid.RowSpacing>5</Grid.RowSpacing>
<ScrollViewer.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static BoolConverters.Not}" Path="IsScrolling" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorCalibration.MenuOpened" />
</MultiBinding>
</ScrollViewer.IsEnabled>
<!-- Меню -->
<ScrollViewer
Grid.Row="0"
HorizontalScrollBarVisibility="Hidden"
IsEnabled="{Binding CanInteract}"
VerticalScrollBarVisibility="Disabled"
>
<Grid ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Анализ -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Калибровка проб -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Калибровка сепаратора -->
<!-- <ColumnDefinition Width="*" MinWidth="200"/> --> <!-- Камера -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Система -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Анализ -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Марки -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Сепараторы -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Пробы -->
</Grid.ColumnDefinitions>
<!-- Анализ -->
@@ -68,24 +67,19 @@
<TextBlock Classes="menu-text" Text="Анализ"/>
</Button>
<!-- Калибровка проб -->
<!-- Марки -->
<Button Grid.Column="1" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="1">
<TextBlock Classes="menu-text" Text="Калибровка проб"/>
<TextBlock Classes="menu-text" Text="Марки"/>
</Button>
<!-- Калибровка сепараторов -->
<!-- Сепараторы -->
<Button Grid.Column="2" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="2">
<TextBlock Classes="menu-text" Text="Калибровка сепараторов"/>
<TextBlock Classes="menu-text" Text="Сепараторы"/>
</Button>
<!-- Камера -->
<!-- <Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3">
<TextBlock Classes="menu-text" Text="Камера"/>
</Button> -->
<!-- Система -->
<Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="4">
<TextBlock Classes="menu-text" Text="Система"/>
<!-- Пробы -->
<Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3">
<TextBlock Classes="menu-text" Text="Пробы"/>
</Button>
</Grid>
@@ -93,14 +87,16 @@
<!-- Основной контент -->
<core:SectionPanel Grid.Row="1" ClipToBounds="True" CurrentIndex="{Binding CurrentIndex}" Orientation="Horizontal" IsScrolling="{Binding IsScrolling}">
<ContentControl Content="{Binding Analysis}"/> <!-- Анализ -->
<ContentControl Content="{Binding SampleCalibration}"/> <!-- Калибровка проб -->
<ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Калибровка сепаратора -->
<!-- <ContentControl Content="{Binding Camera}"/> --> <!-- Камера -->
<ContentControl Content="{Binding System}"/> <!-- Система -->
<ContentControl Content="{Binding Analysis}"/> <!-- Анализ -->
<ContentControl Content="{Binding BrandCalibration}"/> <!-- Марки -->
<ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Сепараторы -->
<ContentControl Content="{Binding SampleCalibration}"/> <!-- Пробы -->
<!-- <ContentControl Content="{Binding Camera}"/> --> <!-- Камера -->
<!-- <ContentControl Content="{Binding System}"/> --> <!-- Система -->
</core:SectionPanel>
</Grid>
</Border>
</UserControl>
-50
View File
@@ -1,50 +0,0 @@
<UserControl
x:Class="GSS2.Views.Common.CameraView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels.Common"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:DataType="vm:CameraViewModel">
<UserControl.Resources>
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
<x:Double x:Key="ButtonFontSize">20</x:Double>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
</Style>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="FontSize" Value="{StaticResource ButtonFontSize}"/>
</Style>
</UserControl.Styles>
<Border BorderThickness="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <!-- Камера -->
<ColumnDefinition Width="Auto"/> <!-- Осветитель -->
</Grid.ColumnDefinitions>
<ContentControl Grid.Column="0" Content="{Binding Camera}"/> <!-- Камера -->
<ContentControl Grid.Column="1" Content="{Binding Illuminator}"/> <!-- Осветитель -->
</Grid>
</Border>
</UserControl>
-11
View File
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.Views.Common;
public partial class CameraView : UserControl
{
public CameraView()
{
InitializeComponent();
}
}
View File
View File
View File
View File
+1 -2
View File
@@ -20,8 +20,7 @@
}
},
"ConnectionStrings": {
"AmineContentCalibrationDatabasePath": "AmineContentCalibration.db",
"AmineContentResultsDatabasePath": "AmineContentResults.db"
"AmineContentDatabasePath": "AmineContent.db"
},
"Hardware": {
"Illuminator": {