From 72ae26eee7bc3ea54ef1ac04a4932b846deda960 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Mon, 29 Jun 2026 15:13:29 +0300 Subject: [PATCH] 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 --- .../Analysis/AmineContent/AnalyzerService.cs | 122 +- .../AmineContent/Database/BrandRecord.cs | 24 +- .../Analysis/AmineContent/Database/Context.cs | 14 +- .../AmineContent/Database/FeatureRecord.cs | 37 - ... => 20260624080642_Migration0.Designer.cs} | 188 +- ...ation0.cs => 20260624080642_Migration0.cs} | 68 +- .../20260624154152_Migration1.Designer.cs | 325 +++ .../Migrations/20260624154152_Migration1.cs | 22 + .../Migrations/ContextModelSnapshot.cs | 186 +- .../AmineContent/Database/ResultRecord.cs | 203 +- .../AmineContent/Database/SampleRecord.cs | 28 +- .../AmineContent/Database/SeparatorRecord.cs | 35 +- .../AmineContent/ImageCapturerService.cs | 5 - GSS2.Core/DependencyInjectionHelper.cs | 48 +- .../ConfiurationSectionExtensions.cs | 1 - GSS2.Core/Extensions/EnumerableExtensions.cs | 28 + GSS2.Core/Hardware/CameraService.cs | 246 +- GSS2.Core/Hardware/ComputeResourcesService.cs | 143 - GSS2.Core/Hardware/LightsService.cs | 10 +- .../Hardware/TemperatureHumidityService.cs | 172 -- GSS2.Core/ImageStorageService.cs | 113 - GSS2.Core/Logging/FileLoggerProvider.cs | 5 +- .../Utils/CompressedFloatListConverter.cs | 65 + ...enConverter.cs => GreaterThanConverter.cs} | 0 ...rter.cs => GreaterThanOrEqualConverter.cs} | 2 +- ...sThenConverter.cs => LessThanConverter.cs} | 2 +- ...nverter.cs => LessThanOrEqualConverter.cs} | 2 +- GSS2.UI.Core/MainWindow.axaml | 17 +- GSS2.UI.Core/SectionPanel.cs | 6 +- .../ViewModels/AsyncImageViewModel.cs | 10 +- GSS2.UI.Core/ViewModels/CameraViewModel.cs | 74 - .../IlluminatorControllerViewModel.cs | 75 - .../ViewModels/MainWindowViewModel.cs | 2 + .../ViewModels/ResourceBarViewModel.cs | 69 - .../ViewModels/ResourceChartViewModel.cs | 67 - .../ResourceCpuUsageBarsViewModel.cs | 46 - GSS2.UI.Core/ViewModels/ResourcesViewModel.cs | 229 -- GSS2.UI.Core/Views/CameraView.axaml | 13 - GSS2.UI.Core/Views/CameraView.axaml.cs | 714 ----- .../Views/IlluminatorControllerView.axaml | 94 - .../Views/IlluminatorControllerView.axaml.cs | 11 - GSS2.UI.Core/Views/ResourceBarView.axaml | 9 - GSS2.UI.Core/Views/ResourceBarView.axaml.cs | 11 - GSS2.UI.Core/Views/ResourceChartView.axaml | 13 - GSS2.UI.Core/Views/ResourceChartView.axaml.cs | 11 - .../Views/ResourceCpuUsageBarsView.axaml | 53 - .../Views/ResourceCpuUsageBarsView.axaml.cs | 11 - GSS2.UI.Core/Views/ResourcesView.axaml | 33 - GSS2.UI.Core/Views/ResourcesView.axaml.cs | 11 - GSS2/Application.axaml | 59 + GSS2/Application.axaml.cs | 10 +- GSS2/Commands/UI.cs | 30 +- GSS2/DependencyInjectionHelper.cs | 14 +- GSS2/Program.cs | 17 +- .../AmineContent/AnalysisViewModel.cs | 2375 ++++++++++++----- .../AmineContent/BrandCalibrationViewModel.cs | 156 +- .../SampleCalibrationViewModel.cs | 443 ++- .../SeparatorCalibrationViewModel.cs | 331 +-- GSS2/ViewModels/AmineContentViewModel.cs | 44 +- GSS2/ViewModels/Common/CameraViewModel.cs | 20 - GSS2/Views/AmineContent/AnalysisView.axaml | 1743 +++++++++--- GSS2/Views/AmineContent/AnalysisView.axaml.cs | 10 +- .../AmineContent/BrandCalibrationView.axaml | 288 +- .../AmineContent/SampleCalibrationView.axaml | 57 +- .../SeparatorCalibrationView.axaml | 71 +- GSS2/Views/AmineContentView.axaml | 62 +- GSS2/Views/Common/CameraView.axaml | 50 - GSS2/Views/Common/CameraView.axaml.cs | 11 - GSS2/Views/NumericKeyboardView.axaml | 0 GSS2/Views/NumericKeyboardView.axaml.cs | 0 GSS2/Views/TextKeyboardView.axaml | 0 GSS2/Views/TextKeyboardView.axaml.cs | 0 GSS2/appsettings.json | 3 +- packages_list.txt | Bin 17078 -> 17116 bytes 74 files changed, 5219 insertions(+), 4248 deletions(-) delete mode 100644 GSS2.Core/Analysis/AmineContent/Database/FeatureRecord.cs rename GSS2.Core/Analysis/AmineContent/Database/Migrations/{20260615113305_Migration0.Designer.cs => 20260624080642_Migration0.Designer.cs} (51%) rename GSS2.Core/Analysis/AmineContent/Database/Migrations/{20260615113305_Migration0.cs => 20260624080642_Migration0.cs} (54%) create mode 100644 GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.Designer.cs create mode 100644 GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.cs delete mode 100644 GSS2.Core/Hardware/ComputeResourcesService.cs delete mode 100644 GSS2.Core/Hardware/TemperatureHumidityService.cs delete mode 100644 GSS2.Core/ImageStorageService.cs create mode 100644 GSS2.Core/Utils/CompressedFloatListConverter.cs rename GSS2.UI.Core/Converters/{GreaterThenConverter.cs => GreaterThanConverter.cs} (100%) rename GSS2.UI.Core/Converters/{GreaterThenOrEqualConverter.cs => GreaterThanOrEqualConverter.cs} (97%) rename GSS2.UI.Core/Converters/{LessThenConverter.cs => LessThanConverter.cs} (97%) rename GSS2.UI.Core/Converters/{LessThenOrEqualConverter.cs => LessThanOrEqualConverter.cs} (97%) delete mode 100644 GSS2.UI.Core/ViewModels/CameraViewModel.cs delete mode 100644 GSS2.UI.Core/ViewModels/IlluminatorControllerViewModel.cs delete mode 100644 GSS2.UI.Core/ViewModels/ResourceBarViewModel.cs delete mode 100644 GSS2.UI.Core/ViewModels/ResourceChartViewModel.cs delete mode 100644 GSS2.UI.Core/ViewModels/ResourceCpuUsageBarsViewModel.cs delete mode 100644 GSS2.UI.Core/ViewModels/ResourcesViewModel.cs delete mode 100644 GSS2.UI.Core/Views/CameraView.axaml delete mode 100644 GSS2.UI.Core/Views/CameraView.axaml.cs delete mode 100644 GSS2.UI.Core/Views/IlluminatorControllerView.axaml delete mode 100644 GSS2.UI.Core/Views/IlluminatorControllerView.axaml.cs delete mode 100644 GSS2.UI.Core/Views/ResourceBarView.axaml delete mode 100644 GSS2.UI.Core/Views/ResourceBarView.axaml.cs delete mode 100644 GSS2.UI.Core/Views/ResourceChartView.axaml delete mode 100644 GSS2.UI.Core/Views/ResourceChartView.axaml.cs delete mode 100644 GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml delete mode 100644 GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml.cs delete mode 100644 GSS2.UI.Core/Views/ResourcesView.axaml delete mode 100644 GSS2.UI.Core/Views/ResourcesView.axaml.cs delete mode 100644 GSS2/ViewModels/Common/CameraViewModel.cs delete mode 100644 GSS2/Views/Common/CameraView.axaml delete mode 100644 GSS2/Views/Common/CameraView.axaml.cs mode change 100755 => 100644 GSS2/Views/NumericKeyboardView.axaml mode change 100755 => 100644 GSS2/Views/NumericKeyboardView.axaml.cs mode change 100755 => 100644 GSS2/Views/TextKeyboardView.axaml mode change 100755 => 100644 GSS2/Views/TextKeyboardView.axaml.cs diff --git a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs index 87d0f62..8b1bf25 100644 --- a/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs +++ b/GSS2.Core/Analysis/AmineContent/AnalyzerService.cs @@ -84,6 +84,8 @@ public partial class AnalyzerService private PredictionEngine? _regressionEngine = null; public bool Initialized { get; private set; } = false; + public int InitializedBrandId { get; private set; } = 0; + public int InitializedSeparatorId { get; private set; } = 0; public AnalyzerService(ILogger logger, Context Context) { @@ -93,6 +95,14 @@ public partial class AnalyzerService public async Task Initialize(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken = default) { + // TODO: Повторная инициализация если изменились калибровочные данные + + if (Initialized && + InitializedBrandId == brand.Id && + InitializedSeparatorId == separator.Id + ) + return; + Initialized = false; _logger.LogInformation("Инициализация"); @@ -116,6 +126,8 @@ public partial class AnalyzerService } Initialized = true; + InitializedBrandId = brand.Id; + InitializedSeparatorId = separator.Id; } private async Task InitializeMlClusterizationEngine(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken) { @@ -126,35 +138,49 @@ public partial class AnalyzerService cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Подготовка данных"); - var sampleRecordIds = _context.SampleRecords - .Where(r => r.BrandId == brand.Id) - .Select(r => r.Id); - _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecordIds.Count()); - if (sampleRecordIds.Count() == 0) + var sampleRecords = _context.SampleRecords + .Where(r => r.BrandId == brand.Id); + + _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count()); + if (sampleRecords.Count() == 0) throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); cancellationToken.ThrowIfCancellationRequested(); - var separatorDataSet = _context.FeatureRecords - .Where(r => r.SeparatorRecordId == separator.Id) - .Select(r => new ClusterizationData + if (separator.Features.Count() == 0) + throw new InvalidDataException($"Не найдено калибровочных признаков для сепаратора: \"{separator.Type}\""); + if (separator.Features.Count() % FEATURES_LENGTH != 0) + throw new InvalidDataException($"Неожидаемое количество калибровочных признаков для сепаратора: \"{separator.Type}\""); + var separatorDataSet = separator.Features.Chunk(FEATURES_LENGTH) + .Select(v => new ClusterizationData { Label = "separator", - Features = r.Values.ToArray() - }); + Features = v.ToArray() + }) + .Shuffle(); + _logger.LogInformation("Загружено векторов для сепаратора: {}", separatorDataSet.Count()); if (separatorDataSet.Count() == 0) throw new InvalidDataException($"Не найдено калибровочных данных для сепаратора: \"{separator.Type}\""); cancellationToken.ThrowIfCancellationRequested(); - var sampleDataSet = _context.FeatureRecords - .Where(r => r.SeparatorRecordId != 0) - .Where(r => sampleRecordIds.Contains(r.SampleRecordId)) - .Select(r => new ClusterizationData + foreach (var sampleRecord in sampleRecords) + { + if (sampleRecord.Features.Count() == 0) + _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0) + _logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + } + + var sampleDataSet = sampleRecords + .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) + .SelectMany(r => r.Features.Chunk(FEATURES_LENGTH)) + .Select(f => new ClusterizationData { Label = "sample", - Features = r.Values.ToArray() + Features = f.ToArray() }) .Shuffle(); + _logger.LogInformation("Загружено векторов для пробы: {}", sampleDataSet.Count()); if (separatorDataSet.Count() == 0) throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); @@ -187,32 +213,40 @@ public partial class AnalyzerService cancellationToken.ThrowIfCancellationRequested(); _logger.LogInformation("Подготовка данных"); - var sampleRecordIds = _context.SampleRecords - .Where(r => r.BrandId == brand.Id) - .Select(r => r.Id); - _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecordIds.Count()); - if (sampleRecordIds.Count() == 0) + var sampleRecords = _context.SampleRecords + .Where(r => r.BrandId == brand.Id); + + _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count()); + if (sampleRecords.Count() == 0) throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); cancellationToken.ThrowIfCancellationRequested(); + foreach (var sampleRecord in sampleRecords) + { + if (sampleRecord.Features.Count() == 0) + _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0) + _logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName); + } + _logger.LogInformation("Подготовка данных"); - var trainDataSet = _context.FeatureRecords - .Where(r => r.SeparatorRecordId != 0) - .Where(r => sampleRecordIds.Contains(r.SampleRecordId)) - .ToList() - .Select(r => - { - var sample = _context.SampleRecords.First(s => s.Id == r.SampleRecordId); - return new RegressionData - { - Value = (float)( - sample.MeasuredContent >= 0 ? sample.MeasuredContent : - sample.MixtureActualRate - ), - Features = r.Values.ToArray() - }; - }) - .Shuffle(); + var trainDataSet = sampleRecords + .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0) + .Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0) + .SelectMany(r => + r.Features + .Chunk(FEATURES_LENGTH) + .Select(f => + new RegressionData + { + Value = (float)(r.MeasuredContent >= 0 ? r.MeasuredContent : r.MixtureActualRate), + Features = f.ToArray() + } + ) + ) + .Shuffle() + .ToList(); + _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); if (trainDataSet.Count() == 0) throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); @@ -232,13 +266,15 @@ public partial class AnalyzerService Vector meanVector = matrix.ColumnSums() / matrix.RowCount; Matrix covarianceMatrix = MatrixCovariance(matrix, meanVector); Matrix invCovarianceMatrix = covarianceMatrix.Inverse(); - trainDataSet = trainDataSet.Where((data, index) => - { - var row = matrix.Row(index); - double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix); + trainDataSet = trainDataSet + .Where((data, index) => + { + var row = matrix.Row(index); + double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix); - return distance <= FILTERING_THRESHOLD; - }).ToList(); + return distance <= FILTERING_THRESHOLD; + }) + .ToList(); _logger.LogInformation("Векторов для обучения после фильтрации: {}", trainDataSet.Count()); var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); cancellationToken.ThrowIfCancellationRequested(); diff --git a/GSS2.Core/Analysis/AmineContent/Database/BrandRecord.cs b/GSS2.Core/Analysis/AmineContent/Database/BrandRecord.cs index 1ca58fe..eea7ed9 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/BrandRecord.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/BrandRecord.cs @@ -11,6 +11,10 @@ public class BrandRecord [Comment("Уникальный идентификатор")] [MaxLength(36)] public string Uuid { get; set; } + [Comment("Дата и время создания")] + public DateTime CreateDateTime { get; set; } + [Comment("Дата и время редактирования")] + public DateTime EditDateTime { get; set; } [Comment("Марка пробы")] [MaxLength(256)] public string BrandName { get; set; } @@ -19,25 +23,41 @@ public class BrandRecord public string MixtureName { get; set; } [Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")] public double MixtureNormalRate { get; set; } + [Comment("Дополнительная информация")] + [MaxLength(1024)] + public string Comment { get; set; } public BrandRecord() - : this(0, null, "", "", 0) + : this(0, null, null, null, "", "", 0, "") { } public BrandRecord( int id = 0, string? uuid = null, + DateTime? createDateTime = null, + DateTime? editDateTime = null, string brandName = "", string mixtureName = "", - double mixtureNormalRate = 0) + double mixtureNormalRate = 0, + string comment = "" + ) { Id = id; if (uuid is null) Uuid = Guid.NewGuid().ToString(); else Uuid = uuid; + if (createDateTime is null) + CreateDateTime = DateTime.UtcNow; + else + CreateDateTime = createDateTime.Value; + if (editDateTime is null) + EditDateTime = DateTime.UtcNow; + else + EditDateTime = editDateTime.Value; BrandName = brandName; MixtureName = mixtureName; MixtureNormalRate = mixtureNormalRate; + Comment = comment; } } diff --git a/GSS2.Core/Analysis/AmineContent/Database/Context.cs b/GSS2.Core/Analysis/AmineContent/Database/Context.cs index 207cb35..10568e5 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/Context.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/Context.cs @@ -2,16 +2,26 @@ using Microsoft.EntityFrameworkCore; namespace GSS2.Core.Analysis.AmineContent.Database; +// TODO: Добавить внешние ключи в таблицы и правило что при удалении ключа ключ должен сбрасываться на 0 public class Context : DbContext { public DbSet BrandRecords => Set(); - public DbSet FeatureRecords => Set(); public DbSet ImageRecords => Set(); - // public DbSet ResultRecords => Set(); + public DbSet ResultRecords => Set(); public DbSet SeparatorRecords => Set(); public DbSet SampleRecords => Set(); public Context(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .Property(e => e.Features) + .HasConversion(new CompressedFloatListConverter()); + modelBuilder.Entity() + .Property(e => e.Features) + .HasConversion(new CompressedFloatListConverter()); + } } diff --git a/GSS2.Core/Analysis/AmineContent/Database/FeatureRecord.cs b/GSS2.Core/Analysis/AmineContent/Database/FeatureRecord.cs deleted file mode 100644 index ebe7485..0000000 --- a/GSS2.Core/Analysis/AmineContent/Database/FeatureRecord.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -using Microsoft.EntityFrameworkCore; - -namespace GSS2.Core.Analysis.AmineContent.Database; - -public class FeatureRecord -{ - [Comment("Идентификатор")] - public int Id { get; set; } - [Comment("Значения вектора")] - public List Values { get; set; } - [Comment("Сепаратор")] - public int SeparatorRecordId { get; set; } - [Comment("Проба")] - public int SampleRecordId { get; set; } - - public FeatureRecord() - : this(0, null, 0, 0) - { } - - public FeatureRecord( - int id = 0, - List? values = null, - int separatorRecordId = 0, - int sampleRecordId = 0 - ) - { - Id = id; - if (values is null) - Values = new List(); - else - Values = values; - SeparatorRecordId = separatorRecordId; - SampleRecordId = sampleRecordId; - } -} diff --git a/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260615113305_Migration0.Designer.cs b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624080642_Migration0.Designer.cs similarity index 51% rename from GSS2.Core/Analysis/AmineContent/Database/Migrations/20260615113305_Migration0.Designer.cs rename to GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624080642_Migration0.Designer.cs index 4ef0c6d..c1d8239 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260615113305_Migration0.Designer.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624080642_Migration0.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace GSS2.Core.Analysis.AmineContent.Database.Migrations { [DbContext(typeof(Context))] - [Migration("20260615113305_Migration0")] + [Migration("20260624080642_Migration0")] partial class Migration0 { /// @@ -33,6 +33,20 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations .HasColumnType("TEXT") .HasComment("Марка пробы"); + b.Property("Comment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + b.Property("MixtureName") .IsRequired() .HasMaxLength(256) @@ -54,31 +68,6 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations b.ToTable("BrandRecords"); }); - modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.FeatureRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasComment("Идентификатор"); - - b.Property("SampleRecordId") - .HasColumnType("INTEGER") - .HasComment("Проба"); - - b.Property("SeparatorRecordId") - .HasColumnType("INTEGER") - .HasComment("Сепаратор"); - - b.PrimitiveCollection("Values") - .IsRequired() - .HasColumnType("TEXT") - .HasComment("Значения вектора"); - - b.HasKey("Id"); - - b.ToTable("FeatureRecords"); - }); - modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b => { b.Property("Id") @@ -96,6 +85,125 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations b.ToTable("ImageRecords"); }); + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Марка пробы"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("ImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Изображение"); + + b.Property("MaskImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Маска"); + + b.Property("MixtureName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название кондиционирующей смеси"); + + b.Property("MixtureNormalRate") + .HasColumnType("REAL") + .HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)"); + + b.Property("ParticlesCount") + .HasColumnType("INTEGER") + .HasComment("Количество частиц"); + + b.Property("ResultImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Изображение результата"); + + b.Property("SampleComment") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация о пробе"); + + b.Property("SampleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название пробы"); + + b.Property("SamplingDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время отбора пробы"); + + b.Property("SamplingPlace") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Место отбора пробы"); + + b.Property("SeparatorType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Тип сепаратора"); + + b.Property("Uuid") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasComment("Уникальный идентификатор"); + + b.PrimitiveCollection("ValueCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения частиц (количество)"); + + b.PrimitiveCollection("ValueData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения частиц (данные)"); + + b.PrimitiveCollection("XCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, X координаты (количество)"); + + b.PrimitiveCollection("XData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, X координаты (данные)"); + + b.PrimitiveCollection("YCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, Y координаты (количество)"); + + b.PrimitiveCollection("YData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, Y координаты (данные)"); + + b.HasKey("Id"); + + b.ToTable("ResultRecords"); + }); + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b => { b.Property("Id") @@ -107,7 +215,15 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations .HasColumnType("INTEGER") .HasComment("Марка"); - b.PrimitiveCollection("FeatureIds") + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.PrimitiveCollection("Features") .IsRequired() .HasColumnType("TEXT") .HasComment("Признаки"); @@ -126,7 +242,7 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations b.Property("SampleComment") .IsRequired() - .HasMaxLength(512) + .HasMaxLength(1024) .HasColumnType("TEXT") .HasComment("Дополнительная информация о пробе"); @@ -164,7 +280,21 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations .HasColumnType("INTEGER") .HasComment("Идентификатор"); - b.PrimitiveCollection("FeatureIds") + b.Property("Comment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.PrimitiveCollection("Features") .IsRequired() .HasColumnType("TEXT") .HasComment("Признаки"); diff --git a/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260615113305_Migration0.cs b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624080642_Migration0.cs similarity index 54% rename from GSS2.Core/Analysis/AmineContent/Database/Migrations/20260615113305_Migration0.cs rename to GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624080642_Migration0.cs index fc5a2fe..e954b34 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260615113305_Migration0.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624080642_Migration0.cs @@ -18,30 +18,18 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations Id = table.Column(type: "INTEGER", nullable: false, comment: "Идентификатор") .Annotation("Sqlite:Autoincrement", true), Uuid = table.Column(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), + CreateDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время создания"), + EditDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время редактирования"), BrandName = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"), MixtureName = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"), - MixtureNormalRate = table.Column(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)") + MixtureNormalRate = table.Column(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"), + Comment = table.Column(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация") }, constraints: table => { table.PrimaryKey("PK_BrandRecords", x => x.Id); }); - migrationBuilder.CreateTable( - name: "FeatureRecords", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false, comment: "Идентификатор") - .Annotation("Sqlite:Autoincrement", true), - Values = table.Column(type: "TEXT", nullable: false, comment: "Значения вектора"), - SeparatorRecordId = table.Column(type: "INTEGER", nullable: false, comment: "Сепаратор"), - SampleRecordId = table.Column(type: "INTEGER", nullable: false, comment: "Проба") - }, - constraints: table => - { - table.PrimaryKey("PK_FeatureRecords", x => x.Id); - }); - migrationBuilder.CreateTable( name: "ImageRecords", columns: table => new @@ -55,6 +43,39 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations table.PrimaryKey("PK_ImageRecords", x => x.Id); }); + migrationBuilder.CreateTable( + name: "ResultRecords", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false, comment: "Идентификатор") + .Annotation("Sqlite:Autoincrement", true), + Uuid = table.Column(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), + CreateDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время создания"), + EditDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время редактирования"), + SampleName = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"), + SampleComment = table.Column(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"), + SamplingDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы"), + SamplingPlace = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"), + SeparatorType = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип сепаратора"), + BrandName = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"), + MixtureName = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"), + MixtureNormalRate = table.Column(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"), + ImageId = table.Column(type: "INTEGER", maxLength: 256, nullable: false, comment: "Изображение"), + MaskImageId = table.Column(type: "INTEGER", maxLength: 256, nullable: false, comment: "Маска"), + ResultImageId = table.Column(type: "INTEGER", maxLength: 256, nullable: false, comment: "Изображение результата"), + ParticlesCount = table.Column(type: "INTEGER", nullable: false, comment: "Количество частиц"), + ValueCount = table.Column(type: "TEXT", nullable: false, comment: "Значения частиц (количество)"), + ValueData = table.Column(type: "TEXT", nullable: false, comment: "Значения частиц (данные)"), + XCount = table.Column(type: "TEXT", nullable: false, comment: "Контура частиц, X координаты (количество)"), + XData = table.Column(type: "TEXT", nullable: false, comment: "Контура частиц, X координаты (данные)"), + YCount = table.Column(type: "TEXT", nullable: false, comment: "Контура частиц, Y координаты (количество)"), + YData = table.Column(type: "TEXT", nullable: false, comment: "Контура частиц, Y координаты (данные)") + }, + constraints: table => + { + table.PrimaryKey("PK_ResultRecords", x => x.Id); + }); + migrationBuilder.CreateTable( name: "SampleRecords", columns: table => new @@ -62,15 +83,17 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations Id = table.Column(type: "INTEGER", nullable: false, comment: "Идентификатор") .Annotation("Sqlite:Autoincrement", true), Uuid = table.Column(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), + CreateDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время создания"), + EditDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время редактирования"), SampleName = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"), - SampleComment = table.Column(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"), + SampleComment = table.Column(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация о пробе"), SamplingDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы (utc)"), BrandId = table.Column(type: "INTEGER", nullable: false, comment: "Марка"), SamplingPlace = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"), MixtureActualRate = table.Column(type: "REAL", nullable: false, comment: "Фактический расход кондиционирующей смеси (кг/т | гр/кг)"), MeasuredContent = table.Column(type: "REAL", nullable: false, comment: "Измеренное количество кондиционирующей смеси (кг/т | гр/кг)"), ImageId = table.Column(type: "INTEGER", nullable: false, comment: "Изображение"), - FeatureIds = table.Column(type: "TEXT", nullable: false, comment: "Признаки") + Features = table.Column(type: "TEXT", nullable: false, comment: "Признаки") }, constraints: table => { @@ -84,9 +107,12 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations Id = table.Column(type: "INTEGER", nullable: false, comment: "Идентификатор") .Annotation("Sqlite:Autoincrement", true), Uuid = table.Column(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), + CreateDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время создания"), + EditDateTime = table.Column(type: "TEXT", nullable: false, comment: "Дата и время редактирования"), Type = table.Column(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип"), + Comment = table.Column(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация"), ImageId = table.Column(type: "INTEGER", nullable: false, comment: "Изображение"), - FeatureIds = table.Column(type: "TEXT", nullable: false, comment: "Признаки") + Features = table.Column(type: "TEXT", nullable: false, comment: "Признаки") }, constraints: table => { @@ -101,10 +127,10 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations name: "BrandRecords"); migrationBuilder.DropTable( - name: "FeatureRecords"); + name: "ImageRecords"); migrationBuilder.DropTable( - name: "ImageRecords"); + name: "ResultRecords"); migrationBuilder.DropTable( name: "SampleRecords"); diff --git a/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.Designer.cs b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.Designer.cs new file mode 100644 index 0000000..e8e8bbb --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.Designer.cs @@ -0,0 +1,325 @@ +// +using System; +using GSS2.Core.Analysis.AmineContent.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GSS2.Core.Analysis.AmineContent.Database.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20260624154152_Migration1")] + partial class Migration1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); + + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.BrandRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Марка пробы"); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("MixtureName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название кондиционирующей смеси"); + + b.Property("MixtureNormalRate") + .HasColumnType("REAL") + .HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)"); + + b.Property("Uuid") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasComment("Уникальный идентификатор"); + + b.HasKey("Id"); + + b.ToTable("BrandRecords"); + }); + + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("Base64") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения вектора"); + + b.HasKey("Id"); + + b.ToTable("ImageRecords"); + }); + + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Марка пробы"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("ImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Изображение"); + + b.Property("MaskImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Маска"); + + b.Property("MixtureName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название кондиционирующей смеси"); + + b.Property("MixtureNormalRate") + .HasColumnType("REAL") + .HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)"); + + b.Property("ParticlesCount") + .HasColumnType("INTEGER") + .HasComment("Количество частиц"); + + b.Property("ResultImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Изображение результата"); + + b.Property("SampleComment") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация о пробе"); + + b.Property("SampleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название пробы"); + + b.Property("SamplingDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время отбора пробы"); + + b.Property("SamplingPlace") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Место отбора пробы"); + + b.Property("SeparatorType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Тип сепаратора"); + + b.Property("Uuid") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasComment("Уникальный идентификатор"); + + b.PrimitiveCollection("ValueCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения частиц (количество)"); + + b.PrimitiveCollection("ValueData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения частиц (данные)"); + + b.PrimitiveCollection("XCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, X координаты (количество)"); + + b.PrimitiveCollection("XData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, X координаты (данные)"); + + b.PrimitiveCollection("YCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, Y координаты (количество)"); + + b.PrimitiveCollection("YData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, Y координаты (данные)"); + + b.HasKey("Id"); + + b.ToTable("ResultRecords"); + }); + + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("BrandId") + .HasColumnType("INTEGER") + .HasComment("Марка"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("Features") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Признаки"); + + b.Property("ImageId") + .HasColumnType("INTEGER") + .HasComment("Изображение"); + + b.Property("MeasuredContent") + .HasColumnType("REAL") + .HasComment("Измеренное количество кондиционирующей смеси (кг/т | гр/кг)"); + + b.Property("MixtureActualRate") + .HasColumnType("REAL") + .HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)"); + + b.Property("SampleComment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация о пробе"); + + b.Property("SampleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название пробы"); + + b.Property("SamplingDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время отбора пробы (utc)"); + + b.Property("SamplingPlace") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Место отбора пробы"); + + b.Property("Uuid") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasComment("Уникальный идентификатор"); + + b.HasKey("Id"); + + b.ToTable("SampleRecords"); + }); + + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SeparatorRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("Comment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("Features") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Признаки"); + + b.Property("ImageId") + .HasColumnType("INTEGER") + .HasComment("Изображение"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Тип"); + + b.Property("Uuid") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasComment("Уникальный идентификатор"); + + b.HasKey("Id"); + + b.ToTable("SeparatorRecords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.cs b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.cs new file mode 100644 index 0000000..3d549b8 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/Database/Migrations/20260624154152_Migration1.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GSS2.Core.Analysis.AmineContent.Database.Migrations +{ + /// + public partial class Migration1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/GSS2.Core/Analysis/AmineContent/Database/Migrations/ContextModelSnapshot.cs b/GSS2.Core/Analysis/AmineContent/Database/Migrations/ContextModelSnapshot.cs index 6b57d15..85e5653 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/Migrations/ContextModelSnapshot.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/Migrations/ContextModelSnapshot.cs @@ -30,6 +30,20 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations .HasColumnType("TEXT") .HasComment("Марка пробы"); + b.Property("Comment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + b.Property("MixtureName") .IsRequired() .HasMaxLength(256) @@ -51,31 +65,6 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations b.ToTable("BrandRecords"); }); - modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.FeatureRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasComment("Идентификатор"); - - b.Property("SampleRecordId") - .HasColumnType("INTEGER") - .HasComment("Проба"); - - b.Property("SeparatorRecordId") - .HasColumnType("INTEGER") - .HasComment("Сепаратор"); - - b.PrimitiveCollection("Values") - .IsRequired() - .HasColumnType("TEXT") - .HasComment("Значения вектора"); - - b.HasKey("Id"); - - b.ToTable("FeatureRecords"); - }); - modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b => { b.Property("Id") @@ -93,6 +82,125 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations b.ToTable("ImageRecords"); }); + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasComment("Идентификатор"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Марка пробы"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("ImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Изображение"); + + b.Property("MaskImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Маска"); + + b.Property("MixtureName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название кондиционирующей смеси"); + + b.Property("MixtureNormalRate") + .HasColumnType("REAL") + .HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)"); + + b.Property("ParticlesCount") + .HasColumnType("INTEGER") + .HasComment("Количество частиц"); + + b.Property("ResultImageId") + .HasMaxLength(256) + .HasColumnType("INTEGER") + .HasComment("Изображение результата"); + + b.Property("SampleComment") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация о пробе"); + + b.Property("SampleName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Название пробы"); + + b.Property("SamplingDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время отбора пробы"); + + b.Property("SamplingPlace") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Место отбора пробы"); + + b.Property("SeparatorType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT") + .HasComment("Тип сепаратора"); + + b.Property("Uuid") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("TEXT") + .HasComment("Уникальный идентификатор"); + + b.PrimitiveCollection("ValueCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения частиц (количество)"); + + b.PrimitiveCollection("ValueData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Значения частиц (данные)"); + + b.PrimitiveCollection("XCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, X координаты (количество)"); + + b.PrimitiveCollection("XData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, X координаты (данные)"); + + b.PrimitiveCollection("YCount") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, Y координаты (количество)"); + + b.PrimitiveCollection("YData") + .IsRequired() + .HasColumnType("TEXT") + .HasComment("Контура частиц, Y координаты (данные)"); + + b.HasKey("Id"); + + b.ToTable("ResultRecords"); + }); + modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b => { b.Property("Id") @@ -104,7 +212,15 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations .HasColumnType("INTEGER") .HasComment("Марка"); - b.PrimitiveCollection("FeatureIds") + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("Features") .IsRequired() .HasColumnType("TEXT") .HasComment("Признаки"); @@ -123,7 +239,7 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations b.Property("SampleComment") .IsRequired() - .HasMaxLength(512) + .HasMaxLength(1024) .HasColumnType("TEXT") .HasComment("Дополнительная информация о пробе"); @@ -161,7 +277,21 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations .HasColumnType("INTEGER") .HasComment("Идентификатор"); - b.PrimitiveCollection("FeatureIds") + b.Property("Comment") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasComment("Дополнительная информация"); + + b.Property("CreateDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время создания"); + + b.Property("EditDateTime") + .HasColumnType("TEXT") + .HasComment("Дата и время редактирования"); + + b.Property("Features") .IsRequired() .HasColumnType("TEXT") .HasComment("Признаки"); diff --git a/GSS2.Core/Analysis/AmineContent/Database/ResultRecord.cs b/GSS2.Core/Analysis/AmineContent/Database/ResultRecord.cs index 1999997..e6e5572 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/ResultRecord.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/ResultRecord.cs @@ -1,60 +1,149 @@ -// using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; -// using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; -// namespace GSS2.Core.Analysis.AmineContent.Database; +namespace GSS2.Core.Analysis.AmineContent.Database; -// public class ResultRecord -// { -// [Comment("Идентификатор")] -// public int Id { get; set; } -// [Comment("Уникальный идентификатор")] -// [MaxLength(36)] -// public required string Uuid { get; set; } -// [Comment("Название пробы")] -// [MaxLength(256)] -// public required string SampleName { get; set; } -// [Comment("Дополнительная информация о пробе")] -// [MaxLength(512)] -// public required string SampleComment { get; set; } -// [Comment("Дата и время отбора пробы")] -// public required DateTime SamplingDateTime { get; set; } -// [Comment("Место отбора пробы")] -// [MaxLength(256)] -// public required string SamplingPlace { get; set; } -// [Comment("Изображение")] -// [MaxLength(256)] -// public required string Image { get; set; } -// [Comment("Тип сепаратора")] -// [MaxLength(256)] -// public required string SeparatorType { get; set; } -// [Comment("Марка пробы")] -// [MaxLength(256)] -// public required string SampleBrand { get; set; } -// [Comment("Маска")] -// [MaxLength(256)] -// public required string MaskImage { get; set; } -// [Comment("Изображение результата")] -// [MaxLength(256)] -// public required string ResultImage { get; set; } -// [Comment("Минимальное значение")] -// public required double MinValue { get; set; } -// [Comment("Максимальное значение")] -// public required double MaxValue { get; set; } -// [Comment("Количество частиц")] -// public required int ParticlesCount { get; set; } -// [Comment("Среднее значение")] -// public required double MeanValue { get; set; } -// [Comment("Средняя дисперсия")] -// public required double MeanVariance { get; set; } -// [Comment("Дисперсия среднего")] -// public required double VarianceOfMean { get; set; } -// [Comment("Среднее качество обработки")] -// public required double? MeanQuality { get; set; } -// [Comment("Средние значения")] -// public required double[] MeanValues { get; set; } -// [Comment("Дисперсии")] -// public required double[] Variances { get; set; } -// [Comment("Качества обработки")] -// public required double[]? Qualities { get; set; } -// } +public class ResultRecord +{ + [Comment("Идентификатор")] + public int Id { get; set; } + [Comment("Уникальный идентификатор")] + [MaxLength(36)] + public string Uuid { get; set; } + [Comment("Дата и время создания")] + public DateTime CreateDateTime { get; set; } + [Comment("Дата и время редактирования")] + public DateTime EditDateTime { get; set; } + [Comment("Название пробы")] + [MaxLength(256)] + public string SampleName { get; set; } + [Comment("Дополнительная информация о пробе")] + [MaxLength(512)] + public string SampleComment { get; set; } + [Comment("Дата и время отбора пробы")] + public DateTime SamplingDateTime { get; set; } + [Comment("Место отбора пробы")] + [MaxLength(256)] + public string SamplingPlace { get; set; } + [Comment("Тип сепаратора")] + [MaxLength(256)] + public string SeparatorType { get; set; } + [Comment("Марка пробы")] + [MaxLength(256)] + public string BrandName { get; set; } + [Comment("Название кондиционирующей смеси")] + [MaxLength(256)] + public string MixtureName { get; set; } + [Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")] + public double MixtureNormalRate { get; set; } + [Comment("Изображение")] + [MaxLength(256)] + public int ImageId { get; set; } + [Comment("Маска")] + [MaxLength(256)] + public int MaskImageId { get; set; } + [Comment("Изображение результата")] + [MaxLength(256)] + public int ResultImageId { get; set; } + [Comment("Количество частиц")] + public int ParticlesCount { get; set; } + [Comment("Значения частиц (количество)")] + public List ValueCount { get; set; } + [Comment("Значения частиц (данные)")] + public List ValueData { get; set; } + [Comment("Контура частиц, X координаты (количество)")] + public List XCount { get; set; } + [Comment("Контура частиц, X координаты (данные)")] + public List XData { get; set; } + [Comment("Контура частиц, Y координаты (количество)")] + public List YCount { get; set; } + [Comment("Контура частиц, Y координаты (данные)")] + public List YData { get; set; } + + public ResultRecord() + : this(0, null, null, null, "", "", null, "", "", "", "", -1, 0, 0, 0, -1, null, null, null) + { } + public ResultRecord( + int id = 0, + string? uuid = null, + DateTime? createDateTime = null, + DateTime? editDateTime = null, + string sampleName = "", + string sampleComment = "", + DateTime? samplingDateTime = null, + string samplingPlace = "", + string separatorType = "", + string brandName = "", + string mixtureName = "", + double mixtureNormalRate = -1, + int imageId = 0, + int maskImageId = 0, + int resultImageId = 0, + int particlesCount = -1, + List? values = null, + List? xs = null, + List? ys = null + ) + { + Id = id; + if (uuid is null) + Uuid = Guid.NewGuid().ToString(); + else + Uuid = uuid; + if (createDateTime is null) + CreateDateTime = DateTime.UtcNow; + else + CreateDateTime = createDateTime.Value; + if (editDateTime is null) + EditDateTime = DateTime.UtcNow; + else + EditDateTime = editDateTime.Value; + SampleName = sampleName; + SampleComment = sampleComment; + if (samplingDateTime is null) + SamplingDateTime = DateTime.UtcNow; + else + SamplingDateTime = samplingDateTime.Value; + SamplingPlace = samplingPlace; + SeparatorType = separatorType; + BrandName = brandName; + MixtureName = mixtureName; + MixtureNormalRate = mixtureNormalRate; + ImageId = imageId; + MaskImageId = maskImageId; + ResultImageId = resultImageId; + ParticlesCount = particlesCount; + if (values is null) + { + ValueCount = new List(); + ValueData = new List(); + } + else + { + ValueCount = values.Select(fs => fs.Count()).ToList(); + ValueData = values.SelectMany(fs => fs).ToList(); + } + if (xs is null) + { + XCount = new List(); + XData = new List(); + } + else + { + XCount = xs.Select(fs => fs.Count()).ToList(); + XData = xs.SelectMany(fs => fs).ToList(); + } + if (ys is null) + { + YCount = new List(); + YData = new List(); + } + else + { + YCount = ys.Select(fs => fs.Count()).ToList(); + YData = ys.SelectMany(fs => fs).ToList(); + } + + } +} diff --git a/GSS2.Core/Analysis/AmineContent/Database/SampleRecord.cs b/GSS2.Core/Analysis/AmineContent/Database/SampleRecord.cs index e85ac64..187cb64 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/SampleRecord.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/SampleRecord.cs @@ -11,11 +11,15 @@ public class SampleRecord [Comment("Уникальный идентификатор")] [MaxLength(36)] public string Uuid { get; set; } + [Comment("Дата и время создания")] + public DateTime CreateDateTime { get; set; } + [Comment("Дата и время редактирования")] + public DateTime EditDateTime { get; set; } [Comment("Название пробы")] [MaxLength(256)] public string SampleName { get; set; } [Comment("Дополнительная информация о пробе")] - [MaxLength(512)] + [MaxLength(1024)] public string SampleComment { get; set; } [Comment("Дата и время отбора пробы (utc)")] public DateTime SamplingDateTime { get; set; } @@ -31,14 +35,16 @@ public class SampleRecord [Comment("Изображение")] public int ImageId { get; set; } [Comment("Признаки")] - public List FeatureIds { get; set; } + public List Features { get; set; } public SampleRecord() - : this(0, null, "", "", null, 0, "", -1, -1, 0, null) + : this(0, null, null, null, "", "", null, 0, "", -1, -1, 0, null) { } public SampleRecord( int id = 0, string? uuid = null, + DateTime? createDateTime = null, + DateTime? editDateTime = null, string sampleName = "", string sampleComment = "", DateTime? samplingDateTime = null, @@ -47,7 +53,7 @@ public class SampleRecord double mixtureActualRate = -1, double measuredContent = -1, int imageId = 0, - List? featureIds = null + List? features = null ) { Id = id; @@ -55,6 +61,14 @@ public class SampleRecord Uuid = Guid.NewGuid().ToString(); else Uuid = uuid; + if (createDateTime is null) + CreateDateTime = DateTime.UtcNow; + else + CreateDateTime = createDateTime.Value; + if (editDateTime is null) + EditDateTime = DateTime.UtcNow; + else + EditDateTime = editDateTime.Value; SampleName = sampleName; SampleComment = sampleComment; if (samplingDateTime is null) @@ -66,9 +80,9 @@ public class SampleRecord MixtureActualRate = mixtureActualRate; MeasuredContent = measuredContent; ImageId = imageId; - if (featureIds is null) - FeatureIds = new List(); + if (features is null) + Features = new List(); else - FeatureIds = featureIds; + Features = features.SelectMany(fs => fs).ToList(); } } diff --git a/GSS2.Core/Analysis/AmineContent/Database/SeparatorRecord.cs b/GSS2.Core/Analysis/AmineContent/Database/SeparatorRecord.cs index ede805c..e347347 100644 --- a/GSS2.Core/Analysis/AmineContent/Database/SeparatorRecord.cs +++ b/GSS2.Core/Analysis/AmineContent/Database/SeparatorRecord.cs @@ -11,24 +11,34 @@ public class SeparatorRecord [Comment("Уникальный идентификатор")] [MaxLength(36)] public string Uuid { get; set; } + [Comment("Дата и время создания")] + public DateTime CreateDateTime { get; set; } + [Comment("Дата и время редактирования")] + public DateTime EditDateTime { get; set; } [Comment("Тип")] [MaxLength(256)] public string Type { get; set; } + [Comment("Дополнительная информация")] + [MaxLength(1024)] + public string Comment { get; set; } [Comment("Изображение")] public int ImageId { get; set; } [Comment("Признаки")] - public List FeatureIds { get; set; } + public List Features { get; set; } public SeparatorRecord() - : this(0, "", "", 0, null) + : this(0, null, null, null, "", "", 0, null) { } public SeparatorRecord( int id = 0, string? uuid = null, + DateTime? createDateTime = null, + DateTime? editDateTime = null, string type = "", + string comment = "", int imageId = 0, - List? featureIds = null + List? features = null ) { Id = id; @@ -36,11 +46,20 @@ public class SeparatorRecord Uuid = Guid.NewGuid().ToString(); else Uuid = uuid; - Type = type; - ImageId = imageId; - if (featureIds is null) - FeatureIds = new List(); + if (createDateTime is null) + CreateDateTime = DateTime.UtcNow; else - FeatureIds = featureIds; + CreateDateTime = createDateTime.Value; + if (editDateTime is null) + EditDateTime = DateTime.UtcNow; + else + EditDateTime = editDateTime.Value; + Type = type; + Comment = comment; + ImageId = imageId; + if (features is null) + Features = new List(); + else + Features = features.SelectMany(fs => fs).ToList(); } } diff --git a/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs b/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs index 6a97a78..4aa8cc9 100644 --- a/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs +++ b/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs @@ -59,8 +59,6 @@ public class ImageCapturerService : IImageCapturerService // Если ViewFinder запущен, то для съёмки изображений его нужно остановить // потом после съёмки снова запустить var needRestartViewFinder = _camera.ViewFinderStarted; - if (needRestartViewFinder) - _camera.StopViewFinder(); try { @@ -117,9 +115,6 @@ public class ImageCapturerService : IImageCapturerService { // В любом случае отключаем осветитель _illuminator.TurnOff(); - // Запускаем обратно ViewFinder, если он был запущен и ещё не запущен обратно - if (needRestartViewFinder && !_camera.ViewFinderStarted) - _camera.StartViewFinder(); } } } diff --git a/GSS2.Core/DependencyInjectionHelper.cs b/GSS2.Core/DependencyInjectionHelper.cs index 12c9a90..4a3fc1d 100644 --- a/GSS2.Core/DependencyInjectionHelper.cs +++ b/GSS2.Core/DependencyInjectionHelper.cs @@ -28,22 +28,22 @@ public static class DependencyInjectionHelper logging.AddProvider(new FileLoggerProvider("last_run.log", false)); return logging; } - public static IServiceCollection AddTemperatureHumidityService(this IServiceCollection services, string section) => services - .AddSingleton( - serviceProvider => - { - var configuration = serviceProvider.GetService(); - if (configuration is null) - throw new Exception("Cannot get configuration"); - var serviceConfiguration = configuration.GetSection(section).Get(); - if (serviceConfiguration is null) - throw new InvalidConfigurationException($"Cannot get configuration {section}"); - var logger = serviceProvider.GetService>(); - if (logger is null) - throw new Exception("Cannot get logger"); - return new TemperatureHumidityService(logger, serviceConfiguration); - } - ); + // public static IServiceCollection AddTemperatureHumidityService(this IServiceCollection services, string section) => services + // .AddSingleton( + // serviceProvider => + // { + // var configuration = serviceProvider.GetService(); + // if (configuration is null) + // throw new Exception("Cannot get configuration"); + // var serviceConfiguration = configuration.GetSection(section).Get(); + // if (serviceConfiguration is null) + // throw new InvalidConfigurationException($"Cannot get configuration {section}"); + // var logger = serviceProvider.GetService>(); + // if (logger is null) + // throw new Exception("Cannot get logger"); + // return new TemperatureHumidityService(logger, serviceConfiguration); + // } + // ); public static IServiceCollection AddLightsService(this IServiceCollection services, string section) => services .AddSingleton( serviceProvider => @@ -92,21 +92,9 @@ public static class DependencyInjectionHelper return new CameraService(logger, serviceConfiguration, autoInitialize); } ); - public static IServiceCollection AddComputeResourcesService(this IServiceCollection services) => services - .AddSingleton(); [Obsolete] public static IServiceCollection AddUsbService(this IServiceCollection services) => services .AddSingleton(); - public static IServiceCollection AddImageStorageService(this IServiceCollection services, DirectoryInfo rootDirectory) => services - .AddSingleton( - serviceProvider => - { - var logger = serviceProvider.GetService>(); - if (logger is null) - throw new Exception("Cannot get logger"); - return new ImageStorageService(logger, rootDirectory); - } - ); public static IServiceCollection AddAmineContentContext(this IServiceCollection services, IConfigurationManager configuration) => services .AddDbContext( @@ -121,9 +109,9 @@ public static class DependencyInjectionHelper public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services, bool mock = false) { if (!mock) - return services.AddSingleton(); + return services.AddSingleton(); - return services.AddSingleton(serviceProvider => + return services.AddSingleton(serviceProvider => { var logger = serviceProvider.GetService>(); if (logger is null) diff --git a/GSS2.Core/Extensions/ConfiurationSectionExtensions.cs b/GSS2.Core/Extensions/ConfiurationSectionExtensions.cs index cd58dee..cd1a29c 100644 --- a/GSS2.Core/Extensions/ConfiurationSectionExtensions.cs +++ b/GSS2.Core/Extensions/ConfiurationSectionExtensions.cs @@ -13,4 +13,3 @@ public static class ConfigurationSectionExtensions return dict; } } - diff --git a/GSS2.Core/Extensions/EnumerableExtensions.cs b/GSS2.Core/Extensions/EnumerableExtensions.cs index ca0ff76..f9bec1c 100644 --- a/GSS2.Core/Extensions/EnumerableExtensions.cs +++ b/GSS2.Core/Extensions/EnumerableExtensions.cs @@ -46,4 +46,32 @@ public static class EnumerableExtensions variance += Math.Pow(value - mean, 2.0); return variance / values.Count(); } + public static double StdDev(this IEnumerable values) => Math.Sqrt(Variance(values)); + public static float Variance(this IEnumerable values) + { + if (values.Count() == 1) + return 0; + + float mean = values.Average(); + float variance = 0.0f; + foreach (int value in values) + variance += MathF.Pow(value - mean, 2.0f); + return variance / values.Count(); + } + public static float StdDev(this IEnumerable values) => MathF.Sqrt(Variance(values)); + + public static IEnumerable> Chunk(this IEnumerable values, IEnumerable chunks) + { + if (chunks.Count() == 0) + throw new InvalidDataException(); + if (values.Count() != chunks.Sum()) + throw new InvalidDataException(); + + var start = 0; + foreach (var chunk in chunks) + { + yield return values.Skip(start).Take(chunk); + start += chunk; + } + } } diff --git a/GSS2.Core/Hardware/CameraService.cs b/GSS2.Core/Hardware/CameraService.cs index a2ceb5a..edcd31f 100644 --- a/GSS2.Core/Hardware/CameraService.cs +++ b/GSS2.Core/Hardware/CameraService.cs @@ -41,7 +41,6 @@ public class CameraService : IDisposable CameraManagerStarted = 1 << 1, CameraAcquired = 1 << 2, CameraStarted = 1 << 3, - ViewFinderFrameBufferAllocated = 1 << 4, ImageCaptureFrameBufferAllocated = 1 << 5, ImageCaptureFrameBuffersMapped = 1 << 6 } @@ -51,10 +50,6 @@ public class CameraService : IDisposable public static CameraServiceConfiguration Default => new CameraServiceConfiguration { CameraId = "", - ViewFinderFrameBufferCount = 5, - ViewFinderWidth = 4056, - ViewFinderHeight = 3080, - ViewFinderFps = 30, ImageCaptureFrameBufferCount = 2, ImageCaptureWidth = 4056, ImageCaptureHeight = 3080, @@ -66,10 +61,6 @@ public class CameraService : IDisposable }; public string CameraId { get; init; } = ""; - public int ViewFinderFrameBufferCount { get; init; } - public int ViewFinderWidth { get; init; } - public int ViewFinderHeight { get; init; } - public int ViewFinderFps { get; init; } public int ImageCaptureFrameBufferCount { get; init; } public int ImageCaptureWidth { get; init; } public int ImageCaptureHeight { get; init; } @@ -87,16 +78,11 @@ public class CameraService : IDisposable private CameraManager? _cameraManager; private Camera? _camera; private CameraConfiguration? _cameraConfiguration; - private StreamConfiguration? _viewFinderStreamConfiguration; private StreamConfiguration? _imageCaptureStreamConfiguration; - private FrameBufferAllocator? _viewFinderFrameBufferAllocator; private FrameBufferAllocator? _imageCaptureFrameBufferAllocator; - private Stream? _viewFinderStream; private Stream? _imageCaptureStream; private List? _imageCaptureMappedBuffers; - private System.Timers.Timer? _viewFinderTimer = null; private System.Timers.ElapsedEventHandler? _viewFinderTimerElapsed = null; - private EventHandler? _viewFinderRequestCompletedHandler = null; private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); private readonly Dictionary _cameraHardSettings = new Dictionary(); @@ -176,10 +162,8 @@ public class CameraService : IDisposable CameraSelect(effectiveCancellationToken); CameraAcquire(effectiveCancellationToken); CameraConfigure(effectiveCancellationToken); - ViewFinderFrameBuffersAllocate(effectiveCancellationToken); ImageCaptureFrameBuffersAllocate(effectiveCancellationToken); CameraStart(effectiveCancellationToken); - ViewFinderFrameBuffersInitialize(effectiveCancellationToken); ImageCaptureFrameBuffersInitialize(effectiveCancellationToken); ImageCaptureFrameBuffersMap(effectiveCancellationToken); } @@ -251,16 +235,8 @@ public class CameraService : IDisposable cancellationToken.ThrowIfCancellationRequested(); - _cameraConfiguration = _camera.GenerateConfiguration( - StreamRoleEnum.Viewfinder, - StreamRoleEnum.Raw); + _cameraConfiguration = _camera.GenerateConfiguration(StreamRoleEnum.Raw); - _viewFinderStreamConfiguration = _cameraConfiguration.First(); - _viewFinderStreamConfiguration.BufferCount = checked((uint)_configuration.ViewFinderFrameBufferCount); - _viewFinderStreamConfiguration.Size = new Size( - checked((uint)_configuration.ViewFinderWidth), - checked((uint)_configuration.ViewFinderHeight) - ); _imageCaptureStreamConfiguration = _cameraConfiguration.Skip(1).First(); _imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount); @@ -275,13 +251,6 @@ public class CameraService : IDisposable throw new Exception("Error while camera configuration validation"); } - _logger.LogDebug(@$"ViewFinder stream configuration: - Size: {_viewFinderStreamConfiguration.Size.ToString(true)} - FrameSize: {_viewFinderStreamConfiguration.FrameSize} - Stride: {_viewFinderStreamConfiguration.Stride} - ColorSpace: {_viewFinderStreamConfiguration.ColorSpace?.ToString(true)} - PixelFormat: {_viewFinderStreamConfiguration.PixelFormat?.ToString(true)}"); - _logger.LogDebug(@$"ImageCapture stream configuration: Size: {_imageCaptureStreamConfiguration.Size.ToString(true)} FrameSize: {_imageCaptureStreamConfiguration.FrameSize} @@ -298,7 +267,6 @@ public class CameraService : IDisposable throw new Exception($"Error while camera configuration {result}"); } - _viewFinderStream = _viewFinderStreamConfiguration.Stream(); _imageCaptureStream = _imageCaptureStreamConfiguration.Stream(); } private void CameraStart(CancellationToken cancellationToken) @@ -320,28 +288,6 @@ public class CameraService : IDisposable _state |= State.CameraStarted; } - private void ViewFinderFrameBuffersAllocate(CancellationToken cancellationToken) - { - if (_camera is null) - throw new InvalidOperationException("Camera not initialized"); - if (_viewFinderStream is null) - throw new InvalidOperationException("ViewFinderStream not initialized"); - - cancellationToken.ThrowIfCancellationRequested(); - - _viewFinderFrameBufferAllocator = new FrameBufferAllocator(_camera); - - cancellationToken.ThrowIfCancellationRequested(); - - int result = _viewFinderFrameBufferAllocator.Allocate(_viewFinderStream); - if (result < 0) - { - _logger.LogCritical("Error while view finder frame buffers allocation {Result}", result); - throw new Exception($"Error while view finder frame buffers allocation {result}"); - } - - _state |= State.ViewFinderFrameBufferAllocated; - } private void ImageCaptureFrameBuffersAllocate(CancellationToken cancellationToken) { if (_camera is null) @@ -364,55 +310,6 @@ public class CameraService : IDisposable _state |= State.ImageCaptureFrameBufferAllocated; } - private void ViewFinderFrameBuffersInitialize(CancellationToken cancellationToken) - { - if (_camera is null) - throw new InvalidOperationException("Camera not initialized"); - if (_viewFinderStream is null) - throw new InvalidOperationException("ViewFinderStream not initialized"); - if (_viewFinderFrameBufferAllocator is null) - throw new InvalidOperationException("ViewFinderFrameBufferAllocator not initialized"); - - cancellationToken.ThrowIfCancellationRequested(); - - var buffers = _viewFinderFrameBufferAllocator.Buffers(_viewFinderStream); - - _logger.LogDebug("View finder frame buffers initialization"); - foreach (var buffer in buffers) - { - cancellationToken.ThrowIfCancellationRequested(); - - var request = _camera.CreateRequest(0); - var result = request.AddBuffer(_viewFinderStream, buffer); - if (result < 0) - throw new Exception("Error while request create"); - - var cts = new TaskCompletionSource(); - void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e) - { - if (e.Request.Handle == request.Handle) - cts.SetResult(); - } - - _camera.RequestCompleted += RequestCompleted; - result = _camera.QueueRequest(request); - if (result < 0) - { - _camera.RequestCompleted -= RequestCompleted; - throw new Exception("Error while request queue"); - } - cts.Task.WaitAsync(cancellationToken).GetAwaiter().GetResult(); - _camera.RequestCompleted -= RequestCompleted; - - _logger.LogDebug("Request completed: \n\tResult: {} \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers); - _logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp); - _logger.LogDebug("Request buffer planes: "); - foreach (var (i, plane) in buffer.Metadata.Planes.Enumerate()) - _logger.LogDebug("Plane[{}]: bytes used: 0x{:X}", i, plane.BytesUsed); - foreach (var (i, plane) in buffer.Planes.Enumerate()) - _logger.LogDebug("Plane[{}]: \n\tFd: 0x{:X} \n\tOffset: 0x{:X} \n\tLength: 0x{:X} \n\tKInvalidOffset: 0x{:X}", i, plane.Fd.Get(), plane.Offset, plane.Length, plane.KInvalidOffset); - } - } private void ImageCaptureFrameBuffersInitialize(CancellationToken cancellationToken) { if (_camera is null) @@ -826,135 +723,6 @@ public class CameraService : IDisposable return image; } - public void StartViewFinder() - { - if (_camera is null) - throw new InvalidOperationException("Camera not initialized"); - if (_viewFinderStream is null) - throw new InvalidOperationException("ViewFinderStream not initialized"); - if (_viewFinderStreamConfiguration is null) - throw new InvalidOperationException("ViewFinderStreamConfiguration not initialized"); - if (_viewFinderFrameBufferAllocator is null) - throw new InvalidOperationException("ViewFinderFrameBufferAllocator not initialized"); - if (_viewFinderTimer is not null || - _viewFinderTimerElapsed is not null || - _viewFinderRequestCompletedHandler is not null) - throw new InvalidOperationException(); - - ViewFinderStarted = true; - - _viewFinderTimer = new System.Timers.Timer() - { - Interval = 1000 / _configuration.ViewFinderFps, - AutoReset = true, - }; - - try - { - var buffers = _viewFinderFrameBufferAllocator.Buffers(_viewFinderStream); - lock (ViewFinderBufferQueue) - foreach (var buffer in buffers) - ViewFinderBufferQueue.Add(buffer); - - void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs ea) - { - if (ea.Request.Cookie != 2) - return; - var buffer = ea.Request.FindBuffer(_viewFinderStream); - // _logger.LogInformation("Completed request for frame buffer {}", buffer); - lock (ViewFinderBufferQueue) - ViewFinderBufferQueue.Add(buffer); - } - _viewFinderRequestCompletedHandler = RequestCompleted; - _camera.RequestCompleted += _viewFinderRequestCompletedHandler; - - void TimerElapsed(object? sender, System.Timers.ElapsedEventArgs ea) - { - if (IsImageCapturing) - return; - - try - { - FrameBuffer buffer; - lock (ViewFinderBufferQueue) - { - if (ViewFinderBufferQueue.Count() > 0 && ( - ViewFinderBufferQueue.First().Request.Cookie != 2 || - ViewFinderBufferQueue.First().Request.Status == Request.StatusEnum.RequestComplete || - ViewFinderBufferQueue.First().Request.Status == Request.StatusEnum.RequestCancelled)) - { - buffer = ViewFinderBufferQueue.First(); - ViewFinderBufferQueue.Remove(buffer); - } - else - return; - } - - var request = _camera.CreateRequest(2); - request.AddBuffer(_viewFinderStream, buffer); - _camera.QueueRequest(request); - // _logger.LogInformation("Queued request for frame buffer {}", buffer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while ViewFinder tick"); - } - } - _viewFinderTimerElapsed = TimerElapsed; - _viewFinderTimer.Elapsed += _viewFinderTimerElapsed; - _viewFinderTimer.Start(); - - _logger.LogInformation("ViewFinder started"); - - } - catch (Exception ex) - { - _logger.LogError(ex, "Error while ViewFinder start"); - _viewFinderTimer.Stop(); - _viewFinderTimer.Dispose(); - _viewFinderTimer = null; - _viewFinderTimerElapsed = null; - _camera.RequestCompleted -= _viewFinderRequestCompletedHandler; - _viewFinderRequestCompletedHandler = null; - lock (ViewFinderBufferQueue) - ViewFinderBufferQueue.Clear(); - } - } - public void StopViewFinder() - { - ViewFinderStarted = false; - _viewFinderTimer?.Stop(); - _viewFinderTimer?.Dispose(); - _viewFinderTimer = null; - _viewFinderTimerElapsed = null; - _camera?.RequestCompleted -= _viewFinderRequestCompletedHandler; - _viewFinderRequestCompletedHandler = null; - lock (ViewFinderBufferQueue) - ViewFinderBufferQueue.Clear(); - } - public (FrameBuffer, StreamConfiguration)? GrabViewFinderFrame() - { - if (IsImageCapturing) - return null; - - FrameBuffer? buffer = null; - lock (ViewFinderBufferQueue) - if (ViewFinderBufferQueue.Count() > 0) - { - buffer = ViewFinderBufferQueue.LastOrDefault(); - if (buffer is not null) - ViewFinderBufferQueue.Remove(buffer); - } - if (buffer is not null && _viewFinderStreamConfiguration is not null) - return (buffer, _viewFinderStreamConfiguration); - return null; - } - public void ReturnViewFinderFrame(FrameBuffer frameBuffer) - { - lock (ViewFinderBufferQueue) - ViewFinderBufferQueue.Insert(0, frameBuffer); - } - protected virtual void Dispose(bool disposing) { if (!_disposedValue) @@ -986,8 +754,6 @@ public class CameraService : IDisposable } _imageCaptureMappedBuffers.Clear(); } - if (_state.HasFlag(State.ViewFinderFrameBufferAllocated) && _viewFinderStream is not null) - _viewFinderFrameBufferAllocator?.Free(_viewFinderStream); if (_state.HasFlag(State.ImageCaptureFrameBufferAllocated) && _imageCaptureStream is not null) _imageCaptureFrameBufferAllocator?.Free(_imageCaptureStream); if (_state.HasFlag(State.CameraStarted)) @@ -997,23 +763,17 @@ public class CameraService : IDisposable if (_state.HasFlag(State.CameraManagerStarted)) _cameraManager?.Stop(); - // _viewFinderStream?.Dispose(); - // _imageCaptureStream?.Dispose(); - // _viewFinderFrameBufferAllocator?.Dispose(); - // _imageCaptureFrameBufferAllocator?.Dispose(); _cameraManager?.Dispose(); } - catch (Exception e) + catch (Exception ex) { - _logger.LogError(e, "Error while CameraService cleanup"); + _logger.LogError(ex, "Error while CameraService cleanup"); } finally { _cameraManager = null; _camera = null; - _viewFinderFrameBufferAllocator = null; _imageCaptureFrameBufferAllocator = null; - _viewFinderStream = null; _imageCaptureStream = null; _imageCaptureMappedBuffers = null; _state = State.NotInitialized; diff --git a/GSS2.Core/Hardware/ComputeResourcesService.cs b/GSS2.Core/Hardware/ComputeResourcesService.cs deleted file mode 100644 index e4ac034..0000000 --- a/GSS2.Core/Hardware/ComputeResourcesService.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Globalization; - -using GSS2.Core.Extensions; - -using Microsoft.Extensions.Logging; - -namespace GSS2.Core.Hardware; - -public class ComputeResourcesService -{ - public record CpuUsage(double AllUsagePercent, List CoreUsagePercents); - public record CpuTemperature(double Current, double Critical); - public record MemoryUsage( - long TotalB, long UsedB, long AvailableB, - double TotalkB, double UsedkB, double AvailablekB, - double TotalMB, double UsedMB, double AvailableMB, - double TotalGB, double UsedGB, double AvailableGB) - { - public MemoryUsage(long totalB, long usedB, long availableB) : - this( - TotalB: totalB, UsedB: usedB, AvailableB: availableB, - TotalkB: totalB / 1000, UsedkB: usedB / 1000, AvailablekB: availableB / 1000, - TotalMB: totalB / Math.Pow(1000.0, 2), UsedMB: usedB / Math.Pow(1000.0, 2), AvailableMB: availableB / Math.Pow(1000.0, 2), - TotalGB: totalB / Math.Pow(1000.0, 3), UsedGB: usedB / Math.Pow(1000.0, 3), AvailableGB: availableB / Math.Pow(1000.0, 3) - ) - { } - } - public record ComputeResourcesSnapshot(CpuUsage CpuUsage, MemoryUsage MemoryUsage, CpuTemperature CpuTemperature, DateTime Timestamp); - private readonly ILogger _logger; - - private (long idle, long total)? _prevAll = null; - private Dictionary? _prevCores = null; - - public ComputeResourcesService(ILogger logger) - { - _logger = logger; - _logger.LogInformation("Initialization"); - _logger.LogInformation("Initialized"); - } - - public ComputeResourcesSnapshot GetSnapshot() - { - return new ComputeResourcesSnapshot( - CpuUsage: GetCpuUsage(), - CpuTemperature: GetCpuTemperature(), - MemoryUsage: GetMemoryUsage(), - Timestamp: DateTime.UtcNow - ); - } - private CpuUsage GetCpuUsage() - { - static (double usage, long idle, long total) ParseLines(string line, (long idle, long total)? prev) - { - var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); - long user = long.Parse(parts[1]); - long nice = long.Parse(parts[2]); - long system = long.Parse(parts[3]); - long idle = long.Parse(parts[4]); - long iowait = line.Length > 5 ? long.Parse(parts[5]) : 0; - long irq = line.Length > 6 ? long.Parse(parts[6]) : 0; - long softirq = line.Length > 7 ? long.Parse(parts[7]) : 0; - - long idleTime = idle + iowait; - long totalTime = idleTime + user + nice + system + irq + softirq; - - double usage = 0; - - if (prev is not null) - { - long totalDiff = totalTime - prev.Value.total; - long idleDiff = idleTime - prev.Value.idle; - - - if (totalDiff > 0) - usage = (double)(totalDiff - idleDiff) / totalDiff * 100.0; - } - - return (usage, idleTime, totalTime); - } - - var lines = File.ReadLines("/proc/stat").Where(l => l.StartsWith("cpu")); - if (lines.Count() == 0) - return new CpuUsage(0, new List()); - - var (allUsage, allIdle, allTotal) = ParseLines(lines.First(), _prevAll); - _prevAll = (allIdle, allTotal); - - var coreUsages = new List(); - - foreach (var (i, line) in lines.Skip(1).Enumerate()) - { - if (_prevCores is null) - _prevCores = new Dictionary(); - if (!_prevCores.ContainsKey(i)) - _prevCores.Add(i, null); - var (coreUsage, coreIdle, coreTotal) = ParseLines(line, _prevCores[i]); - _prevCores[i] = (coreIdle, coreTotal); - coreUsages.Add(coreUsage); - } - - return new CpuUsage( - AllUsagePercent: allUsage, - CoreUsagePercents: coreUsages - ); - } - private CpuTemperature GetCpuTemperature() - { - var currentStr = File.ReadAllText("/sys/class/thermal/thermal_zone0/temp").Trim(); - var current = int.Parse(currentStr) / 1000.0; - - var criticalFile = Directory.GetFiles("/sys/class/thermal/thermal_zone0/", "trip_point_*_type") - .FirstOrDefault(f => File.ReadAllText(f).Trim() == "critical")? - .Replace("_type", "_temp"); - - double critical = 100.0; - if (criticalFile is not null) - { - var criticalStr = File.ReadAllText(criticalFile).Trim(); - critical = int.Parse(criticalStr) / 1000.0; - } - - return new CpuTemperature( - Current: current, - Critical: critical - ); - } - private MemoryUsage GetMemoryUsage() - { - var memInfo = File.ReadAllLines("/proc/meminfo") - .Select(l => l.Split(':', 2)) - .ToDictionary( - p => p[0], - p => long.Parse(p[1].Trim().Split(' ')[0], CultureInfo.InvariantCulture) * 1024 - ); - - long total = memInfo["MemTotal"]; - long available = memInfo.ContainsKey("MemAvailable") ? memInfo["MemAvailable"] : memInfo["MemFree"]; - - long used = total - available; - - return new MemoryUsage(totalB: total, usedB: used, availableB: available); - } -} diff --git a/GSS2.Core/Hardware/LightsService.cs b/GSS2.Core/Hardware/LightsService.cs index 70ff48d..c23aa4d 100644 --- a/GSS2.Core/Hardware/LightsService.cs +++ b/GSS2.Core/Hardware/LightsService.cs @@ -38,12 +38,18 @@ public class LightsService if (configuration is null) { configuration = LightsServiceConfiguration.Default; + _logger.LogWarning("Configuration not set. Using default configuration."); } _configuration = configuration; _logger.LogInformation("Using following configuration: \n{}", _configuration.ToString("\n", 4)); - - _client = new Lights.LightsClient(GrpcChannel.ForAddress(_configuration.ServerAddress)); + var channelOptions = new GrpcChannelOptions + { + Credentials = ChannelCredentials.Insecure, + HttpHandler = new SocketsHttpHandler(), + MaxReconnectBackoff = TimeSpan.FromSeconds(10) + }; + _client = new Lights.LightsClient(GrpcChannel.ForAddress(_configuration.ServerAddress, channelOptions)); } catch (Exception e) { diff --git a/GSS2.Core/Hardware/TemperatureHumidityService.cs b/GSS2.Core/Hardware/TemperatureHumidityService.cs deleted file mode 100644 index 135a016..0000000 --- a/GSS2.Core/Hardware/TemperatureHumidityService.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Device.I2c; - -using Microsoft.Extensions.Logging; - -using GSS2.Core.Abstractions; - -namespace GSS2.Core.Hardware; - -public class TemperatureHumidityService : IDisposable -{ - public record TemperatureHumidityServiceConfiguration : ServiceConfigurationBase - { - public static TemperatureHumidityServiceConfiguration Default => new TemperatureHumidityServiceConfiguration - { - Bus = 6, - Address = 0x45 - }; - - public int Bus { get; init; } - public int Address { get; init; } - } - private bool _disposedValue; - private readonly ILogger _logger; - private readonly TemperatureHumidityServiceConfiguration _configuration; - - private readonly I2cDevice _sensor; - - public TemperatureHumidityService(ILogger logger, TemperatureHumidityServiceConfiguration? configuration = null) - { - _logger = logger; - - _logger.LogInformation("Initialization"); - - try - { - if (configuration is null) - { - configuration = TemperatureHumidityServiceConfiguration.Default; - _logger.LogWarning("Configuration not set. Using default configuration."); - } - _configuration = configuration; - _logger.LogInformation("Using following configuration: \n{}", _configuration.ToString("\n", 4)); - - _sensor = I2cDevice.Create(new I2cConnectionSettings(_configuration.Bus, _configuration.Address)); - } - catch (Exception e) - { - _logger.LogCritical(e, "An exception occurred while initialization"); - throw; - } - - _logger.LogInformation("Initialized"); - } - - public (double temperature, double relativeHumidity) Measure(int measurementsCount = 1, bool checkCrc = true, CancellationToken cancellationToken = default) - { - // В соответствии с даташитом на SHT45-AD1B-R2 псевдокод измерения с высокой точностью следующий: - // i2c_write(i2c_addr=0x44, tx_bytes=[0xFD]) - // wait_seconds(0.01) - // rx_bytes = i2c_read(i2c_addr=0x44, number_of_bytes=6) - // t_ticks = rx_bytes[0] * 256 + rx_bytes[1] - // checksum_t = rx_bytes[2] - // rh_ticks = rx_bytes[3] * 256 + rx_bytes[4] - // checksum_rh = rx_bytes[5] - // t_degC = -45 + 175 * t_ticks/65535 - // rh_pRH = -6 + 125 * rh_ticks/65535 - // if (rh_pRH > 100): - // rh_pRH = 100 - // if (rh_pRH < 0): - // rh_pRH = 0 - - if (measurementsCount <= 0) - throw new ArgumentOutOfRangeException(nameof(measurementsCount)); - - double sumTemperature = 0; - double sumRelativeHumidity = 0; - int successMeasurementsCount = 0; - - Span command = stackalloc byte[] { 0xFD }; - Span data = stackalloc byte[6]; - - for (int i = 0; i < measurementsCount; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - // Отправляем команду - _sensor.Write(command); - // 10 мс ожидания - Task.Delay(10, cancellationToken); - // Читаем данные - _sensor.Read(data); - - // Распаковываем данные [2 * 8-bit T-data; 8-bit CRC; 2 * 8-bit RH-data; 8-bit CRC] - byte temperatureChecksum = data[2]; - byte relativeHumidityChecksum = data[5]; - ushort temperatureTicks = BitConverter.ToUInt16(data.Slice(0, 2)); - int relativeHumidityTicks = BitConverter.ToUInt16(data.Slice(3, 2)); - - // Проверка по CRC - if (checkCrc && (!CheckCrc(data.Slice(0, 2), temperatureChecksum) || - !CheckCrc(data.Slice(3, 2), relativeHumidityChecksum))) - { - _logger.LogWarning("Temperature and humidity sensor CRC error"); - continue; - } - - // Пересчёт - double temperature = -45 + 175 * (temperatureTicks / 65535.0); - double relativeHumidity = -6 + 125 * (relativeHumidityTicks / 65535.0); - relativeHumidity = Math.Clamp(relativeHumidity, 0, 100); - - // Сумма для усреднения - sumTemperature += temperature; - sumRelativeHumidity += relativeHumidity; - successMeasurementsCount++; - } - catch (Exception e) - { - _logger.LogWarning(e, "Temperature and humidity sensor not available on I2C bus"); - } - } - - if (successMeasurementsCount == 0) - { - _logger.LogError("Temperature and humidity sensor: no successful measurements"); - return (double.NaN, double.NaN); - } - - return (sumTemperature / successMeasurementsCount, sumRelativeHumidity / successMeasurementsCount); - } - - private static bool CheckCrc(Span data, byte expectedCrc) - { - byte crc = 0xFF; - foreach (var b in data) - { - crc ^= b; - for (int i = 0; i < 8; i++) - { - if ((crc & 0x80) != 0) - crc = (byte)((crc << 1) ^ 0x31); - else - crc <<= 1; - } - } - return crc == expectedCrc; - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - try - { - _sensor?.Dispose(); - } - catch { } - } - - _disposedValue = true; - } - } - void IDisposable.Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/GSS2.Core/ImageStorageService.cs b/GSS2.Core/ImageStorageService.cs deleted file mode 100644 index d4837cd..0000000 --- a/GSS2.Core/ImageStorageService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace GSS2.Core; - -public class ImageStorageService -{ - private readonly ILogger _logger; - private readonly DirectoryInfo _rootDirectory; - - public ImageStorageService(ILogger logger, DirectoryInfo rootDirectory) - { - _logger = logger; - _logger.LogInformation("Инициализация"); - - _rootDirectory = rootDirectory; - if (!_rootDirectory.Exists) - { - _rootDirectory.Create(); - _logger.LogInformation("Создана директория: {}", _rootDirectory.FullName); - } - - _logger.LogInformation("Инициализировано"); - } - - public async Task SaveAsync(Stream fileStream, string extension, string subDirectory, CancellationToken cancellationToken = default) - { - var fileName = GenerateFileName(extension); - var fullPath = GetFullPath(fileName, subDirectory); - System.Diagnostics.Debug.Assert(fullPath is not null); - - var directory = Path.GetDirectoryName(fullPath)!; - Directory.CreateDirectory(directory); - - await using var file = new FileStream( - fullPath, - FileMode.CreateNew, - FileAccess.Write, - FileShare.None, - 81920, - useAsync: true); - - await fileStream.CopyToAsync(file, cancellationToken); - - _logger.LogInformation("Файл сохранён: {}", fileName); - - return fileName; - } - - public Stream OpenRead(string fileName, string? subDirectory = null) - { - var fullPath = GetFullPath(fileName, subDirectory); - if (fullPath is null) - throw new FileNotFoundException(null, fullPath); - return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); - } - - public bool Delete(string fileName, string? subDirectory = null) - { - var fullPath = GetFullPath(fileName, subDirectory); - if (fullPath is null) - throw new FileNotFoundException(null, fullPath); - - if (!File.Exists(fullPath)) - return false; - - File.Delete(fullPath); - _logger.LogInformation("Файл удалён: {}", fileName); - - var parentDirectory = Directory.GetParent(fullPath); - if (parentDirectory?.GetFiles().Count() == 0) - { - parentDirectory.Delete(); - _logger.LogInformation("Удалена пустая директория: {}", parentDirectory.Name); - } - - return true; - } - - private string GenerateFileName(string extension) - { - var guid = Guid.NewGuid().ToString("N"); - return $"{guid}{extension}"; - } - - public string? GetFullPath(string fileName, string? subDirectory = null) - { - if (fileName.Count() < 4) - return null; - - var level1 = fileName.Substring(0, 2); - var level2 = fileName.Substring(2, 2); - - if (subDirectory is not null) - return Path.Combine(_rootDirectory.FullName, subDirectory, level1, level2, fileName); - - return ListFiles() - .FirstOrDefault(f => Path.GetFileName(f) == fileName); - } - - public List ListFiles(string? subDirectory = null) - { - if (subDirectory is not null) - return Directory.GetDirectories(Path.Join(_rootDirectory.FullName, subDirectory)) - .SelectMany(Directory.GetDirectories) - .SelectMany(Directory.GetFiles) - .ToList(); - - return Directory.GetDirectories(_rootDirectory.FullName) - .Select(Path.GetFileName) - .SelectMany(ListFiles) - .ToList(); - } -} diff --git a/GSS2.Core/Logging/FileLoggerProvider.cs b/GSS2.Core/Logging/FileLoggerProvider.cs index 4063dca..1c703ec 100644 --- a/GSS2.Core/Logging/FileLoggerProvider.cs +++ b/GSS2.Core/Logging/FileLoggerProvider.cs @@ -15,5 +15,8 @@ public sealed class FileLoggerProvider : ILoggerProvider public ILogger CreateLogger(string categoryName) => new FileLogger(categoryName, _filePath); - public void Dispose() { } + public void Dispose() + { + return; + } } diff --git a/GSS2.Core/Utils/CompressedFloatListConverter.cs b/GSS2.Core/Utils/CompressedFloatListConverter.cs new file mode 100644 index 0000000..591b1ef --- /dev/null +++ b/GSS2.Core/Utils/CompressedFloatListConverter.cs @@ -0,0 +1,65 @@ +using System.IO.Compression; + +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +using Newtonsoft.Json; + +/// +/// Компрессор для больших списков чисел с плавающей точкой +/// +public class CompressedFloatListConverter : ValueConverter, string> +{ + public CompressedFloatListConverter() + : base( + v => Compress(v), + v => Decompress(v) + ) + { } + + private static string Compress(List list) + { + if (list is null || list.Count == 0) + return ""; + + byte[] floatBytes = new byte[list.Count * sizeof(float)]; + Buffer.BlockCopy(list.ToArray(), 0, floatBytes, 0, floatBytes.Length); + + using var outputStream = new MemoryStream(); + using (var compressionStream = new BrotliStream(outputStream, CompressionLevel.Optimal)) + compressionStream.Write(floatBytes, 0, floatBytes.Length); + + return Convert.ToBase64String(outputStream.ToArray()); + } + + private static List Decompress(string base64String) + { + if (string.IsNullOrEmpty(base64String)) + return new List(); + + byte[] compressedBytes; + float[]? floatArray; + + try + { + compressedBytes = Convert.FromBase64String(base64String); + } + catch (FormatException) + { + floatArray = JsonConvert.DeserializeObject(base64String); + if (floatArray is null) + throw; + return new List(floatArray); + } + + using var inputStream = new MemoryStream(compressedBytes); + using var decompressionStream = new BrotliStream(inputStream, CompressionMode.Decompress); + using var outputStream = new MemoryStream(); + decompressionStream.CopyTo(outputStream); + + byte[] decompressedBytes = outputStream.ToArray(); + floatArray = new float[decompressedBytes.Length / sizeof(float)]; + Buffer.BlockCopy(decompressedBytes, 0, floatArray, 0, decompressedBytes.Length); + + return new List(floatArray); + } +} \ No newline at end of file diff --git a/GSS2.UI.Core/Converters/GreaterThenConverter.cs b/GSS2.UI.Core/Converters/GreaterThanConverter.cs similarity index 100% rename from GSS2.UI.Core/Converters/GreaterThenConverter.cs rename to GSS2.UI.Core/Converters/GreaterThanConverter.cs diff --git a/GSS2.UI.Core/Converters/GreaterThenOrEqualConverter.cs b/GSS2.UI.Core/Converters/GreaterThanOrEqualConverter.cs similarity index 97% rename from GSS2.UI.Core/Converters/GreaterThenOrEqualConverter.cs rename to GSS2.UI.Core/Converters/GreaterThanOrEqualConverter.cs index f6e75ae..6c319bc 100644 --- a/GSS2.UI.Core/Converters/GreaterThenOrEqualConverter.cs +++ b/GSS2.UI.Core/Converters/GreaterThanOrEqualConverter.cs @@ -4,7 +4,7 @@ using Avalonia.Data.Converters; namespace GSS2.UI.Core.Converters; -public class GreaterThenOrEqualConverter : IValueConverter +public class GreaterThanOrEqualConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { diff --git a/GSS2.UI.Core/Converters/LessThenConverter.cs b/GSS2.UI.Core/Converters/LessThanConverter.cs similarity index 97% rename from GSS2.UI.Core/Converters/LessThenConverter.cs rename to GSS2.UI.Core/Converters/LessThanConverter.cs index 243890e..d762797 100644 --- a/GSS2.UI.Core/Converters/LessThenConverter.cs +++ b/GSS2.UI.Core/Converters/LessThanConverter.cs @@ -4,7 +4,7 @@ using Avalonia.Data.Converters; namespace GSS2.UI.Core.Converters; -public class LessThenConverter : IValueConverter +public class LessThanConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { diff --git a/GSS2.UI.Core/Converters/LessThenOrEqualConverter.cs b/GSS2.UI.Core/Converters/LessThanOrEqualConverter.cs similarity index 97% rename from GSS2.UI.Core/Converters/LessThenOrEqualConverter.cs rename to GSS2.UI.Core/Converters/LessThanOrEqualConverter.cs index 51f6721..233e971 100644 --- a/GSS2.UI.Core/Converters/LessThenOrEqualConverter.cs +++ b/GSS2.UI.Core/Converters/LessThanOrEqualConverter.cs @@ -4,7 +4,7 @@ using Avalonia.Data.Converters; namespace GSS2.UI.Core.Converters; -public class LessThenOrEqualConverter : IValueConverter +public class LessThanOrEqualConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { diff --git a/GSS2.UI.Core/MainWindow.axaml b/GSS2.UI.Core/MainWindow.axaml index dee43a5..038d783 100644 --- a/GSS2.UI.Core/MainWindow.axaml +++ b/GSS2.UI.Core/MainWindow.axaml @@ -1,15 +1,18 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns="https://github.com/avaloniaui" +> diff --git a/GSS2.UI.Core/SectionPanel.cs b/GSS2.UI.Core/SectionPanel.cs index 3224b33..29de368 100644 --- a/GSS2.UI.Core/SectionPanel.cs +++ b/GSS2.UI.Core/SectionPanel.cs @@ -431,6 +431,10 @@ public class SectionPanel : Panel }; IsScrolling = true; - return animation.RunAsync(control).ContinueWith(_ => Dispatcher.UIThread.Invoke(() => IsScrolling = false)); + return animation + .RunAsync(control) + .ContinueWith(_ => + Dispatcher.UIThread.Invoke(() => IsScrolling = false) + ); } } \ No newline at end of file diff --git a/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs b/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs index 45a190a..4b4b66d 100644 --- a/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs +++ b/GSS2.UI.Core/ViewModels/AsyncImageViewModel.cs @@ -4,22 +4,24 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.IdentityModel.Tokens; + namespace GSS2.UI.Core.ViewModels; public partial class AsyncImageRecordViewModel : ViewModelBase { private static readonly SemaphoreSlim _semaphore = new(1, 1); - [ObservableProperty] public partial string? Data { get; set; } + [ObservableProperty] public partial string Data { get; set; } [ObservableProperty] public partial IImage? Image { get; set; } [ObservableProperty] public partial bool IsLoading { get; set; } = false; - public AsyncImageRecordViewModel(string? data = null) + public AsyncImageRecordViewModel(string data = "") { Data = data; } - partial void OnDataChanged(string? value) => _ = LoadAsync(); + partial void OnDataChanged(string value) => _ = LoadAsync(); private async Task LoadAsync() { @@ -27,7 +29,7 @@ public partial class AsyncImageRecordViewModel : ViewModelBase try { - if (Data is not null) + if (!string.IsNullOrEmpty(Data)) { IsLoading = true; byte[] imageBytes = Convert.FromBase64String(Data); diff --git a/GSS2.UI.Core/ViewModels/CameraViewModel.cs b/GSS2.UI.Core/ViewModels/CameraViewModel.cs deleted file mode 100644 index 6ad9983..0000000 --- a/GSS2.UI.Core/ViewModels/CameraViewModel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Microsoft.Extensions.Logging; - -using LibCameraSharp; - -using GSS2.Core.Hardware; - -using CommunityToolkit.Mvvm.ComponentModel; - -using Avalonia.Media; - -namespace GSS2.UI.Core.ViewModels; - -public partial class CameraViewModel : ViewModelBase, IDisposable -{ - private bool _disposedValue; - - private readonly ILogger _logger; - private readonly CameraService _cameraService; - private readonly System.Timers.Timer _renderTimer; - - public event EventHandler? RenderRequested; - - [ObservableProperty] private Color _background = Colors.Transparent; - [ObservableProperty] private Stretch _stretchMode = Stretch.Uniform; - [ObservableProperty] private TileMode _tileMode = TileMode.None; - - public CameraViewModel(ILogger logger, CameraService cameraService) - { - _logger = logger; - _logger.LogInformation("Initialization"); - - _cameraService = cameraService; - _renderTimer = new System.Timers.Timer(33); - _renderTimer.Elapsed += (_, _) => - { - if (!_cameraService.IsImageCapturing) - RenderRequested?.Invoke(this, new EventArgs()); - }; - - _logger.LogInformation("Initialized"); - } - - public void StartViewFinder() - { - _cameraService.StartViewFinder(); - _renderTimer.Start(); - } - public void StopViewFinder() - { - _renderTimer.Stop(); - _cameraService.StopViewFinder(); - } - public bool CanGrabViewFinderFrame() => !_cameraService.IsImageCapturing; - public (FrameBuffer, StreamConfiguration)? GrabViewFinderFrame() => _cameraService.GrabViewFinderFrame(); - public void ReturnViewFinderFrame(FrameBuffer frameBuffer) => _cameraService.ReturnViewFinderFrame(frameBuffer); - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _renderTimer.Stop(); - _renderTimer.Dispose(); - } - _disposedValue = true; - } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/GSS2.UI.Core/ViewModels/IlluminatorControllerViewModel.cs b/GSS2.UI.Core/ViewModels/IlluminatorControllerViewModel.cs deleted file mode 100644 index d64907d..0000000 --- a/GSS2.UI.Core/ViewModels/IlluminatorControllerViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.Extensions.Logging; - -using CommunityToolkit.Mvvm.ComponentModel; - -using Avalonia.Layout; - -using GSS2.Core.Hardware; - -namespace GSS2.UI.Core.ViewModels; - -public partial class IlluminatorControllerViewModel : ViewModelBase -{ - private readonly ILogger _logger; - private readonly IlluminatorService _illuminatorService; - - private bool _whiteIntensityChanging = false; - private bool _uv365IntensityChanging = false; - private bool _uv254IntensityChanging = false; - - [ObservableProperty] private double _whiteIntensity; - [ObservableProperty] private double _uv365Intensity; - [ObservableProperty] private double _uv254Intensity; - [ObservableProperty] private bool _enabled; - [ObservableProperty] Orientation _orientation; - - public IlluminatorControllerViewModel(ILogger logger, IlluminatorService illuminatorService) - { - _logger = logger; - _logger.LogInformation("Initialization"); - - _illuminatorService = illuminatorService; - - WhiteIntensity = 0; - Uv365Intensity = 0; - Uv254Intensity = 0; - Orientation = Orientation.Horizontal; - Enabled = false; - - _logger.LogInformation("Initialized"); - } - - partial void OnWhiteIntensityChanged(double value) - { - if (_whiteIntensityChanging) - return; - _whiteIntensityChanging = true; - _illuminatorService.SetIntensity(white: value); - _whiteIntensityChanging = false; - } - partial void OnUv365IntensityChanged(double value) - { - if (_uv365IntensityChanging) - return; - _uv365IntensityChanging = true; - _illuminatorService.SetIntensity(uv365: value); - _uv365IntensityChanging = false; - } - partial void OnUv254IntensityChanged(double value) - { - if (_uv254IntensityChanging) - return; - _uv254IntensityChanging = true; - _illuminatorService.SetIntensity(uv254: value); - _uv254IntensityChanging = false; - } - - partial void OnEnabledChanged(bool value) - { - if (value) - _illuminatorService.TurnOn(); - else - _illuminatorService.TurnOff(); - } - -} \ No newline at end of file diff --git a/GSS2.UI.Core/ViewModels/MainWindowViewModel.cs b/GSS2.UI.Core/ViewModels/MainWindowViewModel.cs index f6df287..7b4ebad 100644 --- a/GSS2.UI.Core/ViewModels/MainWindowViewModel.cs +++ b/GSS2.UI.Core/ViewModels/MainWindowViewModel.cs @@ -10,6 +10,8 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] public partial WindowDecorations WindowDecorations { get; set; } [ObservableProperty] public partial bool Topmost { get; set; } [ObservableProperty] public partial WindowState WindowState { get; set; } + [ObservableProperty] public partial double Width { get; set; } + [ObservableProperty] public partial double Height { get; set; } public MainWindowViewModel() { diff --git a/GSS2.UI.Core/ViewModels/ResourceBarViewModel.cs b/GSS2.UI.Core/ViewModels/ResourceBarViewModel.cs deleted file mode 100644 index 7a57c8c..0000000 --- a/GSS2.UI.Core/ViewModels/ResourceBarViewModel.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.ObjectModel; - -using CommunityToolkit.Mvvm.ComponentModel; - -using Avalonia; - -using LiveChartsCore; -using LiveChartsCore.Defaults; -using LiveChartsCore.SkiaSharpView; - -using Microsoft.Extensions.Logging; - -namespace GSS2.UI.Core.ViewModels; - -public partial class ResourceBarViewModel : ViewModelBase -{ - private readonly ILogger _logger; - - [ObservableProperty] private ObservableCollection _series; - [ObservableProperty] private Rect _bounds; - - public ResourceBarViewModel(ILogger logger, IEnumerable series) - { - _logger = logger; - _logger.LogInformation("Initialization"); - - Series = new ObservableCollection(series); - - _logger.LogInformation("Initialized"); - } - - partial void OnBoundsChanged(Rect value) - { - var size = Math.Min(value.Width, value.Height); - - var textSize = size switch - { - < 100 => 8, - < 150 => 10, - < 175 => 12, - _ => 14 - }; - var innerRadius = size switch - { - < 50 => size / 2, - < 120 => size / 3, - _ => size / 4, - }; - - foreach (var series in Series.OfType>()) - { - series.DataLabelsSize = textSize; - series.InnerRadius = innerRadius; - series.OuterRadiusOffset = 0; - series.RelativeOuterRadius = 1; - } - } - - public void UpdateValue(double value, double maxValue) - { - if (Series.Count() == 2 && - Series.Skip(0).First().Values is ObservableCollection value1 && - Series.Skip(1).First().Values is ObservableCollection value2) - { - value1.First().Value = value > maxValue ? maxValue : value; - value2.First().Value = value > maxValue ? 0 : maxValue - value; - } - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/ViewModels/ResourceChartViewModel.cs b/GSS2.UI.Core/ViewModels/ResourceChartViewModel.cs deleted file mode 100644 index 64f6776..0000000 --- a/GSS2.UI.Core/ViewModels/ResourceChartViewModel.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.ObjectModel; - -using Microsoft.Extensions.Logging; - -using CommunityToolkit.Mvvm.ComponentModel; - -using Avalonia; - -using LiveChartsCore; -using LiveChartsCore.SkiaSharpView; - -namespace GSS2.UI.Core.ViewModels; - -public partial class ResourceChartViewModel : ViewModelBase -{ - private readonly ILogger _logger; - [ObservableProperty] private ObservableCollection _series; - [ObservableProperty] private ObservableCollection _yAxes; - [ObservableProperty] private ObservableCollection _xAxes; - [ObservableProperty] private Rect _bounds; - - public ResourceChartViewModel(ILogger logger, IEnumerable series, IEnumerable yAxes, IEnumerable xAxes) - { - _logger = logger; - _logger.LogInformation("Initialization"); - - Series = new ObservableCollection(series); - YAxes = new ObservableCollection(yAxes); - XAxes = new ObservableCollection(xAxes); - - _logger.LogInformation("Initialized"); - } - - partial void OnBoundsChanged(Rect value) - { - var textSize = value.Height switch - { - < 100 => 8, - < 150 => 10, - < 200 => 12, - _ => 14 - }; - var labelsDensity = value.Height switch - { - < 100 => 0.2f, - < 150 => 0.3f, - < 200 => 0.4f, - _ => 0.5f - }; - foreach (var axis in YAxes) - { - axis.TextSize = textSize; - axis.NameTextSize = textSize; - axis.LabelsDensity = labelsDensity; - } - } - - public void AppendValue(double value, int maxValuesCount) - { - if (Series.Count() != 1 || Series.First().Values is not ObservableCollection values) - return; - - values.Add(value); - while (values.Count() > maxValuesCount || values.Count() == 0) - values.RemoveAt(0); - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/ViewModels/ResourceCpuUsageBarsViewModel.cs b/GSS2.UI.Core/ViewModels/ResourceCpuUsageBarsViewModel.cs deleted file mode 100644 index a0c74ef..0000000 --- a/GSS2.UI.Core/ViewModels/ResourceCpuUsageBarsViewModel.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.Extensions.Logging; - -using CommunityToolkit.Mvvm.ComponentModel; - -using Avalonia; - -namespace GSS2.UI.Core.ViewModels; - -public partial class ResourceCpuUsageBarsViewModel : ViewModelBase -{ - private readonly ILogger _logger; - - [ObservableProperty] private Rect _bounds; - [ObservableProperty] private bool _isCoresColumnVisible; - [ObservableProperty] private ResourceBarViewModel _cpu; - [ObservableProperty] private ResourceBarViewModel _cpu0; - [ObservableProperty] private ResourceBarViewModel _cpu1; - [ObservableProperty] private ResourceBarViewModel _cpu2; - [ObservableProperty] private ResourceBarViewModel _cpu3; - - public ResourceCpuUsageBarsViewModel( - ILogger logger, - ResourceBarViewModel cpu, - ResourceBarViewModel cpu0, - ResourceBarViewModel cpu1, - ResourceBarViewModel cpu2, - ResourceBarViewModel cpu3 - ) - { - _logger = logger; - _logger.LogInformation("Initialized"); - - Cpu = cpu; - Cpu0 = cpu0; - Cpu1 = cpu1; - Cpu2 = cpu2; - Cpu3 = cpu3; - - _logger.LogInformation("Initialized"); - } - - partial void OnBoundsChanged(Rect value) - { - IsCoresColumnVisible = Math.Min(value.Width, value.Height * 1.5) > 200; - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/ViewModels/ResourcesViewModel.cs b/GSS2.UI.Core/ViewModels/ResourcesViewModel.cs deleted file mode 100644 index 20037fc..0000000 --- a/GSS2.UI.Core/ViewModels/ResourcesViewModel.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System.Collections.ObjectModel; - -using Microsoft.Extensions.Logging; - -using CommunityToolkit.Mvvm.ComponentModel; - -using Avalonia; -using Avalonia.Threading; - -using SkiaSharp; - -using LiveChartsCore.SkiaSharpView; -using LiveChartsCore.SkiaSharpView.Painting; -using LiveChartsCore.Defaults; - -using GSS2.Core.Hardware; - -namespace GSS2.UI.Core.ViewModels; - -public partial class ResourcesViewModel : ViewModelBase, IDisposable -{ - private bool _disposedValue; - - private readonly ComputeResourcesService _computeResourcesService; - private readonly ILogger _logger; - private readonly int _intervalMs = 500; - private readonly int _chartsHistorySize = 120; - private readonly System.Timers.Timer _updateTimer; - - [ObservableProperty] private Rect _bounds; - [ObservableProperty] private bool _isChartsColumnVisible; - [ObservableProperty] private bool _isBarsColumnVisible; - [ObservableProperty] private ResourceChartViewModel _cpuUsageChartViewModel; - [ObservableProperty] private ResourceChartViewModel _cpuTemperatureChartViewModel; - [ObservableProperty] private ResourceChartViewModel _memoryUsageChartViewModel; - [ObservableProperty] private ResourceCpuUsageBarsViewModel _cpuUsageBarsViewModel; - [ObservableProperty] private ResourceBarViewModel _cpuTemperatureBarViewModel; - [ObservableProperty] private ResourceBarViewModel _memoryUsageBarViewModel; - - public ResourcesViewModel( - ILogger logger, - ILogger resourceCpuUsageBarsViewModelLogger, - ILogger resourceChartViewModelCpuUsageLogger, - ILogger resourceChartViewModelCpuTemperatureLogger, - ILogger resourceChartViewModelMemoryUsageLogger, - ILogger resourceBarViewModelCpuUsageLogger, - ILogger resourceBarViewModelCpu0UsageLogger, - ILogger resourceBarViewModelCpu1UsageLogger, - ILogger resourceBarViewModelCpu2UsageLogger, - ILogger resourceBarViewModelCpu3UsageLogger, - ILogger resourceBarViewModelCpuTemperatureLogger, - ILogger resourceBarViewModelMemoryUsageLogger, - ComputeResourcesService computeResourcesService - ) - { - _logger = logger; - _logger.LogInformation("Initialization"); - - _computeResourcesService = computeResourcesService; - - ResourceChartViewModel CreateChartViewModel(ILogger logger, string name, double minLimit, double maxLimit, double minStep) - { - return new ResourceChartViewModel( - logger: logger, - series: - [ - new LineSeries - { - Name = name, - Values = new ObservableCollection(Enumerable.Repeat(0.0, _chartsHistorySize)), - Fill = new SolidColorPaint(SKColors.DeepSkyBlue.WithAlpha(127), 0), - GeometrySize = 0, - ShowDataLabels = false, - Stroke = new SolidColorPaint(SKColors.DeepSkyBlue, 1), - GeometryFill = new SolidColorPaint(SKColors.Transparent, 0), - GeometryStroke = new SolidColorPaint(SKColors.Transparent, 0), - IsHoverable = false, - DataPadding = new LiveChartsCore.Drawing.LvcPoint(0.1f, 0.1f) - } - ], - yAxes: - [ - new Axis - { - MinLimit = minLimit, - MaxLimit = maxLimit, - MinStep = minStep, - Name = name - } - ], - xAxes: - [ - new Axis - { - IsVisible = false, - Padding = new LiveChartsCore.Drawing.Padding(0) - } - ] - ); - } - ResourceBarViewModel CreateBarViewModel(ILogger logger, Func, string> formatter) - { - return new ResourceBarViewModel( - logger: logger, - series: - [ - new PieSeries - { - Values = new ObservableCollection { new ObservableValue(1) }, - Fill = new SolidColorPaint(SKColors.DeepSkyBlue, 0), - ShowDataLabels = true, - IsHoverable = false, - HoverPushout = 0, - DataLabelsFormatter = formatter, - DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.ChartCenter, - DataLabelsPaint = new SolidColorPaint(SKColors.White) - }, - new PieSeries - { - Values = new ObservableCollection { new ObservableValue(1) }, - Fill = new SolidColorPaint(SKColors.DeepSkyBlue.WithAlpha(64), 0), - ShowDataLabels = false, - IsHoverable = false, - HoverPushout = 0 - } - ] - ); - } - - CpuUsageChartViewModel = CreateChartViewModel(resourceChartViewModelCpuUsageLogger, "Нагрузка ЦПУ, %", 0, 100, 10); - CpuTemperatureChartViewModel = CreateChartViewModel(resourceChartViewModelCpuTemperatureLogger, "Температура ЦПУ, ℃", 0, 100, 10); - MemoryUsageChartViewModel = CreateChartViewModel(resourceChartViewModelMemoryUsageLogger, "Нагрузка ОЗУ, МБ", 0, 8000, 1000); - - CpuUsageBarsViewModel = new ResourceCpuUsageBarsViewModel( - resourceCpuUsageBarsViewModelLogger, - CreateBarViewModel(resourceBarViewModelCpuUsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"), - CreateBarViewModel(resourceBarViewModelCpu0UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"), - CreateBarViewModel(resourceBarViewModelCpu1UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"), - CreateBarViewModel(resourceBarViewModelCpu2UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"), - CreateBarViewModel(resourceBarViewModelCpu3UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %") - ); - CpuTemperatureBarViewModel = CreateBarViewModel(resourceBarViewModelCpuTemperatureLogger, value => $"{value.Model?.Value?.ToString("00.00")} ℃"); - MemoryUsageBarViewModel = CreateBarViewModel(resourceBarViewModelMemoryUsageLogger, value => $"{value.Model?.Value?.ToString("000.00")} МБ"); - - Update(); - - _updateTimer = new System.Timers.Timer(_intervalMs); - _updateTimer.Elapsed += UpdateTimerElapsed; - _updateTimer.AutoReset = true; - _updateTimer.Start(); - - _logger.LogInformation("Initialized"); - } - - partial void OnBoundsChanged(Rect value) - { - if (value.Width < 300) - { - IsChartsColumnVisible = false; - IsBarsColumnVisible = true; - } - else if (value.Width < 400) - { - IsChartsColumnVisible = true; - IsBarsColumnVisible = false; - } - else - { - IsChartsColumnVisible = true; - IsBarsColumnVisible = true; - } - } - - private void UpdateTimerElapsed(object? sender, System.Timers.ElapsedEventArgs ea) => Update(); - private void Update() - { - try - { - var snapshot = _computeResourcesService.GetSnapshot(); - Dispatcher.UIThread.Post(() => - { - if (CpuTemperatureChartViewModel.YAxes.Count() == 1) - CpuTemperatureChartViewModel.YAxes.First().MaxLimit = snapshot.CpuTemperature.Critical; - if (MemoryUsageChartViewModel.YAxes.Count() == 1) - MemoryUsageChartViewModel.YAxes.First().MaxLimit = snapshot.MemoryUsage.TotalMB; - - CpuUsageChartViewModel.AppendValue(Math.Round(snapshot.CpuUsage.AllUsagePercent, 2), _chartsHistorySize); - CpuTemperatureChartViewModel.AppendValue(Math.Round(snapshot.CpuTemperature.Current, 2), _chartsHistorySize); - MemoryUsageChartViewModel.AppendValue(Math.Round(snapshot.MemoryUsage.UsedMB, 2), _chartsHistorySize); - - CpuUsageBarsViewModel.Cpu.UpdateValue(Math.Round(snapshot.CpuUsage.AllUsagePercent, 2), 100); - CpuUsageBarsViewModel.Cpu0.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(0).First(), 2), 100); - CpuUsageBarsViewModel.Cpu1.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(1).First(), 2), 100); - CpuUsageBarsViewModel.Cpu2.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(2).First(), 2), 100); - CpuUsageBarsViewModel.Cpu3.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(3).First(), 2), 100); - CpuTemperatureBarViewModel.UpdateValue(Math.Round(snapshot.CpuTemperature.Current, 2), snapshot.CpuTemperature.Critical); - MemoryUsageBarViewModel.UpdateValue(Math.Round(snapshot.MemoryUsage.UsedMB, 2), snapshot.MemoryUsage.TotalMB); - }); - } - catch (Exception e) - { - _logger.LogWarning(e, "Exception while update"); - } - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - try - { - _updateTimer.Stop(); - _updateTimer.Dispose(); - } - catch - { } - } - - _disposedValue = true; - } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/CameraView.axaml b/GSS2.UI.Core/Views/CameraView.axaml deleted file mode 100644 index 93a4a70..0000000 --- a/GSS2.UI.Core/Views/CameraView.axaml +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/GSS2.UI.Core/Views/CameraView.axaml.cs b/GSS2.UI.Core/Views/CameraView.axaml.cs deleted file mode 100644 index 71c7264..0000000 --- a/GSS2.UI.Core/Views/CameraView.axaml.cs +++ /dev/null @@ -1,714 +0,0 @@ -using System.Numerics; -using System.Runtime.InteropServices; - -using Microsoft.Extensions.Logging; - -using Avalonia; -using Avalonia.LogicalTree; -using Avalonia.Media; -using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Controls; -using Avalonia.OpenGL; -using Avalonia.Threading; -using Avalonia.Input; - -using GSS2.UI.Core.ViewModels; -using Avalonia.Markup.Xaml; -using GSS2.Core.UI.Extensions; -using Avalonia.VisualTree; - -namespace GSS2.UI.Core.Views; - -public partial class CameraView : OpenGlControlBase -{ - private class OpenGLContext - { - [DllImport("libEGL.so.1")] public static extern IntPtr eglQueryString(IntPtr dpy, int name); - [DllImport("libEGL.so.1")] public static extern IntPtr eglGetCurrentDisplay(); - [DllImport("libEGL.so.1")] public static extern IntPtr eglGetProcAddress(string procname); - [DllImport("libGL.so.1")] public static extern void glUniform1i(int location, int falue); - [DllImport("libGL.so.1")] public static extern void glUniformMatrix3x2fv(int location, int count, bool transpose, IntPtr value); - [DllImport("libGL.so.1")] public static extern void glUniformMatrix3fv(int location, int count, bool transpose, IntPtr value); - - public const uint DRM_FORMAT_BGRX8888 = 0x34325258; // XR24 XRGB8888 - public const int GL_TRIANGLE_STRIP = 0x0005; - public const int GL_TEXTURE_WRAP_S = 0x2802; - public const int GL_TEXTURE_WRAP_T = 0x2803; - public const int GL_CLAMP_TO_EDGE = 0x812F; - public const int EGL_LINUX_DMA_BUF_EXT = 0x3270; - public const int EGL_LINUX_DRM_FOURCC_EXT = 0x3271; - public const int EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; - public const int EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; - public const int EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; - - public delegate IntPtr GlEGLImageTargetTexture2DOESDelegate(int target, IntPtr image); - public delegate IntPtr EglCreateImageKHRDelegate(IntPtr dpy, IntPtr ctx, int target, IntPtr buffer, int[] attribs); - public delegate bool EglDestroyImageKHRDelegate(IntPtr dpy, IntPtr image); - - private const string VERTEX_SHADER = @" - #version 300 es - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUv; - - uniform mat4 uMvp; - uniform mat3 uUv; - - out vec2 vUv; - - void main() - { - vec3 uv = uUv * vec3(aUv, 1.0); - vUv = uv.xy; - gl_Position = uMvp * vec4(aPos, 0.0, 1.0); - } - "; - private const string FRAGMENT_SHADER = @" - #version 300 es - precision mediump float; - - in vec2 vUv; - uniform sampler2D uTex; - - out vec4 fragColor; - - void main() - { - fragColor = texture(uTex, vUv); - } - "; - private readonly float[] Vertices = - [ - -1f, -1f, 0f, 1f, - 1f, -1f, 1f, 1f, - -1f, 1f, 0f, 0f, - 1f, 1f, 1f, 0f, - ]; - - public IntPtr EglDisplay { get; } - public int Texture { get; } - public int Program { get; } - public int UTexLocation { get; } - public int UMvpLocation { get; } - public int UUvLocation { get; } - public int VAO { get; } - public int VBO { get; } - public EglCreateImageKHRDelegate EglCreateImageKHR { get; } - public EglDestroyImageKHRDelegate EglDestroyImageKHR { get; } - public GlEGLImageTargetTexture2DOESDelegate GlEGLImageTargetTexture2DOES { get; } - - public OpenGLContext(GlInterface gl) - { - var glExtensions = gl.GetString(GlConsts.GL_EXTENSIONS); - if (glExtensions is null) - throw new Exception("GL extensions query returned null string"); - - if (!glExtensions.Contains("GL_OES_EGL_image") && - !glExtensions.Contains("GL_OES_EGL_image_external")) - throw new Exception("Required GL extensions \"GL_OES_EGL_image_external\" or \"GL_OES_EGL_image\" not found"); - - EglDisplay = eglGetCurrentDisplay(); - if (EglDisplay == IntPtr.Zero) - throw new Exception("EGL display is nullptr"); - - var eglExtensions = Marshal.PtrToStringAnsi(eglQueryString(EglDisplay, EglConsts.EGL_EXTENSIONS)); - if (eglExtensions is null) - throw new Exception("EGL extensions query returned null"); - - if (!eglExtensions.Contains("EGL_EXT_image_dma_buf_import")) - throw new Exception("Required EGL extension \"EGL_EXT_image_dma_buf_import\" not found"); - - var pEglCreateImageKHR = eglGetProcAddress("eglCreateImageKHR"); - if (pEglCreateImageKHR == IntPtr.Zero) - throw new Exception("Required EGL method \"eglCreateImageKHR\" not found"); - EglCreateImageKHR = Marshal.GetDelegateForFunctionPointer(pEglCreateImageKHR); - - var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR"); - if (pEglDestroyImageKHR == IntPtr.Zero) - throw new Exception("Required EGL method \"eglDestroyImageKHR\" not found"); - EglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer(pEglDestroyImageKHR); - - var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES"); - if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero) - throw new Exception("Required EGL method \"glEGLImageTargetTexture2DOES\" not found"); - GlEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer(pGlEGLImageTargetTexture2DOES); - - Texture = gl.GenTexture(); ThrowIfOpenGLError(gl); - gl.BindTexture(GlConsts.GL_TEXTURE_2D, Texture); ThrowIfOpenGLError(gl); - gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl); - gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl); - - gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MIN_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl); - gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MAG_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl); - - int vertexShader = gl.CreateShader(GlConsts.GL_VERTEX_SHADER); ThrowIfOpenGLError(gl); - gl.ShaderSourceString(vertexShader, VERTEX_SHADER); ThrowIfOpenGLError(gl); - gl.CompileShader(vertexShader); - - int fragmentShader = gl.CreateShader(GlConsts.GL_FRAGMENT_SHADER); ThrowIfOpenGLError(gl); - gl.ShaderSourceString(fragmentShader, FRAGMENT_SHADER); ThrowIfOpenGLError(gl); - gl.CompileShader(fragmentShader); ThrowIfOpenGLError(gl); - - Program = gl.CreateProgram(); ThrowIfOpenGLError(gl); - gl.AttachShader(Program, vertexShader); ThrowIfOpenGLError(gl); - gl.AttachShader(Program, fragmentShader); ThrowIfOpenGLError(gl); - gl.LinkProgram(Program); ThrowIfOpenGLError(gl); - - gl.DeleteShader(vertexShader); ThrowIfOpenGLError(gl); - gl.DeleteShader(fragmentShader); ThrowIfOpenGLError(gl); - - UTexLocation = gl.GetUniformLocationString(Program, "uTex"); ThrowIfOpenGLError(gl); - UMvpLocation = gl.GetUniformLocationString(Program, "uMvp"); ThrowIfOpenGLError(gl); - UUvLocation = gl.GetUniformLocationString(Program, "uUv"); ThrowIfOpenGLError(gl); - - VAO = gl.GenVertexArray(); ThrowIfOpenGLError(gl); - VBO = gl.GenBuffer(); ThrowIfOpenGLError(gl); - - gl.BindVertexArray(VAO); ThrowIfOpenGLError(gl); - - gl.BindBuffer(GlConsts.GL_ARRAY_BUFFER, VBO); ThrowIfOpenGLError(gl); - unsafe - { - fixed (float* pVertices = Vertices) - gl.BufferData(GlConsts.GL_ARRAY_BUFFER, Vertices.Length * sizeof(float), new IntPtr(pVertices), GlConsts.GL_STATIC_DRAW); ThrowIfOpenGLError(gl); - } - - gl.EnableVertexAttribArray(0); ThrowIfOpenGLError(gl); - gl.VertexAttribPointer(0, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), IntPtr.Zero); ThrowIfOpenGLError(gl); - - gl.EnableVertexAttribArray(1); ThrowIfOpenGLError(gl); - gl.VertexAttribPointer(1, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), 2 * sizeof(float)); ThrowIfOpenGLError(gl); - - gl.BindVertexArray(0); ThrowIfOpenGLError(gl); - } - - private void ThrowIfOpenGLError(GlInterface gl) - { - var error = gl.GetError(); - if (error != 0) - throw new Exception($"OpenGL context initialization error: {error}"); - } - } - - private readonly ILogger _logger; - private readonly Dictionary _importedDmaBuffers = new Dictionary(); - private bool _isViewFinderStarted = false; - private OpenGLContext? _openGLContext = null; - private Point? _lastMousePosition = null; - private Dictionary? _lastTouchPositions = null; - private float _zoom = 1; - private float _panX = 0; - private float _panY = 0; - - public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), defaultValue: Colors.Transparent); - public Color Background - { - get => GetValue(BackgroundProperty); - set => SetValue(BackgroundProperty, value); - } - public static readonly StyledProperty StretchModeProperty = AvaloniaProperty.Register(nameof(StretchMode), defaultValue: Stretch.Uniform); - public Stretch StretchMode - { - get => GetValue(StretchModeProperty); - set => SetValue(StretchModeProperty, value); - } - public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode), defaultValue: TileMode.None); - public TileMode TileMode - { - get => GetValue(TileModeProperty); - set => SetValue(TileModeProperty, value); - } - - public CameraView(ILogger logger) - { - _logger = logger; - AvaloniaXamlLoader.Load(this); - _logger.LogInformation("{} initialized", nameof(CameraView)); - - DoubleTapped += (_, _) => - { - _zoom = 1f; - _panX = 0f; - _panY = 0f; - }; - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs ea) - { - if (DataContext is CameraViewModel viewModel) - { - try - { - viewModel.StartViewFinder(); - viewModel.RenderRequested += RenderRequested; - _isViewFinderStarted = true; - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Exception while view finder start"); - } - } - else - _logger.LogCritical("Cannot start view finder, data context is null or not camera view model"); - base.OnAttachedToVisualTree(ea); - } - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs ea) - { - if (_isViewFinderStarted && DataContext is CameraViewModel viewModel) - { - try - { - viewModel.StopViewFinder(); - foreach (var (fd, image) in _importedDmaBuffers) - _openGLContext?.EglDestroyImageKHR!(_openGLContext.EglDisplay, image); - _isViewFinderStarted = false; - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Exception while view finder stop"); - } - viewModel.RenderRequested -= RenderRequested; - } - else if (_isViewFinderStarted) - _logger.LogCritical("Cannot stop view finder, data context is null or not camera view model"); - - _isViewFinderStarted = false; - base.OnDetachedFromLogicalTree(ea); - } - - protected override void OnPointerWheelChanged(PointerWheelEventArgs ea) - { - if (ea.Pointer.Type == PointerType.Mouse) - { - var position = ea.GetPosition(this); - - // Рассчитываем позицию в координатах изображения (до масштабирования) - var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom; - var imageY = (-position.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертирована - - // Применяем масштаб - float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f; - _zoom *= zoomFactor; - _zoom = Math.Clamp(_zoom, 1f, 10f); - _zoom = (float)Math.Round(_zoom, 2); - - if (_zoom != 1f) - { - // Пересчитываем смещение так, чтобы указатель остался на месте - _panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom); - _panY = (float)(-position.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертирована - } - else - { - // Если зум == 1, то возвращаем изображение к исходному смещению - _panX = 0f; - _panY = 0f; - } - } - base.OnPointerWheelChanged(ea); - } - protected override void OnPointerPressed(PointerPressedEventArgs ea) - { - if (ea.Pointer.Type == PointerType.Mouse) - { - if (ea.Properties.IsLeftButtonPressed) - { - if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям - { - _zoom = 1f; - _panX = 0f; - _panY = 0f; - } - else - _lastMousePosition = ea.GetPosition(this); - } - } - else if (ea.Pointer.Type == PointerType.Touch) - { - if (_lastTouchPositions is null) - _lastTouchPositions = new Dictionary(); - - if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2) - _lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this)); - else if (_lastTouchPositions.ContainsKey(ea.Pointer.Id)) - _lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this); - } - base.OnPointerPressed(ea); - } - protected override void OnPointerReleased(PointerReleasedEventArgs ea) - { - if (ea.Pointer.Type == PointerType.Mouse) - { - if (!ea.Properties.IsLeftButtonPressed) - _lastMousePosition = null; - } - else if (ea.Pointer.Type == PointerType.Touch) - { - if (_lastTouchPositions is not null && _lastTouchPositions.ContainsKey(ea.Pointer.Id)) - _lastTouchPositions.Remove(ea.Pointer.Id); - } - base.OnPointerReleased(ea); - } - protected override void OnPointerMoved(PointerEventArgs ea) - { - if (ea.Pointer.Type == PointerType.Mouse) - { - if (_lastMousePosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем - { - base.OnPointerMoved(ea); - return; - } - var position = ea.GetPosition(this); - _panX -= (float)(_lastMousePosition.Value.X - position.X); - _panY += (float)(_lastMousePosition.Value.Y - position.Y); // Ось Y инвертирована - _lastMousePosition = position; - } - else if (ea.Pointer.Type == PointerType.Touch) - { - if (_lastTouchPositions is null) - { - base.OnPointerMoved(ea); - return; - } - - if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2) - { - _lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this)); - base.OnPointerMoved(ea); - return; - } - - if (_lastTouchPositions.Count() == 1 && _zoom != 1) // Если есть только одно прикосновение и зум != 1, то только перемещение - { - var position = ea.GetPosition(this); - _panX -= (float)(_lastTouchPositions[ea.Pointer.Id].X - position.X); - _panY += (float)(_lastTouchPositions[ea.Pointer.Id].Y - position.Y); // Ось Y инвертирована - _lastTouchPositions[ea.Pointer.Id] = position; - } - else if (_lastTouchPositions.Count() == 2) // Если два прикосновения, то перемещение с приближением - { - var lastPosition1 = _lastTouchPositions.Skip(0).First().Value; - var lastPosition2 = _lastTouchPositions.Skip(1).First().Value; - var lastDistance = Point.Distance(lastPosition1, lastPosition2); - var lastCenter = PointHelper.Center(lastPosition1, lastPosition2); - - _lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this); - - var newPosition1 = _lastTouchPositions.Skip(0).First().Value; - var newPosition2 = _lastTouchPositions.Skip(1).First().Value; - var newDistance = Point.Distance(newPosition1, newPosition2); - var newCenter = PointHelper.Center(newPosition1, newPosition2); - - var zoomFactor = newDistance / lastDistance; - var newZoom = (float)(_zoom * zoomFactor); - newZoom = (float)Math.Clamp(newZoom, 1f, 10f); - - var imageX = (newCenter.X - Bounds.Width / 2 - _panX) / _zoom; - var imageY = (-newCenter.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертирована - - _zoom = newZoom; - - if (_zoom > 1f) - { - _panX = (float)(newCenter.X - Bounds.Width / 2 - imageX * _zoom); - _panY = (float)(-newCenter.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертирована - } - else - { - _zoom = 1f; - _panX = 0f; - _panY = 0f; - } - } - } - base.OnPointerMoved(ea); - } - - public override void Render(DrawingContext context) - { - context.FillRectangle(new SolidColorBrush(Background), new Rect(0, 0, Bounds.Width, Bounds.Height)); - base.Render(context); - } - private void RenderRequested(object? sender, EventArgs ea) - { - Dispatcher.UIThread.Invoke(() => - { - var transformBounds = this.GetTransformedBounds(); - if (transformBounds is not null && - (transformBounds.Value.Clip.Bottom != 0 || - transformBounds.Value.Clip.Top != 0 || - transformBounds.Value.Clip.Left != 0 || - transformBounds.Value.Clip.Right != 0)) - RequestNextFrameRendering(); - }); - } - - protected override void OnOpenGlInit(GlInterface gl) - { - _logger.LogInformation("OpenGL initialized"); - _logger.LogInformation("GL version: {}", gl.GetString(GlConsts.GL_VERSION)); - _logger.LogInformation("GL vendor: {}", gl.GetString(GlConsts.GL_VENDOR)); - _logger.LogInformation("GL renderer: {}", gl.GetString(GlConsts.GL_RENDERER)); - try - { - _openGLContext = new OpenGLContext(gl); - _logger.LogInformation("OpenGL context initialized"); - } - catch (Exception ex) - { - _logger.LogCritical(ex, "Error while OpenGL initialization"); - } - base.OnOpenGlInit(gl); - } - - protected override void OnOpenGlRender(GlInterface gl, int fb) - { - if (_openGLContext is null) - { - _logger.LogError("OpenGL render called with uninitialized EGL"); - return; - } - try - { - (LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)? frame = null; - while (frame is null && (DataContext as CameraViewModel)?.CanGrabViewFinderFrame() == true) - frame = (DataContext as CameraViewModel)?.GrabViewFinderFrame(); - if (frame is null) - return; - var (frameBuffer, streamConfiguration) = frame.Value; - - // 0. Проверяем что полученный фрейм ожидаемый правильный формат - var width = streamConfiguration.Size.Width; - if (width <= 0) - { - _logger.LogError("Cannot render frame, stream configuration validation error, expected frame width >= 0, got {}", streamConfiguration.Size.Width); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - var height = streamConfiguration.Size.Height; - if (height <= 0) - { - _logger.LogError("Cannot render frame, stream configuration validation error, expected frame height >= 0, got {}", streamConfiguration.Size.Height); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - if (streamConfiguration.PixelFormat.Fourcc != OpenGLContext.DRM_FORMAT_BGRX8888) - { - _logger.LogError("Cannot render frame, stream configuration validation error, expected fourcc: 0x{:X}, got 0x{:X}", OpenGLContext.DRM_FORMAT_BGRX8888, streamConfiguration.PixelFormat.Fourcc); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - if (streamConfiguration.PixelFormat.Modifier != 0) - { - _logger.LogError("Cannot render frame, stream configuration validation error, expected DRM modifier: 0x{:X}, got 0x{:X}", 0, streamConfiguration.PixelFormat.Modifier); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - if (frameBuffer.Planes.Count() != 1) - { - _logger.LogError("Cannot render frame, planes validation error, expected planes count: 0, got {}", frameBuffer.Planes.Count()); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - var plane = frameBuffer.Planes.First(); - // if (plane.Length != width * height * 4) - // { - // _logger.LogError("Cannot render frame, plane validation error, expected plane size 0x{:X}, got 0x{:X}", width * height * 4, plane.Length); - // (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - // return; - // } - var fd = plane.Fd.Get(); - if (fd <= 0) - { - _logger.LogError("Cannot render frame, FD validation error, FD >= 0, got 0x{:X}", fd); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - - var stretchMode = StretchMode; - var tileMode = TileMode; - - // 1. Очищаем экран - gl.Disable(GlConsts.GL_SCISSOR_TEST); - gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); - gl.ClearColor(0f, 0f, 0f, 0f); - gl.Clear(GlConsts.GL_COLOR_BUFFER_BIT); - - // 2. Создаём EGL изображение из dma-buf - IntPtr image; - if (_importedDmaBuffers.ContainsKey(fd)) - { - // 2.1. Если EGL изображение для fd уже создано, то берём его из словаря - image = _importedDmaBuffers[fd]; - } - else - { - // 2.2. Если EGL изображение для fd не создано, то создаём - var attribs = new[] - { - EglConsts.EGL_WIDTH, (int)width, - EglConsts.EGL_HEIGHT, (int)height, - OpenGLContext.EGL_LINUX_DRM_FOURCC_EXT, unchecked((int)OpenGLContext.DRM_FORMAT_BGRX8888), - OpenGLContext.EGL_DMA_BUF_PLANE0_FD_EXT, fd, - OpenGLContext.EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, - OpenGLContext.EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)(plane.Length / height), - EglConsts.EGL_NONE - }; - image = _openGLContext.EglCreateImageKHR.Invoke(_openGLContext.EglDisplay, IntPtr.Zero, OpenGLContext.EGL_LINUX_DMA_BUF_EXT, IntPtr.Zero, attribs); - _importedDmaBuffers.Add(fd, image); - } - - // 3. Биндим текстуры текстуры - gl.ActiveTexture(GlConsts.GL_TEXTURE0); - gl.BindTexture(GlConsts.GL_TEXTURE_2D, _openGLContext.Texture); - - // 4. Привязываем EGL изображение к текстуре - _openGLContext.GlEGLImageTargetTexture2DOES!(GlConsts.GL_TEXTURE_2D, image); - - // 5. Вычисляем матрицы трансформаций - var mvp = ComputeMvp(stretchMode, width, height); - var zoom = Matrix4x4.CreateScale(_zoom, _zoom, 1f); - var translate = Matrix4x4.CreateTranslation((float)(_panX / Bounds.Width * 2), (float)(_panY / Bounds.Height * 2), 0); - mvp *= zoom; - mvp *= translate; - var mvpArray = new[] - { - mvp.M11, mvp.M12, mvp.M13, mvp.M14, - mvp.M21, mvp.M22, mvp.M23, mvp.M24, - mvp.M31, mvp.M32, mvp.M33, mvp.M34, - mvp.M41, mvp.M42, mvp.M43, mvp.M44, - }; - unsafe - { - fixed (float* pMvp = mvpArray) - gl.UniformMatrix4fv(_openGLContext.UMvpLocation, 1, false, pMvp); - } - - var uv = ComputeUv(tileMode); - var uvArray = new[] - { - uv.M11, uv.M12, 0f, - uv.M21, uv.M22, 0f, - uv.M31, uv.M32, 1f - }; - unsafe - { - fixed (float* pUv = uvArray) - OpenGLContext.glUniformMatrix3fv(_openGLContext.UUvLocation, 1, false, new IntPtr(pUv)); - } - - // 6. Запускаем шейдер - gl.UseProgram(_openGLContext.Program); - OpenGLContext.glUniform1i(_openGLContext.UTexLocation, 0); - - // 7. Рисуем вершины на экране - gl.BindVertexArray(_openGLContext.VAO); - gl.DrawArrays(OpenGLContext.GL_TRIANGLE_STRIP, 0, 4); - - // 8. Уничтожаем EGL изображение - // _openGLContext.EglDestroyImageKHR!(_openGLContext.EglDisplay, image); - - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception while OpenGL render"); - } - } - private Matrix4x4 ComputeMvp(Stretch stretch, float imageWidth, float imageHeight) - { - float viewWidth = (float)Bounds.Width; - float viewHeight = (float)Bounds.Height; - - float sx = 1f; - float sy = 1f; - - float viewAspect = viewWidth / viewHeight; - float imgAspect = imageWidth / imageHeight; - - switch (stretch) - { - case Stretch.None: - sx = imageWidth / viewWidth; - sy = imageHeight / viewHeight; - break; - - case Stretch.Fill: - break; - - case Stretch.Uniform: - if (imgAspect > viewAspect) - sy = viewAspect / imgAspect; - else - sx = imgAspect / viewAspect; - break; - - case Stretch.UniformToFill: - if (imgAspect > viewAspect) - sx = imgAspect / viewAspect; - else - sy = viewAspect / imgAspect; - break; - } - - return Matrix4x4.CreateScale(sx, sy, 1f); - } - private Matrix3x2 ComputeUv(TileMode mode) - { - float sx = 1f; - float sy = 1f; - float ox = 0f; - float oy = 0f; - - switch (mode) - { - case TileMode.Tile: - sx = sy = 2f; - break; - - case TileMode.FlipX: - sx = -1f; - ox = 1f; - break; - - case TileMode.FlipY: - sy = -1f; - oy = 1f; - break; - - case TileMode.FlipXY: - sx = sy = -1f; - ox = oy = 1f; - break; - } - - return new Matrix3x2( - sx, 0, - 0, sy, - ox, oy - ); - } - private (float sx, float sy) ComputeScale(Stretch mode, float imageWidth, float imageHeight) - { - float viewWidth = (float)Bounds.Width; - float viewHeight = (float)Bounds.Height; - - float scaleX = viewWidth / imageWidth; - float scaleY = viewHeight / imageHeight; - - switch (mode) - { - case Stretch.None: - return (imageWidth / viewWidth, imageHeight / viewHeight); - case Stretch.Fill: - return (1f, 1f); - case Stretch.Uniform: - var sMin = MathF.Min(scaleX, scaleY); - return (imageWidth * sMin / viewWidth, imageHeight * sMin / viewHeight); - case Stretch.UniformToFill: - var sMax = MathF.Max(scaleX, scaleY); - return (imageWidth * sMax / viewWidth, imageHeight * sMax / viewHeight); - default: - return (1f, 1f); - } - } -} diff --git a/GSS2.UI.Core/Views/IlluminatorControllerView.axaml b/GSS2.UI.Core/Views/IlluminatorControllerView.axaml deleted file mode 100644 index 48f783b..0000000 --- a/GSS2.UI.Core/Views/IlluminatorControllerView.axaml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/GSS2.UI.Core/Views/IlluminatorControllerView.axaml.cs b/GSS2.UI.Core/Views/IlluminatorControllerView.axaml.cs deleted file mode 100644 index 810c8dd..0000000 --- a/GSS2.UI.Core/Views/IlluminatorControllerView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace GSS2.UI.Core.Views; - -public partial class IlluminatorControllerView : UserControl -{ - public IlluminatorControllerView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/ResourceBarView.axaml b/GSS2.UI.Core/Views/ResourceBarView.axaml deleted file mode 100644 index 4817c0b..0000000 --- a/GSS2.UI.Core/Views/ResourceBarView.axaml +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/GSS2.UI.Core/Views/ResourceBarView.axaml.cs b/GSS2.UI.Core/Views/ResourceBarView.axaml.cs deleted file mode 100644 index e2c865d..0000000 --- a/GSS2.UI.Core/Views/ResourceBarView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using LiveChartsCore.SkiaSharpView.Avalonia; - -namespace GSS2.UI.Core.Views; - -public partial class ResourceBarView : PieChart -{ - public ResourceBarView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/ResourceChartView.axaml b/GSS2.UI.Core/Views/ResourceChartView.axaml deleted file mode 100644 index 95c13be..0000000 --- a/GSS2.UI.Core/Views/ResourceChartView.axaml +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/GSS2.UI.Core/Views/ResourceChartView.axaml.cs b/GSS2.UI.Core/Views/ResourceChartView.axaml.cs deleted file mode 100644 index dd7d199..0000000 --- a/GSS2.UI.Core/Views/ResourceChartView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using LiveChartsCore.SkiaSharpView.Avalonia; - -namespace GSS2.UI.Core.Views; - -public partial class ResourceChartView : CartesianChart -{ - public ResourceChartView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml b/GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml deleted file mode 100644 index a0b046f..0000000 --- a/GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml.cs b/GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml.cs deleted file mode 100644 index 4d41257..0000000 --- a/GSS2.UI.Core/Views/ResourceCpuUsageBarsView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace GSS2.UI.Core.Views; - -public partial class ResourceCpuUsageBarsView : UserControl -{ - public ResourceCpuUsageBarsView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/GSS2.UI.Core/Views/ResourcesView.axaml b/GSS2.UI.Core/Views/ResourcesView.axaml deleted file mode 100644 index 1c88bac..0000000 --- a/GSS2.UI.Core/Views/ResourcesView.axaml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/GSS2.UI.Core/Views/ResourcesView.axaml.cs b/GSS2.UI.Core/Views/ResourcesView.axaml.cs deleted file mode 100644 index cd891fc..0000000 --- a/GSS2.UI.Core/Views/ResourcesView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace GSS2.UI.Core.Views; - -public partial class ResourcesView : UserControl -{ - public ResourcesView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/GSS2/Application.axaml b/GSS2/Application.axaml index 6730e20..cf8541e 100644 --- a/GSS2/Application.axaml +++ b/GSS2/Application.axaml @@ -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"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GSS2/Application.axaml.cs b/GSS2/Application.axaml.cs index fc766f2..c6812bd 100644 --- a/GSS2/Application.axaml.cs +++ b/GSS2/Application.axaml.cs @@ -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 _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(); @@ -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; diff --git a/GSS2/Commands/UI.cs b/GSS2/Commands/UI.cs index 3b31b52..4df68b0 100644 --- a/GSS2/Commands/UI.cs +++ b/GSS2/Commands/UI.cs @@ -28,6 +28,8 @@ public static class UI public static readonly Option RenderingMode; public static readonly Option HideConsole; public static readonly Option Mock; + public static readonly Option Width; + public static readonly Option 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("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() diff --git a/GSS2/DependencyInjectionHelper.cs b/GSS2/DependencyInjectionHelper.cs index e27d441..4e847ea 100644 --- a/GSS2/DependencyInjectionHelper.cs +++ b/GSS2/DependencyInjectionHelper.cs @@ -27,10 +27,6 @@ public static class DependencyInjectionHelper .AddSingleton() // Core .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() // База .AddSingleton() .AddSingleton() @@ -38,16 +34,18 @@ public static class DependencyInjectionHelper .AddSingleton() // Общие .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() + // .AddSingleton() + // .AddSingleton() + // .AddSingleton() // AmineContent .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); } diff --git a/GSS2/Program.cs b/GSS2/Program.cs index 119a389..7b19d61 100644 --- a/GSS2/Program.cs +++ b/GSS2/Program.cs @@ -4,15 +4,7 @@ public partial class Program { // Аргументы командной строки private static List _args = new List(); - public static string[] Args - { - get - { - var copy = new string[_args.Count()]; - _args.CopyTo(copy); - return copy; - } - } + public static string[] Args => _args.ToArray(); /// /// Точка входа @@ -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; } } diff --git a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs index aa3c77b..a6f0f61 100644 --- a/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs +++ b/GSS2/ViewModels/AmineContent/AnalysisViewModel.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore; @@ -8,638 +9,1649 @@ using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using GSS2.Core; -using GSS2.Core.Analysis.AmineContent; -using GSS2.Core.Analysis.AmineContent.Database.Results; -using GSS2.Core.Extensions; -using GSS2.UI.Core.ViewModels; +using SkiaSharp; using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; - -using SkiaSharp; using LiveChartsCore.SkiaSharpView.Painting.Effects; -using GSS2.UI.Core.Services; + using GSS2.Core.Hardware; +using GSS2.Core.Analysis.AmineContent; +using GSS2.Core.Analysis.AmineContent.Database; +using GSS2.Core.Extensions; + +using GSS2.UI.Core.ViewModels; +using GSS2.UI.Core.Services; + +using UnitsNet; + +using MathNet.Numerics.Statistics; namespace GSS2.ViewModels.AmineContent; public partial class AnalysisViewModel : ViewModelBase { - const double QUALITY_MIN_MEAN = 2.0; - const double QUALITY_ALPHA = 0.7; - const double QUALITY_BETA = 0.3; + /// + /// Внутренняя модель элементов меню + /// + public partial class MenuItemViewModel : ViewModelBase + { + [ObservableProperty] public partial int Id { get; set; } + [ObservableProperty] public partial string SampleName { get; set; } + + public MenuItemViewModel(int id, string sampleName) + { + Id = id; + SampleName = sampleName; + } + } + + /// + /// Внутренняя модель элементов выпадающего списка сепараторов + /// + public partial class SeparatorComboBoxItemViewModel : ViewModelBase + { + [ObservableProperty] public partial int Id { get; set; } + [ObservableProperty] public partial string Type { get; set; } + + public SeparatorComboBoxItemViewModel(int id, string type) + { + Id = id; + Type = type; + } + } + + /// + /// Внутренняя модель элементов выпадающего списка марок + /// + public partial class BrandComboBoxItemViewModel : ViewModelBase + { + [ObservableProperty] public partial int Id { get; set; } + [ObservableProperty] public partial string BrandName { get; set; } + [ObservableProperty] public partial string MixtureName { get; set; } + + public BrandComboBoxItemViewModel(int id, string brandName, string mixtureName) + { + Id = id; + BrandName = brandName; + MixtureName = mixtureName; + + } + } + + /// + /// Внутренняя модель записи + /// + public partial class RecordViewModel : ViewModelBase + { + public const double UNPROCESSED_THRESHOLD = 0.5; + public const double HOTSPOT_THRESHOLD = 1.2; + public const double EPSILON = 1e-10; + public const double UNPROCESSED_FRACTION_THRESHOLD = 10; + + /// + /// Модель гистограммы + /// + public partial class HistogramViewModel : ViewModelBase + { + /// + /// Оси абсцисс + /// + [ObservableProperty] public partial Axis[] XAxis { get; set; } = [new Axis()]; + /// + /// Оси ординат + /// + [ObservableProperty] public partial Axis[] YAxis { get; set; } = [new Axis()]; + /// + /// Серии (значения) + /// + [ObservableProperty] public partial ISeries[] Series { get; set; } = []; + + /// + /// Очистить гистограмму + /// + public void Clear() + { + XAxis = [new Axis()]; + YAxis = [new Axis()]; + Series = Array.Empty(); + } + + /// + /// Заполнить гистограмму + /// + /// Значения + /// Левая граница оси абсцисс + /// Правая граница оси абсцисс + /// Текст слева + /// Текст снизу + /// Текст легенды + public void Set(IEnumerable values, double? min = null, double? max = null, string leftText = "", string bottomText = "", string legend = "") + { + if (values is null || values.Count() == 0) + { + Clear(); + return; + } + + var vals = values.Select(v => (double)v).ToArray(); + double minVal = min ?? vals.Min(); + double maxVal = max ?? vals.Max(); + int binCount = 20; + + double binSize = (maxVal - minVal) / binCount; + var binsPDF = new int[binCount]; + var binsCDF = new float[binCount]; + + foreach (var v in vals) + { + int index = (int)((v - minVal) / binSize); + if (index >= binCount) + index = binCount - 1; + if (index < 0) + index = 0; + binsPDF[index]++; + for (int i = index; i < binCount; i++) + binsCDF[i] += 100.0f / values.Count(); + } + + // Подписи для оси X + var labels = new string[binCount]; + for (int i = 0; i < binCount; i++) + { + double from = minVal + i * binSize; + double to = from + binSize; + labels[i] = $"{from:F2}"; + } + + Series = + [ + new ColumnSeries + { + Values = binsPDF, + Name = legend, + Fill = new SolidColorPaint(SKColors.White), + ScalesYAt = 0 + } + ]; + XAxis = + [ + new Axis + { + Labels = labels, + LabelsRotation = 45, + TextSize = 14, + Name = bottomText, + NameTextSize = 14, + NamePaint=new SolidColorPaint(SKColors.White), + SeparatorsPaint = new SolidColorPaint(SKColors.White), + LabelsPaint = new SolidColorPaint(SKColors.White), + TicksPaint = new SolidColorPaint(SKColors.White), + SubticksPaint = new SolidColorPaint(SKColors.White) + } + ]; + YAxis = + [ + new Axis + { + TextSize = 14, + MinLimit = 0, + Name=leftText, + NameTextSize = 14, + NamePaint=new SolidColorPaint(SKColors.White), + Position = LiveChartsCore.Measure.AxisPosition.Start, + SeparatorsPaint = new SolidColorPaint(SKColors.White), + LabelsPaint = new SolidColorPaint(SKColors.White), + TicksPaint = new SolidColorPaint(SKColors.White), + SubticksPaint = new SolidColorPaint(SKColors.White) + } + ]; + } + /// + /// Заполнить гистограмму + /// + /// Значения + /// Левая граница оси абсцисс + /// Правая граница оси абсцисс + /// Текст слева + /// Текст снизу + /// Текст легенды + public void Set(IEnumerable values, double? min = null, double? max = null, string leftText = "", string bottomText = "", string legend = "") => + Set(values.Select(v => (float)v), min, max, leftText, bottomText, legend); + } + + #region Основные свойства + /// + /// + /// + [ObservableProperty] public partial int Id { get; set; } = 0; + /// + /// + /// + [ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString(); + /// + /// + /// + [ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial string SampleName { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string SampleComment { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial string SamplingPlace { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string SeparatorType { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string BrandName { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string MixtureName { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1; + /// + /// Представление + /// + [ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new(); + /// + /// Представление + /// + [ObservableProperty] public partial AsyncImageRecordViewModel MaskImage { get; set; } = new(); + /// + /// Представление + /// + [ObservableProperty] public partial AsyncImageRecordViewModel ResultImage { get; set; } = new(); + /// + /// + /// + [ObservableProperty] public partial int ParticlesCount { get; set; } = -1; + /// + /// Данные + /// + [ObservableProperty] public partial List Values { get; set; } = new(); + /// + /// Данные + /// + [ObservableProperty] public partial List Xs { get; set; } = new(); + /// + /// Данные + /// + [ObservableProperty] public partial List Ys { get; set; } = new(); + #endregion + + #region Общие свойства + /// + /// Средняя нормированная обработанная площадь, % + /// + [ObservableProperty] public partial double MeanProcessedArea { get; set; } = new(); + /// + /// Средняя нормированная площадь хотспотов, % + /// + [ObservableProperty] public partial double MeanHotspotArea { get; set; } = new(); + /// + /// Средний интегральный оптический показатель + /// + [ObservableProperty] public partial double MeanOpticalIntegral { get; set; } = new(); + /// + /// Средняя внутригранульная неоднородность + /// + [ObservableProperty] public partial double MeanInnerHeterogeneity { get; set; } = new(); + /// + /// Среднее внутригранульное среднеквадратичное отклонение + /// + [ObservableProperty] public partial double MeanInnerStdDev { get; set; } = new(); + /// + /// 50-й процентиль нормированной обработанной площади (медиана), % + /// + [ObservableProperty] public partial double ProcessedAreaP50 { get; set; } = new(); + /// + /// 10-й процентиль нормированной обработанной площади, % + /// + [ObservableProperty] public partial double ProcessedAreaP10 { get; set; } = new(); + /// + /// 90-й процентиль нормированной обработанной площади, % + /// + [ObservableProperty] public partial double ProcessedAreaP90 { get; set; } = new(); + /// + /// Ширина центрального диапазона распределения нормированной обработанной площади, п.п. + /// + [ObservableProperty] public partial double ProcessedAreaDeltaP90P10 { get; set; } = new(); + /// + /// Среднее квадратичное отклонение нормированной обработанной площади + /// + [ObservableProperty] public partial double ProcessedAreaStdDev { get; set; } = new(); + /// + /// Межгранульная неоднородность + /// + [ObservableProperty] public partial double InterHeterogeneity { get; set; } = new(); + /// + /// Доля необработанных гранул, тех гранул нормированная НЕобработанная площадь которых больше % + /// + [ObservableProperty] public partial double UnprocessedFraction { get; set; } = new(); + + /// + /// Нормализованные значения, где
+ /// от 0 до 1 - необработанный пиксель (в изначальных значениях от 0 до * x)
+ /// от 1 до 2 - обработанный пиксель (в изначальных значениях от * x до * x)
+ /// от 2 до 3 - хотспот (в изначальных значениях от * x до максимального значения) + ///
+ public List NormalizedValues { get; set; } = new(); + /// + /// Нормированные обработанные площади + /// + public List ProcessedAreas { get; set; } = new(); + /// + /// Нормированные необработанные площади + /// + public List UnprocessedAreas { get; set; } = new(); + /// + /// Нормированные площади хотспотов + /// + public List HotspotAreas { get; set; } = new(); + /// + /// Интегральные оптические показатели + /// + public List OpticalIntegrals { get; set; } = new(); + /// + /// Внутригранульные среднеквадратичные отклонения + /// + public List InnerStdDevs { get; set; } = new(); + /// + /// Внутригранульные неоднородности + /// + public List InnerHeterogeneities { get; set; } = new(); + + + /// + /// Гистограмма нормированных обработанных площадей + /// + [ObservableProperty] public partial HistogramViewModel ProcessedAreasHistogram { get; set; } = new(); + /// + /// Гистограмма нормированных необработанных площадей + /// + [ObservableProperty] public partial HistogramViewModel UnprocessedAreasHistogram { get; set; } = new(); + /// + /// Гистограмма нормированных площадей хотспотов + /// + [ObservableProperty] public partial HistogramViewModel HotspotAreasHistogram { get; set; } = new(); + /// + /// Гистограмма интегральных оптических показателей + /// + [ObservableProperty] public partial HistogramViewModel OpticalIntegralsHistogram { get; set; } = new(); + /// + /// Гистограмма внутригранульных среднеквадратичных отклонений + /// + [ObservableProperty] public partial HistogramViewModel InnerStdDevsHistogram { get; set; } = new(); + /// + /// Гистограмма внутригранульных неоднородностей + /// + [ObservableProperty] public partial HistogramViewModel InnerHeterogeneitiesHistogram { get; set; } = new(); + #endregion + + #region Свойства отдельных гранул + /// + /// Индекс выбранной гранулы + /// + [ObservableProperty] public partial int ParticleIndex { get; set; } = 0; + + /// + /// Представление выбранной гранулы + /// + [ObservableProperty] public partial AsyncImageRecordViewModel ParticleImage { get; set; } = new(); + /// + /// Представление выбранной гранулы + /// + [ObservableProperty] public partial AsyncImageRecordViewModel ParticleMaskImage { get; set; } = new(); + /// + /// Представление выбранной гранулы + /// + [ObservableProperty] public partial AsyncImageRecordViewModel ParticleResultImage { get; set; } = new(); + + /// + /// Выбрано ли изображение + /// + [ObservableProperty] public partial bool ParticleImageIsVisible { get; set; } = true; + /// + /// Выбрано ли изображение + /// + [ObservableProperty] public partial bool ParticleMaskImageIsVisible { get; set; } = false; + /// + /// Выбрано ли изображение + /// + [ObservableProperty] public partial bool ParticleResultImageIsVisible { get; set; } = false; + + /// + /// Масштаб изображений , и + /// + [ObservableProperty] public partial float ParticleImagesZoom { get; set; } = 1; + /// + /// Смещение по X изображений , и + /// + [ObservableProperty] public partial float ParticleImagesPanX { get; set; } = 0; + /// + /// Смещение по Y изображений , и + /// + [ObservableProperty] public partial float ImagesPanY { get; set; } = 0; + + /// + /// Нормированная обработанная площадь выбранной гранулы + /// + [ObservableProperty] public partial double ParticleProcessedArea { get; set; } = new(); + /// + /// Нормированная необработанная площадь выбранной гранулы + /// + [ObservableProperty] public partial double ParticleUnprocessedArea { get; set; } = new(); + /// + /// Нормированная площадь хотспотов выбранной гранулы + /// + [ObservableProperty] public partial double ParticleHotspotArea { get; set; } = new(); + /// + /// Интегральный оптический показатель выбранной гранулы + /// + [ObservableProperty] public partial double ParticleOpticalIntegral { get; set; } = new(); + /// + /// Внутригранульное среднеквадратичное отклонение выбранной гранулы + /// + [ObservableProperty] public partial double ParticleInnerStdDev { get; set; } = new(); + /// + /// Внутригранульная неоднородность выбранной гранулы + /// + [ObservableProperty] public partial double ParticleInnerHeterogeneity { get; set; } = new(); + /// + /// Гистограмма нормированных значений выбранной гранулы, где
+ /// от 0 до 1 - необработанный пиксель (в изначальных значениях от 0 до * x)
+ /// от 1 до 2 - обработанный пиксель (в изначальных значениях от * x до * x)
+ /// от 2 до 3 - хотспот (в изначальных значениях от * x до максимального значения) + ///
+ [ObservableProperty] public partial HistogramViewModel ParticleValuesHistogram { get; set; } = new(); + #endregion + + /// + /// Пересчитать вычисляемые свойства + /// + public void CalculateProperties() + { + // Очищаем списки + NormalizedValues.Clear(); + ProcessedAreas.Clear(); + UnprocessedAreas.Clear(); + HotspotAreas.Clear(); + OpticalIntegrals.Clear(); + InnerStdDevs.Clear(); + InnerHeterogeneities.Clear(); + + // Для каждой гранулы + var max = Values.Max(vs => vs.Max()); + foreach (var value in Values) + { + // Находим нормализованное значение + var normalized = new double[value.Count()]; + for (int i = 0; i < value.Length; i++) + if (value[i] <= MixtureNormalRate * UNPROCESSED_THRESHOLD) + normalized[i] = value[i] / (MixtureNormalRate * UNPROCESSED_THRESHOLD); + else if (value[i] <= MixtureNormalRate * HOTSPOT_THRESHOLD) + normalized[i] = 1 + (value[i] - (MixtureNormalRate * UNPROCESSED_THRESHOLD)) / ((MixtureNormalRate * HOTSPOT_THRESHOLD) - (MixtureNormalRate * UNPROCESSED_THRESHOLD)); + else + normalized[i] = 2 + (value[i] - (MixtureNormalRate * HOTSPOT_THRESHOLD)) / (max - (MixtureNormalRate * HOTSPOT_THRESHOLD)); + + // Вычисляем значения + var area = normalized.Count(); + var processedArea = normalized.Count(v => v > 1) / area * 100; + var unprocessedArea = normalized.Count(v => v <= 1) / area * 100; + var hotspotArea = normalized.Count(v => v > 2) / area * 100; + var opticalIntegral = normalized.Sum() / area * 100; + var innerStdDev = normalized.StdDev(); + var innerHeterogeneity = innerStdDev / (opticalIntegral + EPSILON); + + // Добавляем значения в списки + NormalizedValues.Add(normalized); + ProcessedAreas.Add(processedArea); + UnprocessedAreas.Add(unprocessedArea); + HotspotAreas.Add(hotspotArea); + OpticalIntegrals.Add(opticalIntegral); + InnerStdDevs.Add(innerStdDev); + InnerHeterogeneities.Add(innerHeterogeneity); + } + + // Вычисляем общие значения + MeanProcessedArea = ProcessedAreas.Mean(); + ProcessedAreaP10 = ProcessedAreas.Percentile(10); + ProcessedAreaP50 = ProcessedAreas.Percentile(50); + ProcessedAreaP90 = ProcessedAreas.Percentile(90); + ProcessedAreaStdDev = ProcessedAreas.StdDev(); + InterHeterogeneity = ProcessedAreaStdDev / MeanProcessedArea * 100; + ProcessedAreaDeltaP90P10 = ProcessedAreaP90 - ProcessedAreaP10; + UnprocessedFraction = (double)UnprocessedAreas.Count(v => v > UNPROCESSED_FRACTION_THRESHOLD) / UnprocessedAreas.Count() * 100; + MeanInnerHeterogeneity = InnerHeterogeneities.Mean(); + MeanInnerStdDev = InnerStdDevs.Mean(); + MeanHotspotArea = HotspotAreas.Mean(); + MeanOpticalIntegral = OpticalIntegrals.Mean(); + + // Обновляем гистограммы + ProcessedAreasHistogram.Set(ProcessedAreas, 0, 100, "Количество гранул, шт", "Нормированная обработанная площадь, %"); + UnprocessedAreasHistogram.Set(UnprocessedAreas, 0, 100, "Количество гранул, шт", "Нормированная необработанная площадь, %"); + HotspotAreasHistogram.Set(HotspotAreas, 0, 100, "Количество гранул, шт", "Нормированная \"горячая\" площадь, %"); + OpticalIntegralsHistogram.Set(OpticalIntegrals, 0, 3, "Количество гранул, шт", "Оптический интегральный показатель, %"); + InnerStdDevsHistogram.Set(InnerStdDevs, InnerStdDevs.Min(), InnerStdDevs.Max(), "Количество гранул, шт", "Внутригранульное среднеквадратичное отклонение"); + InnerHeterogeneitiesHistogram.Set(InnerHeterogeneities, InnerHeterogeneities.Min(), InnerHeterogeneities.Max(), "Количество гранул, шт", "Внутригранульная однородность"); + + // Сбрасываем индекс выбранной гранулы + ParticleIndex = 0; + } + + /// + /// Переключится на следующую гранулу + /// + [RelayCommand] + public void NextParticle() + { + if (ParticlesCount == 0) + return; + ParticleIndex = (ParticleIndex + 1) % ParticlesCount; + } + /// + /// Переключится на предыдущую гранулу + /// + [RelayCommand] + public void PreviousParticle() + { + if (ParticlesCount == 0) + return; + ParticleIndex = (ParticleIndex - 1 + ParticlesCount) % ParticlesCount; + } + /// + /// Переключить на изображение + /// + [RelayCommand] + private void SwitchToImage() + { + ParticleImageIsVisible = true; + ParticleMaskImageIsVisible = false; + ParticleResultImageIsVisible = false; + } + /// + /// Переключить на изображение + /// + [RelayCommand] + private void SwitchToMaskImage() + { + ParticleImageIsVisible = false; + ParticleMaskImageIsVisible = true; + ParticleResultImageIsVisible = false; + } + /// + /// Переключить на изображение + /// + [RelayCommand] + private void SwitchToResultImage() + { + ParticleImageIsVisible = false; + ParticleMaskImageIsVisible = false; + ParticleResultImageIsVisible = true; + } + + /// + /// Вызывается при изменении + /// + partial void OnParticleIndexChanged(int value) + { + // Если количество частиц меньше или равно 0 + if (ParticlesCount <= 0) + { + // Сбрасываем общие значения + ParticleProcessedArea = -1; + ParticleUnprocessedArea = -1; + ParticleHotspotArea = -1; + ParticleOpticalIntegral = -1; + ParticleInnerStdDev = -1; + ParticleInnerHeterogeneity = -1; + ParticleValuesHistogram.Clear(); + return; + } + // Заполняем общие значения из списков + ParticleProcessedArea = ProcessedAreas[value]; + ParticleUnprocessedArea = UnprocessedAreas[value]; + ParticleHotspotArea = HotspotAreas[value]; + ParticleOpticalIntegral = OpticalIntegrals[value]; + ParticleInnerStdDev = InnerStdDevs[value]; + ParticleInnerHeterogeneity = InnerHeterogeneities[value]; + // Обновляем гистограмму + ParticleValuesHistogram.Set(NormalizedValues[value], 0, 3, "Количество, шт", "Значение"); + } + + } public event EventHandler? UpdateCharts; + /// + /// Логгер + /// private readonly ILogger _logger; - private readonly ResultsContext _context; - private readonly ImageCapturerService _imageCapturer; - private readonly ImageStorageService _imageStorage; + /// + /// Контекст базы данных + /// + private readonly Context _context; + /// + /// Сервис для получения изображений с камеры + /// + private readonly IImageCapturerService _imageCapturer; + /// + /// Сервис анализатора + /// private readonly AnalyzerService _analyzer; - private readonly DependencyInjectionViewLocator _viewLocator; - private readonly NavigationService _navigation; + /// + /// Сервис управления сигнальными светодиодами + /// private readonly LightsService _lights; + /// + /// ViewLocator для получения других View по их моделям + /// + private readonly DependencyInjectionViewLocator _viewLocator; + /// + /// Сервис навигации между экранами + /// + private readonly NavigationService _navigation; + /// + /// Задача съёмки изображений + /// + private Task? _сaptureImagesTask = null; + /// + /// Токен отмены задачи съёмки изображений + /// private CancellationTokenSource? _captureImagesTaskCts = null; + /// + /// Меню открыто + /// [ObservableProperty] public partial bool MenuOpened { get; set; } = false; - [ObservableProperty] public partial ObservableCollection Records { get; set; } + /// + /// Элементы меню + /// + [ObservableProperty] public partial ObservableCollection MenuItems { get; set; } = new(); + /// + /// Текущий выбранный элемент меню. Равен -1 если элемент не выбран + /// + [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; - [ObservableProperty] public partial ResultRecord? SelectedRecord { get; set; } - [ObservableProperty] public partial ResultRecord EditingRecord { get; set; } - - [ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0; - [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; - [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true; - [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true; - - [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; - [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; - [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true; - [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true; - - [ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null; - - [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; - [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; - - [ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null; - [ObservableProperty] public partial AsyncImageRecordViewModel? MaskImage { get; set; } = null; - [ObservableProperty] public partial AsyncImageRecordViewModel? ResultImage { get; set; } = null; + /// + /// Запись + /// + [ObservableProperty] public partial RecordViewModel Record { get; set; } = new(); + /// + /// Выбрано ли изображение + /// [ObservableProperty] public partial bool ImageIsVisible { get; set; } = true; + /// + /// Выбрано ли изображение + /// [ObservableProperty] public partial bool MaskImageIsVisible { get; set; } = false; + /// + /// Выбрано ли изображение + /// [ObservableProperty] public partial bool ResultImageIsVisible { get; set; } = false; + /// + /// Масштаб изображений , и + /// [ObservableProperty] public partial float ImagesZoom { get; set; } = 1; + /// + /// Смещение по X изображений , и + /// [ObservableProperty] public partial float ImagesPanX { get; set; } = 0; + /// + /// Смещение по Y изображений , и + /// [ObservableProperty] public partial float ImagesPanY { get; set; } = 0; - [ObservableProperty] public partial SolidColorPaint LegendTextPaint { get; set; } = new SolidColorPaint(SKColors.White); + /// + /// Список элементов выпадающего списка сепараторов + /// + [ObservableProperty] public partial ObservableCollection SeparatorComboBoxItems { get; set; } = new(); + /// + /// Текущий выбранный элемент выпадающего списка сепараторов. Равен -1 если элемент не выбран + /// + [ObservableProperty] public partial int SeparatorComboBoxSelectedIndex { get; set; } = -1; + /// + /// Список элементов выпадающего списка марок + /// + [ObservableProperty] public partial ObservableCollection BrandComboBoxItems { get; set; } = new(); + /// + /// Текущий выбранный элемент выпадающего списка марок. Равен -1 если элемент не выбран + /// + [ObservableProperty] public partial int BrandComboBoxSelectedIndex { get; set; } = -1; - [ObservableProperty] public partial ISeries[] MeanValuesHistogram { get; set; } = []; - [ObservableProperty] public partial Axis[] MeanValuesXAxis { get; set; } = [new Axis()]; - [ObservableProperty] public partial Axis[] MeanValuesYAxis { get; set; } = [new Axis()]; + /// + /// Текущий экран главной секции + /// + [ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0; + /// + /// Можно ли переходить на следующий экран главной секции + /// + [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; + /// + /// Можно ли переходить на предыдущий экран главной секции + /// + [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = false; + /// + /// Происходит ли переход между экранами главной секции + /// + [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; - [ObservableProperty] public partial ISeries[] VariancesHistogram { get; set; } = []; - [ObservableProperty] public partial Axis[] VariancesXAxis { get; set; } = [new Axis()]; - [ObservableProperty] public partial Axis[] VariancesYAxis { get; set; } = [new Axis()]; + /// + /// Текущий экран информационной секции + /// + [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; + /// + /// Можно ли переходить на следующий экран информационной секции + /// + [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; + /// + /// Можно ли переходить на предыдущий экран информационной секции + /// + [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false; + /// + /// Происходит ли переход между экранами информационной секции + /// + [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false; - [ObservableProperty] public partial ISeries[] QualitiesHistogram { get; set; } = []; - [ObservableProperty] public partial Axis[] QualitiesXAxis { get; set; } = [new Axis()]; - [ObservableProperty] public partial Axis[] QualitiesYAxis { get; set; } = [new Axis()]; + /// + /// Проводится ли съёмка изображений + /// + [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; + /// + /// Прогресс съёмки изображений + /// + [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; + + /// + /// Можно ли взаимодействовать с объектом + /// + [ObservableProperty] public partial bool CanInteract { get; set; } = true; + + /// + /// Создаётся ли новая запись + /// + [ObservableProperty] public partial bool IsNewRecord { get; set; } = true; public AnalysisViewModel( ILogger logger, - ResultsContext context, - ImageCapturerService imageCapturer, - ImageStorageService imageStorage, + Context context, + IImageCapturerService imageCapturer, AnalyzerService analyzer, + LightsService lights, DependencyInjectionViewLocator viewLocator, - NavigationService navigation, - LightsService lights + NavigationService navigation ) { + // Устанавливаем readonly поля _logger = logger; _context = context; _imageCapturer = imageCapturer; - _imageStorage = imageStorage; _analyzer = analyzer; + _lights = lights; _viewLocator = viewLocator; _navigation = navigation; - _lights = lights; _logger.LogInformation("Инициализация"); - // Получаем записи из базы данных - Records = new ObservableCollection(_context.ResultRecords); + // Получаем записи выпадающего списка сепараторов из базы данных + SeparatorComboBoxItems = new ObservableCollection(_context.SeparatorRecords.Select(r => new SeparatorComboBoxItemViewModel(r.Id, r.Type))); + // Получаем записи выпадающего списка марок из базы данных + BrandComboBoxItems = new ObservableCollection(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName))); - // Создаём пустые записи - SelectedRecord = null; - // На самом деле заполнять EditingRecord излишне, потому что при изменении SelectedRecord - // вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений - EditingRecord = new() + // Подписываемся на изменения базы данных для отслеживания изменений списка сепараторов и марок + _context.ChangeTracker.StateChanged += (_, ea) => { - SampleName = "", - SampleComment = "", - SamplingDateTime = DateTime.UtcNow, - SamplingPlace = "", - Image = "", - SeparatorType = "", - SampleBrand = "", - MaskImage = "", - ResultImage = "", - MinValue = -1, - MaxValue = -1, - ParticlesCount = -1, - MeanValue = -1, - MeanVariance = -1, - VarianceOfMean = -1, - MeanValues = [], - Variances = [], - Qualities = [], - MeanQuality = -1 + // Обновляем данные в интерфейсе + // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан + _ = Dispatcher.UIThread.InvokeAsync(() => + { + // Если запись является SeparatorRecord + if (ea.Entry.Entity is SeparatorRecord separatorRecord) + { + // Если запись добавлена + if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged) + { + // Добавляем запись + SeparatorComboBoxItems.Add(new SeparatorComboBoxItemViewModel(separatorRecord.Id, separatorRecord.Type)); + } + // Если запись удалена + else if (ea.NewState is EntityState.Deleted) + { + // Ищем удалённую запись в выпадающем списке сепараторов + var deletedComboBoxItem = SeparatorComboBoxItems.FirstOrDefault(r => r.Id == separatorRecord.Id); + // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку + if (deletedComboBoxItem is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems)); + return; + } + // Берём индекс удалённого элемента в выпадающем списке марок + var deletedComboBoxIndex = SeparatorComboBoxItems.IndexOf(deletedComboBoxItem); + // Если текущий индекс равен удаляемому, то устанавливаем, что элемент не выбран + if (SeparatorComboBoxSelectedIndex == deletedComboBoxIndex) + SeparatorComboBoxSelectedIndex = -1; + // Удаляем элемент из выпадающего списка марок + SeparatorComboBoxItems.RemoveAt(deletedComboBoxIndex); + } + // Если запись изменилась + else if (ea.NewState is EntityState.Modified) + { + // Ищем изменённую запись в выпадающем списке марок + var editedComboBoxItem = SeparatorComboBoxItems.FirstOrDefault(r => r.Id == separatorRecord.Id); + // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку + if (editedComboBoxItem is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", separatorRecord.Id, nameof(SeparatorComboBoxItems)); + return; + } + // Обновляем элемент из выпадающего списка марок + editedComboBoxItem.Type = separatorRecord.Type; + } + } + // Если запись является BrandRecord + if (ea.Entry.Entity is BrandRecord brandRecord) + { + // Если запись добавлена + if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged) + { + // Добавляем запись + BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName)); + } + // Если запись удалена + else if (ea.NewState is EntityState.Deleted) + { + // Ищем удалённую запись в выпадающем списке марок + var deletedComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id); + // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку + if (deletedComboBoxItem is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems)); + return; + } + // Берём индекс удалённого элемента в выпадающем списке марок + var deletedComboBoxIndex = BrandComboBoxItems.IndexOf(deletedComboBoxItem); + // Если текущий индекс равен удаляемому, то устанавливаем, что элемент не выбран + if (BrandComboBoxSelectedIndex == deletedComboBoxIndex) + BrandComboBoxSelectedIndex = -1; + // Удаляем элемент из выпадающего списка марок + BrandComboBoxItems.RemoveAt(deletedComboBoxIndex); + } + // Если запись изменилась + else if (ea.NewState is EntityState.Modified) + { + // Ищем изменённую запись в выпадающем списке марок + var editedComboBoxItem = BrandComboBoxItems.FirstOrDefault(r => r.Id == brandRecord.Id); + // Если запись не найдена в выпадающем списке марок, то логгируем как ошибку + if (editedComboBoxItem is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в списке {}", brandRecord.Id, nameof(BrandComboBoxItems)); + return; + } + // Обновляем элемент из выпадающего списка марок + editedComboBoxItem.BrandName = brandRecord.BrandName; + editedComboBoxItem.MixtureName = brandRecord.MixtureName; + } + } + }); + }; + + // Получаем записи меню из базы данных + MenuItems = new ObservableCollection(_context.ResultRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName))); + if (MenuItems.Count() > 0) // Если записи есть, + MenuSelectedIndex = 0; // Выбираем первую запись + else // Если записей нет, + MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана + + // Подписываемся на изменения свойств для обновления CanScroll + PropertyChanged += (_, ea) => + { + if (ea.PropertyName == nameof(MenuOpened) || + ea.PropertyName == nameof(MainSectionIsScrolling) || + ea.PropertyName == nameof(InfoSectionIsScrolling)) + UpdateCanInteract(); }; _logger.LogInformation("Инициализировано"); } /// - /// Вызывается при изменении .
- /// Копирует в или издаёт пустую . + /// Вызывается при изменении ///
- partial void OnSelectedRecordChanged(ResultRecord? value) + partial void OnMenuSelectedIndexChanged(int value) { - // Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись) - // то создаём пустую запись - if (SelectedRecord is null) + // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку + if (value >= MenuItems.Count()) + _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count()); + // Если значение больше или равно 0, то значит выбран один из элементов меню + if (value >= 0 && value < MenuItems.Count()) { - EditingRecord = new() + _logger.LogTrace("Выбран элемент из списка: {}", value); + // Находим запись в базе данных + var selectedRecord = _context.ResultRecords.Find(MenuItems[value].Id); + // Если запись найдена, то заполняем поля из базы данных + if (selectedRecord is not null) { - SampleName = "", - SampleComment = "", - SamplingDateTime = DateTime.UtcNow, - SamplingPlace = "", - Image = "", - SeparatorType = "", - SampleBrand = "", - MaskImage = "", - ResultImage = "", - MinValue = -1, - MaxValue = -1, - ParticlesCount = -1, - MeanValue = -1, - MeanVariance = -1, - VarianceOfMean = -1, - MeanValues = [], - Variances = [], - Qualities = [], - MeanQuality = -1 - }; + // Заполняем простые поля + 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.SeparatorType = selectedRecord.SeparatorType; + Record.BrandName = selectedRecord.BrandName; + Record.MixtureName = selectedRecord.MixtureName; + Record.MixtureNormalRate = selectedRecord.MixtureNormalRate; + Record.ParticlesCount = selectedRecord.ParticlesCount; + Record.Values = selectedRecord.ValueData.Chunk(selectedRecord.ValueCount).Select(v => v.ToArray()).ToList(); + Record.Xs = selectedRecord.XData.Chunk(selectedRecord.XCount).Select(v => v.ToArray()).ToList(); + Record.Ys = selectedRecord.YData.Chunk(selectedRecord.YCount).Select(v => v.ToArray()).ToList(); + + // Ищем изображение в базе данных + var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (imageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение + Record.Image.Data = imageRecord?.Base64 ?? ""; + + // Ищем изображение маски в базе данных + var maskImageRecord = _context.ImageRecords.Find(selectedRecord.MaskImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (maskImageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.MaskImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение + Record.MaskImage.Data = maskImageRecord?.Base64 ?? ""; + + // Ищем изображение результата в базе данных + var resultImageRecord = _context.ImageRecords.Find(selectedRecord.ResultImageId); + // Если изображение не найдено в базе данных, то логгируем как ошибку + if (resultImageRecord is null) + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ResultImageId, nameof(_context.ImageRecords)); + // В любом случае заполняем изображение + Record.MaskImage.Data = resultImageRecord?.Base64 ?? ""; + + // Сбрасываем приближение изображений + ImagesPanX = 0; + ImagesPanY = 0; + ImagesZoom = 1; + + // Устанавливаем, что запись выбрана из базы данных + IsNewRecord = false; + // Вычисляем дополнительные свойства + Record.CalculateProperties(); + + return; + } + // Если значение не найдено в базе данных + else + { + // Логгируем как ошибку + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords)); + var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс + // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями + MenuSelectedIndex = -1; + // Удаляем несуществующий элемент из списка + MenuItems.RemoveAt(nonExistingIndex); + return; + } } + + // Если элемент не выбран, заполняем поля пустыми значениями + _logger.LogTrace("Выбран новый элемент"); + var dateTime = DateTime.UtcNow; + Record.Id = 0; + Record.Uuid = Guid.NewGuid().ToString(); + Record.CreateDateTime = dateTime; + Record.EditDateTime = dateTime; + Record.SampleName = ""; + Record.SampleComment = ""; + Record.SamplingDateTime = dateTime; + Record.SamplingPlace = ""; + Record.SeparatorType = ""; + Record.BrandName = ""; + Record.MixtureName = ""; + Record.MixtureNormalRate = -1; + Record.Image.Data = ""; + Record.MaskImage.Data = ""; + Record.MaskImage.Data = ""; + Record.Values = new List(); + Record.Xs = new List(); + Record.Ys = new List(); + + // Сбрасываем приближение изображений + ImagesPanX = 0; + ImagesPanY = 0; + ImagesZoom = 1; + + // Устанавливаем, что запись не выбрана из базы данных + IsNewRecord = true; + // Вычисляем дополнительные свойства + Record.CalculateProperties(); + } + + /// + /// Обновить значение + /// + private void UpdateCanInteract() + { + CanInteract = !MenuOpened; + CanInteract &= !MainSectionIsScrolling; + CanInteract &= !InfoSectionIsScrolling; + } + + // Команды управления меню + /// + /// Открыть меню + /// + [RelayCommand] private void OpenMenu() => MenuOpened = true; + /// + /// Закрыть меню + /// + [RelayCommand] private void CloseMenu() => MenuOpened = false; + /// + /// Переключить видимость меню + /// + [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; + + /// + /// Переключить главную секцию информация <-> изображения + /// + [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; + /// + /// Переключить информационную секцию на следующую + /// + [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--; + /// + /// Переключить информационную секцию на предыдущую + /// + [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++; + + /// + /// Переключить на изображение + /// + [RelayCommand] + private void SwitchToImage() + { + ImageIsVisible = true; + MaskImageIsVisible = false; + ResultImageIsVisible = false; + } + /// + /// Переключить на изображение + /// + [RelayCommand] + private void SwitchToMaskImage() + { + ImageIsVisible = false; + MaskImageIsVisible = true; + ResultImageIsVisible = false; + } + /// + /// Переключить на изображение + /// + [RelayCommand] + private void SwitchToResultImage() + { + ImageIsVisible = false; + MaskImageIsVisible = false; + ResultImageIsVisible = true; + } + + /// + /// Создать новую запись + /// + [RelayCommand] + private void CreateRecord() + { + _logger.LogInformation("Создание новой записи"); + // Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и + // при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется, + // потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1, + // поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged + if (MenuSelectedIndex != -1) + MenuSelectedIndex = -1; else + OnMenuSelectedIndexChanged(-1); + } + + /// + /// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана + /// + [RelayCommand] + private void DeleteRecord() + { + try { - // Иначе копируем запись, отвязывая её от базы данных - EditingRecord = CopyRecord(SelectedRecord); + _logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(ResultRecord.Id), Record.Id, nameof(ResultRecord.SampleName), Record.SampleName); + // Если запись не выбрана, то просто создаём новую запись + if (MenuSelectedIndex < 0) + { + _logger.LogTrace("Запись не выбрана, создаём новую"); + CreateRecord(); + return; + } + + // Создаём модель для экрана подтверждения + var confirmViewModel = new DeleteConfirmViewModel(_navigation) + { + Question = "Вы уверены, что хотите удалить запись?" + }; + + // Создаём коллбек который вызовется при экрана подтверждения + void OnConfirmCloses(object? sender, bool result) + { + // Если пользователь отменил удаление, то выходим из коллбека + if (!result) + { + _logger.LogTrace("Пользователь отменил удаление"); + return; + } + + // Если запись не выбрана, то просто создаём новую запись + if (MenuSelectedIndex < 0) + { + _logger.LogTrace("Запись не выбрана, создаём новую"); + CreateRecord(); + return; + } + + // Сохраняем индекс удалённый записи и находим её в базе данных + var deletedMenuIndex = MenuSelectedIndex; + var deletedRecordId = Record.Id; + var deletedRecord = _context.ResultRecords.Find(deletedRecordId); + // Создаём новую запись + CreateRecord(); + + // Удаляем запись из списка + MenuItems.RemoveAt(deletedMenuIndex); + // Если удалённая запись не найдена в базе данных, то логгируем как ошибку + if (deletedRecord is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.ResultRecords)); + return; + } + + // Удаляем запись из базы данных и сохраняем изменения в базе данных + _context.ResultRecords.Remove(deletedRecord); + _context.SaveChanges(); + _logger.LogTrace("Запись удалена"); + + // Отписываемся от события закрытия экрана подтверждения + confirmViewModel.Closes -= OnConfirmCloses; + } + + // Создаём View экрана подтверждения + var confirmView = _viewLocator.Build(confirmViewModel); + // Если View не найден, то логгируем как ошибку + if (confirmView is null) + { + _logger.LogError("Не удалось найти View для экрана подтверждения"); + return; + } + + _logger.LogTrace("Задаём вопрос"); + // Подписываемся на событие закрытия экрана подтверждения + confirmViewModel.Closes += OnConfirmCloses; + // Преходим на экран подтверждения, при закрытии он сам вернёт экран обратно на текущий + _navigation.NavigateTo(confirmView); + } + catch (Exception ex) + { + _logger.LogError(ex, "Исключение при удалении записи"); } } /// - /// Вызывается при изменении .
- /// Изменяет изображением из . + /// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи ///
- partial void OnEditingRecordChanged(ResultRecord value) - { - var avgImagePath = _imageStorage.GetFullPath(value.Image, "result-avg"); - if (File.Exists(avgImagePath)) - { - if (Image is null) - Image = new AsyncImageRecordViewModel(avgImagePath); - else - Image.Path = avgImagePath; - } - else - Image = null; - - var maskImagePath = _imageStorage.GetFullPath(value.MaskImage, "result-mask"); - if (File.Exists(maskImagePath)) - { - if (MaskImage is null) - MaskImage = new AsyncImageRecordViewModel(maskImagePath); - else - MaskImage.Path = maskImagePath; - } - else - MaskImage = null; - - var resultImagePath = _imageStorage.GetFullPath(value.ResultImage, "result"); - if (File.Exists(resultImagePath)) - { - if (ResultImage is null) - ResultImage = new AsyncImageRecordViewModel(resultImagePath); - else - ResultImage.Path = resultImagePath; - } - else - ResultImage = null; - - try - { - (MeanValuesHistogram, MeanValuesXAxis, MeanValuesYAxis) = BuildHistogram( - value.MeanValues, - 0, - Math.Max(value.MeanValues.Count() > 0 ? value.MeanValues.Max() : 0, 1), - leftText: "Количество частиц", - bottomText: "Среднее знечение" - ); - (VariancesHistogram, VariancesXAxis, VariancesYAxis) = BuildHistogram( - value.Variances, - 0, - Math.Max(value.Variances.Count() > 0 ? value.Variances.Max() : 0, 1), - leftText: "Количество частиц", - bottomText: "Дисперсия" - ); - (QualitiesHistogram, QualitiesXAxis, QualitiesYAxis) = BuildHistogram( - value.Qualities ?? [], - 0, - 100, - leftText: "Количество частиц", - bottomText: "Качество обработки" - ); - } - catch (Exception ex) - { - _logger.LogError(ex, ""); - } - - } - - // Просто команды управления меню - [RelayCommand] private void OpenMenu() => MenuOpened = true; - [RelayCommand] private void CloseMenu() => MenuOpened = false; - [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; - - // Команда создания новой записи - // вызывает цепочку действий по изменению EditingRecord и Images - [RelayCommand] - private void CreateRecord() - { - // Если уже создана новая запись, то SelectedRecord == null и - // при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется, - // потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null, - // поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged - if (SelectedRecord is not null) - SelectedRecord = null; - else - OnSelectedRecordChanged(null); - } - - // Команда удаления записи, создаёт новую запись и удаляет старую из списка и базы данных - [RelayCommand] - private void DeleteRecord() - { - var confirmViewModel = new DeleteConfirmViewModel(_navigation) - { - Question = "Вы уверены, что хотиите удаить запись?" - }; - if (SelectedRecord is null) - { - CreateRecord(); - return; - } - confirmViewModel.Closes += (_, ea) => - { - if (ea != true) - return; - - // Сохраняем текущую запись - var deletedRecord = SelectedRecord; - // Создаём новую запись - CreateRecord(); - - // Если запись на была сохранена в базу данных, то ничего не делаем - if (deletedRecord is null) - return; - - // Если запись была в без данных, то - // она была и в Records, удаляем её из Records - Records.Remove(deletedRecord); - // Удаляем запись из базы данных и сохраняем изменения - // TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления, - // но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске, - // тогда удалённые записи будут заполнять диск - // TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища - _context.ResultRecords.Remove(deletedRecord); - _context.SaveChanges(); - }; - - var convfirmView = _viewLocator.Build(confirmViewModel); - if (convfirmView is not null) - _navigation.NavigateTo(convfirmView); - } - - // // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной [RelayCommand] private void SaveRecord() { - // Если это новая запись - if (SelectedRecord is null) + try { - // Если это новая запись, то копируем её - var newRecord = CopyRecord(EditingRecord); - - // Если записи нет в базе данных - if (_context.Entry(newRecord).State is EntityState.Detached) + _logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(ResultRecord.Id), Record.Id, nameof(ResultRecord.SampleName), Record.SampleName); + // Если запись не выбрана + if (MenuSelectedIndex < 0) { - // То добавляем её в базу + // Создаём запись изображения + var imageId = 0; + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: Record.Image.Data); + // Добавляем запись изображения в базу данных + var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + imageId = attachedImageRecord.Entity.Id; + } + + // Создаём запись маски + var maskImageId = 0; + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: Record.MaskImage.Data); + // Добавляем запись изображения в базу данных + var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + maskImageId = attachedImageRecord.Entity.Id; + } + + // Создаём запись изображения результата + var resultImageId = 0; + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: Record.ResultImage.Data); + // Добавляем запись изображения в базу данных + var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + resultImageId = attachedImageRecord.Entity.Id; + } + + + _logger.LogTrace("Создаём новую запись"); + var dateTime = DateTime.UtcNow; + // Заполняем запись + var newRecord = new ResultRecord( + uuid: Record.Uuid, + createDateTime: dateTime, + editDateTime: dateTime, + sampleName: Record.SampleName, + sampleComment: Record.SampleComment, + samplingDateTime: Record.SamplingDateTime, + samplingPlace: Record.SamplingPlace, + separatorType: Record.SeparatorType, + brandName: Record.BrandName, + mixtureName: Record.MixtureName, + mixtureNormalRate: Record.MixtureNormalRate, + imageId: imageId, + maskImageId: maskImageId, + resultImageId: resultImageId, + particlesCount: Record.ParticlesCount, + values: Record.Values, + xs: Record.Xs, + ys: Record.Ys + ); + // Добавляем запись в базу данных var attachedRecord = _context.ResultRecords.Add(newRecord); - // Заменяем текущую запись на привязанную к базе данных - newRecord = attachedRecord.Entity; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись сохранена"); + + // Добавляем новый элемент в меню + MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName)); + // Выбираем последний элемент из меню + MenuSelectedIndex = MenuItems.Count() - 1; } - - // Сохраняем изменения в базе данных - _context.SaveChanges(); - // Добавляем запись в список - Records.Add(newRecord); - // Выбираем запись - SelectedRecord = newRecord; - } - // Если эта запись есть в базе - else - { - // Переносим данные из редактируемой записи в выбранную - SelectedRecord.SampleName = EditingRecord.SampleName; - SelectedRecord.SampleComment = EditingRecord.SampleComment; - SelectedRecord.SamplingDateTime = EditingRecord.SamplingDateTime; - SelectedRecord.SamplingPlace = EditingRecord.SamplingPlace; - SelectedRecord.Image = EditingRecord.Image; - SelectedRecord.SeparatorType = EditingRecord.SeparatorType; - SelectedRecord.SampleBrand = EditingRecord.SampleBrand; - SelectedRecord.MaskImage = EditingRecord.MaskImage; - SelectedRecord.ResultImage = EditingRecord.ResultImage; - SelectedRecord.MeanValue = EditingRecord.MeanValue; - SelectedRecord.MeanVariance = EditingRecord.MeanVariance; - SelectedRecord.VarianceOfMean = EditingRecord.VarianceOfMean; - SelectedRecord.Qualities = EditingRecord.Qualities; - SelectedRecord.MeanQuality = EditingRecord.MeanQuality; - - // Если записи нет в базе данных - if (_context.Entry(SelectedRecord).State is EntityState.Detached) + // Если запись выбрана + else { - // То добавляем её в базу - var attachedRecord = _context.ResultRecords.Add(SelectedRecord); - // Находим текущую запись в списке - var index = Records.IndexOf(SelectedRecord); - // Заменяем её в списке на привязанную к базе данных - Records[index] = attachedRecord.Entity; - // Заменяем выбранную запись на привязанную - SelectedRecord = attachedRecord.Entity; - } + // Находим запись в базе данных + var attachedRecord = _context.ResultRecords.Find(Record.Id); + // Если запись не найдена, то логгируем как ошибку + if (attachedRecord is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.ResultRecords)); + return; + } - // Сохраняем изменения в базе данных - _context.SaveChanges(); + _logger.LogTrace("Записываем изменения в базу данных"); + + // Обновляем изображение + { + // Ищем запись изображения в базе данных + var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId); + // Если запись изображения не найдена + if (attachedImageRecord is null) + { + // Если изображение есть + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: Record.Image.Data); + // Добавляем запись изображения в базу данных + attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + attachedRecord.ImageId = attachedImageRecord.Id; + } + } + // Если запись изображения найдена + else + { + // Если изображение есть + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Записываем изменения в запись изображения"); + // Вносим изменения + attachedImageRecord.Base64 = Record.Image.Data; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + } + // Если изображения нет + else + { + // Удаляем изображение из базы данных + _context.ImageRecords.Remove(attachedImageRecord); + // Сохраняем изменения в базе данных + _context.SaveChanges(); + attachedRecord.ImageId = 0; + } + } + } + // Обновляем маску + { + // Ищем запись маски в базе данных + var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.MaskImageId); + // Если запись изображения не найдена + if (attachedImageRecord is null) + { + // Если изображение есть + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: Record.Image.Data); + // Добавляем запись изображения в базу данных + attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + attachedRecord.MaskImageId = 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.MaskImageId = 0; + } + } + } + // Обновляем изображение результата + { + // Ищем запись маски в базе данных + var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ResultImageId); + // Если запись изображения не найдена + if (attachedImageRecord is null) + { + // Если изображение есть + if (!string.IsNullOrEmpty(Record.Image.Data)) + { + _logger.LogTrace("Создаём новую запись изображения"); + // Заполняем запись изображения + var imageRecord = new ImageRecord(base64: Record.Image.Data); + // Добавляем запись изображения в базу данных + attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity; + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись изображения сохранена"); + attachedRecord.ResultImageId = 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.ResultImageId = 0; + } + } + } + + // Определяем Id выбранной марки + var selectedBrandId = 0; + // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку + if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count()) + _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count()); + // Если значение больше 0, то достаём Id из списка элементов + else if (BrandComboBoxSelectedIndex >= 0) + selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id; + + // Записываем изменения + attachedRecord.EditDateTime = DateTime.UtcNow; + attachedRecord.SampleName = Record.SampleName; + attachedRecord.SampleComment = Record.SampleComment; + attachedRecord.SamplingDateTime = Record.SamplingDateTime; + attachedRecord.SamplingPlace = Record.SamplingPlace; + attachedRecord.SeparatorType = Record.SeparatorType; + attachedRecord.BrandName = Record.BrandName; + attachedRecord.MixtureName = Record.MixtureName; + attachedRecord.MixtureNormalRate = Record.MixtureNormalRate; + attachedRecord.ParticlesCount = Record.ParticlesCount; + attachedRecord.ValueCount = Record.Values.Select(fs => fs.Count()).ToList(); + attachedRecord.ValueData = Record.Values.SelectMany(fs => fs).ToList(); + attachedRecord.XCount = Record.Xs.Select(fs => fs.Count()).ToList(); + attachedRecord.XData = Record.Xs.SelectMany(fs => fs).ToList(); + attachedRecord.YCount = Record.Ys.Select(fs => fs.Count()).ToList(); + attachedRecord.YData = Record.Ys.SelectMany(fs => fs).ToList(); + + // Сохраняем изменения в базе данных + _context.SaveChanges(); + _logger.LogTrace("Запись сохранена"); + + // Записываем изменения в меню + _logger.LogTrace("Записываем изменения в меню"); + MenuItems[MenuSelectedIndex].SampleName = Record.SampleName; + + // Устанавливаем, что запись выбрана из базы данных + IsNewRecord = false; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Исключение при сохранении записи"); } } - // Команда изменения основной секции (информация <-> изображения) - [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; - - // Команда навигации по информации - [RelayCommand] - private void InfoSectionPrevious() - { - InfoSectionCurrentIndex--; - UpdateCharts?.Invoke(this, new EventArgs()); - } - [RelayCommand] - private void InfoSectionNext() - { - InfoSectionCurrentIndex++; - UpdateCharts?.Invoke(this, new EventArgs()); - } - - // Команда съёмки изображений + /// + /// Снять изображения + /// [RelayCommand] private void CaptureImage() { - // Если съёмка уже начата, то ничего не делаем - if (CaptureImagesTask is not null) + // Если съёмка уже начата, то логгируем как ошибку + if (_сaptureImagesTask is not null) + { + _logger.LogError("Задача съёмки уже запущена"); return; + } // Создаём токен отмены задачи _captureImagesTaskCts = new CancellationTokenSource(); var cancellationToken = _captureImagesTaskCts.Token; - // Обновляем данные в интерфейсе - // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, - // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан - _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}"); + ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}"; // Запускаем получение изображений асинхронно - CaptureImagesTask = Task.Run(async () => + _сaptureImagesTask = Task.Run(async () => { // Список полученных изображений var imagesData = new List>(); // Обновляем данные в интерфейсе - // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); - // Получаем изображения + // Создаём задачу получения изображений var captureTask = _imageCapturer.CaptureImages(async (data) => { // Распаковываем данные var (i, visible, uv365, uv254, image) = data; - // Выбрасываем исключение если задача отменена + // Выбрасываем исключение, если задача отменена cancellationToken.ThrowIfCancellationRequested(); // Обновляем данные в интерфейсе - // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}"); + // Создаём задачу сохранения изображения var imagesDataTask = Task.Run(() => { - // Сохраняем png изображение во временную папку - var path = Path.Join(Path.GetTempPath(), $"gss2-result-{visible}-{uv365}-{uv254}.png"); + // Выбрасываем исключение, если задача отменена + cancellationToken.ThrowIfCancellationRequested(); + // Сохраняем PNG изображение во временную папку + var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png"); image.ImWrite(path); // Освобождаем память изображения - image.Release(); + image.Dispose(); // Добавляем данные изображения в список return new ImageData(path, visible, uv365, uv254); - }); + }, cancellationToken); + // Добавляем задачу сохранения изображения в список imagesData.Add(imagesDataTask); }, cancellationToken); - // Обрабатываем изображения - var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t => + // Создаём задачу обработки изображений, + // эта задача начнётся только после того как завершится + // задача съёмки изображений и все задачи сохранения изображений + var processTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t => { // Обновляем данные в интерфейсе - // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений"); - // Проверяем исключения задачи получения изображений - if (t.Exception is not null) + + // Если во время выполнения задачи съёмки изображений произошло исключение, + // то логгируем все исключения и выходим + if (captureTask.Exception is not null) { - _logger.LogError(t.Exception, "Исключение во время получения изображений"); - foreach (var ex in t.Exception.InnerExceptions ?? []) - _logger.LogError(ex, "Исключение во время получения изображений"); + _logger.LogError(captureTask.Exception, "Исключение во время получения изображения"); + foreach (var ex in captureTask.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время получения изображения"); return; } - // Проверяем исключения задач сохранения изображений + + // Если во время выполнения задач сохранения изображений произошло исключение, + // то логгируем все исключения и выходим + bool anyImageDataException = false; foreach (var imageData in imagesData) + { if (imageData.Exception is not null) { - _logger.LogError(imageData.Exception, "Исключение во время получения изображения"); + _logger.LogError(imageData.Exception, "Исключение во время сохранения изображения"); foreach (var ex in imageData.Exception.InnerExceptions ?? []) - _logger.LogError(ex, "Исключение во время получения изображения"); - return; + _logger.LogError(ex, "Исключение во время сохранения изображения"); + anyImageDataException = true; } + } + if (anyImageDataException) + return; + + // Если во время выполнения произошло исключение, + // то логгируем все исключения и выходим + if (t.Exception is not null) + { + _logger.LogError(t.Exception, "Исключение во время обработки изображений"); + foreach (var ex in t.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время обработки изображений"); + return; + } // Инициализируем анализатор - if (!_analyzer.Initialized) + var brand = _context.BrandRecords.Find(BrandComboBoxItems[BrandComboBoxSelectedIndex].Id); + var separator = _context.SeparatorRecords.Find(SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id); + if (brand is null) { - _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей"); - await _analyzer.Initialize(); + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", BrandComboBoxItems[BrandComboBoxSelectedIndex].Id, nameof(_context.BrandRecords)); + return; } - // Инициализируем анализатор + if (separator is null) + { + _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SeparatorComboBoxItems[SeparatorComboBoxSelectedIndex].Id, nameof(_context.SeparatorRecords)); + return; + } + _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей"); + await _analyzer.Initialize(brand, separator); + + // Проверяем что анализатор инициализирован if (!_analyzer.Initialized) { _logger.LogError("Ошибка при инициализации анализатора"); return; } - // Обновляем данные в редактируемой записи - // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // Обновляем данные в интерфейсе + // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Анализ"); // Проводим анализ var result = await _analyzer.Analyze(imagesData.Select(i => i.Result)); // Если анализ проведён - if (result is not null) - { - _logger.LogInformation("Анализ завершён"); - // Распаковываем результат - var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value; - - try - { - _logger.LogInformation("Анализ завершён [2]"); - // Вычисляем минимальное и максимальное значение - OpenCvSharp.Cv2.MinMaxLoc(resultImage, out double minValue, out double maxValue, out _, out _, mask); - // Вычисляем среднее значение - var meanValue = OpenCvSharp.Cv2.Mean(resultImage, mask).Val0; - _logger.LogInformation("Миниальное значение: {}, Максимальное значние: {}", minValue, maxValue); - _logger.LogInformation("Колличество конутров: {}, Среднее значние: {}", contours.Count(), meanValue); - - _logger.LogInformation("Анализ завершён [3]"); - // Вычисляем среднее и дисперсию каждого контура - var means = new List(); - var variances = new List(); - var qualities = new List(); - - // Создаём временную маску для каждого контура - var contourMask = new OpenCvSharp.Mat( - new OpenCvSharp.Size( - AnalyzerService.ANALYSIS_IMAGE_SIZE, - AnalyzerService.ANALYSIS_IMAGE_SIZE - ), - OpenCvSharp.MatType.CV_8UC1, - new OpenCvSharp.Scalar(0) - ); - - // Проходим по каждому контуру - foreach (var contour in contours) - { - // Обнуляем маску - contourMask.SetTo(new OpenCvSharp.Scalar(0)); - // Заполняем контур на маске - OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1); - - // Вычисляем среднее и среднеквадратическое отклонение - OpenCvSharp.Cv2.MeanStdDev(resultImage, out var mean, out var stdDev, contourMask); - // Добавляем значения в лист - means.Add(mean.Val0); - variances.Add(Math.Pow(stdDev.Val0, 2)); - qualities.Add(CalculateQuality(mean.Val0, Math.Pow(stdDev.Val0, 2))); - } - contourMask.Release(); - - _logger.LogInformation("Анализ завершён [4]"); - // Вычисляем среднюю дисперсию и дисперсию среднего - double meanVariance = variances.Average(); - double varianceOfMean = means.Variance(); - _logger.LogInformation("Средняя дивергенция: {}, Дивергенция среднего: {}", meanVariance, varianceOfMean); - - _logger.LogInformation("Анализ завершён [5]"); - // Кодируем и сохраняем среднее изображение - OpenCvSharp.Cv2.DrawContours(avgImage, contours, -1, OpenCvSharp.Scalar.Red, 1, OpenCvSharp.LineTypes.Link8); - OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); - string avgPath; - using (var stream = new MemoryStream(avgData)) - avgPath = await _imageStorage.SaveAsync(stream, ".png", "result-avg"); - - // Освобождаем память среднего изображения - avgImage.Release(); - avgData = null; - - _logger.LogInformation("Анализ завершён [6]"); - // Кодируем и сохраняем маску - OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData); - string maskPath; - using (var stream = new MemoryStream(maskData)) - maskPath = await _imageStorage.SaveAsync(stream, ".png", "result-mask"); - - // Освобождаем память маски - mask.Release(); - maskData = null; - - _logger.LogInformation("Анализ завершён [7]"); - // Преобразуем изображение (0...maxValue) -> (0...255) - resultImage.ConvertTo(resultImage, OpenCvSharp.MatType.CV_8UC1, 255 / (maxValue - 0), 255 * 0 / (maxValue - 0)); - - // Кодируем и сохраняем изображение результата - OpenCvSharp.Cv2.ImEncode(".png", resultImage, out var resultData); - string resultPath; - using (var stream = new MemoryStream(resultData)) - resultPath = await _imageStorage.SaveAsync(stream, ".png", "result"); - - // Освобождаем память изображения результата - resultImage.Release(); - resultData = null; - - _logger.LogInformation("Анализ завершён [8]"); - // Заполняем поля в редактируемой записи - EditingRecord.Image = avgPath; - EditingRecord.SeparatorType = separatorType; - EditingRecord.SampleBrand = sampleBrand; - EditingRecord.MaskImage = maskPath; - EditingRecord.ResultImage = resultPath; - EditingRecord.MinValue = minValue; - EditingRecord.MaxValue = maxValue; - EditingRecord.ParticlesCount = contours.Count(); - EditingRecord.MeanValue = meanValue; - EditingRecord.MeanVariance = meanVariance; - EditingRecord.VarianceOfMean = varianceOfMean; - EditingRecord.MeanValues = means.ToArray(); - EditingRecord.Variances = variances.ToArray(); - EditingRecord.MeanQuality = qualities.Average(); - EditingRecord.Qualities = qualities.ToArray(); - - // Обновляем данные в редактируемой записи - // Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, - // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан - _ = Dispatcher.UIThread.InvokeAsync(() => EditingRecord = CopyRecord(EditingRecord)); - - // Если ошибок не было, то мигаем светодиодами зелёным - _ = Task.Run(async () => - { - await Task.Delay(1000); - _lights.Flash(1, 1, System.Drawing.Color.Green); - await Task.Delay(3000); - _lights.Disconnect(); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Исключение во время анализа"); - // Если была ошибка, то мигаем светодиодами красным - _ = Task.Run(async () => - { - await Task.Delay(1000); - _lights.Flash(1, 1, System.Drawing.Color.Red); - await Task.Delay(3000); - _lights.Disconnect(); - }); - } - - } - else + if (result is null) { _logger.LogError("Ошибка при выполнении анализа"); // Если была ошибка, то мигаем светодиодами красным @@ -651,203 +1663,252 @@ public partial class AnalysisViewModel : ViewModelBase _lights.Disconnect(); }); } + + // Распаковываем результат + var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value; + + try + { + // Кодируем среднее изображение + OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); + Record.Image.Data = Convert.ToBase64String(avgData); + // Освобождаем память среднего изображения + avgImage.Dispose(); + + // Создаём маску контура + var contourMask = new OpenCvSharp.Mat( + new OpenCvSharp.Size( + AnalyzerService.ANALYSIS_IMAGE_SIZE, + AnalyzerService.ANALYSIS_IMAGE_SIZE + ), + OpenCvSharp.MatType.CV_8UC1, + new OpenCvSharp.Scalar(0) + ); + + Record.Values = new List(); + // Проходим по каждому контуру + foreach (var contour in contours) + { + // Обнуляем маску контура + contourMask.SetTo(new OpenCvSharp.Scalar(0)); + // Заполняем контур на маске + OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1); + // Чуть-чуть обрезаем его по краям + OpenCvSharp.Cv2.Erode(contourMask, contourMask, OpenCvSharp.Cv2.GetStructuringElement(OpenCvSharp.MorphShapes.Ellipse, new OpenCvSharp.Size(5, 5))); + // Находим прямоугольник ограничивающий конур + var bbox = OpenCvSharp.Cv2.BoundingRect(contour); + // Проходимся по каждому пикселю в прямоугольнике и если он попадает в маску, то сохраняем значение результата + var contourValues = new List(); + for (int i = bbox.Top; i < bbox.Bottom; i++) + for (int j = bbox.Left; j < bbox.Right; j++) + if (contourMask.Get(i, j) > 127) + contourValues.Add(resultImage.Get(i, j)); + // Сохраняем полученные значения + Record.Values.Add(contourValues.ToArray()); + } + // Освобождаем память маски контура + contourMask.Dispose(); + + // Преобразуем конура в массивы координат + Record.Xs = contours.Select(c => c.Select(p => (float)p.X).ToArray()).ToList(); + Record.Ys = contours.Select(c => c.Select(p => (float)p.Y).ToArray()).ToList(); + + // Вычисляем пороговые значения + var unprocessedThreshold = RecordViewModel.UNPROCESSED_THRESHOLD * Record.MixtureNormalRate; + var hotspotsThreshold = RecordViewModel.HOTSPOT_THRESHOLD * Record.MixtureNormalRate; + + OpenCvSharp.Cv2.MinMaxLoc(resultImage, out _, out double max); + if (max <= hotspotsThreshold) + max = hotspotsThreshold + 0.001; + var lutIndices = new OpenCvSharp.Mat(resultImage.Size(), OpenCvSharp.MatType.CV_8UC1, OpenCvSharp.Scalar.All(0)); + + using (var mask1 = new OpenCvSharp.Mat()) + { + OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(0), OpenCvSharp.Scalar.All(unprocessedThreshold), mask1); + + var scale1 = 85.0 / unprocessedThreshold; + + using (var res1 = new OpenCvSharp.Mat()) + { + resultImage.ConvertTo(res1, OpenCvSharp.MatType.CV_8UC1, scale1); + res1.CopyTo(lutIndices, mask1); + } + } + using (var mask2 = new OpenCvSharp.Mat()) + { + OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(unprocessedThreshold), OpenCvSharp.Scalar.All(hotspotsThreshold), mask2); + + double scale2 = 85.0 / (hotspotsThreshold - unprocessedThreshold); + double shift2 = -unprocessedThreshold * scale2 + 85.0; + + using (var res2 = new OpenCvSharp.Mat()) + { + resultImage.ConvertTo(res2, OpenCvSharp.MatType.CV_8UC1, scale2, shift2); + res2.CopyTo(lutIndices, mask2); + } + } + using (var mask3 = new OpenCvSharp.Mat()) + { + OpenCvSharp.Cv2.InRange(resultImage, OpenCvSharp.Scalar.All(hotspotsThreshold), OpenCvSharp.Scalar.All(max), mask3); + + double scale3 = 85.0 / (max - hotspotsThreshold); + double shift3 = -hotspotsThreshold * scale3 + 170.0; + + using (var res3 = new OpenCvSharp.Mat()) + { + resultImage.ConvertTo(res3, OpenCvSharp.MatType.CV_8UC1, scale3, shift3); + res3.CopyTo(lutIndices, mask3); + } + } + + // Создаём и применяем LUT + var lut = CreateLut(); + var coloredResult = new OpenCvSharp.Mat(); + OpenCvSharp.Cv2.LUT(lutIndices, lut, coloredResult); + // Устанавливаем в 0 всё что не попадает в маску + var invMask = new OpenCvSharp.Mat(mask.Size(), mask.Type(), OpenCvSharp.Scalar.All(0)); + OpenCvSharp.Cv2.BitwiseNot(mask, invMask); + coloredResult.SetTo(OpenCvSharp.Scalar.All(0), invMask); + + // Освобождаем память, оригинальное изображение результата можно удалить + lutIndices.Dispose(); + lut.Dispose(); + invMask.Dispose(); + resultImage.Dispose(); + + // Кодируем маску + OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData); + Record.MaskImage.Data = Convert.ToBase64String(maskData); + // Освобождаем память маски + mask.Release(); + + // Кодируем изображение результата + OpenCvSharp.Cv2.ImEncode(".png", coloredResult, out var resultData); + Record.ResultImage.Data = Convert.ToBase64String(resultData); + // Освобождаем память изображения результата + coloredResult.Release(); + + // Пересчитываем значения свойств + Record.CalculateProperties(); + + // Если ошибок не было, то мигаем светодиодами зелёным + _ = Task.Run(async () => + { + await Task.Delay(1000); + _lights.Flash(1, 1, System.Drawing.Color.Green); + await Task.Delay(3000); + _lights.Disconnect(); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Исключение во время анализа"); + // Если была ошибка, то мигаем светодиодами красным + _ = Task.Run(async () => + { + await Task.Delay(1000); + _lights.Flash(1, 1, System.Drawing.Color.Red); + await Task.Delay(3000); + _lights.Disconnect(); + }); + } + }, cancellationToken); - // Сбрасываем состояние задачи съёмки изображений - _ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t => + // Создаём задачу которая выполнится после завершения всех других + _ = Task.WhenAll([captureTask, .. imagesData, processTask]).ContinueWith(t => { + // Если во время выполнения задачи съёмки изображений произошло исключение, + // то логгируем все исключения и выходим + if (captureTask.Exception is not null) + { + _logger.LogError(captureTask.Exception, "Исключение во время получения изображения"); + foreach (var ex in captureTask.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время получения изображения"); + return; + } + + // Если во время выполнения задач сохранения изображений произошло исключение, + // то логгируем все исключения и выходим + bool anyImageDataException = false; + foreach (var imageData in imagesData) + { + if (imageData.Exception is not null) + { + _logger.LogError(imageData.Exception, "Исключение во время сохранения изображения"); + foreach (var ex in imageData.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время сохранения изображения"); + anyImageDataException = true; + } + } + if (anyImageDataException) + return; + + // Если во время выполнения задачи обработки изображений произошло исключение, + // то логгируем все исключения и выходим + if (processTask.Exception is not null) + { + _logger.LogError(processTask.Exception, "Исключение во время обработки изображений"); + foreach (var ex in processTask.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время обработки изображений"); + return; + } + + // Если во время выполнения произошло исключение, + // то логгируем все исключения и выходим + if (t.Exception is not null) + { + _logger.LogError(t.Exception, "Исключение во время обработки изображений"); + foreach (var ex in t.Exception.InnerExceptions ?? []) + _logger.LogError(ex, "Исключение во время обработки изображений"); + return; + } + // Обновляем данные в интерфейсе - // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, + // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача, // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан _ = Dispatcher.UIThread.InvokeAsync(() => { + // Сбрасываем состояние задачи съёмки изображений ImagesIsCapturing = false; ImagesCapturingProgress = ""; - CaptureImagesTask = null; + _сaptureImagesTask = null; }); }); }); } - // Команда отмены задачи съёмки изображений + /// + /// Отменить задачу съёмки изображения + /// [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); - // Команды переключения изображений - [RelayCommand] - private void SwitchToImage() + private static OpenCvSharp.Mat CreateLut() { - ImageIsVisible = true; - MaskImageIsVisible = false; - ResultImageIsVisible = false; - } - [RelayCommand] - private void SwitchToMaskImage() - { - ImageIsVisible = false; - MaskImageIsVisible = true; - ResultImageIsVisible = false; - } - [RelayCommand] - private void SwitchToResultImage() - { - ImageIsVisible = false; - MaskImageIsVisible = false; - ResultImageIsVisible = true; - } + var lut = new OpenCvSharp.Mat(256, 1, OpenCvSharp.MatType.CV_8UC3); + var indexer = lut.GetGenericIndexer(); - /// - /// Копирует , отвязывая её от базы данных - /// - /// Скопированная - private static ResultRecord CopyRecord(ResultRecord source) - { - return new ResultRecord + for (int i = 0; i <= 85; i++) { - Id = source.Id, - SampleName = source.SampleName, - SampleComment = source.SampleComment, - SamplingDateTime = source.SamplingDateTime, - SamplingPlace = source.SamplingPlace, - Image = source.Image, - SeparatorType = source.SeparatorType, - SampleBrand = source.SampleBrand, - MaskImage = source.MaskImage, - ResultImage = source.ResultImage, - MinValue = source.MinValue, - MaxValue = source.MaxValue, - ParticlesCount = source.ParticlesCount, - MeanValue = source.MeanValue, - MeanVariance = source.MeanVariance, - VarianceOfMean = source.VarianceOfMean, - MeanValues = source.MeanValues, - Variances = source.Variances, - Qualities = source.Qualities ?? - Enumerable.Zip(source.MeanValues, source.Variances) - .Select((pair) => CalculateQuality(pair.First, pair.Second)) - .ToArray(), - MeanQuality = source.MeanQuality ?? - Enumerable.Zip(source.MeanValues, source.Variances) - .Select((pair) => CalculateQuality(pair.First, pair.Second)) - .Average(), - }; - } - - private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) EmptyHistogram() - { - return (Array.Empty(), [new Axis()], [new Axis()]); - } - private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") - { - if (values == null || values.Count() == 0) - return EmptyHistogram(); - - var vals = values.Select(v => (double)v).ToArray(); - double minVal = min ?? vals.Min(); - double maxVal = max ?? vals.Max(); - int binCount = 20; - - double binSize = (maxVal - minVal) / binCount; - var binsPDF = new int[binCount]; - var binsCDF = new float[binCount]; - - foreach (var v in vals) - { - int index = (int)((v - minVal) / binSize); - if (index >= binCount) - index = binCount - 1; - if (index < 0) - index = 0; - binsPDF[index]++; - for (int i = index; i < binCount; i++) - binsCDF[i] += 100.0f / values.Count(); + var val = (byte)Math.Round(i * (255.0 / 85.0)); + indexer[i, 0] = new OpenCvSharp.Vec3b(val, val, val); } - // Подписи для оси X - var labels = new string[binCount]; - for (int i = 0; i < binCount; i++) + for (int i = 86; i <= 170; i++) { - double from = minVal + i * binSize; - double to = from + binSize; - labels[i] = $"{from:F2}"; + var factor = (i - 85) / 85.0; // от 0 до 1 + var rbVal = (byte)Math.Round(255 * (1.0 - factor)); + indexer[i, 0] = new OpenCvSharp.Vec3b(rbVal, 255, rbVal); } - return - ( - new ISeries[] - { - new ColumnSeries - { - Values = binsPDF, - Name = barLegend, - Fill = new SolidColorPaint(SKColors.White), - ScalesYAt = 0 - }, - new LineSeries - { - Values = binsCDF, - Name = lineLegend, - Fill = new SolidColorPaint(SKColors.Transparent), - Stroke = new SolidColorPaint(SKColors.Red), - GeometryFill = new SolidColorPaint(SKColors.Red), - GeometryStroke = new SolidColorPaint(SKColors.Red), - GeometrySize = 8, - ScalesYAt = 1 - } - }, - new Axis[] - { - new Axis - { - Labels = labels, - LabelsRotation = 45, - TextSize = 14, - Name = bottomText, - NameTextSize = 14, - NamePaint=new SolidColorPaint(SKColors.White), - SeparatorsPaint = new SolidColorPaint(SKColors.White), - LabelsPaint = new SolidColorPaint(SKColors.White), - TicksPaint = new SolidColorPaint(SKColors.White), - SubticksPaint = new SolidColorPaint(SKColors.White) - } - }, - new Axis[] - { - new Axis - { - TextSize = 14, - MinLimit = 0, - Name=leftText, - NameTextSize = 14, - NamePaint=new SolidColorPaint(SKColors.White), - Position = LiveChartsCore.Measure.AxisPosition.Start, - SeparatorsPaint = new SolidColorPaint(SKColors.White), - LabelsPaint = new SolidColorPaint(SKColors.White), - TicksPaint = new SolidColorPaint(SKColors.White), - SubticksPaint = new SolidColorPaint(SKColors.White) - }, - new Axis - { - TextSize = 14, - MinLimit = 0f, - MaxLimit = 100f, - Name=rightText, - NameTextSize = 14, - NamePaint=new SolidColorPaint(SKColors.White), - Position = LiveChartsCore.Measure.AxisPosition.End, - SeparatorsPaint = new SolidColorPaint(SKColors.White) - { - PathEffect = new DashEffect([4, 4]) - }, - LabelsPaint = new SolidColorPaint(SKColors.White), - TicksPaint = new SolidColorPaint(SKColors.White), - SubticksPaint = new SolidColorPaint(SKColors.White) - } - } - ); - } - private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") => - BuildHistogram(values.Select(v => (float)v), min, max, leftText, rightText, bottomText, barLegend, lineLegend); + for (int i = 171; i <= 255; i++) + { + var factor = (i - 170) / 85.0; // от 0 до 1 + var gVal = (byte)Math.Round(255 * (1.0 - factor)); + var rVal = (byte)Math.Round(255 * factor); + indexer[i, 0] = new OpenCvSharp.Vec3b(0, gVal, rVal); // BGR + } - private static double CalculateQuality(double mean, double variance) => - 100.0 * Math.Exp(-1 * (QUALITY_ALPHA * Math.Max(0, QUALITY_MIN_MEAN - mean) + QUALITY_BETA * Math.Pow(variance, 2))); + return lut; + } } diff --git a/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs b/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs index 23c37e7..bb6fa46 100644 --- a/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs +++ b/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs @@ -31,6 +31,45 @@ public partial class BrandCalibrationViewModel : ViewModelBase } } + /// + /// Внутренняя модель записи + /// + public partial class RecordViewModel : ViewModelBase + { + /// + /// + /// + [ObservableProperty] public partial int Id { get; set; } = 0; + /// + /// + /// + [ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString(); + /// + /// + /// + [ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial string BrandName { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string MixtureName { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1; + /// + /// + /// + [ObservableProperty] public partial string Comment { get; set; } = ""; + } + /// /// Логгер /// @@ -60,30 +99,33 @@ public partial class BrandCalibrationViewModel : ViewModelBase /// Текущий выбранный элемент меню. Равен -1 если элемент не выбран ///
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; + /// - /// выбранного элемента + /// Запись /// - [ObservableProperty] public partial int SelectedId { get; set; } = 0; + [ObservableProperty] public partial RecordViewModel Record { get; set; } = new(); + /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString(); + /// Текущий экран информационной секции + /// + [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; /// - /// выбранного элемента + /// Можно ли переходить на следующий экран информационной секции /// - [ObservableProperty] public partial string SelectedBrandName { get; set; } = ""; + [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; /// - /// выбранного элемента + /// Можно ли переходить на предыдущий экран информационной секции /// - [ObservableProperty] public partial string SelectedMixtureName { get; set; } = ""; + [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false; /// - /// выбранного элемента + /// Происходит ли переход между экранами информационной секции /// - [ObservableProperty] public partial double SelectedMixtureNormalRate { get; set; } = -1; + [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false; + /// - /// Можно ли производить перемещение между экранами + /// Можно ли взаимодействовать с объектом /// - [ObservableProperty] public partial bool CanScroll { get; set; } = true; + [ObservableProperty] public partial bool CanInteract { get; set; } = true; public BrandCalibrationViewModel( ILogger 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 = ""; } /// - /// Обновить значение + /// Обновить значение /// - private void UpdateCanScroll() + private void UpdateCanInteract() { - CanScroll = !MenuOpened; + CanInteract = !MenuOpened; + CanInteract &= !InfoSectionIsScrolling; } @@ -185,6 +236,15 @@ public partial class BrandCalibrationViewModel : ViewModelBase /// Переключить видимость меню /// [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; + + /// + /// Переключить информационную секцию на следующую + /// + [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--; + /// + /// Переключить информационную секцию на предыдущую + /// + [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++; /// /// Создать новую запись @@ -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) diff --git a/GSS2/ViewModels/AmineContent/SampleCalibrationViewModel.cs b/GSS2/ViewModels/AmineContent/SampleCalibrationViewModel.cs index 01826cd..19914b1 100644 --- a/GSS2/ViewModels/AmineContent/SampleCalibrationViewModel.cs +++ b/GSS2/ViewModels/AmineContent/SampleCalibrationViewModel.cs @@ -53,6 +53,63 @@ public partial class SampleCalibrationViewModel : ViewModelBase } } + /// + /// Внутренняя модель записи + /// + public partial class RecordViewModel : ViewModelBase + { + /// + /// + /// + [ObservableProperty] public partial int Id { get; set; } = 0; + /// + /// + /// + [ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString(); + /// + /// + /// + [ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial string SampleName { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string SampleComment { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial string SamplingPlace { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial double MixtureActualRate { get; set; } = -1; + /// + /// + /// + [ObservableProperty] public partial double MeasuredContent { get; set; } = -1; + + /// + /// Представление + /// + [ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new(); + + /// + /// Данные + /// + [ObservableProperty] public partial List Features { get; set; } = new(); + } + /// /// Логгер /// @@ -83,19 +140,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase /// private CancellationTokenSource? _captureImagesTaskCts = null; - /// - /// выбранного элемента - /// - private int _selectedBrandId = 0; - /// - /// Данные выбранного элемента - /// - private string? _selectedImageData = null; - /// - /// Данные выбранного элемента - /// - private List _selectedFeaturesData = new(); - /// /// Меню открыто /// @@ -109,43 +153,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase /// [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; + /// + /// Запись + /// + [ObservableProperty] public partial RecordViewModel Record { get; set; } = new(); - /// - /// выбранного элемента - /// - [ObservableProperty] public partial int SelectedId { get; set; } = 0; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString(); - /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedSampleName { get; set; } = ""; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedSampleComment { get; set; } = ""; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial DateTime SelectedSamplingDateTime { get; set; } = DateTime.UtcNow; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedSamplingPlace { get; set; } = ""; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial double SelectedMixtureActualRate { get; set; } = -1; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial double SelectedMeasuredContent { get; set; } = -1; - /// - /// Представление выбранного элемента - /// - [ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new(); /// /// Список элементов выпадающего списка марок /// @@ -172,7 +184,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase /// [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; - /// /// Текущий экран информационной секции /// @@ -200,9 +211,9 @@ public partial class SampleCalibrationViewModel : ViewModelBase [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; /// - /// Можно ли производить перемещение между экранами + /// Можно ли взаимодействовать с объектом /// - [ObservableProperty] public partial bool CanScroll { get; set; } + [ObservableProperty] public partial bool CanInteract { get; set; } = true; public SampleCalibrationViewModel( ILogger logger, @@ -220,13 +231,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase _navigation = navigation; _logger.LogInformation("Инициализация"); - // Получаем записи меню из базы данных - MenuItems = new ObservableCollection(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName))); - if (MenuItems.Count() > 0) // Если записи есть, - MenuSelectedIndex = 0; // Выбираем первую запись - else // Если записей нет, - MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана - // Получаем записи выпадающего списка марок из базы данных BrandComboBoxItems = new ObservableCollection(_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(_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(); // Ищем изображение в базе данных 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(); BrandComboBoxSelectedIndex = -1; } /// - /// Вызывается при изменении + /// Обновить значение /// - partial void OnBrandComboBoxSelectedIndexChanged(int value) + private void UpdateCanInteract() { - if (value >= 0 && value < BrandComboBoxItems.Count()) - _selectedBrandId = BrandComboBoxItems[value].Id; - } - - /// - /// Обновить значение - /// - 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(); - 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>(); // Обновляем данные в интерфейсе - // 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(() => { diff --git a/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs b/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs index 478fb92..b599bf8 100644 --- a/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs +++ b/GSS2/ViewModels/AmineContent/SeparatorCalibrationViewModel.cs @@ -35,6 +35,45 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase } } + /// + /// Внутренняя модель записи + /// + public partial class RecordViewModel : ViewModelBase + { + /// + /// + /// + [ObservableProperty] public partial int Id { get; set; } = 0; + /// + /// + /// + [ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString(); + /// + /// + /// + [ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow; + /// + /// + /// + [ObservableProperty] public partial string Type { get; set; } = ""; + /// + /// + /// + [ObservableProperty] public partial string Comment { get; set; } = ""; + /// + /// Представление + /// + [ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new(); + /// + /// Данные + /// + [ObservableProperty] public partial List Features { get; set; } = new(); + } + /// /// Логгер /// @@ -65,15 +104,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase /// private CancellationTokenSource? _captureImagesTaskCts = null; - /// - /// Данные выбранного элемента - /// - private string? _selectedImageData = null; - /// - /// Данные выбранного элемента - /// - private List _selectedFeaturesData = new(); - /// /// Меню открыто /// @@ -88,21 +118,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; /// - /// выбранного элемента + /// Запись /// - [ObservableProperty] public partial int SelectedId { get; set; } = 0; - /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString(); - /// - /// выбранного элемента - /// - [ObservableProperty] public partial string SelectedType { get; set; } = ""; - /// - /// Представление выбранного элемента - /// - [ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new(); + [ObservableProperty] public partial RecordViewModel Record { get; set; } = new(); /// /// Текущий экран главной секции @@ -112,6 +130,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase /// Можно ли переходить на следующий экран главной секции /// [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; + /// /// Можно ли переходить на предыдущий экран главной секции /// @@ -120,6 +139,22 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase /// Происходит ли переход между экранами главной секции /// [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; + /// + /// Текущий экран информационной секции + /// + [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0; + /// + /// Можно ли переходить на следующий экран информационной секции + /// + [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false; + /// + /// Можно ли переходить на предыдущий экран информационной секции + /// + [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false; + /// + /// Происходит ли переход между экранами информационной секции + /// + [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false; /// /// Проводится ли съёмка изображений @@ -131,9 +166,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; /// - /// Можно ли производить перемещение между экранами + /// Можно ли взаимодействовать с объектом /// - [ObservableProperty] public partial bool CanScroll { get; set; } + [ObservableProperty] public partial bool CanInteract { get; set; } = true; public SeparatorCalibrationViewModel( ILogger 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(); // Ищем изображение в базе данных 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(); } /// - /// Обновить значение + /// Обновить значение /// - 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 /// Переключить главную секцию информация <-> изображения /// [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; - + /// + /// Переключить информационную секцию на следующую + /// + [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--; + /// + /// Переключить информационную секцию на предыдущую + /// + [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++; + /// /// Создать новую запись /// @@ -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(); - 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>(); // Обновляем данные в интерфейсе - // 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 }); } - // Команда отмены задачи съёмки изображений + /// + /// Отменить задачу съёмки изображения + /// [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); } diff --git a/GSS2/ViewModels/AmineContentViewModel.cs b/GSS2/ViewModels/AmineContentViewModel.cs index 816004b..51a1218 100644 --- a/GSS2/ViewModels/AmineContentViewModel.cs +++ b/GSS2/ViewModels/AmineContentViewModel.cs @@ -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] diff --git a/GSS2/ViewModels/Common/CameraViewModel.cs b/GSS2/ViewModels/Common/CameraViewModel.cs deleted file mode 100644 index 24c7ce9..0000000 --- a/GSS2/ViewModels/Common/CameraViewModel.cs +++ /dev/null @@ -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; - } -} diff --git a/GSS2/Views/AmineContent/AnalysisView.axaml b/GSS2/Views/AmineContent/AnalysisView.axaml index a110c33..f521a54 100644 --- a/GSS2/Views/AmineContent/AnalysisView.axaml +++ b/GSS2/Views/AmineContent/AnalysisView.axaml @@ -1,248 +1,327 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:vm="using:GSS2.ViewModels.AmineContent" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns="https://github.com/avaloniaui" +> - - - - - - + - - + + + + 5 + + + + + + + 5 - + Orientation="Vertical" + VerticalAlignment="Stretch" + > - - - - - - + - - + + + + 5 + + + + + + + 5 - - + Orientation="Horizontal" + > - - + + + + + + + + + 5 - - - - - - - - + + + + + + + + - - - + 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - - - - + 5 - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 5 @@ -250,135 +329,1022 @@ - - - - + 5 - - - - + - - - - - - - - - - + + + + + - + + + + + + + - + - - + + - - - - - - - - + + + + + + - + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + - - + - - + + + + + + + 5 + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + 5 + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + 5 + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - @@ -483,22 +1489,37 @@ - - - - - - - - - + + + + + + + + - + - + diff --git a/GSS2/Views/AmineContent/AnalysisView.axaml.cs b/GSS2/Views/AmineContent/AnalysisView.axaml.cs index 3dd0bda..ef473aa 100644 --- a/GSS2/Views/AmineContent/AnalysisView.axaml.cs +++ b/GSS2/Views/AmineContent/AnalysisView.axaml.cs @@ -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(); } } diff --git a/GSS2/Views/AmineContent/BrandCalibrationView.axaml b/GSS2/Views/AmineContent/BrandCalibrationView.axaml index 9a846f6..3d23856 100644 --- a/GSS2/Views/AmineContent/BrandCalibrationView.axaml +++ b/GSS2/Views/AmineContent/BrandCalibrationView.axaml @@ -14,7 +14,7 @@ - + @@ -40,12 +40,14 @@ + 5 - + + 5 @@ -55,115 +57,201 @@ Classes="vertical" Command="{Binding ToggleMenuCommand}" Grid.Column="0" + Grid.Row="0" + Grid.RowSpan="3" VerticalAlignment="Stretch" > - + - + - - - - + + - 5 + + + + - - - - - - - - - + 5 - 5 + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + 5 - + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + + diff --git a/GSS2/Views/AmineContent/SampleCalibrationView.axaml b/GSS2/Views/AmineContent/SampleCalibrationView.axaml index c28e83a..52364ff 100644 --- a/GSS2/Views/AmineContent/SampleCalibrationView.axaml +++ b/GSS2/Views/AmineContent/SampleCalibrationView.axaml @@ -14,7 +14,7 @@ - + @@ -51,8 +51,8 @@ - - + + 5 @@ -101,12 +101,12 @@ 5 - - + + - - + + @@ -126,7 +126,7 @@ Grid.Row="1" IsReadOnly="True" PlaceholderText="ИД" - Text="{Binding SelectedId}" + Text="{Binding Record.Id}" /> @@ -180,7 +180,7 @@ @@ -204,7 +204,7 @@ Grid.Column="0" Grid.Row="5" PlaceholderText="Дата и время отбора пробы" - Text="{Binding SelectedSamplingDateTime, Converter={StaticResource DateTimeConverter}}" + Text="{Binding Record.SamplingDateTime, Converter={StaticResource DateTimeConverter}}" /> @@ -258,7 +258,7 @@ Grid.Row="1" Increment="0.0001" PlaceholderText="Фактический расход смеси (кг/т | гр/кг)" - Value="{Binding SelectedMixtureActualRate}" + Value="{Binding Record.MixtureActualRate}" /> @@ -363,15 +363,14 @@ - - + + @@ -379,11 +378,11 @@ - + - + @@ -402,8 +401,8 @@ > - - + + @@ -417,8 +416,8 @@ - - + + @@ -433,18 +432,20 @@ diff --git a/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml b/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml index aca0287..9aca86d 100644 --- a/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml +++ b/GSS2/Views/AmineContent/SeparatorCalibrationView.axaml @@ -14,7 +14,7 @@ - + @@ -80,7 +80,8 @@ - + + 5 @@ -99,7 +100,7 @@ Grid.Row="1" IsReadOnly="True" PlaceholderText="ИД" - Text="{Binding SelectedId}" + Text="{Binding Record.Id}" /> - + + + + - - - - - - - - - - - - - - @@ -166,25 +169,19 @@ - - - - - - - + + @@ -196,7 +193,7 @@ - + @@ -215,8 +212,8 @@ > - - + + @@ -230,8 +227,8 @@ - - + + @@ -246,18 +243,20 @@ diff --git a/GSS2/Views/AmineContentView.axaml b/GSS2/Views/AmineContentView.axaml index 7346f58..6caa828 100644 --- a/GSS2/Views/AmineContentView.axaml +++ b/GSS2/Views/AmineContentView.axaml @@ -36,31 +36,30 @@ - + - - + 5 - - - - - - + + - - - - - + + + + @@ -68,24 +67,19 @@ - + - - + + - - - - - - @@ -93,14 +87,16 @@ - - - - - + + + + + + + diff --git a/GSS2/Views/Common/CameraView.axaml b/GSS2/Views/Common/CameraView.axaml deleted file mode 100644 index a1b8d47..0000000 --- a/GSS2/Views/Common/CameraView.axaml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - 20 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/GSS2/Views/Common/CameraView.axaml.cs b/GSS2/Views/Common/CameraView.axaml.cs deleted file mode 100644 index 0c2edf9..0000000 --- a/GSS2/Views/Common/CameraView.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace GSS2.Views.Common; - -public partial class CameraView : UserControl -{ - public CameraView() - { - InitializeComponent(); - } -} diff --git a/GSS2/Views/NumericKeyboardView.axaml b/GSS2/Views/NumericKeyboardView.axaml old mode 100755 new mode 100644 diff --git a/GSS2/Views/NumericKeyboardView.axaml.cs b/GSS2/Views/NumericKeyboardView.axaml.cs old mode 100755 new mode 100644 diff --git a/GSS2/Views/TextKeyboardView.axaml b/GSS2/Views/TextKeyboardView.axaml old mode 100755 new mode 100644 diff --git a/GSS2/Views/TextKeyboardView.axaml.cs b/GSS2/Views/TextKeyboardView.axaml.cs old mode 100755 new mode 100644 diff --git a/GSS2/appsettings.json b/GSS2/appsettings.json index 16426f7..740b154 100644 --- a/GSS2/appsettings.json +++ b/GSS2/appsettings.json @@ -20,8 +20,7 @@ } }, "ConnectionStrings": { - "AmineContentCalibrationDatabasePath": "AmineContentCalibration.db", - "AmineContentResultsDatabasePath": "AmineContentResults.db" + "AmineContentDatabasePath": "AmineContent.db" }, "Hardware": { "Illuminator": { diff --git a/packages_list.txt b/packages_list.txt index 26204f7b4883d443ff67eadf32c9a5d5e6b2b39d..0089f0807ead8ce98fe8622fbbb5a23f30ea28b3 100644 GIT binary patch delta 48 ucmdni%6O-hal