diff --git a/Assemblies/RawDataHandler-Vendor-UnSupported.1.2.9082.378.nupkg b/Assemblies/RawDataHandler-Vendor-UnSupported.1.2.9082.378.nupkg deleted file mode 100644 index 76763bfc1..000000000 Binary files a/Assemblies/RawDataHandler-Vendor-UnSupported.1.2.9082.378.nupkg and /dev/null differ diff --git a/Assemblies/RawDataHandler-Vendor-UnSupported.1.3.9699.471.nupkg b/Assemblies/RawDataHandler-Vendor-UnSupported.1.3.9699.471.nupkg new file mode 100644 index 000000000..1ad2e3149 Binary files /dev/null and b/Assemblies/RawDataHandler-Vendor-UnSupported.1.3.9699.471.nupkg differ diff --git a/src/Common/CommonStandard/DataStructure/ConcurrentLruCache.cs b/src/Common/CommonStandard/DataStructure/ConcurrentLruCache.cs new file mode 100644 index 000000000..9ff8649ae --- /dev/null +++ b/src/Common/CommonStandard/DataStructure/ConcurrentLruCache.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Threading; + +namespace CompMs.Common.DataStructure; + +public sealed class ConcurrentLruCache +{ + private readonly LruCache _cache; + private readonly ReaderWriterLockSlim _locker; + + public ConcurrentLruCache(int capacity, IEqualityComparer comparer) { + _cache = new LruCache(capacity, comparer); + _locker = new(); + } + + public ConcurrentLruCache(int capacity) { + _cache = new LruCache(capacity); + _locker = new(); + } + + public TValue Get(TKey key) { + _locker.EnterReadLock(); + try { + return _cache.Get(key); + } + finally { + _locker.ExitReadLock(); + } + } + + public void Put(TKey key, TValue value) { + _locker.EnterWriteLock(); + try { + _cache.Put(key, value); + } + finally { + _locker.ExitWriteLock(); + } + } + + public bool ContainsKey(TKey key) { + _locker.EnterReadLock(); + try { + return _cache.ContainsKey(key); + } + finally { + _locker.ExitReadLock(); + } + } + + public bool TryGet(TKey key, out TValue value) { + _locker.EnterReadLock(); + try { + if (_cache.ContainsKey(key)) { + value = _cache.Get(key); + return true; + } + value = default; + return false; + } + finally { + _locker.ExitReadLock(); + } + } +} diff --git a/src/MSDIAL5/MsdialCore/Enum/SupportFormat.cs b/src/MSDIAL5/MsdialCore/Enum/SupportFormat.cs index c79881782..9239b14fb 100644 --- a/src/MSDIAL5/MsdialCore/Enum/SupportFormat.cs +++ b/src/MSDIAL5/MsdialCore/Enum/SupportFormat.cs @@ -3,6 +3,6 @@ using System.Text; namespace CompMs.MsdialCore.Enum { - public enum SupportMsRawDataExtension { abf, ibf, cdf, mzml, wiff, raw, d, wiff2, qgd, lcd, lrp } + public enum SupportMsRawDataExtension { abf, ibf, cdf, mzml, wiff, raw, d, wiff2, qgd, lcd, lrp, imzml } public enum MsdialDataStorageFormat { dcl, pai, arf, aef, msf, bfasta, bpep, spep, prf, rtc } } diff --git a/src/MSDIAL5/MsdialCore/MsdialCore.csproj b/src/MSDIAL5/MsdialCore/MsdialCore.csproj index 8776eabea..aa06f4e98 100644 --- a/src/MSDIAL5/MsdialCore/MsdialCore.csproj +++ b/src/MSDIAL5/MsdialCore/MsdialCore.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/src/MSDIAL5/MsdialDimsCore/Algorithm/DimsDataProvider.cs b/src/MSDIAL5/MsdialDimsCore/Algorithm/DimsDataProvider.cs index 81ef1389f..c620f9149 100644 --- a/src/MSDIAL5/MsdialDimsCore/Algorithm/DimsDataProvider.cs +++ b/src/MSDIAL5/MsdialDimsCore/Algorithm/DimsDataProvider.cs @@ -152,6 +152,82 @@ public int BinningMz(double mz) { } } + public class DimsAccumulateDataProvider : BaseDataProvider + { + public DimsAccumulateDataProvider(IEnumerable spectrums, double massTolerance, double timeBegin, double timeEnd) + : base(AccumulateRawSpectrums( + spectrums + .Where(spec => timeBegin <= spec.ScanStartTime && spec.ScanStartTime <= timeEnd) + .Select(spec => spec.ShallowCopy()) + .ToArray(), + massTolerance)) { + + } + + public DimsAccumulateDataProvider(RawMeasurement rawObj, double massTolerance, double timeBegin, double timeEnd) + : this(rawObj.SpectrumList, massTolerance, timeBegin, timeEnd) { + + } + + public DimsAccumulateDataProvider(AnalysisFileBean file, double massTolerance, double timeBegin, double timeEnd, bool isGuiProcess = false, int retry = 5) + : this(LoadMeasurement(file, true, false, isGuiProcess, retry).SpectrumList, massTolerance, timeBegin, timeEnd) { + + } + + + private static List AccumulateRawSpectrums(RawSpectrum[] spectrums, double massTolerance) { + var targetmz4ppmcalc = 500.0; + var binning = new Binning(targetmz4ppmcalc, massTolerance); + var ms1Spectrums = spectrums.Where(spectrum => spectrum.MsLevel == 1).ToList(); + var groups = ms1Spectrums.SelectMany(spectrum => spectrum.Spectrum) + .GroupBy(peak => binning.BinningMz(peak.Mz)); + var massBins = new Dictionary(); + foreach (var group in groups) { + var peaks = group.ToList(); + var accIntensity = peaks.Sum(peak => peak.Intensity); + var basepeak = peaks.Argmax(peak => peak.Intensity); + massBins[group.Key] = new double[] { basepeak.Mz, accIntensity, basepeak.Intensity }; + } + var result = ms1Spectrums.First().ShallowCopy(); + SpectrumParser.setSpectrumProperties(result, massBins, isSortMz: true); + var results = new[] { result }.Concat(spectrums.Where(spectrum => spectrum.MsLevel != 1)).ToList(); + for (int i = 0; i < results.Count; i++) { + results[i].Index = i; + } + return results; + } + + class Binning + { + public double Pivot { get; } + public double Tolerance { get; } + public double Ppm { get; } + public double LogTolerance { get; } + + public Binning(double pivot, double tolerance) { + Pivot = pivot; + Tolerance = tolerance; + Ppm = tolerance / Pivot * 1_000_000d; + LogTolerance = Math.Log(1 + Ppm / 1_000_000d); + } + + /// + /// Calculates the bin index for a given m/z value. + /// + /// The mass-to-charge (m/z) value. + /// The bin index as an integer. + public int BinningMz(double mz) { + if (mz <= Pivot) { + return (int)(mz / Tolerance); + } + else { + int pivotbin = (int)(Pivot / Tolerance) + 1; + return pivotbin + (int)(Math.Log(mz / Pivot) / LogTolerance); + } + } + } + } + public class DimsBpiDataProviderFactory : IDataProviderFactory, IDataProviderFactory { @@ -238,4 +314,35 @@ public IDataProvider Create(RawMeasurement source) { return new DimsAverageDataProvider(source, massTolerance, timeBegin, timeEnd); } } + + public class DimsAccumulateDataProviderFactory + : IDataProviderFactory, IDataProviderFactory + { + private readonly double massTolerance; + private readonly double timeBegin; + private readonly double timeEnd; + private readonly int retry; + private readonly bool isGuiProcess; + + public DimsAccumulateDataProviderFactory( + double massTolerance, + double timeBegin = double.MinValue, + double timeEnd = double.MaxValue, + int retry = 5, + bool isGuiProcess = false) { + this.massTolerance = massTolerance; + this.timeBegin = timeBegin; + this.timeEnd = timeEnd; + this.retry = retry; + this.isGuiProcess = isGuiProcess; + } + + public IDataProvider Create(AnalysisFileBean source) { + return new DimsAccumulateDataProvider(source, massTolerance, timeBegin, timeEnd, isGuiProcess, retry); + } + + public IDataProvider Create(RawMeasurement source) { + return new DimsAccumulateDataProvider(source, massTolerance, timeBegin, timeEnd); + } + } } diff --git a/src/MSDIAL5/MsdialDimsCore/Parameter/DimsProviderFactoryParameter.cs b/src/MSDIAL5/MsdialDimsCore/Parameter/DimsProviderFactoryParameter.cs index 3db66edb3..fd19ad14e 100644 --- a/src/MSDIAL5/MsdialDimsCore/Parameter/DimsProviderFactoryParameter.cs +++ b/src/MSDIAL5/MsdialDimsCore/Parameter/DimsProviderFactoryParameter.cs @@ -10,6 +10,7 @@ namespace CompMs.MsdialDimsCore.Parameter [Union(0, typeof(DimsBpiDataProviderFactoryParameter))] [Union(1, typeof(DimsTicDataProviderFactoryParameter))] [Union(2, typeof(DimsAverageDataProviderFactoryParameter))] + [Union(3, typeof(DimsAccumulateDataProviderFactoryParameter))] public interface IDimsDataProviderFactoryParameter { IDataProviderFactory Create(int retry, bool isGuiProcess); @@ -84,4 +85,29 @@ public IDataProviderFactory Create() { return new DimsAverageDataProviderFactory(MassTolerance, TimeBegin, TimeEnd); } } + + [MessagePackObject] + public class DimsAccumulateDataProviderFactoryParameter: IDimsDataProviderFactoryParameter + { + public DimsAccumulateDataProviderFactoryParameter(double timeBegin, double timeEnd, double massTolerance) { + TimeBegin = timeBegin; + TimeEnd = timeEnd; + MassTolerance = massTolerance; + } + + [Key(nameof(TimeBegin))] + public double TimeBegin { get; } + [Key(nameof(TimeEnd))] + public double TimeEnd { get; } + [Key(nameof(MassTolerance))] + public double MassTolerance { get; } + + public IDataProviderFactory Create(int retry, bool isGuiProcess) { + return new DimsAccumulateDataProviderFactory(MassTolerance, TimeBegin, TimeEnd, retry, isGuiProcess); + } + + public IDataProviderFactory Create() { + return new DimsAccumulateDataProviderFactory(MassTolerance, TimeBegin, TimeEnd); + } + } } diff --git a/src/MSDIAL5/MsdialDimsCore/ProcessFile.cs b/src/MSDIAL5/MsdialDimsCore/ProcessFile.cs index ab1542999..15f6ec90e 100644 --- a/src/MSDIAL5/MsdialDimsCore/ProcessFile.cs +++ b/src/MSDIAL5/MsdialDimsCore/ProcessFile.cs @@ -79,7 +79,7 @@ public async Task RunAsync(AnalysisFileBean file, ProcessOption option, IProgres // faeture detections Console.WriteLine("Peak picking started"); var ms1Spectrum = provider.LoadMs1Spectrums().Argmax(spec => spec.Spectrum.Length); - var chromPeaks = DataAccess.ConvertRawPeakElementToChromatogramPeakList(ms1Spectrum.Spectrum); + var chromPeaks = DataAccess.ConvertRawPeakElementToChromatogramPeakList(ms1Spectrum.Spectrum, param.MassRangeBegin, param.MassRangeEnd); var sChromPeaks = new Chromatogram(chromPeaks, ChromXType.Mz, ChromXUnit.Mz).ChromatogramSmoothing(param.SmoothingMethod, param.SmoothingLevel).AsPeakArray(); var peakPickResults = PeakDetection.PeakDetectionVS1(sChromPeaks, param.MinimumDatapoints, param.MinimumAmplitude); diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Chart/BitmapImageModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Chart/BitmapImageModel.cs index f2d0055ac..ae49044b5 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Chart/BitmapImageModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Chart/BitmapImageModel.cs @@ -1,5 +1,7 @@ using CompMs.CommonMVVM; using System; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -9,8 +11,9 @@ internal sealed class BitmapImageModel : BindableBase { public static readonly int ImageMargin = 1; + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); + private Func? _bitmapSourceFactory; - private BitmapSource? _bitmapSource; public BitmapImageModel(BitmapSource bitmapSource, string title) { _bitmapSource = bitmapSource; @@ -22,8 +25,39 @@ public BitmapImageModel(Func bitmapSourceFactory, string title) { Title = title; } - public string Title { get; } - public BitmapSource BitmapSource => _bitmapSource ?? _bitmapSourceFactory!.Invoke(); + public string Title { get; internal set; } + + public BitmapSource? BitmapSource { + get => _bitmapSource; + private set => SetProperty(ref _bitmapSource, value); + } + private BitmapSource? _bitmapSource; + + public bool LoadingBitmap { + get => _loadingBitmap; + private set => SetProperty(ref _loadingBitmap, value); + } + private bool _loadingBitmap = false; + + public async Task EnsureBitmapSourceAsync() { + if (_bitmapSource is not null || LoadingBitmap) { + return; + } + + await _semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try { + if (_bitmapSource is not null || LoadingBitmap) { + return; + } + LoadingBitmap = true; + BitmapSource = await Task.Run(() => _bitmapSourceFactory!.Invoke()).ConfigureAwait(false); + LoadingBitmap = false; + } + finally { + _semaphoreSlim.Release(); + } + } public BitmapImageModel WithPalette(BitmapPalette palette) { if (_bitmapSource is null) { diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Dims/DimsDataCollectionSettingModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Dims/DimsDataCollectionSettingModel.cs index 54f162e3d..a302e44e8 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Dims/DimsDataCollectionSettingModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Dims/DimsDataCollectionSettingModel.cs @@ -33,6 +33,12 @@ public bool UseAverageMs1 { } private bool useAverageMs1 = false; + public bool UseAccumulateMs1 { + get => useAccumulateMs1; + set => SetProperty(ref useAccumulateMs1, value); + } + private bool useAccumulateMs1 = false; + public double TimeBegin { get => timeBegin; set => SetProperty(ref timeBegin, value); @@ -65,6 +71,9 @@ public IDimsDataProviderFactoryParameter CreateDataProviderFactoryParameter() { else if (UseAverageMs1) { return new DimsAverageDataProviderFactoryParameter(TimeBegin, TimeEnd, MassTolerance); } + else if (UseAccumulateMs1) { + return new DimsAccumulateDataProviderFactoryParameter(TimeBegin, TimeEnd, MassTolerance); + } throw new NotSupportedException(); } @@ -92,6 +101,13 @@ private void PrepareProviderFactoryParameter(IDimsDataProviderFactoryParameter f TimeEnd = averageParameter.TimeEnd; MassTolerance = averageParameter.MassTolerance; break; + case DimsAccumulateDataProviderFactoryParameter accumulateParameter: + UseMs1WithHighestTic = false; + UseAccumulateMs1 = true; + TimeBegin = accumulateParameter.TimeBegin; + TimeEnd = accumulateParameter.TimeEnd; + MassTolerance = accumulateParameter.MassTolerance; + break; } } } diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IWholeImageResultModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IWholeImageResultModel.cs index 3228c91ea..ca9f56f7c 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IWholeImageResultModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IWholeImageResultModel.cs @@ -2,5 +2,5 @@ internal interface IWholeImageResultModel { - public IntensityImageModel? SelectedPeakIntensities { get; } + public IntensityImagePlaceholderModel IntensityImagePlaceholder { get; } } diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/ImagingRoiModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/ImagingRoiModel.cs index 02be664b8..3eb44218b 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/ImagingRoiModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/ImagingRoiModel.cs @@ -50,8 +50,8 @@ public void Select() { IsSelected = true; } - public async Task SavePositionsAsync(CancellationToken token = default) { - using var stream = File.Open($"{Id}.csv", FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + public async Task SavePositionsAsync(string directoryPath, CancellationToken token = default) { + using var stream = File.Open(Path.Combine(directoryPath, $"{Id}.csv"), FileMode.Create, FileAccess.Write, FileShare.ReadWrite); var header = string.Join(",", ["XIndex", "YIndex", "Pos"]); var encoded = UTF8Encoding.Default.GetBytes(header + "\n"); await stream.WriteAsync(encoded, 0, encoded.Length).ConfigureAwait(false); diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImageModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImageModel.cs index 3450892f6..77d5c4f2a 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImageModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImageModel.cs @@ -17,7 +17,7 @@ internal sealed class IntensityImageModel : BindableBase { private readonly MaldiFrames _frameInfos; private readonly RawIntensityOnPixelsLoader _intensitiesLoader; - private readonly int _peakIndex; + internal readonly int _peakIndex; public IntensityImageModel(MaldiFrames frameInfos, ChromatogramPeakFeatureModel peak, MaldiFrameLaserInfo laserInfo, RawIntensityOnPixelsLoader intensitiesLoader, int peakIndex) { _frameInfos = frameInfos; @@ -42,7 +42,7 @@ public BitmapImageModel BitmapImageModel { var factory = () => { var image = new byte[width * stride * height]; - var pixels = _intensitiesLoader.Load(_peakIndex); + var pixels = _intensitiesLoader.LoadAsync(_peakIndex).Result; var features = pixels.PixelPeakFeaturesList[0]; var sorted = features.IntensityArray.OrderBy(x => x).ToArray(); var length = sorted.Length; @@ -64,7 +64,7 @@ public async Task SaveAsync(Stream stream, bool skipUnknownPeaks = true, System. if (skipUnknownPeaks && string.IsNullOrEmpty(Peak.Name)) { return; } - var pixels = _intensitiesLoader.Load(_peakIndex); + var pixels = await _intensitiesLoader.LoadAsync(_peakIndex, token).ConfigureAwait(false); var row = string.Format("{0},{1},{2},{3},", Peak.MasterPeakID, Peak.Name, Mz.Value, Drift.Value) + string.Join(",", pixels.PixelPeakFeaturesList[0].IntensityArray); var encoded = UTF8Encoding.Default.GetBytes(row + "\n"); await stream.WriteAsync(encoded, 0, encoded.Length, token).ConfigureAwait(false); diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImagePlaceholderModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImagePlaceholderModel.cs new file mode 100644 index 000000000..aeaa7a852 --- /dev/null +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/IntensityImagePlaceholderModel.cs @@ -0,0 +1,76 @@ +using CompMs.App.Msdial.Common; +using CompMs.App.Msdial.Model.Chart; +using CompMs.Common.DataStructure; +using CompMs.CommonMVVM; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace CompMs.App.Msdial.Model.Imaging; + +internal sealed class IntensityImagePlaceholderModel : BindableBase +{ + private readonly ConcurrentLruCache _cache; + private readonly MaldiFrames _frameInfos; + private readonly RawIntensityOnPixelsLoader _intensitiesLoader; + + public IntensityImagePlaceholderModel(MaldiFrames frameInfos, RawIntensityOnPixelsLoader intensitiesLoader) { + _cache = new ConcurrentLruCache(32); + _frameInfos = frameInfos; + _intensitiesLoader = intensitiesLoader; + } + + public BitmapImageModel? CurrentImage { + get => _currentImage; + private set => SetProperty(ref _currentImage, value); + } + private BitmapImageModel? _currentImage = null; + + public void ResetImage() { + CurrentImage = null; + } + + public async Task EnsureImageAsync(int index, string title, CancellationToken token = default) { + if (_cache.TryGet(index, out var imageModel)) { + if (imageModel.Title != title) { + imageModel.Title = title; + } + CurrentImage = imageModel; + return; + } + + var xmin = BitmapImageModel.WithMarginToPoint(_frameInfos.XIndexPosMin); + var ymin = BitmapImageModel.WithMarginToPoint(_frameInfos.YIndexPosMin); + var width = BitmapImageModel.WithMarginToLength(_frameInfos.XIndexWidth); + var height = BitmapImageModel.WithMarginToLength(_frameInfos.YIndexHeight); + var pf = PixelFormats.Indexed8; + var stride = (pf.BitsPerPixel + 7) / 8; + + token.ThrowIfCancellationRequested(); + + var image = new byte[width * stride * height]; + var pixels = await _intensitiesLoader.LoadAsync(index, token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + var features = pixels.PixelPeakFeaturesList[0]; + var sorted = features.IntensityArray.OrderBy(x => x).ToArray(); + var length = sorted.Length; + var zmin = sorted.DefaultIfEmpty(0d).First(); + var zmax = sorted.DefaultIfEmpty(255).ElementAt((int)(length * .99)); + if (zmin == zmax) { + zmax = zmin + 1; + } + foreach (var (intensity, info) in features.IntensityArray.Zip(_frameInfos.Infos, (x, y) => (x, y))) { + image[width * stride * (info.YIndexPos - ymin) + (info.XIndexPos - xmin) * stride] = (byte)Math.Max(1, Math.Min((intensity - zmin) / (zmax - zmin), 1d) * 255); + } + + token.ThrowIfCancellationRequested(); + + imageModel = BitmapImageModel.Create(image, width, height, pf, Colormaps.Viridis, title); + _cache.Put(index, imageModel); + CurrentImage = imageModel; + } +} diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RawIntensityOnPixelsLoader.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RawIntensityOnPixelsLoader.cs index 3592f1357..900f08497 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RawIntensityOnPixelsLoader.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RawIntensityOnPixelsLoader.cs @@ -2,6 +2,8 @@ using CompMs.Common.DataObj; using CompMs.RawDataHandler.Core; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace CompMs.App.Msdial.Model.Imaging; @@ -10,17 +12,17 @@ internal sealed class RawIntensityOnPixelsLoader private readonly List _targets; private readonly AnalysisFileBeanModel _file; private readonly MaldiFrames _frames; + private readonly RawDataAccess _rawDataAccess; public RawIntensityOnPixelsLoader(List targets, AnalysisFileBeanModel file, MaldiFrames frames) { _targets = targets; _file = file; _frames = frames; + _rawDataAccess = new RawDataAccess(_file.AnalysisFilePath, 0, getProfileData: true, isImagingMsData: true, isGuiProcess: true) { DriftToleranceForPixelData = .1d }; } - - public RawSpectraOnPixels Load(int index) { - using RawDataAccess rawDataAccess = new RawDataAccess(_file.AnalysisFilePath, 0, getProfileData: true, isImagingMsData: true, isGuiProcess: true) { DriftToleranceForPixelData = .1d }; - return rawDataAccess.GetRawPixelFeature(_targets, index, [.. _frames.Infos], isNewProcess: false); + public async Task LoadAsync(int index, CancellationToken token = default) { + return await _rawDataAccess.GetRawPixelFeatureAsync(_targets, index, [.. _frames.Infos], isNewProcess: false, token: token).ConfigureAwait(false); } } diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiAccess.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiAccess.cs index bfc446ab0..99ad87d1d 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiAccess.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiAccess.cs @@ -28,6 +28,9 @@ public RoiAccess(RoiModel subset, RoiModel? master) } public T[] Access(T[] array) { + if (array.Length == _idxs.Count) { + return array; + } var result = new T[_idxs.Count]; for (int i = 0; i < _idxs.Count; i++) { result[i] = array[_idxs[i]]; diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiModel.cs index d20f9610d..de8b5fab1 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiModel.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -66,9 +68,9 @@ public RoiModel(AnalysisFileBeanModel file, int id, MaldiFrames frames, Color co public Color Color { get; } public BitmapImageModel RoiImage { get; } - public RawSpectraOnPixels RetrieveRawSpectraOnPixels(List targetElements) { + public async Task RetrieveRawSpectraOnPixelsAsync(List targetElements, CancellationToken token = default) { using RawDataAccess rawDataAccess = new RawDataAccess(File.AnalysisFilePath, 0, true, true, true); - return rawDataAccess.GetRawPixelFeatures(targetElements, [.. Frames.Infos], isNewProcess: false) + return await rawDataAccess.GetRawPixelFeaturesAsync(targetElements, [.. Frames.Infos], isNewProcess: false, token).ConfigureAwait(false) ?? new RawSpectraOnPixels { PixelPeakFeaturesList = new List(0), XYFrames = new List(0), }; } diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiPeakSummaryModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiPeakSummaryModel.cs index a5286c32b..85d35a2eb 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiPeakSummaryModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/RoiPeakSummaryModel.cs @@ -1,6 +1,7 @@ using CompMs.App.Msdial.Model.DataObj; using CompMs.CommonMVVM; using System.Linq; +using System.Threading.Tasks; namespace CompMs.App.Msdial.Model.Imaging { @@ -19,18 +20,27 @@ public RoiPeakSummaryModel(RoiAccess access, ChromatogramPeakFeatureModel peak, public ChromatogramPeakFeatureModel Peak { get; } - public double AccumulatedIntensity { - get { - if (_accumulatedIntensity is null) { - CalculateAccumulatedIntensity(); - } - return _accumulatedIntensity ?? 0d; - } + public double? AccumulatedIntensity { + get => _accumulatedIntensity; + private set => SetProperty(ref _accumulatedIntensity, value); } private double? _accumulatedIntensity = null; - private void CalculateAccumulatedIntensity() { - _accumulatedIntensity = _access.Access(_intensitiesLoader.Load(_peakIndex).PixelPeakFeaturesList[0].IntensityArray).Average(); + public bool IsAccumulatedIntensityLoading { + get => _isAccumulatedIntensityLoading; + private set => SetProperty(ref _isAccumulatedIntensityLoading, value); + } + private bool _isAccumulatedIntensityLoading = false; + + public async Task EnsureCalculateAccumulatedIntensityAsync() { + if (_accumulatedIntensity is not null || _isAccumulatedIntensityLoading) { + return; + } + IsAccumulatedIntensityLoading = true; + var pixelsTask = _intensitiesLoader.LoadAsync(_peakIndex); + var pixels = await pixelsTask.ConfigureAwait(false); + AccumulatedIntensity = _access.Access(pixels.PixelPeakFeaturesList[0].IntensityArray).Average(); + IsAccumulatedIntensityLoading = false; } } } diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/SaveImagesModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/SaveImagesModel.cs index 32110576e..a60bada2a 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Imaging/SaveImagesModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Imaging/SaveImagesModel.cs @@ -1,4 +1,5 @@ using CompMs.CommonMVVM; +using Microsoft.Win32; using System; using System.Collections.Generic; using System.IO; @@ -29,23 +30,36 @@ public string Path public Task SaveAsync(CancellationToken token = default) { - var image = _imageResult.SelectedPeakIntensities?.BitmapImageModel; + string? filePath = Path; + if (string.IsNullOrEmpty(filePath)) { + var dialog = new SaveFileDialog + { + Filter = "PNG Image|*.png|GIF Image|*.gif", + DefaultExt = "png", + FileName = "image.png", + }; + if (dialog.ShowDialog() == true) { + filePath = dialog.FileName; + } + } + if (string.IsNullOrEmpty(filePath)) { + return Task.CompletedTask; + } + + var image = _imageResult.IntensityImagePlaceholder.CurrentImage; if (image is null) { return Task.CompletedTask; } var rois = _roiModels.Where(roi => roi.IsSelected).Select(roi => roi.Roi.RoiImage).ToArray(); - return Task.Run(() => + return Task.Run(async () => { - if (string.IsNullOrEmpty(Path)) { - return; - } BitmapEncoder? encoder = null; - if (Path.EndsWith("png")) + if (filePath.EndsWith("png")) { encoder = new PngBitmapEncoder(); } - else if (Path.EndsWith("gif")) + else if (filePath.EndsWith("gif")) { encoder = new GifBitmapEncoder(); } @@ -53,12 +67,15 @@ public Task SaveAsync(CancellationToken token = default) { return; } + + await Task.WhenAll([image.EnsureBitmapSourceAsync(), .. rois.Select(roi => roi.EnsureBitmapSourceAsync())]).ConfigureAwait(false); + encoder.Frames.Add(BitmapFrame.Create(image.BitmapSource)); foreach (var roi in rois) { encoder.Frames.Add(BitmapFrame.Create(roi.BitmapSource)); } - using (var stream = File.Open(Path, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (var stream = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) { encoder.Save(stream); } diff --git a/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsImageModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsImageModel.cs index 05ff0e4a1..70b18dc53 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsImageModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsImageModel.cs @@ -5,6 +5,7 @@ using CompMs.App.Msdial.Model.Service; using CompMs.Common.DataObj.Result; using CompMs.CommonMVVM; +using CompMs.Graphics.Window; using CompMs.MsdialCore.Algorithm; using CompMs.MsdialCore.Algorithm.Annotation; using CompMs.MsdialCore.DataObj; @@ -77,18 +78,32 @@ public void RemoveRoi(ImagingRoiModel model) { ImagingRoiModels.Remove(model); } - public async Task SaveRoisAsync(CancellationToken token = default) { + public async Task SaveRoisAsync(CancellationToken token = default) { + var dialog = new SelectFolderDialog + { + Title = "Select folder to save ROI positions", + }; + if (dialog.ShowDialog() != DialogResult.OK) { + return false; + } + var folderPath = dialog.SelectedPath!; + var tasks = new List(); foreach (var roi in ImagingRoiModels) { - tasks.Add(roi.SavePositionsAsync(token)); + tasks.Add(roi.SavePositionsAsync(folderPath, token)); } await Task.WhenAll(tasks).ConfigureAwait(false); + return true; } public async Task SaveIntensitiesAsync(CancellationToken token = default) { await Task.WhenAll([SaveRoisAsync(token), ImageResult.SaveIntensitiesAsync(token)]).ConfigureAwait(false); } + public async Task ExportIntensitiesAsync(CancellationToken token = default) { + await ImageResult.SaveIntensitiesAsync(token).ConfigureAwait(false); + } + public void LoadRoi() { string? path = null; var request = new OpenFileRequest(p => path = p) diff --git a/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsMethodModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsMethodModel.cs index c92e79ad5..ee7010eb0 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsMethodModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/ImagingDimsMethodModel.cs @@ -38,7 +38,7 @@ public ImagingDimsMethodModel(AnalysisFileBeanModelCollection analysisFileBeanMo _projectBaseParameter = projectBaseParameter; StudyContext = studyContext; _evaluator = FacadeMatchResultEvaluator.FromDataBases(storage.DataBases); - _providerFactory = new StandardDataProviderFactory().ContraMap((AnalysisFileBean file) => file.LoadRawMeasurement(isImagingMsData: true, isGuiProcess: true, retry: 5, sleepMilliSeconds: 5000)); + _providerFactory = storage.Parameter.ProviderFactoryParameter.Create(retry: 5, isGuiProcess: true); //new StandardDataProviderFactory().ContraMap((AnalysisFileBean file) => file.LoadRawMeasurement(isImagingMsData: true, isGuiProcess: true, retry: 5, sleepMilliSeconds: 5000)); ImageModels = []; Image = ImageModels.FirstOrDefault(); @@ -67,7 +67,8 @@ public override async Task RunAsync(ProcessOption option, CancellationToken toke if (option.HasFlag(ProcessOption.Identification)) { var queryFatoires = _storage.CreateAnnotationQueryFactoryStorage(); var annotationProcess = new StandardAnnotationProcess(queryFatoires.MoleculeQueryFactories, _evaluator, _storage.DataBaseMapper); - var processor = new ProcessFile(_providerFactory, _storage, annotationProcess, _evaluator); + var providerFactory = _storage.Parameter.ProviderFactoryParameter.Create(retry: 5, isGuiProcess: true); + var processor = new ProcessFile(providerFactory, _storage, annotationProcess, _evaluator); var runner = new ProcessRunner(processor, 2); await runner.RunAllAsync(_storage.AnalysisFiles, option, Enumerable.Repeat?>(null, _storage.AnalysisFiles.Count), null, token).ConfigureAwait(false); foreach (var file in files) { @@ -114,7 +115,7 @@ public override Task SaveAsync() return Task.CompletedTask; } - return Image.ImageResult.SaveAsync(); + return Task.WhenAll(ImageModels.Select(image => image.ImageResult.SaveAsync())); } public AnalysisResultExportModel CreateExportAnalysisModel() { diff --git a/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/WholeImageResultModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/WholeImageResultModel.cs index 351e7b324..c0bb3ed2f 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/WholeImageResultModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/ImagingDims/WholeImageResultModel.cs @@ -11,6 +11,7 @@ using CompMs.MsdialCore.DataObj; using CompMs.MsdialDimsCore.Parameter; using CompMs.RawDataHandler.Core; +using Microsoft.Win32; using Reactive.Bindings; using Reactive.Bindings.Extensions; using Reactive.Bindings.Notifiers; @@ -50,9 +51,20 @@ public WholeImageResultModel(AnalysisFileBeanModel file, MaldiFrames maldiFrames MaldiFrameLaserInfo laserInfo = file.File.GetMaldiFrameLaserInfo(); _intensities = new ObservableCollection(analysisModel.Ms1Peaks.Select((peak, index) => new IntensityImageModel(maldiFrames, peak, laserInfo, rawIntensityLoader, index))); Intensities = new ReadOnlyObservableCollection(_intensities); + IntensityImagePlaceholder = new IntensityImagePlaceholderModel(maldiFrames, rawIntensityLoader); analysisModel.Target.Select(p => _intensities.FirstOrDefault(intensity => intensity.Peak == p)) - .Subscribe(intensity => SelectedPeakIntensities = intensity) - .AddTo(Disposables); + .Subscribe(intensity => { + if (intensity is null) { + IntensityImagePlaceholder.ResetImage(); + } + else { + var title = $"m/z {intensity.Mz.Value}"; + if (!string.IsNullOrEmpty(intensity.Peak.Name)) { + title = $"{intensity.Peak.Name}, {title}"; + } + _ = IntensityImagePlaceholder.EnsureImageAsync(intensity._peakIndex, title); + } + }).AddTo(Disposables); _file = file; _maldiFrames = maldiFrames; _wholeRoi = wholeRoi; @@ -68,12 +80,7 @@ public WholeImageResultModel(AnalysisFileBeanModel file, MaldiFrames maldiFrames public ObservableCollection Peaks => AnalysisModel.Ms1Peaks; - public IntensityImageModel? SelectedPeakIntensities - { - get => _selectedPeakIntensities; - set => SetProperty(ref _selectedPeakIntensities, value); - } - private IntensityImageModel? _selectedPeakIntensities; + public IntensityImagePlaceholderModel IntensityImagePlaceholder { get; } public ReactivePropertySlim Target => AnalysisModel.Target; @@ -85,8 +92,21 @@ public ImagingRoiModel CreateImagingRoiModel(RoiModel roi) return result; } - public async Task SaveIntensitiesAsync(CancellationToken token = default) { - using var writer = File.Open("pixel_intensities.csv", FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + public async Task SaveIntensitiesAsync(CancellationToken token = default) { + var filePath = string.Empty; + var dialog = new SaveFileDialog + { + Filter = "Pixel intensity file|*.csv", + DefaultExt = "csv", + FileName = "pixel_intensities.csv", + }; + if (dialog.ShowDialog() == true) { + filePath = dialog.FileName; + } + if (string.IsNullOrEmpty(filePath)) { + return false; + } + using var writer = File.Open(filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); var header = string.Join(",", new[] { "ID", "Name", "m/z", "Drift", }.Concat(_maldiFrames.Infos.Select(info => $"{info.XIndexPos}_{info.YIndexPos}"))); var encoded = UTF8Encoding.Default.GetBytes(header + "\n"); writer.Write(encoded, 0, encoded.Length); @@ -104,6 +124,11 @@ public async Task SaveIntensitiesAsync(CancellationToken token = default) { }, token)); } await Task.WhenAll(tasks).ConfigureAwait(false); + var task = Task.CompletedTask; + await task; + task.Wait(); + + return true; } public void ResetRawSpectraOnPixels() { diff --git a/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsImageModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsImageModel.cs index 42204b0ea..4d2bc66f0 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsImageModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsImageModel.cs @@ -5,6 +5,7 @@ using CompMs.App.Msdial.Model.Service; using CompMs.Common.DataObj.Result; using CompMs.CommonMVVM; +using CompMs.Graphics.Window; using CompMs.MsdialCore.Algorithm; using CompMs.MsdialCore.Algorithm.Annotation; using CompMs.MsdialCore.DataObj; @@ -77,12 +78,22 @@ public void RemoveRoi(ImagingRoiModel model) { ImagingRoiModels.Remove(model); } - public async Task SaveRoisAsync(CancellationToken token = default) { + public async Task SaveRoisAsync(CancellationToken token = default) { + var dialog = new SelectFolderDialog + { + Title = "Select folder to save ROI positions", + }; + if (dialog.ShowDialog() != DialogResult.OK) { + return false; + } + var folderPath = dialog.SelectedPath!; + var tasks = new List(); foreach (var roi in ImagingRoiModels) { - tasks.Add(roi.SavePositionsAsync(token)); + tasks.Add(roi.SavePositionsAsync(folderPath, token)); } await Task.WhenAll(tasks).ConfigureAwait(false); + return true; } public async Task SaveIntensitiesAsync(CancellationToken token = default) { diff --git a/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsMethodModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsMethodModel.cs index 3497495ea..3c1543eb0 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsMethodModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/ImagingImmsMethodModel.cs @@ -109,7 +109,7 @@ public override Task SaveAsync() return Task.CompletedTask; } - return Image.ImageResult.SaveAsync(); + return Task.WhenAll(ImageModels.Select(image => image.ImageResult.SaveAsync())); } public AnalysisResultExportModel CreateExportAnalysisModel() { diff --git a/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/WholeImageResultModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/WholeImageResultModel.cs index 4d8b80b53..399823332 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/WholeImageResultModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/ImagingImms/WholeImageResultModel.cs @@ -50,8 +50,20 @@ public WholeImageResultModel(AnalysisFileBeanModel file, MaldiFrames maldiFrames MaldiFrameLaserInfo laserInfo = file.File.GetMaldiFrameLaserInfo(); _intensities = new ObservableCollection(analysisModel.Ms1Peaks.Select((peak, index) => new IntensityImageModel(maldiFrames, peak, laserInfo, rawIntensityLoader, index))); Intensities = new ReadOnlyObservableCollection(_intensities); + IntensityImagePlaceholder = new IntensityImagePlaceholderModel(maldiFrames, rawIntensityLoader); analysisModel.Target.Select(p => _intensities.FirstOrDefault(intensity => intensity.Peak == p)) - .Subscribe(intensity => SelectedPeakIntensities = intensity) + .Subscribe(intensity => { + if (intensity is null) { + IntensityImagePlaceholder.ResetImage(); + } + else { + var title = $"m/z {intensity.Mz.Value}, Mobility {intensity.Drift.Value} [1/K0]"; + if (!string.IsNullOrEmpty(intensity.Peak.Name)) { + title = $"{intensity.Peak.Name}, {title}"; + } + _ = IntensityImagePlaceholder.EnsureImageAsync(intensity._peakIndex, title); + } + }) .AddTo(Disposables); _file = file; _maldiFrames = maldiFrames; @@ -64,17 +76,12 @@ public WholeImageResultModel(AnalysisFileBeanModel file, MaldiFrames maldiFrames public ReadOnlyObservableCollection Intensities { get; } + public IntensityImagePlaceholderModel IntensityImagePlaceholder { get; } + public AnalysisPeakPlotModel PeakPlotModel => AnalysisModel.PlotModel; public ObservableCollection Peaks => AnalysisModel.Ms1Peaks; - public IntensityImageModel? SelectedPeakIntensities - { - get => _selectedPeakIntensities; - set => SetProperty(ref _selectedPeakIntensities, value); - } - private IntensityImageModel? _selectedPeakIntensities; - public ReactivePropertySlim Target => AnalysisModel.Target; public ImagingRoiModel CreateImagingRoiModel(RoiModel roi) diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Loader/MsDecSpectrumFromFileLoader.cs b/src/MSDIAL5/MsdialGuiApp/Model/Loader/MsDecSpectrumFromFileLoader.cs index bdb31aa5d..5404351a7 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Loader/MsDecSpectrumFromFileLoader.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Loader/MsDecSpectrumFromFileLoader.cs @@ -34,10 +34,15 @@ public MsDecSpectrumFromFileLoader(AnalysisFileBeanModel file) { return null; } var rep = ((IAnnotatedObject)prop).MatchResults.Representative; - if (!rep.IsUnknown && _file.GetMSDecLoader(rep.CollisionEnergy) is { } loader) { - return loader.LoadMSDecResult(prop.MSDecResultID); + try { + if (!rep.IsUnknown && _file.GetMSDecLoader(rep.CollisionEnergy) is { } loader) { + return loader.LoadMSDecResult(prop.MSDecResultID); + } + return _file.MSDecLoader.LoadMSDecResult(prop.MSDecResultID); + } + catch (ArgumentOutOfRangeException) { + return null; } - return _file.MSDecLoader.LoadMSDecResult(prop.MSDecResultID); }); } diff --git a/src/MSDIAL5/MsdialGuiApp/View/Chart/BitmapImageView.xaml b/src/MSDIAL5/MsdialGuiApp/View/Chart/BitmapImageView.xaml index 5c376ae96..40ad7abb4 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Chart/BitmapImageView.xaml +++ b/src/MSDIAL5/MsdialGuiApp/View/Chart/BitmapImageView.xaml @@ -11,7 +11,7 @@ d:DesignHeight="450" d:DesignWidth="800"> - diff --git a/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/DimsImagingRibbon.xaml b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/DimsImagingRibbon.xaml index 6b1626abf..4ae01f80b 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/DimsImagingRibbon.xaml +++ b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/DimsImagingRibbon.xaml @@ -9,6 +9,7 @@ xmlns:vmchart="clr-namespace:CompMs.App.Msdial.ViewModel.Chart" xmlns:chartSetting="clr-namespace:CompMs.App.Msdial.View.ChartSetting" xmlns:ribbon="clr-namespace:CompMs.App.Msdial.View.RibbonControl" + xmlns:diimaging="clr-namespace:CompMs.App.Msdial.View.ImagingDims" d:DataContext="{d:DesignInstance Type={x:Type vm:ImagingDimsMainViewModel}}"> @@ -30,6 +31,7 @@ + @@ -58,5 +60,6 @@ d:DataContext="{d:DesignInstance Type={x:Type vmchart:BarChartViewModel}}" Visibility="{Binding IsFocused.Value, Converter={StaticResource BooleanToVisibility}, FallbackValue=Collapsed}" Style="{StaticResource FocusWhenVisible}"/> + diff --git a/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingMainView.xaml b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingMainView.xaml index 19bcf43d4..b4a11a7f4 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingMainView.xaml +++ b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingMainView.xaml @@ -187,12 +187,12 @@ Padding="5,0" HorizontalAlignment="Center" VerticalAlignment="Bottom"> - + - + @@ -305,14 +305,21 @@ + + @@ -352,6 +359,7 @@ - + @@ -382,7 +390,7 @@ VerticalAlignment="Stretch" HorizontalAlignment="Stretch" IsSelectable="{Binding SelectedImageViewModel.RoiEditViewModel.IsEditable, Mode=OneWay}" - Visibility="{Binding IsChecked, ElementName=SelectorVisibility, Converter={StaticResource BooleanToVisibility}}"/> + Visibility="{Binding SelectedImageViewModel.RoiEditViewModel.IsEditable, Mode=OneWay, Converter={StaticResource BooleanToVisibility}}"/> diff --git a/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingTab.xaml b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingTab.xaml new file mode 100644 index 000000000..543106142 --- /dev/null +++ b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingTab.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingTab.xaml.cs b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingTab.xaml.cs new file mode 100644 index 000000000..1df4c920a --- /dev/null +++ b/src/MSDIAL5/MsdialGuiApp/View/ImagingDims/ImagingTab.xaml.cs @@ -0,0 +1,14 @@ +using System.Windows.Controls.Ribbon; + +namespace CompMs.App.Msdial.View.ImagingDims +{ + /// + /// Interaction logic for ImagingTab.xaml + /// + public partial class ImagingTab : RibbonTab + { + public ImagingTab() { + InitializeComponent(); + } + } +} diff --git a/src/MSDIAL5/MsdialGuiApp/View/ImagingImms/ImagingMainView.xaml b/src/MSDIAL5/MsdialGuiApp/View/ImagingImms/ImagingMainView.xaml index 43a9a2ee5..0acc88094 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/ImagingImms/ImagingMainView.xaml +++ b/src/MSDIAL5/MsdialGuiApp/View/ImagingImms/ImagingMainView.xaml @@ -197,12 +197,12 @@ Padding="5,0" HorizontalAlignment="Center" VerticalAlignment="Bottom"> - + - + @@ -318,14 +318,21 @@ + + @@ -365,6 +372,7 @@ - + diff --git a/src/MSDIAL5/MsdialGuiApp/View/Setting/DataCollectionSettingView.xaml b/src/MSDIAL5/MsdialGuiApp/View/Setting/DataCollectionSettingView.xaml index d04d70dee..37187abae 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Setting/DataCollectionSettingView.xaml +++ b/src/MSDIAL5/MsdialGuiApp/View/Setting/DataCollectionSettingView.xaml @@ -78,7 +78,8 @@ - + + diff --git a/src/MSDIAL5/MsdialGuiApp/View/Setting/DatasetFileSettingView.xaml.cs b/src/MSDIAL5/MsdialGuiApp/View/Setting/DatasetFileSettingView.xaml.cs index ec73e948a..e65b40552 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Setting/DatasetFileSettingView.xaml.cs +++ b/src/MSDIAL5/MsdialGuiApp/View/Setting/DatasetFileSettingView.xaml.cs @@ -18,6 +18,7 @@ public partial class DatasetFileSettingView : UserControl { new FileSelectionItem("Hive HMD file", ".hmd"), new FileSelectionItem("Hive mzB file", ".mzb"), new FileSelectionItem("mzML file", ".mzml"), + new FileSelectionItem("imzML file", ".imzml"), new FileSelectionItem("netCDF file", ".cdf"), new FileSelectionItem("IBF file", ".ibf"), new FileSelectionItem("WIFF file", ".wiff"), diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/Chart/BitmapImageViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/Chart/BitmapImageViewModel.cs index f4f9bcf60..c51dda495 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/Chart/BitmapImageViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/Chart/BitmapImageViewModel.cs @@ -1,5 +1,8 @@ using CompMs.App.Msdial.Model.Chart; using CompMs.CommonMVVM; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using System.Threading.Tasks; using System.Windows.Media.Imaging; namespace CompMs.App.Msdial.ViewModel.Chart @@ -10,9 +13,14 @@ internal sealed class BitmapImageViewModel : ViewModelBase public BitmapImageViewModel(BitmapImageModel model) { _model = model; + BitmapSource = model.ObserveProperty(m => m.BitmapSource).ToReadOnlyReactivePropertySlim().AddTo(Disposables); } public string Title => _model.Title; - public BitmapSource BitmapSource => _model.BitmapSource; + public ReadOnlyReactivePropertySlim BitmapSource { get; } + + public async Task EnsureBitmapSourceAsync() { + await _model.EnsureBitmapSourceAsync().ConfigureAwait(false); + } } } diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/Dims/DimsDataCollectionSettingViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/Dims/DimsDataCollectionSettingViewModel.cs index b365c8165..3c48f2bdd 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/Dims/DimsDataCollectionSettingViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/Dims/DimsDataCollectionSettingViewModel.cs @@ -21,6 +21,9 @@ public DimsDataCollectionSettingViewModel(DimsDataCollectionSettingModel model) UseAverageMs1 = Model .ToReactivePropertySlimAsSynchronized(m => m.UseAverageMs1) .AddTo(Disposables); + UseAccumulateMs1 = Model + .ToReactivePropertySlimAsSynchronized(m => m.UseAccumulateMs1) + .AddTo(Disposables); TimeBegin = Model .ToReactivePropertySlimAsSynchronized(m => m.TimeBegin) .AddTo(Disposables); @@ -44,6 +47,7 @@ public void Commit() { public ReactivePropertySlim UseMs1WithHighestTic { get; } public ReactivePropertySlim UseMs1WithHighestBpi { get; } public ReactivePropertySlim UseAverageMs1 { get; } + public ReactivePropertySlim UseAccumulateMs1 { get; } public ReactivePropertySlim TimeBegin { get; } public ReactivePropertySlim TimeEnd { get; } public ReactivePropertySlim MassTolerance { get; } diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/ImagingRoiViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/ImagingRoiViewModel.cs index 9e0edaeb8..62ddc52fd 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/ImagingRoiViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/ImagingRoiViewModel.cs @@ -2,6 +2,7 @@ using CompMs.CommonMVVM; using Reactive.Bindings; using Reactive.Bindings.Extensions; +using System; using System.Linq; using System.Reactive.Linq; @@ -15,16 +16,22 @@ public ImagingRoiViewModel(ImagingRoiModel model) { RoiPeakSummaries = model.RoiPeakSummaries.ToReadOnlyReactiveCollection(summary => new RoiPeakSummaryViewModel(summary)).AddTo(Disposables); SelectedRoiPeakSummary = model.ToReactivePropertyAsSynchronized( m => m.SelectedRoiPeakSummary, - mox => mox.Select(m => RoiPeakSummaries.FirstOrDefault(vm => vm.Model == m)), + mox => mox.Select(m => (RoiPeakSummaryViewModel?)RoiPeakSummaries.FirstOrDefault(vm => vm.Model == m)), vmox => vmox.Select(vm => vm?.Model)) .AddTo(Disposables); Roi = new RoiViewModel(model.Roi).AddTo(Disposables); IsSelected = model.ToReactivePropertySlimAsSynchronized(m => m.IsSelected).AddTo(Disposables); + + SelectedRoiPeakSummary.Subscribe(summary => { + if (summary is not null) { + _ = summary.EnsureCalculateAccumulatedIntensity(); + } + }).AddTo(Disposables); } public ReactivePropertySlim Id { get; } public ReadOnlyReactiveCollection RoiPeakSummaries { get; } - public ReactiveProperty SelectedRoiPeakSummary { get; } + public ReactiveProperty SelectedRoiPeakSummary { get; } public RoiViewModel Roi { get; } public ReactivePropertySlim IsSelected { get; } public ImagingRoiModel Model { get; } diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/RoiPeakSummaryViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/RoiPeakSummaryViewModel.cs index 538c181ae..7347071ff 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/RoiPeakSummaryViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/Imaging/RoiPeakSummaryViewModel.cs @@ -1,6 +1,9 @@ using CompMs.App.Msdial.Model.Imaging; using CompMs.CommonMVVM; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; using System; +using System.Threading.Tasks; namespace CompMs.App.Msdial.ViewModel.Imaging { @@ -8,10 +11,17 @@ internal sealed class RoiPeakSummaryViewModel : ViewModelBase { public RoiPeakSummaryViewModel(RoiPeakSummaryModel model) { Model = model ?? throw new ArgumentNullException(nameof(model)); + AccumulatedIntensity = model.ObserveProperty(m => m.AccumulatedIntensity).ToReadOnlyReactivePropertySlim(initialValue: null).AddTo(Disposables); + IsAccumulatedIntensityLoading = model.ObserveProperty(m => m.IsAccumulatedIntensityLoading).ToReadOnlyReactivePropertySlim().AddTo(Disposables); } public RoiPeakSummaryModel Model { get; } - public double AccumulatedIntensity => Model.AccumulatedIntensity; + public ReadOnlyReactivePropertySlim AccumulatedIntensity { get; } + public ReadOnlyReactivePropertySlim IsAccumulatedIntensityLoading { get; } + + public Task EnsureCalculateAccumulatedIntensity() { + return Model.EnsureCalculateAccumulatedIntensityAsync(); + } } } diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/ImagingDimsImageViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/ImagingDimsImageViewModel.cs index 4510e020b..cf340ac23 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/ImagingDimsImageViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/ImagingDimsImageViewModel.cs @@ -28,7 +28,9 @@ public ImagingDimsImageViewModel(ImagingDimsImageModel model, FocusControlManage AddRoiCommand = new AsyncReactiveCommand().WithSubscribe(model.AddRoiAsync).AddTo(Disposables); RemoveRoiCommand = new ReactiveCommand().WithSubscribe(model.RemoveRoi).AddTo(Disposables); SaveIntensitiesCommand = new AsyncReactiveCommand().WithSubscribe(() => model.SaveIntensitiesAsync()).AddTo(Disposables); + SaveRoiCommand = new AsyncReactiveCommand().WithSubscribe(() => model.SaveRoisAsync()).AddTo(Disposables); LoadRoiCommand = new ReactiveCommand().WithSubscribe(model.LoadRoi).AddTo(Disposables); + ExportIntensitiesCommand = new AsyncReactiveCommand().WithSubscribe(() => model.ExportIntensitiesAsync()).AddTo(Disposables); } public string ImageTitle => _model.File.AnalysisFileName; @@ -41,5 +43,7 @@ public ImagingDimsImageViewModel(ImagingDimsImageModel model, FocusControlManage public AsyncReactiveCommand AddRoiCommand { get; } public ReactiveCommand RemoveRoiCommand { get; } public AsyncReactiveCommand SaveIntensitiesCommand { get; } + public AsyncReactiveCommand SaveRoiCommand { get; } public ReactiveCommand LoadRoiCommand { get; } + public AsyncReactiveCommand ExportIntensitiesCommand { get; } } diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/WholeImageResultViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/WholeImageResultViewModel.cs index 02f46d69d..1e2238912 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/WholeImageResultViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingDims/WholeImageResultViewModel.cs @@ -26,19 +26,18 @@ public WholeImageResultViewModel(WholeImageResultModel model, FocusControlManage AnalysisViewModel = analysisViewModel; Intensities = model.Intensities.ToReadOnlyReactiveCollection(intensity => new IntensityImageViewModel(intensity)).AddTo(Disposables); - SelectedPeakIntensities = model.ToReactivePropertyAsSynchronized( - m => m.SelectedPeakIntensities, - mox => mox.Select(m => Intensities.FirstOrDefault(vm => vm.Model == m)), - vmox => vmox.Select(vm => vm?.Model)) - .AddTo(Disposables); ImagingRoiViewModel = new ImagingRoiViewModel(model.ImagingRoiModel).AddTo(Disposables); + IntensityImagePlaceholder = model.IntensityImagePlaceholder.ObserveProperty(m => m.CurrentImage) + .Select(m => m is null ? null : new BitmapImageViewModel(m)) + .DisposePreviousValue() + .ToReadOnlyReactivePropertySlim().AddTo(Disposables); } public DimsAnalysisViewModel AnalysisViewModel { get; } public ImagingRoiViewModel ImagingRoiViewModel { get; } public ReadOnlyReactiveCollection Intensities { get; } public AnalysisPeakPlotViewModel PeakPlotViewModel => AnalysisViewModel.PlotViewModel; - public ReactiveProperty SelectedPeakIntensities { get; } + public ReadOnlyReactivePropertySlim IntensityImagePlaceholder { get; } public ICommand ShowIonTableCommand => AnalysisViewModel.ShowIonTableCommand; diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/ImagingImmsImageViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/ImagingImmsImageViewModel.cs index ae15e3299..53dd03f46 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/ImagingImmsImageViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/ImagingImmsImageViewModel.cs @@ -27,6 +27,7 @@ public ImagingImmsImageViewModel(ImagingImmsImageModel model, FocusControlManage AddRoiCommand = new AsyncReactiveCommand().WithSubscribe(model.AddRoiAsync).AddTo(Disposables); RemoveRoiCommand = new ReactiveCommand().WithSubscribe(model.RemoveRoi).AddTo(Disposables); SaveIntensitiesCommand = new AsyncReactiveCommand().WithSubscribe(() => model.SaveIntensitiesAsync()).AddTo(Disposables); + SaveRoiCommand = new AsyncReactiveCommand().WithSubscribe(() => model.SaveRoisAsync()).AddTo(Disposables); LoadRoiCommand = new ReactiveCommand().WithSubscribe(model.LoadRoi).AddTo(Disposables); } @@ -40,6 +41,7 @@ public ImagingImmsImageViewModel(ImagingImmsImageModel model, FocusControlManage public AsyncReactiveCommand AddRoiCommand { get; } public ReactiveCommand RemoveRoiCommand { get; } public AsyncReactiveCommand SaveIntensitiesCommand { get; } + public AsyncReactiveCommand SaveRoiCommand { get; } public ReactiveCommand LoadRoiCommand { get; } } } diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/WholeImageResultViewModel.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/WholeImageResultViewModel.cs index 658f8d5eb..c8ee97bbe 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/WholeImageResultViewModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/ImagingImms/WholeImageResultViewModel.cs @@ -9,7 +9,6 @@ using Reactive.Bindings; using Reactive.Bindings.Extensions; using Reactive.Bindings.Notifiers; -using System.Linq; using System.Reactive.Linq; using System.Windows.Input; @@ -25,18 +24,17 @@ public WholeImageResultViewModel(WholeImageResultModel model, FocusControlManage AnalysisViewModel = analysisViewModel; Intensities = model.Intensities.ToReadOnlyReactiveCollection(intensity => new IntensityImageViewModel(intensity)).AddTo(Disposables); - SelectedPeakIntensities = model.ToReactivePropertyAsSynchronized( - m => m.SelectedPeakIntensities, - mox => mox.Select(m => Intensities.FirstOrDefault(vm => vm.Model == m)), - vmox => vmox.Select(vm => vm?.Model)) - .AddTo(Disposables); ImagingRoiViewModel = new ImagingRoiViewModel(model.ImagingRoiModel).AddTo(Disposables); + IntensityImagePlaceholder = model.IntensityImagePlaceholder.ObserveProperty(m => m.CurrentImage) + .Select(m => m is null ? null : new BitmapImageViewModel(m)) + .DisposePreviousValue() + .ToReadOnlyReactivePropertySlim().AddTo(Disposables); } public ImmsAnalysisViewModel AnalysisViewModel { get; } public AnalysisPeakPlotViewModel PeakPlotViewModel => AnalysisViewModel.PlotViewModel; public ReadOnlyReactiveCollection Intensities { get; } - public ReactiveProperty SelectedPeakIntensities { get; } + public ReadOnlyReactivePropertySlim IntensityImagePlaceholder { get; } public ImagingRoiViewModel ImagingRoiViewModel { get; } public ICommand ShowIonTableCommand => AnalysisViewModel.ShowIonTableCommand; diff --git a/src/MSDIAL5/MsdialImmsImagingCore/Process/FileProcess.cs b/src/MSDIAL5/MsdialImmsImagingCore/Process/FileProcess.cs index 4041d1032..32770e572 100644 --- a/src/MSDIAL5/MsdialImmsImagingCore/Process/FileProcess.cs +++ b/src/MSDIAL5/MsdialImmsImagingCore/Process/FileProcess.cs @@ -38,7 +38,7 @@ public async Task RunAsync(AnalysisFileBean file, IDataProvider provider, Action var chromPeakFeatures = await file.LoadChromatogramPeakFeatureCollectionAsync(token).ConfigureAwait(false); var _elements = chromPeakFeatures.Items.Select(item => new Raw2DElement(item.PeakFeature.Mass, item.PeakFeature.ChromXsTop.Drift.Value)).ToList(); - var pixels = RetrieveRawSpectraOnPixels(file, _elements, true); + var pixels = await RetrieveRawSpectraOnPixelsAsync(file, _elements, true, token).ConfigureAwait(false); } public async Task RunAsyncTest(AnalysisFileBean file, IDataProvider provider, Action? reportAction = null, CancellationToken token = default) { @@ -46,15 +46,13 @@ public async Task RunAsyncTest(AnalysisFileBean file, IDataProvider provider, Ac var chromPeakFeatures = await file.LoadChromatogramPeakFeatureCollectionAsync(token).ConfigureAwait(false); var _elements = chromPeakFeatures.Items.Select(item => new Raw2DElement(item.PeakFeature.Mass, item.PeakFeature.ChromXsTop.Drift.Value)).ToList(); - var pixels = RetrieveRawSpectraOnPixels(file, _elements, true); - + var pixels = await RetrieveRawSpectraOnPixelsAsync(file, _elements, true, token).ConfigureAwait(false); } - - private RawSpectraOnPixels RetrieveRawSpectraOnPixels(AnalysisFileBean file, List targetElements, bool isNewFileProcess) { + private async Task RetrieveRawSpectraOnPixelsAsync(AnalysisFileBean file, List targetElements, bool isNewFileProcess, CancellationToken token = default) { if (targetElements.IsEmptyOrNull()) return null; using (RawDataAccess rawDataAccess = new RawDataAccess(file.AnalysisFilePath, 0, true, true, true, 10, 0.02, 0.015)) { - return rawDataAccess.GetRawPixelFeatures(targetElements, file.GetMaldiFrames(), isNewFileProcess) + return await rawDataAccess.GetRawPixelFeaturesAsync(targetElements, file.GetMaldiFrames(), isNewFileProcess, token).ConfigureAwait(false) ?? new RawSpectraOnPixels { PixelPeakFeaturesList = new List(0), XYFrames = new List(0), }; } } diff --git a/tests/MSDIAL5/MsdialCoreTestApp/Process/MaldiMsProcessTest.cs b/tests/MSDIAL5/MsdialCoreTestApp/Process/MaldiMsProcessTest.cs index 177eed1f3..8ea05e8f7 100644 --- a/tests/MSDIAL5/MsdialCoreTestApp/Process/MaldiMsProcessTest.cs +++ b/tests/MSDIAL5/MsdialCoreTestApp/Process/MaldiMsProcessTest.cs @@ -209,7 +209,7 @@ public static void TimsOnTest() { var featureElements = features.Select(n => new Raw2DElement(n.PeakFeature.Mass, n.PeakFeature.ChromXsTop.Value)).ToList(); Console.WriteLine("Reading data..."); using (var access = new RawDataAccess(filepath, 0, false, true, false)) { - pixelData = access.GetRawPixelFeatures(featureElements, null); + pixelData = access.GetRawPixelFeaturesAsync(featureElements, null).Result; } //foreach (var (feature, pixel) in IEnumerableExtension.Zip(features, pixelData.PixelPeakFeaturesList)) { @@ -304,7 +304,7 @@ public static void TimsOffTest() { var featureElements = features.Select(n => new Raw2DElement() { Mz = n.PeakFeature.Mass }).ToList(); Console.WriteLine("Reading data..."); using (var access = new RawDataAccess(filepath, 0, false, true, false)) { - pixelData = access.GetRawPixelFeatures(featureElements, null); + pixelData = access.GetRawPixelFeaturesAsync(featureElements, null).Result; } foreach (var (feature, pixel) in features.Zip(pixelData.PixelPeakFeaturesList, (feature, pixel) => (Feature: feature, Pixel: pixel))) {