feat: massive rework

1) GSS2.Core:
- strip prefixes in GSS2.Core.Analysis.AmineContent namespace class names
- add IEnumerable<double>.Variance extension
- remake analysis + update database so not needed to store all every file, calibration data also stored in database, also currently capturing images stored in system temporary directory and rewriting every time
2) GSS.UI.Core: AsyncImageRecordViewModel make zoom and pans public
3) GSS:
- remove image-storage-clear command
- remove ResultsView and ResultsViewModel, results presented in analysis
- rework calibration views
- add analysis views
- add text and number fields editors view
- add confirmation view on danger buttons
This commit is contained in:
2026-05-13 12:56:32 +03:00
parent ec95eef375
commit 3cc4d21947
63 changed files with 4359 additions and 3961 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,534 @@
using Microsoft.Extensions.Logging;
using Microsoft.ML;
using Microsoft.ML.Data;
using OpenCvSharp;
using GSS2.Core.Extensions;
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
namespace GSS2.Core.Analysis.AmineContent;
public partial class AnalyzerService
{
//TODO: Перенести все гиперпараметры в appsettings.json
public const int CHANNELS_COUNT = 3;
public const int IMAGES_COUNT = 12;
public const int FEATURES_LENGTH = (CHANNELS_COUNT + 3) * IMAGES_COUNT;
public const int EFFECTIVE_IMAGE_SIZE = 200;
public const int ANALYSIS_IMAGE_SIZE = 1000;
public const int SEPARATOR_ROI_THRESHOLD = 5;
public const float CONTOUR_MAX_SIZE = 100;
public const float CONTOUR_MAX_AREA = (float)Math.PI * (CONTOUR_MAX_SIZE * CONTOUR_MAX_SIZE) / 4.0f;
public const float CONTOUR_MIN_SIZE = 12;
public const float CONTOUR_MIN_AREA = (float)Math.PI * (CONTOUR_MIN_SIZE * CONTOUR_MIN_SIZE) / 4.0f / 4.0f;
public const int SEPARATOR_X = 519;
public const int SEPARATOR_Y = 17;
public const int SEPARATOR_WIDTH = 2989;
public const int SEPARATOR_HEIGHT = 2985;
private class ClusterizationData
{
[ColumnName("Label")]
public required string Label { get; set; }
[ColumnName("Features")]
[VectorType(FEATURES_LENGTH)]
public required float[] Features { get; set; }
}
private class ClusterizationPrediction
{
[ColumnName("PredictedLabel")]
public string Label { get; set; }
public float Probability { get; set; }
public float[] Score { get; set; }
public ClusterizationPrediction()
{
Label = "";
Probability = 0;
Score = new float[2];
}
}
private class RegressionData
{
[ColumnName("Label")]
public required float Value { get; set; }
[ColumnName("Features")]
[VectorType(FEATURES_LENGTH)]
public required float[] Features { get; set; }
}
private class RegressionPrediction
{
[ColumnName("Score")]
public float Value { get; set; }
public RegressionPrediction()
{
Value = -1;
}
}
private readonly ILogger<AnalyzerService> _logger;
private readonly CalibrationContext _calibrationContext;
private MLContext? _ml = null;
private PredictionEngine<ClusterizationData, ClusterizationPrediction>? _clusterizationEngine = null;
private PredictionEngine<RegressionData, RegressionPrediction>? _regressionEngine = null;
public bool Initialized { get; private set; } = false;
public AnalyzerService(ILogger<AnalyzerService> logger, CalibrationContext calibrationContext)
{
_logger = logger;
_calibrationContext = calibrationContext;
}
public async Task Initialize(string? brand = null, CancellationToken cancellationToken = default)
{
Initialized = false;
_logger.LogInformation("Инициализация");
_ml = new MLContext();
_ml.Log += (_, ea) =>
{
if (ea.Kind >= Microsoft.ML.Runtime.ChannelMessageKind.Info)
_logger.LogInformation("ML: [{}] {}", ea.Kind, ea.Message);
};
try
{
await InitializeMlClusterizationEngine(cancellationToken);
await InitializeMlRegressionEngine(brand, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Исключение во время инициализации моделей");
return;
}
Initialized = true;
}
private async Task InitializeMlClusterizationEngine(CancellationToken cancellationToken)
{
_logger.LogInformation("Инициализация модели кластеризации");
var separatorPrefix = "SEPARATOR";
var samplePrefix = "SAMPLE";
if (_ml is null)
throw new InvalidOperationException();
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Создание конвейера");
var pipeline = _ml.Transforms.Conversion.MapValueToKey("Label")
.Append(_ml.MulticlassClassification.Trainers.LbfgsMaximumEntropy())
.Append(_ml.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Подготовка данных");
var trainDataSet = _calibrationContext.VectorRecords.Select(r =>
new ClusterizationData
{
Label = r.SeparatorRecord != null ?
$"{separatorPrefix}|{r.SeparatorRecord.Type}" :
r.SampleRecord != null ?
$"{samplePrefix}|{r.SampleRecord.SampleBrand}" :
"",
Features = r.Values
})
.ToList();
var trainData = _ml.Data.LoadFromEnumerable(trainDataSet);
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Обучение модели");
var model = pipeline.Fit(trainData);
_clusterizationEngine = _ml.Model.CreatePredictionEngine<ClusterizationData, ClusterizationPrediction>(model);
model.Dispose();
}
private async Task InitializeMlRegressionEngine(string? brand, CancellationToken cancellationToken)
{
_logger.LogInformation("Инициализация модели регрессии для марки \"{}\"", brand);
if (_ml is null)
throw new InvalidOperationException();
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Создание конвейера");
var pipeline = _ml.Regression.Trainers.Sdca();
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Подготовка данных");
var trainDataSet =
_calibrationContext.VectorRecords
.Where(r => r.SampleRecord != null)
.Where(r => brand == null || r.SampleRecord!.SampleBrand == brand)
.Select(r =>
new RegressionData
{
Value = (float)(
r.SampleRecord!.MeasuredContent >= 0 ?
r.SampleRecord!.MeasuredContent :
r.SampleRecord!.MixtureActualRate >= 0 ?
r.SampleRecord!.MixtureActualRate :
r.SampleRecord!.MixtureNormalRate
),
Features = r.Values
})
.ToList();
var trainData = _ml.Data.LoadFromEnumerable(trainDataSet);
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Обучение модели");
var model = pipeline.Fit(trainData);
_regressionEngine = _ml.Model.CreatePredictionEngine<RegressionData, RegressionPrediction>(model);
model.Dispose();
}
public async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours, Mat result, string separatorType, string sampleBrand)?> Analyze(IEnumerable<ImageData> imagesData)
{
if (!Initialized)
return null;
if (imagesData.Count() != IMAGES_COUNT)
return null;
try
{
var segmentationResult = await Segment(imagesData);
if (segmentationResult is null)
return null;
var (avgImage, roi, mask, contours, separatorType, sampleBrand) = segmentationResult.Value;
var images = new List<(Mat mat, double visible, double uv365, double uv254)>();
foreach (var imageData in imagesData
.OrderBy(i => i.VisibleIntensity)
.ThenBy(i => i.Uv365Intensity)
.ThenBy(i => i.Uv254Intensity))
images.Add((
LoadAndPrepare(imageData.Path, roi, ANALYSIS_IMAGE_SIZE),
imageData.VisibleIntensity,
imageData.Uv365Intensity,
imageData.Uv254Intensity
));
var result = new Mat(new Size(ANALYSIS_IMAGE_SIZE, ANALYSIS_IMAGE_SIZE), MatType.CV_32FC1, new Scalar(0));
var values = new float[ANALYSIS_IMAGE_SIZE * ANALYSIS_IMAGE_SIZE];
for (var row = 0; row < ANALYSIS_IMAGE_SIZE; row++)
for (var col = 0; col < ANALYSIS_IMAGE_SIZE; col++)
{
if (mask.At<byte>(row, col) != 255)
continue;
var vector = new float[FEATURES_LENGTH];
foreach (var (i, image) in images.Enumerate())
{
vector[i * (CHANNELS_COUNT + 3) + 0] = (float)image.visible;
vector[i * (CHANNELS_COUNT + 3) + 1] = (float)image.uv365;
vector[i * (CHANNELS_COUNT + 3) + 2] = (float)image.uv254;
var color = image.mat.At<Vec3b>(row, col);
vector[i * (CHANNELS_COUNT + 3) + 3] = color.Item0 / 255.0f;
vector[i * (CHANNELS_COUNT + 3) + 4] = color.Item1 / 255.0f;
vector[i * (CHANNELS_COUNT + 3) + 5] = color.Item2 / 255.0f;
}
var prediction = _regressionEngine?.Predict(new RegressionData
{
Value = -1,
Features = vector
});
if (prediction is null)
continue;
result.Set<float>(row, col, prediction.Value);
values[row * ANALYSIS_IMAGE_SIZE + col] = prediction.Value;
}
var oldMean = Cv2.Mean(result, mask).Val0;
values.Sort();
float p999 = values[(int)(values.Length * 0.999)];
Cv2.Threshold(result, result, p999, p999, ThresholdTypes.Trunc);
var newMean = Cv2.Mean(result, mask).Val0;
var scale = oldMean / newMean;
Cv2.ConvertScaleAbs(result, result, scale);
return (avgImage, roi, mask, contours, result, separatorType, sampleBrand);
}
catch (Exception ex)
{
_logger.LogError(ex, "Исключение при проведении анализа");
return null;
}
}
private async Task<(Mat avgImage, Rect roi, Mat mask, IEnumerable<Point[]> contours, string separatorType, string sampleBrand)?> Segment(IEnumerable<ImageData> imagesData)
{
var separatorPrefix = "SEPARATOR";
var samplePrefix = "SAMPLE";
if (imagesData.Count() != IMAGES_COUNT)
return null;
var avgImage = CalculateAverageImage(imagesData.Select(i => i.Path));
if (avgImage is null)
return null;
var roi = CalculateSeparatorRoi(avgImage);
if (roi is null)
{
avgImage.Release();
return null;
}
avgImage = new Mat(avgImage, roi.Value);
Cv2.Resize(avgImage, avgImage, new Size(ANALYSIS_IMAGE_SIZE, ANALYSIS_IMAGE_SIZE));
var images = new List<(Mat mat, double visible, double uv365, double uv254)>();
foreach (var imageData in imagesData
.OrderBy(i => i.VisibleIntensity)
.ThenBy(i => i.Uv365Intensity)
.ThenBy(i => i.Uv254Intensity))
images.Add((
LoadAndPrepare(imageData.Path, roi.Value, ANALYSIS_IMAGE_SIZE),
imageData.VisibleIntensity,
imageData.Uv365Intensity,
imageData.Uv254Intensity
));
var separatorMask = new Mat(new Size(ANALYSIS_IMAGE_SIZE, ANALYSIS_IMAGE_SIZE), MatType.CV_8UC1, new Scalar(0));
Cv2.Circle(separatorMask, ANALYSIS_IMAGE_SIZE / 2, ANALYSIS_IMAGE_SIZE / 2, ANALYSIS_IMAGE_SIZE / 2, new Scalar(255), -1);
var result = new Mat(new Size(ANALYSIS_IMAGE_SIZE, ANALYSIS_IMAGE_SIZE), MatType.CV_16UC1, new Scalar(0));
var keyMap = new Dictionary<string, ushort>();
ushort lastKey = 0;
for (var row = 0; row < ANALYSIS_IMAGE_SIZE; row++)
for (var col = 0; col < ANALYSIS_IMAGE_SIZE; col++)
{
if (separatorMask.At<byte>(row, col) != 255)
continue;
var vector = new float[FEATURES_LENGTH];
foreach (var (i, image) in images.Enumerate())
{
vector[i * (CHANNELS_COUNT + 3) + 0] = (float)image.visible;
vector[i * (CHANNELS_COUNT + 3) + 1] = (float)image.uv365;
vector[i * (CHANNELS_COUNT + 3) + 2] = (float)image.uv254;
var color = image.mat.At<Vec3b>(row, col);
vector[i * (CHANNELS_COUNT + 3) + 3] = color.Item0 / 255.0f;
vector[i * (CHANNELS_COUNT + 3) + 4] = color.Item1 / 255.0f;
vector[i * (CHANNELS_COUNT + 3) + 5] = color.Item2 / 255.0f;
}
var prediction = _clusterizationEngine?.Predict(new ClusterizationData
{
Label = "",
Features = vector
});
if (prediction is null)
continue;
if (!keyMap.ContainsKey(prediction.Label))
keyMap.Add(prediction.Label, ++lastKey);
result.Set<ushort>(row, col, keyMap[prediction.Label]);
}
foreach (var image in images)
image.mat.Release();
var valueMap = keyMap.ToDictionary(kv => kv.Value, kv => kv.Key);
var frequency = new Dictionary<string, int>();
for (var row = 0; row < ANALYSIS_IMAGE_SIZE; row++)
for (var col = 0; col < ANALYSIS_IMAGE_SIZE; col++)
{
var value = result.At<ushort>(row, col);
if (value == 0)
continue;
if (!frequency.ContainsKey(valueMap[value]))
frequency.Add(valueMap[value], 0);
frequency[valueMap[value]] += 1;
if (valueMap[value].StartsWith(separatorPrefix))
result.Set<ushort>(row, col, 0);
if (valueMap[value].StartsWith(samplePrefix))
result.Set<ushort>(row, col, ushort.MaxValue);
}
result.ConvertTo(result, MatType.CV_8UC1, 1 / 255.0);
Cv2.MorphologyEx(result, result, MorphTypes.Close, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)), iterations: 2);
Cv2.MorphologyEx(result, result, MorphTypes.Open, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)), iterations: 2);
Cv2.Dilate(result, result, Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)));
Cv2.GaussianBlur(result, result, new Size(5, 5), 1);
Cv2.Threshold(result, result, 127, 255, ThresholdTypes.Binary);
Cv2.FindContours(result, out var contours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
result = new Mat(new Size(ANALYSIS_IMAGE_SIZE, ANALYSIS_IMAGE_SIZE), MatType.CV_8UC1, new Scalar(0));
Cv2.DrawContours(result, contours.Where(ContourIsValid), -1, new Scalar(255), -1);
return (
avgImage,
roi.Value,
result,
contours.Where(ContourIsValid),
frequency
.Where(kv => kv.Key.StartsWith(separatorPrefix))
.MaxBy(kv => kv.Value)
.Key
.Substring(separatorPrefix.Count() + 1),
frequency
.Where(kv => kv.Key.StartsWith(samplePrefix))
.MaxBy(kv => kv.Value)
.Key
.Substring(samplePrefix.Count() + 1)
);
}
public static (Mat avgImage, Rect roi, List<float[]> vectors)? GetFeatureVectors(IEnumerable<ImageData> imagesData)
{
if (imagesData.Count() != IMAGES_COUNT)
return null;
var avgImage = CalculateAverageImage(imagesData.Select(i => i.Path));
if (avgImage is null)
return null;
var roi = CalculateSeparatorRoi(avgImage);
if (roi is null)
return null;
var images = new List<(Mat mat, double visible, double uv365, double uv254)>();
foreach (var imageData in imagesData
.OrderBy(i => i.VisibleIntensity)
.ThenBy(i => i.Uv365Intensity)
.ThenBy(i => i.Uv254Intensity))
images.Add((
LoadAndPrepare(imageData.Path, roi.Value),
imageData.VisibleIntensity,
imageData.Uv365Intensity,
imageData.Uv254Intensity
));
var separatorMask = new Mat(new Size(EFFECTIVE_IMAGE_SIZE, EFFECTIVE_IMAGE_SIZE), MatType.CV_8UC1, new Scalar(0));
Cv2.Circle(separatorMask, EFFECTIVE_IMAGE_SIZE / 2, EFFECTIVE_IMAGE_SIZE / 2, EFFECTIVE_IMAGE_SIZE / 2, new Scalar(255), -1);
var vectors = new List<float[]>();
for (var row = 0; row < EFFECTIVE_IMAGE_SIZE; row++)
for (var col = 0; col < EFFECTIVE_IMAGE_SIZE; col++)
{
if (separatorMask.At<byte>(row, col) != 255)
continue;
var vector = new float[FEATURES_LENGTH];
foreach (var (i, image) in images.Enumerate())
{
vector[i * (CHANNELS_COUNT + 3) + 0] = (float)image.visible;
vector[i * (CHANNELS_COUNT + 3) + 1] = (float)image.uv365;
vector[i * (CHANNELS_COUNT + 3) + 2] = (float)image.uv254;
var color = image.mat.At<Vec3b>(row, col).ToVec3f() / 255.0f;
vector[i * (CHANNELS_COUNT + 3) + 3] = color.Item0;
vector[i * (CHANNELS_COUNT + 3) + 4] = color.Item1;
vector[i * (CHANNELS_COUNT + 3) + 5] = color.Item2;
}
vectors.Add(vector);
}
foreach (var (mat, _, _, _) in images)
mat.Release();
return (avgImage, roi.Value, vectors);
}
private static Mat LoadAndPrepare(string imagePath, Rect roi, int size = EFFECTIVE_IMAGE_SIZE)
{
var image = Cv2.ImRead(imagePath);
image = new Mat(image, roi);
Cv2.GaussianBlur(image, image, new Size(5, 5), 1);
Cv2.Resize(image, image, new Size(size, size));
return image;
}
public static Mat? CalculateAverageImage(IEnumerable<string> imagePaths)
{
if (imagePaths.Count() == 0)
return null;
Mat avgImage = new Mat();
foreach (var imagePath in imagePaths)
{
var image = Cv2.ImRead(imagePath);
if (image.Channels() != 3)
return null;
if (avgImage.Empty())
avgImage = new Mat(image.Size(), MatType.CV_16UC3, new Scalar(0, 0, 0));
if (image.Size().Width != avgImage.Size().Width ||
image.Size().Height != avgImage.Size().Height)
return null;
Cv2.Add(image, avgImage, avgImage, dtype: MatType.CV_16U);
image.Release();
}
avgImage.ConvertTo(avgImage, MatType.CV_8UC(avgImage.Channels()), 1f / imagePaths.Count());
return avgImage;
}
public static Rect? CalculateSeparatorRoi(Mat avgImage)
{
if (avgImage.Empty())
return null;
return new Rect(SEPARATOR_X, SEPARATOR_Y, SEPARATOR_WIDTH, SEPARATOR_HEIGHT);
}
// Этот метод конечно хорош, но проблема с тем, что при калибровке на чёрном продукте
// он выдаёт очень обрезает часть полезного изображения из-за неравномерности освещения
// public static Rect? CalculateSeparatorRoi(Mat avgImage)
// {
// if (avgImage.Empty())
// return null;
// Mat avgImageClone = new Mat();
// Mat mask = new Mat();
// Rect? roi = null;
// avgImage.CopyTo(avgImageClone);
// avgImageClone = avgImageClone.CvtColor(ColorConversionCodes.BGR2GRAY);
// Cv2.Threshold(avgImageClone, mask, SEPARATOR_ROI_THRESHOLD, 255, ThresholdTypes.Binary);
// Cv2.FindContours(mask, out var contours, out _, RetrievalModes.List, ContourApproximationModes.ApproxSimple);
// var maxContour = contours.MaxBy(c => Cv2.ContourArea(c));
// if (maxContour is null)
// return null;
// roi = Cv2.BoundingRect(maxContour);
// avgImageClone.Release();
// mask.Release();
// return roi;
// }
private static bool ContourIsValid(Point[] contour)
{
var bbox = Cv2.BoundingRect(contour);
var area = Cv2.ContourArea(contour);
return CONTOUR_MIN_SIZE < bbox.Width && CONTOUR_MIN_SIZE < bbox.Height && CONTOUR_MIN_AREA < area &&
CONTOUR_MAX_SIZE > bbox.Width && CONTOUR_MAX_SIZE > bbox.Height && CONTOUR_MAX_AREA > area;
}
}
@@ -1,80 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration;
public class AmineContentCalibrationContext : DbContext
{
public DbSet<ImageRecord> ImageRecords => Set<ImageRecord>();
public DbSet<SeparatorRecord> SeparatorRecords => Set<SeparatorRecord>();
public DbSet<CalibrationRecord> CalibrationRecords => Set<CalibrationRecord>();
public AmineContentCalibrationContext(DbContextOptions<AmineContentCalibrationContext> options)
: base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ImageRecord>(entity =>
{
entity.HasKey(x => x.Id);
entity.Property(x => x.ImagePath)
.IsRequired()
.HasMaxLength(1024);
});
modelBuilder.Entity<SeparatorRecord>(entity =>
{
entity.HasKey(x => x.Id);
entity.HasMany(x => x.Images)
.WithOne(x => x.UsedInSeparatorRecord)
.HasForeignKey(x => x.UsedInSeparatorRecordId)
.OnDelete(DeleteBehavior.Cascade);
entity.Property(x => x.Type)
.IsRequired()
.HasMaxLength(256);
entity.Property(x => x.Batch)
.IsRequired()
.HasMaxLength(256);
});
modelBuilder.Entity<CalibrationRecord>(entity =>
{
entity.HasKey(x => x.Id);
entity.HasMany(x => x.SampleImages)
.WithOne(x => x.UsedInCalibrationRecordSample)
.HasForeignKey(x => x.UsedInCalibrationRecordSampleId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasMany(x => x.SeparatorImages)
.WithMany(x => x.UsedInCalibrationRecordsSeparator)
.UsingEntity<Dictionary<string, object>>(
"CalibrationSeparatorImage",
j => j
.HasOne<ImageRecord>()
.WithMany()
.HasForeignKey("ImageRecordId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne<CalibrationRecord>()
.WithMany()
.HasForeignKey("CalibrationRecordId")
.OnDelete(DeleteBehavior.Cascade),
j =>
{
j.HasKey("CalibrationRecordId", "ImageRecordId");
j.ToTable("CalibrationSeparatorImages");
});
entity.Property(x => x.SampleName)
.HasMaxLength(512);
entity.Property(x => x.SampleComment)
.HasMaxLength(2048);
entity.Property(x => x.SampleBrand)
.HasMaxLength(512);
entity.Property(x => x.SamplingPlace)
.HasMaxLength(512);
entity.Property(x => x.MixtureName)
.HasMaxLength(512);
});
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration;
public class CalibrationContext : DbContext
{
public DbSet<SeparatorRecord> SeparatorRecords => Set<SeparatorRecord>();
public DbSet<SampleRecord> SampleRecords => Set<SampleRecord>();
public DbSet<VectorRecord> VectorRecords => Set<VectorRecord>();
public CalibrationContext(DbContextOptions<CalibrationContext> options)
: base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SeparatorRecord>()
.HasMany(x => x.VectorRecords)
.WithOne(x => x.SeparatorRecord)
.HasForeignKey(x => x.SeparatorRecordId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<SampleRecord>()
.HasMany(x => x.VectorRecords)
.WithOne(x => x.SampleRecord)
.HasForeignKey(x => x.SampleRecordId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -3,16 +3,16 @@ using Microsoft.EntityFrameworkCore.Design;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration; namespace GSS2.Core.Analysis.AmineContent.Database.Calibration;
public class AmineContentCalibrationContextFactory : IDesignTimeDbContextFactory<AmineContentCalibrationContext> public class CalibrationContextFactory : IDesignTimeDbContextFactory<CalibrationContext>
{ {
public AmineContentCalibrationContext CreateDbContext(string[] args) public CalibrationContext CreateDbContext(string[] args)
{ {
var optionsBuilder = new DbContextOptionsBuilder<AmineContentCalibrationContext>(); var optionsBuilder = new DbContextOptionsBuilder<CalibrationContext>();
// Строка подключения для миграций в design-time // Строка подключения для миграций в design-time
// в runtime должна использоваться настоящая ConnectionString // в runtime должна использоваться настоящая ConnectionString
optionsBuilder.UseSqlite("Data Source=AmineContentCalibration.db"); optionsBuilder.UseSqlite("Data Source=AmineContentCalibration.db");
return new AmineContentCalibrationContext(optionsBuilder.Options); return new CalibrationContext(optionsBuilder.Options);
} }
} }
@@ -1,27 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration;
public record ImageRecord
{
[Comment("Уникальный идентификатор")]
public int Id { get; set; }
[Comment("Интенсивность видимого света (0..1)")]
public required double VisibleIntensity { get; set; }
[Comment("Интенсивность УФ излучения 365 нм (0..1)")]
public required double Uv365Intensity { get; set; }
[Comment("Интенсивность УФ излучения 254 нм (0..1)")]
public required double Uv254Intensity { get; set; }
[Comment("Путь к файлу изображения")]
public required string ImagePath { get; set; }
// 1:N
public int? UsedInSeparatorRecordId { get; set; }
public SeparatorRecord? UsedInSeparatorRecord { get; set; }
public int? UsedInCalibrationRecordSampleId { get; set; }
public CalibrationRecord? UsedInCalibrationRecordSample { get; set; }
// M:N
public List<CalibrationRecord> UsedInCalibrationRecordsSeparator { get; set; } = new();
}
@@ -1,137 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
{
/// <inheritdoc />
public partial class AmineContentCalibration0 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CalibrationRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
SampleName = table.Column<string>(type: "TEXT", maxLength: 512, nullable: true, comment: "Название пробы"),
SampleComment = table.Column<string>(type: "TEXT", maxLength: 2048, nullable: true, comment: "Дополнительная информация о пробе"),
SampleBrand = table.Column<string>(type: "TEXT", maxLength: 512, nullable: true, comment: "Марка пробы"),
SampleMass = table.Column<double>(type: "REAL", nullable: true, comment: "Масса пробы (кг)"),
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: true, comment: "Дата и время отбора пробы"),
SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 512, nullable: true, comment: "Место отбора пробы"),
MixtureName = table.Column<string>(type: "TEXT", maxLength: 512, nullable: true, comment: "Название используемой кондиционирующей смеси"),
MixtureAmineContent = table.Column<double>(type: "REAL", nullable: true, comment: "Содержание аминов в кондиционирующей смеси (%)"),
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: true, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"),
MixtureActualRate = table.Column<double>(type: "REAL", nullable: true, comment: "Фактический расход кондиционирующей смеси (кг/т | гр/кг)"),
MeasuredContent = table.Column<double>(type: "REAL", nullable: true, comment: "Измеренное количество масла (кг/т | гр/кг)")
},
constraints: table =>
{
table.PrimaryKey("PK_CalibrationRecords", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SeparatorRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
Type = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип сепаратора"),
Batch = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Партия сепаратора")
},
constraints: table =>
{
table.PrimaryKey("PK_SeparatorRecords", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ImageRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
VisibleIntensity = table.Column<double>(type: "REAL", nullable: false, comment: "Интенсивность видимого света (0..1)"),
Uv365Intensity = table.Column<double>(type: "REAL", nullable: false, comment: "Интенсивность УФ излучения 365 нм (0..1)"),
Uv254Intensity = table.Column<double>(type: "REAL", nullable: false, comment: "Интенсивность УФ излучения 254 нм (0..1)"),
ImagePath = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false, comment: "Путь к файлу изображения"),
UsedInSeparatorRecordId = table.Column<int>(type: "INTEGER", nullable: true),
UsedInCalibrationRecordSampleId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ImageRecords", x => x.Id);
table.ForeignKey(
name: "FK_ImageRecords_CalibrationRecords_UsedInCalibrationRecordSampleId",
column: x => x.UsedInCalibrationRecordSampleId,
principalTable: "CalibrationRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ImageRecords_SeparatorRecords_UsedInSeparatorRecordId",
column: x => x.UsedInSeparatorRecordId,
principalTable: "SeparatorRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CalibrationSeparatorImages",
columns: table => new
{
CalibrationRecordId = table.Column<int>(type: "INTEGER", nullable: false),
ImageRecordId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CalibrationSeparatorImages", x => new { x.CalibrationRecordId, x.ImageRecordId });
table.ForeignKey(
name: "FK_CalibrationSeparatorImages_CalibrationRecords_CalibrationRecordId",
column: x => x.CalibrationRecordId,
principalTable: "CalibrationRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_CalibrationSeparatorImages_ImageRecords_ImageRecordId",
column: x => x.ImageRecordId,
principalTable: "ImageRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CalibrationSeparatorImages_ImageRecordId",
table: "CalibrationSeparatorImages",
column: "ImageRecordId");
migrationBuilder.CreateIndex(
name: "IX_ImageRecords_UsedInCalibrationRecordSampleId",
table: "ImageRecords",
column: "UsedInCalibrationRecordSampleId");
migrationBuilder.CreateIndex(
name: "IX_ImageRecords_UsedInSeparatorRecordId",
table: "ImageRecords",
column: "UsedInSeparatorRecordId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CalibrationSeparatorImages");
migrationBuilder.DropTable(
name: "ImageRecords");
migrationBuilder.DropTable(
name: "CalibrationRecords");
migrationBuilder.DropTable(
name: "SeparatorRecords");
}
}
}
@@ -10,9 +10,9 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
{ {
[DbContext(typeof(AmineContentCalibrationContext))] [DbContext(typeof(CalibrationContext))]
[Migration("20260313083001_AmineContentCalibration0")] [Migration("20260428223237_Calibration0")]
partial class AmineContentCalibration0 partial class Calibration0
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -20,120 +20,76 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("CalibrationSeparatorImage", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SampleRecord", b =>
{
b.Property<int>("CalibrationRecordId")
.HasColumnType("INTEGER");
b.Property<int>("ImageRecordId")
.HasColumnType("INTEGER");
b.HasKey("CalibrationRecordId", "ImageRecordId");
b.HasIndex("ImageRecordId");
b.ToTable("CalibrationSeparatorImages", (string)null);
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор"); .HasComment("Уникальный идентификатор");
b.Property<double?>("MeasuredContent") b.Property<string>("Image")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение");
b.Property<double>("MeasuredContent")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Измеренное количество масла (кг/т | гр/кг)"); .HasComment("Измеренное количество масла (кг/т | гр/кг)");
b.Property<double?>("MixtureActualRate") b.Property<double>("MixtureActualRate")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)"); .HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)");
b.Property<double?>("MixtureAmineContent") b.Property<double>("MixtureAmineContent")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Содержание аминов в кондиционирующей смеси (%)"); .HasComment("Содержание аминов в кондиционирующей смеси (%)");
b.Property<string>("MixtureName") b.Property<string>("MixtureName")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Название используемой кондиционирующей смеси"); .HasComment("Название используемой кондиционирующей смеси");
b.Property<double?>("MixtureNormalRate") b.Property<double>("MixtureNormalRate")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)"); .HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
b.Property<string>("SampleBrand") b.Property<string>("SampleBrand")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Марка пробы"); .HasComment("Марка пробы");
b.Property<string>("SampleComment") b.Property<string>("SampleComment")
.HasMaxLength(2048) .IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе"); .HasComment("Дополнительная информация о пробе");
b.Property<double?>("SampleMass") b.Property<double>("SampleMass")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Масса пробы (кг)"); .HasComment("Масса пробы (кг)");
b.Property<string>("SampleName") b.Property<string>("SampleName")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Название пробы"); .HasComment("Название пробы");
b.Property<DateTime?>("SamplingDateTime") b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы"); .HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace") b.Property<string>("SamplingPlace")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Место отбора пробы"); .HasComment("Место отбора пробы");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("CalibrationRecords"); b.ToTable("SampleRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.ImageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<string>("ImagePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Путь к файлу изображения");
b.Property<int?>("UsedInCalibrationRecordSampleId")
.HasColumnType("INTEGER");
b.Property<int?>("UsedInSeparatorRecordId")
.HasColumnType("INTEGER");
b.Property<double>("Uv254Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 254 нм (0..1)");
b.Property<double>("Uv365Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 365 нм (0..1)");
b.Property<double>("VisibleIntensity")
.HasColumnType("REAL")
.HasComment("Интенсивность видимого света (0..1)");
b.HasKey("Id");
b.HasIndex("UsedInCalibrationRecordSampleId");
b.HasIndex("UsedInSeparatorRecordId");
b.ToTable("ImageRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b =>
@@ -149,6 +105,12 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Партия сепаратора"); .HasComment("Партия сепаратора");
b.Property<string>("Image")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
@@ -160,46 +122,58 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
b.ToTable("SeparatorRecords"); b.ToTable("SeparatorRecords");
}); });
modelBuilder.Entity("CalibrationSeparatorImage", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.VectorRecord", b =>
{ {
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", null) b.Property<int>("Id")
.WithMany() .ValueGeneratedOnAdd()
.HasForeignKey("CalibrationRecordId") .HasColumnType("INTEGER")
.OnDelete(DeleteBehavior.Cascade) .HasComment("Уникальный идентификатор");
.IsRequired();
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.ImageRecord", null) b.Property<int?>("SampleRecordId")
.WithMany() .HasColumnType("INTEGER");
.HasForeignKey("ImageRecordId")
.OnDelete(DeleteBehavior.Cascade) b.Property<int?>("SeparatorRecordId")
.IsRequired(); .HasColumnType("INTEGER");
b.PrimitiveCollection<string>("Values")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения вектора");
b.HasKey("Id");
b.HasIndex("SampleRecordId");
b.HasIndex("SeparatorRecordId");
b.ToTable("VectorRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.ImageRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.VectorRecord", b =>
{ {
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", "UsedInCalibrationRecordSample") b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.SampleRecord", "SampleRecord")
.WithMany("SampleImages") .WithMany("VectorRecords")
.HasForeignKey("UsedInCalibrationRecordSampleId") .HasForeignKey("SampleRecordId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", "UsedInSeparatorRecord") b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", "SeparatorRecord")
.WithMany("Images") .WithMany("VectorRecords")
.HasForeignKey("UsedInSeparatorRecordId") .HasForeignKey("SeparatorRecordId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.Navigation("UsedInCalibrationRecordSample"); b.Navigation("SampleRecord");
b.Navigation("UsedInSeparatorRecord"); b.Navigation("SeparatorRecord");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SampleRecord", b =>
{ {
b.Navigation("SampleImages"); b.Navigation("VectorRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b =>
{ {
b.Navigation("Images"); b.Navigation("VectorRecords");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
@@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
{
/// <inheritdoc />
public partial class Calibration0 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SampleRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
SampleName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"),
SampleComment = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"),
SampleBrand = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"),
SampleMass = table.Column<double>(type: "REAL", nullable: false, comment: "Масса пробы (кг)"),
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы"),
SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"),
MixtureName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название используемой кондиционирующей смеси"),
MixtureAmineContent = table.Column<double>(type: "REAL", nullable: false, comment: "Содержание аминов в кондиционирующей смеси (%)"),
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"),
MixtureActualRate = table.Column<double>(type: "REAL", nullable: false, comment: "Фактический расход кондиционирующей смеси (кг/т | гр/кг)"),
MeasuredContent = table.Column<double>(type: "REAL", nullable: false, comment: "Измеренное количество масла (кг/т | гр/кг)"),
Image = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Изображение")
},
constraints: table =>
{
table.PrimaryKey("PK_SampleRecords", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SeparatorRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
Type = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип сепаратора"),
Batch = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Партия сепаратора"),
Image = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Изображение")
},
constraints: table =>
{
table.PrimaryKey("PK_SeparatorRecords", x => x.Id);
});
migrationBuilder.CreateTable(
name: "VectorRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
SeparatorRecordId = table.Column<int>(type: "INTEGER", nullable: true),
SampleRecordId = table.Column<int>(type: "INTEGER", nullable: true),
Values = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения вектора")
},
constraints: table =>
{
table.PrimaryKey("PK_VectorRecords", x => x.Id);
table.ForeignKey(
name: "FK_VectorRecords_SampleRecords_SampleRecordId",
column: x => x.SampleRecordId,
principalTable: "SampleRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_VectorRecords_SeparatorRecords_SeparatorRecordId",
column: x => x.SeparatorRecordId,
principalTable: "SeparatorRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_VectorRecords_SampleRecordId",
table: "VectorRecords",
column: "SampleRecordId");
migrationBuilder.CreateIndex(
name: "IX_VectorRecords_SeparatorRecordId",
table: "VectorRecords",
column: "SeparatorRecordId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "VectorRecords");
migrationBuilder.DropTable(
name: "SampleRecords");
migrationBuilder.DropTable(
name: "SeparatorRecords");
}
}
}
@@ -9,128 +9,84 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
{ {
[DbContext(typeof(AmineContentCalibrationContext))] [DbContext(typeof(CalibrationContext))]
partial class AmineContentCalibrationContextModelSnapshot : ModelSnapshot partial class CalibrationContextModelSnapshot : ModelSnapshot
{ {
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0"); modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("CalibrationSeparatorImage", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SampleRecord", b =>
{
b.Property<int>("CalibrationRecordId")
.HasColumnType("INTEGER");
b.Property<int>("ImageRecordId")
.HasColumnType("INTEGER");
b.HasKey("CalibrationRecordId", "ImageRecordId");
b.HasIndex("ImageRecordId");
b.ToTable("CalibrationSeparatorImages", (string)null);
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор"); .HasComment("Уникальный идентификатор");
b.Property<double?>("MeasuredContent") b.Property<string>("Image")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение");
b.Property<double>("MeasuredContent")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Измеренное количество масла (кг/т | гр/кг)"); .HasComment("Измеренное количество масла (кг/т | гр/кг)");
b.Property<double?>("MixtureActualRate") b.Property<double>("MixtureActualRate")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)"); .HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)");
b.Property<double?>("MixtureAmineContent") b.Property<double>("MixtureAmineContent")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Содержание аминов в кондиционирующей смеси (%)"); .HasComment("Содержание аминов в кондиционирующей смеси (%)");
b.Property<string>("MixtureName") b.Property<string>("MixtureName")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Название используемой кондиционирующей смеси"); .HasComment("Название используемой кондиционирующей смеси");
b.Property<double?>("MixtureNormalRate") b.Property<double>("MixtureNormalRate")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)"); .HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
b.Property<string>("SampleBrand") b.Property<string>("SampleBrand")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Марка пробы"); .HasComment("Марка пробы");
b.Property<string>("SampleComment") b.Property<string>("SampleComment")
.HasMaxLength(2048) .IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе"); .HasComment("Дополнительная информация о пробе");
b.Property<double?>("SampleMass") b.Property<double>("SampleMass")
.HasColumnType("REAL") .HasColumnType("REAL")
.HasComment("Масса пробы (кг)"); .HasComment("Масса пробы (кг)");
b.Property<string>("SampleName") b.Property<string>("SampleName")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Название пробы"); .HasComment("Название пробы");
b.Property<DateTime?>("SamplingDateTime") b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы"); .HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace") b.Property<string>("SamplingPlace")
.HasMaxLength(512) .IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Место отбора пробы"); .HasComment("Место отбора пробы");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("CalibrationRecords"); b.ToTable("SampleRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.ImageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<string>("ImagePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Путь к файлу изображения");
b.Property<int?>("UsedInCalibrationRecordSampleId")
.HasColumnType("INTEGER");
b.Property<int?>("UsedInSeparatorRecordId")
.HasColumnType("INTEGER");
b.Property<double>("Uv254Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 254 нм (0..1)");
b.Property<double>("Uv365Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 365 нм (0..1)");
b.Property<double>("VisibleIntensity")
.HasColumnType("REAL")
.HasComment("Интенсивность видимого света (0..1)");
b.HasKey("Id");
b.HasIndex("UsedInCalibrationRecordSampleId");
b.HasIndex("UsedInSeparatorRecordId");
b.ToTable("ImageRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b =>
@@ -146,6 +102,12 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Партия сепаратора"); .HasComment("Партия сепаратора");
b.Property<string>("Image")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение");
b.Property<string>("Type") b.Property<string>("Type")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
@@ -157,46 +119,58 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Calibration.Migrations
b.ToTable("SeparatorRecords"); b.ToTable("SeparatorRecords");
}); });
modelBuilder.Entity("CalibrationSeparatorImage", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.VectorRecord", b =>
{ {
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", null) b.Property<int>("Id")
.WithMany() .ValueGeneratedOnAdd()
.HasForeignKey("CalibrationRecordId") .HasColumnType("INTEGER")
.OnDelete(DeleteBehavior.Cascade) .HasComment("Уникальный идентификатор");
.IsRequired();
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.ImageRecord", null) b.Property<int?>("SampleRecordId")
.WithMany() .HasColumnType("INTEGER");
.HasForeignKey("ImageRecordId")
.OnDelete(DeleteBehavior.Cascade) b.Property<int?>("SeparatorRecordId")
.IsRequired(); .HasColumnType("INTEGER");
b.PrimitiveCollection<string>("Values")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения вектора");
b.HasKey("Id");
b.HasIndex("SampleRecordId");
b.HasIndex("SeparatorRecordId");
b.ToTable("VectorRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.ImageRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.VectorRecord", b =>
{ {
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", "UsedInCalibrationRecordSample") b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.SampleRecord", "SampleRecord")
.WithMany("SampleImages") .WithMany("VectorRecords")
.HasForeignKey("UsedInCalibrationRecordSampleId") .HasForeignKey("SampleRecordId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", "UsedInSeparatorRecord") b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", "SeparatorRecord")
.WithMany("Images") .WithMany("VectorRecords")
.HasForeignKey("UsedInSeparatorRecordId") .HasForeignKey("SeparatorRecordId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.Navigation("UsedInCalibrationRecordSample"); b.Navigation("SampleRecord");
b.Navigation("UsedInSeparatorRecord"); b.Navigation("SeparatorRecord");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.CalibrationRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SampleRecord", b =>
{ {
b.Navigation("SampleImages"); b.Navigation("VectorRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Calibration.SeparatorRecord", b =>
{ {
b.Navigation("Images"); b.Navigation("VectorRecords");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
@@ -1,35 +1,43 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration; namespace GSS2.Core.Analysis.AmineContent.Database.Calibration;
public record CalibrationRecord public record SampleRecord
{ {
[Comment("Уникальный идентификатор")] [Comment("Уникальный идентификатор")]
public int Id { get; set; } public int Id { get; set; }
[Comment("Название пробы")] [Comment("Название пробы")]
public required string? SampleName { get; set; } [MaxLength(256)]
public required string SampleName { get; set; }
[Comment("Дополнительная информация о пробе")] [Comment("Дополнительная информация о пробе")]
public required string? SampleComment { get; set; } [MaxLength(512)]
public required string SampleComment { get; set; }
[Comment("Марка пробы")] [Comment("Марка пробы")]
public required string? SampleBrand { get; set; } [MaxLength(256)]
public required string SampleBrand { get; set; }
[Comment("Масса пробы (кг)")] [Comment("Масса пробы (кг)")]
public required double? SampleMass { get; set; } public required double SampleMass { get; set; }
[Comment("Дата и время отбора пробы")] [Comment("Дата и время отбора пробы")]
public required DateTime? SamplingDateTime { get; set; } public required DateTime SamplingDateTime { get; set; }
[Comment("Место отбора пробы")] [Comment("Место отбора пробы")]
public required string? SamplingPlace { get; set; } [MaxLength(256)]
public required string SamplingPlace { get; set; }
[Comment("Название используемой кондиционирующей смеси")] [Comment("Название используемой кондиционирующей смеси")]
public required string? MixtureName { get; set; } [MaxLength(256)]
public required string MixtureName { get; set; }
[Comment("Содержание аминов в кондиционирующей смеси (%)")] [Comment("Содержание аминов в кондиционирующей смеси (%)")]
public required double? MixtureAmineContent { get; set; } public required double MixtureAmineContent { get; set; }
[Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")] [Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")]
public required double? MixtureNormalRate { get; set; } public required double MixtureNormalRate { get; set; }
[Comment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)")] [Comment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)")]
public required double? MixtureActualRate { get; set; } public required double MixtureActualRate { get; set; }
[Comment("Измеренное количество масла (кг/т | гр/кг)")] [Comment("Измеренное количество масла (кг/т | гр/кг)")]
public required double? MeasuredContent { get; set; } public required double MeasuredContent { get; set; }
[Comment("Изображения пробы")] [Comment("Изображение")]
public required List<ImageRecord> SampleImages { get; set; } [MaxLength(256)]
[Comment("Изображения пустого сепаратора")] public required string Image { get; set; }
public required List<ImageRecord> SeparatorImages { get; set; } [Comment("Векторы")]
public required List<VectorRecord> VectorRecords { get; set; } = new();
} }
@@ -1,4 +1,4 @@
using System.IO.Pipelines; using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -9,9 +9,14 @@ public record SeparatorRecord
[Comment("Уникальный идентификатор")] [Comment("Уникальный идентификатор")]
public int Id { get; set; } public int Id { get; set; }
[Comment("Тип сепаратора")] [Comment("Тип сепаратора")]
[MaxLength(256)]
public required string Type { get; set; } public required string Type { get; set; }
[Comment("Партия сепаратора")] [Comment("Партия сепаратора")]
[MaxLength(256)]
public required string Batch { get; set; } public required string Batch { get; set; }
[Comment("Изображения сепаратора")] [Comment("Изображение")]
public required List<ImageRecord> Images { get; set; } [MaxLength(256)]
public required string Image { get; set; }
[Comment("Векторы")]
public required List<VectorRecord> VectorRecords { get; set; } = new();
} }
@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Calibration;
public record VectorRecord
{
[Comment("Уникальный идентификатор")]
public int Id { get; set; }
[Comment("Значения вектора")]
public required float[] Values { get; set; }
public int? SeparatorRecordId { get; set; }
public SeparatorRecord? SeparatorRecord { get; set; }
public int? SampleRecordId { get; set; }
public SampleRecord? SampleRecord { get; set; }
}
@@ -1,70 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Results;
public class AmineContentResultsContext : DbContext
{
public class Record
{
[Comment("Уникальный идентификатор")]
public int Id { get; set; }
[Comment("Название пробы")]
public string SampleName { get; set; } = string.Empty;
[Comment("Дополнительная информация о пробе")]
public string SampleComment { get; set; } = string.Empty;
[Comment("Марка пробы")]
public string SampleBrand { get; set; } = string.Empty;
[Comment("Масса пробы (кг)")]
public double SampleMass { get; set; } = double.NaN;
[Comment("Дата и время отбора пробы")]
public DateTime SamplingDateTime { get; set; } = DateTime.Now;
[Comment("Место отбора пробы")]
public string SamplingPlace { get; set; } = string.Empty;
[Comment("Изображения пробы")]
public List<ImageData> SampleImages { get; set; } = new();
[Comment("Изображения пустого сепаратора")]
public List<ImageData> SeparatorImages { get; set; } = new();
[Comment("Температура воздуха (℃)")]
public double AirTemperature { get; set; } = new();
[Comment("Относительная влажность воздуха (%)")]
public double AirHumidity { get; set; } = new();
}
public class ImageData
{
[Comment("Уникальный идентификатор")]
public int Id { get; set; }
[Comment("Интенсивность видимого света (0..1)")]
public double VisibleIntensity { get; set; } = double.NaN;
[Comment("Интенсивность УФ излучения 365 нм (0..1)")]
public double Uv365Intensity { get; set; } = double.NaN;
[Comment("Интенсивность УФ излучения 254 нм (0..1)")]
public double Uv254Intensity { get; set; } = double.NaN;
[Comment("Тип изображения (sample - проба или separator - сепаратор)")]
public string ImageType { get; set; } = string.Empty;
[Comment("Путь к файлу изображения")]
public string ImagePath { get; set; } = string.Empty;
[Comment("Записи в которых это изображение используется как изображение пробы")]
public List<Record> SampleRecords { get; set; } = new();
[Comment("Записи в которых это изображение используется как изображение сепаратора")]
public List<Record> SeparatorRecords { get; set; } = new();
}
public DbSet<Record> Records => Set<Record>();
public DbSet<ImageData> Images => Set<ImageData>();
public AmineContentResultsContext(DbContextOptions<AmineContentResultsContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Record>()
.HasMany(r => r.SampleImages)
.WithMany(i => i.SampleRecords)
.UsingEntity(j => j.ToTable("SampleImages"));
modelBuilder.Entity<Record>()
.HasMany(r => r.SeparatorImages)
.WithMany(i => i.SeparatorRecords)
.UsingEntity(j => j.ToTable("SeparatorImages"));
}
}
@@ -1,167 +0,0 @@
// <auto-generated />
using System;
using GSS2.Core.Analysis.AmineContent.Database.Results;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations
{
[DbContext(typeof(AmineContentResultsContext))]
[Migration("20260313083009_AmineContentResults0")]
partial class AmineContentResults0
{
/// <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.Results.AmineContentResultsContext+ImageData", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<string>("ImagePath")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Путь к файлу изображения");
b.Property<string>("ImageType")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Тип изображения (sample - проба или separator - сепаратор)");
b.Property<double>("Uv254Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 254 нм (0..1)");
b.Property<double>("Uv365Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 365 нм (0..1)");
b.Property<double>("VisibleIntensity")
.HasColumnType("REAL")
.HasComment("Интенсивность видимого света (0..1)");
b.HasKey("Id");
b.ToTable("Images");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+Record", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<double>("AirHumidity")
.HasColumnType("REAL")
.HasComment("Относительная влажность воздуха (%)");
b.Property<double>("AirTemperature")
.HasColumnType("REAL")
.HasComment("Температура воздуха (℃)");
b.Property<string>("SampleBrand")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Марка пробы");
b.Property<string>("SampleComment")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе");
b.Property<double>("SampleMass")
.HasColumnType("REAL")
.HasComment("Масса пробы (кг)");
b.Property<string>("SampleName")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Название пробы");
b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Место отбора пробы");
b.HasKey("Id");
b.ToTable("Records");
});
modelBuilder.Entity("ImageDataRecord", b =>
{
b.Property<int>("SampleImagesId")
.HasColumnType("INTEGER");
b.Property<int>("SampleRecordsId")
.HasColumnType("INTEGER");
b.HasKey("SampleImagesId", "SampleRecordsId");
b.HasIndex("SampleRecordsId");
b.ToTable("SampleImages", (string)null);
});
modelBuilder.Entity("ImageDataRecord1", b =>
{
b.Property<int>("SeparatorImagesId")
.HasColumnType("INTEGER");
b.Property<int>("SeparatorRecordsId")
.HasColumnType("INTEGER");
b.HasKey("SeparatorImagesId", "SeparatorRecordsId");
b.HasIndex("SeparatorRecordsId");
b.ToTable("SeparatorImages", (string)null);
});
modelBuilder.Entity("ImageDataRecord", b =>
{
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+ImageData", null)
.WithMany()
.HasForeignKey("SampleImagesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+Record", null)
.WithMany()
.HasForeignKey("SampleRecordsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ImageDataRecord1", b =>
{
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+ImageData", null)
.WithMany()
.HasForeignKey("SeparatorImagesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+Record", null)
.WithMany()
.HasForeignKey("SeparatorRecordsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -1,126 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations
{
/// <inheritdoc />
public partial class AmineContentResults0 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Images",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
VisibleIntensity = table.Column<double>(type: "REAL", nullable: false, comment: "Интенсивность видимого света (0..1)"),
Uv365Intensity = table.Column<double>(type: "REAL", nullable: false, comment: "Интенсивность УФ излучения 365 нм (0..1)"),
Uv254Intensity = table.Column<double>(type: "REAL", nullable: false, comment: "Интенсивность УФ излучения 254 нм (0..1)"),
ImageType = table.Column<string>(type: "TEXT", nullable: false, comment: "Тип изображения (sample - проба или separator - сепаратор)"),
ImagePath = table.Column<string>(type: "TEXT", nullable: false, comment: "Путь к файлу изображения")
},
constraints: table =>
{
table.PrimaryKey("PK_Images", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Records",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
SampleName = table.Column<string>(type: "TEXT", nullable: false, comment: "Название пробы"),
SampleComment = table.Column<string>(type: "TEXT", nullable: false, comment: "Дополнительная информация о пробе"),
SampleBrand = table.Column<string>(type: "TEXT", nullable: false, comment: "Марка пробы"),
SampleMass = table.Column<double>(type: "REAL", nullable: false, comment: "Масса пробы (кг)"),
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы"),
SamplingPlace = table.Column<string>(type: "TEXT", nullable: false, comment: "Место отбора пробы"),
AirTemperature = table.Column<double>(type: "REAL", nullable: false, comment: "Температура воздуха (℃)"),
AirHumidity = table.Column<double>(type: "REAL", nullable: false, comment: "Относительная влажность воздуха (%)")
},
constraints: table =>
{
table.PrimaryKey("PK_Records", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SampleImages",
columns: table => new
{
SampleImagesId = table.Column<int>(type: "INTEGER", nullable: false),
SampleRecordsId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SampleImages", x => new { x.SampleImagesId, x.SampleRecordsId });
table.ForeignKey(
name: "FK_SampleImages_Images_SampleImagesId",
column: x => x.SampleImagesId,
principalTable: "Images",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SampleImages_Records_SampleRecordsId",
column: x => x.SampleRecordsId,
principalTable: "Records",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SeparatorImages",
columns: table => new
{
SeparatorImagesId = table.Column<int>(type: "INTEGER", nullable: false),
SeparatorRecordsId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SeparatorImages", x => new { x.SeparatorImagesId, x.SeparatorRecordsId });
table.ForeignKey(
name: "FK_SeparatorImages_Images_SeparatorImagesId",
column: x => x.SeparatorImagesId,
principalTable: "Images",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SeparatorImages_Records_SeparatorRecordsId",
column: x => x.SeparatorRecordsId,
principalTable: "Records",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SampleImages_SampleRecordsId",
table: "SampleImages",
column: "SampleRecordsId");
migrationBuilder.CreateIndex(
name: "IX_SeparatorImages_SeparatorRecordsId",
table: "SeparatorImages",
column: "SeparatorRecordsId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SampleImages");
migrationBuilder.DropTable(
name: "SeparatorImages");
migrationBuilder.DropTable(
name: "Images");
migrationBuilder.DropTable(
name: "Records");
}
}
}
@@ -0,0 +1,123 @@
// <auto-generated />
using System;
using GSS2.Core.Analysis.AmineContent.Database.Results;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations
{
[DbContext(typeof(ResultsContext))]
[Migration("20260429070705_Results0")]
partial class Results0
{
/// <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.Results.ResultRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<string>("Image")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение");
b.Property<string>("MaskImage")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Маска");
b.Property<double>("MaxValue")
.HasColumnType("REAL")
.HasComment("Максимальное значение");
b.Property<double>("MeanValue")
.HasColumnType("REAL")
.HasComment("Среднее значение");
b.PrimitiveCollection<string>("MeanValues")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Средние значения");
b.Property<double>("MeanVariance")
.HasColumnType("REAL")
.HasComment("Средняя дисперсия");
b.Property<double>("MinValue")
.HasColumnType("REAL")
.HasComment("Минимальное значение");
b.Property<int>("ParticlesCount")
.HasColumnType("INTEGER")
.HasComment("Количество частиц");
b.Property<string>("ResultImage")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение результата");
b.Property<string>("SampleBrand")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.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<double>("VarianceOfMean")
.HasColumnType("REAL")
.HasComment("Дисперсия среднего");
b.PrimitiveCollection<string>("Variances")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Дисперсии");
b.HasKey("Id");
b.ToTable("ResultRecords");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations
{
/// <inheritdoc />
public partial class Results0 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ResultRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Уникальный идентификатор")
.Annotation("Sqlite:Autoincrement", true),
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: "Место отбора пробы"),
Image = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Изображение"),
SeparatorType = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип сепаратора"),
SampleBrand = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"),
MaskImage = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Маска"),
ResultImage = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Изображение результата"),
MinValue = table.Column<double>(type: "REAL", nullable: false, comment: "Минимальное значение"),
MaxValue = table.Column<double>(type: "REAL", nullable: false, comment: "Максимальное значение"),
ParticlesCount = table.Column<int>(type: "INTEGER", nullable: false, comment: "Количество частиц"),
MeanValue = table.Column<double>(type: "REAL", nullable: false, comment: "Среднее значение"),
MeanVariance = table.Column<double>(type: "REAL", nullable: false, comment: "Средняя дисперсия"),
VarianceOfMean = table.Column<double>(type: "REAL", nullable: false, comment: "Дисперсия среднего"),
MeanValues = table.Column<string>(type: "TEXT", nullable: false, comment: "Средние значения"),
Variances = table.Column<string>(type: "TEXT", nullable: false, comment: "Дисперсии")
},
constraints: table =>
{
table.PrimaryKey("PK_ResultRecords", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ResultRecords");
}
}
}
@@ -1,164 +0,0 @@
// <auto-generated />
using System;
using GSS2.Core.Analysis.AmineContent.Database.Results;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations
{
[DbContext(typeof(AmineContentResultsContext))]
partial class AmineContentResultsContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+ImageData", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<string>("ImagePath")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Путь к файлу изображения");
b.Property<string>("ImageType")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Тип изображения (sample - проба или separator - сепаратор)");
b.Property<double>("Uv254Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 254 нм (0..1)");
b.Property<double>("Uv365Intensity")
.HasColumnType("REAL")
.HasComment("Интенсивность УФ излучения 365 нм (0..1)");
b.Property<double>("VisibleIntensity")
.HasColumnType("REAL")
.HasComment("Интенсивность видимого света (0..1)");
b.HasKey("Id");
b.ToTable("Images");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+Record", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<double>("AirHumidity")
.HasColumnType("REAL")
.HasComment("Относительная влажность воздуха (%)");
b.Property<double>("AirTemperature")
.HasColumnType("REAL")
.HasComment("Температура воздуха (℃)");
b.Property<string>("SampleBrand")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Марка пробы");
b.Property<string>("SampleComment")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе");
b.Property<double>("SampleMass")
.HasColumnType("REAL")
.HasComment("Масса пробы (кг)");
b.Property<string>("SampleName")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Название пробы");
b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Место отбора пробы");
b.HasKey("Id");
b.ToTable("Records");
});
modelBuilder.Entity("ImageDataRecord", b =>
{
b.Property<int>("SampleImagesId")
.HasColumnType("INTEGER");
b.Property<int>("SampleRecordsId")
.HasColumnType("INTEGER");
b.HasKey("SampleImagesId", "SampleRecordsId");
b.HasIndex("SampleRecordsId");
b.ToTable("SampleImages", (string)null);
});
modelBuilder.Entity("ImageDataRecord1", b =>
{
b.Property<int>("SeparatorImagesId")
.HasColumnType("INTEGER");
b.Property<int>("SeparatorRecordsId")
.HasColumnType("INTEGER");
b.HasKey("SeparatorImagesId", "SeparatorRecordsId");
b.HasIndex("SeparatorRecordsId");
b.ToTable("SeparatorImages", (string)null);
});
modelBuilder.Entity("ImageDataRecord", b =>
{
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+ImageData", null)
.WithMany()
.HasForeignKey("SampleImagesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+Record", null)
.WithMany()
.HasForeignKey("SampleRecordsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ImageDataRecord1", b =>
{
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+ImageData", null)
.WithMany()
.HasForeignKey("SeparatorImagesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("GSS2.Core.Analysis.AmineContent.Database.Results.AmineContentResultsContext+Record", null)
.WithMany()
.HasForeignKey("SeparatorRecordsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,120 @@
// <auto-generated />
using System;
using GSS2.Core.Analysis.AmineContent.Database.Results;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Results.Migrations
{
[DbContext(typeof(ResultsContext))]
partial class ResultsContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.Results.ResultRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Уникальный идентификатор");
b.Property<string>("Image")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение");
b.Property<string>("MaskImage")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Маска");
b.Property<double>("MaxValue")
.HasColumnType("REAL")
.HasComment("Максимальное значение");
b.Property<double>("MeanValue")
.HasColumnType("REAL")
.HasComment("Среднее значение");
b.PrimitiveCollection<string>("MeanValues")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Средние значения");
b.Property<double>("MeanVariance")
.HasColumnType("REAL")
.HasComment("Средняя дисперсия");
b.Property<double>("MinValue")
.HasColumnType("REAL")
.HasComment("Минимальное значение");
b.Property<int>("ParticlesCount")
.HasColumnType("INTEGER")
.HasComment("Количество частиц");
b.Property<string>("ResultImage")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Изображение результата");
b.Property<string>("SampleBrand")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.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<double>("VarianceOfMean")
.HasColumnType("REAL")
.HasComment("Дисперсия среднего");
b.PrimitiveCollection<string>("Variances")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Дисперсии");
b.HasKey("Id");
b.ToTable("ResultRecords");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,53 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Results;
public class ResultRecord
{
[Comment("Уникальный идентификатор")]
public int Id { 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[] MeanValues { get; set; }
[Comment("Дисперсии")]
public required double[] Variances { get; set; }
}
@@ -0,0 +1,12 @@
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database.Results;
public class ResultsContext : DbContext
{
public DbSet<ResultRecord> ResultRecords => Set<ResultRecord>();
public ResultsContext(DbContextOptions<ResultsContext> options)
: base(options)
{ }
}
@@ -3,16 +3,16 @@ using Microsoft.EntityFrameworkCore.Design;
namespace GSS2.Core.Analysis.AmineContent.Database.Results; namespace GSS2.Core.Analysis.AmineContent.Database.Results;
public class AmineContentResultsContextFactory : IDesignTimeDbContextFactory<AmineContentResultsContext> public class ResultsContextFactory : IDesignTimeDbContextFactory<ResultsContext>
{ {
public AmineContentResultsContext CreateDbContext(string[] args) public ResultsContext CreateDbContext(string[] args)
{ {
var optionsBuilder = new DbContextOptionsBuilder<AmineContentResultsContext>(); var optionsBuilder = new DbContextOptionsBuilder<ResultsContext>();
// Строка подключения для миграций в design-time // Строка подключения для миграций в design-time
// в runtime должна использоваться настоящая ConnectionString // в runtime должна использоваться настоящая ConnectionString
optionsBuilder.UseSqlite("Data Source=AmineContentResults.db"); optionsBuilder.UseSqlite("Data Source=AmineContentResults.db");
return new AmineContentResultsContext(optionsBuilder.Options); return new ResultsContext(optionsBuilder.Options);
} }
} }
@@ -11,7 +11,7 @@ using OpenCvSharp;
namespace GSS2.Core.Analysis.AmineContent; namespace GSS2.Core.Analysis.AmineContent;
public class AmineContentImageCapturerService public class ImageCapturerService
{ {
// Список интенсивностей освещения для каждого кадра // Список интенсивностей освещения для каждого кадра
public static readonly ReadOnlyCollection<(double visible, double uv365, double uv254)> IlluminatorIntensities = public static readonly ReadOnlyCollection<(double visible, double uv365, double uv254)> IlluminatorIntensities =
@@ -30,13 +30,13 @@ public class AmineContentImageCapturerService
(0.00625, 1.0, 1.0) (0.00625, 1.0, 1.0)
]; ];
private readonly ILogger<AmineContentImageCapturerService> _logger; private readonly ILogger<ImageCapturerService> _logger;
private readonly CameraService _camera; private readonly CameraService _camera;
private readonly IlluminatorService _illuminator; private readonly IlluminatorService _illuminator;
private readonly LightsService _lights; private readonly LightsService _lights;
public AmineContentImageCapturerService( public ImageCapturerService(
ILogger<AmineContentImageCapturerService> logger, ILogger<ImageCapturerService> logger,
CameraService camera, CameraService camera,
IlluminatorService illuminator, IlluminatorService illuminator,
LightsService lights LightsService lights
@@ -0,0 +1,17 @@
namespace GSS2.Core.Analysis.AmineContent;
public record ImageData
{
public string Path { get; set; }
public double VisibleIntensity { get; set; }
public double Uv365Intensity { get; set; }
public double Uv254Intensity { get; set; }
public ImageData(string path, double visibleIntensity, double uv365Intensity, double uv254Intensity)
{
Path = path;
VisibleIntensity = visibleIntensity;
Uv365Intensity = uv365Intensity;
Uv254Intensity = uv254Intensity;
}
}
+7 -4
View File
@@ -95,6 +95,9 @@ public static class DependencyInjectionHelper
); );
public static IServiceCollection AddComputeResourcesService(this IServiceCollection services) => services public static IServiceCollection AddComputeResourcesService(this IServiceCollection services) => services
.AddSingleton<ComputeResourcesService>(); .AddSingleton<ComputeResourcesService>();
[Obsolete]
public static IServiceCollection AddUsbService(this IServiceCollection services) => services
.AddSingleton<UsbService>();
public static IServiceCollection AddImageStorageService(this IServiceCollection services, DirectoryInfo rootDirectory) => services public static IServiceCollection AddImageStorageService(this IServiceCollection services, DirectoryInfo rootDirectory) => services
.AddSingleton<ImageStorageService>( .AddSingleton<ImageStorageService>(
serviceProvider => serviceProvider =>
@@ -107,7 +110,7 @@ public static class DependencyInjectionHelper
); );
public static IServiceCollection AddAmineContentCalibrationContext(this IServiceCollection services, IConfigurationManager configuration) => services public static IServiceCollection AddAmineContentCalibrationContext(this IServiceCollection services, IConfigurationManager configuration) => services
.AddDbContext<AmineContentCalibrationContext>( .AddDbContext<CalibrationContext>(
options => options =>
{ {
var databasePath = configuration.GetConnectionString("AmineContentCalibrationDatabasePath"); var databasePath = configuration.GetConnectionString("AmineContentCalibrationDatabasePath");
@@ -116,7 +119,7 @@ public static class DependencyInjectionHelper
ServiceLifetime.Singleton ServiceLifetime.Singleton
); );
public static IServiceCollection AddAmineContentResultsContext(this IServiceCollection services, IConfigurationManager configuration) => services public static IServiceCollection AddAmineContentResultsContext(this IServiceCollection services, IConfigurationManager configuration) => services
.AddDbContext<AmineContentResultsContext>( .AddDbContext<ResultsContext>(
options => options =>
{ {
var databasePath = configuration.GetConnectionString("AmineContentResultsDatabasePath"); var databasePath = configuration.GetConnectionString("AmineContentResultsDatabasePath");
@@ -126,8 +129,8 @@ public static class DependencyInjectionHelper
); );
public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services) => services public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services) => services
.AddSingleton<AmineContentImageCapturerService>(); .AddSingleton<ImageCapturerService>();
public static IServiceCollection AddAmineContentAnalyzerService(this IServiceCollection services) => services public static IServiceCollection AddAmineContentAnalyzerService(this IServiceCollection services) => services
.AddSingleton<AmineContentAnalyzerService>(); .AddSingleton<AnalyzerService>();
} }
+12 -1
View File
@@ -34,5 +34,16 @@ public static class EnumerableExtensions
foreach (var kv in collection) foreach (var kv in collection)
values.Add(kv); values.Add(kv);
} }
}
public static double Variance(this IEnumerable<double> values)
{
if (values.Count() == 1)
return 0;
double mean = values.Average();
double variance = 0.0;
foreach (int value in values)
variance += Math.Pow(value - mean, 2.0);
return variance / values.Count();
}
}
+1
View File
@@ -17,6 +17,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.ML" Version="5.0.0" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" /> <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.linux-arm" Version="4.11.0.20250506" /> <PackageReference Include="OpenCvSharp4.runtime.linux-arm" Version="4.11.0.20250506" />
<PackageReference Include="System.Device.Gpio" Version="4.0.1" /> <PackageReference Include="System.Device.Gpio" Version="4.0.1" />
+72
View File
@@ -0,0 +1,72 @@
using System.Device.I2c;
using Microsoft.Extensions.Logging;
using GSS2.Core.Abstractions;
using System.Diagnostics;
namespace GSS2.Core.Hardware;
[Obsolete("Not implemented")]
public class UsbService : IDisposable
{
private bool _disposedValue;
private readonly ILogger<UsbService> _logger;
private readonly Process _monitor;
public UsbService(ILogger<UsbService> logger)
{
_logger = logger;
_logger.LogInformation("Инициализация");
_monitor = new Process();
_monitor.StartInfo = new ProcessStartInfo()
{
FileName = "udisksctl",
Arguments = "monitor",
RedirectStandardOutput = true,
RedirectStandardError = true
};
_monitor.OutputDataReceived += MonitorDataReceived;
_monitor.ErrorDataReceived += MonitorDataReceived;
_monitor.Exited += (s, ea) => _logger.LogWarning("Монитор udisksctl отключен");
if (_monitor.Start())
_logger.LogInformation("Монитор udisksctl запущен");
else
_logger.LogError("Не удалось запустить монитор udisksctl");
_monitor.BeginOutputReadLine();
_monitor.BeginErrorReadLine();
_logger.LogInformation("Инициализировано");
}
private void MonitorDataReceived(object sender, DataReceivedEventArgs ea)
{
if (ea.Data is null)
return;
_logger.LogInformation("Data rec {}", ea.Data);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
try
{
_monitor.Close();
}
catch { }
}
_disposedValue = true;
}
}
void IDisposable.Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace GSS2.Core.Hardware; namespace GSS2.Core;
public class ImageStorageService public class ImageStorageService
{ {
@@ -10,19 +10,19 @@ public class ImageStorageService
public ImageStorageService(ILogger<ImageStorageService> logger, DirectoryInfo rootDirectory) public ImageStorageService(ILogger<ImageStorageService> logger, DirectoryInfo rootDirectory)
{ {
_logger = logger; _logger = logger;
_logger.LogInformation("Initialization"); _logger.LogInformation("Инициализация");
_rootDirectory = rootDirectory; _rootDirectory = rootDirectory;
if (!_rootDirectory.Exists) if (!_rootDirectory.Exists)
{ {
_rootDirectory.Create(); _rootDirectory.Create();
_logger.LogInformation("Created directory: {}", _rootDirectory.FullName); _logger.LogInformation("Создана директория: {}", _rootDirectory.FullName);
} }
_logger.LogInformation("Initialized"); _logger.LogInformation("Инициализировано");
} }
public async Task<string> SaveAsync(Stream fileStream, string extension, string subDirectory, CancellationToken ct = default) public async Task<string> SaveAsync(Stream fileStream, string extension, string subDirectory, CancellationToken cancellationToken = default)
{ {
var fileName = GenerateFileName(extension); var fileName = GenerateFileName(extension);
var fullPath = GetFullPath(fileName, subDirectory); var fullPath = GetFullPath(fileName, subDirectory);
@@ -39,9 +39,9 @@ public class ImageStorageService
81920, 81920,
useAsync: true); useAsync: true);
await fileStream.CopyToAsync(file, ct); await fileStream.CopyToAsync(file, cancellationToken);
_logger.LogInformation("File saved: {FileName}", fileName); _logger.LogInformation("Файл сохранён: {}", fileName);
return fileName; return fileName;
} }
@@ -64,13 +64,13 @@ public class ImageStorageService
return false; return false;
File.Delete(fullPath); File.Delete(fullPath);
_logger.LogInformation("File removed: {}", fileName); _logger.LogInformation("Файл удалён: {}", fileName);
var parentDirectory = Directory.GetParent(fullPath); var parentDirectory = Directory.GetParent(fullPath);
if (parentDirectory?.GetFiles().Count() == 0) if (parentDirectory?.GetFiles().Count() == 0)
{ {
parentDirectory.Delete(); parentDirectory.Delete();
_logger.LogInformation("Empty directory removed: {}", parentDirectory.Name); _logger.LogInformation("Удалена пустая директория: {}", parentDirectory.Name);
} }
return true; return true;
@@ -84,6 +84,9 @@ public class ImageStorageService
public string? GetFullPath(string fileName, string? subDirectory = null) public string? GetFullPath(string fileName, string? subDirectory = null)
{ {
if (fileName.Count() < 4)
return null;
var level1 = fileName.Substring(0, 2); var level1 = fileName.Substring(0, 2);
var level2 = fileName.Substring(2, 2); var level2 = fileName.Substring(2, 2);
@@ -107,4 +110,4 @@ public class ImageStorageService
.SelectMany(ListFiles) .SelectMany(ListFiles)
.ToList(); .ToList();
} }
} }
+2 -2
View File
@@ -1,2 +1,2 @@
dotnet ef migrations add -o .\Analysis\AmineContent\Database\Calibration\Migrations --project .\GSS2.Core\ AmineContentCalibration0 --context AmineContentCalibrationContext dotnet ef migrations add -o .\Analysis\AmineContent\Database\Calibration\Migrations --project .\GSS2.Core\ Calibration0 --context CalibrationContext
dotnet ef migrations add -o .\Analysis\AmineContent\Database\Results\Migrations --project .\GSS2.Core\ AmineContentResults0 --context AmineContentResultsContext dotnet ef migrations add -o .\Analysis\AmineContent\Database\Results\Migrations --project .\GSS2.Core\ Results0 --context ResultsContext
+2 -2
View File
@@ -1,2 +1,2 @@
dotnet ef migrations add -o ./Analysis/AmineContent/Database/Calibration/Migrations --project ./GSS2.Core/ AmineContentCalibration0 --context AmineContentCalibrationContext dotnet ef migrations add -o ./Analysis/AmineContent/Database/Calibration/Migrations --project ./GSS2.Core/ Calibration0 --context CalibrationContext
dotnet ef migrations add -o ./Analysis/AmineContent/Database/Results/Migrations --project ./GSS2.Core/ AmineContentResults0 --context AmineContentResultsContext dotnet ef migrations add -o ./Analysis/AmineContent/Database/Results/Migrations --project ./GSS2.Core/ Results0 --context ResultsContext
+27 -25
View File
@@ -1,30 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.LinuxFramebuffer" Version="11.3.11" /> <PackageReference Include="Avalonia.LinuxFramebuffer" Version="12.0.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc6.1" /> <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Avalonia" Version="11.3.11" /> <PackageReference Include="Avalonia" Version="12.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" /> <PackageReference Include="Avalonia.Desktop" Version="12.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.0" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11" /> <PackageReference Include="Avalonia.Fonts.Inter" Version="12.0.0" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11"> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets> <!-- Avalonia.X11 зависит от Avalonia.FreeDesktop который зависит от Tmds.DBus.Protocol==0.90.3 -->
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <!-- Tmds.DBus.Protocol==0.90.3 имеет критическую уязвмость, поэтому превентивно установлена версия с исправлением -->
</PackageReference> <PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" /> <PackageReference Include="Xaml.Behaviors.Interactivity" Version="12.0.0" />
</ItemGroup> <PackageReference Include="Xaml.Behaviors.Interactions" Version="12.0.0" />
<PackageReference Include="Xaml.Behaviors.Interactions.Custom" Version="12.0.0" />
<ItemGroup> </ItemGroup>
<ProjectReference Include="..\GSS2.Core\GSS2.Core.csproj" />
</ItemGroup> <ItemGroup>
<ProjectReference Include="..\GSS2.Core\GSS2.Core.csproj" />
</ItemGroup>
</Project> </Project>
+6 -10
View File
@@ -4,34 +4,30 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
namespace GSS2.UI.Core.ViewModels; namespace GSS2.UI.Core.ViewModels;
public partial class AsyncImageRecordViewModel : ViewModelBase public partial class AsyncImageRecordViewModel : ViewModelBase
{ {
private static readonly SemaphoreSlim _semaphore = new(1, 1); private static readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly string _path; [ObservableProperty] public partial string Path { get; set; }
[ObservableProperty] public partial ImageRecord ImageRecord { get; set; }
[ObservableProperty] public partial IImage? Image { get; set; } [ObservableProperty] public partial IImage? Image { get; set; }
[ObservableProperty] public partial bool IsLoading { get; set; } = true; [ObservableProperty] public partial bool IsLoading { get; set; } = true;
public AsyncImageRecordViewModel(string path, ImageRecord imageRecord) public AsyncImageRecordViewModel(string path)
{ {
_path = path; Path = path;
ImageRecord = imageRecord;
_ = LoadAsync();
} }
partial void OnPathChanged(string value) => _ = LoadAsync();
private async Task LoadAsync() private async Task LoadAsync()
{ {
await _semaphore.WaitAsync(); await _semaphore.WaitAsync();
try try
{ {
var bitmap = await Task.Run(() => new Bitmap(_path)); var bitmap = await Task.Run(() => new Bitmap(Path));
await Dispatcher.UIThread.InvokeAsync(() => await Dispatcher.UIThread.InvokeAsync(() =>
{ {
+2 -2
View File
@@ -23,7 +23,7 @@ public partial class Application : Avalonia.Application
private bool _allShutdownSequencesRunning; private bool _allShutdownSequencesRunning;
public NavigationService Navigation { get; } public NavigationService? Navigation { get; private set; } = null;
public Application(IServiceProvider services, bool fullscreen = false) public Application(IServiceProvider services, bool fullscreen = false)
{ {
@@ -37,7 +37,6 @@ public partial class Application : Avalonia.Application
// Создаём DependencyInjectionViewLocator // Создаём DependencyInjectionViewLocator
_logger = _services.GetRequiredService<ILogger<Application>>(); _logger = _services.GetRequiredService<ILogger<Application>>();
_viewLocator = _services.GetRequiredService<DependencyInjectionViewLocator>(); _viewLocator = _services.GetRequiredService<DependencyInjectionViewLocator>();
Navigation = _services.GetRequiredService<NavigationService>();
} }
public override void Initialize() public override void Initialize()
@@ -131,6 +130,7 @@ public partial class Application : Avalonia.Application
desktop.MainWindow = mainWindow; desktop.MainWindow = mainWindow;
// Переключаемся на начальный элемент управления // Переключаемся на начальный элемент управления
Navigation = _services.GetRequiredService<NavigationService>();
Navigation.NavigateTo(mainView); Navigation.NavigateTo(mainView);
break; break;
-160
View File
@@ -1,160 +0,0 @@
using System.CommandLine;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using GSS2.Core;
using GSS2.Core.Hardware;
using GSS2.Core.Analysis.AmineContent.Database.Calibration;
using GSS2.Core.Analysis.AmineContent.Database.Results;
namespace GSS2.Commands;
public static class ImageStorageClear
{
private class ImageStorageClearWorker : BackgroundService
{
private readonly ILogger<ImageStorageClearWorker> _logger;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly AmineContentCalibrationContext _context;
private readonly ImageStorageService _imageStorage;
public ImageStorageClearWorker(
ILogger<ImageStorageClearWorker> logger,
IHostApplicationLifetime applicationLifetime,
AmineContentCalibrationContext context,
ImageStorageService imageStorage
)
{
_applicationLifetime = applicationLifetime;
_logger = logger;
_context = context;
_imageStorage = imageStorage;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Логгируем информацию
_logger.LogInformation("Очистка хранилища от неиспользуемых изображений...");
// Получаем все изображения из базы данных
var recordsFiles = _context.ImageRecords.Select(r => r.ImagePath);
// Получаем все изображения в хранилище
var storedFiles = _imageStorage.ListFiles()
.Select(Path.GetFileName)
.OfType<string>();
// Находим неиспользуемые изображения
var unusedFiles = storedFiles.Except(recordsFiles)
.ToList()
.Order();
// Если неиспользуемых изображений нет, то закрываемся
if (unusedFiles is null || unusedFiles.Count() == 0)
{
_logger.LogInformation("Неиспользуемые изображения не найдены");
_applicationLifetime.StopApplication();
return;
}
_logger.LogInformation("Найдено неиспользуемых изображений: {}\n\t{}", unusedFiles.Count(), string.Join("\n\t", unusedFiles));
// Пока пользователь не выдал правильное действие или не закрыл приложение
while (!stoppingToken.IsCancellationRequested)
{
// Задаём вопрос
_logger.LogInformation("Удалить эти файлы? (Да/Нет) ");
// Читаем консоль
var input = Console.ReadLine()?.Trim().ToLower();
// Если нет, то выходим
if (input == "n" || input == "no" || input == "н" || input == "нет")
break;
// Если да, то удаляем файлы и выходим
if (input == "y" || input == "yes" || input == "д" || input == "да")
{
foreach (var file in unusedFiles)
_imageStorage.Delete(file);
break;
}
}
_applicationLifetime.StopApplication();
}
}
public static readonly RootCommand RootCommand;
private static readonly Command Command;
static ImageStorageClear()
{
// Создаём команды, опции, аргументы и пр.
RootCommand = new("Очистить хранилище от неиспользуемых изображений");
Command = new("image-storage-clear")
{
Description = "Очистить хранилище от неиспользуемых изображений"
};
// Изменяем описания стандартных опций
Helper.TranslateDefaultOptionDescriptions(RootCommand);
// Наполняем команды
RootCommand.SetAction(CommandAction);
Command.SetAction(CommandAction);
}
/// <summary>
/// Получить команду как корневую
/// </summary>
public static RootCommand GetCommand() => RootCommand;
/// <summary>
/// Зарегистрировать команду как подкоманду
/// </summary>
/// <param name="command">Команда в которой необходимо зарегистрировать подкоманду</param>
public static void RegisterCommand(Command command) => command.Add(Command);
private static void CommandAction(ParseResult result)
{
// Создаём хост
var builder = Host.CreateApplicationBuilder();
// Добавляем конфигурацию из appsettings.json
builder.Configuration.AddAppSettingsJson();
// Конфигурируем логгер
builder.Logging.ConfigureLogging();
// Добавляем сервисы баз данных
builder.Services.AddAmineContentCalibrationContext(builder.Configuration);
builder.Services.AddAmineContentResultsContext(builder.Configuration);
// Добавляем сервисы аппаратной части
builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images"));
// Добавляем рабочий сервис
builder.Services.AddHostedService<ImageStorageClearWorker>();
// Собираем хост
var host = builder.Build();
// Применяем миграции баз данных если их схема данных изменялась
using (var scope = host.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<AmineContentCalibrationContext>();
context.Database.Migrate();
}
using (var scope = host.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<AmineContentResultsContext>();
context.Database.Migrate();
}
// Запускаем хост
host.Run();
}
}
+4 -2
View File
@@ -12,6 +12,7 @@ using GSS2.Core;
using GSS2.Core.Logging; using GSS2.Core.Logging;
using GSS2.Core.Analysis.AmineContent.Database.Calibration; using GSS2.Core.Analysis.AmineContent.Database.Calibration;
using GSS2.Core.Analysis.AmineContent.Database.Results; using GSS2.Core.Analysis.AmineContent.Database.Results;
using GSS2.Core.Hardware;
namespace GSS2.Commands; namespace GSS2.Commands;
@@ -160,6 +161,7 @@ public static class UI
builder.Services.AddComputeResourcesService(); builder.Services.AddComputeResourcesService();
builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images")); builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images"));
builder.Services.AddAmineContentImageCapturerService(); builder.Services.AddAmineContentImageCapturerService();
builder.Services.AddAmineContentAnalyzerService();
// Добавляем элементы интерфейса как сервисы // Добавляем элементы интерфейса как сервисы
builder.Services.AddViewsAndModels(); builder.Services.AddViewsAndModels();
@@ -174,12 +176,12 @@ public static class UI
// Применяем миграции баз данных если их схема данных изменялась // Применяем миграции баз данных если их схема данных изменялась
using (var scope = host.Services.CreateScope()) using (var scope = host.Services.CreateScope())
{ {
var context = scope.ServiceProvider.GetRequiredService<AmineContentCalibrationContext>(); var context = scope.ServiceProvider.GetRequiredService<CalibrationContext>();
context.Database.Migrate(); context.Database.Migrate();
} }
using (var scope = host.Services.CreateScope()) using (var scope = host.Services.CreateScope())
{ {
var context = scope.ServiceProvider.GetRequiredService<AmineContentResultsContext>(); var context = scope.ServiceProvider.GetRequiredService<ResultsContext>();
context.Database.Migrate(); context.Database.Migrate();
} }
+5 -2
View File
@@ -31,6 +31,11 @@ public static class DependencyInjectionHelper
.AddSingleton<GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel>() .AddSingleton<GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel>()
.AddSingleton<GSS2.UI.Core.Views.CameraView>() .AddSingleton<GSS2.UI.Core.Views.CameraView>()
.AddSingleton<GSS2.UI.Core.Views.IlluminatorControllerView>() .AddSingleton<GSS2.UI.Core.Views.IlluminatorControllerView>()
// База
.AddSingleton<ViewModels.DeleteConfirmViewModel>()
.AddSingleton<ViewModels.NumericKeyboardViewModel>()
.AddSingleton<Views.DeleteConfirmView>()
.AddSingleton<Views.NumericKeyboardView>()
// Общие // Общие
.AddSingleton<ViewModels.Common.SystemViewModel>() .AddSingleton<ViewModels.Common.SystemViewModel>()
.AddSingleton<ViewModels.Common.CameraViewModel>() .AddSingleton<ViewModels.Common.CameraViewModel>()
@@ -39,12 +44,10 @@ public static class DependencyInjectionHelper
// AmineContent // AmineContent
.AddSingleton<ViewModels.AmineContent.StartViewModel>() .AddSingleton<ViewModels.AmineContent.StartViewModel>()
.AddSingleton<ViewModels.AmineContent.AnalysisViewModel>() .AddSingleton<ViewModels.AmineContent.AnalysisViewModel>()
.AddSingleton<ViewModels.AmineContent.ResultsViewModel>()
.AddSingleton<ViewModels.AmineContent.SampleCalibrationViewModel>() .AddSingleton<ViewModels.AmineContent.SampleCalibrationViewModel>()
.AddSingleton<ViewModels.AmineContent.SeparatorCalibrationViewModel>() .AddSingleton<ViewModels.AmineContent.SeparatorCalibrationViewModel>()
.AddSingleton<Views.AmineContent.StartView>() .AddSingleton<Views.AmineContent.StartView>()
.AddSingleton<Views.AmineContent.AnalysisView>() .AddSingleton<Views.AmineContent.AnalysisView>()
.AddSingleton<Views.AmineContent.ResultsView>()
.AddSingleton<Views.AmineContent.SampleCalibrationView>() .AddSingleton<Views.AmineContent.SampleCalibrationView>()
.AddSingleton<Views.AmineContent.SeparatorCalibrationView>(); .AddSingleton<Views.AmineContent.SeparatorCalibrationView>();
} }
+2 -1
View File
@@ -5,6 +5,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<NoWarn>$(NoWarn);IDE0090;IDE1006;IDE0028;IDE0305;IDE0290;CA1826;CA1829;CA1873</NoWarn> <NoWarn>$(NoWarn);IDE0090;IDE1006;IDE0028;IDE0305;IDE0290;CA1826;CA1829;CA1873</NoWarn>
@@ -17,7 +18,7 @@
<PackageReference Include="Avalonia.LinuxFramebuffer" Version="12.0.0" /> <PackageReference Include="Avalonia.LinuxFramebuffer" Version="12.0.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.0" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0" /> <PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.1.0-dev-365" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
-1
View File
@@ -30,7 +30,6 @@ public partial class Program
var rootCommand = Commands.UI.GetCommand(); var rootCommand = Commands.UI.GetCommand();
Commands.CameraTuning.RegisterCommand(rootCommand); Commands.CameraTuning.RegisterCommand(rootCommand);
Commands.Capture.RegisterCommand(rootCommand); Commands.Capture.RegisterCommand(rootCommand);
Commands.ImageStorageClear.RegisterCommand(rootCommand);
// Обрабатываем аргументы // Обрабатываем аргументы
var parseResult = rootCommand?.Parse(_args); var parseResult = rootCommand?.Parse(_args);
@@ -1,11 +1,787 @@
using System.Collections.ObjectModel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GSS2.Core;
using GSS2.Core.Analysis.AmineContent;
using GSS2.Core.Analysis.AmineContent.Database.Results;
using GSS2.Core.Extensions;
using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.ViewModels;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
using LiveChartsCore.SkiaSharpView.Painting.Effects;
using GSS2.UI.Core.Services;
namespace GSS2.ViewModels.AmineContent; namespace GSS2.ViewModels.AmineContent;
public partial class AnalysisViewModel : ViewModelBase public partial class AnalysisViewModel : ViewModelBase
{ {
public AnalysisViewModel() public event EventHandler? UpdateCharts;
private readonly ILogger<AnalysisViewModel> _logger;
private readonly ResultsContext _context;
private readonly ImageCapturerService _imageCapturer;
private readonly ImageStorageService _imageStorage;
private readonly AnalyzerService _analyzer;
private readonly DependencyInjectionViewLocator _viewLocator;
private readonly NavigationService _navigation;
private CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] public partial bool MenuOpened { get; set; } = false;
[ObservableProperty] public partial ObservableCollection<ResultRecord> Records { get; set; }
[ObservableProperty] public partial ResultRecord? SelectedRecord { get; set; }
[ObservableProperty] public partial ResultRecord EditingRecord { get; set; }
[ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true;
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true;
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true;
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true;
[ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null;
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
[ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null;
[ObservableProperty] public partial AsyncImageRecordViewModel? MaskImage { get; set; } = null;
[ObservableProperty] public partial AsyncImageRecordViewModel? ResultImage { get; set; } = null;
[ObservableProperty] public partial bool ImageIsVisible { get; set; } = true;
[ObservableProperty] public partial bool MaskImageIsVisible { get; set; } = false;
[ObservableProperty] public partial bool ResultImageIsVisible { get; set; } = false;
[ObservableProperty] public partial float ImagesZoom { get; set; } = 1;
[ObservableProperty] public partial float ImagesPanX { get; set; } = 0;
[ObservableProperty] public partial float ImagesPanY { get; set; } = 0;
[ObservableProperty] public partial SolidColorPaint LegendTextPaint { get; set; } = new SolidColorPaint(SKColors.White);
[ObservableProperty] public partial ISeries[] MeanValuesHistogram { get; set; } = [];
[ObservableProperty] public partial Axis[] MeanValuesXAxis { get; set; } = [new Axis()];
[ObservableProperty] public partial Axis[] MeanValuesYAxis { get; set; } = [new Axis()];
[ObservableProperty] public partial ISeries[] VariancesHistogram { get; set; } = [];
[ObservableProperty] public partial Axis[] VariancesXAxis { get; set; } = [new Axis()];
[ObservableProperty] public partial Axis[] VariancesYAxis { get; set; } = [new Axis()];
public AnalysisViewModel(
ILogger<AnalysisViewModel> logger,
ResultsContext context,
ImageCapturerService imageCapturer,
ImageStorageService imageStorage,
AnalyzerService analyzer,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
)
{ {
_logger = logger;
_context = context;
_imageCapturer = imageCapturer;
_imageStorage = imageStorage;
_analyzer = analyzer;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация");
// Получаем записи из базы данных
Records = new ObservableCollection<ResultRecord>(_context.ResultRecords);
// Создаём пустые записи
SelectedRecord = null;
// На самом деле заполнять EditingRecord излишне, потому что при изменении SelectedRecord
// вызовется OnSelectedRecordChanged который сделает то же самое, но сделано это для избавления от предупреждений
EditingRecord = new()
{
SampleName = "",
SampleComment = "",
SamplingDateTime = DateTime.UtcNow,
SamplingPlace = "",
Image = "",
SeparatorType = "",
SampleBrand = "",
MaskImage = "",
ResultImage = "",
MinValue = -1,
MaxValue = -1,
ParticlesCount = -1,
MeanValue = -1,
MeanVariance = -1,
VarianceOfMean = -1,
MeanValues = [],
Variances = []
};
_logger.LogInformation("Инициализировано");
}
/// <summary>
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
/// </summary>
partial void OnSelectedRecordChanged(ResultRecord? value)
{
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
// то создаём пустую запись
if (SelectedRecord is null)
{
EditingRecord = new()
{
SampleName = "",
SampleComment = "",
SamplingDateTime = DateTime.UtcNow,
SamplingPlace = "",
Image = "",
SeparatorType = "",
SampleBrand = "",
MaskImage = "",
ResultImage = "",
MinValue = -1,
MaxValue = -1,
ParticlesCount = -1,
MeanValue = -1,
MeanVariance = -1,
VarianceOfMean = -1,
MeanValues = [],
Variances = []
};
}
else
{
// Иначе копируем запись, отвязывая её от базы данных
EditingRecord = CopyRecord(SelectedRecord);
}
}
/// <summary>
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
/// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
/// </summary>
partial void OnEditingRecordChanged(ResultRecord value)
{
var avgImagePath = _imageStorage.GetFullPath(value.Image, "result-avg");
if (File.Exists(avgImagePath))
{
if (Image is null)
Image = new AsyncImageRecordViewModel(avgImagePath);
else
Image.Path = avgImagePath;
}
else
Image = null;
var maskImagePath = _imageStorage.GetFullPath(value.MaskImage, "result-mask");
if (File.Exists(maskImagePath))
{
if (MaskImage is null)
MaskImage = new AsyncImageRecordViewModel(maskImagePath);
else
MaskImage.Path = maskImagePath;
}
else
MaskImage = null;
var resultImagePath = _imageStorage.GetFullPath(value.ResultImage, "result");
if (File.Exists(resultImagePath))
{
if (ResultImage is null)
ResultImage = new AsyncImageRecordViewModel(resultImagePath);
else
ResultImage.Path = resultImagePath;
}
else
ResultImage = null;
try
{
(MeanValuesHistogram, MeanValuesXAxis, MeanValuesYAxis) = BuildHistogram(
value.MeanValues,
0,
Math.Max(value.MeanValues.Count() > 0 ? value.MeanValues.Max() : 0, 1),
leftText: "Количество частиц",
bottomText: "Среднее знечение"
);
(VariancesHistogram, VariancesXAxis, VariancesYAxis) = BuildHistogram(
value.Variances,
0,
Math.Max(value.Variances.Count() > 0 ? value.Variances.Max() : 0, 1),
leftText: "Количество частиц",
bottomText: "Дисперсия"
);
}
catch (Exception ex)
{
_logger.LogError(ex, "");
}
} }
// Просто команды управления меню
[RelayCommand] private void OpenMenu() => MenuOpened = true;
[RelayCommand] private void CloseMenu() => MenuOpened = false;
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
// Команда создания новой записи
// вызывает цепочку действий по изменению EditingRecord и Images
[RelayCommand]
private void CreateRecord()
{
// Если уже создана новая запись, то SelectedRecord == null и
// при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null,
// поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged
if (SelectedRecord is not null)
SelectedRecord = null;
else
OnSelectedRecordChanged(null);
}
// Команда удаления записи, создаёт новую запись и удаляет старую из списка и базы данных
[RelayCommand]
private void DeleteRecord()
{
var confirmViewModel = new DeleteConfirmViewModel(_navigation)
{
Question = "Вы уверены, что хотиите удаить запись?"
};
if (SelectedRecord is null)
{
CreateRecord();
return;
}
confirmViewModel.Closes += (_, ea) =>
{
if (ea != true)
return;
// Сохраняем текущую запись
var deletedRecord = SelectedRecord;
// Создаём новую запись
CreateRecord();
// Если запись на была сохранена в базу данных, то ничего не делаем
if (deletedRecord is null)
return;
// Если запись была в без данных, то
// она была и в Records, удаляем её из Records
Records.Remove(deletedRecord);
// Удаляем запись из базы данных и сохраняем изменения
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске,
// тогда удалённые записи будут заполнять диск
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
_context.ResultRecords.Remove(deletedRecord);
_context.SaveChanges();
};
var convfirmView = _viewLocator.Build(confirmViewModel);
if (convfirmView is not null)
_navigation.NavigateTo(convfirmView);
}
// // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
[RelayCommand]
private void SaveRecord()
{
// Если это новая запись
if (SelectedRecord is null)
{
// Если это новая запись, то копируем её
var newRecord = CopyRecord(EditingRecord);
// Если записи нет в базе данных
if (_context.Entry(newRecord).State is EntityState.Detached)
{
// То добавляем её в базу
var attachedRecord = _context.ResultRecords.Add(newRecord);
// Заменяем текущую запись на привязанную к базе данных
newRecord = attachedRecord.Entity;
}
// Сохраняем изменения в базе данных
_context.SaveChanges();
// Добавляем запись в список
Records.Add(newRecord);
// Выбираем запись
SelectedRecord = newRecord;
}
// Если эта запись есть в базе
else
{
// Переносим данные из редактируемой записи в выбранную
SelectedRecord.SampleName = EditingRecord.SampleName;
SelectedRecord.SampleComment = EditingRecord.SampleComment;
SelectedRecord.SamplingDateTime = EditingRecord.SamplingDateTime;
SelectedRecord.SamplingPlace = EditingRecord.SamplingPlace;
SelectedRecord.Image = EditingRecord.Image;
SelectedRecord.SeparatorType = EditingRecord.SeparatorType;
SelectedRecord.SampleBrand = EditingRecord.SampleBrand;
SelectedRecord.MaskImage = EditingRecord.MaskImage;
SelectedRecord.ResultImage = EditingRecord.ResultImage;
SelectedRecord.MeanValue = EditingRecord.MeanValue;
SelectedRecord.MeanVariance = EditingRecord.MeanVariance;
SelectedRecord.VarianceOfMean = EditingRecord.VarianceOfMean;
// Если записи нет в базе данных
if (_context.Entry(SelectedRecord).State is EntityState.Detached)
{
// То добавляем её в базу
var attachedRecord = _context.ResultRecords.Add(SelectedRecord);
// Находим текущую запись в списке
var index = Records.IndexOf(SelectedRecord);
// Заменяем её в списке на привязанную к базе данных
Records[index] = attachedRecord.Entity;
// Заменяем выбранную запись на привязанную
SelectedRecord = attachedRecord.Entity;
}
// Сохраняем изменения в базе данных
_context.SaveChanges();
}
}
// Команда изменения основной секции (информация <-> изображения)
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
// Команда навигации по информации
[RelayCommand]
private void InfoSectionPrevious()
{
InfoSectionCurrentIndex--;
UpdateCharts?.Invoke(this, new EventArgs());
}
[RelayCommand]
private void InfoSectionNext()
{
InfoSectionCurrentIndex++;
UpdateCharts?.Invoke(this, new EventArgs());
}
// Команда съёмки изображений
[RelayCommand]
private void CaptureImage()
{
// Если съёмка уже начата, то ничего не делаем
if (CaptureImagesTask is not null)
return;
// Создаём токен отмены задачи
_captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Запускаем получение изображений асинхронно
CaptureImagesTask = Task.Run(async () =>
{
// Список полученных изображений
var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
// Получаем изображения
var captureTask = _imageCapturer.CaptureImages(async (data) =>
{
// Распаковываем данные
var (i, visible, uv365, uv254, image) = data;
// Выбрасываем исключение если задача отменена
cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
var imagesDataTask = Task.Run(() =>
{
// Сохраняем png изображение во временную папку
var path = Path.Join(Path.GetTempPath(), $"gss2-result-{visible}-{uv365}-{uv254}.png");
image.ImWrite(path);
// Освобождаем память изображения
image.Release();
// Добавляем данные изображения в список
return new ImageData(path, visible, uv365, uv254);
});
imagesData.Add(imagesDataTask);
}, cancellationToken);
// Обрабатываем изображения
var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
{
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
// Проверяем исключения задачи получения изображений
if (t.Exception is not null)
{
_logger.LogError(t.Exception, "Исключение во время получения изображений");
foreach (var ex in t.Exception.InnerExceptions ?? [])
_logger.LogError(ex, "Исключение во время получения изображений");
return;
}
// Проверяем исключения задач сохранения изображений
foreach (var imageData in imagesData)
if (imageData.Exception is not null)
{
_logger.LogError(imageData.Exception, "Исключение во время получения изображения");
foreach (var ex in imageData.Exception.InnerExceptions ?? [])
_logger.LogError(ex, "Исключение во время получения изображения");
return;
}
// Инициализируем анализатор
if (!_analyzer.Initialized)
{
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Инициализация моделей");
await _analyzer.Initialize();
}
// Инициализируем анализатор
if (!_analyzer.Initialized)
{
_logger.LogError("Ошибка при инициализации анализатора");
return;
}
// Обновляем данные в редактируемой записи
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Анализ");
// Проводим анализ
var result = await _analyzer.Analyze(imagesData.Select(i => i.Result));
// Если анализ проведён
if (result is not null)
{
_logger.LogInformation("Анализ завершён");
// Распаковываем результат
var (avgImage, _, mask, contours, resultImage, separatorType, sampleBrand) = result.Value;
try
{
_logger.LogInformation("Анализ завершён [2]");
// Вычисляем минимальное и максимальное значение
OpenCvSharp.Cv2.MinMaxLoc(resultImage, out double minValue, out double maxValue, out _, out _, mask);
// Вычисляем среднее значение
var meanValue = OpenCvSharp.Cv2.Mean(resultImage, mask).Val0;
_logger.LogInformation("Миниальное значение: {}, Максимальное значние: {}", minValue, maxValue);
_logger.LogInformation("Колличество конутров: {}, Среднее значние: {}", contours.Count(), meanValue);
_logger.LogInformation("Анализ завершён [3]");
// Вычисляем среднее и дисперсию каждого контура
var means = new List<double>();
var variances = new List<double>();
// Создаём временную маску для каждого контура
var contourMask = new OpenCvSharp.Mat(
new OpenCvSharp.Size(
AnalyzerService.ANALYSIS_IMAGE_SIZE,
AnalyzerService.ANALYSIS_IMAGE_SIZE
),
OpenCvSharp.MatType.CV_8UC1,
new OpenCvSharp.Scalar(0)
);
// Проходим по каждому контуру
foreach (var contour in contours)
{
// Обнуляем маску
contourMask.SetTo(new OpenCvSharp.Scalar(0));
// Заполняем контур на маске
OpenCvSharp.Cv2.DrawContours(contourMask, [contour], -1, new OpenCvSharp.Scalar(255), -1);
// Вычисляем среднее и среднеквадратическое отклонение
OpenCvSharp.Cv2.MeanStdDev(resultImage, out var mean, out var stdDev, contourMask);
// Добавляем значения в лист
means.Add(mean.Val0);
variances.Add(Math.Pow(stdDev.Val0, 2));
}
contourMask.Release();
_logger.LogInformation("Анализ завершён [4]");
// Вычисляем среднюю дисперсию и дисперсию среднего
double meanVariance = variances.Average();
double varianceOfMean = means.Variance();
_logger.LogInformation("Средняя дивергенция: {}, Дивергенция среднего: {}", meanVariance, varianceOfMean);
_logger.LogInformation("Анализ завершён [5]");
// Кодируем и сохраняем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
string avgPath;
using (var stream = new MemoryStream(avgData))
avgPath = await _imageStorage.SaveAsync(stream, ".png", "result-avg");
// Освобождаем память среднего изображения
avgImage.Release();
avgData = null;
_logger.LogInformation("Анализ завершён [6]");
// Кодируем и сохраняем маску
OpenCvSharp.Cv2.ImEncode(".png", mask, out var maskData);
string maskPath;
using (var stream = new MemoryStream(maskData))
maskPath = await _imageStorage.SaveAsync(stream, ".png", "result-mask");
// Освобождаем память маски
mask.Release();
maskData = null;
_logger.LogInformation("Анализ завершён [7]");
// Преобразуем изображение (minValue...maxValue) -> (0...255)
resultImage.ConvertTo(resultImage, OpenCvSharp.MatType.CV_8UC1, 255 / (maxValue - minValue), 255 * minValue / (maxValue - minValue));
// Кодируем и сохраняем изображение результата
OpenCvSharp.Cv2.ImEncode(".png", resultImage, out var resultData);
string resultPath;
using (var stream = new MemoryStream(resultData))
resultPath = await _imageStorage.SaveAsync(stream, ".png", "result");
// Освобождаем память изображения результата
resultImage.Release();
resultData = null;
_logger.LogInformation("Анализ завершён [8]");
// Заполняем поля в редактируемой записи
EditingRecord.Image = avgPath;
EditingRecord.SeparatorType = separatorType;
EditingRecord.SampleBrand = sampleBrand;
EditingRecord.MaskImage = maskPath;
EditingRecord.ResultImage = resultPath;
EditingRecord.MinValue = minValue;
EditingRecord.MaxValue = maxValue;
EditingRecord.ParticlesCount = contours.Count();
EditingRecord.MeanValue = meanValue;
EditingRecord.MeanVariance = meanVariance;
EditingRecord.VarianceOfMean = varianceOfMean;
EditingRecord.MeanValues = means.ToArray();
EditingRecord.Variances = variances.ToArray();
// Обновляем данные в редактируемой записи
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => EditingRecord = CopyRecord(EditingRecord));
}
catch (Exception ex)
{
_logger.LogError(ex, "Исключение во время анализа");
}
}
else
{
_logger.LogError("Ошибка при выполнении анализа");
}
}, cancellationToken);
// Сбрасываем состояние задачи съёмки изображений
_ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t =>
{
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() =>
{
ImagesIsCapturing = false;
ImagesCapturingProgress = "";
CaptureImagesTask = null;
});
});
});
}
// Команда отмены задачи съёмки изображений
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
// Команды переключения изображений
[RelayCommand]
private void SwitchToImage()
{
ImageIsVisible = true;
MaskImageIsVisible = false;
ResultImageIsVisible = false;
}
[RelayCommand]
private void SwitchToMaskImage()
{
ImageIsVisible = false;
MaskImageIsVisible = true;
ResultImageIsVisible = false;
}
[RelayCommand]
private void SwitchToResultImage()
{
ImageIsVisible = false;
MaskImageIsVisible = false;
ResultImageIsVisible = true;
}
/// <summary>
/// Копирует <see cref="ResultRecord"/>, отвязывая её от базы данных
/// </summary>
/// <returns>Скопированная <see cref="ResultRecord"/></returns>
private static ResultRecord CopyRecord(ResultRecord source)
{
return new ResultRecord
{
Id = source.Id,
SampleName = source.SampleName,
SampleComment = source.SampleComment,
SamplingDateTime = source.SamplingDateTime,
SamplingPlace = source.SamplingPlace,
Image = source.Image,
SeparatorType = source.SeparatorType,
SampleBrand = source.SampleBrand,
MaskImage = source.MaskImage,
ResultImage = source.ResultImage,
MinValue = source.MinValue,
MaxValue = source.MaxValue,
ParticlesCount = source.ParticlesCount,
MeanValue = source.MeanValue,
MeanVariance = source.MeanVariance,
VarianceOfMean = source.VarianceOfMean,
MeanValues = source.MeanValues,
Variances = source.Variances
};
}
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) EmptyHistogram()
{
return (Array.Empty<ISeries>(), [new Axis()], [new Axis()]);
}
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable<float> values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "")
{
if (values == null || values.Count() == 0)
return EmptyHistogram();
var vals = values.Select(v => (double)v).ToArray();
double minVal = min ?? vals.Min();
double maxVal = max ?? vals.Max();
int binCount = 20;
double binSize = (maxVal - minVal) / binCount;
var binsPDF = new int[binCount];
var binsCDF = new float[binCount];
foreach (var v in vals)
{
int index = (int)((v - minVal) / binSize);
if (index >= binCount)
index = binCount - 1;
if (index < 0)
index = 0;
binsPDF[index]++;
for (int i = index; i < binCount; i++)
binsCDF[i] += 100.0f / values.Count();
}
// Подписи для оси X
var labels = new string[binCount];
for (int i = 0; i < binCount; i++)
{
double from = minVal + i * binSize;
double to = from + binSize;
labels[i] = $"{from:F2}";
}
return
(
new ISeries[]
{
new ColumnSeries<int>
{
Values = binsPDF,
Name = barLegend,
Fill = new SolidColorPaint(SKColors.White),
ScalesYAt = 0
},
new LineSeries<float>
{
Values = binsCDF,
Name = lineLegend,
Fill = new SolidColorPaint(SKColors.Transparent),
Stroke = new SolidColorPaint(SKColors.Red),
GeometryFill = new SolidColorPaint(SKColors.Red),
GeometryStroke = new SolidColorPaint(SKColors.Red),
GeometrySize = 8,
ScalesYAt = 1
}
},
new Axis[]
{
new Axis
{
Labels = labels,
LabelsRotation = 45,
TextSize = 14,
Name = bottomText,
NameTextSize = 14,
NamePaint=new SolidColorPaint(SKColors.White),
SeparatorsPaint = new SolidColorPaint(SKColors.White),
LabelsPaint = new SolidColorPaint(SKColors.White),
TicksPaint = new SolidColorPaint(SKColors.White),
SubticksPaint = new SolidColorPaint(SKColors.White)
}
},
new Axis[]
{
new Axis
{
TextSize = 14,
MinLimit = 0,
Name=leftText,
NameTextSize = 14,
NamePaint=new SolidColorPaint(SKColors.White),
Position = LiveChartsCore.Measure.AxisPosition.Start,
SeparatorsPaint = new SolidColorPaint(SKColors.White),
LabelsPaint = new SolidColorPaint(SKColors.White),
TicksPaint = new SolidColorPaint(SKColors.White),
SubticksPaint = new SolidColorPaint(SKColors.White)
},
new Axis
{
TextSize = 14,
MinLimit = 0f,
MaxLimit = 100f,
Name=rightText,
NameTextSize = 14,
NamePaint=new SolidColorPaint(SKColors.White),
Position = LiveChartsCore.Measure.AxisPosition.End,
SeparatorsPaint = new SolidColorPaint(SKColors.White)
{
PathEffect = new DashEffect([4, 4])
},
LabelsPaint = new SolidColorPaint(SKColors.White),
TicksPaint = new SolidColorPaint(SKColors.White),
SubticksPaint = new SolidColorPaint(SKColors.White)
}
}
);
}
private static (ISeries[] Series, Axis[] XAxes, Axis[] YAxes) BuildHistogram(IEnumerable<double> values, double? min = null, double? max = null, string leftText = "", string rightText = "", string bottomText = "", string barLegend = "", string lineLegend = "") =>
BuildHistogram(values.Select(v => (float)v), min, max, leftText, rightText, bottomText, barLegend, lineLegend);
} }
@@ -1,11 +0,0 @@
using GSS2.UI.Core.ViewModels;
namespace GSS2.ViewModels.AmineContent;
public partial class ResultsViewModel : ViewModelBase
{
public ResultsViewModel()
{
}
}
@@ -8,28 +8,30 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using GSS2.Core;
using GSS2.Core.Analysis.AmineContent; using GSS2.Core.Analysis.AmineContent;
using GSS2.Core.Analysis.AmineContent.Database.Calibration; using GSS2.Core.Analysis.AmineContent.Database.Calibration;
using GSS2.Core.Hardware;
using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.ViewModels;
using System.Diagnostics; using GSS2.UI.Core.Services;
namespace GSS2.ViewModels.AmineContent; namespace GSS2.ViewModels.AmineContent;
public partial class SampleCalibrationViewModel : ViewModelBase public partial class SampleCalibrationViewModel : ViewModelBase
{ {
private readonly ILogger<SampleCalibrationViewModel> _logger; private readonly ILogger<SampleCalibrationViewModel> _logger;
private readonly AmineContentCalibrationContext _context; private readonly CalibrationContext _context;
private readonly AmineContentImageCapturerService _imageCapturer; private readonly ImageCapturerService _imageCapturer;
private readonly ImageStorageService _imageStorage; private readonly ImageStorageService _imageStorage;
private readonly DependencyInjectionViewLocator _viewLocator;
private readonly NavigationService _navigation;
private CancellationTokenSource? _captureImagesTaskCts = null; private CancellationTokenSource? _captureImagesTaskCts = null;
[ObservableProperty] public partial bool MenuOpened { get; set; } = false; [ObservableProperty] public partial bool MenuOpened { get; set; } = false;
[ObservableProperty] public partial ObservableCollection<CalibrationRecord> Records { get; set; } [ObservableProperty] public partial ObservableCollection<SampleRecord> Records { get; set; }
[ObservableProperty] public partial CalibrationRecord? SelectedRecord { get; set; } [ObservableProperty] public partial SampleRecord? SelectedRecord { get; set; }
[ObservableProperty] public partial CalibrationRecord EditingRecord { get; set; } [ObservableProperty] public partial SampleRecord EditingRecord { get; set; }
[ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0; [ObservableProperty] public partial int MainSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
@@ -43,45 +45,31 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null; [ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null;
[ObservableProperty] public partial int ImagesSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool ImagesSectionCanScrollForward { get; set; } = false;
[ObservableProperty] public partial bool ImagesSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool ImagesSectionIsScrolling { get; set; } = true;
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> SeparatorImages { get; set; } = new(); [ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null;
[ObservableProperty] public partial int SeparatorImagesSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool SeparatorImagesSectionCanScrollForward { get; set; } = false;
[ObservableProperty] public partial bool SeparatorImagesSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool SeparatorImagesSectionIsScrolling { get; set; } = true;
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> SampleImages { get; set; } = new();
[ObservableProperty] public partial int SampleImagesSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool SampleImagesSectionCanScrollForward { get; set; } = false;
[ObservableProperty] public partial bool SampleImagesSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool SampleImagesSectionIsScrolling { get; set; } = true;
public SampleCalibrationViewModel( public SampleCalibrationViewModel(
ILogger<SampleCalibrationViewModel> logger, ILogger<SampleCalibrationViewModel> logger,
AmineContentCalibrationContext context, CalibrationContext context,
AmineContentImageCapturerService imageCapturer, ImageCapturerService imageCapturer,
ImageStorageService imageStorage ImageStorageService imageStorage,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
) )
{ {
_logger = logger; _logger = logger;
_context = context; _context = context;
_imageCapturer = imageCapturer; _imageCapturer = imageCapturer;
_imageStorage = imageStorage; _imageStorage = imageStorage;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация"); _logger.LogInformation("Инициализация");
// Получаем записи из базы данных // Получаем записи из базы данных
Records = new ObservableCollection<CalibrationRecord>( Records = new ObservableCollection<SampleRecord>(_context.SampleRecords);
_context.CalibrationRecords
.Include(r => r.SampleImages)
.Include(r => r.SeparatorImages)
);
// Берём первую запись // Берём первую запись
SelectedRecord = Records.FirstOrDefault(); SelectedRecord = Records.FirstOrDefault();
@@ -95,15 +83,15 @@ public partial class SampleCalibrationViewModel : ViewModelBase
SampleComment = "", SampleComment = "",
SampleBrand = "", SampleBrand = "",
SampleMass = -1.0, SampleMass = -1.0,
SamplingDateTime = DateTime.Now, SamplingDateTime = DateTime.UtcNow,
SamplingPlace = "", SamplingPlace = "",
MixtureName = "", MixtureName = "",
MixtureAmineContent = -1.0, MixtureAmineContent = -1.0,
MixtureNormalRate = -1.0, MixtureNormalRate = -1.0,
MixtureActualRate = -1.0, MixtureActualRate = -1.0,
MeasuredContent = -1.0, MeasuredContent = -1.0,
SampleImages = new(), Image = "",
SeparatorImages = new() VectorRecords = new()
}; };
else else
EditingRecord = CopyRecord(SelectedRecord); EditingRecord = CopyRecord(SelectedRecord);
@@ -115,11 +103,10 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// Вызывается при изменении <see cref="SelectedRecord"/>.<br/> /// Вызывается при изменении <see cref="SelectedRecord"/>.<br/>
/// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>. /// Копирует <see cref="SelectedRecord"/> в <see cref="EditingRecord"/> или издаёт пустую <see cref="EditingRecord"/>.
/// </summary> /// </summary>
partial void OnSelectedRecordChanged(CalibrationRecord? value) partial void OnSelectedRecordChanged(SampleRecord? value)
{ {
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись) // Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
// то создаём пустую запись // то создаём пустую запись
// TODO: Подумать над тем, чтобы сразу удалять несохранённые изображения из хранилища
if (SelectedRecord is null) if (SelectedRecord is null)
{ {
EditingRecord = new() EditingRecord = new()
@@ -128,15 +115,15 @@ public partial class SampleCalibrationViewModel : ViewModelBase
SampleComment = "", SampleComment = "",
SampleBrand = "", SampleBrand = "",
SampleMass = -1.0, SampleMass = -1.0,
SamplingDateTime = DateTime.Now, SamplingDateTime = DateTime.UtcNow,
SamplingPlace = "", SamplingPlace = "",
MixtureName = "", MixtureName = "",
MixtureAmineContent = -1.0, MixtureAmineContent = -1.0,
MixtureNormalRate = -1.0, MixtureNormalRate = -1.0,
MixtureActualRate = -1.0, MixtureActualRate = -1.0,
MeasuredContent = -1.0, MeasuredContent = -1.0,
SampleImages = new(), Image = "",
SeparatorImages = new() VectorRecords = new()
}; };
} }
else else
@@ -148,63 +135,21 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// <summary> /// <summary>
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/> /// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
/// Заполняет <see cref="Images"/> изображениями из <see cref="EditingRecord"/>. /// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
/// </summary> /// </summary>
partial void OnEditingRecordChanged(CalibrationRecord value) partial void OnEditingRecordChanged(SampleRecord value)
{ {
// Обновляем изображения сепаратора var imagePath = _imageStorage.GetFullPath(value.Image, "calibration-sample");
if (!File.Exists(imagePath))
{ {
var images = value.SeparatorImages Image = null;
// Для каждого изображения return;
.Select(i =>
{
// Находим путь через ImageStorageService
string? path;
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
if (ImagesSectionCurrentIndex == 0)
path = _imageStorage.GetFullPath(i.ImagePath, "separator");
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
else if (ImagesSectionCurrentIndex == 1)
path = _imageStorage.GetFullPath(i.ImagePath, "sample");
else
throw new UnreachableException();
// Если изображения не найдено, возвращаем null
if (path is null)
return null;
// Создаём AsyncImageRecordViewModel
return new AsyncImageRecordViewModel(path, i);
})
// Оставляем только не null значения
.OfType<AsyncImageRecordViewModel>();
// Создаём новую коллекцию изображений
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
SeparatorImages.Clear();
SeparatorImages = new(images);
} }
// Обновляем изображения пробы
{
var images = value.SampleImages
// Для каждого изображения
.Select(i =>
{
// Находим путь через ImageStorageService
var path = _imageStorage.GetFullPath(i.ImagePath, "sample");
// Если изображения не найдено, возвращаем null
if (path is null)
return null;
// Создаём AsyncImageRecordViewModel
return new AsyncImageRecordViewModel(path, i);
})
// Оставляем только не null значения
.OfType<AsyncImageRecordViewModel>();
// Создаём новую коллекцию изображений if (Image is null)
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой Image = new AsyncImageRecordViewModel(imagePath);
SampleImages.Clear(); else
SampleImages = new(images); Image.Path = imagePath;
}
} }
// Просто команды управления меню // Просто команды управления меню
@@ -217,7 +162,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void CreateRecord() private void CreateRecord()
{ {
// Из-за того что, если создана новая запись, то SelectedRecord == null // Если уже создана новая запись, то SelectedRecord == null и
// при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется, // при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null, // потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null,
// поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged // поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged
@@ -231,28 +176,47 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void DeleteRecord() private void DeleteRecord()
{ {
// Сохраняем текущую запись var confirmViewModel = new DeleteConfirmViewModel(_navigation)
var deletedRecord = SelectedRecord; {
// Создаём новую запись Question = "Вы уверены, что хотиите удаить запись?"
CreateRecord(); };
if (SelectedRecord is null)
// Если запись на была сохранена в базу данных, то ничего не делаем {
if (deletedRecord is null) CreateRecord();
return; return;
}
confirmViewModel.Closes += (_, ea) =>
{
if (ea != true)
return;
// Если запись была в без данных, то // Сохраняем текущую запись
// она была и в Records, удаляем её из Records var deletedRecord = SelectedRecord;
Records.Remove(deletedRecord); // Создаём новую запись
// Удаляем запись из базы данных и сохраняем изменения CreateRecord();
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске, // Если запись на была сохранена в базу данных, то ничего не делаем
// тогда удалённые записи будут заполнять диск if (deletedRecord is null)
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища return;
_context.CalibrationRecords.Remove(deletedRecord);
_context.SaveChanges(); // Если запись была в без данных, то
// она была и в Records, удаляем её из Records
Records.Remove(deletedRecord);
// Удаляем запись из базы данных и сохраняем изменения
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске,
// тогда удалённые записи будут заполнять диск
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
_context.SampleRecords.Remove(deletedRecord);
_context.SaveChanges();
};
var convfirmView = _viewLocator.Build(confirmViewModel);
if (convfirmView is not null)
_navigation.NavigateTo(convfirmView);
} }
// Команда сохранения записи, сохраняет новую запись или изменения в отредактированной // // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
[RelayCommand] [RelayCommand]
private void SaveRecord() private void SaveRecord()
{ {
@@ -262,41 +226,26 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если это новая запись, то копируем её // Если это новая запись, то копируем её
var newRecord = CopyRecord(EditingRecord); var newRecord = CopyRecord(EditingRecord);
// Для каждого изображения сепаратора в записи // Для каждого вектора
for (int i = 0; i < newRecord.SeparatorImages.Count(); i++) for (int i = 0; i < newRecord.VectorRecords.Count(); i++)
{ {
var image = newRecord.SeparatorImages[i]; var vector = newRecord.VectorRecords[i];
// Если изображение есть в базе данных, то пропускаем его // Если вектор есть в базе данных, то пропускаем его
if (_context.Entry(image).State is not EntityState.Detached) if (_context.Entry(vector).State is not EntityState.Detached)
continue; continue;
// Если изображения нет в базе данных, то добавляем его в базу // Если вектора нет в базе данных, то добавляем его в базу
var attachedImage = _context.ImageRecords.Add(image); var attachedRecord = _context.VectorRecords.Add(vector);
// Удаляем отвязанное от базы данных изображение из записи // Удаляем отвязанное от базы данных изображение из записи
newRecord.SeparatorImages.RemoveAt(i); newRecord.VectorRecords.RemoveAt(i);
// Добавляем привязанное изображение к записи // Добавляем привязанное изображение к записи
newRecord.SeparatorImages.Insert(i, attachedImage.Entity); newRecord.VectorRecords.Insert(i, attachedRecord.Entity);
}
// Для каждого изображения пробы в записи
for (int i = 0; i < newRecord.SampleImages.Count(); i++)
{
var image = newRecord.SampleImages[i];
// Если изображение есть в базе данных, то пропускаем его
if (_context.Entry(image).State is not EntityState.Detached)
continue;
// Если изображения нет в базе данных, то добавляем его в базу
var attachedImage = _context.ImageRecords.Add(image);
// Удаляем отвязанное от базы данных изображение из записи
newRecord.SampleImages.RemoveAt(i);
// Добавляем привязанное изображение к записи
newRecord.SampleImages.Insert(i, attachedImage.Entity);
} }
// Если записи нет в базе данных // Если записи нет в базе данных
if (_context.Entry(newRecord).State is EntityState.Detached) if (_context.Entry(newRecord).State is EntityState.Detached)
{ {
// То добавляем её в базу // То добавляем её в базу
var attachedRecord = _context.CalibrationRecords.Add(newRecord); var attachedRecord = _context.SampleRecords.Add(newRecord);
// Заменяем текущую запись на привязанную к базе данных // Заменяем текущую запись на привязанную к базе данных
newRecord = attachedRecord.Entity; newRecord = attachedRecord.Entity;
} }
@@ -323,111 +272,51 @@ public partial class SampleCalibrationViewModel : ViewModelBase
SelectedRecord.MixtureNormalRate = EditingRecord.MixtureNormalRate; SelectedRecord.MixtureNormalRate = EditingRecord.MixtureNormalRate;
SelectedRecord.MixtureActualRate = EditingRecord.MixtureActualRate; SelectedRecord.MixtureActualRate = EditingRecord.MixtureActualRate;
SelectedRecord.MeasuredContent = EditingRecord.MeasuredContent; SelectedRecord.MeasuredContent = EditingRecord.MeasuredContent;
SelectedRecord.Image = EditingRecord.Image;
// Удаляем старые изображения сепаратора в записи // Удаляем старые векторы в записи
{ {
// Находим изображения которые нужно удалить - // Находим векторы которые нужно удалить -
// те которые есть в выбранной записи, но нет в редактируемой // те которые есть в выбранной записи, но нет в редактируемой
var imagesToRemove = SelectedRecord.SeparatorImages var vectorsToRemove = SelectedRecord.VectorRecords
.Where(e => EditingRecord.SeparatorImages.All(i => i.Id != e.Id)) .Where(e => EditingRecord.VectorRecords.All(i => i.Id != e.Id))
.ToList(); .ToList();
// Удаляем старые изображения // Удаляем старые векторы
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища foreach (var image in vectorsToRemove)
foreach (var image in imagesToRemove) SelectedRecord.VectorRecords.Remove(image);
SelectedRecord.SeparatorImages.Remove(image);
// Для каждого изображения в редактируемой записи // Для каждого вектора в редактируемой записи
foreach (var editedImage in EditingRecord.SeparatorImages) foreach (var editedImage in EditingRecord.VectorRecords)
{ {
// Находим изображение в выбранной записи (оно будет привязано к базе данных) // Находим изображение в выбранной записи (оно будет привязано к базе данных)
var existing = SelectedRecord.SeparatorImages var existing = SelectedRecord.VectorRecords
.FirstOrDefault(i => i.Id == editedImage.Id); .FirstOrDefault(i => i.Id == editedImage.Id);
// Если изображение не найдено // Если вектор не найден
// Копируем вектор в выбранную запись
if (existing is null) if (existing is null)
{ SelectedRecord.VectorRecords.Add(CopyRecord(editedImage));
// Копируем изображение в выбранную запись // Если вектор найден
SelectedRecord.SeparatorImages.Add(CopyRecord(editedImage)); // Изменяем данные вектора на данные вектора из редактируемой записи
}
// Если изображение найдено
else else
{ existing.Values = editedImage.Values;
// Изменяем данные изображения на данные изображения из редактируемой записи
existing.ImagePath = editedImage.ImagePath;
existing.VisibleIntensity = editedImage.VisibleIntensity;
existing.Uv365Intensity = editedImage.Uv365Intensity;
existing.Uv254Intensity = editedImage.Uv254Intensity;
}
} }
// Для каждого изображения в выбранной записи // Для каждого вектора в выбранной записи
for (int i = 0; i < SelectedRecord.SeparatorImages.Count(); i++) for (int i = 0; i < SelectedRecord.VectorRecords.Count(); i++)
{ {
var image = SelectedRecord.SeparatorImages[i]; var vector = SelectedRecord.VectorRecords[i];
// Если изображение есть в базе данных, то пропускаем его // Если вектор есть в базе данных, то пропускаем его
if (_context.Entry(image).State is not EntityState.Detached) if (_context.Entry(vector).State is not EntityState.Detached)
continue; continue;
// Если изображения нет в базе данных, то добавляем его в базу // Если изображения нет в базе данных, то добавляем его в базу
var attachedImage = _context.ImageRecords.Add(image); var attachedRecord = _context.VectorRecords.Add(vector);
// Удаляем отвязанное от базы данных изображение из записи // Удаляем отвязанное от базы данных изображение из записи
SelectedRecord.SeparatorImages.RemoveAt(i); SelectedRecord.VectorRecords.RemoveAt(i);
// Добавляем привязанное изображение к записи // Добавляем привязанное изображение к записи
SelectedRecord.SeparatorImages.Insert(i, attachedImage.Entity); SelectedRecord.VectorRecords.Insert(i, attachedRecord.Entity);
}
}
// Удаляем старые изображения пробы в записи
{
// Находим изображения которые нужно удалить -
// те которые есть в выбранной записи, но нет в редактируемой
var imagesToRemove = SelectedRecord.SampleImages
.Where(e => EditingRecord.SampleImages.All(i => i.Id != e.Id))
.ToList();
// Удаляем старые изображения
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
foreach (var image in imagesToRemove)
SelectedRecord.SampleImages.Remove(image);
// Для каждого изображения в редактируемой записи
foreach (var editedImage in EditingRecord.SampleImages)
{
// Находим изображение в выбранной записи (оно будет привязано к базе данных)
var existing = SelectedRecord.SampleImages
.FirstOrDefault(i => i.Id == editedImage.Id);
// Если изображение не найдено
if (existing is null)
{
// Копируем изображение в выбранную запись
SelectedRecord.SampleImages.Add(CopyRecord(editedImage));
}
// Если изображение найдено
else
{
// Изменяем данные изображения на данные изображения из редактируемой записи
existing.ImagePath = editedImage.ImagePath;
existing.VisibleIntensity = editedImage.VisibleIntensity;
existing.Uv365Intensity = editedImage.Uv365Intensity;
existing.Uv254Intensity = editedImage.Uv254Intensity;
}
}
// Для каждого изображения в выбранной записи
for (int i = 0; i < SelectedRecord.SampleImages.Count(); i++)
{
var image = SelectedRecord.SampleImages[i];
// Если изображение есть в базе данных, то пропускаем его
if (_context.Entry(image).State is not EntityState.Detached)
continue;
// Если изображения нет в базе данных, то добавляем его в базу
var attachedImage = _context.ImageRecords.Add(image);
// Удаляем отвязанное от базы данных изображение из записи
SelectedRecord.SampleImages.RemoveAt(i);
// Добавляем привязанное изображение к записи
SelectedRecord.SampleImages.Insert(i, attachedImage.Entity);
} }
} }
@@ -435,7 +324,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
if (_context.Entry(SelectedRecord).State is EntityState.Detached) if (_context.Entry(SelectedRecord).State is EntityState.Detached)
{ {
// То добавляем её в базу // То добавляем её в базу
var attachedRecord = _context.CalibrationRecords.Add(SelectedRecord); var attachedRecord = _context.SampleRecords.Add(SelectedRecord);
// Находим текущую запись в списке // Находим текущую запись в списке
var index = Records.IndexOf(SelectedRecord); var index = Records.IndexOf(SelectedRecord);
// Заменяем её в списке на привязанную к базе данных // Заменяем её в списке на привязанную к базе данных
@@ -456,17 +345,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--; [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++; [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
// Команда навигации по информации
[RelayCommand] private void ImagesSectionSwitch() => ImagesSectionCurrentIndex = ImagesSectionCurrentIndex == 0 ? 1 : 0;
// Команда навигации по изображениям сепаратора
[RelayCommand] private void SeparatorImagesSectionPrevious() => SeparatorImagesSectionCurrentIndex--;
[RelayCommand] private void SeparatorImagesSectionNext() => SeparatorImagesSectionCurrentIndex++;
// Команда навигации по изображениям пробы
[RelayCommand] private void SampleImagesSectionPrevious() => SampleImagesSectionCurrentIndex--;
[RelayCommand] private void SampleImagesSectionNext() => SampleImagesSectionCurrentIndex++;
// Команда съёмки изображений // Команда съёмки изображений
[RelayCommand] [RelayCommand]
private void CaptureImage() private void CaptureImage()
@@ -479,19 +357,24 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_captureImagesTaskCts = new CancellationTokenSource(); _captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token; var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Запускаем получение изображений асинхронно // Запускаем получение изображений асинхронно
CaptureImagesTask = Task.Run(async () => CaptureImagesTask = Task.Run(async () =>
{ {
// Список полученных изображений // Список полученных изображений
var imageRecords = new List<ImageRecord>(); var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
// Получаем изображения // Получаем изображения
var task = _imageCapturer.CaptureImages(async (data) => var captureTask = _imageCapturer.CaptureImages(async (data) =>
{ {
// Распаковываем данные // Распаковываем данные
var (i, visible, uv365, uv254, image) = data; var (i, visible, uv365, uv254, image) = data;
@@ -499,104 +382,94 @@ public partial class SampleCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {AmineContentImageCapturerService.IlluminatorIntensities.Count()}"); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
_ = Task.Run(async () => var imagesDataTask = Task.Run(() =>
{ {
// Кодируем изображение в png // Сохраняем png изображение во временную папку
var bytes = image.ImEncode(".png"); var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png");
// Сохраняем изображение в ImageStorageService image.ImWrite(path);
string fileName;
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора
if (ImagesSectionCurrentIndex == 0)
using (var stream = new MemoryStream(bytes))
fileName = await _imageStorage.SaveAsync(stream, ".png", "separator", cancellationToken);
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
else if (ImagesSectionCurrentIndex == 1)
using (var stream = new MemoryStream(bytes))
fileName = await _imageStorage.SaveAsync(stream, ".png", "sample", cancellationToken);
else
throw new UnreachableException();
// Освобождаем память изображения // Освобождаем память изображения
image.Release(); image.Release();
image = null;
bytes = null;
// Добавляем изображение в список полученных // Добавляем данные изображения в список
imageRecords.Add( return new ImageData(path, visible, uv365, uv254);
new ImageRecord()
{
ImagePath = fileName,
VisibleIntensity = visible,
Uv365Intensity = uv365,
Uv254Intensity = uv254,
}
);
}); });
imagesData.Add(imagesDataTask);
}, cancellationToken); }, cancellationToken);
_ = task.ContinueWith(t => // Обрабатываем изображения
var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
{ {
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
// Проверяем исключения задачи получения изображений
if (t.Exception is not null) if (t.Exception is not null)
{ {
_logger.LogError(t.Exception, "Исключение во время получения изображений"); _logger.LogError(t.Exception, "Исключение во время получения изображений");
foreach (var ex in t.Exception.InnerExceptions ?? []) foreach (var ex in t.Exception.InnerExceptions ?? [])
_logger.LogError(ex, "Исключение во время получения изображений"); _logger.LogError(ex, "Исключение во время получения изображений");
return;
} }
var images = imageRecords // Проверяем исключения задач сохранения изображений
// Для каждого изображения foreach (var imageData in imagesData)
.Select(i => if (imageData.Exception is not null)
{ {
// Находим путь через ImageStorageService _logger.LogError(imageData.Exception, "Исключение во время получения изображения");
string? path; foreach (var ex in imageData.Exception.InnerExceptions ?? [])
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора _logger.LogError(ex, "Исключение во время получения изображения");
if (ImagesSectionCurrentIndex == 0) return;
path = _imageStorage.GetFullPath(i.ImagePath, "separator"); }
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы
else if (ImagesSectionCurrentIndex == 1)
path = _imageStorage.GetFullPath(i.ImagePath, "sample");
else
throw new UnreachableException();
// Если изображения не найдено, возвращаем null
if (path is null)
return null;
// Создаём AsyncImageRecordViewModel // Вычисляем вектор признаков
return new AsyncImageRecordViewModel(path, i); var result = AnalyzerService.GetFeatureVectors(imagesData.Select(i => i.Result));
}) // Если вектор признаков вычислен
// Оставляем только не null значения if (result is not null)
.OfType<AsyncImageRecordViewModel>();
// Обновляем данные в редактируемой коллекции
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() =>
{ {
// Если ImagesSectionCurrentIndex == 0, то выбрана секция с изображениями сепаратора // Распаковываем результат
if (ImagesSectionCurrentIndex == 0) var (avgImage, roi, features) = result.Value;
{ // Обрабатываем среднее изображение
EditingRecord.SeparatorImages = imageRecords; avgImage = new OpenCvSharp.Mat(avgImage, roi);
// Создаём новую коллекцию изображений OpenCvSharp.Cv2.Resize(
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой avgImage,
SeparatorImages = new(images); avgImage,
} new OpenCvSharp.Size(
// Если ImagesSectionCurrentIndex == 1, то выбрана секция с изображениями пробы AnalyzerService.ANALYSIS_IMAGE_SIZE,
else if (ImagesSectionCurrentIndex == 1) AnalyzerService.ANALYSIS_IMAGE_SIZE
{ )
EditingRecord.SampleImages = imageRecords; );
// Создаём новую коллекцию изображений
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой
SampleImages = new(images);
}
else
throw new UnreachableException();
});
// Сбрасываем состояние задачи съёмки изображений // Кодируем и сохраняем среднее изображение
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
string avgPath;
using (var stream = new MemoryStream(avgData))
avgPath = await _imageStorage.SaveAsync(stream, ".png", "calibration-sample");
// Заполняем поля в редактируемой записи
EditingRecord.Image = avgPath;
EditingRecord.VectorRecords = features.Select(f => new VectorRecord { Values = f }).ToList();
// Обновляем данные в редактируемой записи
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => OnEditingRecordChanged(EditingRecord));
}
else
{
_logger.LogError("Ошибка при вычислении векторов признаков");
}
}, cancellationToken);
// Сбрасываем состояние задачи съёмки изображений
_ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t =>
{
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => _ = Dispatcher.UIThread.InvokeAsync(() =>
{ {
@@ -612,12 +485,12 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
/// <summary> /// <summary>
/// Копирует <see cref="CalibrationRecord"/>, отвязывая её от базы данных /// Копирует <see cref="SampleRecord"/>, отвязывая её от базы данных
/// </summary> /// </summary>
/// <returns>Скопированная <see cref="CalibrationRecord"/></returns> /// <returns>Скопированная <see cref="SampleRecord"/></returns>
private static CalibrationRecord CopyRecord(CalibrationRecord source) private static SampleRecord CopyRecord(SampleRecord source)
{ {
return new CalibrationRecord return new SampleRecord
{ {
Id = source.Id, Id = source.Id,
SampleName = source.SampleName, SampleName = source.SampleName,
@@ -631,27 +504,22 @@ public partial class SampleCalibrationViewModel : ViewModelBase
MixtureNormalRate = source.MixtureNormalRate, MixtureNormalRate = source.MixtureNormalRate,
MixtureActualRate = source.MixtureActualRate, MixtureActualRate = source.MixtureActualRate,
MeasuredContent = source.MeasuredContent, MeasuredContent = source.MeasuredContent,
SampleImages = source.SampleImages Image = source.Image,
.Select(CopyRecord) VectorRecords = source.VectorRecords
.ToList(),
SeparatorImages = source.SeparatorImages
.Select(CopyRecord) .Select(CopyRecord)
.ToList() .ToList()
}; };
} }
/// <summary> /// <summary>
/// Копирует <see cref="ImageRecord"/>, отвязывая её от базы данных /// Копирует <see cref="VectorRecord"/>, отвязывая её от базы данных
/// </summary> /// </summary>
/// <returns>Скопированная <see cref="ImageRecord"/></returns> /// <returns>Скопированная <see cref="VectorRecord"/></returns>
private static ImageRecord CopyRecord(ImageRecord source) private static VectorRecord CopyRecord(VectorRecord source)
{ {
return new ImageRecord return new VectorRecord
{ {
Id = source.Id, Id = source.Id,
ImagePath = source.ImagePath, Values = (float[])source.Values.Clone()
VisibleIntensity = source.VisibleIntensity,
Uv365Intensity = source.Uv365Intensity,
Uv254Intensity = source.Uv254Intensity
}; };
} }
} }
@@ -8,19 +8,22 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using GSS2.Core;
using GSS2.Core.Analysis.AmineContent; using GSS2.Core.Analysis.AmineContent;
using GSS2.Core.Analysis.AmineContent.Database.Calibration; using GSS2.Core.Analysis.AmineContent.Database.Calibration;
using GSS2.Core.Hardware;
using GSS2.UI.Core.ViewModels; using GSS2.UI.Core.ViewModels;
using GSS2.UI.Core.Services;
namespace GSS2.ViewModels.AmineContent; namespace GSS2.ViewModels.AmineContent;
public partial class SeparatorCalibrationViewModel : ViewModelBase public partial class SeparatorCalibrationViewModel : ViewModelBase
{ {
private readonly ILogger<SeparatorCalibrationViewModel> _logger; private readonly ILogger<SeparatorCalibrationViewModel> _logger;
private readonly AmineContentCalibrationContext _context; private readonly CalibrationContext _context;
private readonly AmineContentImageCapturerService _imageCapturer; private readonly ImageCapturerService _imageCapturer;
private readonly ImageStorageService _imageStorage; private readonly ImageStorageService _imageStorage;
private readonly DependencyInjectionViewLocator _viewLocator;
private readonly NavigationService _navigation;
private CancellationTokenSource? _captureImagesTaskCts = null; private CancellationTokenSource? _captureImagesTaskCts = null;
@@ -35,31 +38,38 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true; [ObservableProperty] public partial bool MainSectionCanScrollForward { get; set; } = true;
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true; [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = true;
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = true;
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = true;
[ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null; [ObservableProperty] public partial Task? CaptureImagesTask { get; set; } = null;
[ObservableProperty] public partial ObservableCollection<AsyncImageRecordViewModel> Images { get; set; } = new();
[ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false; [ObservableProperty] public partial bool ImagesIsCapturing { get; set; } = false;
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
[ObservableProperty] public partial int ImagesSectionCurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool ImagesSectionCanScrollForward { get; set; } = false; [ObservableProperty] public partial AsyncImageRecordViewModel? Image { get; set; } = null;
[ObservableProperty] public partial bool ImagesSectionCanScrollBackward { get; set; } = false;
[ObservableProperty] public partial bool ImagesSectionIsScrolling { get; set; } = true;
public SeparatorCalibrationViewModel( public SeparatorCalibrationViewModel(
ILogger<SeparatorCalibrationViewModel> logger, ILogger<SeparatorCalibrationViewModel> logger,
AmineContentCalibrationContext context, CalibrationContext context,
AmineContentImageCapturerService imageCapturer, ImageCapturerService imageCapturer,
ImageStorageService imageStorage ImageStorageService imageStorage,
DependencyInjectionViewLocator viewLocator,
NavigationService navigation
) )
{ {
_logger = logger; _logger = logger;
_context = context; _context = context;
_imageCapturer = imageCapturer; _imageCapturer = imageCapturer;
_imageStorage = imageStorage; _imageStorage = imageStorage;
_viewLocator = viewLocator;
_navigation = navigation;
_logger.LogInformation("Инициализация"); _logger.LogInformation("Инициализация");
// Получаем записи из базы данных // Получаем записи из базы данных
Records = new ObservableCollection<SeparatorRecord>(_context.SeparatorRecords.Include(r => r.Images)); Records = new ObservableCollection<SeparatorRecord>(_context.SeparatorRecords);
// Берём первую запись // Берём первую запись
SelectedRecord = Records.FirstOrDefault(); SelectedRecord = Records.FirstOrDefault();
@@ -71,7 +81,8 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
{ {
Type = "", Type = "",
Batch = "", Batch = "",
Images = new() Image = "",
VectorRecords = new()
}; };
else else
EditingRecord = CopyRecord(SelectedRecord); EditingRecord = CopyRecord(SelectedRecord);
@@ -87,14 +98,14 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
{ {
// Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись) // Если выбранная запись отсутствует (в базе данных нет записей или создана новая запись)
// то создаём пустую запись // то создаём пустую запись
// TODO: Подумать над тем, чтобы сразу удалять несохранённые изображения из хранилища
if (SelectedRecord is null) if (SelectedRecord is null)
{ {
EditingRecord = new() EditingRecord = new()
{ {
Type = "", Type = "",
Batch = "", Batch = "",
Images = new() Image = "",
VectorRecords = new()
}; };
} }
else else
@@ -106,29 +117,22 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// <summary> /// <summary>
/// Вызывается при изменении <see cref="EditingRecord"/>.<br/> /// Вызывается при изменении <see cref="EditingRecord"/>.<br/>
/// Заполняет <see cref="Images"/> изображениями из <see cref="EditingRecord"/>. /// Изменяет <see cref="Image"/> изображением из <see cref="EditingRecord"/>.
/// </summary> /// </summary>
partial void OnEditingRecordChanged(SeparatorRecord value) partial void OnEditingRecordChanged(SeparatorRecord value)
{ {
var images = value.Images
// Для каждого изображения
.Select(i =>
{
// Находим путь через ImageStorageService
var path = _imageStorage.GetFullPath(i.ImagePath, "separator");
// Если изображения не найдено, возвращаем null
if (path is null)
return null;
// Создаём AsyncImageRecordViewModel
return new AsyncImageRecordViewModel(path, i);
})
// Оставляем только не null значения
.OfType<AsyncImageRecordViewModel>();
// Создаём новую коллекцию изображений var imagePath = _imageStorage.GetFullPath(value.Image, "calibration-sample");
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой if (!File.Exists(imagePath))
Images.Clear(); {
Images = new(images); Image = null;
return;
}
if (Image is null)
Image = new AsyncImageRecordViewModel(imagePath);
else
Image.Path = imagePath;
} }
// Просто команды управления меню // Просто команды управления меню
@@ -141,7 +145,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void CreateRecord() private void CreateRecord()
{ {
// Из-за того что, если создана новая запись, то SelectedRecord == null // Если уже создана новая запись, то SelectedRecord == null и
// при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется, // при простом присвоении SelectedRecord = null событие SelectedRecordChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null, // потому что событие срабатывает только при изменении значения, а значение было null и присвоилось ему null,
// поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged // поэтому если SelectedRecord == null, то нужно явно вызвать метод OnSelectedRecordChanged
@@ -155,28 +159,47 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private void DeleteRecord() private void DeleteRecord()
{ {
// Сохраняем текущую запись var confirmViewModel = new DeleteConfirmViewModel(_navigation)
var deletedRecord = SelectedRecord; {
// Создаём новую запись Question = "Вы уверены, что хотиите удаить запись?"
CreateRecord(); };
if (SelectedRecord is null)
// Если запись на была сохранена в базу данных, то ничего не делаем {
if (deletedRecord is null) CreateRecord();
return; return;
}
confirmViewModel.Closes += (_, ea) =>
{
if (ea != true)
return;
// Если запись была в без данных, то // Сохраняем текущую запись
// она была и в Records, удаляем её из Records var deletedRecord = SelectedRecord;
Records.Remove(deletedRecord); // Создаём новую запись
// Удаляем запись из базы данных и сохраняем изменения CreateRecord();
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске, // Если запись на была сохранена в базу данных, то ничего не делаем
// тогда удалённые записи будут заполнять диск if (deletedRecord is null)
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища return;
_context.SeparatorRecords.Remove(deletedRecord);
_context.SaveChanges(); // Если запись была в без данных, то
// она была и в Records, удаляем её из Records
Records.Remove(deletedRecord);
// Удаляем запись из базы данных и сохраняем изменения
// TODO: Подумать над тем, что возможно стоит не удалять запись полностью, а добавить флаг удаления,
// но тогда нужно будет сохранять и изображения этой записи, как в базе данных, так и на диске,
// тогда удалённые записи будут заполнять диск
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
_context.SeparatorRecords.Remove(deletedRecord);
_context.SaveChanges();
};
var convfirmView = _viewLocator.Build(confirmViewModel);
if (convfirmView is not null)
_navigation.NavigateTo(convfirmView);
} }
// Команда сохранения записи, сохраняет новую запись или изменения в отредактированной // // Команда сохранения записи, сохраняет новую запись или изменения в отредактированной
[RelayCommand] [RelayCommand]
private void SaveRecord() private void SaveRecord()
{ {
@@ -186,20 +209,20 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Если это новая запись, то копируем её // Если это новая запись, то копируем её
var newRecord = CopyRecord(EditingRecord); var newRecord = CopyRecord(EditingRecord);
// Для каждого изображения в записи // Для каждого вектора
for (int i = 0; i < newRecord.Images.Count(); i++) for (int i = 0; i < newRecord.VectorRecords.Count(); i++)
{ {
var image = newRecord.Images[i]; var vector = newRecord.VectorRecords[i];
// Если изображение есть в базе данных, то пропускаем его // Если вектор есть в базе данных, то пропускаем его
if (_context.Entry(image).State is not EntityState.Detached) if (_context.Entry(vector).State is not EntityState.Detached)
continue; continue;
// Если изображения нет в базе данных, то добавляем его в базу // Если вектора нет в базе данных, то добавляем его в базу
var attachedImage = _context.ImageRecords.Add(image); var attachedRecord = _context.VectorRecords.Add(vector);
// Удаляем отвязанное от базы данных изображение из записи // Удаляем отвязанное от базы данных изображение из записи
newRecord.Images.RemoveAt(i); newRecord.VectorRecords.RemoveAt(i);
// Добавляем привязанное изображение к записи // Добавляем привязанное изображение к записи
newRecord.Images.Insert(i, attachedImage.Entity); newRecord.VectorRecords.Insert(i, attachedRecord.Entity);
} }
// Если записи нет в базе данных // Если записи нет в базе данных
if (_context.Entry(newRecord).State is EntityState.Detached) if (_context.Entry(newRecord).State is EntityState.Detached)
@@ -223,57 +246,54 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Переносим данные из редактируемой записи в выбранную // Переносим данные из редактируемой записи в выбранную
SelectedRecord.Type = EditingRecord.Type; SelectedRecord.Type = EditingRecord.Type;
SelectedRecord.Batch = EditingRecord.Batch; SelectedRecord.Batch = EditingRecord.Batch;
SelectedRecord.Image = EditingRecord.Image;
// Находим изображения которые нужно удалить - // Удаляем старые векторы в записи
// те которые есть в выбранной записи, но нет в редактируемой
var imagesToRemove = SelectedRecord.Images
.Where(e => EditingRecord.Images.All(i => i.Id != e.Id))
.ToList();
// Удаляем старые изображения
// TODO: Подумать над тем, чтобы сразу удалять изображения из хранилища
foreach (var image in imagesToRemove)
SelectedRecord.Images.Remove(image);
// Для каждого изображения в редактируемой записи
foreach (var editedImage in EditingRecord.Images)
{ {
// Находим изображение в выбранной записи (оно будет привязано к базе данных) // Находим векторы которые нужно удалить -
var existing = SelectedRecord.Images // те которые есть в выбранной записи, но нет в редактируемой
.FirstOrDefault(i => i.Id == editedImage.Id); var vectorsToRemove = SelectedRecord.VectorRecords
.Where(e => EditingRecord.VectorRecords.All(i => i.Id != e.Id))
.ToList();
// Если изображение не найдено // Удаляем старые векторы
if (existing is null) foreach (var image in vectorsToRemove)
SelectedRecord.VectorRecords.Remove(image);
// Для каждого вектора в редактируемой записи
foreach (var editedImage in EditingRecord.VectorRecords)
{ {
// Копируем изображение в выбранную запись // Находим изображение в выбранной записи (оно будет привязано к базе данных)
SelectedRecord.Images.Add(CopyRecord(editedImage)); var existing = SelectedRecord.VectorRecords
.FirstOrDefault(i => i.Id == editedImage.Id);
// Если вектор не найден
// Копируем вектор в выбранную запись
if (existing is null)
SelectedRecord.VectorRecords.Add(CopyRecord(editedImage));
// Если вектор найден
// Изменяем данные вектора на данные вектора из редактируемой записи
else
existing.Values = editedImage.Values;
} }
// Если изображение найдено
else // Для каждого вектора в выбранной записи
for (int i = 0; i < SelectedRecord.VectorRecords.Count(); i++)
{ {
// Изменяем данные изображения на данные изображения из редактируемой записи var vector = SelectedRecord.VectorRecords[i];
existing.ImagePath = editedImage.ImagePath; // Если вектор есть в базе данных, то пропускаем его
existing.VisibleIntensity = editedImage.VisibleIntensity; if (_context.Entry(vector).State is not EntityState.Detached)
existing.Uv365Intensity = editedImage.Uv365Intensity; continue;
existing.Uv254Intensity = editedImage.Uv254Intensity;
// Если изображения нет в базе данных, то добавляем его в базу
var attachedRecord = _context.VectorRecords.Add(vector);
// Удаляем отвязанное от базы данных изображение из записи
SelectedRecord.VectorRecords.RemoveAt(i);
// Добавляем привязанное изображение к записи
SelectedRecord.VectorRecords.Insert(i, attachedRecord.Entity);
} }
} }
// Для каждого изображения в выбранной записи
for (int i = 0; i < SelectedRecord.Images.Count(); i++)
{
var image = SelectedRecord.Images[i];
// Если изображение есть в базе данных, то пропускаем его
if (_context.Entry(image).State is not EntityState.Detached)
continue;
// Если изображения нет в базе данных, то добавляем его в базу
var attachedImage = _context.ImageRecords.Add(image);
// Удаляем отвязанное от базы данных изображение из записи
SelectedRecord.Images.RemoveAt(i);
// Добавляем привязанное изображение к записи
SelectedRecord.Images.Insert(i, attachedImage.Entity);
}
// Если записи нет в базе данных // Если записи нет в базе данных
if (_context.Entry(SelectedRecord).State is EntityState.Detached) if (_context.Entry(SelectedRecord).State is EntityState.Detached)
{ {
@@ -295,9 +315,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Команда изменения основной секции (информация <-> изображения) // Команда изменения основной секции (информация <-> изображения)
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
// Команда навигации по изображениям // Команда навигации по информации
[RelayCommand] private void ImagesSectionPrevious() => ImagesSectionCurrentIndex--; [RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
[RelayCommand] private void ImagesSectionNext() => ImagesSectionCurrentIndex++; [RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
// Команда съёмки изображений // Команда съёмки изображений
[RelayCommand] [RelayCommand]
@@ -311,19 +331,24 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
_captureImagesTaskCts = new CancellationTokenSource(); _captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token; var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Запускаем получение изображений асинхронно // Запускаем получение изображений асинхронно
CaptureImagesTask = Task.Run(async () => CaptureImagesTask = Task.Run(async () =>
{ {
// Список полученных изображений // Список полученных изображений
var imageRecords = new List<ImageRecord>(); var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
// Получаем изображения // Получаем изображения
var task = _imageCapturer.CaptureImages(async (data) => var captureTask = _imageCapturer.CaptureImages(async (data) =>
{ {
// Распаковываем данные // Распаковываем данные
var (i, visible, uv365, uv254, image) = data; var (i, visible, uv365, uv254, image) = data;
@@ -331,73 +356,94 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а // Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {AmineContentImageCapturerService.IlluminatorIntensities.Count()}"); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
_ = Task.Run(async () => var imagesDataTask = Task.Run(() =>
{ {
// Кодируем изображение в png // Сохраняем png изображение во временную папку
var bytes = image.ImEncode(".png"); var path = Path.Join(Path.GetTempPath(), $"gss2-calibration-sample-{visible}-{uv365}-{uv254}.png");
// Сохраняем изображение в ImageStorageService image.ImWrite(path);
string fileName;
using (var stream = new MemoryStream(bytes))
fileName = await _imageStorage.SaveAsync(stream, ".png", "separator", cancellationToken);
// Освобождаем память изображения // Освобождаем память изображения
image.Release(); image.Release();
image = null;
bytes = null;
// Добавляем изображение в список полученных // Добавляем данные изображения в список
imageRecords.Add( return new ImageData(path, visible, uv365, uv254);
new ImageRecord()
{
ImagePath = fileName,
VisibleIntensity = visible,
Uv365Intensity = uv365,
Uv254Intensity = uv254,
}
);
}); });
imagesData.Add(imagesDataTask);
}, cancellationToken); }, cancellationToken);
_ = task.ContinueWith(t => // Обрабатываем изображения
var postTask = Task.WhenAll([captureTask, .. imagesData]).ContinueWith(async t =>
{ {
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
// Проверяем исключения задачи получения изображений
if (t.Exception is not null) if (t.Exception is not null)
{ {
_logger.LogError(t.Exception, "Исключение во время получения изображений"); _logger.LogError(t.Exception, "Исключение во время получения изображений");
foreach (var ex in t.Exception.InnerExceptions ?? []) foreach (var ex in t.Exception.InnerExceptions ?? [])
_logger.LogError(ex, "Исключение во время получения изображений"); _logger.LogError(ex, "Исключение во время получения изображений");
return;
} }
var images = imageRecords // Проверяем исключения задач сохранения изображений
// Для каждого изображения foreach (var imageData in imagesData)
.Select(i => if (imageData.Exception is not null)
{ {
// Находим путь через ImageStorageService _logger.LogError(imageData.Exception, "Исключение во время получения изображения");
var path = _imageStorage.GetFullPath(i.ImagePath, "separator"); foreach (var ex in imageData.Exception.InnerExceptions ?? [])
// Если изображения не найдено, возвращаем null _logger.LogError(ex, "Исключение во время получения изображения");
if (path is null) return;
return null; }
// Создаём AsyncImageRecordViewModel
return new AsyncImageRecordViewModel(path, i);
})
// Оставляем только не null значения
.OfType<AsyncImageRecordViewModel>();
// Обновляем данные в редактируемой коллекции // Вычисляем вектор признаков
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а var result = AnalyzerService.GetFeatureVectors(imagesData.Select(i => i.Result));
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // Если вектор признаков вычислен
_ = Dispatcher.UIThread.InvokeAsync(() => if (result is not null)
{ {
EditingRecord.Images = imageRecords; // Распаковываем результат
// Создаём новую коллекцию изображений var (avgImage, roi, features) = result.Value;
// TODO: Подумать над тем, чтобы чистить старую коллекцию перед созданием новой // Обрабатываем среднее изображение
Images = new(images); avgImage = new OpenCvSharp.Mat(avgImage, roi);
}); OpenCvSharp.Cv2.Resize(
avgImage,
avgImage,
new OpenCvSharp.Size(
AnalyzerService.ANALYSIS_IMAGE_SIZE,
AnalyzerService.ANALYSIS_IMAGE_SIZE
)
);
// Сбрасываем состояние задачи съёмки изображений // Кодируем и сохраняем среднее изображение
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,а OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
string avgPath;
using (var stream = new MemoryStream(avgData))
avgPath = await _imageStorage.SaveAsync(stream, ".png", "calibration-sample");
// Заполняем поля в редактируемой записи
EditingRecord.Image = avgPath;
EditingRecord.VectorRecords = features.Select(f => new VectorRecord { Values = f }).ToList();
// Обновляем данные в редактируемой записи
// Так же Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => OnEditingRecordChanged(EditingRecord));
}
else
{
_logger.LogError("Ошибка при вычислении векторов признаков");
}
}, cancellationToken);
// Сбрасываем состояние задачи съёмки изображений
_ = Task.WhenAll([captureTask, .. imagesData, postTask]).ContinueWith(t =>
{
// Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => _ = Dispatcher.UIThread.InvokeAsync(() =>
{ {
@@ -423,24 +469,22 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
Id = source.Id, Id = source.Id,
Type = source.Type, Type = source.Type,
Batch = source.Batch, Batch = source.Batch,
Images = source.Images Image = source.Image,
VectorRecords = source.VectorRecords
.Select(CopyRecord) .Select(CopyRecord)
.ToList() .ToList()
}; };
} }
/// <summary> /// <summary>
/// Копирует <see cref="ImageRecord"/>, отвязывая её от базы данных /// Копирует <see cref="VectorRecord"/>, отвязывая её от базы данных
/// </summary> /// </summary>
/// <returns>Скопированная <see cref="ImageRecord"/></returns> /// <returns>Скопированная <see cref="VectorRecord"/></returns>
private static ImageRecord CopyRecord(ImageRecord source) private static VectorRecord CopyRecord(VectorRecord source)
{ {
return new ImageRecord return new VectorRecord
{ {
Id = source.Id, Id = source.Id,
ImagePath = source.ImagePath, Values = (float[])source.Values.Clone()
VisibleIntensity = source.VisibleIntensity,
Uv365Intensity = source.Uv365Intensity,
Uv254Intensity = source.Uv254Intensity
}; };
} }
} }
-6
View File
@@ -13,9 +13,7 @@ public partial class AmineContentViewModel : ViewModelBase
{ {
private readonly NavigationService _navigationService; private readonly NavigationService _navigationService;
[ObservableProperty] public partial StartViewModel Start { get; set; }
[ObservableProperty] public partial AnalysisViewModel Analysis { get; set; } [ObservableProperty] public partial AnalysisViewModel Analysis { get; set; }
[ObservableProperty] public partial ResultsViewModel Results { get; set; }
[ObservableProperty] public partial SampleCalibrationViewModel SampleCalibration { get; set; } [ObservableProperty] public partial SampleCalibrationViewModel SampleCalibration { get; set; }
[ObservableProperty] public partial SeparatorCalibrationViewModel SeparatorCalibration { get; set; } [ObservableProperty] public partial SeparatorCalibrationViewModel SeparatorCalibration { get; set; }
[ObservableProperty] public partial CameraViewModel Camera { get; set; } [ObservableProperty] public partial CameraViewModel Camera { get; set; }
@@ -26,9 +24,7 @@ public partial class AmineContentViewModel : ViewModelBase
public AmineContentViewModel( public AmineContentViewModel(
NavigationService navigationService, NavigationService navigationService,
StartViewModel start,
AnalysisViewModel analysis, AnalysisViewModel analysis,
ResultsViewModel results,
SampleCalibrationViewModel sampleCalibration, SampleCalibrationViewModel sampleCalibration,
SeparatorCalibrationViewModel separatorCalibration, SeparatorCalibrationViewModel separatorCalibration,
CameraViewModel camera, CameraViewModel camera,
@@ -36,9 +32,7 @@ public partial class AmineContentViewModel : ViewModelBase
) )
{ {
_navigationService = navigationService; _navigationService = navigationService;
Start = start;
Analysis = analysis; Analysis = analysis;
Results = results;
SampleCalibration = sampleCalibration; SampleCalibration = sampleCalibration;
SeparatorCalibration = separatorCalibration; SeparatorCalibration = separatorCalibration;
Camera = camera; Camera = camera;
+38
View File
@@ -0,0 +1,38 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GSS2.UI.Core.Services;
using GSS2.UI.Core.ViewModels;
namespace GSS2.ViewModels;
public partial class DeleteConfirmViewModel : ViewModelBase
{
public event EventHandler<bool>? Closes;
private readonly NavigationService _navigation;
[ObservableProperty] public partial string Question { get; set; } = "";
[ObservableProperty] public partial bool Result { get; set; } = false;
public DeleteConfirmViewModel(NavigationService navigation)
{
_navigation = navigation;
}
[RelayCommand]
private void Delete()
{
Result = true;
Closes?.Invoke(this, Result);
_navigation.NavigateBack();
}
[RelayCommand]
private void Close()
{
Result = false;
Closes?.Invoke(this, Result);
_navigation.NavigateBack();
}
}
@@ -0,0 +1,36 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GSS2.UI.Core.Services;
using GSS2.UI.Core.ViewModels;
namespace GSS2.ViewModels;
public partial class NumericKeyboardViewModel : ViewModelBase
{
public event EventHandler<double?>? Closes;
private readonly NavigationService _navigation;
[ObservableProperty] public partial double? Value { get; set; } = null;
public NumericKeyboardViewModel(NavigationService navigation)
{
_navigation = navigation;
}
[RelayCommand]
private void Accept()
{
Closes?.Invoke(this, Value);
_navigation.NavigateBack();
}
[RelayCommand]
private void Cancel()
{
Value = null;
Closes?.Invoke(this, Value);
_navigation.NavigateBack();
}
}
+37
View File
@@ -0,0 +1,37 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GSS2.UI.Core.Services;
using GSS2.UI.Core.ViewModels;
namespace GSS2.ViewModels;
public partial class TextKeyboardViewModel : ViewModelBase
{
public event EventHandler<string?>? Closes;
private readonly NavigationService _navigation;
[ObservableProperty] public partial string? Value { get; set; } = null;
[ObservableProperty] public partial bool IsMultiline { get; set; } = false;
public TextKeyboardViewModel(NavigationService navigation)
{
_navigation = navigation;
}
[RelayCommand]
private void Accept()
{
Closes?.Invoke(this, Value);
_navigation.NavigateBack();
}
[RelayCommand]
private void Cancel()
{
Value = null;
Closes?.Invoke(this, Value);
_navigation.NavigateBack();
}
}
+597 -2
View File
@@ -3,11 +3,606 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels.AmineContent" xmlns:vm="using:GSS2.ViewModels.AmineContent"
xmlns:converters="using:GSS2.UI.Core.Converters"
xmlns:avalonia_converters="using:Avalonia.Data.Converters"
xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ia="using:Avalonia.Xaml.Interactions.Custom"
xmlns:controls="using:GSS2.Controls"
xmlns:core="using:GSS2.UI.Core"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
x:DataType="vm:AnalysisViewModel"> x:DataType="vm:AnalysisViewModel">
<Grid> <UserControl.Resources>
<TextBlock Text="Анализ" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/> <converters:NotEqualsConverter x:Key="NotEqualsConverter" />
<converters:EqualsConverter x:Key="EqualsConverter" />
<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" />
</UserControl.Resources>
<UserControl.Styles>
<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>
</UserControl.Styles>
<Grid Background="{StaticResource MainBackground}">
<!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}" ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Главная панель -->
<RowDefinition Height="Auto" /> <!-- Кнопки главной панели -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Удалить -->
<ColumnDefinition Width="*" /> <!-- Сохранить -->
<ColumnDefinition Width="*" /> <!-- К информации/К изображению -->
</Grid.ColumnDefinitions>
<!-- Главная панель -->
<core:SectionPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
CanScrollBackward="{Binding MainSectionCanScrollBackward}"
CanScrollForward="{Binding MainSectionCanScrollForward}"
ClipToBounds="True"
CurrentIndex="{Binding MainSectionCurrentIndex}"
IsScrolling="{Binding MainSectionIsScrolling}"
Orientation="Vertical">
<!-- Информация -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Информация -->
<RowDefinition Height="Auto" /> <!-- Кнопки -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
<ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<!-- Кнопка меню -->
<Button Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding ToggleMenuCommand}">
<TextBlock Text="☰"/>
</Button>
<!-- Информация -->
<core:SectionPanel Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
CanScrollBackward="{Binding InfoSectionCanScrollBackward}"
CanScrollForward="{Binding InfoSectionCanScrollForward}"
ClipToBounds="True"
CurrentIndex="{Binding InfoSectionCurrentIndex}"
Cyclic="True"
IsScrolling="{Binding InfoSectionIsScrolling}"
Orientation="Horizontal">
<!-- ИД / Название пробы / Дата и время отбора пробы / Место отбора пробы -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- ИД (подпись) -->
<RowDefinition Height="Auto" /> <!-- ИД (поле) -->
<RowDefinition Height="Auto" /> <!-- Название пробы (подпись) -->
<RowDefinition Height="Auto" /> <!-- Название пробы (поле) -->
<RowDefinition Height="Auto" /> <!-- Дата и время отбора пробы (подпись) -->
<RowDefinition Height="Auto" /> <!-- Дата и время отбора пробы (поле) -->
<RowDefinition Height="Auto" /> <!-- Место отбора пробы (подпись) -->
<RowDefinition Height="Auto" /> <!-- Место отбора пробы (поле) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<!-- Информация -->
<!-- ИД (подпись) -->
<TextBlock Grid.Row="0" Classes="mini" Text="ИД:" />
<!-- ИД (поле) -->
<TextBox Grid.Row="1" Classes="singleline" Text="{Binding EditingRecord.Id}" PlaceholderText="ИД" IsReadOnly="True" />
<!-- Название пробы (подпись) -->
<TextBlock Grid.Row="2" Classes="mini" Text="Название пробы:" />
<!-- Название пробы (поле) -->
<controls:TouchTextBox Grid.Row="3" Classes="singleline" Text="{Binding EditingRecord.SampleName}" PlaceholderText="Название пробы" />
<!-- Дата и время отбора пробы (подпись) -->
<TextBlock Grid.Row="4" Classes="mini" Text="Дата и время отбора пробы:" />
<!-- Дата и время отбора пробы (поле) -->
<controls:TouchTextBox Grid.Row="5" Classes="singleline" Text="{Binding EditingRecord.SamplingDateTime, Converter={StaticResource DateTimeConverter}}" PlaceholderText="Дата и время отбора пробы" />
<!-- Место отбора пробы (подпись) -->
<TextBlock Grid.Row="6" Classes="mini" Text="Место отбора пробы:" />
<!-- Место отбора пробы (поле) -->
<controls:TouchTextBox Grid.Row="7" Classes="singleline" Text="{Binding EditingRecord.SamplingPlace}" PlaceholderText="Место отбора пробы" />
</Grid>
<!-- Дополнительная информация о пробе -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Дополнительная информация о пробе (подпись) -->
<RowDefinition Height="*" /> <!-- Дополнительная информация о пробе (поле) -->
</Grid.RowDefinitions>
<!-- Информация -->
<!-- Дополнительная информация о пробе (подпись) -->
<TextBlock Grid.Row="0" Classes="mini" Text="Дополнительная информация о пробе:" />
<!-- Дополнительная информация о пробе (поле) -->
<controls:TouchTextBox Grid.Row="1" Classes="multiline" Text="{Binding EditingRecord.SampleComment}" PlaceholderText="Дополнительная информация о пробе" />
</Grid>
<!-- Марка пробы / Тип сепаратора / Среднее значение / Средняя дисперсия -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Марка пробы (подпись) / Тип сепаратора (подпись) -->
<RowDefinition Height="Auto" /> <!-- Марка пробы (поле) / Тип сепаратора (поле) -->
<RowDefinition Height="Auto" /> <!-- Минимальное значение (подпись) / Максимальное значение (подпись) -->
<RowDefinition Height="Auto" /> <!-- Минимальное значение (поле) / Максимальное значение (поле) -->
<RowDefinition Height="Auto" /> <!-- Количество частиц (подпись) / Среднее значение (подпись) -->
<RowDefinition Height="Auto" /> <!-- Количество частиц (поле) / Среднее значение (поле) -->
<RowDefinition Height="Auto" /> <!-- Средняя дисперсия (подпись) / Дисперсия среднего (подпись) -->
<RowDefinition Height="Auto" /> <!-- Средняя дисперсия (поле) / Дисперсия среднего (поле)-->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<!-- Информация -->
<!-- Марка пробы (подпись) -->
<TextBlock Grid.Row="0" Grid.Column="0" Classes="mini" Text="Марка пробы:" />
<!-- Марка пробы (поле) -->
<TextBox Grid.Row="1" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SampleBrand}" PlaceholderText="Марка пробы:" IsReadOnly="True" />
<!-- Тип сепаратора (подпись) -->
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="Тип сепаратора:" />
<!-- Тип сепаратора (поле) -->
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SeparatorType}" PlaceholderText="Тип сепаратора:" IsReadOnly="True" />
<!-- Минимальное значение (подпись) -->
<TextBlock Grid.Row="2" Grid.Column="0" Classes="mini" Text="Минимальное значение:" />
<!-- Минимальное значение (поле) -->
<TextBox Grid.Row="3" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.MinValue, StringFormat={}{0:N4}}" PlaceholderText="Минимальное значение:" IsReadOnly="True" />
<!-- Максимальное значение (подпись) -->
<TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Максимальное значение:" />
<!-- Максимальное значение (поле) -->
<TextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.MaxValue, StringFormat={}{0:N4}}" PlaceholderText="Максимальное значение:" IsReadOnly="True" />
<!-- Количество частиц (подпись) -->
<TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Количество частиц:" />
<!-- Количество частиц (поле) -->
<TextBox Grid.Row="5" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.ParticlesCount}" PlaceholderText="Количество частиц:" IsReadOnly="True" />
<!-- Среднее значение (подпись) -->
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Среднее значение:" />
<!-- Среднее значение (поле) -->
<TextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.MeanValue, StringFormat={}{0:N4}}" PlaceholderText="Среднее значение:" IsReadOnly="True" />
<!-- Средняя дисперсия (подпись) -->
<TextBlock Grid.Row="6" Grid.Column="0" Classes="mini" Text="Средняя дисперсия:" />
<!-- Средняя дисперсия (поле) -->
<TextBox Grid.Row="7" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.MeanVariance, StringFormat={}{0:N4}}" PlaceholderText="Средняя дисперсия:" IsReadOnly="True" />
<!-- Дисперсия среднего (подпись) -->
<TextBlock Grid.Row="6" Grid.Column="1" Classes="mini" Text="Дисперсия среднего:" />
<!-- Дисперсия среднего (поле) -->
<TextBox Grid.Row="7" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.VarianceOfMean, StringFormat={}{0:N4}}" PlaceholderText="Дисперсия среднего:" IsReadOnly="True" />
</Grid>
<!-- Изображения -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <!-- Изображения -->
<ColumnDefinition Width="Auto"/> <!-- Кнопки -->
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Grid.Row="0" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding SwitchToImageCommand}">
<TextBlock Text="1"/>
</Button>
<Button Grid.Column="1" Grid.Row="1" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding SwitchToMaskImageCommand}">
<TextBlock Text="2"/>
</Button>
<Button Grid.Column="1" Grid.Row="2" Classes="vertical" VerticalAlignment="Stretch" Command="{Binding SwitchToResultImageCommand}">
<TextBlock Text="3"/>
</Button>
<!-- Изображенияч -->
<Grid Grid.Column="0" Grid.Row="0" Grid.RowSpan="3">
<!-- Изображения -->
<Grid>
<Grid.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="Image" />
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="MaskImage" />
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="ResultImage" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</Grid.IsVisible>
<!-- Изображения -->
<core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
PanX="{Binding ImagesPanX, Mode=TwoWay}"
PanY="{Binding ImagesPanY, Mode=TwoWay}"
ImageSource="{Binding Image.Image}"
Background="Black"
IsVisible="{Binding ImageIsVisible}"/>
<core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
PanX="{Binding ImagesPanX, Mode=TwoWay}"
PanY="{Binding ImagesPanY, Mode=TwoWay}"
ImageSource="{Binding MaskImage.Image}"
Background="Black"
IsVisible="{Binding MaskImageIsVisible}"/>
<core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Zoom="{Binding ImagesZoom, Mode=TwoWay}"
PanX="{Binding ImagesPanX, Mode=TwoWay}"
PanY="{Binding ImagesPanY, Mode=TwoWay}"
ImageSource="{Binding ResultImage.Image}"
Background="Black"
IsVisible="{Binding ResultImageIsVisible}"/>
<!-- Оверлей загрузки -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding Image.IsLoading}">
<StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.Or}">
<Binding Path="Image.IsLoading" />
<Binding Path="MaskImage.IsLoading" />
<Binding Path="ResultImage.IsLoading" />
<Binding Path="ImagesIsCapturing" />
</MultiBinding>
</StackPanel.IsVisible>
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
</StackPanel>
</Grid>
<!-- Оверлей "Изображения отсутствуют" -->
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Изображения отсутствуют">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="Image" />
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="MaskImage" />
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="ResultImage" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей прогресс -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
</StackPanel>
</Grid>
</Grid>
<!-- Гистограмма средних значений -->
<lvc:CartesianChart x:Name="MeanValuesChart"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
LegendTextPaint="{Binding LegendTextPaint, Mode=OneWay}"
Series="{Binding MeanValuesHistogram, Mode=OneWay}"
XAxes="{Binding MeanValuesXAxis, Mode=OneWay}"
YAxes="{Binding MeanValuesYAxis, Mode=OneWay}" />
<!-- Гистограмма дисперсий -->
<lvc:CartesianChart x:Name="VariancesChart"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
LegendTextPaint="{Binding LegendTextPaint, Mode=OneWay}"
Series="{Binding VariancesHistogram, Mode=OneWay}"
XAxes="{Binding VariancesXAxis, Mode=OneWay}"
YAxes="{Binding VariancesYAxis, Mode=OneWay}" />
</core:SectionPanel>
<!-- Кнопка влево -->
<Button Grid.Column="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Command="{Binding InfoSectionPreviousCommand}">
<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 Grid.Column="2" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Command="{Binding InfoSectionNextCommand}">
<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>
<!-- Изображение -->
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Изображение -->
<RowDefinition Height="Auto" /> <!-- Кнопка "Получить изображения" / "Отменить съёмку" -->
</Grid.RowDefinitions>
<!-- Изображение -->
<Grid>
<!-- Изображение -->
<Grid>
<Grid.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="Image" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</Grid.IsVisible>
<!-- Изображение -->
<core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ImageSource="{Binding Image.Image}"
Background="Black"
IsVisible="{Binding Image.IsLoading, Converter={x:Static BoolConverters.Not}}" />
<!-- Оверлей загрузки -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding Image.IsLoading}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
</StackPanel>
</Grid>
<!-- Оверлей "Изображения отсутствуют" -->
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Изображения отсутствуют">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="Image" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<!-- Оверлей прогресс -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
</StackPanel>
</Grid>
<!-- Кнопка "Получить изображения" -->
<Button Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Text="Получить изображения"/>
</Button>
<!-- Кнопка "Отменить съёмку" -->
<Button Grid.Row="1"
HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Text="Отменить съёмку"/>
</Button>
</Grid>
</core:SectionPanel>
<!-- Кнопки главной панели -->
<!-- Удалить -->
<Button Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" Classes="danger" Command="{Binding DeleteRecordCommand}">
<TextBlock Text="Удалить"/>
</Button>
<!-- Сохранить -->
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" Command="{Binding SaveRecordCommand}" Content="Сохранить">
<TextBlock Text="Сохранить"/>
</Button>
<!-- К информации -->
<Button Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}"
Content="К информации"
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}">
<TextBlock Text="К информации"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="MainSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="MainSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<!-- К изображению -->
<Button Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}"
Content="К изображению"
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
<TextBlock Text="К изображению"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="MainSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="MainSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid>
<!-- Полупрозрачный оверлей -->
<Border Background="{StaticResource Overlay}" IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
<Interaction.Behaviors>
<TappedTrigger>
<InvokeCommandAction Command="{Binding CloseMenuCommand}" />
</TappedTrigger>
</Interaction.Behaviors>
</Border>
<!-- Выезжающая боковая панель -->
<Border HorizontalAlignment="Left"
Background="{StaticResource MenuBackground}" BorderBrush="{StaticResource MenuBorder}" BorderThickness="0,0,1,0"
IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
<Border.Transitions>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.25" />
</Transitions>
</Border.Transitions>
<Interaction.Behaviors>
<DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="True">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" />
</DataTriggerBehavior>
<DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="False">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" />
</DataTriggerBehavior>
</Interaction.Behaviors>
<!-- Содержимое меню -->
<Grid RowSpacing="10" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Grid.Row="0" MinWidth="200" HorizontalAlignment="Stretch" Command="{Binding CreateRecordCommand}">
<TextBlock Text="Новая запись"/>
</Button>
<ListBox Grid.Row="1" Background="{StaticResource MenuBackground}" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource MainBackground}" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource MainBackground}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Margin="3" CornerRadius="5">
<TextBlock Classes="mini" HorizontalAlignment="Center" VerticalAlignment="Center">
<Run Text="{Binding Id, StringFormat='[{0}] '}" />
<Run Text="{Binding SampleName}" />
</TextBlock>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -1,5 +1,7 @@
using Avalonia.Controls; using Avalonia.Controls;
using GSS2.ViewModels.AmineContent;
namespace GSS2.Views.AmineContent; namespace GSS2.Views.AmineContent;
public partial class AnalysisView : UserControl public partial class AnalysisView : UserControl
@@ -7,5 +9,14 @@ public partial class AnalysisView : UserControl
public AnalysisView() public AnalysisView()
{ {
InitializeComponent(); InitializeComponent();
(DataContext as AnalysisViewModel)?.UpdateCharts += OnChartsUpdate;
DataContextChanged += (_, ea) => (DataContext as AnalysisViewModel)?.UpdateCharts += OnChartsUpdate;
}
private void OnChartsUpdate(object? sender, EventArgs ea)
{
MeanValuesChart.CoreChart.Update();
VariancesChart.CoreChart.Update();
} }
} }
-13
View File
@@ -1,13 +0,0 @@
<UserControl
x:Class="GSS2.Views.AmineContent.ResultsView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels.AmineContent"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:DataType="vm:ResultsViewModel">
<Grid>
<TextBlock Text="Результаты" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.Views.AmineContent;
public partial class ResultsView : UserControl
{
public ResultsView()
{
InitializeComponent();
}
}
@@ -7,9 +7,8 @@
xmlns:avalonia_converters="using:Avalonia.Data.Converters" xmlns:avalonia_converters="using:Avalonia.Data.Converters"
xmlns:i="using:Avalonia.Xaml.Interactivity" xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ia="using:Avalonia.Xaml.Interactions.Custom" xmlns:ia="using:Avalonia.Xaml.Interactions.Custom"
xmlns:local="using:GSS2" xmlns:controls="using:GSS2.Controls"
xmlns:core="using:GSS2.UI.Core" xmlns:core="using:GSS2.UI.Core"
xmlns:core_vm="using:GSS2.UI.Core.ViewModels"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:DataType="vm:SampleCalibrationViewModel"> x:DataType="vm:SampleCalibrationViewModel">
@@ -83,7 +82,7 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Удалить --> <ColumnDefinition Width="*" /> <!-- Удалить -->
<ColumnDefinition Width="*" /> <!-- Сохранить --> <ColumnDefinition Width="*" /> <!-- Сохранить -->
<ColumnDefinition Width="*" /> <!-- К информации/К изображениям --> <ColumnDefinition Width="*" /> <!-- К информации/К изображению -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Главная панель --> <!-- Главная панель -->
@@ -153,23 +152,23 @@
<!-- Название пробы (подпись) --> <!-- Название пробы (подпись) -->
<TextBlock Grid.Row="2" Grid.Column="0" Classes="mini" Text="Название пробы:" /> <TextBlock Grid.Row="2" Grid.Column="0" Classes="mini" Text="Название пробы:" />
<!-- Название пробы (поле) --> <!-- Название пробы (поле) -->
<local:TouchTextBox Grid.Row="3" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SampleName}" PlaceholderText="Название пробы" /> <controls:TouchTextBox Grid.Row="3" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SampleName}" PlaceholderText="Название пробы" />
<!-- Марка пробы (подпись) --> <!-- Марка пробы (подпись) -->
<TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Марка пробы:" /> <TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Марка пробы:" />
<!-- Марка пробы (поле) --> <!-- Марка пробы (поле) -->
<local:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SampleBrand}" PlaceholderText="Марка пробы" /> <controls:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SampleBrand}" PlaceholderText="Марка пробы" />
<!-- Дата и время отбора пробы (подпись) --> <!-- Дата и время отбора пробы (подпись) -->
<TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Дата и время отбора пробы:" /> <TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Дата и время отбора пробы:" />
<!-- Дата и время отбора пробы (поле) --> <!-- Дата и время отбора пробы (поле) -->
<local:TouchTextBox Grid.Row="5" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SamplingDateTime, Converter={StaticResource DateTimeConverter}}" PlaceholderText="Дата и время отбора пробы" /> <controls:TouchTextBox Grid.Row="5" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.SamplingDateTime, Converter={StaticResource DateTimeConverter}}" PlaceholderText="Дата и время отбора пробы" />
<!-- Место отбора пробы (подпись) --> <!-- Место отбора пробы (подпись) -->
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Место отбора пробы:" /> <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Место отбора пробы:" />
<!-- Место отбора пробы (поле) --> <!-- Место отбора пробы (поле) -->
<local:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SamplingPlace}" PlaceholderText="Место отбора пробы" /> <controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.SamplingPlace}" PlaceholderText="Место отбора пробы" />
<!-- Масса пробы (кг) (подпись) --> <!-- Масса пробы (кг) (подпись) -->
<TextBlock Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Масса пробы (кг):" /> <TextBlock Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Масса пробы (кг):" />
<!-- Масса пробы (кг) (поле) --> <!-- Масса пробы (кг) (поле) -->
<local:TouchNumericUpDown Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.SampleMass}" PlaceholderText="Масса пробы (кг)" /> <controls:TouchNumericUpDown Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.SampleMass}" PlaceholderText="Масса пробы (кг)" />
</Grid> </Grid>
@@ -197,23 +196,23 @@
<!-- Название используемой кондиционирующей смеси (подпись) --> <!-- Название используемой кондиционирующей смеси (подпись) -->
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Название используемой кондиционирующей смеси:" /> <TextBlock Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Название используемой кондиционирующей смеси:" />
<!-- Название используемой кондиционирующей смеси (поле) --> <!-- Название используемой кондиционирующей смеси (поле) -->
<local:TouchTextBox Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.MixtureName}" PlaceholderText="Название используемой кондиционирующей смеси" /> <controls:TouchTextBox Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" Text="{Binding EditingRecord.MixtureName}" PlaceholderText="Название используемой кондиционирующей смеси" />
<!-- Содержание люминофоров в кондиционирующей смеси (%) (подпись) --> <!-- Содержание люминофоров в кондиционирующей смеси (%) (подпись) -->
<TextBlock Grid.Row="2" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Содержание люминофоров в кондиционирующей смеси (%):" /> <TextBlock Grid.Row="2" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Содержание люминофоров в кондиционирующей смеси (%):" />
<!-- Содержание люминофоров в кондиционирующей смеси (%) (поле) --> <!-- Содержание люминофоров в кондиционирующей смеси (%) (поле) -->
<local:TouchNumericUpDown Grid.Row="3" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0" Increment="0.1" Value="{Binding EditingRecord.MixtureAmineContent}" PlaceholderText="Содержание люминофоров в кондиционирующей смеси (%)" /> <controls:TouchNumericUpDown Grid.Row="3" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0" Increment="0.1" Value="{Binding EditingRecord.MixtureAmineContent}" PlaceholderText="Содержание люминофоров в кондиционирующей смеси (%)" />
<!-- Норма расхода кондиционирующей смеси (кг/т | гр/кг) (подпись) --> <!-- Норма расхода кондиционирующей смеси (кг/т | гр/кг) (подпись) -->
<TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Норма расхода (кг/т | гр/кг):" /> <TextBlock Grid.Row="4" Grid.Column="0" Classes="mini" Text="Норма расхода (кг/т | гр/кг):" />
<!-- Норма расхода кондиционирующей смеси (кг/т | гр/кг) (поле) --> <!-- Норма расхода кондиционирующей смеси (кг/т | гр/кг) (поле) -->
<local:TouchNumericUpDown Grid.Row="5" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MixtureNormalRate}" PlaceholderText="Норма расхода кондиционирующей смеси (кг/т | гр/кг)" /> <controls:TouchNumericUpDown Grid.Row="5" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MixtureNormalRate}" PlaceholderText="Норма расхода кондиционирующей смеси (кг/т | гр/кг)" />
<!-- Фактический расход кондиционирующей смеси (кг/т | гр/кг) (подпись) --> <!-- Фактический расход кондиционирующей смеси (кг/т | гр/кг) (подпись) -->
<TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Фактический расход (кг/т | гр/кг):" /> <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Фактический расход (кг/т | гр/кг):" />
<!-- Фактический расход кондиционирующей смеси (кг/т | гр/кг) (поле) --> <!-- Фактический расход кондиционирующей смеси (кг/т | гр/кг) (поле) -->
<local:TouchNumericUpDown Grid.Row="5" Grid.Column="1" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MixtureActualRate}" PlaceholderText="Фактический расход кондиционирующей смеси (кг/т | гр/кг)" /> <controls:TouchNumericUpDown Grid.Row="5" Grid.Column="1" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MixtureActualRate}" PlaceholderText="Фактический расход кондиционирующей смеси (кг/т | гр/кг)" />
<!-- Измеренное количество кондиционирующей смеси (кг/т | гр/кг) (подпись) --> <!-- Измеренное количество кондиционирующей смеси (кг/т | гр/кг) (подпись) -->
<TextBlock Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Измеренное количество кондиционирующей смеси (кг/т | гр/кг):" /> <TextBlock Grid.Row="6" Grid.ColumnSpan="2" Grid.Column="0" Classes="mini" Text="Измеренное количество кондиционирующей смеси (кг/т | гр/кг):" />
<!-- Измеренное количество кондиционирующей смеси (кг/т | гр/кг) (поле) --> <!-- Измеренное количество кондиционирующей смеси (кг/т | гр/кг) (поле) -->
<local:TouchNumericUpDown Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MeasuredContent}" PlaceholderText="Измеренное количество кондиционирующей смеси (кг/т | гр/кг)" /> <controls:TouchNumericUpDown Grid.Row="7" Grid.ColumnSpan="2" Grid.Column="0" Classes="singleline" FormatString="0.0000" Increment="0.0001" Value="{Binding EditingRecord.MeasuredContent}" PlaceholderText="Измеренное количество кондиционирующей смеси (кг/т | гр/кг)" />
</Grid> </Grid>
@@ -229,19 +228,15 @@
<!-- Дополнительная информация о пробе (подпись) --> <!-- Дополнительная информация о пробе (подпись) -->
<TextBlock Grid.Row="0" Classes="mini" Text="Дополнительная информация о пробе:" /> <TextBlock Grid.Row="0" Classes="mini" Text="Дополнительная информация о пробе:" />
<!-- Дополнительная информация о пробе (поле) --> <!-- Дополнительная информация о пробе (поле) -->
<local:TouchTextBox Grid.Row="1" Classes="multiline" Text="{Binding EditingRecord.SampleComment}" PlaceholderText="Дополнительная информация о пробе" /> <controls:TouchTextBox Grid.Row="1" Classes="multiline" Text="{Binding EditingRecord.SampleComment}" PlaceholderText="Дополнительная информация о пробе" />
</Grid> </Grid>
</core:SectionPanel> </core:SectionPanel>
<!-- Кнопка влево --> <!-- Кнопка влево -->
<Button <Button Grid.Column="1" Grid.Row="1"
Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Grid.Row="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding InfoSectionPreviousCommand}"> Command="{Binding InfoSectionPreviousCommand}">
<Button.IsEnabled> <Button.IsEnabled>
@@ -256,12 +251,8 @@
</Button> </Button>
<!-- Кнопка вправо --> <!-- Кнопка вправо -->
<Button <Button Grid.Column="2" Grid.Row="1"
Grid.Column="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Grid.Row="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding InfoSectionNextCommand}"> Command="{Binding InfoSectionNextCommand}">
<Button.IsEnabled> <Button.IsEnabled>
@@ -277,421 +268,75 @@
</Grid> </Grid>
<!-- Изображения --> <!-- Изображение -->
<Grid ColumnSpacing="5" RowSpacing="5"> <Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Изображения --> <RowDefinition Height="*" /> <!-- Изображение -->
<RowDefinition Height="Auto" /> <!-- Кнопка "Получить изображения"/"Отменить съёмку" --> <RowDefinition Height="Auto" /> <!-- Кнопка "Получить изображения" / "Отменить съёмку" -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <!-- Изображение -->
<ColumnDefinition Width="*" /> <!-- Кнопка "Получить изображения"/"Отменить съёмку" --> <Grid>
<ColumnDefinition Width="*" /> <!-- Кнопки "Изображения сепаратора"/"Изображения пробы" -->
</Grid.ColumnDefinitions>
<!-- Изображения --> <!-- Изображение -->
<core:SectionPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
CanScrollBackward="{Binding ImagesSectionCanScrollBackward}"
CanScrollForward="{Binding ImagesSectionCanScrollForward}"
ClipToBounds="True"
CurrentIndex="{Binding ImagesSectionCurrentIndex}"
IsScrolling="{Binding ImagesSectionIsScrolling}"
Orientation="Vertical">
<!-- Изображения сепаратора -->
<Grid> <Grid>
<Grid ColumnSpacing="5"> <Grid.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="Image" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</Grid.IsVisible>
<Grid.IsVisible> <!-- Изображение -->
<MultiBinding Converter="{x:Static BoolConverters.And}"> <core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ImageSource="{Binding Image.Image}"
Background="Black"
IsVisible="{Binding Image.IsLoading, Converter={x:Static BoolConverters.Not}}" />
<Binding <!-- Оверлей загрузки -->
Converter="{StaticResource NotEqualsConverter}" <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding Image.IsLoading}">
ConverterParameter="0"
Path="SeparatorImages.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</Grid.IsVisible>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка влево -->
<ColumnDefinition Width="*" /> <!-- Изображения -->
<ColumnDefinition Width="Auto" /> <!-- Кнопка вправо -->
</Grid.ColumnDefinitions>
<!-- Кнопка влево -->
<Button
Grid.Column="0"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding SeparatorImagesSectionPreviousCommand}">
<TextBlock Text="◀"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SeparatorImagesSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorImagesSectionIsScrolling" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<!-- Изображения -->
<core:SectionPanel
Grid.Column="1"
CanScrollBackward="{Binding SeparatorImagesSectionCanScrollBackward}"
CanScrollForward="{Binding SeparatorImagesSectionCanScrollForward}"
ChildrenSource="{Binding SeparatorImages}"
ClipToBounds="True"
CurrentIndex="{Binding SeparatorImagesSectionCurrentIndex}"
Cyclic="True"
IsScrolling="{Binding SeparatorImagesSectionIsScrolling}"
Orientation="Horizontal">
<core:SectionPanel.ChildrenTemplate>
<DataTemplate>
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Изображения -->
<ColumnDefinition Width="Auto" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Изображения -->
<RowDefinition Height="Auto" /> <!-- ИД (подпись) -->
<RowDefinition Height="Auto" /> <!-- ИД (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность видимого (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность видимого (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (поле) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<!-- Изображения -->
<Grid Grid.Row="0" Grid.RowSpan="12" Grid.Column="0">
<core:ZoomPanImage
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
x:DataType="core_vm:AsyncImageRecordViewModel"
ImageSource="{Binding Image}"
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" x:DataType="core_vm:AsyncImageRecordViewModel" IsVisible="{Binding IsLoading}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
</StackPanel>
</Grid>
<!-- Информация -->
<!-- ИД (подпись) -->
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД: " />
<!-- ИД (поле) -->
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Id}" />
<!-- Интенсивность видимого (подпись) -->
<TextBlock Grid.Row="3" Grid.Column="1" Classes="mini" Text="Интенсивность видимого: " />
<!-- Интенсивность видимого (поле) -->
<TextBox Grid.Row="4" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.VisibleIntensity}" />
<!-- Интенсивность УФ 365 нм (подпись) -->
<TextBlock Grid.Row="5" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 365 нм: " />
<!-- Интенсивность УФ 365 нм (поле) -->
<TextBox Grid.Row="6" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv365Intensity}" />
<!-- Интенсивность УФ 254 нм (подпись) -->
<TextBlock Grid.Row="7" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 254 нм: " />
<!-- Интенсивность УФ 254 нм (поле) -->
<TextBox Grid.Row="8" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv254Intensity}" />
</Grid>
</DataTemplate>
</core:SectionPanel.ChildrenTemplate>
</core:SectionPanel>
<!-- Кнопка вправо -->
<Button
Grid.Column="2"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding SeparatorImagesSectionNextCommand}">
<TextBlock Text="▶"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SeparatorImagesSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorImagesSectionIsScrolling" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Изображения сепаратора отсутствуют">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding
Converter="{StaticResource EqualsConverter}"
ConverterParameter="0"
Path="SeparatorImages.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" /> <ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" /> <TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- Изображения пробы --> <!-- Оверлей "Изображения отсутствуют" -->
<Grid> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Изображения отсутствуют">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="Image" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<Grid ColumnSpacing="5"> <!-- Оверлей прогресс -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
</StackPanel>
<Grid.IsVisible> </Grid>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding
Converter="{StaticResource NotEqualsConverter}"
ConverterParameter="0"
Path="SampleImages.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</Grid.IsVisible>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка влево -->
<ColumnDefinition Width="*" /> <!-- Изображения -->
<ColumnDefinition Width="Auto" /> <!-- Кнопка вправо -->
</Grid.ColumnDefinitions>
<!-- Кнопка влево -->
<Button
Grid.Column="0"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding SampleImagesSectionPreviousCommand}">
<TextBlock Text="◀"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SampleImagesSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SampleImagesSectionIsScrolling" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<!-- Изображения -->
<core:SectionPanel
Grid.Column="1"
CanScrollBackward="{Binding SampleImagesSectionCanScrollBackward}"
CanScrollForward="{Binding SampleImagesSectionCanScrollForward}"
ChildrenSource="{Binding SampleImages}"
ClipToBounds="True"
CurrentIndex="{Binding SampleImagesSectionCurrentIndex}"
Cyclic="True"
IsScrolling="{Binding SampleImagesSectionIsScrolling}"
Orientation="Horizontal">
<core:SectionPanel.ChildrenTemplate>
<DataTemplate>
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Изображения -->
<ColumnDefinition Width="Auto" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Изображения -->
<RowDefinition Height="Auto" /> <!-- ИД (подпись) -->
<RowDefinition Height="Auto" /> <!-- ИД (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность видимого (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность видимого (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (поле) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<!-- Изображения -->
<Grid Grid.Row="0" Grid.RowSpan="12" Grid.Column="0">
<core:ZoomPanImage
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
x:DataType="core_vm:AsyncImageRecordViewModel"
ImageSource="{Binding Image}"
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" x:DataType="core_vm:AsyncImageRecordViewModel" IsVisible="{Binding IsLoading}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
</StackPanel>
</Grid>
<!-- Информация -->
<!-- ИД (подпись) -->
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД: " />
<!-- ИД (поле) -->
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Id}" />
<!-- Интенсивность видимого (подпись) -->
<TextBlock Grid.Row="3" Grid.Column="1" Classes="mini" Text="Интенсивность видимого: " />
<!-- Интенсивность видимого (поле) -->
<TextBox Grid.Row="4" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.VisibleIntensity}" />
<!-- Интенсивность УФ 365 нм (подпись) -->
<TextBlock Grid.Row="5" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 365 нм: " />
<!-- Интенсивность УФ 365 нм (поле) -->
<TextBox Grid.Row="6" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv365Intensity}" />
<!-- Интенсивность УФ 254 нм (подпись) -->
<TextBlock Grid.Row="7" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 254 нм: " />
<!-- Интенсивность УФ 254 нм (поле) -->
<TextBox Grid.Row="8" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv254Intensity}" />
</Grid>
</DataTemplate>
</core:SectionPanel.ChildrenTemplate>
</core:SectionPanel>
<!-- Кнопка вправо -->
<Button
Grid.Column="2"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding SampleImagesSectionNextCommand}">
<TextBlock Text="▶"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SampleImagesSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SampleImagesSectionIsScrolling" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Изображения пробы отсутствуют">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding
Converter="{StaticResource EqualsConverter}"
ConverterParameter="0"
Path="SampleImages.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
</StackPanel>
</Grid>
</core:SectionPanel>
<!-- Кнопка "Получить изображения" --> <!-- Кнопка "Получить изображения" -->
<Button <Button Grid.Row="1"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}" Command="{Binding CaptureImageCommand}"
IsEnabled="{Binding !ImagesSectionIsScrolling}"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}"> IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Text="Получить изображения"/> <TextBlock Text="Получить изображения"/>
</Button> </Button>
<!-- Кнопка "Отменить съёмку" --> <!-- Кнопка "Отменить съёмку" -->
<Button <Button Grid.Row="1"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}" Command="{Binding CancelImageCapturingCommand}"
IsEnabled="{Binding !ImagesSectionIsScrolling}"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Text="Отменить съёмку"/> <TextBlock Text="Отменить съёмку"/>
</Button> </Button>
<!-- Кнопка "Изображения сепаратора" -->
<Button
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Stretch"
Command="{Binding ImagesSectionSwitchCommand}"
IsEnabled="{Binding !ImagesSectionIsScrolling}"
IsVisible="{Binding ImagesSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=1}">
<TextBlock Text="Изображения сепаратора"/>
</Button>
<!-- Кнопка "Изображения пробы" -->
<Button
Grid.Row="1"
Grid.Column="1"
HorizontalAlignment="Stretch"
Command="{Binding ImagesSectionSwitchCommand}"
IsEnabled="{Binding !ImagesSectionIsScrolling}"
IsVisible="{Binding ImagesSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
<TextBlock Text="Изображения пробы"/>
</Button>
</Grid> </Grid>
</core:SectionPanel> </core:SectionPanel>
@@ -708,9 +353,7 @@
</Button> </Button>
<!-- К информации --> <!-- К информации -->
<Button <Button Grid.Row="1" Grid.Column="2"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}" Command="{Binding SwitchMainSectionCommand}"
Content="К информации" Content="К информации"
@@ -727,16 +370,14 @@
</Button> </Button>
<!-- К изображениям --> <!-- К изображению -->
<Button <Button Grid.Row="1" Grid.Column="2"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}" Command="{Binding SwitchMainSectionCommand}"
Content="К изображениям" Content="К изображению"
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}"> IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
<TextBlock Text="К изображениям"/> <TextBlock Text="К изображению"/>
<Button.IsEnabled> <Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
@@ -759,13 +400,9 @@
</Border> </Border>
<!-- Выезжающая боковая панель --> <!-- Выезжающая боковая панель -->
<Border <Border HorizontalAlignment="Left"
HorizontalAlignment="Left" Background="{StaticResource MenuBackground}" BorderBrush="{StaticResource MenuBorder}" BorderThickness="0,0,1,0"
Background="{StaticResource MenuBackground}" IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
BorderBrush="{StaticResource MenuBorder}"
BorderThickness="0,0,1,0"
IsEnabled="{Binding MenuOpened}"
IsVisible="{Binding MenuOpened}">
<Border.Transitions> <Border.Transitions>
<Transitions> <Transitions>
@@ -775,22 +412,12 @@
<Interaction.Behaviors> <Interaction.Behaviors>
<DataTriggerBehavior <DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="True">
Binding="{Binding MenuOpened}"
ComparisonCondition="Equal"
Value="True">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" /> <ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" />
</DataTriggerBehavior> </DataTriggerBehavior>
<DataTriggerBehavior <DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="False">
Binding="{Binding MenuOpened}"
ComparisonCondition="Equal"
Value="False">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" /> <ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" />
</DataTriggerBehavior> </DataTriggerBehavior>
</Interaction.Behaviors> </Interaction.Behaviors>
@@ -803,14 +430,11 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button Grid.Row="0" <Button Grid.Row="0" MinWidth="200" HorizontalAlignment="Stretch" Command="{Binding CreateRecordCommand}">
MinWidth="200"
HorizontalAlignment="Stretch"
Command="{Binding CreateRecordCommand}">
<TextBlock Text="Новая запись"/> <TextBlock Text="Новая запись"/>
</Button> </Button>
<ListBox Grid.Row="1" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}"> <ListBox Grid.Row="1" Background="{StaticResource MenuBackground}" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
@@ -7,15 +7,15 @@
xmlns:avalonia_converters="using:Avalonia.Data.Converters" xmlns:avalonia_converters="using:Avalonia.Data.Converters"
xmlns:i="using:Avalonia.Xaml.Interactivity" xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ia="using:Avalonia.Xaml.Interactions.Custom" xmlns:ia="using:Avalonia.Xaml.Interactions.Custom"
xmlns:local="using:GSS2" xmlns:controls="using:GSS2.Controls"
xmlns:core="using:GSS2.UI.Core" xmlns:core="using:GSS2.UI.Core"
xmlns:core_vm="using:GSS2.UI.Core.ViewModels"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:DataType="vm:SeparatorCalibrationViewModel"> x:DataType="vm:SeparatorCalibrationViewModel">
<UserControl.Resources> <UserControl.Resources>
<converters:NotEqualsConverter x:Key="NotEqualsConverter" /> <converters:NotEqualsConverter x:Key="NotEqualsConverter" />
<converters:EqualsConverter x:Key="EqualsConverter" /> <converters:EqualsConverter x:Key="EqualsConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
<SolidColorBrush x:Key="MenuBackground" Color="#00357a" /> <SolidColorBrush x:Key="MenuBackground" Color="#00357a" />
<SolidColorBrush x:Key="MenuBorder" Color="#0865b2" /> <SolidColorBrush x:Key="MenuBorder" Color="#0865b2" />
@@ -53,6 +53,8 @@
<Style Selector="TextBox.multiline"> <Style Selector="TextBox.multiline">
<Setter Property="VerticalContentAlignment" Value="Top" /> <Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="AcceptsReturn" Value="True" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style> </Style>
<Style Selector="TextBlock"> <Style Selector="TextBlock">
@@ -80,7 +82,7 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Удалить --> <ColumnDefinition Width="*" /> <!-- Удалить -->
<ColumnDefinition Width="*" /> <!-- Сохранить --> <ColumnDefinition Width="*" /> <!-- Сохранить -->
<ColumnDefinition Width="*" /> <!-- К информации/К изображениям --> <ColumnDefinition Width="*" /> <!-- К информации/К изображению -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Главная панель --> <!-- Главная панель -->
@@ -124,186 +126,60 @@
<!-- Тип (подпись) --> <!-- Тип (подпись) -->
<TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" /> <TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" />
<!-- Тип (поле) --> <!-- Тип (поле) -->
<local:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" /> <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="Партия:" /> <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" />
<!-- Партия (поле) --> <!-- Партия (поле) -->
<local:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" /> <controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" />
</Grid> </Grid>
<!-- Изображения --> <!-- Изображение -->
<Grid ColumnSpacing="5" RowSpacing="5"> <Grid ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Изображения --> <RowDefinition Height="*" /> <!-- Изображение -->
<RowDefinition Height="Auto" /> <!-- Кнопка "получить изображения" --> <RowDefinition Height="Auto" /> <!-- Кнопка "Получить изображения" / "Отменить съёмку" -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Изображения --> <!-- Изображение -->
<Grid Grid.Row="0"> <Grid>
<Grid ColumnSpacing="5"> <!-- Изображение -->
<Grid>
<Grid.IsVisible> <Grid.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="Image" />
<Binding
Converter="{StaticResource NotEqualsConverter}"
ConverterParameter="0"
Path="Images.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</Grid.IsVisible> </Grid.IsVisible>
<Grid.ColumnDefinitions> <!-- Изображение -->
<ColumnDefinition Width="Auto" /> <!-- Кнопка влево --> <core:ZoomPanImage HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
<ColumnDefinition Width="*" /> <!-- Изображения --> ImageSource="{Binding Image.Image}"
<ColumnDefinition Width="Auto" /> <!-- Кнопка вправо --> Background="Black"
</Grid.ColumnDefinitions> IsVisible="{Binding Image.IsLoading, Converter={x:Static BoolConverters.Not}}" />
<!-- Кнопка влево --> <!-- Оверлей загрузки -->
<Button <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding Image.IsLoading}">
Grid.Column="0" <ProgressBar IsIndeterminate="True" />
VerticalAlignment="Stretch" <TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
HorizontalContentAlignment="Center" </StackPanel>
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding ImagesSectionPreviousCommand}">
<TextBlock Text="◀"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="ImagesSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<!-- Изображения -->
<core:SectionPanel
Grid.Column="1"
CanScrollBackward="{Binding ImagesSectionCanScrollBackward}"
CanScrollForward="{Binding ImagesSectionCanScrollForward}"
ChildrenSource="{Binding Images}"
ClipToBounds="True"
CurrentIndex="{Binding ImagesSectionCurrentIndex}"
Cyclic="True"
IsScrolling="{Binding ImagesSectionIsScrolling}"
Orientation="Horizontal">
<core:SectionPanel.ChildrenTemplate>
<DataTemplate>
<Grid ColumnSpacing="5" RowSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Изображения -->
<ColumnDefinition Width="Auto" /> <!-- Информация -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Изображения -->
<RowDefinition Height="Auto" /> <!-- ИД (подпись) -->
<RowDefinition Height="Auto" /> <!-- ИД (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность видимого (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность видимого (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 365 нм (поле) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (подпись) -->
<RowDefinition Height="Auto" /> <!-- Интенсивность УФ 254 нм (поле) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<!-- Изображения -->
<Grid Grid.Row="0" Grid.RowSpan="12" Grid.Column="0">
<core:ZoomPanImage
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
x:DataType="core_vm:AsyncImageRecordViewModel"
ImageSource="{Binding Image}"
IsVisible="{Binding IsLoading, Converter={x:Static BoolConverters.Not}}" />
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" x:DataType="core_vm:AsyncImageRecordViewModel" IsVisible="{Binding IsLoading}">
<ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="Изображение загружается" />
</StackPanel>
</Grid>
<!-- Информация -->
<!-- ИД (подпись) -->
<TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД: " />
<!-- ИД (поле) -->
<TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Id}" />
<!-- Интенсивность видимого (подпись) -->
<TextBlock Grid.Row="3" Grid.Column="1" Classes="mini" Text="Интенсивность видимого: " />
<!-- Интенсивность видимого (поле) -->
<TextBox Grid.Row="4" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.VisibleIntensity}" />
<!-- Интенсивность УФ 365 нм (подпись) -->
<TextBlock Grid.Row="5" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 365 нм: " />
<!-- Интенсивность УФ 365 нм (поле) -->
<TextBox Grid.Row="6" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv365Intensity}" />
<!-- Интенсивность УФ 254 нм (подпись) -->
<TextBlock Grid.Row="7" Grid.Column="1" Classes="mini" Text="Интенсивность УФ 254 нм: " />
<!-- Интенсивность УФ 254 нм (поле) -->
<TextBox Grid.Row="8" Grid.Column="1" Classes="singleline" IsReadOnly="True" x:DataType="core_vm:AsyncImageRecordViewModel" Text="{Binding ImageRecord.Uv254Intensity}" />
</Grid>
</DataTemplate>
</core:SectionPanel.ChildrenTemplate>
</core:SectionPanel>
<!-- Кнопка вправо -->
<Button
Grid.Column="2"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="vertical"
Command="{Binding ImagesSectionNextCommand}">
<TextBlock Text="▶"/>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="ImagesSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
</Button>
</Grid> </Grid>
<TextBlock <!-- Оверлей "Изображения отсутствуют" -->
HorizontalAlignment="Center" <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="Изображения отсутствуют">
VerticalAlignment="Center"
Text="Изображения сепаратора отсутствуют">
<TextBlock.IsVisible> <TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="Image" />
<Binding
Converter="{StaticResource EqualsConverter}"
ConverterParameter="0"
Path="Images.Count" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</TextBlock.IsVisible> </TextBlock.IsVisible>
</TextBlock> </TextBlock>
<!-- Оверлей прогресс -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}"> <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" IsVisible="{Binding ImagesIsCapturing}">
<ProgressBar IsIndeterminate="True" /> <ProgressBar IsIndeterminate="True" />
<TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" /> <TextBlock HorizontalAlignment="Center" Text="{Binding ImagesCapturingProgress}" />
@@ -311,20 +187,18 @@
</Grid> </Grid>
<!-- Кнопка "получить изображения" --> <!-- Кнопка "Получить изображения" -->
<Button <Button Grid.Row="1"
Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}" Command="{Binding CaptureImageCommand}"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}"> IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Text="Получить изображения"/> <TextBlock Text="Получить изображения"/>
</Button> </Button>
<Button <!-- Кнопка "Отменить съёмку" -->
Grid.Row="1" <Button Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}" Command="{Binding CancelImageCapturingCommand}"
Content="Отменить съёмку"
IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{Binding CaptureImagesTask, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Text="Отменить съёмку"/> <TextBlock Text="Отменить съёмку"/>
</Button> </Button>
@@ -345,9 +219,7 @@
</Button> </Button>
<!-- К информации --> <!-- К информации -->
<Button <Button Grid.Row="1" Grid.Column="2"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}" Command="{Binding SwitchMainSectionCommand}"
Content="К информации" Content="К информации"
@@ -364,16 +236,14 @@
</Button> </Button>
<!-- К изображениям --> <!-- К изображению -->
<Button <Button Grid.Row="1" Grid.Column="2"
Grid.Row="1"
Grid.Column="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding SwitchMainSectionCommand}" Command="{Binding SwitchMainSectionCommand}"
Content="К изображениям" Content="К изображению"
IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}"> IsVisible="{Binding MainSectionCurrentIndex, Converter={StaticResource EqualsConverter}, ConverterParameter=0}">
<TextBlock Text="К изображениям"/> <TextBlock Text="К изображению"/>
<Button.IsEnabled> <Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
@@ -396,13 +266,9 @@
</Border> </Border>
<!-- Выезжающая боковая панель --> <!-- Выезжающая боковая панель -->
<Border <Border HorizontalAlignment="Left"
HorizontalAlignment="Left" Background="{StaticResource MenuBackground}" BorderBrush="{StaticResource MenuBorder}" BorderThickness="0,0,1,0"
Background="{StaticResource MenuBackground}" IsEnabled="{Binding MenuOpened}" IsVisible="{Binding MenuOpened}">
BorderBrush="{StaticResource MenuBorder}"
BorderThickness="0,0,1,0"
IsEnabled="{Binding MenuOpened}"
IsVisible="{Binding MenuOpened}">
<Border.Transitions> <Border.Transitions>
<Transitions> <Transitions>
@@ -412,22 +278,12 @@
<Interaction.Behaviors> <Interaction.Behaviors>
<DataTriggerBehavior <DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="True">
Binding="{Binding MenuOpened}"
ComparisonCondition="Equal"
Value="True">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" /> <ChangePropertyAction PropertyName="RenderTransform" Value="translateX(0px)" />
</DataTriggerBehavior> </DataTriggerBehavior>
<DataTriggerBehavior <DataTriggerBehavior Binding="{Binding MenuOpened}" ComparisonCondition="Equal" Value="False">
Binding="{Binding MenuOpened}"
ComparisonCondition="Equal"
Value="False">
<ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" /> <ChangePropertyAction PropertyName="RenderTransform" Value="translateX(-320px)" />
</DataTriggerBehavior> </DataTriggerBehavior>
</Interaction.Behaviors> </Interaction.Behaviors>
@@ -440,14 +296,11 @@
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button Grid.Row="0" <Button Grid.Row="0" MinWidth="200" HorizontalAlignment="Stretch" Command="{Binding CreateRecordCommand}">
MinWidth="200"
HorizontalAlignment="Stretch"
Command="{Binding CreateRecordCommand}">
<TextBlock Text="Новая запись"/> <TextBlock Text="Новая запись"/>
</Button> </Button>
<ListBox Grid.Row="1" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}"> <ListBox Grid.Row="1" Background="{StaticResource MenuBackground}" ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
+11 -25
View File
@@ -56,48 +56,36 @@
<Grid ColumnSpacing="5"> <Grid ColumnSpacing="5">
<Grid.ColumnDefinitions> <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"/> <!-- Камера --> <ColumnDefinition Width="*" MinWidth="200"/> <!-- Камера -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Системная страница --> <ColumnDefinition Width="*" MinWidth="200"/> <!-- Система -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Начальная страница -->
<Button Grid.Column="0" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="0">
<TextBlock Classes="menu-text" Text="Начальная страница"/>
</Button>
<!-- Анализ --> <!-- Анализ -->
<Button Grid.Column="1" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="1"> <Button Grid.Column="0" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="0">
<TextBlock Classes="menu-text" Text="Анализ"/> <TextBlock Classes="menu-text" Text="Анализ"/>
</Button> </Button>
<!-- Результаты -->
<Button Grid.Column="2" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="2">
<TextBlock Classes="menu-text" Text="Результаты"/>
</Button>
<!-- Калибровка проб --> <!-- Калибровка проб -->
<Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3"> <Button Grid.Column="1" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="1">
<TextBlock Classes="menu-text" Text="Калибровка проб"/> <TextBlock Classes="menu-text" Text="Калибровка проб"/>
</Button> </Button>
<!-- Калибровка сепаратора --> <!-- Калибровка сепараторов -->
<Button Grid.Column="4" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="4"> <Button Grid.Column="2" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="2">
<TextBlock Classes="menu-text" Text="Калибровка сепаратора"/> <TextBlock Classes="menu-text" Text="Калибровка сепараторов"/>
</Button> </Button>
<!-- Камера --> <!-- Камера -->
<Button Grid.Column="5" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="5"> <Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3">
<TextBlock Classes="menu-text" Text="Камера"/> <TextBlock Classes="menu-text" Text="Камера"/>
</Button> </Button>
<!-- Системная страница --> <!-- Система -->
<Button Grid.Column="6" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="6"> <Button Grid.Column="4" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="4">
<TextBlock Classes="menu-text" Text="Системная страница"/> <TextBlock Classes="menu-text" Text="Система"/>
</Button> </Button>
</Grid> </Grid>
@@ -105,13 +93,11 @@
<!-- Основной контент --> <!-- Основной контент -->
<core:SectionPanel Grid.Row="1" ClipToBounds="True" CurrentIndex="{Binding CurrentIndex}" Orientation="Horizontal" IsScrolling="{Binding IsScrolling}"> <core:SectionPanel Grid.Row="1" ClipToBounds="True" CurrentIndex="{Binding CurrentIndex}" Orientation="Horizontal" IsScrolling="{Binding IsScrolling}">
<ContentControl Content="{Binding Start}"/> <!-- Начальная страница -->
<ContentControl Content="{Binding Analysis}"/> <!-- Анализ --> <ContentControl Content="{Binding Analysis}"/> <!-- Анализ -->
<ContentControl Content="{Binding Results}"/> <!-- Результаты -->
<ContentControl Content="{Binding SampleCalibration}"/> <!-- Калибровка проб --> <ContentControl Content="{Binding SampleCalibration}"/> <!-- Калибровка проб -->
<ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Калибровка сепаратора --> <ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Калибровка сепаратора -->
<ContentControl Content="{Binding Camera}"/> <!-- Камера --> <ContentControl Content="{Binding Camera}"/> <!-- Камера -->
<ContentControl Content="{Binding System}"/> <!-- Системная страница --> <ContentControl Content="{Binding System}"/> <!-- Система -->
</core:SectionPanel> </core:SectionPanel>
</Grid> </Grid>
+64
View File
@@ -0,0 +1,64 @@
<UserControl
x:Class="GSS2.Views.DeleteConfirmView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels"
x:DataType="vm:DeleteConfirmViewModel">
<UserControl.Resources>
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonDangerBackground" Color="#b20808" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<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="TextBlock">
<Setter Property="FontSize" Value="20"/>
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UserControl.Styles>
<Grid Background="{StaticResource MainBackground}">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" RowSpacing="5" ColumnSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" Text="{Binding Question}"/>
<Button Grid.Row="1" Grid.Column="1" Classes="danger" Command="{Binding DeleteCommand}">
<TextBlock Text="Удалить"/>
</Button>
<Button Grid.Row="1" Grid.Column="2" Command="{Binding CloseCommand}">
<TextBlock Text="Отменить"/>
</Button>
</Grid>
</Grid>
</UserControl>
+11
View File
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace GSS2.Views;
public partial class DeleteConfirmView : UserControl
{
public DeleteConfirmView()
{
InitializeComponent();
}
}
+1
View File
@@ -7,5 +7,6 @@ public partial class MainView : UserControl
public MainView() public MainView()
{ {
InitializeComponent(); InitializeComponent();
Loaded += (_, _) => (DataContext as GSS2.ViewModels.MainViewModel)?.NavigateToAmineContentAnalysis();
} }
} }
+129
View File
@@ -0,0 +1,129 @@
<UserControl
x:Class="GSS2.Views.NumericKeyboardView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels"
x:DataType="vm:NumericKeyboardViewModel"
Value="{Binding Value, Mode=TwoWay}">
<UserControl.Resources>
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#004a94" />
<Setter Property="Foreground" Value="#eef7ff" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#004a94" />
<Setter Property="Foreground" Value="#eef7ff" />
</Style>
<!-- Стиль для поля ввода -->
<Style Selector="TextPresenter">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="CaretBrush" Value="#004a94" />
</Style>
<!-- Стиль обёртки TextPresenter -->
<Style Selector="Border.DisplayBorder">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="#eef7ff" />
<Setter Property="CornerRadius" Value="5" />
</Style>
</UserControl.Styles>
<Grid Background="{StaticResource MainBackground}">
<Grid Margin="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Classes="DisplayBorder">
<TextPresenter x:Name="Text" FontSize="20" Foreground="#004a94" />
</Border>
<Button Grid.Column="1" Click="Backspace" Content="⌫" />
</Grid>
<Grid Grid.Row="1" Grid.RowSpan="4" ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Click="KeyPressed" Content="1" />
<Button Grid.Row="0" Grid.Column="1" Click="KeyPressed" Content="2" />
<Button Grid.Row="0" Grid.Column="2" Click="KeyPressed" Content="3" />
<Button Grid.Row="1" Grid.Column="0" Click="KeyPressed" Content="4" />
<Button Grid.Row="1" Grid.Column="1" Click="KeyPressed" Content="5" />
<Button Grid.Row="1" Grid.Column="2" Click="KeyPressed" Content="6" />
<Button Grid.Row="2" Grid.Column="0" Click="KeyPressed" Content="7" />
<Button Grid.Row="2" Grid.Column="1" Click="KeyPressed" Content="8" />
<Button Grid.Row="2" Grid.Column="2" Click="KeyPressed" Content="9" />
<Button Grid.Row="3" Grid.Column="0" Click="KeyPressed" Content="-" />
<Button Grid.Row="3" Grid.Column="1" Click="KeyPressed" Content="0" />
<Button Grid.Row="3" Grid.Column="2" Click="KeyPressed" Content="," />
</Grid>
<Grid Grid.Row="5" ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Click="MoveCursorLeft" Content="◀" />
<Button Grid.Column="1" Click="Accept" Command="{Binding AcceptCommand}" Content="Применить" />
<Button Grid.Column="2" Click="Cancel" Command="{Binding CancelCommand}" Content="Отменить" />
<Button Grid.Column="3" Click="MoveCursorRight" Content="▶" />
</Grid>
</Grid>
</Grid>
</UserControl>
+128
View File
@@ -0,0 +1,128 @@
using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Interactivity;
namespace GSS2.Views;
public partial class NumericKeyboardView : UserControl
{
public static readonly StyledProperty<double?> ValueProperty = AvaloniaProperty.Register<NumericKeyboardView, double?>(nameof(Value));
public double? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly RoutedEvent<RoutedEventArgs> ClosedEvent = RoutedEvent.Register<NumericKeyboardView, RoutedEventArgs>(nameof(KeyboardClosed), RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> KeyboardClosed
{
add => AddHandler(ClosedEvent, value);
remove => RemoveHandler(ClosedEvent, value);
}
private readonly TextPresenter? _presenter;
private StringBuilder _text = new();
private bool changingValue = false;
private int _cursor = 0;
public NumericKeyboardView()
{
InitializeComponent();
ValueProperty.Changed.AddClassHandler<NumericKeyboardView>((sender, value) =>
{
if (sender != this)
return;
if (changingValue)
return;
if (value.NewValue is not double doubleValue)
{
changingValue = true;
_text = new();
_cursor = _text.Length;
changingValue = false;
UpdateDisplay();
return;
}
changingValue = true;
_text.Clear();
_text.Insert(0, doubleValue.ToString("G6").Replace(".", ","));
_cursor = _text.Length;
changingValue = false;
UpdateDisplay();
});
_presenter = this.FindControl<TextPresenter>("Text");
if (Value is not null)
_text.Insert(0, Value.Value.ToString("G6").Replace(".", ","));
_cursor = _text.Length;
_presenter?.ShowCaret();
UpdateDisplay();
}
private void KeyPressed(object? sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.Content is not string symbol) return;
if (symbol == "," && _cursor == 0)
return;
if (symbol == "," && _text.ToString().Contains(','))
{
var index = _text.ToString().IndexOf(",");
_text.Remove(index, 1);
if (index < _cursor)
_cursor--;
}
_text.Insert(_cursor, symbol);
_cursor++;
UpdateDisplay();
}
private void Backspace(object? sender, RoutedEventArgs e)
{
if (_cursor > 0)
{
_cursor--;
_text.Remove(_cursor, 1);
UpdateDisplay();
}
}
private void MoveCursorLeft(object? sender, RoutedEventArgs e)
{
_cursor = Math.Max(0, _cursor - 1);
UpdateDisplay();
}
private void MoveCursorRight(object? sender, RoutedEventArgs e)
{
_cursor = Math.Min(_text.Length, _cursor + 1);
UpdateDisplay();
}
private void Accept(object? sender, RoutedEventArgs e)
{
if (double.TryParse(_text.ToString().Replace(",", "."), out double value))
Value = value;
}
private void Cancel(object? sender, RoutedEventArgs e) => Value = null;
private void UpdateDisplay()
{
if (_presenter is null) return;
_presenter.Text = _text.ToString();
_presenter.CaretIndex = _cursor;
_presenter.InvalidateVisual();
if (double.TryParse(_text.ToString().Replace(",", "."), out double value))
Value = value;
}
}
+422
View File
@@ -0,0 +1,422 @@
<UserControl
x:Class="GSS2.Views.TextKeyboardView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels"
x:DataType="vm:TextKeyboardViewModel"
Value="{Binding Value, Mode=TwoWay}"
IsMultiline="{Binding IsMultiline}">
<UserControl.Resources>
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#004a94" />
<Setter Property="Foreground" Value="#eef7ff" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#004a94" />
<Setter Property="Foreground" Value="#eef7ff" />
</Style>
<!-- Стиль для поля ввода -->
<Style Selector="TextPresenter">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="Focusable" Value="False" />
<Setter Property="CaretBrush" Value="#004a94" />
</Style>
<!-- Стиль обёртки TextPresenter -->
<Style Selector="Border.DisplayBorder">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background" Value="#eef7ff" />
<Setter Property="CornerRadius" Value="5" />
</Style>
</UserControl.Styles>
<Grid Background="{StaticResource MainBackground}">
<Grid Margin="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Classes="DisplayBorder" ClipToBounds="True" >
<TextPresenter x:Name="Text" Foreground="#004a94"
FontSize="20"
TextAlignment="Center" VerticalAlignment="Bottom" HorizontalAlignment="Stretch"
Margin="0,0,0,24"/>
</Border>
<Button Grid.Column="1" Click="Backspace" Content="⌫" />
</Grid>
<Grid x:Name="RussianLowercase" Grid.Row="1" Grid.RowSpan="4" ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Click="KeyPressed" Content="1" />
<Button Grid.Row="0" Grid.Column="1" Click="KeyPressed" Content="2" />
<Button Grid.Row="0" Grid.Column="2" Click="KeyPressed" Content="3" />
<Button Grid.Row="0" Grid.Column="3" Click="KeyPressed" Content="4" />
<Button Grid.Row="0" Grid.Column="4" Click="KeyPressed" Content="5" />
<Button Grid.Row="0" Grid.Column="5" Click="KeyPressed" Content="6" />
<Button Grid.Row="0" Grid.Column="6" Click="KeyPressed" Content="7" />
<Button Grid.Row="0" Grid.Column="7" Click="KeyPressed" Content="8" />
<Button Grid.Row="0" Grid.Column="8" Click="KeyPressed" Content="9" />
<Button Grid.Row="0" Grid.Column="9" Click="KeyPressed" Content="0" />
<Button Grid.Row="0" Grid.Column="10" Click="KeyPressed" Content="-" />
<Button Grid.Row="0" Grid.Column="11" Click="KeyPressed" Content="=" />
<Button Grid.Row="1" Grid.Column="0" Click="KeyPressed" Content="й" />
<Button Grid.Row="1" Grid.Column="1" Click="KeyPressed" Content="ц" />
<Button Grid.Row="1" Grid.Column="2" Click="KeyPressed" Content="у" />
<Button Grid.Row="1" Grid.Column="3" Click="KeyPressed" Content="к" />
<Button Grid.Row="1" Grid.Column="4" Click="KeyPressed" Content="е" />
<Button Grid.Row="1" Grid.Column="5" Click="KeyPressed" Content="н" />
<Button Grid.Row="1" Grid.Column="6" Click="KeyPressed" Content="г" />
<Button Grid.Row="1" Grid.Column="7" Click="KeyPressed" Content="ш" />
<Button Grid.Row="1" Grid.Column="8" Click="KeyPressed" Content="щ" />
<Button Grid.Row="1" Grid.Column="9" Click="KeyPressed" Content="з" />
<Button Grid.Row="1" Grid.Column="10" Click="KeyPressed" Content="х" />
<Button Grid.Row="1" Grid.Column="11" Click="KeyPressed" Content="ъ" />
<Button Grid.Row="2" Grid.Column="0" Click="SwitchCase" Content="⇧" FontSize="20" />
<Button Grid.Row="2" Grid.Column="1" Click="KeyPressed" Content="ф" />
<Button Grid.Row="2" Grid.Column="2" Click="KeyPressed" Content="ы" />
<Button Grid.Row="2" Grid.Column="3" Click="KeyPressed" Content="в" />
<Button Grid.Row="2" Grid.Column="4" Click="KeyPressed" Content="а" />
<Button Grid.Row="2" Grid.Column="5" Click="KeyPressed" Content="п" />
<Button Grid.Row="2" Grid.Column="6" Click="KeyPressed" Content="р" />
<Button Grid.Row="2" Grid.Column="7" Click="KeyPressed" Content="о" />
<Button Grid.Row="2" Grid.Column="8" Click="KeyPressed" Content="л" />
<Button Grid.Row="2" Grid.Column="9" Click="KeyPressed" Content="д" />
<Button Grid.Row="2" Grid.Column="10" Click="KeyPressed" Content="ж" />
<Button Grid.Row="2" Grid.Column="11" Click="KeyPressed" Content="э" />
<Button Grid.Row="3" Grid.Column="0" Click="SwitchLagnuage" Content="АНГ" />
<Button Grid.Row="3" Grid.Column="1" Click="KeyPressed" Content="я" />
<Button Grid.Row="3" Grid.Column="2" Click="KeyPressed" Content="ч" />
<Button Grid.Row="3" Grid.Column="3" Click="KeyPressed" Content="с" />
<Button Grid.Row="3" Grid.Column="4" Click="KeyPressed" Content="м" />
<Button Grid.Row="3" Grid.Column="5" Click="KeyPressed" Content="и" />
<Button Grid.Row="3" Grid.Column="6" Click="KeyPressed" Content="т" />
<Button Grid.Row="3" Grid.Column="7" Click="KeyPressed" Content="ь" />
<Button Grid.Row="3" Grid.Column="8" Click="KeyPressed" Content="б" />
<Button Grid.Row="3" Grid.Column="9" Click="KeyPressed" Content="ю" />
<Button Grid.Row="3" Grid.Column="10" Click="KeyPressed" Content="." />
<Button x:Name="EnterRussianLowercase" Grid.Row="3" Grid.Column="11" Click="EnterKeyPressed" Content="⏎" FontSize="20" />
</Grid>
<Grid x:Name="RussianUppercase" Grid.Row="1" Grid.RowSpan="4" ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Click="KeyPressed" Content="!" />
<Button Grid.Row="0" Grid.Column="1" Click="KeyPressed" Content="&quot;" />
<Button Grid.Row="0" Grid.Column="2" Click="KeyPressed" Content="№" />
<Button Grid.Row="0" Grid.Column="3" Click="KeyPressed" Content=";" />
<Button Grid.Row="0" Grid.Column="4" Click="KeyPressed" Content="%" />
<Button Grid.Row="0" Grid.Column="5" Click="KeyPressed" Content=":" />
<Button Grid.Row="0" Grid.Column="6" Click="KeyPressed" Content="?" />
<Button Grid.Row="0" Grid.Column="7" Click="KeyPressed" Content="*" />
<Button Grid.Row="0" Grid.Column="8" Click="KeyPressed" Content="(" />
<Button Grid.Row="0" Grid.Column="9" Click="KeyPressed" Content=")" />
<Button Grid.Row="0" Grid.Column="10" Click="KeyPressed" Content="_" />
<Button Grid.Row="0" Grid.Column="11" Click="KeyPressed" Content="+" />
<Button Grid.Row="1" Grid.Column="0" Click="KeyPressed" Content="Й" />
<Button Grid.Row="1" Grid.Column="1" Click="KeyPressed" Content="Ц" />
<Button Grid.Row="1" Grid.Column="2" Click="KeyPressed" Content="У" />
<Button Grid.Row="1" Grid.Column="3" Click="KeyPressed" Content="К" />
<Button Grid.Row="1" Grid.Column="4" Click="KeyPressed" Content="Е" />
<Button Grid.Row="1" Grid.Column="5" Click="KeyPressed" Content="Н" />
<Button Grid.Row="1" Grid.Column="6" Click="KeyPressed" Content="Г" />
<Button Grid.Row="1" Grid.Column="7" Click="KeyPressed" Content="Ш" />
<Button Grid.Row="1" Grid.Column="8" Click="KeyPressed" Content="Щ" />
<Button Grid.Row="1" Grid.Column="9" Click="KeyPressed" Content="З" />
<Button Grid.Row="1" Grid.Column="10" Click="KeyPressed" Content="Х" />
<Button Grid.Row="1" Grid.Column="11" Click="KeyPressed" Content="Ъ" />
<Button Grid.Row="2" Grid.Column="0" Click="SwitchCase" Content="⇩" FontSize="20" />
<Button Grid.Row="2" Grid.Column="1" Click="KeyPressed" Content="Ф" />
<Button Grid.Row="2" Grid.Column="2" Click="KeyPressed" Content="Ы" />
<Button Grid.Row="2" Grid.Column="3" Click="KeyPressed" Content="В" />
<Button Grid.Row="2" Grid.Column="4" Click="KeyPressed" Content="А" />
<Button Grid.Row="2" Grid.Column="5" Click="KeyPressed" Content="П" />
<Button Grid.Row="2" Grid.Column="6" Click="KeyPressed" Content="Р" />
<Button Grid.Row="2" Grid.Column="7" Click="KeyPressed" Content="О" />
<Button Grid.Row="2" Grid.Column="8" Click="KeyPressed" Content="Л" />
<Button Grid.Row="2" Grid.Column="9" Click="KeyPressed" Content="Д" />
<Button Grid.Row="2" Grid.Column="10" Click="KeyPressed" Content="Ж" />
<Button Grid.Row="2" Grid.Column="11" Click="KeyPressed" Content="Э" />
<Button Grid.Row="3" Grid.Column="0" Click="SwitchLagnuage" Content="АНГ" />
<Button Grid.Row="3" Grid.Column="1" Click="KeyPressed" Content="Я" />
<Button Grid.Row="3" Grid.Column="2" Click="KeyPressed" Content="Ч" />
<Button Grid.Row="3" Grid.Column="3" Click="KeyPressed" Content="С" />
<Button Grid.Row="3" Grid.Column="4" Click="KeyPressed" Content="М" />
<Button Grid.Row="3" Grid.Column="5" Click="KeyPressed" Content="И" />
<Button Grid.Row="3" Grid.Column="6" Click="KeyPressed" Content="Т" />
<Button Grid.Row="3" Grid.Column="7" Click="KeyPressed" Content="Ь" />
<Button Grid.Row="3" Grid.Column="8" Click="KeyPressed" Content="Б" />
<Button Grid.Row="3" Grid.Column="9" Click="KeyPressed" Content="Ю" />
<Button Grid.Row="3" Grid.Column="10" Click="KeyPressed" Content="," />
<Button x:Name="EnterRussianUppercase" Grid.Row="3" Grid.Column="11" Click="EnterKeyPressed" Content="⏎" FontSize="20" />
</Grid>
<Grid x:Name="EnglishLowercase" Grid.Row="1" Grid.RowSpan="4" ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Click="KeyPressed" Content="1" />
<Button Grid.Row="0" Grid.Column="1" Click="KeyPressed" Content="2" />
<Button Grid.Row="0" Grid.Column="2" Click="KeyPressed" Content="3" />
<Button Grid.Row="0" Grid.Column="3" Click="KeyPressed" Content="4" />
<Button Grid.Row="0" Grid.Column="4" Click="KeyPressed" Content="5" />
<Button Grid.Row="0" Grid.Column="5" Click="KeyPressed" Content="6" />
<Button Grid.Row="0" Grid.Column="6" Click="KeyPressed" Content="7" />
<Button Grid.Row="0" Grid.Column="7" Click="KeyPressed" Content="8" />
<Button Grid.Row="0" Grid.Column="8" Click="KeyPressed" Content="9" />
<Button Grid.Row="0" Grid.Column="9" Click="KeyPressed" Content="0" />
<Button Grid.Row="0" Grid.Column="10" Click="KeyPressed" Content="-" />
<Button Grid.Row="0" Grid.Column="11" Click="KeyPressed" Content="=" />
<Button Grid.Row="1" Grid.Column="0" Click="KeyPressed" Content="q" />
<Button Grid.Row="1" Grid.Column="1" Click="KeyPressed" Content="w" />
<Button Grid.Row="1" Grid.Column="2" Click="KeyPressed" Content="e" />
<Button Grid.Row="1" Grid.Column="3" Click="KeyPressed" Content="r" />
<Button Grid.Row="1" Grid.Column="4" Click="KeyPressed" Content="t" />
<Button Grid.Row="1" Grid.Column="5" Click="KeyPressed" Content="y" />
<Button Grid.Row="1" Grid.Column="6" Click="KeyPressed" Content="u" />
<Button Grid.Row="1" Grid.Column="7" Click="KeyPressed" Content="i" />
<Button Grid.Row="1" Grid.Column="8" Click="KeyPressed" Content="o" />
<Button Grid.Row="1" Grid.Column="9" Click="KeyPressed" Content="p" />
<Button Grid.Row="1" Grid.Column="10" Click="KeyPressed" Content="[" />
<Button Grid.Row="1" Grid.Column="11" Click="KeyPressed" Content="]" />
<Button Grid.Row="2" Grid.Column="0" Click="SwitchCase" Content="⇧" FontSize="20" />
<Button Grid.Row="2" Grid.Column="1" Click="KeyPressed" Content="a" />
<Button Grid.Row="2" Grid.Column="2" Click="KeyPressed" Content="s" />
<Button Grid.Row="2" Grid.Column="3" Click="KeyPressed" Content="d" />
<Button Grid.Row="2" Grid.Column="4" Click="KeyPressed" Content="f" />
<Button Grid.Row="2" Grid.Column="5" Click="KeyPressed" Content="g" />
<Button Grid.Row="2" Grid.Column="6" Click="KeyPressed" Content="h" />
<Button Grid.Row="2" Grid.Column="7" Click="KeyPressed" Content="j" />
<Button Grid.Row="2" Grid.Column="8" Click="KeyPressed" Content="k" />
<Button Grid.Row="2" Grid.Column="9" Click="KeyPressed" Content="l" />
<Button Grid.Row="2" Grid.Column="10" Click="KeyPressed" Content=";" />
<Button Grid.Row="2" Grid.Column="11" Click="KeyPressed" Content="'" />
<Button Grid.Row="3" Grid.Column="0" Click="SwitchLagnuage" Content="РУС" />
<Button Grid.Row="3" Grid.Column="1" Click="KeyPressed" Content="z" />
<Button Grid.Row="3" Grid.Column="2" Click="KeyPressed" Content="x" />
<Button Grid.Row="3" Grid.Column="3" Click="KeyPressed" Content="c" />
<Button Grid.Row="3" Grid.Column="4" Click="KeyPressed" Content="v" />
<Button Grid.Row="3" Grid.Column="5" Click="KeyPressed" Content="b" />
<Button Grid.Row="3" Grid.Column="6" Click="KeyPressed" Content="n" />
<Button Grid.Row="3" Grid.Column="7" Click="KeyPressed" Content="m" />
<Button Grid.Row="3" Grid.Column="8" Click="KeyPressed" Content="," />
<Button Grid.Row="3" Grid.Column="9" Click="KeyPressed" Content="." />
<Button Grid.Row="3" Grid.Column="10" Click="KeyPressed" Content="/" />
<Button x:Name="EnterEnglishLowercase" Grid.Row="3" Grid.Column="11" Click="EnterKeyPressed" Content="⏎" FontSize="20" />
</Grid>
<Grid x:Name="EnglishUppercase" Grid.Row="1" Grid.RowSpan="4" ColumnSpacing="5" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Click="KeyPressed" Content="!" />
<Button Grid.Row="0" Grid.Column="1" Click="KeyPressed" Content="@" />
<Button Grid.Row="0" Grid.Column="2" Click="KeyPressed" Content="#" />
<Button Grid.Row="0" Grid.Column="3" Click="KeyPressed" Content="$" />
<Button Grid.Row="0" Grid.Column="4" Click="KeyPressed" Content="%" />
<Button Grid.Row="0" Grid.Column="5" Click="KeyPressed" Content="^" />
<Button Grid.Row="0" Grid.Column="6" Click="KeyPressed" Content="&amp;" />
<Button Grid.Row="0" Grid.Column="7" Click="KeyPressed" Content="*" />
<Button Grid.Row="0" Grid.Column="8" Click="KeyPressed" Content="(" />
<Button Grid.Row="0" Grid.Column="9" Click="KeyPressed" Content=")" />
<Button Grid.Row="0" Grid.Column="10" Click="KeyPressed" Content="_" />
<Button Grid.Row="0" Grid.Column="11" Click="KeyPressed" Content="+" />
<Button Grid.Row="1" Grid.Column="0" Click="KeyPressed" Content="Q" />
<Button Grid.Row="1" Grid.Column="1" Click="KeyPressed" Content="W" />
<Button Grid.Row="1" Grid.Column="2" Click="KeyPressed" Content="E" />
<Button Grid.Row="1" Grid.Column="3" Click="KeyPressed" Content="R" />
<Button Grid.Row="1" Grid.Column="4" Click="KeyPressed" Content="T" />
<Button Grid.Row="1" Grid.Column="5" Click="KeyPressed" Content="Y" />
<Button Grid.Row="1" Grid.Column="6" Click="KeyPressed" Content="U" />
<Button Grid.Row="1" Grid.Column="7" Click="KeyPressed" Content="I" />
<Button Grid.Row="1" Grid.Column="8" Click="KeyPressed" Content="O" />
<Button Grid.Row="1" Grid.Column="9" Click="KeyPressed" Content="P" />
<Button Grid.Row="1" Grid.Column="10" Click="KeyPressed" Content="\{" />
<Button Grid.Row="1" Grid.Column="11" Click="KeyPressed" Content="}" />
<Button Grid.Row="2" Grid.Column="0" Click="SwitchCase" Content="⇩" FontSize="20" />
<Button Grid.Row="2" Grid.Column="1" Click="KeyPressed" Content="A" />
<Button Grid.Row="2" Grid.Column="2" Click="KeyPressed" Content="S" />
<Button Grid.Row="2" Grid.Column="3" Click="KeyPressed" Content="D" />
<Button Grid.Row="2" Grid.Column="4" Click="KeyPressed" Content="F" />
<Button Grid.Row="2" Grid.Column="5" Click="KeyPressed" Content="G" />
<Button Grid.Row="2" Grid.Column="6" Click="KeyPressed" Content="H" />
<Button Grid.Row="2" Grid.Column="7" Click="KeyPressed" Content="J" />
<Button Grid.Row="2" Grid.Column="8" Click="KeyPressed" Content="K" />
<Button Grid.Row="2" Grid.Column="9" Click="KeyPressed" Content="L" />
<Button Grid.Row="2" Grid.Column="10" Click="KeyPressed" Content=":" />
<Button Grid.Row="2" Grid.Column="11" Click="KeyPressed" Content="&quot;" />
<Button Grid.Row="3" Grid.Column="0" Click="SwitchLagnuage" Content="РУС" />
<Button Grid.Row="3" Grid.Column="1" Click="KeyPressed" Content="Z" />
<Button Grid.Row="3" Grid.Column="2" Click="KeyPressed" Content="X" />
<Button Grid.Row="3" Grid.Column="3" Click="KeyPressed" Content="C" />
<Button Grid.Row="3" Grid.Column="4" Click="KeyPressed" Content="V" />
<Button Grid.Row="3" Grid.Column="5" Click="KeyPressed" Content="B" />
<Button Grid.Row="3" Grid.Column="6" Click="KeyPressed" Content="N" />
<Button Grid.Row="3" Grid.Column="7" Click="KeyPressed" Content="M" />
<Button Grid.Row="3" Grid.Column="8" Click="KeyPressed" Content="&lt;" />
<Button Grid.Row="3" Grid.Column="9" Click="KeyPressed" Content=">" />
<Button Grid.Row="3" Grid.Column="10" Click="KeyPressed" Content="?" />
<Button x:Name="EnterEnglishUppercase" Grid.Row="3" Grid.Column="11" Click="EnterKeyPressed" Content="⏎" FontSize="20" />
</Grid>
<!-- Нижние кнопки управления -->
<Grid Grid.Row="5" ColumnDefinitions="*,*,*,*" ColumnSpacing="5">
<Button Grid.Column="0" Click="MoveCursorLeft" Content="◀" />
<Button Grid.Column="1" Click="Accept" Command="{Binding AcceptCommand}" Content="Применить" />
<Button Grid.Column="2" Click="Cancel" Command="{Binding CancelCommand}" Content="Отменить" />
<Button Grid.Column="3" Click="MoveCursorRight" Content="▶" />
</Grid>
</Grid>
</Grid>
</UserControl>
+172
View File
@@ -0,0 +1,172 @@
using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Interactivity;
namespace GSS2.Views;
public partial class TextKeyboardView : UserControl
{
public static readonly StyledProperty<string?> ValueProperty = AvaloniaProperty.Register<NumericKeyboardView, string?>(nameof(Value));
public string? Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly StyledProperty<bool> IsMultilineProperty = AvaloniaProperty.Register<NumericKeyboardView, bool>(nameof(IsMultiline), defaultValue: false);
public bool IsMultiline
{
get => GetValue(IsMultilineProperty);
set => SetValue(IsMultilineProperty, value);
}
public static readonly RoutedEvent<RoutedEventArgs> ClosedEvent = RoutedEvent.Register<NumericKeyboardView, RoutedEventArgs>(nameof(KeyboardClosed), RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> KeyboardClosed
{
add => AddHandler(ClosedEvent, value);
remove => RemoveHandler(ClosedEvent, value);
}
private readonly TextPresenter? _presenter;
private StringBuilder _text = new();
private bool _changingValue = false;
private int _cursor = 0;
private bool _isUppercase = false;
private bool _isEnglish = false;
public TextKeyboardView(string? value = null)
{
InitializeComponent();
ValueProperty.Changed.AddClassHandler<TextKeyboardView>((sender, value) =>
{
if (sender != this)
return;
if (_changingValue)
return;
if (value.NewValue is not string strValue)
{
_changingValue = true;
_text = new();
_cursor = _text.Length;
_changingValue = false;
UpdateDisplay();
return;
}
_changingValue = true;
_text.Clear();
_text.Insert(0, strValue);
_cursor = _text.Length;
_changingValue = false;
UpdateDisplay();
});
IsMultilineProperty.Changed.AddClassHandler<TextKeyboardView>((sender, value) =>
{
if (sender != this)
return;
if (value.NewValue is not bool boolValue)
return;
EnterRussianLowercase.IsVisible = boolValue;
EnterRussianUppercase.IsVisible = boolValue;
EnterEnglishLowercase.IsVisible = boolValue;
EnterEnglishUppercase.IsVisible = boolValue;
});
_presenter = this.FindControl<TextPresenter>("Text");
if (!string.IsNullOrEmpty(value))
_text.Append(value);
_cursor = _text.Length;
_presenter?.ShowCaret();
UpdateDisplay();
RussianLowercase.IsVisible = !_isUppercase && !_isEnglish;
RussianUppercase.IsVisible = _isUppercase && !_isEnglish;
EnglishLowercase.IsVisible = !_isUppercase && _isEnglish;
EnglishUppercase.IsVisible = _isUppercase && _isEnglish;
EnterRussianLowercase.IsVisible = IsMultiline;
EnterRussianUppercase.IsVisible = IsMultiline;
EnterEnglishLowercase.IsVisible = IsMultiline;
EnterEnglishUppercase.IsVisible = IsMultiline;
}
private void KeyPressed(object? sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.Content is not string symbol) return;
_text.Insert(_cursor, symbol);
_cursor++;
UpdateDisplay();
}
private void EnterKeyPressed(object? sender, RoutedEventArgs e)
{
_text.Insert(_cursor, "\n");
_cursor++;
UpdateDisplay();
}
private void Backspace(object? sender, RoutedEventArgs e)
{
if (_cursor > 0)
{
_cursor--;
_text.Remove(_cursor, 1);
UpdateDisplay();
}
}
private void SwitchCase(object? sender, RoutedEventArgs e)
{
_isUppercase = !_isUppercase;
RussianLowercase.IsVisible = !_isUppercase && !_isEnglish;
RussianUppercase.IsVisible = _isUppercase && !_isEnglish;
EnglishLowercase.IsVisible = !_isUppercase && _isEnglish;
EnglishUppercase.IsVisible = _isUppercase && _isEnglish;
}
private void SwitchLagnuage(object? sender, RoutedEventArgs e)
{
_isEnglish = !_isEnglish;
RussianLowercase.IsVisible = !_isUppercase && !_isEnglish;
RussianUppercase.IsVisible = _isUppercase && !_isEnglish;
EnglishLowercase.IsVisible = !_isUppercase && _isEnglish;
EnglishUppercase.IsVisible = _isUppercase && _isEnglish;
}
private void MoveCursorLeft(object? sender, RoutedEventArgs e)
{
_cursor = Math.Max(0, _cursor - 1);
UpdateDisplay();
}
private void MoveCursorRight(object? sender, RoutedEventArgs e)
{
_cursor = Math.Min(_text.Length, _cursor + 1);
UpdateDisplay();
}
private void Accept(object? sender, RoutedEventArgs e)
{
Value = _text.ToString();
}
private void Cancel(object? sender, RoutedEventArgs e) => Value = null;
private void UpdateDisplay()
{
if (_presenter is null)
return;
_presenter.Text = _text.ToString();
_presenter.CaretIndex = _cursor;
_presenter.InvalidateVisual();
Value = _text.ToString();
}
}
+14 -14
View File
@@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.Core", "GSS2.Core\GSS2.Core.csproj", "{C691BFCC-A6EF-4331-B7C4-E264441BEE0D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.Core", "GSS2.Core\GSS2.Core.csproj", "{C691BFCC-A6EF-4331-B7C4-E264441BEE0D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.Test", "GSS2.Test\GSS2.Test.csproj", "{94F2F407-23DC-4945-AE67-B33C540DEF28}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GSS2.LightsControl", "GSS2.LightsControl", "{B8CA8CAF-03B6-2A4B-B2EF-758B3B2ADED2}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GSS2.LightsControl", "GSS2.LightsControl", "{B8CA8CAF-03B6-2A4B-B2EF-758B3B2ADED2}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSharpClient", "CSharpClient", "{478CE71E-3653-8F33-D0F9-CF70B2F74279}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CSharpClient", "CSharpClient", "{478CE71E-3653-8F33-D0F9-CF70B2F74279}"
@@ -17,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.FrameDecoders", "GSS2.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.UI.Core", "GSS2.UI.Core\GSS2.UI.Core.csproj", "{52424CF4-88F4-440A-AA37-21F5A080AA16}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.UI.Core", "GSS2.UI.Core\GSS2.UI.Core.csproj", "{52424CF4-88F4-440A-AA37-21F5A080AA16}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2", "GSS2\GSS2.csproj", "{F25580D6-0573-4765-A904-A4FB53D2D755}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -39,18 +39,6 @@ Global
{C691BFCC-A6EF-4331-B7C4-E264441BEE0D}.Release|x64.Build.0 = Release|Any CPU {C691BFCC-A6EF-4331-B7C4-E264441BEE0D}.Release|x64.Build.0 = Release|Any CPU
{C691BFCC-A6EF-4331-B7C4-E264441BEE0D}.Release|x86.ActiveCfg = Release|Any CPU {C691BFCC-A6EF-4331-B7C4-E264441BEE0D}.Release|x86.ActiveCfg = Release|Any CPU
{C691BFCC-A6EF-4331-B7C4-E264441BEE0D}.Release|x86.Build.0 = Release|Any CPU {C691BFCC-A6EF-4331-B7C4-E264441BEE0D}.Release|x86.Build.0 = Release|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Debug|x64.ActiveCfg = Debug|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Debug|x64.Build.0 = Debug|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Debug|x86.ActiveCfg = Debug|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Debug|x86.Build.0 = Debug|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Release|Any CPU.Build.0 = Release|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Release|x64.ActiveCfg = Release|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Release|x64.Build.0 = Release|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Release|x86.ActiveCfg = Release|Any CPU
{94F2F407-23DC-4945-AE67-B33C540DEF28}.Release|x86.Build.0 = Release|Any CPU
{D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Debug|x64.ActiveCfg = Debug|Any CPU {D0130B90-6ECA-49CA-A126-EFEFF481FC74}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -87,6 +75,18 @@ Global
{52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x64.Build.0 = Release|Any CPU {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x64.Build.0 = Release|Any CPU
{52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x86.ActiveCfg = Release|Any CPU {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x86.ActiveCfg = Release|Any CPU
{52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x86.Build.0 = Release|Any CPU {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x86.Build.0 = Release|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Debug|x64.ActiveCfg = Debug|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Debug|x64.Build.0 = Debug|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Debug|x86.ActiveCfg = Debug|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Debug|x86.Build.0 = Debug|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Release|Any CPU.Build.0 = Release|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Release|x64.ActiveCfg = Release|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Release|x64.Build.0 = Release|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Release|x86.ActiveCfg = Release|Any CPU
{F25580D6-0573-4765-A904-A4FB53D2D755}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE