forked from amkovkov/GranuSightSoftware2
comprehensive update
1) remove unused components: camera view (due it causes segmentation faults), compute resources and all related, illuminator controller (due in useless without camera view), image storage service, temperature and humidity service 2) database schema - reduce tables references, features storing in records themselves in compressed form, add records creation and editing date and time, add separator comment column 3) analysis - rework of pipeline and ui, now database storing only raw data and all display values calculated from it 4) lights service - add reconnection if disconnected 5) add width and height command line arguments 6) fix some typos and other issues
This commit is contained in:
@@ -84,6 +84,8 @@ public partial class AnalyzerService
|
||||
private PredictionEngine<RegressionData, RegressionPrediction>? _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<AnalyzerService> 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<double> meanVector = matrix.ColumnSums() / matrix.RowCount;
|
||||
Matrix<double> covarianceMatrix = MatrixCovariance(matrix, meanVector);
|
||||
Matrix<double> 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,26 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GSS2.Core.Analysis.AmineContent.Database;
|
||||
|
||||
// TODO: Добавить внешние ключи в таблицы и правило что при удалении ключа ключ должен сбрасываться на 0
|
||||
public class Context : DbContext
|
||||
{
|
||||
public DbSet<BrandRecord> BrandRecords => Set<BrandRecord>();
|
||||
public DbSet<FeatureRecord> FeatureRecords => Set<FeatureRecord>();
|
||||
public DbSet<ImageRecord> ImageRecords => Set<ImageRecord>();
|
||||
// public DbSet<ResultRecord> ResultRecords => Set<ResultRecord>();
|
||||
public DbSet<ResultRecord> ResultRecords => Set<ResultRecord>();
|
||||
public DbSet<SeparatorRecord> SeparatorRecords => Set<SeparatorRecord>();
|
||||
public DbSet<SampleRecord> SampleRecords => Set<SampleRecord>();
|
||||
|
||||
public Context(DbContextOptions<Context> options)
|
||||
: base(options)
|
||||
{ }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<SeparatorRecord>()
|
||||
.Property(e => e.Features)
|
||||
.HasConversion(new CompressedFloatListConverter());
|
||||
modelBuilder.Entity<SampleRecord>()
|
||||
.Property(e => e.Features)
|
||||
.HasConversion(new CompressedFloatListConverter());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<float> 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<float>? values = null,
|
||||
int separatorRecordId = 0,
|
||||
int sampleRecordId = 0
|
||||
)
|
||||
{
|
||||
Id = id;
|
||||
if (values is null)
|
||||
Values = new List<float>();
|
||||
else
|
||||
Values = values;
|
||||
SeparatorRecordId = separatorRecordId;
|
||||
SampleRecordId = sampleRecordId;
|
||||
}
|
||||
}
|
||||
+159
-29
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -33,6 +33,20 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Марка пробы");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<int>("SampleRecordId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Проба");
|
||||
|
||||
b.Property<int>("SeparatorRecordId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Сепаратор");
|
||||
|
||||
b.PrimitiveCollection<string>("Values")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения вектора");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("FeatureRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b =>
|
||||
{
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<string>("BrandName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Марка пробы");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<int>("ImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение");
|
||||
|
||||
b.Property<int>("MaskImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Маска");
|
||||
|
||||
b.Property<string>("MixtureName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название кондиционирующей смеси");
|
||||
|
||||
b.Property<double>("MixtureNormalRate")
|
||||
.HasColumnType("REAL")
|
||||
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
|
||||
|
||||
b.Property<int>("ParticlesCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Количество частиц");
|
||||
|
||||
b.Property<int>("ResultImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение результата");
|
||||
|
||||
b.Property<string>("SampleComment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация о пробе");
|
||||
|
||||
b.Property<string>("SampleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название пробы");
|
||||
|
||||
b.Property<DateTime>("SamplingDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время отбора пробы");
|
||||
|
||||
b.Property<string>("SamplingPlace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Место отбора пробы");
|
||||
|
||||
b.Property<string>("SeparatorType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Тип сепаратора");
|
||||
|
||||
b.Property<string>("Uuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Уникальный идентификатор");
|
||||
|
||||
b.PrimitiveCollection<string>("ValueCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения частиц (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("ValueData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения частиц (данные)");
|
||||
|
||||
b.PrimitiveCollection<string>("XCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, X координаты (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("XData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, X координаты (данные)");
|
||||
|
||||
b.PrimitiveCollection<string>("YCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, Y координаты (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("YData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, Y координаты (данные)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ResultRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -107,7 +215,15 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Марка");
|
||||
|
||||
b.PrimitiveCollection<string>("FeatureIds")
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.PrimitiveCollection<string>("Features")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Признаки");
|
||||
@@ -126,7 +242,7 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
|
||||
b.Property<string>("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<string>("FeatureIds")
|
||||
b.Property<string>("Comment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.PrimitiveCollection<string>("Features")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Признаки");
|
||||
+47
-21
@@ -18,30 +18,18 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
|
||||
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
|
||||
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
|
||||
BrandName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"),
|
||||
MixtureName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"),
|
||||
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)")
|
||||
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"),
|
||||
Comment = table.Column<string>(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<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Values = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения вектора"),
|
||||
SeparatorRecordId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Сепаратор"),
|
||||
SampleRecordId = table.Column<int>(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<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
|
||||
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
|
||||
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
|
||||
SampleName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"),
|
||||
SampleComment = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"),
|
||||
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы"),
|
||||
SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"),
|
||||
SeparatorType = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип сепаратора"),
|
||||
BrandName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"),
|
||||
MixtureName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"),
|
||||
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"),
|
||||
ImageId = table.Column<int>(type: "INTEGER", maxLength: 256, nullable: false, comment: "Изображение"),
|
||||
MaskImageId = table.Column<int>(type: "INTEGER", maxLength: 256, nullable: false, comment: "Маска"),
|
||||
ResultImageId = table.Column<int>(type: "INTEGER", maxLength: 256, nullable: false, comment: "Изображение результата"),
|
||||
ParticlesCount = table.Column<int>(type: "INTEGER", nullable: false, comment: "Количество частиц"),
|
||||
ValueCount = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения частиц (количество)"),
|
||||
ValueData = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения частиц (данные)"),
|
||||
XCount = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, X координаты (количество)"),
|
||||
XData = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, X координаты (данные)"),
|
||||
YCount = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, Y координаты (количество)"),
|
||||
YData = table.Column<string>(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<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
|
||||
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
|
||||
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
|
||||
SampleName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"),
|
||||
SampleComment = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"),
|
||||
SampleComment = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация о пробе"),
|
||||
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы (utc)"),
|
||||
BrandId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Марка"),
|
||||
SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"),
|
||||
MixtureActualRate = table.Column<double>(type: "REAL", nullable: false, comment: "Фактический расход кондиционирующей смеси (кг/т | гр/кг)"),
|
||||
MeasuredContent = table.Column<double>(type: "REAL", nullable: false, comment: "Измеренное количество кондиционирующей смеси (кг/т | гр/кг)"),
|
||||
ImageId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Изображение"),
|
||||
FeatureIds = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки")
|
||||
Features = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -84,9 +107,12 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
|
||||
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
|
||||
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип"),
|
||||
Comment = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация"),
|
||||
ImageId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Изображение"),
|
||||
FeatureIds = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки")
|
||||
Features = table.Column<string>(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");
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<string>("BrandName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Марка пробы");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("MixtureName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название кондиционирующей смеси");
|
||||
|
||||
b.Property<double>("MixtureNormalRate")
|
||||
.HasColumnType("REAL")
|
||||
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
|
||||
|
||||
b.Property<string>("Uuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Уникальный идентификатор");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("BrandRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<string>("Base64")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения вектора");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ImageRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<string>("BrandName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Марка пробы");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<int>("ImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение");
|
||||
|
||||
b.Property<int>("MaskImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Маска");
|
||||
|
||||
b.Property<string>("MixtureName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название кондиционирующей смеси");
|
||||
|
||||
b.Property<double>("MixtureNormalRate")
|
||||
.HasColumnType("REAL")
|
||||
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
|
||||
|
||||
b.Property<int>("ParticlesCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Количество частиц");
|
||||
|
||||
b.Property<int>("ResultImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение результата");
|
||||
|
||||
b.Property<string>("SampleComment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация о пробе");
|
||||
|
||||
b.Property<string>("SampleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название пробы");
|
||||
|
||||
b.Property<DateTime>("SamplingDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время отбора пробы");
|
||||
|
||||
b.Property<string>("SamplingPlace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Место отбора пробы");
|
||||
|
||||
b.Property<string>("SeparatorType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Тип сепаратора");
|
||||
|
||||
b.Property<string>("Uuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Уникальный идентификатор");
|
||||
|
||||
b.PrimitiveCollection<string>("ValueCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения частиц (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("ValueData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения частиц (данные)");
|
||||
|
||||
b.PrimitiveCollection<string>("XCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, X координаты (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("XData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, X координаты (данные)");
|
||||
|
||||
b.PrimitiveCollection<string>("YCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, Y координаты (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("YData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, Y координаты (данные)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ResultRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<int>("BrandId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Марка");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("Features")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Признаки");
|
||||
|
||||
b.Property<int>("ImageId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение");
|
||||
|
||||
b.Property<double>("MeasuredContent")
|
||||
.HasColumnType("REAL")
|
||||
.HasComment("Измеренное количество кондиционирующей смеси (кг/т | гр/кг)");
|
||||
|
||||
b.Property<double>("MixtureActualRate")
|
||||
.HasColumnType("REAL")
|
||||
.HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)");
|
||||
|
||||
b.Property<string>("SampleComment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация о пробе");
|
||||
|
||||
b.Property<string>("SampleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название пробы");
|
||||
|
||||
b.Property<DateTime>("SamplingDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время отбора пробы (utc)");
|
||||
|
||||
b.Property<string>("SamplingPlace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Место отбора пробы");
|
||||
|
||||
b.Property<string>("Uuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Уникальный идентификатор");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SampleRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SeparatorRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("Features")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Признаки");
|
||||
|
||||
b.Property<int>("ImageId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Тип");
|
||||
|
||||
b.Property<string>("Uuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Уникальный идентификатор");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SeparatorRecords");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Migration1 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,20 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Марка пробы");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<int>("SampleRecordId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Проба");
|
||||
|
||||
b.Property<int>("SeparatorRecordId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Сепаратор");
|
||||
|
||||
b.PrimitiveCollection<string>("Values")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения вектора");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("FeatureRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b =>
|
||||
{
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Идентификатор");
|
||||
|
||||
b.Property<string>("BrandName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Марка пробы");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<int>("ImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение");
|
||||
|
||||
b.Property<int>("MaskImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Маска");
|
||||
|
||||
b.Property<string>("MixtureName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название кондиционирующей смеси");
|
||||
|
||||
b.Property<double>("MixtureNormalRate")
|
||||
.HasColumnType("REAL")
|
||||
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
|
||||
|
||||
b.Property<int>("ParticlesCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Количество частиц");
|
||||
|
||||
b.Property<int>("ResultImageId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Изображение результата");
|
||||
|
||||
b.Property<string>("SampleComment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация о пробе");
|
||||
|
||||
b.Property<string>("SampleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Название пробы");
|
||||
|
||||
b.Property<DateTime>("SamplingDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время отбора пробы");
|
||||
|
||||
b.Property<string>("SamplingPlace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Место отбора пробы");
|
||||
|
||||
b.Property<string>("SeparatorType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Тип сепаратора");
|
||||
|
||||
b.Property<string>("Uuid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Уникальный идентификатор");
|
||||
|
||||
b.PrimitiveCollection<string>("ValueCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения частиц (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("ValueData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Значения частиц (данные)");
|
||||
|
||||
b.PrimitiveCollection<string>("XCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, X координаты (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("XData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, X координаты (данные)");
|
||||
|
||||
b.PrimitiveCollection<string>("YCount")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, Y координаты (количество)");
|
||||
|
||||
b.PrimitiveCollection<string>("YData")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Контура частиц, Y координаты (данные)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ResultRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -104,7 +212,15 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
.HasColumnType("INTEGER")
|
||||
.HasComment("Марка");
|
||||
|
||||
b.PrimitiveCollection<string>("FeatureIds")
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("Features")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Признаки");
|
||||
@@ -123,7 +239,7 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
|
||||
|
||||
b.Property<string>("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<string>("FeatureIds")
|
||||
b.Property<string>("Comment")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дополнительная информация");
|
||||
|
||||
b.Property<DateTime>("CreateDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время создания");
|
||||
|
||||
b.Property<DateTime>("EditDateTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Дата и время редактирования");
|
||||
|
||||
b.Property<string>("Features")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasComment("Признаки");
|
||||
|
||||
@@ -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<int> ValueCount { get; set; }
|
||||
[Comment("Значения частиц (данные)")]
|
||||
public List<float> ValueData { get; set; }
|
||||
[Comment("Контура частиц, X координаты (количество)")]
|
||||
public List<int> XCount { get; set; }
|
||||
[Comment("Контура частиц, X координаты (данные)")]
|
||||
public List<float> XData { get; set; }
|
||||
[Comment("Контура частиц, Y координаты (количество)")]
|
||||
public List<int> YCount { get; set; }
|
||||
[Comment("Контура частиц, Y координаты (данные)")]
|
||||
public List<float> 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<float[]>? values = null,
|
||||
List<float[]>? xs = null,
|
||||
List<float[]>? 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<int>();
|
||||
ValueData = new List<float>();
|
||||
}
|
||||
else
|
||||
{
|
||||
ValueCount = values.Select(fs => fs.Count()).ToList();
|
||||
ValueData = values.SelectMany(fs => fs).ToList();
|
||||
}
|
||||
if (xs is null)
|
||||
{
|
||||
XCount = new List<int>();
|
||||
XData = new List<float>();
|
||||
}
|
||||
else
|
||||
{
|
||||
XCount = xs.Select(fs => fs.Count()).ToList();
|
||||
XData = xs.SelectMany(fs => fs).ToList();
|
||||
}
|
||||
if (ys is null)
|
||||
{
|
||||
YCount = new List<int>();
|
||||
YData = new List<float>();
|
||||
}
|
||||
else
|
||||
{
|
||||
YCount = ys.Select(fs => fs.Count()).ToList();
|
||||
YData = ys.SelectMany(fs => fs).ToList();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> FeatureIds { get; set; }
|
||||
public List<float> 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<int>? featureIds = null
|
||||
List<float[]>? 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<int>();
|
||||
if (features is null)
|
||||
Features = new List<float>();
|
||||
else
|
||||
FeatureIds = featureIds;
|
||||
Features = features.SelectMany(fs => fs).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> FeatureIds { get; set; }
|
||||
public List<float> 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<int>? featureIds = null
|
||||
List<float[]>? 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<int>();
|
||||
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<float>();
|
||||
else
|
||||
Features = features.SelectMany(fs => fs).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IConfiguration>();
|
||||
if (configuration is null)
|
||||
throw new Exception("Cannot get configuration");
|
||||
var serviceConfiguration = configuration.GetSection(section).Get<TemperatureHumidityService.TemperatureHumidityServiceConfiguration>();
|
||||
if (serviceConfiguration is null)
|
||||
throw new InvalidConfigurationException($"Cannot get configuration {section}");
|
||||
var logger = serviceProvider.GetService<ILogger<TemperatureHumidityService>>();
|
||||
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<IConfiguration>();
|
||||
// if (configuration is null)
|
||||
// throw new Exception("Cannot get configuration");
|
||||
// var serviceConfiguration = configuration.GetSection(section).Get<TemperatureHumidityService.TemperatureHumidityServiceConfiguration>();
|
||||
// if (serviceConfiguration is null)
|
||||
// throw new InvalidConfigurationException($"Cannot get configuration {section}");
|
||||
// var logger = serviceProvider.GetService<ILogger<TemperatureHumidityService>>();
|
||||
// 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<ComputeResourcesService>();
|
||||
[Obsolete]
|
||||
public static IServiceCollection AddUsbService(this IServiceCollection services) => services
|
||||
.AddSingleton<UsbService>();
|
||||
public static IServiceCollection AddImageStorageService(this IServiceCollection services, DirectoryInfo rootDirectory) => services
|
||||
.AddSingleton<ImageStorageService>(
|
||||
serviceProvider =>
|
||||
{
|
||||
var logger = serviceProvider.GetService<ILogger<ImageStorageService>>();
|
||||
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<Context>(
|
||||
@@ -121,9 +109,9 @@ public static class DependencyInjectionHelper
|
||||
public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services, bool mock = false)
|
||||
{
|
||||
if (!mock)
|
||||
return services.AddSingleton<ImageCapturerService>();
|
||||
return services.AddSingleton<IImageCapturerService, ImageCapturerService>();
|
||||
|
||||
return services.AddSingleton<ImageCapturerMockService>(serviceProvider =>
|
||||
return services.AddSingleton<IImageCapturerService, ImageCapturerMockService>(serviceProvider =>
|
||||
{
|
||||
var logger = serviceProvider.GetService<ILogger<ImageCapturerMockService>>();
|
||||
if (logger is null)
|
||||
|
||||
@@ -13,4 +13,3 @@ public static class ConfigurationSectionExtensions
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<double> values) => Math.Sqrt(Variance(values));
|
||||
public static float Variance(this IEnumerable<float> 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<float> values) => MathF.Sqrt(Variance(values));
|
||||
|
||||
public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(this IEnumerable<TValue> values, IEnumerable<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MappedBuffer>? _imageCaptureMappedBuffers;
|
||||
private System.Timers.Timer? _viewFinderTimer = null;
|
||||
private System.Timers.ElapsedEventHandler? _viewFinderTimerElapsed = null;
|
||||
private EventHandler<Camera.RequestCompletedEventArgs>? _viewFinderRequestCompletedHandler = null;
|
||||
private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource();
|
||||
private readonly Dictionary<string, object> _cameraHardSettings = new Dictionary<string, object>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<double> 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<ComputeResourcesService> _logger;
|
||||
|
||||
private (long idle, long total)? _prevAll = null;
|
||||
private Dictionary<int, (long idle, long total)?>? _prevCores = null;
|
||||
|
||||
public ComputeResourcesService(ILogger<ComputeResourcesService> 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<double>());
|
||||
|
||||
var (allUsage, allIdle, allTotal) = ParseLines(lines.First(), _prevAll);
|
||||
_prevAll = (allIdle, allTotal);
|
||||
|
||||
var coreUsages = new List<double>();
|
||||
|
||||
foreach (var (i, line) in lines.Skip(1).Enumerate())
|
||||
{
|
||||
if (_prevCores is null)
|
||||
_prevCores = new Dictionary<int, (long idle, long total)?>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<TemperatureHumidityService> _logger;
|
||||
private readonly TemperatureHumidityServiceConfiguration _configuration;
|
||||
|
||||
private readonly I2cDevice _sensor;
|
||||
|
||||
public TemperatureHumidityService(ILogger<TemperatureHumidityService> 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<byte> command = stackalloc byte[] { 0xFD };
|
||||
Span<byte> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace GSS2.Core;
|
||||
|
||||
public class ImageStorageService
|
||||
{
|
||||
private readonly ILogger<ImageStorageService> _logger;
|
||||
private readonly DirectoryInfo _rootDirectory;
|
||||
|
||||
public ImageStorageService(ILogger<ImageStorageService> logger, DirectoryInfo rootDirectory)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Инициализация");
|
||||
|
||||
_rootDirectory = rootDirectory;
|
||||
if (!_rootDirectory.Exists)
|
||||
{
|
||||
_rootDirectory.Create();
|
||||
_logger.LogInformation("Создана директория: {}", _rootDirectory.FullName);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Инициализировано");
|
||||
}
|
||||
|
||||
public async Task<string> 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<string> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.IO.Compression;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Компрессор для больших списков чисел с плавающей точкой
|
||||
/// </summary>
|
||||
public class CompressedFloatListConverter : ValueConverter<List<float>, string>
|
||||
{
|
||||
public CompressedFloatListConverter()
|
||||
: base(
|
||||
v => Compress(v),
|
||||
v => Decompress(v)
|
||||
)
|
||||
{ }
|
||||
|
||||
private static string Compress(List<float> 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<float> Decompress(string base64String)
|
||||
{
|
||||
if (string.IsNullOrEmpty(base64String))
|
||||
return new List<float>();
|
||||
|
||||
byte[] compressedBytes;
|
||||
float[]? floatArray;
|
||||
|
||||
try
|
||||
{
|
||||
compressedBytes = Convert.FromBase64String(base64String);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
floatArray = JsonConvert.DeserializeObject<float[]>(base64String);
|
||||
if (floatArray is null)
|
||||
throw;
|
||||
return new List<float>(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<float>(floatArray);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
@@ -1,15 +1,18 @@
|
||||
<Window
|
||||
Height="{Binding Height}"
|
||||
Title="{Binding Title}"
|
||||
Topmost="{Binding Topmost}"
|
||||
Width="{Binding Width}"
|
||||
WindowDecorations="{Binding WindowDecorations}"
|
||||
WindowState="{Binding WindowState}"
|
||||
x:Class="GSS2.UI.Core.MainWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
Title="{Binding Title}"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
WindowDecorations="{Binding WindowDecorations}"
|
||||
Topmost="{Binding Topmost}"
|
||||
WindowState="{Binding WindowState}">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
>
|
||||
|
||||
<ContentControl x:Name="MainContent" />
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<CameraViewModel> _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<CameraViewModel> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<IlluminatorControllerViewModel> _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<IlluminatorControllerViewModel> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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<ResourceBarViewModel> _logger;
|
||||
|
||||
[ObservableProperty] private ObservableCollection<ISeries> _series;
|
||||
[ObservableProperty] private Rect _bounds;
|
||||
|
||||
public ResourceBarViewModel(ILogger<ResourceBarViewModel> logger, IEnumerable<ISeries> series)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Initialization");
|
||||
|
||||
Series = new ObservableCollection<ISeries>(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<PieSeries<ObservableValue>>())
|
||||
{
|
||||
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<ObservableValue> value1 &&
|
||||
Series.Skip(1).First().Values is ObservableCollection<ObservableValue> value2)
|
||||
{
|
||||
value1.First().Value = value > maxValue ? maxValue : value;
|
||||
value2.First().Value = value > maxValue ? 0 : maxValue - value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ResourceChartViewModel> _logger;
|
||||
[ObservableProperty] private ObservableCollection<ISeries> _series;
|
||||
[ObservableProperty] private ObservableCollection<Axis> _yAxes;
|
||||
[ObservableProperty] private ObservableCollection<Axis> _xAxes;
|
||||
[ObservableProperty] private Rect _bounds;
|
||||
|
||||
public ResourceChartViewModel(ILogger<ResourceChartViewModel> logger, IEnumerable<ISeries> series, IEnumerable<Axis> yAxes, IEnumerable<Axis> xAxes)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Initialization");
|
||||
|
||||
Series = new ObservableCollection<ISeries>(series);
|
||||
YAxes = new ObservableCollection<Axis>(yAxes);
|
||||
XAxes = new ObservableCollection<Axis>(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<double> values)
|
||||
return;
|
||||
|
||||
values.Add(value);
|
||||
while (values.Count() > maxValuesCount || values.Count() == 0)
|
||||
values.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
@@ -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<ResourceCpuUsageBarsViewModel> _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<ResourceCpuUsageBarsViewModel> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<ResourcesViewModel> _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<ResourcesViewModel> logger,
|
||||
ILogger<ResourceCpuUsageBarsViewModel> resourceCpuUsageBarsViewModelLogger,
|
||||
ILogger<ResourceChartViewModel> resourceChartViewModelCpuUsageLogger,
|
||||
ILogger<ResourceChartViewModel> resourceChartViewModelCpuTemperatureLogger,
|
||||
ILogger<ResourceChartViewModel> resourceChartViewModelMemoryUsageLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelCpuUsageLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelCpu0UsageLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelCpu1UsageLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelCpu2UsageLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelCpu3UsageLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelCpuTemperatureLogger,
|
||||
ILogger<ResourceBarViewModel> resourceBarViewModelMemoryUsageLogger,
|
||||
ComputeResourcesService computeResourcesService
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.LogInformation("Initialization");
|
||||
|
||||
_computeResourcesService = computeResourcesService;
|
||||
|
||||
ResourceChartViewModel CreateChartViewModel(ILogger<ResourceChartViewModel> logger, string name, double minLimit, double maxLimit, double minStep)
|
||||
{
|
||||
return new ResourceChartViewModel(
|
||||
logger: logger,
|
||||
series:
|
||||
[
|
||||
new LineSeries<double>
|
||||
{
|
||||
Name = name,
|
||||
Values = new ObservableCollection<double>(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<ResourceBarViewModel> logger, Func<LiveChartsCore.Kernel.ChartPoint<ObservableValue, LiveChartsCore.SkiaSharpView.Drawing.Geometries.DoughnutGeometry, LiveChartsCore.SkiaSharpView.Drawing.Geometries.LabelGeometry>, string> formatter)
|
||||
{
|
||||
return new ResourceBarViewModel(
|
||||
logger: logger,
|
||||
series:
|
||||
[
|
||||
new PieSeries<ObservableValue>
|
||||
{
|
||||
Values = new ObservableCollection<ObservableValue> { 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<ObservableValue>
|
||||
{
|
||||
Values = new ObservableCollection<ObservableValue> { 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);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<Control
|
||||
x:Class="GSS2.UI.Core.Views.CameraView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
x:DataType="vm:CameraViewModel"
|
||||
Background="{Binding Background}"
|
||||
StretchMode="{Binding StretchMode}"
|
||||
TileMode="{Binding TileMode}" />
|
||||
@@ -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<EglCreateImageKHRDelegate>(pEglCreateImageKHR);
|
||||
|
||||
var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR");
|
||||
if (pEglDestroyImageKHR == IntPtr.Zero)
|
||||
throw new Exception("Required EGL method \"eglDestroyImageKHR\" not found");
|
||||
EglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer<EglDestroyImageKHRDelegate>(pEglDestroyImageKHR);
|
||||
|
||||
var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero)
|
||||
throw new Exception("Required EGL method \"glEGLImageTargetTexture2DOES\" not found");
|
||||
GlEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer<GlEGLImageTargetTexture2DOESDelegate>(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<CameraView> _logger;
|
||||
private readonly Dictionary<int, IntPtr> _importedDmaBuffers = new Dictionary<int, nint>();
|
||||
private bool _isViewFinderStarted = false;
|
||||
private OpenGLContext? _openGLContext = null;
|
||||
private Point? _lastMousePosition = null;
|
||||
private Dictionary<int, Point>? _lastTouchPositions = null;
|
||||
private float _zoom = 1;
|
||||
private float _panX = 0;
|
||||
private float _panY = 0;
|
||||
|
||||
public static readonly StyledProperty<Color> BackgroundProperty = AvaloniaProperty.Register<CameraView, Color>(nameof(Background), defaultValue: Colors.Transparent);
|
||||
public Color Background
|
||||
{
|
||||
get => GetValue(BackgroundProperty);
|
||||
set => SetValue(BackgroundProperty, value);
|
||||
}
|
||||
public static readonly StyledProperty<Stretch> StretchModeProperty = AvaloniaProperty.Register<CameraView, Stretch>(nameof(StretchMode), defaultValue: Stretch.Uniform);
|
||||
public Stretch StretchMode
|
||||
{
|
||||
get => GetValue(StretchModeProperty);
|
||||
set => SetValue(StretchModeProperty, value);
|
||||
}
|
||||
public static readonly StyledProperty<TileMode> TileModeProperty = AvaloniaProperty.Register<CameraView, TileMode>(nameof(TileMode), defaultValue: TileMode.None);
|
||||
public TileMode TileMode
|
||||
{
|
||||
get => GetValue(TileModeProperty);
|
||||
set => SetValue(TileModeProperty, value);
|
||||
}
|
||||
|
||||
public CameraView(ILogger<CameraView> 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<int, Point>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="GSS2.UI.Core.Views.IlluminatorControllerView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:behaviors="using:GSS2.UI.Core.Behaviors"
|
||||
xmlns:converters="using:GSS2.UI.Core.Converters"
|
||||
xmlns:layout="using:Avalonia.Layout"
|
||||
xmlns:local="using:GSS2.UI.Core.Views"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
x:DataType="vm:IlluminatorControllerViewModel">
|
||||
|
||||
<UserControl.Styles>
|
||||
|
||||
<Style Selector="Slider.horizontal">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="Maximum" Value="1" />
|
||||
<Setter Property="Minimum" Value="0" />
|
||||
<Setter Property="Orientation" Value="Vertical" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Slider.vertical">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="Maximum" Value="1" />
|
||||
<Setter Property="Minimum" Value="0" />
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
</Style>
|
||||
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid IsVisible="{Binding Orientation, Converter={x:Static converters:OrientationToBoolConverter.IsHorizontal}}">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="20">
|
||||
|
||||
<Slider Classes="horizontal" Value="{Binding WhiteIntensity}" Maximum="0.03" />
|
||||
<Slider Classes="horizontal" Value="{Binding Uv365Intensity}" />
|
||||
<Slider Classes="horizontal" Value="{Binding Uv254Intensity}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<ToggleButton
|
||||
Grid.Row="1"
|
||||
Margin="0,10,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsChecked="{Binding Enabled}">
|
||||
<TextBlock Text="Включить" FontSize="18"/>
|
||||
</ToggleButton>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid IsVisible="{Binding Orientation, Converter={x:Static converters:OrientationToBoolConverter.IsVertical}}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="20">
|
||||
|
||||
<Slider Classes="vertical" Value="{Binding WhiteIntensity}" Maximum="0.03" />
|
||||
<Slider Classes="vertical" Value="{Binding Uv365Intensity}" />
|
||||
<Slider Classes="vertical" Value="{Binding Uv254Intensity}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<ToggleButton
|
||||
Grid.Column="1"
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
IsChecked="{Binding Enabled}">
|
||||
<TextBlock Text="Включить" FontSize="18"/>
|
||||
</ToggleButton>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace GSS2.UI.Core.Views;
|
||||
|
||||
public partial class IlluminatorControllerView : UserControl
|
||||
{
|
||||
public IlluminatorControllerView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<lvc:PieChart
|
||||
x:Class="GSS2.UI.Core.Views.ResourceBarView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
x:DataType="vm:ResourceBarViewModel"
|
||||
Bounds="{Binding Bounds, Mode=OneWayToSource}"
|
||||
Series="{Binding Series}" />
|
||||
@@ -1,11 +0,0 @@
|
||||
using LiveChartsCore.SkiaSharpView.Avalonia;
|
||||
|
||||
namespace GSS2.UI.Core.Views;
|
||||
|
||||
public partial class ResourceBarView : PieChart
|
||||
{
|
||||
public ResourceBarView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<lvc:CartesianChart
|
||||
x:Class="GSS2.UI.Core.Views.ResourceChartView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
x:DataType="vm:ResourceChartViewModel"
|
||||
Bounds="{Binding Bounds, Mode=OneWayToSource}"
|
||||
EasingFunction="{x:Null}"
|
||||
Series="{Binding Series}"
|
||||
XAxes="{Binding XAxes}"
|
||||
YAxes="{Binding YAxes}"
|
||||
ZoomMode="None" />
|
||||
@@ -1,11 +0,0 @@
|
||||
using LiveChartsCore.SkiaSharpView.Avalonia;
|
||||
|
||||
namespace GSS2.UI.Core.Views;
|
||||
|
||||
public partial class ResourceChartView : CartesianChart
|
||||
{
|
||||
public ResourceChartView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="GSS2.UI.Core.Views.ResourceCpuUsageBarsView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:behaviors="using:GSS2.UI.Core.Behaviors"
|
||||
xmlns:local="using:GSS2.UI.Core.Views"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
x:DataType="vm:ResourceCpuUsageBarsViewModel"
|
||||
Bounds="{Binding Bounds, Mode=OneWayToSource}">
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsCoresColumnVisible}" />
|
||||
<ColumnDefinition Width="*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsCoresColumnVisible}" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<local:ResourceBarView
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="0"
|
||||
DataContext="{Binding Cpu}" />
|
||||
|
||||
<local:ResourceBarView
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
DataContext="{Binding Cpu0}" />
|
||||
|
||||
<local:ResourceBarView
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
DataContext="{Binding Cpu1}" />
|
||||
|
||||
<local:ResourceBarView
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
DataContext="{Binding Cpu2}" />
|
||||
|
||||
<local:ResourceBarView
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
DataContext="{Binding Cpu3}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace GSS2.UI.Core.Views;
|
||||
|
||||
public partial class ResourceCpuUsageBarsView : UserControl
|
||||
{
|
||||
public ResourceCpuUsageBarsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="GSS2.UI.Core.Views.ResourcesView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:behaviors="using:GSS2.UI.Core.Behaviors"
|
||||
xmlns:local="using:GSS2.UI.Core.Views"
|
||||
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
|
||||
xmlns:vm="using:GSS2.UI.Core.ViewModels"
|
||||
x:DataType="vm:ResourcesViewModel"
|
||||
Bounds="{Binding Bounds, Mode=OneWayToSource}">
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="3*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsChartsColumnVisible}" />
|
||||
<ColumnDefinition Width="*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsBarsColumnVisible}" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Column="0" RowDefinitions="*,*,*">
|
||||
<local:ResourceChartView Grid.Row="0" DataContext="{Binding CpuUsageChartViewModel}" />
|
||||
<local:ResourceChartView Grid.Row="1" DataContext="{Binding CpuTemperatureChartViewModel}" />
|
||||
<local:ResourceChartView Grid.Row="2" DataContext="{Binding MemoryUsageChartViewModel}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Column="1" RowDefinitions="*,*,*">
|
||||
<local:ResourceCpuUsageBarsView Grid.Row="0" DataContext="{Binding CpuUsageBarsViewModel}" />
|
||||
<local:ResourceBarView Grid.Row="1" DataContext="{Binding CpuTemperatureBarViewModel}" />
|
||||
<local:ResourceBarView Grid.Row="2" DataContext="{Binding MemoryUsageBarViewModel}" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace GSS2.UI.Core.Views;
|
||||
|
||||
public partial class ResourcesView : UserControl
|
||||
{
|
||||
public ResourcesView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,69 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:GSS2"
|
||||
xmlns:converters="using:GSS2.UI.Core.Converters"
|
||||
RequestedThemeVariant="Dark">
|
||||
|
||||
<Application.Resources>
|
||||
<converters:NotEqualsConverter x:Key="NotEqualsConverter" />
|
||||
<converters:EqualsConverter x:Key="EqualsConverter" />
|
||||
<converters:GreaterThanConverter x:Key="GreaterThanConverter" />
|
||||
<converters:GreaterThanOrEqualConverter x:Key="GreaterThanOrEqualConverter" />
|
||||
<converters:LessThanConverter x:Key="LessThanConverter" />
|
||||
<converters:LessThanOrEqualConverter x:Key="LessThanOrEqualConverter" />
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
|
||||
<SolidColorBrush x:Key="MenuBackground" Color="#00357a" />
|
||||
<SolidColorBrush x:Key="MenuBorder" Color="#0865b2" />
|
||||
<SolidColorBrush x:Key="Overlay" Color="#0865b23f" />
|
||||
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
|
||||
<SolidColorBrush x:Key="ButtonDangerBackground" Color="#b20808" />
|
||||
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
|
||||
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
|
||||
<Style Selector="Button">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.danger">
|
||||
<Setter Property="Background" Value="{StaticResource ButtonDangerBackground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.vertical">
|
||||
<Setter Property="MinWidth" Value="40" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox.singleline">
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox.multiline">
|
||||
<Setter Property="VerticalContentAlignment" Value="Top" />
|
||||
<Setter Property="AcceptsReturn" Value="True" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.mini">
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="Margin" Value="0,0,0,0" />
|
||||
</Style>
|
||||
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
|
||||
@@ -18,6 +18,8 @@ public partial class Application : Avalonia.Application
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly bool _fullscreen;
|
||||
private readonly double _width;
|
||||
private readonly double _height;
|
||||
private readonly ILogger<Application> _logger;
|
||||
private readonly DependencyInjectionViewLocator _viewLocator;
|
||||
|
||||
@@ -25,10 +27,12 @@ public partial class Application : Avalonia.Application
|
||||
|
||||
public NavigationService? Navigation { get; private set; } = null;
|
||||
|
||||
public Application(IServiceProvider services, bool fullscreen = false)
|
||||
public Application(IServiceProvider services, bool fullscreen = false, int width = 0, int height = 0)
|
||||
{
|
||||
_services = services;
|
||||
_fullscreen = fullscreen;
|
||||
_width = width <= 0 ? double.NaN : width;
|
||||
_height = height <= 0 ? double.NaN : height;
|
||||
|
||||
// Создаём AvaloniaLogSink, который перенаправляет логи из Avalonia в хост
|
||||
var loggerFactory = _services.GetRequiredService<ILoggerFactory>();
|
||||
@@ -124,7 +128,9 @@ public partial class Application : Avalonia.Application
|
||||
{
|
||||
WindowDecorations = _fullscreen ? Avalonia.Controls.WindowDecorations.None : Avalonia.Controls.WindowDecorations.Full,
|
||||
Topmost = _fullscreen,
|
||||
WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal
|
||||
WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal,
|
||||
Width = _width,
|
||||
Height = _height
|
||||
},
|
||||
};
|
||||
desktop.MainWindow = mainWindow;
|
||||
|
||||
+23
-7
@@ -28,6 +28,8 @@ public static class UI
|
||||
public static readonly Option<RenderingModes> RenderingMode;
|
||||
public static readonly Option<bool> HideConsole;
|
||||
public static readonly Option<bool> Mock;
|
||||
public static readonly Option<int> Width;
|
||||
public static readonly Option<int> Height;
|
||||
|
||||
static UI()
|
||||
{
|
||||
@@ -56,6 +58,16 @@ public static class UI
|
||||
Description = "Запуск в фиктивном режиме без инициализации и использования периферии",
|
||||
DefaultValueFactory = (result) => false
|
||||
};
|
||||
Width = new("--width")
|
||||
{
|
||||
Description = "Ширина окна, если меньше или равна 0, то устанавливается ширина по-умолчанию",
|
||||
DefaultValueFactory = (result) => 0
|
||||
};
|
||||
Height = new("--height")
|
||||
{
|
||||
Description = "Высота окна, если меньше или равна 0, то устанавливается высота по-умолчанию",
|
||||
DefaultValueFactory = (result) => 0
|
||||
};
|
||||
|
||||
// Изменяем описания стандартных опций
|
||||
Helper.TranslateDefaultOptionDescriptions(RootCommand);
|
||||
@@ -65,12 +77,16 @@ public static class UI
|
||||
RootCommand.Add(RenderingMode);
|
||||
RootCommand.Add(HideConsole);
|
||||
RootCommand.Add(Mock);
|
||||
RootCommand.Add(Width);
|
||||
RootCommand.Add(Height);
|
||||
RootCommand.SetAction(CommandAction);
|
||||
|
||||
Command.Add(Fullscreen);
|
||||
Command.Add(RenderingMode);
|
||||
Command.Add(HideConsole);
|
||||
Command.Add(Mock);
|
||||
Command.Add(Width);
|
||||
Command.Add(Height);
|
||||
Command.SetAction(CommandAction);
|
||||
}
|
||||
|
||||
@@ -92,6 +108,8 @@ public static class UI
|
||||
var renderingMode = result.GetValue(RenderingMode);
|
||||
var hideConsole = result.GetValue(HideConsole);
|
||||
var mock = result.GetValue(Mock);
|
||||
var width = result.GetValue(Width);
|
||||
var height = result.GetValue(Height);
|
||||
|
||||
// Если выбран режим рендеринга DRM, то приложение будет рисоваться вместе с консолью,
|
||||
// поэтому если не перехватить консоль, то вывод в неё будет рисоваться вместе с интерфейсом
|
||||
@@ -105,7 +123,7 @@ public static class UI
|
||||
_ = host.RunAsync();
|
||||
|
||||
// Собираем графическое приложение
|
||||
var ui = BuildApplication(host, fullscreen);
|
||||
var ui = BuildApplication(host, fullscreen, width, height);
|
||||
switch (renderingMode)
|
||||
{
|
||||
case RenderingModes.X11:
|
||||
@@ -160,15 +178,13 @@ public static class UI
|
||||
builder.Services.AddAmineContentContext(builder.Configuration);
|
||||
|
||||
// Добавляем сервисы аппаратной части и прочее
|
||||
builder.Services.AddLightsService("Hardware:Lights");
|
||||
if (!mock)
|
||||
{
|
||||
builder.Services.AddLightsService("Hardware:Lights");
|
||||
builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
|
||||
// builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
|
||||
builder.Services.AddIlluminatorService("Hardware:Illuminator");
|
||||
builder.Services.AddCameraService("Hardware:Camera", true);
|
||||
builder.Services.AddComputeResourcesService();
|
||||
}
|
||||
builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images"));
|
||||
builder.Services.AddAmineContentImageCapturerService(mock);
|
||||
builder.Services.AddAmineContentAnalyzerService();
|
||||
|
||||
@@ -193,8 +209,8 @@ public static class UI
|
||||
return host;
|
||||
}
|
||||
|
||||
private static AppBuilder BuildApplication(IHost host, bool fullscreen) =>
|
||||
AppBuilder.Configure(() => new Application(host.Services, fullscreen))
|
||||
private static AppBuilder BuildApplication(IHost host, bool fullscreen, int width, int height) =>
|
||||
AppBuilder.Configure(() => new Application(host.Services, fullscreen, width, height))
|
||||
.UsePlatformDetect()
|
||||
.UseSkia()
|
||||
.LogToTrace()
|
||||
|
||||
@@ -27,10 +27,6 @@ public static class DependencyInjectionHelper
|
||||
.AddSingleton<AmineContentView>()
|
||||
// Core
|
||||
.AddSingleton<GSS2.UI.Core.ViewModels.MainWindowViewModel>()
|
||||
.AddSingleton<GSS2.UI.Core.ViewModels.CameraViewModel>()
|
||||
.AddSingleton<GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel>()
|
||||
.AddSingleton<GSS2.UI.Core.Views.CameraView>()
|
||||
.AddSingleton<GSS2.UI.Core.Views.IlluminatorControllerView>()
|
||||
// База
|
||||
.AddSingleton<ViewModels.DeleteConfirmViewModel>()
|
||||
.AddSingleton<ViewModels.NumericKeyboardViewModel>()
|
||||
@@ -38,16 +34,18 @@ public static class DependencyInjectionHelper
|
||||
.AddSingleton<Views.NumericKeyboardView>()
|
||||
// Общие
|
||||
.AddSingleton<ViewModels.Common.SystemViewModel>()
|
||||
.AddSingleton<ViewModels.Common.CameraViewModel>()
|
||||
.AddSingleton<Views.Common.SystemView>()
|
||||
.AddSingleton<Views.Common.CameraView>()
|
||||
// .AddSingleton<ViewModels.Common.CameraViewModel>()
|
||||
// .AddSingleton<Views.Common.SystemView>()
|
||||
// .AddSingleton<Views.Common.CameraView>()
|
||||
// AmineContent
|
||||
.AddSingleton<ViewModels.AmineContent.StartViewModel>()
|
||||
.AddSingleton<ViewModels.AmineContent.AnalysisViewModel>()
|
||||
.AddSingleton<ViewModels.AmineContent.SampleCalibrationViewModel>()
|
||||
.AddSingleton<ViewModels.AmineContent.SeparatorCalibrationViewModel>()
|
||||
.AddSingleton<ViewModels.AmineContent.BrandCalibrationViewModel>()
|
||||
.AddSingleton<Views.AmineContent.StartView>()
|
||||
.AddSingleton<Views.AmineContent.AnalysisView>()
|
||||
.AddSingleton<Views.AmineContent.SampleCalibrationView>()
|
||||
.AddSingleton<Views.AmineContent.SeparatorCalibrationView>();
|
||||
.AddSingleton<Views.AmineContent.SeparatorCalibrationView>()
|
||||
.AddSingleton<Views.AmineContent.BrandCalibrationView>();
|
||||
}
|
||||
|
||||
+6
-11
@@ -4,15 +4,7 @@ public partial class Program
|
||||
{
|
||||
// Аргументы командной строки
|
||||
private static List<string> _args = new List<string>();
|
||||
public static string[] Args
|
||||
{
|
||||
get
|
||||
{
|
||||
var copy = new string[_args.Count()];
|
||||
_args.CopyTo(copy);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
public static string[] Args => _args.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Точка входа
|
||||
@@ -60,8 +52,11 @@ public partial class Program
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Ошибка при выполнении программы:");
|
||||
Console.Write(ex.Message);
|
||||
Console.Write(ex.StackTrace);
|
||||
Console.Error.WriteLine(ex.Message);
|
||||
if (ex.InnerException is not null)
|
||||
Console.Error.WriteLine($"Внутреннее исключение: {ex.InnerException}");
|
||||
Console.Error.WriteLine(ex.StackTrace);
|
||||
Environment.Exit(1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,45 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя модель записи
|
||||
/// </summary>
|
||||
public partial class RecordViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.Id"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int Id { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.Uuid"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.CreateDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.EditDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.BrandName"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string BrandName { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.MixtureName"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string MixtureName { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.MixtureNormalRate"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1;
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.Comment"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string Comment { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логгер
|
||||
/// </summary>
|
||||
@@ -60,30 +99,33 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.Id"/> выбранного элемента
|
||||
/// Запись
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
|
||||
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.Uuid"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// Текущий экран информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.BrandName"/> выбранного элемента
|
||||
/// Можно ли переходить на следующий экран информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedBrandName { get; set; } = "";
|
||||
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.MixtureName"/> выбранного элемента
|
||||
/// Можно ли переходить на предыдущий экран информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedMixtureName { get; set; } = "";
|
||||
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
|
||||
/// <summary>
|
||||
/// <see cref="BrandRecord.MixtureNormalRate"/> выбранного элемента
|
||||
/// Происходит ли переход между экранами информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial double SelectedMixtureNormalRate { get; set; } = -1;
|
||||
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Можно ли производить перемещение между экранами
|
||||
/// Можно ли взаимодействовать с объектом
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool CanScroll { get; set; } = true;
|
||||
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
|
||||
|
||||
public BrandCalibrationViewModel(
|
||||
ILogger<BrandCalibrationViewModel> logger,
|
||||
@@ -109,8 +151,9 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
// Подписываемся на изменения свойств для обновления CanScroll
|
||||
PropertyChanged += (_, ea) =>
|
||||
{
|
||||
if (ea.PropertyName == nameof(MenuOpened))
|
||||
UpdateCanScroll();
|
||||
if (ea.PropertyName == nameof(MenuOpened) ||
|
||||
ea.PropertyName == nameof(InfoSectionIsScrolling))
|
||||
UpdateCanInteract();
|
||||
};
|
||||
|
||||
_logger.LogInformation("Инициализировано");
|
||||
@@ -133,11 +176,14 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
// Если запись найдена, то заполняем поля из базы данных
|
||||
if (selectedRecord is not null)
|
||||
{
|
||||
SelectedId = selectedRecord.Id;
|
||||
SelectedUuid = selectedRecord.Uuid;
|
||||
SelectedBrandName = selectedRecord.BrandName;
|
||||
SelectedMixtureName = selectedRecord.MixtureName;
|
||||
SelectedMixtureNormalRate = selectedRecord.MixtureNormalRate;
|
||||
Record.Id = selectedRecord.Id;
|
||||
Record.Uuid = selectedRecord.Uuid;
|
||||
Record.CreateDateTime = selectedRecord.CreateDateTime;
|
||||
Record.EditDateTime = selectedRecord.EditDateTime;
|
||||
Record.BrandName = selectedRecord.BrandName;
|
||||
Record.MixtureName = selectedRecord.MixtureName;
|
||||
Record.MixtureNormalRate = selectedRecord.MixtureNormalRate;
|
||||
Record.Comment = selectedRecord.Comment;
|
||||
return;
|
||||
}
|
||||
// Если значение не найдено в базе данных
|
||||
@@ -156,19 +202,24 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Если элемент не выбран, заполняем поля пустыми значениями
|
||||
_logger.LogTrace("Выбран новый элемент");
|
||||
SelectedId = 0;
|
||||
SelectedUuid = Guid.NewGuid().ToString();
|
||||
SelectedBrandName = "";
|
||||
SelectedMixtureName = "";
|
||||
SelectedMixtureNormalRate = -1;
|
||||
var dateTime = DateTime.UtcNow;
|
||||
Record.Id = 0;
|
||||
Record.Uuid = Guid.NewGuid().ToString();
|
||||
Record.CreateDateTime = dateTime;
|
||||
Record.EditDateTime = dateTime;
|
||||
Record.BrandName = "";
|
||||
Record.MixtureName = "";
|
||||
Record.MixtureNormalRate = -1;
|
||||
Record.Comment = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновить значение <see cref="CanScroll"/>
|
||||
/// Обновить значение <see cref="CanInteract"/>
|
||||
/// </summary>
|
||||
private void UpdateCanScroll()
|
||||
private void UpdateCanInteract()
|
||||
{
|
||||
CanScroll = !MenuOpened;
|
||||
CanInteract = !MenuOpened;
|
||||
CanInteract &= !InfoSectionIsScrolling;
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +236,15 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
/// Переключить видимость меню
|
||||
/// </summary>
|
||||
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
|
||||
|
||||
/// <summary>
|
||||
/// Переключить информационную секцию на следующую
|
||||
/// </summary>
|
||||
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
||||
/// <summary>
|
||||
/// Переключить информационную секцию на предыдущую
|
||||
/// </summary>
|
||||
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
||||
|
||||
/// <summary>
|
||||
/// Создать новую запись
|
||||
@@ -212,7 +272,7 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName);
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
|
||||
// Если запись не выбрана, то просто создаём новую запись
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
@@ -247,7 +307,7 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Сохраняем индекс удалённый записи и находим её в базе данных
|
||||
var deletedMenuIndex = MenuSelectedIndex;
|
||||
var deletedRecordId = SelectedId;
|
||||
var deletedRecordId = Record.Id;
|
||||
var deletedRecord = _context.BrandRecords.Find(deletedRecordId);
|
||||
// Создаём новую запись
|
||||
CreateRecord();
|
||||
@@ -299,17 +359,21 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName);
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
|
||||
// Если запись не выбрана
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
_logger.LogTrace("Создаём новую запись");
|
||||
var dateTime = DateTime.UtcNow;
|
||||
// Создаём новую запись
|
||||
var newRecord = new BrandRecord(
|
||||
uuid: SelectedUuid,
|
||||
brandName: SelectedBrandName,
|
||||
mixtureName: SelectedMixtureName,
|
||||
mixtureNormalRate: SelectedMixtureNormalRate
|
||||
uuid: Record.Uuid,
|
||||
createDateTime: dateTime,
|
||||
editDateTime: dateTime,
|
||||
brandName: Record.BrandName,
|
||||
mixtureName: Record.MixtureName,
|
||||
mixtureNormalRate: Record.MixtureNormalRate,
|
||||
comment: Record.Comment
|
||||
);
|
||||
|
||||
// Добавляем запись в базу данных
|
||||
@@ -328,22 +392,22 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
else
|
||||
{
|
||||
// Находим запись в базе данных
|
||||
var attachedRecord = _context.BrandRecords.Find(SelectedId);
|
||||
var attachedRecord = _context.BrandRecords.Find(Record.Id);
|
||||
// Если запись не найдена, то логгируем как ошибку
|
||||
if (attachedRecord is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.BrandRecords));
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.BrandRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Записываем изменения в базу данных");
|
||||
// Если в данных есть изменения, то записываем их
|
||||
if (attachedRecord.BrandName != SelectedBrandName)
|
||||
attachedRecord.BrandName = SelectedBrandName;
|
||||
if (attachedRecord.MixtureName != SelectedMixtureName)
|
||||
attachedRecord.MixtureName = SelectedMixtureName;
|
||||
if (attachedRecord.MixtureNormalRate != SelectedMixtureNormalRate)
|
||||
attachedRecord.MixtureNormalRate = SelectedMixtureNormalRate;
|
||||
|
||||
// Записываем изменения
|
||||
attachedRecord.EditDateTime = DateTime.UtcNow;
|
||||
attachedRecord.BrandName = Record.BrandName;
|
||||
attachedRecord.MixtureName = Record.MixtureName;
|
||||
attachedRecord.MixtureNormalRate = Record.MixtureNormalRate;
|
||||
attachedRecord.Comment = Record.Comment;
|
||||
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
@@ -351,8 +415,8 @@ public partial class BrandCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Записываем изменения в меню
|
||||
_logger.LogTrace("Записываем изменения в меню");
|
||||
MenuItems[MenuSelectedIndex].BrandName = SelectedBrandName;
|
||||
MenuItems[MenuSelectedIndex].MixtureName = SelectedMixtureName;
|
||||
MenuItems[MenuSelectedIndex].BrandName = Record.BrandName;
|
||||
MenuItems[MenuSelectedIndex].MixtureName = Record.MixtureName;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -53,6 +53,63 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя модель записи
|
||||
/// </summary>
|
||||
public partial class RecordViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.Id"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int Id { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.Uuid"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.CreateDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.EditDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SampleName"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SampleName { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SampleComment"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SampleComment { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SamplingDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SamplingPlace"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SamplingPlace { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.MixtureActualRate"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial double MixtureActualRate { get; set; } = -1;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.MeasuredContent"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial double MeasuredContent { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Представление <see cref="SampleRecord.ImageId"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Данные <see cref="SampleRecord.FeatureIds"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial List<float[]> Features { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логгер
|
||||
/// </summary>
|
||||
@@ -83,19 +140,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _captureImagesTaskCts = null;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.BrandId"/> выбранного элемента
|
||||
/// </summary>
|
||||
private int _selectedBrandId = 0;
|
||||
/// <summary>
|
||||
/// Данные <see cref="SampleRecord.ImageId"/> выбранного элемента
|
||||
/// </summary>
|
||||
private string? _selectedImageData = null;
|
||||
/// <summary>
|
||||
/// Данные <see cref="SampleRecord.FeatureIds"/> выбранного элемента
|
||||
/// </summary>
|
||||
private List<float[]> _selectedFeaturesData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Меню открыто
|
||||
/// </summary>
|
||||
@@ -109,43 +153,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Запись
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.Id"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.Uuid"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SampleName"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedSampleName { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SampleComment"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedSampleComment { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SamplingDateTime"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime SelectedSamplingDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.SamplingPlace"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedSamplingPlace { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.MixtureActualRate"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial double SelectedMixtureActualRate { get; set; } = -1;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.MeasuredContent"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial double SelectedMeasuredContent { get; set; } = -1;
|
||||
/// <summary>
|
||||
/// Представление <see cref="SampleRecord.ImageId"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Список элементов выпадающего списка марок
|
||||
/// </summary>
|
||||
@@ -172,7 +184,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Текущий экран информационной секции
|
||||
/// </summary>
|
||||
@@ -200,9 +211,9 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Можно ли производить перемещение между экранами
|
||||
/// Можно ли взаимодействовать с объектом
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool CanScroll { get; set; }
|
||||
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
|
||||
|
||||
public SampleCalibrationViewModel(
|
||||
ILogger<SampleCalibrationViewModel> logger,
|
||||
@@ -220,13 +231,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
_navigation = navigation;
|
||||
_logger.LogInformation("Инициализация");
|
||||
|
||||
// Получаем записи меню из базы данных
|
||||
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
|
||||
if (MenuItems.Count() > 0) // Если записи есть,
|
||||
MenuSelectedIndex = 0; // Выбираем первую запись
|
||||
else // Если записей нет,
|
||||
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
|
||||
|
||||
// Получаем записи выпадающего списка марок из базы данных
|
||||
BrandComboBoxItems = new ObservableCollection<BrandComboBoxItemViewModel>(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName)));
|
||||
// Подписываемся на изменения базы данных для отслеживания изменений списка марок
|
||||
@@ -242,7 +246,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
return;
|
||||
|
||||
// Если запись добавлена
|
||||
if (ea.NewState is EntityState.Added)
|
||||
if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged)
|
||||
{
|
||||
// Добавляем запись
|
||||
BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName));
|
||||
@@ -284,13 +288,20 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
});
|
||||
};
|
||||
|
||||
// Получаем записи меню из базы данных
|
||||
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
|
||||
if (MenuItems.Count() > 0) // Если записи есть,
|
||||
MenuSelectedIndex = 0; // Выбираем первую запись
|
||||
else // Если записей нет,
|
||||
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
|
||||
|
||||
// Подписываемся на изменения свойств для обновления CanScroll
|
||||
PropertyChanged += (_, ea) =>
|
||||
{
|
||||
if (ea.PropertyName == nameof(MenuOpened) ||
|
||||
ea.PropertyName == nameof(MainSectionIsScrolling) ||
|
||||
ea.PropertyName == nameof(InfoSectionIsScrolling))
|
||||
UpdateCanScroll();
|
||||
UpdateCanInteract();
|
||||
};
|
||||
|
||||
_logger.LogInformation("Инициализировано");
|
||||
@@ -314,32 +325,36 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
if (selectedRecord is not null)
|
||||
{
|
||||
// Заполняем простые поля
|
||||
SelectedId = selectedRecord.Id;
|
||||
SelectedUuid = selectedRecord.Uuid;
|
||||
SelectedSampleName = selectedRecord.SampleName;
|
||||
SelectedSampleComment = selectedRecord.SampleComment;
|
||||
SelectedSamplingDateTime = selectedRecord.SamplingDateTime;
|
||||
SelectedSamplingPlace = selectedRecord.SamplingPlace;
|
||||
SelectedMixtureActualRate = selectedRecord.MixtureActualRate;
|
||||
SelectedMeasuredContent = selectedRecord.MeasuredContent;
|
||||
_selectedBrandId = selectedRecord.BrandId;
|
||||
Record.Id = selectedRecord.Id;
|
||||
Record.Uuid = selectedRecord.Uuid;
|
||||
Record.CreateDateTime = selectedRecord.CreateDateTime;
|
||||
Record.EditDateTime = selectedRecord.EditDateTime;
|
||||
Record.SampleName = selectedRecord.SampleName;
|
||||
Record.SampleComment = selectedRecord.SampleComment;
|
||||
Record.SamplingDateTime = selectedRecord.SamplingDateTime;
|
||||
Record.SamplingPlace = selectedRecord.SamplingPlace;
|
||||
Record.MixtureActualRate = selectedRecord.MixtureActualRate;
|
||||
Record.MeasuredContent = selectedRecord.MeasuredContent;
|
||||
if (selectedRecord.Features.Count() > 0)
|
||||
Record.Features = selectedRecord.Features.Chunk(AnalyzerService.FEATURES_LENGTH).Select(v => v.ToArray()).ToList();
|
||||
else
|
||||
Record.Features = new List<float[]>();
|
||||
|
||||
// Ищем изображение в базе данных
|
||||
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
|
||||
// Если изображение не найдено в базе данных, то логгируем как ошибку
|
||||
if (imageRecord is null)
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
|
||||
// В любом случае заполняем изображение, оно заполнится null если запись не найдена
|
||||
_selectedImageData = imageRecord?.Base64;
|
||||
SelectedImage.Data = _selectedImageData;
|
||||
// В любом случае заполняем изображение
|
||||
Record.Image.Data = imageRecord?.Base64 ?? "";
|
||||
|
||||
// Ищем марку в базе данных
|
||||
var brandRecord = _context.BrandRecords.Find(_selectedBrandId);
|
||||
var brandRecord = _context.BrandRecords.Find(selectedRecord.BrandId);
|
||||
// Если марка не найдена в базе данных
|
||||
if (brandRecord is null)
|
||||
{
|
||||
// Логгируем как ошибку
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", _selectedBrandId, nameof(_context.BrandRecords));
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.BrandId, nameof(_context.BrandRecords));
|
||||
// Устанавливаем, что элемент не выбран
|
||||
BrandComboBoxSelectedIndex = -1;
|
||||
}
|
||||
@@ -356,12 +371,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
BrandComboBoxSelectedIndex = BrandComboBoxItems.IndexOf(brandComboBoxItem);
|
||||
}
|
||||
|
||||
// Ищем все признаки в базе данных
|
||||
_selectedFeaturesData = _context.FeatureRecords
|
||||
.Where(r => r.SampleRecordId == SelectedId)
|
||||
.Select(r => r.Values.ToArray())
|
||||
.ToList();
|
||||
|
||||
return;
|
||||
}
|
||||
// Если значение не найдено в базе данных
|
||||
@@ -380,36 +389,30 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Если элемент не выбран, заполняем поля пустыми значениями
|
||||
_logger.LogTrace("Выбран новый элемент");
|
||||
SelectedId = 0;
|
||||
SelectedUuid = Guid.NewGuid().ToString();
|
||||
SelectedSampleName = "";
|
||||
SelectedSampleComment = "";
|
||||
SelectedSamplingDateTime = DateTime.UtcNow;
|
||||
SelectedSamplingPlace = "";
|
||||
SelectedMixtureActualRate = -1;
|
||||
SelectedMeasuredContent = -1;
|
||||
SelectedImage.Data = null;
|
||||
var dateTime = DateTime.UtcNow;
|
||||
Record.Id = 0;
|
||||
Record.Uuid = Guid.NewGuid().ToString();
|
||||
Record.CreateDateTime = dateTime;
|
||||
Record.EditDateTime = dateTime;
|
||||
Record.SampleName = "";
|
||||
Record.SampleComment = "";
|
||||
Record.SamplingDateTime = dateTime;
|
||||
Record.SamplingPlace = "";
|
||||
Record.MixtureActualRate = -1;
|
||||
Record.MeasuredContent = -1;
|
||||
Record.Image.Data = "";
|
||||
Record.Features = new List<float[]>();
|
||||
BrandComboBoxSelectedIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при изменении <see cref="BrandComboBoxSelectedIndexC"/>
|
||||
/// Обновить значение <see cref="CanInteract"/>
|
||||
/// </summary>
|
||||
partial void OnBrandComboBoxSelectedIndexChanged(int value)
|
||||
private void UpdateCanInteract()
|
||||
{
|
||||
if (value >= 0 && value < BrandComboBoxItems.Count())
|
||||
_selectedBrandId = BrandComboBoxItems[value].Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновить значение <see cref="CanScroll"/>
|
||||
/// </summary>
|
||||
private void UpdateCanScroll()
|
||||
{
|
||||
CanScroll = !MenuOpened;
|
||||
CanScroll &= !MainSectionIsScrolling;
|
||||
CanScroll &= !InfoSectionIsScrolling;
|
||||
CanScroll &= !ImagesIsCapturing;
|
||||
CanInteract = !MenuOpened;
|
||||
CanInteract &= !MainSectionIsScrolling;
|
||||
CanInteract &= !InfoSectionIsScrolling;
|
||||
}
|
||||
|
||||
// Команды управления меню
|
||||
@@ -449,7 +452,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
|
||||
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
|
||||
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
|
||||
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
|
||||
// поэтому если Record.Record != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
|
||||
if (MenuSelectedIndex != -1)
|
||||
MenuSelectedIndex = -1;
|
||||
else
|
||||
@@ -465,7 +468,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName);
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.SampleName), Record.SampleName);
|
||||
// Если запись не выбрана, то просто создаём новую запись
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
@@ -500,7 +503,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Сохраняем индекс удалённый записи и находим её в базе данных
|
||||
var deletedMenuIndex = MenuSelectedIndex;
|
||||
var deletedRecordId = SelectedId;
|
||||
var deletedRecordId = Record.Id;
|
||||
var deletedRecord = _context.SampleRecords.Find(deletedRecordId);
|
||||
// Создаём новую запись
|
||||
CreateRecord();
|
||||
@@ -510,7 +513,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
|
||||
if (deletedRecord is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SampleRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,33 +555,50 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName);
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.SampleName), Record.SampleName);
|
||||
// Если запись не выбрана
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
// Создаём запись изображения
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||||
// Добавляем запись изображения в базу данных
|
||||
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
var imageId = 0;
|
||||
if (!string.IsNullOrEmpty(Record.Image.Data))
|
||||
{
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: Record.Image.Data);
|
||||
// Добавляем запись изображения в базу данных
|
||||
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
imageId = attachedImageRecord.Entity.Id;
|
||||
}
|
||||
|
||||
// Определяем Id выбранной марки
|
||||
var selectedBrandId = 0;
|
||||
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
|
||||
if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count())
|
||||
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count());
|
||||
// Если значение больше 0, то достаём Id из списка элементов
|
||||
else if (BrandComboBoxSelectedIndex >= 0)
|
||||
selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id;
|
||||
|
||||
_logger.LogTrace("Создаём новую запись");
|
||||
var dateTime = DateTime.UtcNow;
|
||||
// Заполняем запись
|
||||
var newRecord = new SampleRecord(
|
||||
uuid: SelectedUuid,
|
||||
sampleName: SelectedSampleName,
|
||||
sampleComment: SelectedSampleComment,
|
||||
samplingDateTime: SelectedSamplingDateTime,
|
||||
brandId: _selectedBrandId,
|
||||
samplingPlace: SelectedSamplingPlace,
|
||||
mixtureActualRate: SelectedMixtureActualRate,
|
||||
measuredContent: SelectedMeasuredContent,
|
||||
imageId: attachedImageRecord.Entity.Id,
|
||||
featureIds: []
|
||||
uuid: Record.Uuid,
|
||||
createDateTime: dateTime,
|
||||
editDateTime: dateTime,
|
||||
sampleName: Record.SampleName,
|
||||
sampleComment: Record.SampleComment,
|
||||
samplingDateTime: Record.SamplingDateTime,
|
||||
brandId: selectedBrandId,
|
||||
samplingPlace: Record.SamplingPlace,
|
||||
mixtureActualRate: Record.MixtureActualRate,
|
||||
measuredContent: Record.MeasuredContent,
|
||||
imageId: imageId,
|
||||
features: Record.Features
|
||||
);
|
||||
// Добавляем запись в базу данных
|
||||
var attachedRecord = _context.SampleRecords.Add(newRecord);
|
||||
@@ -586,29 +606,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись сохранена");
|
||||
|
||||
_logger.LogTrace("Создаём новые записи признаков");
|
||||
// Заполняем записи признаков и добавляем их в базу данных
|
||||
var attachedFeatureRecords = new List<FeatureRecord>();
|
||||
foreach (var feature in _selectedFeaturesData)
|
||||
{
|
||||
var featureRecord = new FeatureRecord(
|
||||
values: feature,
|
||||
sampleRecordId: attachedRecord.Entity.Id
|
||||
);
|
||||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||||
attachedFeatureRecords.Add(attachedFeatureRecord.Entity);
|
||||
}
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Записи признаков сохранены");
|
||||
|
||||
_logger.LogTrace("Заполняем признаки в сохранённой записи");
|
||||
// Добавляем идентификаторы признаков в запись
|
||||
attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id));
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Сохранение записи завершено");
|
||||
|
||||
// Добавляем новый элемент в меню
|
||||
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName));
|
||||
// Выбираем последний элемент из меню
|
||||
@@ -618,11 +615,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
else
|
||||
{
|
||||
// Находим запись в базе данных
|
||||
var attachedRecord = _context.SampleRecords.Find(SelectedId);
|
||||
var attachedRecord = _context.SampleRecords.Find(Record.Id);
|
||||
// Если запись не найдена, то логгируем как ошибку
|
||||
if (attachedRecord is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SampleRecords));
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SampleRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -630,79 +627,65 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Ищем запись изображения в базе данных
|
||||
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId);
|
||||
// Если запись не найдена
|
||||
// Если запись изображения не найдена
|
||||
if (attachedImageRecord is null)
|
||||
{
|
||||
// Логгируем как ошибку
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords));
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||||
// Добавляем запись изображения в базу данных
|
||||
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
}
|
||||
|
||||
// Ищем записи признаков в базе данных
|
||||
var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SampleRecordId == SelectedId);
|
||||
// Если количество признаков в базе данных больше чем текущее
|
||||
if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count())
|
||||
{
|
||||
// Находим количество признаков которое нужно удалить
|
||||
var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count();
|
||||
// Логгируем
|
||||
_logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count());
|
||||
// Удаляем необходимое количество признаков из конца
|
||||
attachedFeatureRecords.TakeLast(featuresRecordsToRemove);
|
||||
}
|
||||
|
||||
// Заполняем все имеющиеся признаки в базе данных новыми значениями
|
||||
foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate())
|
||||
attachedFeatureRecord.Values = _selectedFeaturesData[i];
|
||||
|
||||
// Если количество признаков в безе данных меньше чем текущее
|
||||
if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count())
|
||||
{
|
||||
// Находим количество признаков которое нужно добавить
|
||||
var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count();
|
||||
// Логгируем
|
||||
_logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count());
|
||||
// Заполняем записи недостающих признаков и добавляем их в базу данных
|
||||
for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++)
|
||||
// Если изображение есть
|
||||
if (!string.IsNullOrEmpty(Record.Image.Data))
|
||||
{
|
||||
var featureRecord = new FeatureRecord(
|
||||
values: _selectedFeaturesData[i],
|
||||
sampleRecordId: attachedRecord.Id
|
||||
);
|
||||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||||
attachedFeatureRecords.Append(attachedFeatureRecord.Entity);
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: Record.Image.Data);
|
||||
// Добавляем запись изображения в базу данных
|
||||
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
attachedRecord.ImageId = attachedImageRecord.Id;
|
||||
}
|
||||
}
|
||||
// Если запись изображения найдена
|
||||
else
|
||||
{
|
||||
// Если изображение есть
|
||||
if (!string.IsNullOrEmpty(Record.Image.Data))
|
||||
{
|
||||
_logger.LogTrace("Записываем изменения в запись изображения");
|
||||
// Вносим изменения
|
||||
attachedImageRecord.Base64 = Record.Image.Data;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
}
|
||||
// Если изображения нет
|
||||
else
|
||||
{
|
||||
// Удаляем изображение из базы данных
|
||||
_context.ImageRecords.Remove(attachedImageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
attachedRecord.ImageId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Записи признаков сохранена");
|
||||
// Определяем Id выбранной марки
|
||||
var selectedBrandId = 0;
|
||||
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
|
||||
if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count())
|
||||
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count());
|
||||
// Если значение больше 0, то достаём Id из списка элементов
|
||||
else if (BrandComboBoxSelectedIndex >= 0)
|
||||
selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id;
|
||||
|
||||
// Если в данных есть изменения, то записываем их
|
||||
if (attachedRecord.SampleName != SelectedSampleName)
|
||||
attachedRecord.SampleName = SelectedSampleName;
|
||||
if (attachedRecord.SampleComment != SelectedSampleComment)
|
||||
attachedRecord.SampleComment = SelectedSampleComment;
|
||||
if (attachedRecord.SamplingDateTime != SelectedSamplingDateTime)
|
||||
attachedRecord.SamplingDateTime = SelectedSamplingDateTime;
|
||||
if (attachedRecord.BrandId != _selectedBrandId)
|
||||
attachedRecord.BrandId = _selectedBrandId;
|
||||
if (attachedRecord.SamplingPlace != SelectedSamplingPlace)
|
||||
attachedRecord.SamplingPlace = SelectedSamplingPlace;
|
||||
if (attachedRecord.MixtureActualRate != SelectedMixtureActualRate)
|
||||
attachedRecord.MixtureActualRate = SelectedMixtureActualRate;
|
||||
if (attachedRecord.MeasuredContent != SelectedMeasuredContent)
|
||||
attachedRecord.MeasuredContent = SelectedMeasuredContent;
|
||||
if (attachedRecord.ImageId != attachedImageRecord.Id)
|
||||
attachedRecord.ImageId = attachedImageRecord.Id;
|
||||
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
|
||||
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
|
||||
// Записываем изменения
|
||||
attachedRecord.EditDateTime = DateTime.UtcNow;
|
||||
attachedRecord.SampleName = Record.SampleName;
|
||||
attachedRecord.SampleComment = Record.SampleComment;
|
||||
attachedRecord.SamplingDateTime = Record.SamplingDateTime;
|
||||
attachedRecord.BrandId = selectedBrandId;
|
||||
attachedRecord.SamplingPlace = Record.SamplingPlace;
|
||||
attachedRecord.MixtureActualRate = Record.MixtureActualRate;
|
||||
attachedRecord.MeasuredContent = Record.MeasuredContent;
|
||||
attachedRecord.Features = Record.Features.SelectMany(fs => fs).ToList();
|
||||
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
@@ -710,7 +693,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Записываем изменения в меню
|
||||
_logger.LogTrace("Записываем изменения в меню");
|
||||
MenuItems[MenuSelectedIndex].SampleName = SelectedSampleName;
|
||||
MenuItems[MenuSelectedIndex].SampleName = Record.SampleName;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -736,10 +719,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
_captureImagesTaskCts = new CancellationTokenSource();
|
||||
var cancellationToken = _captureImagesTaskCts.Token;
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||||
ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}";
|
||||
|
||||
// Запускаем задачу получения изображений
|
||||
_сaptureImagesTask = Task.Run(async () =>
|
||||
@@ -748,7 +728,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
var imagesData = new List<Task<ImageData>>();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
|
||||
|
||||
@@ -761,7 +741,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||||
|
||||
@@ -793,7 +773,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
|
||||
|
||||
@@ -846,7 +826,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
var (avgImage, roi, features) = result.Value;
|
||||
|
||||
// Сохраняем вектор признаков
|
||||
_selectedFeaturesData = features;
|
||||
Record.Features = features;
|
||||
|
||||
// Обрабатываем среднее изображение
|
||||
avgImage = new OpenCvSharp.Mat(avgImage, roi);
|
||||
@@ -861,15 +841,14 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Кодируем среднее изображение
|
||||
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
|
||||
_selectedImageData = Convert.ToBase64String(avgData);
|
||||
|
||||
// Очищаем память среднего изображения
|
||||
avgImage.Dispose();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData);
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
|
||||
}, cancellationToken);
|
||||
|
||||
// Создаём задачу которая выполнится после завершения всех других
|
||||
@@ -922,7 +901,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
|
||||
@@ -35,6 +35,45 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Внутренняя модель записи
|
||||
/// </summary>
|
||||
public partial class RecordViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Id"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int Id { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Uuid"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.CreateDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SampleRecord.EditDateTime"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Type"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string Type { get; set; } = "";
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Comment"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string Comment { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Представление <see cref="SeparatorRecord.ImageId"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Данные <see cref="SeparatorRecord.Features"/>
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial List<float[]> Features { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логгер
|
||||
/// </summary>
|
||||
@@ -65,15 +104,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _captureImagesTaskCts = null;
|
||||
|
||||
/// <summary>
|
||||
/// Данные <see cref="SeparatorRecord.ImageId"/> выбранного элемента
|
||||
/// </summary>
|
||||
private string? _selectedImageData = null;
|
||||
/// <summary>
|
||||
/// Данные <see cref="SeparatorRecord.FeatureIds"/> выбранного элемента
|
||||
/// </summary>
|
||||
private List<float[]> _selectedFeaturesData = new();
|
||||
|
||||
/// <summary>
|
||||
/// Меню открыто
|
||||
/// </summary>
|
||||
@@ -88,21 +118,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Id"/> выбранного элемента
|
||||
/// Запись
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Uuid"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
|
||||
/// <summary>
|
||||
/// <see cref="SeparatorRecord.Type"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial string SelectedType { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Представление <see cref="SeparatorRecord.ImageId"/> выбранного элемента
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
|
||||
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Текущий экран главной секции
|
||||
@@ -112,6 +130,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
/// Можно ли переходить на следующий экран главной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Можно ли переходить на предыдущий экран главной секции
|
||||
/// </summary>
|
||||
@@ -120,6 +139,22 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
/// Происходит ли переход между экранами главной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Текущий экран информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
|
||||
/// <summary>
|
||||
/// Можно ли переходить на следующий экран информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Можно ли переходить на предыдущий экран информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Происходит ли переход между экранами информационной секции
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Проводится ли съёмка изображений
|
||||
@@ -131,9 +166,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Можно ли производить перемещение между экранами
|
||||
/// Можно ли взаимодействовать с объектом
|
||||
/// </summary>
|
||||
[ObservableProperty] public partial bool CanScroll { get; set; }
|
||||
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
|
||||
|
||||
public SeparatorCalibrationViewModel(
|
||||
ILogger<SeparatorCalibrationViewModel> logger,
|
||||
@@ -162,8 +197,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
PropertyChanged += (_, ea) =>
|
||||
{
|
||||
if (ea.PropertyName == nameof(MenuOpened) ||
|
||||
ea.PropertyName == nameof(MainSectionIsScrolling))
|
||||
UpdateCanScroll();
|
||||
ea.PropertyName == nameof(MainSectionIsScrolling) ||
|
||||
ea.PropertyName == nameof(InfoSectionIsScrolling))
|
||||
UpdateCanInteract();
|
||||
};
|
||||
|
||||
_logger.LogInformation("Инициализировано");
|
||||
@@ -187,24 +223,26 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
if (selectedRecord is not null)
|
||||
{
|
||||
// Заполняем простые поля
|
||||
SelectedId = selectedRecord.Id;
|
||||
SelectedUuid = selectedRecord.Uuid;
|
||||
SelectedType = selectedRecord.Type;
|
||||
Record.Id = selectedRecord.Id;
|
||||
Record.Uuid = selectedRecord.Uuid;
|
||||
Record.CreateDateTime = selectedRecord.CreateDateTime;
|
||||
Record.EditDateTime = selectedRecord.EditDateTime;
|
||||
Record.Type = selectedRecord.Type;
|
||||
Record.Comment = selectedRecord.Comment;
|
||||
if (selectedRecord.Features.Count() > 0)
|
||||
Record.Features = selectedRecord.Features.Chunk(AnalyzerService.FEATURES_LENGTH).Select(v => v.ToArray()).ToList();
|
||||
else
|
||||
Record.Features = new List<float[]>();
|
||||
|
||||
// Ищем изображение в базе данных
|
||||
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
|
||||
// Если изображение не найдено в базе данных, то логгируем как ошибку
|
||||
if (imageRecord is null)
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
|
||||
// В любом случае заполняем изображение, оно заполнится null если запись не найдена
|
||||
_selectedImageData = imageRecord?.Base64;
|
||||
SelectedImage.Data = _selectedImageData;
|
||||
// В любом случае заполняем изображение
|
||||
Record.Image.Data = imageRecord?.Base64 ?? "";
|
||||
|
||||
// Ищем все признаки в базе данных
|
||||
_selectedFeaturesData = _context.FeatureRecords
|
||||
.Where(r => r.SeparatorRecordId == SelectedId)
|
||||
.Select(r => r.Values.ToArray())
|
||||
.ToList();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -224,20 +262,25 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Если элемент не выбран, заполняем поля пустыми значениями
|
||||
_logger.LogTrace("Выбран новый элемент");
|
||||
SelectedId = 0;
|
||||
SelectedUuid = Guid.NewGuid().ToString();
|
||||
SelectedType = "";
|
||||
SelectedImage.Data = null;
|
||||
var dateTime = DateTime.UtcNow;
|
||||
Record.Id = 0;
|
||||
Record.Uuid = Guid.NewGuid().ToString();
|
||||
Record.CreateDateTime = dateTime;
|
||||
Record.EditDateTime = dateTime;
|
||||
Record.Type = "";
|
||||
Record.Comment = "";
|
||||
Record.Image.Data = "";
|
||||
Record.Features = new List<float[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновить значение <see cref="CanScroll"/>
|
||||
/// Обновить значение <see cref="CanInteract"/>
|
||||
/// </summary>
|
||||
private void UpdateCanScroll()
|
||||
private void UpdateCanInteract()
|
||||
{
|
||||
CanScroll = !MenuOpened;
|
||||
CanScroll &= !MainSectionIsScrolling;
|
||||
CanScroll &= !ImagesIsCapturing;
|
||||
CanInteract = !MenuOpened;
|
||||
CanInteract &= !MainSectionIsScrolling;
|
||||
CanInteract &= !InfoSectionIsScrolling;
|
||||
}
|
||||
|
||||
// Команды управления меню
|
||||
@@ -258,7 +301,15 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
/// Переключить главную секцию информация <-> изображения
|
||||
/// </summary>
|
||||
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Переключить информационную секцию на следующую
|
||||
/// </summary>
|
||||
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
|
||||
/// <summary>
|
||||
/// Переключить информационную секцию на предыдущую
|
||||
/// </summary>
|
||||
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
|
||||
|
||||
/// <summary>
|
||||
/// Создать новую запись
|
||||
/// </summary>
|
||||
@@ -285,7 +336,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
|
||||
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), Record.Id, nameof(SeparatorRecord.Type), Record.Type);
|
||||
// Если запись не выбрана, то просто создаём новую запись
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
@@ -320,7 +371,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Сохраняем индекс удалённый записи и находим её в базе данных
|
||||
var deletedMenuIndex = MenuSelectedIndex;
|
||||
var deletedRecordId = SelectedId;
|
||||
var deletedRecordId = Record.Id;
|
||||
var deletedRecord = _context.SeparatorRecords.Find(deletedRecordId);
|
||||
// Создаём новую запись
|
||||
CreateRecord();
|
||||
@@ -330,7 +381,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку
|
||||
if (deletedRecord is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SeparatorRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -372,27 +423,36 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType);
|
||||
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), Record.Id, nameof(SeparatorRecord.Type), Record.Type);
|
||||
// Если запись не выбрана
|
||||
if (MenuSelectedIndex < 0)
|
||||
{
|
||||
// Создаём запись изображения
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||||
// Добавляем запись изображения в базу данных
|
||||
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
var imageId = 0;
|
||||
if (!string.IsNullOrEmpty(Record.Image.Data))
|
||||
{
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: Record.Image.Data);
|
||||
// Добавляем запись изображения в базу данных
|
||||
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
imageId = attachedImageRecord.Entity.Id;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Создаём новую запись");
|
||||
var dateTime = DateTime.UtcNow;
|
||||
// Заполняем запись
|
||||
var newRecord = new SeparatorRecord(
|
||||
uuid: SelectedUuid,
|
||||
type: SelectedType,
|
||||
imageId: attachedImageRecord.Entity.Id,
|
||||
featureIds: []
|
||||
uuid: Record.Uuid,
|
||||
createDateTime: dateTime,
|
||||
editDateTime: dateTime,
|
||||
type: Record.Type,
|
||||
comment: Record.Comment,
|
||||
imageId: imageId,
|
||||
features: Record.Features
|
||||
);
|
||||
// Добавляем запись в базу данных
|
||||
var attachedRecord = _context.SeparatorRecords.Add(newRecord);
|
||||
@@ -400,29 +460,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись сохранена");
|
||||
|
||||
_logger.LogTrace("Создаём новые записи признаков");
|
||||
// Заполняем записи признаков и добавляем их в базу данных
|
||||
var attachedFeatureRecords = new List<FeatureRecord>();
|
||||
foreach (var feature in _selectedFeaturesData)
|
||||
{
|
||||
var featureRecord = new FeatureRecord(
|
||||
values: feature,
|
||||
sampleRecordId: attachedRecord.Entity.Id
|
||||
);
|
||||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||||
attachedFeatureRecords.Add(attachedFeatureRecord.Entity);
|
||||
}
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Записи признаков сохранены");
|
||||
|
||||
_logger.LogTrace("Заполняем признаки в сохранённой записи");
|
||||
// Добавляем идентификаторы признаков в запись
|
||||
attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id));
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Сохранение записи завершено");
|
||||
|
||||
// Добавляем новый элемент в меню
|
||||
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.Type));
|
||||
// Выбираем последний элемент из меню
|
||||
@@ -432,11 +469,11 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
else
|
||||
{
|
||||
// Находим запись в базе данных
|
||||
var attachedRecord = _context.SeparatorRecords.Find(SelectedId);
|
||||
var attachedRecord = _context.SeparatorRecords.Find(Record.Id);
|
||||
// Если запись не найдена, то логгируем как ошибку
|
||||
if (attachedRecord is null)
|
||||
{
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SeparatorRecords));
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SeparatorRecords));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -449,62 +486,48 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
{
|
||||
// Логгируем как ошибку
|
||||
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords));
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
|
||||
// Добавляем запись изображения в базу данных
|
||||
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
}
|
||||
|
||||
// Ищем записи признаков в базе данных
|
||||
var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SeparatorRecordId == SelectedId);
|
||||
// Если количество признаков в базе данных больше чем текущее
|
||||
if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count())
|
||||
{
|
||||
// Находим количество признаков которое нужно удалить
|
||||
var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count();
|
||||
// Логгируем
|
||||
_logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count());
|
||||
// Удаляем необходимое количество признаков из конца
|
||||
attachedFeatureRecords.TakeLast(featuresRecordsToRemove);
|
||||
}
|
||||
|
||||
// Заполняем все имеющиеся признаки в базе данных новыми значениями
|
||||
foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate())
|
||||
attachedFeatureRecord.Values = _selectedFeaturesData[i];
|
||||
|
||||
// Если количество признаков в безе данных меньше чем текущее
|
||||
if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count())
|
||||
{
|
||||
// Находим количество признаков которое нужно добавить
|
||||
var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count();
|
||||
// Логгируем
|
||||
_logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count());
|
||||
// Заполняем записи недостающих признаков и добавляем их в базу данных
|
||||
for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++)
|
||||
// Если изображение есть
|
||||
if (!string.IsNullOrEmpty(Record.Image.Data))
|
||||
{
|
||||
var featureRecord = new FeatureRecord(
|
||||
values: _selectedFeaturesData[i],
|
||||
sampleRecordId: attachedRecord.Id
|
||||
);
|
||||
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
|
||||
attachedFeatureRecords.Append(attachedFeatureRecord.Entity);
|
||||
_logger.LogTrace("Создаём новую запись изображения");
|
||||
// Заполняем запись изображения
|
||||
var imageRecord = new ImageRecord(base64: Record.Image.Data);
|
||||
// Добавляем запись изображения в базу данных
|
||||
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Запись изображения сохранена");
|
||||
attachedRecord.ImageId = attachedImageRecord.Id;
|
||||
}
|
||||
}
|
||||
// Если запись найдена
|
||||
else
|
||||
{
|
||||
// Если изображение есть
|
||||
if (!string.IsNullOrEmpty(Record.Image.Data))
|
||||
{
|
||||
_logger.LogTrace("Записываем изменения в запись изображения");
|
||||
// Вносим изменения
|
||||
attachedImageRecord.Base64 = Record.Image.Data;
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
}
|
||||
// Если изображения нет
|
||||
else
|
||||
{
|
||||
// Удаляем изображение из базы данных
|
||||
_context.ImageRecords.Remove(attachedImageRecord);
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
attachedRecord.ImageId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_context.SaveChanges();
|
||||
_logger.LogTrace("Записи признаков сохранена");
|
||||
|
||||
// Если в данных есть изменения, то записываем их
|
||||
if (attachedRecord.Type != SelectedType)
|
||||
attachedRecord.Type = SelectedType;
|
||||
if (attachedRecord.ImageId != attachedImageRecord.Id)
|
||||
attachedRecord.ImageId = attachedImageRecord.Id;
|
||||
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
|
||||
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
|
||||
// Записываем изменения
|
||||
attachedRecord.EditDateTime = DateTime.UtcNow;
|
||||
attachedRecord.Type = Record.Type;
|
||||
attachedRecord.Comment = Record.Comment;
|
||||
attachedRecord.Features = Record.Features.SelectMany(fs => fs).ToList();
|
||||
|
||||
// Сохраняем изменения в базе данных
|
||||
_context.SaveChanges();
|
||||
@@ -512,7 +535,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Записываем изменения в меню
|
||||
_logger.LogTrace("Записываем изменения в меню");
|
||||
MenuItems[MenuSelectedIndex].Type = SelectedType;
|
||||
MenuItems[MenuSelectedIndex].Type = Record.Type;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -538,10 +561,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
_captureImagesTaskCts = new CancellationTokenSource();
|
||||
var cancellationToken = _captureImagesTaskCts.Token;
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||||
ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}";
|
||||
|
||||
// Запускаем получение изображений асинхронно
|
||||
_сaptureImagesTask = Task.Run(async () =>
|
||||
@@ -550,7 +570,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
var imagesData = new List<Task<ImageData>>();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
|
||||
|
||||
@@ -563,7 +583,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
|
||||
|
||||
@@ -595,7 +615,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
|
||||
|
||||
@@ -648,7 +668,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
var (avgImage, roi, features) = result.Value;
|
||||
|
||||
// Сохраняем вектор признаков
|
||||
_selectedFeaturesData = features;
|
||||
Record.Features = features;
|
||||
|
||||
// Обрабатываем среднее изображение
|
||||
avgImage = new OpenCvSharp.Mat(avgImage, roi);
|
||||
@@ -663,15 +683,14 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
|
||||
// Кодируем среднее изображение
|
||||
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
|
||||
_selectedImageData = Convert.ToBase64String(avgData);
|
||||
|
||||
// Очищаем память среднего изображения
|
||||
avgImage.Dispose();
|
||||
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData);
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
|
||||
}, cancellationToken);
|
||||
|
||||
// Создаём задачу которая выполнится после завершения всех других
|
||||
@@ -724,7 +743,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
// Обновляем данные в интерфейсе
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
|
||||
// Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
|
||||
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
|
||||
_ = Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
@@ -737,6 +756,8 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
|
||||
});
|
||||
}
|
||||
|
||||
// Команда отмены задачи съёмки изображений
|
||||
/// <summary>
|
||||
/// Отменить задачу съёмки изображения
|
||||
/// </summary>
|
||||
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
using GSS2.UI.Core.ViewModels;
|
||||
|
||||
namespace GSS2.ViewModels.Common;
|
||||
|
||||
public partial class CameraViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty] public partial GSS2.UI.Core.ViewModels.CameraViewModel Camera { get; set; }
|
||||
[ObservableProperty] public partial GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel Illuminator { get; set; }
|
||||
|
||||
public CameraViewModel(
|
||||
GSS2.UI.Core.ViewModels.CameraViewModel camera,
|
||||
GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel illuminator
|
||||
)
|
||||
{
|
||||
Camera = camera;
|
||||
Illuminator = illuminator;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,12 @@ public partial class AnalysisView : UserControl
|
||||
|
||||
private void OnChartsUpdate(object? sender, EventArgs ea)
|
||||
{
|
||||
MeanValuesChart.CoreChart.Update();
|
||||
VariancesChart.CoreChart.Update();
|
||||
QualitiesChart.CoreChart.Update();
|
||||
ProcessedAreasHistogram.CoreChart.Update();
|
||||
UnprocessedAreasHistogram.CoreChart.Update();
|
||||
HotspotAreasHistogram.CoreChart.Update();
|
||||
OpticalIntegralsHistogram.CoreChart.Update();
|
||||
InnerStdDevsHistogram.CoreChart.Update();
|
||||
InnerHeterogeneitiesHistogram.CoreChart.Update();
|
||||
ParticleValuesHistogram.CoreChart.Update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<Grid Background="{StaticResource MainBackground}">
|
||||
|
||||
<!-- Основная форма -->
|
||||
<Grid IsEnabled="{Binding !MenuOpened}">
|
||||
<Grid IsEnabled="{Binding CanInteract}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
|
||||
@@ -40,12 +40,14 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" /> <!-- Информация -->
|
||||
<RowDefinition Height="*" /> <!-- Информация -->
|
||||
<RowDefinition Height="Auto" /> <!-- Кнопки -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
@@ -55,115 +57,201 @@
|
||||
Classes="vertical"
|
||||
Command="{Binding ToggleMenuCommand}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
VerticalAlignment="Stretch"
|
||||
>
|
||||
<TextBlock Text="☰" />
|
||||
<TextBlock Text="☰"/>
|
||||
</Button>
|
||||
|
||||
<!-- Информация -->
|
||||
<Grid Grid.Column="1">
|
||||
<core:SectionPanel
|
||||
CanScrollBackward="{Binding InfoSectionCanScrollBackward}"
|
||||
CanScrollForward="{Binding InfoSectionCanScrollForward}"
|
||||
ClipToBounds="True"
|
||||
CurrentIndex="{Binding InfoSectionCurrentIndex}"
|
||||
Cyclic="True"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="0"
|
||||
IsScrolling="{Binding InfoSectionIsScrolling}"
|
||||
Orientation="Horizontal"
|
||||
>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- Id / Uuid / BrandName / MixtureName / MixtureNormalRate -->
|
||||
<Grid>
|
||||
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- BrandName (подпись) | MixtureName (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- BrandName (поле ) | MixtureName (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- MixtureNormalRate (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- MixtureNormalRate (поле ) -->
|
||||
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- BrandName (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- BrandName (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- MixtureName (подпись) | MixtureNormalRate (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- MixtureName (поле ) | MixtureNormalRate (поле ) -->
|
||||
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Id (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Text="ИД:"
|
||||
/>
|
||||
<!-- Id (поле) -->
|
||||
<TextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="ИД"
|
||||
Text="{Binding SelectedId}"
|
||||
/>
|
||||
<!-- Uuid (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Text="Уникальный ИД:"
|
||||
/>
|
||||
<!-- Uuid (поле) -->
|
||||
<TextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="Уникальный ИД"
|
||||
Text="{Binding SelectedUuid}"
|
||||
/>
|
||||
<!-- BrandName (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
Text="Марка продукта:"
|
||||
/>
|
||||
<!-- BrandName (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.Row="3"
|
||||
PlaceholderText="Марка продукта"
|
||||
Text="{Binding SelectedBrandName}"
|
||||
/>
|
||||
<!-- MixtureName (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
Text="Марка смеси:"
|
||||
/>
|
||||
<!-- MixtureName (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="1"
|
||||
Grid.Row="3"
|
||||
PlaceholderText="Марка смеси"
|
||||
Text="{Binding SelectedMixtureName}"
|
||||
/>
|
||||
<!-- MixtureNormalRate (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini" Text="Норма расхода смеси (кг/т | гр/кг):"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="4"
|
||||
/>
|
||||
<!-- MixtureNormalRate (поле) -->
|
||||
<controls:TouchNumericUpDown
|
||||
Classes="singleline"
|
||||
FormatString="0.0000"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="5"
|
||||
Increment="0.0001"
|
||||
PlaceholderText="Норма расхода смеси (кг/т | гр/кг)"
|
||||
Value="{Binding SelectedMixtureNormalRate}"
|
||||
/>
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
|
||||
</Grid>
|
||||
<!-- Id (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Text="ИД:"
|
||||
/>
|
||||
<!-- Id (поле) -->
|
||||
<TextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="ИД"
|
||||
Text="{Binding Record.Id}"
|
||||
/>
|
||||
<!-- Uuid (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Text="Уникальный ИД:"
|
||||
/>
|
||||
<!-- Uuid (поле) -->
|
||||
<TextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="Уникальный ИД"
|
||||
Text="{Binding Record.Uuid}"
|
||||
/>
|
||||
<!-- BrandName (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="2"
|
||||
Text="Марка продукта:"
|
||||
/>
|
||||
<!-- BrandName (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="3"
|
||||
PlaceholderText="Марка продукта"
|
||||
Text="{Binding Record.BrandName}"
|
||||
/>
|
||||
<!-- MixtureName (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4"
|
||||
Text="Марка смеси:"
|
||||
/>
|
||||
<!-- MixtureName (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="singleline"
|
||||
Grid.Column="0"
|
||||
Grid.Row="5"
|
||||
PlaceholderText="Марка смеси"
|
||||
Text="{Binding Record.MixtureName}"
|
||||
/>
|
||||
<!-- MixtureNormalRate (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini" Text="Норма расхода смеси (кг/т | гр/кг):"
|
||||
Grid.Column="1"
|
||||
Grid.Row="4"
|
||||
/>
|
||||
<!-- MixtureNormalRate (поле) -->
|
||||
<controls:TouchNumericUpDown
|
||||
Classes="singleline"
|
||||
FormatString="0.0000"
|
||||
Grid.Column="1"
|
||||
Grid.Row="5"
|
||||
Increment="0.0001"
|
||||
PlaceholderText="Норма расхода смеси (кг/т | гр/кг)"
|
||||
Value="{Binding Record.MixtureNormalRate}"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Comment -->
|
||||
<Grid>
|
||||
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <!-- Comment (подпись) -->
|
||||
<RowDefinition Height="*" /> <!-- Comment (поле ) -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Comment (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Row="0"
|
||||
Text="Дополнительная информация:"
|
||||
/>
|
||||
<!-- Comment (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="multiline"
|
||||
Grid.Row="1"
|
||||
PlaceholderText="Дополнительная информация"
|
||||
Text="{Binding Record.Comment}"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
|
||||
</core:SectionPanel>
|
||||
|
||||
<!-- Кнопка влево -->
|
||||
<Button
|
||||
Command="{Binding InfoSectionPreviousCommand}"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
>
|
||||
|
||||
<Button.IsEnabled>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="InfoSectionCanScrollBackward" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
|
||||
</MultiBinding>
|
||||
</Button.IsEnabled>
|
||||
|
||||
<TextBlock Text="◀"/>
|
||||
|
||||
</Button>
|
||||
|
||||
<!-- Кнопка вправо -->
|
||||
<Button
|
||||
Command="{Binding InfoSectionNextCommand}"
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
>
|
||||
|
||||
<Button.IsEnabled>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="InfoSectionCanScrollForward" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
|
||||
</MultiBinding>
|
||||
</Button.IsEnabled>
|
||||
|
||||
<TextBlock Text="▶"/>
|
||||
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<Grid Background="{StaticResource MainBackground}">
|
||||
|
||||
<!-- Основная форма -->
|
||||
<Grid IsEnabled="{Binding !MenuOpened}">
|
||||
<Grid IsEnabled="{Binding CanInteract}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
|
||||
@@ -51,8 +51,8 @@
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
<ColumnDefinition Width="*" /> <!-- Информация -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
@@ -101,12 +101,12 @@
|
||||
<Grid.ColumnSpacing>5</Grid.ColumnSpacing>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- SampleName (подпись) | BrandComboBox (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- SampleName (поле ) | BrandComboBox (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (подпись) | SamplingPlace (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (поле ) | SamplingPlace (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (подпись) | SamplingPlace (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (поле ) | SamplingPlace (поле ) -->
|
||||
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="ИД"
|
||||
Text="{Binding SelectedId}"
|
||||
Text="{Binding Record.Id}"
|
||||
/>
|
||||
<!-- Uuid (подпись) -->
|
||||
<TextBlock
|
||||
@@ -142,7 +142,7 @@
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="Уникальный ИД"
|
||||
Text="{Binding SelectedUuid}"
|
||||
Text="{Binding Record.Uuid}"
|
||||
/>
|
||||
<!-- SampleName (подпись) -->
|
||||
<TextBlock
|
||||
@@ -156,7 +156,7 @@
|
||||
Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Classes="singleline"
|
||||
Text="{Binding SelectedSampleName}"
|
||||
Text="{Binding Record.SampleName}"
|
||||
PlaceholderText="Название пробы"
|
||||
/>
|
||||
<!-- BrandComboBox (подпись) -->
|
||||
@@ -180,7 +180,7 @@
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SampleCalibrationViewModel+BrandComboBoxItemViewModel">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
>
|
||||
<Run Text="{Binding Id, StringFormat='[{0}] '}" />
|
||||
@@ -204,7 +204,7 @@
|
||||
Grid.Column="0"
|
||||
Grid.Row="5"
|
||||
PlaceholderText="Дата и время отбора пробы"
|
||||
Text="{Binding SelectedSamplingDateTime, Converter={StaticResource DateTimeConverter}}"
|
||||
Text="{Binding Record.SamplingDateTime, Converter={StaticResource DateTimeConverter}}"
|
||||
/>
|
||||
<!-- SamplingPlace (подпись) -->
|
||||
<TextBlock
|
||||
@@ -219,7 +219,7 @@
|
||||
Grid.Column="1"
|
||||
Grid.Row="5"
|
||||
PlaceholderText="Место отбора пробы"
|
||||
Text="{Binding SelectedSamplingPlace}"
|
||||
Text="{Binding Record.SamplingPlace}"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
@@ -258,7 +258,7 @@
|
||||
Grid.Row="1"
|
||||
Increment="0.0001"
|
||||
PlaceholderText="Фактический расход смеси (кг/т | гр/кг)"
|
||||
Value="{Binding SelectedMixtureActualRate}"
|
||||
Value="{Binding Record.MixtureActualRate}"
|
||||
/>
|
||||
<!-- MeasuredContent (подпись) -->
|
||||
<TextBlock
|
||||
@@ -274,7 +274,7 @@
|
||||
Grid.Row="1"
|
||||
Increment="0.0001"
|
||||
PlaceholderText="Измеренное содержание смеси (кг/т | гр/кг)"
|
||||
Value="{Binding SelectedMeasuredContent}"
|
||||
Value="{Binding Record.MeasuredContent}"
|
||||
/>
|
||||
<!-- SampleComment (подпись) -->
|
||||
<TextBlock
|
||||
@@ -290,7 +290,7 @@
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="3"
|
||||
PlaceholderText="Дополнительная информация о пробе"
|
||||
Text="{Binding SelectedSampleComment}"
|
||||
Text="{Binding Record.SampleComment}"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
@@ -363,15 +363,14 @@
|
||||
<core:ZoomPanImage
|
||||
Background="Black"
|
||||
HorizontalAlignment="Stretch"
|
||||
ImageSource="{Binding SelectedImage.Image}"
|
||||
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
|
||||
ImageSource="{Binding Record.Image.Image}"
|
||||
VerticalAlignment="Stretch"
|
||||
>
|
||||
|
||||
<core:ZoomPanImage.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</core:ZoomPanImage.IsVisible>
|
||||
@@ -379,11 +378,11 @@
|
||||
</core:ZoomPanImage>
|
||||
|
||||
<!-- Оверлей "Изображение загружается" -->
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
|
||||
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
|
||||
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="SelectedImage.IsLoading" />
|
||||
<Binding Path="Record.Image.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</StackPanel.IsVisible>
|
||||
@@ -402,8 +401,8 @@
|
||||
>
|
||||
<TextBlock.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</TextBlock.IsVisible>
|
||||
@@ -417,8 +416,8 @@
|
||||
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
|
||||
<Binding Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</StackPanel.IsVisible>
|
||||
@@ -433,18 +432,20 @@
|
||||
|
||||
<!-- Кнопка "Получить изображения" -->
|
||||
<Button
|
||||
Command="{Binding CaptureImageCommand}"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CaptureImageCommand}"
|
||||
IsVisible="{Binding !ImagesIsCapturing}"
|
||||
>
|
||||
<TextBlock Text="Получить изображения"/>
|
||||
</Button>
|
||||
|
||||
<!-- Кнопка "Отменить съёмку" -->
|
||||
<Button
|
||||
Command="{Binding CancelImageCapturingCommand}"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CancelImageCapturingCommand}"
|
||||
IsVisible="{Binding ImagesIsCapturing}"
|
||||
>
|
||||
<TextBlock Text="Отменить съёмку"/>
|
||||
</Button>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<Grid Background="{StaticResource MainBackground}">
|
||||
|
||||
<!-- Основная форма -->
|
||||
<Grid IsEnabled="{Binding !MenuOpened}">
|
||||
<Grid IsEnabled="{Binding CanInteract}">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
|
||||
@@ -80,7 +80,8 @@
|
||||
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Type (подпись) -->
|
||||
<RowDefinition Height="Auto" /> <!-- Type (поле ) -->
|
||||
<RowDefinition Height="*" /> <!-- Пустое пространство -->
|
||||
<RowDefinition Height="Auto" /> <!-- Comment (подпись) -->
|
||||
<RowDefinition Height="*" /> <!-- Comment (поле ) -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
@@ -99,7 +100,7 @@
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="ИД"
|
||||
Text="{Binding SelectedId}"
|
||||
Text="{Binding Record.Id}"
|
||||
/>
|
||||
<!-- Uuid (подпись) -->
|
||||
<TextBlock
|
||||
@@ -115,7 +116,7 @@
|
||||
Grid.Row="1"
|
||||
IsReadOnly="True"
|
||||
PlaceholderText="Уникальный ИД"
|
||||
Text="{Binding SelectedUuid}"
|
||||
Text="{Binding Record.Uuid}"
|
||||
/>
|
||||
<!-- Type (подпись) -->
|
||||
<TextBlock
|
||||
@@ -131,25 +132,27 @@
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="3"
|
||||
PlaceholderText="Тип"
|
||||
Text="{Binding SelectedType}"
|
||||
Text="{Binding Record.Type}"
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
<!-- Comment (подпись) -->
|
||||
<TextBlock
|
||||
Classes="mini"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="4"
|
||||
Text="Дополнительная информация:"
|
||||
/>
|
||||
<!-- Comment (поле) -->
|
||||
<controls:TouchTextBox
|
||||
Classes="multiline"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Row="5"
|
||||
PlaceholderText="Дополнительная информация"
|
||||
Text="{Binding Record.Comment}"
|
||||
/>
|
||||
|
||||
<!-- Информация -->
|
||||
<Grid Grid.Column="1">
|
||||
<!-- ИД (подпись) -->
|
||||
<!-- <TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД:" /> -->
|
||||
<!-- ИД (поле) -->
|
||||
<!-- <TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Id}" PlaceholderText="ИД" IsReadOnly="True" /> -->
|
||||
<!-- Тип (подпись) -->
|
||||
<!-- <TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" /> -->
|
||||
<!-- Тип (поле) -->
|
||||
<!-- <controls:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" /> -->
|
||||
<!-- Партия (подпись) -->
|
||||
<!-- <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" /> -->
|
||||
<!-- Партия (поле) -->
|
||||
<!-- <controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" /> -->
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
@@ -166,25 +169,19 @@
|
||||
|
||||
<!-- Изображение -->
|
||||
<Grid>
|
||||
|
||||
<!-- Изображение -->
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Изображение -->
|
||||
<core:ZoomPanImage
|
||||
Background="Black"
|
||||
HorizontalAlignment="Stretch"
|
||||
ImageSource="{Binding SelectedImage.Image}"
|
||||
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
|
||||
ImageSource="{Binding Record.Image.Image}"
|
||||
VerticalAlignment="Stretch"
|
||||
>
|
||||
|
||||
<core:ZoomPanImage.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</core:ZoomPanImage.IsVisible>
|
||||
@@ -196,7 +193,7 @@
|
||||
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="SelectedImage.IsLoading" />
|
||||
<Binding Path="Record.Image.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</StackPanel.IsVisible>
|
||||
@@ -215,8 +212,8 @@
|
||||
>
|
||||
<TextBlock.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</TextBlock.IsVisible>
|
||||
@@ -230,8 +227,8 @@
|
||||
|
||||
<StackPanel.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" />
|
||||
<Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
|
||||
<Binding Path="ImagesIsCapturing" />
|
||||
</MultiBinding>
|
||||
</StackPanel.IsVisible>
|
||||
@@ -246,18 +243,20 @@
|
||||
|
||||
<!-- Кнопка "Получить изображения" -->
|
||||
<Button
|
||||
Command="{Binding CaptureImageCommand}"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CaptureImageCommand}"
|
||||
IsVisible="{Binding !ImagesIsCapturing}"
|
||||
>
|
||||
<TextBlock Text="Получить изображения"/>
|
||||
</Button>
|
||||
|
||||
<!-- Кнопка "Отменить съёмку" -->
|
||||
<Button
|
||||
Command="{Binding CancelImageCapturingCommand}"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding CancelImageCapturingCommand}"
|
||||
IsVisible="{Binding ImagesIsCapturing}"
|
||||
>
|
||||
<TextBlock Text="Отменить съёмку"/>
|
||||
</Button>
|
||||
|
||||
@@ -36,31 +36,30 @@
|
||||
|
||||
<Border Background="{StaticResource MainBackground}" BorderThickness="0">
|
||||
|
||||
<Grid Margin="5" RowSpacing="5">
|
||||
<Grid Margin="5">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/> <!-- Меню -->
|
||||
<RowDefinition Height="*"/> <!-- Основной контент -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Меню -->
|
||||
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
|
||||
<Grid.RowSpacing>5</Grid.RowSpacing>
|
||||
|
||||
<ScrollViewer.IsEnabled>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="IsScrolling" />
|
||||
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorCalibration.MenuOpened" />
|
||||
</MultiBinding>
|
||||
</ScrollViewer.IsEnabled>
|
||||
<!-- Меню -->
|
||||
<ScrollViewer
|
||||
Grid.Row="0"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
IsEnabled="{Binding CanInteract}"
|
||||
VerticalScrollBarVisibility="Disabled"
|
||||
>
|
||||
|
||||
<Grid ColumnSpacing="5">
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Анализ -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Калибровка проб -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Калибровка сепаратора -->
|
||||
<!-- <ColumnDefinition Width="*" MinWidth="200"/> --> <!-- Камера -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Система -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Анализ -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Марки -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Сепараторы -->
|
||||
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Пробы -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Анализ -->
|
||||
@@ -68,24 +67,19 @@
|
||||
<TextBlock Classes="menu-text" Text="Анализ"/>
|
||||
</Button>
|
||||
|
||||
<!-- Калибровка проб -->
|
||||
<!-- Марки -->
|
||||
<Button Grid.Column="1" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="1">
|
||||
<TextBlock Classes="menu-text" Text="Калибровка проб"/>
|
||||
<TextBlock Classes="menu-text" Text="Марки"/>
|
||||
</Button>
|
||||
|
||||
<!-- Калибровка сепараторов -->
|
||||
|
||||
<!-- Сепараторы -->
|
||||
<Button Grid.Column="2" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="2">
|
||||
<TextBlock Classes="menu-text" Text="Калибровка сепараторов"/>
|
||||
<TextBlock Classes="menu-text" Text="Сепараторы"/>
|
||||
</Button>
|
||||
|
||||
<!-- Камера -->
|
||||
<!-- <Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3">
|
||||
<TextBlock Classes="menu-text" Text="Камера"/>
|
||||
</Button> -->
|
||||
|
||||
<!-- Система -->
|
||||
<Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="4">
|
||||
<TextBlock Classes="menu-text" Text="Система"/>
|
||||
|
||||
<!-- Пробы -->
|
||||
<Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3">
|
||||
<TextBlock Classes="menu-text" Text="Пробы"/>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
@@ -93,14 +87,16 @@
|
||||
|
||||
<!-- Основной контент -->
|
||||
<core:SectionPanel Grid.Row="1" ClipToBounds="True" CurrentIndex="{Binding CurrentIndex}" Orientation="Horizontal" IsScrolling="{Binding IsScrolling}">
|
||||
<ContentControl Content="{Binding Analysis}"/> <!-- Анализ -->
|
||||
<ContentControl Content="{Binding SampleCalibration}"/> <!-- Калибровка проб -->
|
||||
<ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Калибровка сепаратора -->
|
||||
<!-- <ContentControl Content="{Binding Camera}"/> --> <!-- Камера -->
|
||||
<ContentControl Content="{Binding System}"/> <!-- Система -->
|
||||
<ContentControl Content="{Binding Analysis}"/> <!-- Анализ -->
|
||||
<ContentControl Content="{Binding BrandCalibration}"/> <!-- Марки -->
|
||||
<ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Сепараторы -->
|
||||
<ContentControl Content="{Binding SampleCalibration}"/> <!-- Пробы -->
|
||||
<!-- <ContentControl Content="{Binding Camera}"/> --> <!-- Камера -->
|
||||
<!-- <ContentControl Content="{Binding System}"/> --> <!-- Система -->
|
||||
</core:SectionPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<UserControl
|
||||
x:Class="GSS2.Views.Common.CameraView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:GSS2.ViewModels.Common"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
x:DataType="vm:CameraViewModel">
|
||||
|
||||
<UserControl.Resources>
|
||||
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
|
||||
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
|
||||
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
|
||||
<x:Double x:Key="ButtonFontSize">20</x:Double>
|
||||
</UserControl.Resources>
|
||||
|
||||
<UserControl.Styles>
|
||||
|
||||
<Style Selector="Button">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="{StaticResource ButtonFontSize}"/>
|
||||
</Style>
|
||||
|
||||
</UserControl.Styles>
|
||||
|
||||
<Border BorderThickness="0">
|
||||
<Grid>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/> <!-- Камера -->
|
||||
<ColumnDefinition Width="Auto"/> <!-- Осветитель -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentControl Grid.Column="0" Content="{Binding Camera}"/> <!-- Камера -->
|
||||
|
||||
<ContentControl Grid.Column="1" Content="{Binding Illuminator}"/> <!-- Осветитель -->
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,11 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace GSS2.Views.Common;
|
||||
|
||||
public partial class CameraView : UserControl
|
||||
{
|
||||
public CameraView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -20,8 +20,7 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"AmineContentCalibrationDatabasePath": "AmineContentCalibration.db",
|
||||
"AmineContentResultsDatabasePath": "AmineContentResults.db"
|
||||
"AmineContentDatabasePath": "AmineContent.db"
|
||||
},
|
||||
"Hardware": {
|
||||
"Illuminator": {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user