diff --git a/AppShell.xaml b/AppShell.xaml index b53f6d5..8fb7b76 100644 --- a/AppShell.xaml +++ b/AppShell.xaml @@ -6,6 +6,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:EnlightenMAUI" Shell.FlyoutBehavior="Disabled" + Shell.TabBarBackgroundColor="#333" + Shell.TabBarForegroundColor="#EEE" + Shell.TabBarDisabledColor="#555" + Shell.TabBarUnselectedColor="#BBB" + Shell.TabBarTitleColor="#EEE" Title="EnlightenMAUI"> diff --git a/EnlightenMAUI.csproj b/EnlightenMAUI.csproj index 5a3876a..36f1972 100644 --- a/EnlightenMAUI.csproj +++ b/EnlightenMAUI.csproj @@ -51,7 +51,7 @@ False - 0.9.56 + 0.9.60 EnlightenMobile $(DefineConstants);USE_DECON .so @@ -59,7 +59,7 @@ False - 0.9.56 + 0.9.60 EnlightenMobile apk .so @@ -92,6 +92,8 @@ + + @@ -185,6 +187,9 @@ MSBuild:Compile + + MSBuild:Compile + MSBuild:Compile @@ -214,14 +219,13 @@ - PreserveNewest - + PreserveNewest diff --git a/MauiProgram.cs b/MauiProgram.cs index 77beccc..4ac887a 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -6,6 +6,11 @@ using EnlightenMAUI.Models; using System.Reflection; using Telerik.Maui.Controls.Compatibility; +using Android.Content.Res; + +#if ANDROID +using AndroidX.AppCompat.Widget; +#endif namespace EnlightenMAUI; @@ -38,6 +43,50 @@ public static MauiApp CreateMauiApp() builder.Services.AddTransient(); + Microsoft.Maui.Handlers.RadioButtonHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) => + { +#if ANDROID + if (handler.PlatformView is Android.Widget.RadioButton radioButton) + { + var states = new int[][] + { + new int[] { Android.Resource.Attribute.StateChecked }, + new int[] { -Android.Resource.Attribute.StateChecked } + }; + + // Define the colors corresponding to the states (border color) + var colors = new int[] + { + new Android.Graphics.Color(0x27, 0xc0, 0xa1), + new Android.Graphics.Color(0x27, 0xc0, 0xa1) + }; + + // Create a ColorStateList with the states and colors + var colorStateList = new Android.Content.Res.ColorStateList(states, colors); + + // Apply the color tint to the button's border + radioButton.ButtonTintList = colorStateList; + } +#endif + }); + + /* + Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) => + { +#if ANDROID + //handler.PlatformView.BorderStyle = Android.Graphics.Color.White; + var pv = handler.PlatformView; + if (pv is AppCompatEditText editText) + { + editText.Background?.Mutate(); + var color = new Android.Graphics.Color(0x27, 0xc0, 0xa1); + var hintColor = new Android.Graphics.Color(0xBB, 0xBB, 0xBB); + editText.BackgroundTintList = ColorStateList.ValueOf(Android.Graphics.Color.Transparent); + //editText.SetHintTextColor(hintColor); + } +#endif + }); + */ #if DEBUG builder.Logging.AddDebug(); diff --git a/Models/API6BLESpectrometer.cs b/Models/API6BLESpectrometer.cs index b45c8fb..35a893a 100644 --- a/Models/API6BLESpectrometer.cs +++ b/Models/API6BLESpectrometer.cs @@ -700,7 +700,7 @@ public async Task updateRSSI() logger.debug("current RSSI {0}", rssi); NotifyPropertyChanged("rssi"); await Task.Delay(500); - if (rssi < -90) + if (rssi < -105) { ++badSignalCount; @@ -845,7 +845,7 @@ public override async Task takeOneAveragedAsync() stretchedDark = new double[smoothed.Length]; measurement.dark = stretchedDark; measurement.postProcessed = smoothed; - measurement.processingMethod = "Noise and Background Removal"; + measurement.processingMethod = PlatformUtil.modelName; } else { @@ -853,7 +853,8 @@ public override async Task takeOneAveragedAsync() double[] newIntensities = Wavecal.mapWavenumbers(wavenumbers, measurement.processed, staticWavenumbers); measurement.wavenumbers = staticWavenumbers; measurement.postProcessed = newIntensities; - } + measurement.processingMethod = ""; + } //////////////////////////////////////////////////////////////////////// // Store Measurement diff --git a/Models/API9BLESpectrometer.cs b/Models/API9BLESpectrometer.cs index 12088c1..5012f84 100644 --- a/Models/API9BLESpectrometer.cs +++ b/Models/API9BLESpectrometer.cs @@ -1179,7 +1179,7 @@ public async Task updateRSSI() logger.debug("current RSSI {0}", rssi); NotifyPropertyChanged("rssi"); await Task.Delay(500); - if (rssi < -90) + if (rssi < -105) { ++badSignalCount; @@ -1322,7 +1322,7 @@ public override async Task takeOneAveragedAsync() measurement.rawDark = dark; measurement.dark = stretchedDark; measurement.postProcessed = smoothed; - measurement.processingMethod = "Noise and Background Removal"; + measurement.processingMethod = PlatformUtil.modelName; } else { @@ -1330,7 +1330,8 @@ public override async Task takeOneAveragedAsync() double[] newIntensities = Wavecal.mapWavenumbers(wavenumbers, measurement.processed, staticWavenumbers); measurement.wavenumbers = staticWavenumbers; measurement.postProcessed = newIntensities; - } + measurement.processingMethod = ""; + } //////////////////////////////////////////////////////////////////////// // Store Measurement diff --git a/Models/BluetoothSpectrometer.cs b/Models/BluetoothSpectrometer.cs index c701eb5..3dfc4a7 100644 --- a/Models/BluetoothSpectrometer.cs +++ b/Models/BluetoothSpectrometer.cs @@ -1230,7 +1230,7 @@ public async Task updateRSSI() logger.debug("current RSSI {0}", rssi); NotifyPropertyChanged("rssi"); await Task.Delay(500); - if (rssi < -90) + if (rssi < -105) { ++badSignalCount; @@ -1373,7 +1373,7 @@ public override async Task takeOneAveragedAsync() measurement.rawDark = dark; measurement.dark = stretchedDark; measurement.postProcessed = smoothed; - measurement.processingMethod = "Noise and Background Removal"; + measurement.processingMethod = PlatformUtil.modelName; } else { @@ -1381,6 +1381,7 @@ public override async Task takeOneAveragedAsync() double[] newIntensities = Wavecal.mapWavenumbers(wavenumbers, measurement.processed, staticWavenumbers); measurement.wavenumbers = staticWavenumbers; measurement.postProcessed = newIntensities; + measurement.processingMethod = ""; } //////////////////////////////////////////////////////////////////////// diff --git a/Models/Measurement.cs b/Models/Measurement.cs index 3fe2615..269e7f8 100644 --- a/Models/Measurement.cs +++ b/Models/Measurement.cs @@ -6,6 +6,12 @@ namespace EnlightenMAUI.Models; +public class AgnosticSimpleMeasurement +{ + public Tuple, List> data; + public Dictionary metadata; +} + public class spectrumJSON { public string id; @@ -225,7 +231,7 @@ public double[] postProcessed public double excitationNM { get; set; } public float[] wavecalCoeffs { get; set; } public double? laserPower { get; set; } - public string processingMethod { get; set; } = "Wavenumber Interpolation Only"; + public string processingMethod { get; set; } = ""; //////////////////////////////////////////////////////////////////////// // Methods @@ -559,7 +565,7 @@ public void postProcess() /// - support full ENLIGHTEN metadata /// - support SaveOptions (selectable output fields) /// - public async Task saveAsync(bool librarySave = false, bool autoSave = false) + public async Task saveAsync(bool librarySave = false, bool autoSave = false, bool forceWrite = false, Dictionary forcedMetadata = null) { logger.debug("Measurement.saveAsync: starting"); @@ -584,7 +590,7 @@ public async Task saveAsync(bool librarySave = false, bool autoSave = fals return true; } - if (processed is null || raw is null || spec is null) + if ((processed is null || raw is null || spec is null) && !forceWrite) { logger.error("saveAsync: nothing to save"); return false; @@ -593,9 +599,15 @@ 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, PlatformUtil.getFileName(filename)); + using (StreamWriter sw = new StreamWriter(pathname)) { - writeMetadata(sw); + if (forcedMetadata != null) + writeMetadata(sw, forcedMetadata); + else if (spec != null) + writeMetadata(sw); sw.WriteLine(); writeSpectra(sw, librarySave); } @@ -656,7 +668,8 @@ void writeMetadata(StreamWriter sw) sw.WriteLine("Laser Wavelength, {0}", spec.eeprom.laserExcitationWavelengthNMFloat); sw.WriteLine("Timestamp, {0}", timestamp.ToString("dd/MM/yyyy HH:mm:ss.fff")); sw.WriteLine("Library Used, {0}", libraryUsed); - sw.WriteLine("Processing Method, {0}", spec.measurement.processingMethod); + sw.WriteLine("DalaiRamanID.DALAI Enabled, {0}", spec.measurement.processingMethod.Length > 0); + sw.WriteLine("DalaiRamanID.DALAI Model, {0}", spec.measurement.processingMethod); if (spec.measurement.declaredScore.HasValue) { if (spec.measurement.declaredMatch.Length > 0) @@ -680,7 +693,15 @@ void writeMetadata(StreamWriter sw) sw.WriteLine("QR Scan, {0}", spec.qrValue); sw.WriteLine("Host Description, {0}", settings.hostDescription); if (location != null) - sw.WriteLine("Location, lat {0}, lon {1}", location.Latitude, location.Longitude); + sw.WriteLine("Location, lat {0} : lon {1}", location.Latitude, location.Longitude); + } + + void writeMetadata(StreamWriter sw, Dictionary metadata) + { + foreach (string key in metadata.Keys) + { + sw.WriteLine($"{key},{metadata[key]}"); + } } string render(double[] a, int index, string format = "f2") 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/USBSpectrometer.cs b/Models/USBSpectrometer.cs index 84dcc09..e2c402c 100644 --- a/Models/USBSpectrometer.cs +++ b/Models/USBSpectrometer.cs @@ -526,7 +526,7 @@ public override async Task takeOneAveragedAsync() measurement.rawDark = dark; measurement.dark = stretchedDark; measurement.postProcessed = smoothed; - measurement.processingMethod = "Noise and Background Removal"; + measurement.processingMethod = PlatformUtil.modelName; } else { @@ -534,6 +534,7 @@ public override async Task takeOneAveragedAsync() double[] newIntensities = Wavecal.mapWavenumbers(wavenumbers, measurement.processed, staticWavenumbers); measurement.wavenumbers = staticWavenumbers; measurement.postProcessed = newIntensities; + measurement.processingMethod = ""; } //////////////////////////////////////////////////////////////////////// diff --git a/Models/UserLibrary.cs b/Models/UserLibrary.cs new file mode 100644 index 0000000..cf7bb78 --- /dev/null +++ b/Models/UserLibrary.cs @@ -0,0 +1,62 @@ +using EnlightenMAUI.Platforms; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EnlightenMAUI.Models +{ + internal class UserLibrary + { + static UserLibrary instance = null; + + Settings settings = Settings.getInstance(); + public Dictionary userSpectra = new Dictionary(); + public List userSpectraKeys + { + get + { + return new List(userSpectra.Keys); + } + } + + 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(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: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); + } + } + + public void addSpectrum(Measurement m, string tag) + { + 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); + userSpectra.Add(userTag, tag); + } + } + } +} diff --git a/Models/WPLibrary.cs b/Models/WPLibrary.cs index 5aaa89b..cab018d 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; @@ -23,14 +24,20 @@ public class SimpleCSVParser { int colWavenumber = 0; int colIntensity = 1; - int VIGNETTE_START = 3; // drop the first 3 pixels + public int vignetteStart = 3; // drop the first 3 pixels int VIGNETTE_COUNT = int.MaxValue; // For now don't vignette the end string state; public List wavenumbers = new List(); public List intensities = new List(); + public Dictionary metadata = new Dictionary(); 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 +59,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,19 +89,24 @@ 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) { if (intensities.Count >= VIGNETTE_COUNT) return; - if (linecount++ < VIGNETTE_START) + if (linecount++ < vignetteStart) return; } double wavenumber = Convert.ToDouble(tok[colWavenumber]); double intensity = Convert.ToDouble(tok[colIntensity]); + //logger.info("parsed library values {0} : {1}", wavenumber, intensity); + wavenumbers.Add(wavenumber); intensities.Add(intensity); } @@ -97,6 +124,221 @@ public async Task parseFile(string pathname) return await parseStream(stream); } + public async Task parseStream(Stream stream, bool strictHeaders = false, string tag = null) + { + this.strictHeaders = strictHeaders; + state = "READING_METADATA"; + string line; + + if (tag != null) + logger.info("starting read for {0}", tag); + + 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 (tag != null) + logger.info("attempting metadata read for {0}", tag); + + 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(); + + metadata.Add(key, value); + + 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") + { + if (tag != null) + logger.info("attempting header read for {0}", tag); + if (line.Length == 0) + { + // skip extra blank + } + else if (isNum(line)) + { + state = "READING_DATA"; + readValues(tok); + } + else + { + readHeader(tok); + state = "READING_DATA"; + if (tag != null) + logger.info("finished header read for {0}, col wn = {1}, col int = {2}", tag, colWavenumber, colIntensity); + } + } + else if (state == "READING_DATA") + { + readValues(tok); + } + else + { + errorType = ErrorTypes.INVALID_STATE; + return false; + } + } + } + logger.info("finished full read for {0}", tag); + return true; + } + + 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 Dictionary> metadata = new Dictionary>(); + public Dictionary> unassignedMetadata = 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) + { + int totalSamples = 0; + for (int i = 0; i < tok.Count; i++) + { + string s = tok[i].ToLower(); + if (s != "wavenumber" && s.Length > 1) + { + ++totalSamples; + } + } + + 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); + Dictionary unitMetadata = new Dictionary(); + + foreach (string metaKey in unassignedMetadata.Keys) + { + if (unassignedMetadata[metaKey].Count == totalSamples) + unitMetadata.Add(metaKey, unassignedMetadata[metaKey][entryCols.Count - 1]); + else if (unassignedMetadata[metaKey].Count == 1) + unitMetadata.Add(metaKey, unassignedMetadata[metaKey][0]); + else + unitMetadata.Add(metaKey, ""); + } + + metadata.Add(s, unitMetadata); + + 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"; @@ -134,6 +376,17 @@ public async Task parseStream(Stream stream) { // process metadata string key = tok[0].Trim().ToLower(); + + List metadatas = new List(); + + for (int j = 1; j < tok.Count; j++) + { + if (tok[j].Trim().Length > 0) + metadatas.Add(tok[j].Trim()); + } + + unassignedMetadata.Add(key, metadatas); + string value = tok[1].Trim(); if (key == "label") @@ -151,6 +404,12 @@ public async Task parseStream(Stream stream) 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); @@ -358,6 +617,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].data.Item1, data[sample].data.Item2, data[sample].metadata); + } + } + } + } + + public async Task handleSampleImport(string sample, List wavenumbers, List intensities, Dictionary metadata) + { + 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, forceWrite: true, forcedMetadata: metadata); + } + 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..cb8a1b0 100644 --- a/Pages/AnalysisPage.xaml +++ b/Pages/AnalysisPage.xaml @@ -67,66 +67,90 @@ -