From 8a212688b499e6d1bfa14574704e777d6d813348 Mon Sep 17 00:00:00 2001 From: Trent Stohrer Date: Tue, 24 Mar 2026 11:31:09 -0400 Subject: [PATCH 01/16] starting in on UserLibrary --- Models/UserLibrary.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Models/UserLibrary.cs diff --git a/Models/UserLibrary.cs b/Models/UserLibrary.cs new file mode 100644 index 0000000..3d4b5db --- /dev/null +++ b/Models/UserLibrary.cs @@ -0,0 +1,24 @@ +using EnlightenMAUI.Platforms; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EnlightenMAUI.Models +{ + internal class UserLibrary + { + Settings settings = Settings.getInstance(); + + public async Task loadTodaySpectra() + { + + Dictionary library = new Dictionary(); + Dictionary originalRaws = new Dictionary(); + Dictionary originalDarks = new Dictionary(); + string path = PlatformUtil.getAutoSavePath(settings.highLevelAutoSave); + await PlatformUtil.loadFiles(useAssets: false, path, library, originalRaws, originalDarks, true, null); + + } + + } +} From f75e48dea819309701611c7617a1d1c49cf1b9cf Mon Sep 17 00:00:00 2001 From: Trent Stohrer Date: Tue, 24 Mar 2026 11:31:45 -0400 Subject: [PATCH 02/16] starting in on UserLibrary --- ViewModels/AnalysisViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ViewModels/AnalysisViewModel.cs b/ViewModels/AnalysisViewModel.cs index 670a24c..8611f5e 100644 --- a/ViewModels/AnalysisViewModel.cs +++ b/ViewModels/AnalysisViewModel.cs @@ -29,6 +29,7 @@ internal class AnalysisViewModel : INotifyPropertyChanged public event ToastNotification notifyToast; Measurement lastMeas; SelectionPopupViewModel sublibraryViewModel = new SelectionPopupViewModel(); + List userSpectra = new List(); Logger logger = Logger.getInstance(); public Library library; From c868d2634943885ce1079d51a0c3c5bac82c3d43 Mon Sep 17 00:00:00 2001 From: Trent Stohrer Date: Wed, 1 Apr 2026 09:28:27 -0400 Subject: [PATCH 03/16] adding logical structures for bulk export --- Models/Measurement.cs | 3 ++ Models/UserLibrary.cs | 42 ++++++++++++++++++++- Models/WPLibrary.cs | 42 ++++++++++++++++++--- Platforms/Android/PlatformUtil.cs | 62 ++++++++++++++++++++----------- Platforms/iOS/PlatformUtil.cs | 2 +- ViewModels/AnalysisViewModel.cs | 1 + ViewModels/ScopeViewModel.cs | 1 + 7 files changed, 123 insertions(+), 30 deletions(-) diff --git a/Models/Measurement.cs b/Models/Measurement.cs index 3fe2615..1a45e69 100644 --- a/Models/Measurement.cs +++ b/Models/Measurement.cs @@ -593,6 +593,9 @@ public async Task saveAsync(bool librarySave = false, bool autoSave = fals pathname = Path.Join(savePath, filename); logger.debug($"Measurement.saveAsync: creating {pathname}"); + UserLibrary ul = UserLibrary.getInstance(); + ul.addSpectrum(this, filename); + using (StreamWriter sw = new StreamWriter(pathname)) { writeMetadata(sw); diff --git a/Models/UserLibrary.cs b/Models/UserLibrary.cs index 3d4b5db..c33a4c5 100644 --- a/Models/UserLibrary.cs +++ b/Models/UserLibrary.cs @@ -7,18 +7,56 @@ namespace EnlightenMAUI.Models { internal class UserLibrary { + static UserLibrary instance = null; + Settings settings = Settings.getInstance(); + Dictionary userSpectra = new Dictionary(); + public List userSpectraKeys + { + get + { + return new List(userSpectra.Keys); + } + } - public async Task loadTodaySpectra() + static public UserLibrary getInstance() { + if (instance == null) + instance = new UserLibrary(); + return instance; + } + + public UserLibrary() + { + loadTodaySpectra(); + } + + public async Task loadTodaySpectra() + { Dictionary library = new Dictionary(); Dictionary originalRaws = new Dictionary(); Dictionary originalDarks = new Dictionary(); string path = PlatformUtil.getAutoSavePath(settings.highLevelAutoSave); - await PlatformUtil.loadFiles(useAssets: false, path, library, originalRaws, originalDarks, true, null); + await PlatformUtil.loadFiles(useAssets: false, path, library, originalRaws, originalDarks, true, null, skipSearch: true); + foreach (string tag in library.Keys) + { + Measurement m = library[tag]; + string userTag = $"{m.timestamp.ToString("HH:mm")} {(m.declaredMatch != null && m.declaredMatch.Length > 0 ? m.declaredMatch[0] : "")} {(m.declaredScore.HasValue ? m.declaredScore.Value.ToString("f2") : "")}"; + Logger.getInstance().debug("adding {0} to user library as {1}", userTag, tag); + userSpectra.Add(userTag, tag); + } } + public void addSpectrum(Measurement m, string tag) + { + string userTag = $"{m.timestamp.ToString("HH:mm")} {(m.declaredMatch != null && m.declaredMatch.Length > 0 ? m.declaredMatch[0] : "")} {(m.declaredScore.HasValue ? m.declaredScore.Value.ToString("f2") : "")}"; + if (!userSpectra.ContainsKey(userTag)) + { + Logger.getInstance().debug("adding {0} to user library as {1}", userTag, tag); + userSpectra.Add(userTag, tag); + } + } } } diff --git a/Models/WPLibrary.cs b/Models/WPLibrary.cs index 5aaa89b..5cba3e5 100644 --- a/Models/WPLibrary.cs +++ b/Models/WPLibrary.cs @@ -30,7 +30,12 @@ public class SimpleCSVParser public List wavenumbers = new List(); public List intensities = new List(); public string name = null; + public string matches = null; + public string score = null; + public string timestamp = null; public ErrorTypes errorType = ErrorTypes.SUCCESS; + + bool strictHeaders = false; int linecount = 0; Logger logger = Logger.getInstance(); @@ -52,14 +57,29 @@ private void readHeader(List tok) { for (int i = 0; i < tok.Count; i++) { - string s = tok[i].ToLower(); - if (s == "wavenumber") + string s = tok[i].ToLower().Trim(); + + if (strictHeaders) { - colWavenumber = i; + if (s == "wavenumber") + { + colWavenumber = i; + } + else if (s == "spectrum") + { + colIntensity = i; + } } - else if (Regex.Match(s, "processed|spectrum|spectra|intensity").Success) + else { - colIntensity = i; + if (s == "wavenumber") + { + colWavenumber = i; + } + else if (Regex.Match(s, "processed|spectrum|spectra|intensity").Success) + { + colIntensity = i; + } } } } @@ -67,6 +87,9 @@ private void readHeader(List tok) void readValues(List tok) { int len = tok.Count; + if (tok[0].Length == 0) + return; + if ((len < colWavenumber + 1) || (len < colIntensity + 1)) { return; } if (VIGNETTE_COUNT > 0) { @@ -97,8 +120,9 @@ public async Task parseFile(string pathname) return await parseStream(stream); } - public async Task parseStream(Stream stream) + public async Task parseStream(Stream stream, bool strictHeaders = false) { + this.strictHeaders = strictHeaders; state = "READING_METADATA"; string line; using (StreamReader sr = new StreamReader(stream)) @@ -138,6 +162,12 @@ public async Task parseStream(Stream stream) if (key == "label") name = value; + if (key == "compound matches" || key == "compound match") + matches = value; + if (key == "match score") + score = value; + if (key == "timestamp") + timestamp = value; } } else if (state == "READING_HEADER") diff --git a/Platforms/Android/PlatformUtil.cs b/Platforms/Android/PlatformUtil.cs index bbaed0c..74f6b0b 100644 --- a/Platforms/Android/PlatformUtil.cs +++ b/Platforms/Android/PlatformUtil.cs @@ -1345,7 +1345,7 @@ public static List getSubLibraries() return compLibrary; } - public async static Task> loadFiles(bool useAssets, string root, Dictionary library, Dictionary originalRaws, Dictionary originalDarks, bool doDecon = true, string correctionFileName = "etalon_correction.json") + public async static Task> loadFiles(bool useAssets, string root, Dictionary library, Dictionary originalRaws, Dictionary originalDarks, bool doDecon = true, string correctionFileName = "etalon_correction.json", bool skipSearch = false) { if (useAssets) { @@ -1387,33 +1387,43 @@ public async static Task> loadFiles(bool useAsse { var cacheDirs = Platform.AppContext.GetExternalFilesDirs(null); Java.IO.File libraryFolder = null; - string[] rootPath = root.Split('/'); - int depth = rootPath.Length; - foreach (var cDir in cacheDirs) + if (!skipSearch) { - libraryFolder = traverseDown(rootPath, cDir); - if (libraryFolder != null) - break; + string[] rootPath = root.Split('/'); + int depth = rootPath.Length; - } + foreach (var cDir in cacheDirs) + { + libraryFolder = traverseDown(rootPath, cDir); + if (libraryFolder != null) + break; + + } - if (libraryFolder == null) + if (libraryFolder == null) + { + /* + if (library.Count > 0) + loadSucceeded = true; + isLoading = false; + InvokeLoadFinished(); + return; + */ + return library; + } + } + else { - /* - if (library.Count > 0) - loadSucceeded = true; - isLoading = false; - InvokeLoadFinished(); - return; - */ - return library; + libraryFolder = new Java.IO.File(root); } Regex csvReg = new Regex(@".*\.csv$"); Regex jsonReg = new Regex(@".*\.json$"); var libraryFiles = libraryFolder.ListFiles(); + logger.debug("Found {0} files in user library folder {1}", libraryFiles.Length, libraryFolder.AbsolutePath); + foreach (var libraryFile in libraryFiles) { @@ -1432,7 +1442,7 @@ public async static Task> loadFiles(bool useAsse { try { - await loadCSV(libraryFile, originalRaws, library); + await loadCSV(libraryFile, originalRaws, library, strictParse: skipSearch); } catch (Exception e) { @@ -1509,7 +1519,7 @@ static Java.IO.File traverseDown(string[] rootPath, Java.IO.File dir) return libraryFolder; } - static async Task loadCSV(Java.IO.File file, Dictionary originalRaws, Dictionary library) + static async Task loadCSV(Java.IO.File file, Dictionary originalRaws, Dictionary library, bool strictParse = false) { logger.info("start loading library file from {0}", file.AbsolutePath); @@ -1518,13 +1528,16 @@ static async Task loadCSV(Java.IO.File file, Dictionary origin SimpleCSVParser parser = new SimpleCSVParser(); Stream s = System.IO.File.OpenRead(file.AbsolutePath); StreamReader sr = new StreamReader(s); - await parser.parseStream(s); + await parser.parseStream(s, strictParse); Measurement m = new Measurement(); m.wavenumbers = parser.wavenumbers.ToArray(); m.raw = parser.intensities.ToArray(); m.excitationNM = 785; - + m.timestamp = DateTime.ParseExact(parser.timestamp, "dd/MM/yyyy HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture); + m.declaredMatch = new string[] { parser.matches }; + if (parser.score != null) + m.declaredScore = Double.Parse(parser.score); #if USE_DECON Deconvolution.Spectrum spec = new Deconvolution.Spectrum(parser.wavenumbers, parser.intensities); @@ -1565,6 +1578,11 @@ static async Task loadCSV(Java.IO.File file, Dictionary origin Measurement updated = new Measurement(); updated.wavenumbers = wavenumbers; updated.raw = newIntensities; + updated.excitationNM = m.excitationNM; + updated.timestamp = m.timestamp; + updated.declaredMatch = m.declaredMatch; + updated.declaredScore = m.declaredScore; + //double airPLSLambda = 10000; //int airPLSMaxIter = 100; //double[] array = AirPLS.smooth(updated.processed, airPLSLambda, airPLSMaxIter, 0.001, verbose: false, (int)roiStart, (int)roiEnd); @@ -1573,6 +1591,8 @@ static async Task loadCSV(Java.IO.File file, Dictionary origin //updated.raw = shortened; //updated.dark = null; + //logger.debug("adding {0} to library (timestamp {1})", name, ) + library.Add(name, updated); #if USE_DECON diff --git a/Platforms/iOS/PlatformUtil.cs b/Platforms/iOS/PlatformUtil.cs index dd8690a..79ec2da 100644 --- a/Platforms/iOS/PlatformUtil.cs +++ b/Platforms/iOS/PlatformUtil.cs @@ -1230,7 +1230,7 @@ public static List getSubLibraries() return compLibrary; } - public async static Task> loadFiles(bool useAssets, string root, Dictionary library, Dictionary originalRaws, Dictionary originalDarks, bool doDecon = true, string correctionFileName = "etalon_correction.json") + public async static Task> loadFiles(bool useAssets, string root, Dictionary library, Dictionary originalRaws, Dictionary originalDarks, bool doDecon = true, string correctionFileName = "etalon_correction.json", bool skipSearch = false) { //isLoading = true; NSUrl extPath = NSFileManager.DefaultManager.GetUrl( diff --git a/ViewModels/AnalysisViewModel.cs b/ViewModels/AnalysisViewModel.cs index a3cb54d..3e52d47 100644 --- a/ViewModels/AnalysisViewModel.cs +++ b/ViewModels/AnalysisViewModel.cs @@ -32,6 +32,7 @@ internal class AnalysisViewModel : INotifyPropertyChanged List userSpectra = new List(); Logger logger = Logger.getInstance(); + UserLibrary userLibrary = UserLibrary.getInstance(); public Library library; public Spectrometer spec; static AnalysisViewModel instance = null; diff --git a/ViewModels/ScopeViewModel.cs b/ViewModels/ScopeViewModel.cs index 4462a12..7cee148 100644 --- a/ViewModels/ScopeViewModel.cs +++ b/ViewModels/ScopeViewModel.cs @@ -76,6 +76,7 @@ public class ScopeViewModel : INotifyPropertyChanged Settings settings; Logger logger = Logger.getInstance(); + UserLibrary userLibrary = UserLibrary.getInstance(); Library library; Task libraryLoader; From c0dd39f88558a8094e593aa7cee9e12ce1ca710d Mon Sep 17 00:00:00 2001 From: Trent Stohrer Date: Wed, 1 Apr 2026 10:08:41 -0400 Subject: [PATCH 04/16] live adding to library seems to work --- Models/Measurement.cs | 2 +- Platforms/Android/PlatformUtil.cs | 50 +++++++++++++++++++++++++++---- Platforms/iOS/PlatformUtil.cs | 18 +++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/Models/Measurement.cs b/Models/Measurement.cs index 1a45e69..3a50c46 100644 --- a/Models/Measurement.cs +++ b/Models/Measurement.cs @@ -594,7 +594,7 @@ public async Task saveAsync(bool librarySave = false, bool autoSave = fals logger.debug($"Measurement.saveAsync: creating {pathname}"); UserLibrary ul = UserLibrary.getInstance(); - ul.addSpectrum(this, filename); + ul.addSpectrum(this, PlatformUtil.getFileName(filename)); using (StreamWriter sw = new StreamWriter(pathname)) { diff --git a/Platforms/Android/PlatformUtil.cs b/Platforms/Android/PlatformUtil.cs index 74f6b0b..a8759df 100644 --- a/Platforms/Android/PlatformUtil.cs +++ b/Platforms/Android/PlatformUtil.cs @@ -506,9 +506,47 @@ async static Task findUserFilesDeeper(Java.IO.File folder, Spectrometer spec, Di } } + static string getFileName(Java.IO.File file) + { + string name = file.Name; + string[] parts = name.Split('.'); + StringBuilder sb = new StringBuilder(); + foreach (var part in parts) + { + if (sb.Length > 0 && part != parts.Last()) + sb.Append('.'); + + if (part != parts.Last()) + sb.Append(part); + else + break; + } + + return sb.ToString(); + } + + public static string getFileName(string name) + { + string temp = name.Split('/').Last(); + string[] parts = temp.Split('.'); + StringBuilder sb = new StringBuilder(); + foreach (var part in parts) + { + if (sb.Length > 0 && part != parts.Last()) + sb.Append('.'); + + if (part != parts.Last()) + sb.Append(part); + else + break; + } + + return sb.ToString(); + } + async static Task addUserFile(Java.IO.File file, Spectrometer spec, Dictionary dict) { - string name = file.AbsolutePath.Split('/').Last().Split('.').First(); + string name = getFileName(file); await loadCSV(file, spec, dict); } @@ -516,7 +554,7 @@ async static Task loadCSV(Java.IO.File file, Spectrometer spec, Dictionary origin { logger.info("start loading library file from {0}", file.AbsolutePath); - string name = file.AbsolutePath.Split('/').Last().Split('.').First(); + string name = getFileName(file); SimpleCSVParser parser = new SimpleCSVParser(); Stream s = System.IO.File.OpenRead(file.AbsolutePath); @@ -1608,7 +1646,7 @@ static async Task loadCSV(string file, Dictionary originalRaws { logger.info("start loading library file from {0}", file); - string name = file.Split('/').Last().Split('.').First(); + string name = getFileName(file); SimpleCSVParser parser = new SimpleCSVParser(); @@ -1683,7 +1721,7 @@ static async Task loadJSON(Java.IO.File file, Dictionary origi { logger.info("start loading library file from {0}", file.AbsolutePath); - string name = file.AbsolutePath.Split('/').Last().Split('.').First(); + string name = getFileName(file); SimpleCSVParser parser = new SimpleCSVParser(); Stream s = System.IO.File.OpenRead(file.AbsolutePath); @@ -1742,7 +1780,7 @@ static async Task loadJSON(string file, Dictionary originalRaw { logger.info("start loading library file from {0}", file); - string name = file.Split('/').Last().Split('.').First(); + string name = getFileName(file); SimpleCSVParser parser = new SimpleCSVParser(); Stream s = System.IO.File.OpenRead(file); diff --git a/Platforms/iOS/PlatformUtil.cs b/Platforms/iOS/PlatformUtil.cs index 79ec2da..1c27d1b 100644 --- a/Platforms/iOS/PlatformUtil.cs +++ b/Platforms/iOS/PlatformUtil.cs @@ -372,6 +372,24 @@ async static Task findUserFilesDeeper(NSUrl folder, Spectrometer spec, Dictionar } } + public static string getFileName(string name) + { + string[] parts = name.Split('.'); + StringBuilder sb = new StringBuilder(); + foreach (var part in parts) + { + if (sb.Length > 0 && part != parts.Last()) + sb.Append('.'); + + if (part != parts.Last()) + sb.Append(part); + else + break; + } + + return sb.ToString(); + } + async static Task addUserFile(NSUrl file, Spectrometer spec, Dictionary dict) { await loadCSV(file, spec, dict); From 27ff661c94021e2ffe92767a0863500c52f1027d Mon Sep 17 00:00:00 2001 From: Trent Stohrer Date: Wed, 1 Apr 2026 14:14:25 -0400 Subject: [PATCH 05/16] fixed deprecated popup controls, export now zips and shares as one would expect (feature is functionally complete) --- EnlightenMAUI.csproj | 3 + Models/API6BLESpectrometer.cs | 3 +- Models/Settings.cs | 2 +- Models/UserLibrary.cs | 8 +- Platforms/Android/PlatformUtil.cs | 55 +++++++++++++ Platforms/iOS/PlatformUtil.cs | 4 + Popups/AddToLibraryPopup.xaml | 2 +- Popups/OverlaysPopup.xaml | 42 +++++----- ViewModels/AnalysisViewModel.cs | 107 +++++++++++++++++++++++--- ViewModels/ScopeViewModel.cs | 3 +- ViewModels/SelectionPopupViewModel.cs | 19 +++++ 11 files changed, 206 insertions(+), 42 deletions(-) diff --git a/EnlightenMAUI.csproj b/EnlightenMAUI.csproj index 3661320..fb1e14d 100644 --- a/EnlightenMAUI.csproj +++ b/EnlightenMAUI.csproj @@ -185,6 +185,9 @@ MSBuild:Compile + + MSBuild:Compile + MSBuild:Compile diff --git a/Models/API6BLESpectrometer.cs b/Models/API6BLESpectrometer.cs index 0adad27..1fb6c33 100644 --- a/Models/API6BLESpectrometer.cs +++ b/Models/API6BLESpectrometer.cs @@ -1,5 +1,4 @@ -using Android.Renderscripts; -using EnlightenMAUI.Common; +using EnlightenMAUI.Common; using EnlightenMAUI.Platforms; using Plugin.BLE.Abstractions.Contracts; using System.ComponentModel; diff --git a/Models/Settings.cs b/Models/Settings.cs index 176318c..ecd4a97 100644 --- a/Models/Settings.cs +++ b/Models/Settings.cs @@ -259,7 +259,7 @@ public bool autoRetry PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(autoRetry))); } } - bool _autoRetry = true; + bool _autoRetry = false; public string getSavePath() diff --git a/Models/UserLibrary.cs b/Models/UserLibrary.cs index c33a4c5..cf7bb78 100644 --- a/Models/UserLibrary.cs +++ b/Models/UserLibrary.cs @@ -10,7 +10,7 @@ internal class UserLibrary static UserLibrary instance = null; Settings settings = Settings.getInstance(); - Dictionary userSpectra = new Dictionary(); + public Dictionary userSpectra = new Dictionary(); public List userSpectraKeys { get @@ -37,13 +37,13 @@ public async Task loadTodaySpectra() Dictionary library = new Dictionary(); Dictionary originalRaws = new Dictionary(); Dictionary originalDarks = new Dictionary(); - string path = PlatformUtil.getAutoSavePath(settings.highLevelAutoSave); + string path = PlatformUtil.getAutoSavePath(true); await PlatformUtil.loadFiles(useAssets: false, path, library, originalRaws, originalDarks, true, null, skipSearch: true); foreach (string tag in library.Keys) { Measurement m = library[tag]; - string userTag = $"{m.timestamp.ToString("HH:mm")} {(m.declaredMatch != null && m.declaredMatch.Length > 0 ? m.declaredMatch[0] : "")} {(m.declaredScore.HasValue ? m.declaredScore.Value.ToString("f2") : "")}"; + string userTag = $"{m.timestamp.ToString("HH:mm:ss")} {(m.declaredMatch != null && m.declaredMatch.Length > 0 ? m.declaredMatch[0] : "")} {(m.declaredScore.HasValue ? m.declaredScore.Value.ToString("f2") : "")}"; Logger.getInstance().debug("adding {0} to user library as {1}", userTag, tag); userSpectra.Add(userTag, tag); } @@ -51,7 +51,7 @@ public async Task loadTodaySpectra() public void addSpectrum(Measurement m, string tag) { - string userTag = $"{m.timestamp.ToString("HH:mm")} {(m.declaredMatch != null && m.declaredMatch.Length > 0 ? m.declaredMatch[0] : "")} {(m.declaredScore.HasValue ? m.declaredScore.Value.ToString("f2") : "")}"; + string userTag = $"{m.timestamp.ToString("HH:mm:ss")} {(m.declaredMatch != null && m.declaredMatch.Length > 0 ? m.declaredMatch[0] : "")} {(m.declaredScore.HasValue ? m.declaredScore.Value.ToString("f2") : "")}"; if (!userSpectra.ContainsKey(userTag)) { Logger.getInstance().debug("adding {0} to user library as {1}", userTag, tag); diff --git a/Platforms/Android/PlatformUtil.cs b/Platforms/Android/PlatformUtil.cs index a8759df..071e50e 100644 --- a/Platforms/Android/PlatformUtil.cs +++ b/Platforms/Android/PlatformUtil.cs @@ -20,6 +20,7 @@ using NumSharp; using System.Collections.ObjectModel; using System.Diagnostics; +using System.IO.Compression; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -617,6 +618,60 @@ static async Task loadCorrections(string file) } } + public static async Task ZipFiles(string[] paths, string destination) + { + try + { + var f = new Java.IO.File(destination); + if (!f.Exists()) + f.Mkdirs(); + + foreach (var path in paths) + { + string name = getFileName(path) + ".csv"; + string fileDest = Path.Combine(destination, name); + System.IO.File.Copy(path, fileDest); + } + + + FileStream fs = System.IO.File.OpenWrite(destination + ".zip"); + await ZipFile.CreateFromDirectoryAsync(destination, fs); + fs.Flush(); + fs.Close(); + return destination + ".zip"; + /* + BufferedInputStream origin = null; + Stream dest = System.IO.File.OpenHandle(destination); + ZipOutputStream zout = new ZipOutputStream(new BufferedOutputStream(dest)); + byte[] data = new byte[1024]; + + for (int i = 0; i < paths.Length; i++) + { + + FileInputStream fi = new FileInputStream(paths[i]); + origin = new BufferedInputStream(fi, BUFFER); + + ZipEntry entry = new ZipEntry(paths[i].substring(paths[i].lastIndexOf("/") + 1)); + out.putNextEntry(entry); + int count; + + while ((count = origin.read(data, 0, BUFFER)) != -1) + { + out.write(data, 0, count); + } + origin.close(); + } + + out.close(); + */ + } + catch (Exception e) + { + return null; + //e.printStackTrace(); + } + } + public static double[] ProcessBackground(double[] wavenumbers, double[] counts, string serial, double fwhm, int roiStart, bool useSimple = false) { try diff --git a/Platforms/iOS/PlatformUtil.cs b/Platforms/iOS/PlatformUtil.cs index 1c27d1b..e9f99d3 100644 --- a/Platforms/iOS/PlatformUtil.cs +++ b/Platforms/iOS/PlatformUtil.cs @@ -461,6 +461,10 @@ static async Task loadCorrections(string file) logger.error("correction load failed with error {0}", ex.Message); } } + public static async Task ZipFiles(string[] paths, string destination) + { + return null; + } public static double[] ProcessBackground(double[] wavenumbers, double[] counts, string serial, double fwhm, int roiStart, bool useSimple = false) { diff --git a/Popups/AddToLibraryPopup.xaml b/Popups/AddToLibraryPopup.xaml index 857f97a..a63f3c3 100644 --- a/Popups/AddToLibraryPopup.xaml +++ b/Popups/AddToLibraryPopup.xaml @@ -6,7 +6,7 @@ xmlns:local="clr-namespace:EnlightenMAUI.Popups" HorizontalOptions="Fill"> - + diff --git a/Popups/OverlaysPopup.xaml b/Popups/OverlaysPopup.xaml index 6abb726..8597921 100644 --- a/Popups/OverlaysPopup.xaml +++ b/Popups/OverlaysPopup.xaml @@ -5,32 +5,30 @@ xmlns:local="clr-namespace:EnlightenMAUI" x:Class="EnlightenMAUI.Popups.OverlaysPopup" HorizontalOptions="Fill"> - + - + - - - - - - - - + MaximumHeightRequest="300" + BackgroundColor="Transparent" + VerticalOptions="Start" + x:DataType="local:ViewModels.SelectionPopupViewModel" + HorizontalOptions="Fill" + Margin="0, 0, 0, 10"> + + + + + - - + + diff --git a/ViewModels/AnalysisViewModel.cs b/ViewModels/AnalysisViewModel.cs index 3e52d47..36d07ec 100644 --- a/ViewModels/AnalysisViewModel.cs +++ b/ViewModels/AnalysisViewModel.cs @@ -470,25 +470,110 @@ async Task changeCorrection() notifyToast?.Invoke($"Raman correction applied for future samples"); } + SelectionPopupViewModel spvm; async Task ShareSpectrum() { - var ok = await spec.measurement.saveAsync(); + List entries = userLibrary.userSpectraKeys; + entries.Sort(); + entries.Reverse(); + List lib = new List(); + foreach (var entry in entries) + { + SelectionMetadata sm = new SelectionMetadata(entry, entry == entries.First()); + lib.Add(sm); + } - if (ok) + spvm = new SelectionPopupViewModel(lib); + spvm.exportName = $"{spec.eeprom.serialNumber}-{DateTime.Now.ToString("ddMMyy-HHmm")}"; + ExportPopup ep = new ExportPopup(spvm); + spvm.triggerClose += (s, e) => ep.CloseAsync(); + ep.Closed += ExportPopup_Closed; + await Shell.Current.ShowPopupAsync(ep); + + if (false) { - string savePath = Settings.getInstance().getSavePath(); - string pathname = Path.Join(savePath, spec.measurement.filename); - try + var ok = await spec.measurement.saveAsync(); + + if (ok) { - await Share.Default.RequestAsync(new ShareFileRequest + string savePath = Settings.getInstance().getSavePath(); + string pathname = Path.Join(savePath, spec.measurement.filename); + try + { + await Share.Default.RequestAsync(new ShareFileRequest + { + Title = spec.measurement.filename + " " + matchString, + File = new ShareFile(pathname) + }); + } + catch (Exception ex) { - Title = spec.measurement.filename + " " + matchString, - File = new ShareFile(pathname) - }); + logger.error("Share failed with exception {0}", ex.Message); + } } - catch (Exception ex) + } + } + + private async void ExportPopup_Closed(object sender, EventArgs e) + { + if (spvm.save.HasValue && spvm.save.Value) + { + logger.debug("export save triggered"); + List paths = new List(); + foreach (var entries in spvm.selections) { - logger.error("Share failed with exception {0}", ex.Message); + if (entries.selected) + { + if (userLibrary.userSpectra.ContainsKey(entries.name)) + paths.Add(userLibrary.userSpectra[entries.name]); + } + } + + if (paths.Count > 0) + { + string savePath = Settings.getInstance().getAutoSavePath(); + + if (paths.Count == 1) + { + string pathname = Path.Join(savePath, paths[0] + ".csv"); + try + { + await Share.Default.RequestAsync(new ShareFileRequest + { + Title = savePath, + File = new ShareFile(pathname) + }); + } + catch (Exception ex) + { + logger.error("Share failed with exception {0}", ex.Message); + } + } + else if (paths.Count > 1) + { + List toShare = new List(); + foreach (string path in paths) + { + toShare.Add(Path.Join(savePath, path + ".csv")); + } + + string pathname = Path.Join(savePath, spvm.exportName); + string share = await PlatformUtil.ZipFiles(toShare.ToArray(), pathname); + try + { + await Share.Default.RequestAsync(new ShareFileRequest + { + Title = spvm.exportName, + File = new ShareFile(share) + }); + } + catch (Exception ex) + { + logger.error("Share failed with exception {0}", ex.Message); + } + } + + } } } diff --git a/ViewModels/ScopeViewModel.cs b/ViewModels/ScopeViewModel.cs index 7cee148..9ac4d4a 100644 --- a/ViewModels/ScopeViewModel.cs +++ b/ViewModels/ScopeViewModel.cs @@ -76,7 +76,6 @@ public class ScopeViewModel : INotifyPropertyChanged Settings settings; Logger logger = Logger.getInstance(); - UserLibrary userLibrary = UserLibrary.getInstance(); Library library; Task libraryLoader; @@ -1763,6 +1762,8 @@ async Task doMatchAsync() } else { + spec.measurement.declaredMatch = null; + spec.measurement.declaredScore = null; if (settings.autoRetry && AnalysisViewModel.getInstance().currentParamSet == "Faster") { ScopeViewModel_TriggerIncreasedPrecision(this, AnalysisViewModel.getInstance()); diff --git a/ViewModels/SelectionPopupViewModel.cs b/ViewModels/SelectionPopupViewModel.cs index dcab73d..a38f358 100644 --- a/ViewModels/SelectionPopupViewModel.cs +++ b/ViewModels/SelectionPopupViewModel.cs @@ -12,11 +12,30 @@ namespace EnlightenMAUI.ViewModels public class SelectionPopupViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; + public EventHandler triggerClose; + public ObservableCollection selections { get; private set; } + public string exportName + { + get => _exportName; + set + { + _exportName = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(exportName))); + } + } + + string _exportName = ""; + public bool? save = null; + + public Command saveCommand { get; private set; } + public Command closeCommand { get; private set; } public SelectionPopupViewModel(List selectionList) { selections = new ObservableCollection(selectionList); + saveCommand = new Command(() => { save = true; triggerClose(this, this); }); + closeCommand = new Command(() => { save = false; triggerClose(this, this); }); } public SelectionPopupViewModel() { From 8e2a73779130d25fa97d5ed90d04e88cffbf2711 Mon Sep 17 00:00:00 2001 From: Trent Stohrer Date: Thu, 2 Apr 2026 08:58:35 -0400 Subject: [PATCH 06/16] library import theoretically works (needs testing) --- Models/WPLibrary.cs | 203 ++++++++++++++++++++++++++++++ Pages/AnalysisPage.xaml | 2 + Pages/SettingsPage.xaml | 2 +- Platforms/Android/PlatformUtil.cs | 21 ++++ Platforms/iOS/PlatformUtil.cs | 14 ++- ViewModels/AnalysisViewModel.cs | 35 ++++++ ViewModels/SettingsViewModel.cs | 2 +- 7 files changed, 273 insertions(+), 6 deletions(-) diff --git a/Models/WPLibrary.cs b/Models/WPLibrary.cs index 5cba3e5..ab8e6d1 100644 --- a/Models/WPLibrary.cs +++ b/Models/WPLibrary.cs @@ -11,6 +11,7 @@ using Common = EnlightenMAUI.Common; using EnlightenMAUI.Platforms; using System.Reflection.Metadata; +using Telerik.Licensing.Json; #if USE_DECON using Deconvolution = DeconvolutionMAUI; @@ -203,6 +204,180 @@ public async Task parseStream(Stream stream, bool strictHeaders = false) public enum ErrorTypes { SUCCESS, NULL_STREAM, INVALID_STATE, NO_INTENSITIES }; } + + public class MultiCSVParser + { + int colWavenumber = 0; + Dictionary entryCols = new Dictionary(); + string state; + + public List wavenumbers = new List(); + public Dictionary> intensities = new Dictionary>(); + public string name = null; + public ErrorTypes errorType = ErrorTypes.SUCCESS; + int linecount = 0; + Logger logger = Logger.getInstance(); + + int finalCol = 0; + + public MultiCSVParser() + { + } + + private bool isNum(string line) + { + if (line.Length == 0) + { + return false; + } + char c = line[0]; + return ('0' <= c && c <= '9') || c == '-'; + } + + private void readHeader(List tok) + { + for (int i = 0; i < tok.Count; i++) + { + string s = tok[i].ToLower(); + if (s == "wavenumber") + { + colWavenumber = i; + } + else if (s != null && s.Length > 1) + { + if (!entryCols.ContainsKey(s)) + { + entryCols.Add(s, i); + if (i > finalCol) + finalCol = i; + } + } + } + } + + void readValues(List tok) + { + int len = tok.Count; + if ((len < colWavenumber + 1) || (len < finalCol + 1)) { return; } + + double wavenumber = Convert.ToDouble(tok[colWavenumber]); + bool wavenumberAdded = false; + + foreach (string tag in entryCols.Keys) + { + if (!intensities.ContainsKey(tag)) + intensities.Add(tag, new List()); + + if (!isNum(tok[entryCols[tag]])) + continue; + + if (!wavenumberAdded) + { + wavenumbers.Add(wavenumber); + wavenumberAdded = true; + } + + double intensity = Convert.ToDouble(tok[entryCols[tag]]); + + intensities[tag].Add(intensity); + } + } + + public async Task parseFile(string pathname) + { + var assembly = IntrospectionExtensions.GetTypeInfo(typeof(SimpleCSVParser)).Assembly; + Stream stream = assembly.GetManifestResourceStream(pathname); + if (stream is null) + { + errorType = ErrorTypes.NULL_STREAM; + return false; + } + + return await parseStream(stream); + } + + public async Task parseStream(Stream stream) + { + state = "READING_METADATA"; + string line; + using (StreamReader sr = new StreamReader(stream)) + { + while ((line = await sr.ReadLineAsync()) != null) + { + line.Trim(); + + // some files have "CSV blanks" (lines of nothing but commas) + if (Regex.Match(line, "^[, ]*$").Success) + { + line = ""; + } + + List tok = line.Split(',').ToList(); + + if (state == "READING_METADATA") + { + if (isNum(line)) + { + // We found a digit, so either this file doesn't have metadata, + // or we're already past it. Unfortunately, this probably means + // we don't know what the field ordering is, so assume defaults. + state = "READING_DATA"; + readValues(tok); + } + else if (line.Length == 0) + { + // we found a blank, so assume next row is header + state = "READING_HEADER"; + } + else if (tok.Count > 1) + { + // process metadata + string key = tok[0].Trim().ToLower(); + string value = tok[1].Trim(); + + if (key == "label") + name = value; + } + } + else if (state == "READING_HEADER") + { + if (line.Length == 0) + { + // skip extra blank + } + else if (isNum(line)) + { + state = "READING_DATA"; + readValues(tok); + } + else if (line.Contains("Processed") && !line.Contains("Wavenumber")) + { + // the multi-spectrum format from Enlighten needs to skip a line after the "gap" + // this helps identify if we're in that format + continue; + } + else + { + readHeader(tok); + state = "READING_DATA"; + } + } + else if (state == "READING_DATA") + { + readValues(tok); + } + else + { + errorType = ErrorTypes.INVALID_STATE; + return false; + } + } + } + return true; + } + + public enum ErrorTypes { SUCCESS, NULL_STREAM, INVALID_STATE, NO_INTENSITIES }; + } public abstract class Library @@ -388,6 +563,34 @@ async Task loadFiles(bool useAssets, string root, bool doDecon = true, string co logger.debug("finished prepping data for decon"); } + public async Task importLibrary(string path) + { + var data = await PlatformUtil.ImportLibrary(path); + + if (data != null && data.Count > 0) + { + foreach (string sample in data.Keys) + { + if (!library.ContainsKey(sample)) + { + await handleSampleImport(sample, data[sample].Item1, data[sample].Item2); + } + } + } + } + + public async Task handleSampleImport(string sample, List wavenumbers, List intensities) + { + Measurement m = new Measurement(); + m.wavenumbers = wavenumbers.ToArray(); + m.raw = intensities.ToArray(); + m.excitationNM = 785; + + addSampleToLibrary(sample, m); + m.filename = sample + ".csv"; + await m.saveAsync(librarySave: true); + } + public override async Task> findMatch(Measurement spectrum) { logger.debug("Library.findMatch: trying to match spectrum"); diff --git a/Pages/AnalysisPage.xaml b/Pages/AnalysisPage.xaml index 963a96f..1a867ad 100644 --- a/Pages/AnalysisPage.xaml +++ b/Pages/AnalysisPage.xaml @@ -128,6 +128,8 @@ +