diff --git a/app.jl b/app.jl index 79525d7..fda643e 100644 --- a/app.jl +++ b/app.jl @@ -22,7 +22,7 @@ using Dates using Base.Threads # Bring MSIData into App module's scope -using .MSI_src: MSIData, OpenMSIData, process_spectrum, IterateSpectra, ImzMLSource, _iterate_spectra_fast, MzMLSource, find_mass, ViridisPalette, get_mz_slice, get_multiple_mz_slices, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile, generate_colorbar_image, load_and_prepare_mask, set_global_mz_range!, main_precalculation, MutableSpectrum +using .MSI_src: MSIData, OpenMSIData, process_spectrum, IterateSpectra, ImzMLSource, _iterate_spectra_fast, MzMLSource, find_mass, ViridisPalette, get_mz_slice, get_multiple_mz_slices, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile, generate_colorbar_image, load_and_prepare_mask, set_global_mz_range!, main_precalculation, MutableSpectrum, execute_full_preprocessing if !@isdefined(increment_image) include("./julia_imzML_visual.jl") @@ -67,6 +67,30 @@ else log_memory_usage(context::String, msi_data_val) = nothing # No-op for production end +function validate_parse(validation_errors::Vector{String}, param_str::String, param_name::String, target_type::Type, step_name::String) + println("DEBUG: Validating ($step_name) Parameter '$param_name'. Received value: '$param_str'") + if isempty(param_str) + push!(validation_errors, "($step_name) Parameter '$param_name' is empty.") + return nothing + end + val = tryparse(target_type, param_str) + if val === nothing + push!(validation_errors, "($step_name) Parameter '$param_name' ('$param_str') is not a valid $(target_type).") + return nothing + end + return val +end + +# Helper function to check if a pipeline step is enabled +function is_step_enabled(step_name::String, pipeline_order::Vector{Dict{String, Any}}) + for step in pipeline_order + if get(step, "name", "") == step_name + return get(step, "enabled", false) + end + end + return false # Default to disabled if step not found +end + @genietools # == Reactive code == @@ -116,6 +140,7 @@ end @in compareBtn=false # To open dialog @in createMeanPlot=false # To generate mean spectrum plot @in createXYPlot=false # To generate an spectrum plot according to the xy values inputed + @in createNSpectrumPlot=false # To generate an spectrum plot according to spectrum order @in createSumPlot=false # To generate a sum of all the spectrum plots @in image3dPlot=false # To generate 3d plot based on current image @in triq3dPlot=false # To generate 3d plot based on current triq image @@ -264,7 +289,6 @@ end Dict("name" => "normalization", "label" => "Normalization", "enabled" => true), Dict("name" => "peak_binning", "label" => "Peak Binning", "enabled" => true) ] - @in preprocessing_mask_route = "" @in selected_spectrum_id_for_plot = 1 @in feature_matrix_result = nothing @in bin_info_result = nothing @@ -274,41 +298,21 @@ end Dict("mz" => 155.0349, "label" => "DHB_M+H"), ] - # --- Methods for Reference Peaks List --- - function addReferencePeak() - push!(reference_peaks_list, Dict("mz" => 0.0, "label" => "")) - reference_peaks_list = deepcopy(reference_peaks_list) # Force reactivity - end - - function removeReferencePeak(index::Int) - deleteat!(reference_peaks_list, index) - reference_peaks_list = deepcopy(reference_peaks_list) # Force reactivity - end - - # Step reordering functions - function moveStepUp(index::Int) - if index > 1 - pipeline_step_order = deepcopy(pipeline_step_order) - temp = pipeline_step_order[index] - pipeline_step_order[index] = pipeline_step_order[index-1] - pipeline_step_order[index-1] = temp - pipeline_step_order = pipeline_step_order # Force reactivity - end - end - - function moveStepDown(index::Int) - if index < length(pipeline_step_order) - pipeline_step_order = deepcopy(pipeline_step_order) - temp = pipeline_step_order[index] - pipeline_step_order[index] = pipeline_step_order[index+1] - pipeline_step_order[index+1] = temp - pipeline_step_order = pipeline_step_order # Force reactivity - end - end @in save_feature_matrix_btn = false + @in recalculate_suggestions_btn = false + @in addReferencePeak = false + @in removeReferencePeak = false + @in moveStepUp = false + @in moveStepDown = false # Trigger for running the full pipeline @in run_full_pipeline = false + @in enable_standards = true + @in action_index = -1 + @in remove_peak_trigger = false + @in move_step_up_trigger = false + @in move_step_down_trigger = false + @in toggle_step_trigger = false @out current_pipeline_step = "" # To indicate which step is currently running in the full pipeline @in export_params_btn = false @@ -453,7 +457,13 @@ end showgrid=true, tickformat = ".3g" ), - margin=attr(l=0,r=0,t=120,b=0,pad=0) + margin=attr(l=0,r=0,t=120,b=0,pad=0), + legend=attr( + x=1.0, + y=1.0, + xanchor="right", + yanchor="top" + ) ) # Dummy 2D scatter plot traceSpectra=PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines", marker=attr(size=1, color="blue", opacity=0.1)) @@ -558,7 +568,7 @@ end msg = "Opening file: $(basename(picked_route))..." try - dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML|mzml))$ "i => "") + dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML|mzml))$"i => "") registry = load_registry(registry_path) existing_entry = get(registry, dataset_name, nothing) @@ -938,119 +948,339 @@ end @onbutton run_full_pipeline begin progressPrep = true current_pipeline_step = "Initializing..." + println("DEBUG: run_full_pipeline started.") + # println("DEBUG: Current pipeline_step_order configuration: $pipeline_step_order") try - # 1. Data Preparation - if isempty(selected_folder_main) - msg = "No dataset selected. Please process a file first." + # --- 1. Initial Checks and Data Loading --- + println("DEBUG: Performing initial checks and data loading...") + if msi_data === nothing || isempty(selected_folder_main) + msg = "No dataset loaded. Please load a file using 'Select an imzMl / mzML file'." warning_msg = true + println("DEBUG: $msg") return end registry = load_registry(registry_path) - entry = registry[selected_folder_main] + entry = get(registry, selected_folder_main, nothing) + if entry === nothing + msg = "Selected dataset '$(selected_folder_main)' not found in registry. Please reload the file." + warning_msg = true + println("DEBUG: $msg") + return + end target_path = entry["source_path"] - if msi_data === nothing || full_route != target_path - if msi_data !== nothing - close(msi_data) - end + # Ensure msi_data is for the currently selected file + if full_route != target_path + println("DEBUG: Active file path changed. Reloading MSI data: $(basename(target_path))") + if msi_data !== nothing; close(msi_data); end + msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) + else + println("DEBUG: Using already loaded MSI data for $(basename(target_path)).") end - # Apply mask if enabled + # Mask path retrieval from registry + local mask_path_for_pipeline::Union{String, Nothing} = nothing + if maskEnabled + println("DEBUG: Masking is ENABLED.") + if get(entry, "has_mask", false) + mask_path_candidate = get(entry, "mask_path", "") + if isfile(mask_path_candidate) + mask_path_for_pipeline = mask_path_candidate + println("DEBUG: Using mask for pipeline: $(mask_path_for_pipeline)") + else + msg = "Mask enabled but file not found: $(mask_path_candidate). Aborting pipeline." + warning_msg = true + @warn msg + println("DEBUG: $msg") + return + end + else + msg = "Mask enabled but no valid mask entry found for: $(selected_folder_main). Aborting pipeline." + warning_msg = true + @warn msg + println("DEBUG: $msg") + return + end + else + println("DEBUG: Masking is DISABLED. No mask will be applied.") + end + + # Apply mask if enabled to get indices to process spectrum_indices_to_process = collect(1:length(msi_data.spectra_metadata)) - if maskEnabled && !isempty(preprocessing_mask_route) && isfile(preprocessing_mask_route) - current_pipeline_step = "Loading mask..." - mask_matrix = load_and_prepare_mask(preprocessing_mask_route, msi_data.image_dims) + if mask_path_for_pipeline !== nothing + current_pipeline_step = "Applying mask..." + println("DEBUG: Applying mask matrix to filter spectra...") + mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, msi_data.image_dims) masked_indices_set = get_masked_spectrum_indices(msi_data, mask_matrix) spectrum_indices_to_process = collect(masked_indices_set) + if isempty(spectrum_indices_to_process) + msg = "No spectra remaining after applying mask. Aborting pipeline." + warning_msg = true + println("DEBUG: $msg") + return + end + println("DEBUG: $(length(spectrum_indices_to_process)) spectra remaining after mask application.") + else + println("DEBUG: No mask applied. Processing all $(length(msi_data.spectra_metadata)) spectra.") end # Initialize spectra data structure current_pipeline_step = "Loading spectra..." + println("DEBUG: Loading $(length(spectrum_indices_to_process)) spectra into MutableSpectrum objects...") current_spectra = Vector{MutableSpectrum}(undef, length(spectrum_indices_to_process)) - Threads. @threads for i in 1:length(spectrum_indices_to_process) + Threads.@threads for i in 1:length(spectrum_indices_to_process) original_idx = spectrum_indices_to_process[i] - mz, intensity = GetSpectrum(msi_data, original_idx) - current_spectra[i] = MutableSpectrum(original_idx, mz, intensity, []) + mz, intensity = GetSpectrum(msi_data, original_idx) # Fetch mz and intensity for the current spectrum + current_spectra[i] = MutableSpectrum(original_idx, Float64.(mz), Float64.(intensity), NamedTuple{(:mz, :intensity, :fwhm, :shape_r2, :snr, :prominence), NTuple{6, Float64}}[]) + end + println("DEBUG: All spectra loaded into temporary structure for processing.") + + + # --- 2. Parameter Assembly with Validation --- + current_pipeline_step = "Configuring parameters..." + println("DEBUG: Configuring parameters and validating enabled steps...") + ref_peaks = Dict{Float64, String}(p["mz"] => p["label"] for p in reference_peaks_list) + + final_params = Dict{Symbol, Dict{Symbol, Any}}() + validation_errors = String[] + + # --- Stabilization --- + println("DEBUG: Checking Stabilization step (name: stabilization)") + if is_step_enabled("stabilization", pipeline_step_order) + println("DEBUG: Stabilization step is ENABLED. Setting method: $(stabilization_method).") + final_params[:Stabilization] = Dict{Symbol, Any}(:method => Symbol(stabilization_method)) + else + println("DEBUG: Stabilization step is DISABLED. Skipping.") end - # 2. Parameter Assembly - current_pipeline_step = "Configuring parameters..." - final_params = Dict( - :Stabilization => Dict(:method => Symbol(stabilization_method)), - :Smoothing => Dict( + # --- Smoothing --- + println("DEBUG: Checking Smoothing step (name: smoothing)") + if is_step_enabled("smoothing", pipeline_step_order) + println("DEBUG: Smoothing step is ENABLED. Validating parameters.") + window_val = validate_parse(validation_errors, smoothing_window, "Window", Int, "Smoothing") + order_val = validate_parse(validation_errors, smoothing_order, "Order", Int, "Smoothing") + final_params[:Smoothing] = Dict{Symbol, Any}( :method => Symbol(smoothing_method), - :window => parse(Int, smoothing_window), - :order => parse(Int, smoothing_order) - ), - :BaselineCorrection => Dict( - :method => Symbol(baseline_method), - :iterations => parse(Int, baseline_iterations), - :window => parse(Int, baseline_window) - ), - :Normalization => Dict(:method => Symbol(normalization_method)), - :PeakPicking => Dict( - :method => Symbol(peak_picking_method), - :snr_threshold => parse(Float64, peak_picking_snr_threshold), - :half_window => parse(Int, peak_picking_half_window), - :min_peak_prominence => parse(Float64, peak_picking_min_peak_prominence), - :merge_peaks_tolerance => parse(Float64, peak_picking_merge_peaks_tolerance) - ), - :PeakSelection => Dict( - :min_snr => parse(Float64, peak_selection_min_snr), - :min_fwhm_ppm => parse(Float64, peak_selection_min_fwhm_ppm), - :max_fwhm_ppm => parse(Float64, peak_selection_max_fwhm_ppm), - :min_shape_r2 => parse(Float64, peak_selection_min_shape_r2) - ), - :Calibration => Dict( - :method => :internal_standards, - :ppm_tolerance => parse(Float64, calibration_ppm_tolerance), - :fit_order => parse(Int, calibration_fit_order) - ), - :PeakAlignment => Dict( - :method => Symbol(alignment_method), - :tolerance => parse(Float64, alignment_tolerance), - :tolerance_unit => Symbol(alignment_tolerance_unit) - ), - :PeakBinning => Dict( - :method => Symbol(binning_method), - :tolerance => parse(Float64, binning_tolerance), - :tolerance_unit => Symbol(binning_tolerance_unit), - :min_peak_per_bin => parse(Int, binning_min_peak_per_bin) + :window => something(window_val, 9), + :order => something(order_val, 2) ) - ) + if window_val !== nothing && window_val < 1 + push!(validation_errors, "(Smoothing) Window must be positive.") + end + if order_val !== nothing && order_val < 0 + push!(validation_errors, "(Smoothing) Order must be non-negative.") + end + println("DEBUG: Smoothing parameters set: method=$(smoothing_method), window=$(something(window_val, 9)), order=$(something(order_val, 2)).") + else + println("DEBUG: Smoothing step is DISABLED. Skipping parameter validation.") + end + + # --- Baseline Correction --- + println("DEBUG: Checking Baseline Correction step (name: baseline_correction)") + if is_step_enabled("baseline_correction", pipeline_step_order) + println("DEBUG: Baseline Correction step is ENABLED. Validating parameters.") + iterations_val = validate_parse(validation_errors, baseline_iterations, "Iterations", Int, "Baseline Correction") + baseline_window_val = validate_parse(validation_errors, baseline_window, "Window", Int, "Baseline Correction") + final_params[:BaselineCorrection] = Dict{Symbol, Any}( + :method => Symbol(baseline_method), + :iterations => something(iterations_val, 100), + :window => something(baseline_window_val, 20) + ) + if iterations_val !== nothing && iterations_val < 0 + push!(validation_errors, "(Baseline Correction) Iterations must be non-negative.") + end + if baseline_window_val !== nothing && baseline_window_val < 1 + push!(validation_errors, "(Baseline Correction) Window must be positive.") + end + println("DEBUG: Baseline Correction parameters set: method=$(baseline_method), iterations=$(something(iterations_val, 100)), window=$(something(baseline_window_val, 20)).") + else + println("DEBUG: Baseline Correction step is DISABLED. Skipping parameter validation.") + end + + # --- Normalization --- + println("DEBUG: Checking Normalization step (name: normalization)") + if is_step_enabled("normalization", pipeline_step_order) + println("DEBUG: Normalization step is ENABLED. Setting method: $(normalization_method).") + final_params[:Normalization] = Dict{Symbol, Any}(:method => Symbol(normalization_method)) + else + println("DEBUG: Normalization step is DISABLED. Skipping.") + end + + + # --- Peak Picking --- + println("DEBUG: Checking Peak Picking step (name: peak_picking)") + if is_step_enabled("peak_picking", pipeline_step_order) + println("DEBUG: Peak Picking step is ENABLED. Validating parameters.") + snr_threshold_val = validate_parse(validation_errors, peak_picking_snr_threshold, "SNR Threshold", Float64, "Peak Picking") + half_window_val = validate_parse(validation_errors, peak_picking_half_window, "Half Window", Int, "Peak Picking") + min_peak_prominence_val = validate_parse(validation_errors, peak_picking_min_peak_prominence, "Min Prominence", Float64, "Peak Picking") + merge_peaks_tolerance_val = validate_parse(validation_errors, peak_picking_merge_peaks_tolerance, "Merge Tolerance", Float64, "Peak Picking") + final_params[:PeakPicking] = Dict{Symbol, Any}( + :method => Symbol(peak_picking_method), + :snr_threshold => something(snr_threshold_val, 3.0), + :half_window => something(half_window_val, 10), + :min_peak_prominence => something(min_peak_prominence_val, 0.1), + :merge_peaks_tolerance => something(merge_peaks_tolerance_val, 0.002) + ) + if snr_threshold_val !== nothing && snr_threshold_val < 0 + push!(validation_errors, "(Peak Picking) SNR Threshold must be non-negative.") + end + if half_window_val !== nothing && half_window_val < 1 + push!(validation_errors, "(Peak Picking) Half Window must be positive.") + end + if min_peak_prominence_val !== nothing && (min_peak_prominence_val < 0 || min_peak_prominence_val > 1) + push!(validation_errors, "(Peak Picking) Min Prominence must be between 0 and 1.") + end + if merge_peaks_tolerance_val !== nothing && merge_peaks_tolerance_val < 0 + push!(validation_errors, "(Peak Picking) Merge Tolerance must be non-negative.") + end + println("DEBUG: Peak Picking parameters set: method=$(peak_picking_method), snr_threshold=$(something(snr_threshold_val, 3.0)), half_window=$(something(half_window_val, 10))...") + else + println("DEBUG: Peak Picking step is DISABLED. Skipping parameter validation.") + end + + # --- Peak Selection --- + println("DEBUG: Checking Peak Selection step (name: peak_selection)") + if is_step_enabled("peak_selection", pipeline_step_order) + println("DEBUG: Peak Selection step is ENABLED. Validating parameters.") + min_snr_val = validate_parse(validation_errors, peak_selection_min_snr, "Min SNR", Float64, "Peak Selection") + min_fwhm_ppm_val = validate_parse(validation_errors, peak_selection_min_fwhm_ppm, "Min FWHM", Float64, "Peak Selection") + max_fwhm_ppm_val = validate_parse(validation_errors, peak_selection_max_fwhm_ppm, "Max FWHM", Float64, "Peak Selection") + min_shape_r2_val = validate_parse(validation_errors, peak_selection_min_shape_r2, "Min Shape R2", Float64, "Peak Selection") + final_params[:PeakSelection] = Dict{Symbol, Any}( + :min_snr => something(min_snr_val, 0.0), + :min_fwhm_ppm => something(min_fwhm_ppm_val, 0.0), + :max_fwhm_ppm => something(max_fwhm_ppm_val, Inf), + :min_shape_r2 => something(min_shape_r2_val, 0.0) + ) + if min_snr_val !== nothing && min_snr_val < 0 + push!(validation_errors, "(Peak Selection) Min SNR must be non-negative.") + end + if min_fwhm_ppm_val !== nothing && min_fwhm_ppm_val < 0 + push!(validation_errors, "(Peak Selection) Min FWHM must be non-negative.") + end + if max_fwhm_ppm_val !== nothing && max_fwhm_ppm_val < 0 + push!(validation_errors, "(Peak Selection) Max FWHM must be non-negative.") + end + if min_shape_r2_val !== nothing && (min_shape_r2_val < 0 || min_shape_r2_val > 1) + push!(validation_errors, "(Peak Selection) Min Shape R2 must be between 0 and 1.") + end + println("DEBUG: Peak Selection parameters set: min_snr=$(something(min_snr_val, 0.0)), min_fwhm_ppm=$(something(min_fwhm_ppm_val, 0.0))...") + else + println("DEBUG: Peak Selection step is DISABLED. Skipping parameter validation.") + end + + + # --- Calibration --- + println("DEBUG: Checking Calibration step (name: calibration)") + if is_step_enabled("calibration", pipeline_step_order) + println("DEBUG: Calibration step is ENABLED. Validating parameters.") + ppm_tolerance_cal_val = validate_parse(validation_errors, calibration_ppm_tolerance, "PPM Tolerance", Float64, "Calibration") + fit_order_val = validate_parse(validation_errors, calibration_fit_order, "Fit Order", Int, "Calibration") + final_params[:Calibration] = Dict{Symbol, Any}( + :method => :internal_standards, # Fixed method + :ppm_tolerance => something(ppm_tolerance_cal_val, 20.0), + :fit_order => something(fit_order_val, 1) # Default to linear + ) + if ppm_tolerance_cal_val !== nothing && ppm_tolerance_cal_val < 0 + push!(validation_errors, "(Calibration) PPM Tolerance must be non-negative.") + end + if fit_order_val !== nothing && (fit_order_val < 0 || fit_order_val > 2) + push!(validation_errors, "(Calibration) Fit Order must be 0, 1, or 2.") + end + if enable_standards && isempty(ref_peaks) + push!(validation_errors, "(Calibration) Internal Standards are enabled, but no reference peaks are defined.") + end + println("DEBUG: Calibration parameters set: ppm_tolerance=$(something(ppm_tolerance_cal_val, 20.0)), fit_order=$(something(fit_order_val, 1)).") + else + println("DEBUG: Calibration step is DISABLED. Skipping parameter validation.") + end + + + # --- Peak Alignment --- + println("DEBUG: Checking Peak Alignment step (name: peak_alignment)") + if is_step_enabled("peak_alignment", pipeline_step_order) + println("DEBUG: Peak Alignment step is ENABLED. Validating parameters.") + alignment_tolerance_val = validate_parse(validation_errors, alignment_tolerance, "Tolerance", Float64, "Peak Alignment") + final_params[:PeakAlignment] = Dict{Symbol, Any}( + :method => Symbol(alignment_method), + :tolerance => something(alignment_tolerance_val, 0.002), + :tolerance_unit => Symbol(alignment_tolerance_unit) + ) + if alignment_tolerance_val !== nothing && alignment_tolerance_val < 0 + push!(validation_errors, "(Peak Alignment) Tolerance must be non-negative.") + end + println("DEBUG: Peak Alignment parameters set: method=$(alignment_method), tolerance=$(something(alignment_tolerance_val, 0.002)), tolerance_unit=$(alignment_tolerance_unit).") + else + println("DEBUG: Peak Alignment step is DISABLED. Skipping parameter validation.") + end + + + # --- Peak Binning --- + println("DEBUG: Checking Peak Binning step (name: peak_binning)") + if is_step_enabled("peak_binning", pipeline_step_order) + println("DEBUG: Peak Binning step is ENABLED. Validating parameters.") + binning_tolerance_val = validate_parse(validation_errors, binning_tolerance, "Tolerance", Float64, "Peak Binning") + min_peak_per_bin_val = validate_parse(validation_errors, binning_min_peak_per_bin, "Min Peaks Per Bin", Int, "Peak Binning") + final_params[:PeakBinning] = Dict{Symbol, Any}( + :method => Symbol(binning_method), + :tolerance => something(binning_tolerance_val, 20.0), + :tolerance_unit => Symbol(binning_tolerance_unit), + :min_peak_per_bin => something(min_peak_per_bin_val, 3) + ) + if binning_tolerance_val !== nothing && binning_tolerance_val < 0 + push!(validation_errors, "(Peak Binning) Tolerance must be non-negative.") + end + if min_peak_per_bin_val !== nothing && min_peak_per_bin_val < 1 + push!(validation_errors, "(Peak Binning) Min Peaks Per Bin must be positive.") + end + println("DEBUG: Peak Binning parameters set: method=$(binning_method), tolerance=$(something(binning_tolerance_val, 20.0)), min_peak_per_bin=$(something(min_peak_per_bin_val, 3))...") + else + println("DEBUG: Peak Binning step is DISABLED. Skipping parameter validation.") + end + + + if !isempty(validation_errors) + msg = "Pipeline setup errors:\n" * join(validation_errors, "\n") + warning_msg = true + println("DEBUG: Validation errors encountered: $validation_errors") + return + end # Build pipeline steps from enabled steps in order pipeline_stp = [step["name"] for step in pipeline_step_order if step["enabled"]] + println("DEBUG: Final enabled pipeline steps to execute: $pipeline_stp") - # Convert reference peaks - ref_peaks = Dict(p["mz"] => p["label"] for p in reference_peaks_list) - # 3. Execute Pipeline current_pipeline_step = "Running preprocessing pipeline..." + println("DEBUG: Starting pipeline execution with $(length(pipeline_stp)) enabled steps.") feature_matrix_result, bin_info_result = execute_full_preprocessing( current_spectra, final_params, pipeline_stp, ref_peaks, - maskEnabled ? preprocessing_mask_route : nothing + mask_path_for_pipeline ) do step current_pipeline_step = "Processing: $step" + println("DEBUG: Processing step: $step") end + println("DEBUG: Pipeline execution finished.") # 4. Update Results Display current_pipeline_step = "Updating results..." + println("DEBUG: Updating results display after pipeline completion.") # Find the spectrum to display in "after" plot display_spectrum_idx = findfirst(s -> s.id == selected_spectrum_id_for_plot, current_spectra) if display_spectrum_idx !== nothing processed_spectrum = current_spectra[display_spectrum_idx] + println("DEBUG: Displaying spectrum $(selected_spectrum_id_for_plot) after processing.") - # Create "after" plot data after_trace = PlotlyBase.scatter( x=processed_spectrum.mz, y=processed_spectrum.intensity, @@ -1060,10 +1290,9 @@ end traces_after = [after_trace] - # Add peaks if they exist if !isempty(processed_spectrum.peaks) peak_mzs = [p.mz for p in processed_spectrum.peaks] - peak_intensities = [p.intensity for p in processed_spectrum.peaks] # Fixed: Should be peak.intensity + peak_intensities = [p.intensity for p in processed_spectrum.peaks] peak_trace = PlotlyBase.scatter( x=peak_mzs, y=peak_intensities, @@ -1072,14 +1301,19 @@ end marker=attr(color="red", size=8) ) push!(traces_after, peak_trace) + println("DEBUG: $(length(processed_spectrum.peaks)) peaks picked for spectrum $(selected_spectrum_id_for_plot).") + else + println("DEBUG: No peaks picked for spectrum $(selected_spectrum_id_for_plot).") end plotdata_after = traces_after plotlayout_after = PlotlyBase.Layout( - title="After Preprocessing (Spectrum $selected_spectrum_id_for_plot)", + title="After Preprocessing (Spectrum $(selected_spectrum_id_for_plot))", xaxis_title="m/z", yaxis_title="Intensity" ) + else + println("DEBUG: Selected spectrum for display ($(selected_spectrum_id_for_plot)) not found in processed spectra.") end # Save feature matrix if binning was performed @@ -1088,18 +1322,254 @@ end mkpath(output_dir) save_feature_matrix(feature_matrix_result, bin_info_result, output_dir) msg = "Pipeline completed successfully. Feature matrix saved." + println("DEBUG: Feature matrix saved to $output_dir") else - msg = "Pipeline completed successfully." + msg = "Pipeline completed successfully. No feature matrix generated (binning step not enabled)." + println("DEBUG: $msg") end catch e msg = "Error during pipeline execution: $e" warning_msg = true @error "Pipeline failed" exception=(e, catch_backtrace()) + println("DEBUG: Pipeline caught an exception: $e") finally progressPrep = false current_pipeline_step = "" - GC.gc() + println("DEBUG: run_full_pipeline finished (finally block).") + GC.gc() # Trigger garbage collection + if Sys.islinux() + ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS + end + end + end + + @onbutton recalculate_suggestions_btn begin + if msi_data === nothing + msg = "Please load a file first." + warning_msg = true + return + end + + try + msg = "Recalculating suggestions..." + progressPrep = true + + ref_peaks = Dict(p["mz"] => p["label"] for p in reference_peaks_list) + recommended_params = main_precalculation(msi_data, reference_peaks=ref_peaks) + + for (step_name, params) in recommended_params + for (param_key, value) in params + # Convert value to appropriate type before assignment + processed_value = if value === nothing + "" + elseif value isa Tuple + @warn "Skipping invalid parameter suggestion (tuple): $value for $param_key" + "" # Set to empty string for safety + elseif value isa Number + string(value) + else + string(value) + end + + if isempty(processed_value) && !(processed_value isa Number) + continue # Skip if processed_value is an empty string and not a number type + end + + # Map recommended parameters to suggested_* reactive variables + if step_name == :Smoothing + if param_key == :window + suggested_smoothing_window = processed_value + smoothing_window = processed_value + elseif param_key == :order + suggested_smoothing_order = processed_value + smoothing_order = processed_value + end + elseif step_name == :BaselineCorrection + if param_key == :iterations + suggested_baseline_iterations = processed_value + baseline_iterations = processed_value + elseif param_key == :window + suggested_baseline_window = processed_value + baseline_window = processed_value + end + elseif step_name == :PeakAlignment + if param_key == :span + suggested_alignment_span = processed_value + alignment_span = processed_value + elseif param_key == :tolerance + suggested_alignment_tolerance = processed_value + alignment_tolerance = processed_value + elseif param_key == :max_shift_ppm + suggested_alignment_max_shift_ppm = processed_value + alignment_max_shift_ppm = processed_value + elseif param_key == :min_matched_peaks + suggested_alignment_min_matched_peaks = processed_value + alignment_min_matched_peaks = processed_value + end + elseif step_name == :Calibration + if param_key == :fit_order + suggested_calibration_fit_order = processed_value + calibration_fit_order = processed_value + elseif param_key == :ppm_tolerance + suggested_calibration_ppm_tolerance = processed_value + calibration_ppm_tolerance = processed_value + end + elseif step_name == :PeakPicking + if param_key == :snr_threshold + suggested_peak_picking_snr_threshold = processed_value + peak_picking_snr_threshold = processed_value + elseif param_key == :half_window + suggested_peak_picking_half_window = processed_value + peak_picking_half_window = processed_value + elseif param_key == :min_peak_prominence + suggested_peak_picking_min_peak_prominence = processed_value + peak_picking_min_peak_prominence = processed_value + elseif param_key == :merge_peaks_tolerance + suggested_peak_picking_merge_peaks_tolerance = processed_value + peak_picking_merge_peaks_tolerance = processed_value + elseif param_key == :min_peak_width_ppm + suggested_peak_picking_min_peak_width_ppm = processed_value + peak_picking_min_peak_width_ppm = processed_value + elseif param_key == :max_peak_width_ppm + suggested_peak_picking_max_peak_width_ppm = processed_value + peak_picking_max_peak_width_ppm = processed_value + elseif param_key == :min_peak_shape_r2 + suggested_peak_picking_min_peak_shape_r2 = processed_value + peak_picking_min_peak_shape_r2 = processed_value + end + elseif step_name == :PeakSelection + if param_key == :min_snr + suggested_peak_selection_min_snr = processed_value + peak_selection_min_snr = processed_value + elseif param_key == :min_fwhm_ppm + suggested_peak_selection_min_fwhm_ppm = processed_value + peak_selection_min_fwhm_ppm = processed_value + elseif param_key == :max_fwhm_ppm + suggested_peak_selection_max_fwhm_ppm = processed_value + peak_selection_max_fwhm_ppm = processed_value + elseif param_key == :min_shape_r2 + suggested_peak_selection_min_shape_r2 = processed_value + peak_selection_min_shape_r2 = processed_value + elseif param_key == :frequency_threshold + suggested_peak_selection_frequency_threshold = processed_value + peak_selection_frequency_threshold = processed_value + elseif param_key == :correlation_threshold + suggested_peak_selection_correlation_threshold = processed_value + peak_selection_correlation_threshold = processed_value + end + elseif step_name == :PeakBinning + if param_key == :tolerance + suggested_binning_tolerance = processed_value + binning_tolerance = processed_value + elseif param_key == :frequency_threshold + suggested_binning_frequency_threshold = processed_value + binning_frequency_threshold = processed_value + elseif param_key == :min_peak_per_bin + suggested_binning_min_peak_per_bin = processed_value + binning_min_peak_per_bin = processed_value + elseif param_key == :max_bin_width_ppm + suggested_binning_max_bin_width_ppm = processed_value + binning_max_bin_width_ppm = processed_value + elseif param_key == :num_uniform_bins + suggested_binning_num_uniform_bins = processed_value + binning_num_uniform_bins = processed_value + end + end + end + end + + # Also set method types for steps + if haskey(recommended_params, :Smoothing) && haskey(recommended_params[:Smoothing], :method) + smoothing_method = string(recommended_params[:Smoothing][:method]) + end + if haskey(recommended_params, :BaselineCorrection) && haskey(recommended_params[:BaselineCorrection], :method) + baseline_method = string(recommended_params[:BaselineCorrection][:method]) + end + if haskey(recommended_params, :Normalization) && haskey(recommended_params[:Normalization], :method) + normalization_method = string(recommended_params[:Normalization][:method]) + end + if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :method) + alignment_method = string(recommended_params[:PeakAlignment][:method]) + end + if haskey(recommended_params, :PeakPicking) && haskey(recommended_params[:PeakPicking], :method) + peak_picking_method = string(recommended_params[:PeakPicking][:method]) + end + if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :method) + binning_method = string(recommended_params[:PeakBinning][:method]) + end + if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :tolerance_unit) + alignment_tolerance_unit = string(recommended_params[:PeakAlignment][:tolerance_unit]) + end + if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :tolerance_unit) + binning_tolerance_unit = string(recommended_params[:PeakBinning][:tolerance_unit]) + end + + msg = "Suggestions have been recalculated." + catch e + msg = "Failed to recalculate suggestions: $e" + warning_msg = true + @error "Recalculation failed" exception=(e, catch_backtrace()) + finally + progressPrep = false + end + end + + @onbutton addReferencePeak begin + new_list = deepcopy(reference_peaks_list) + push!(new_list, Dict("mz" => 0.0, "label" => "")) + reference_peaks_list = new_list # Assign new list to trigger reactivity + end + + @onbutton remove_peak_trigger begin + if action_index > -1 + julia_index = action_index + 1 + new_list = deepcopy(reference_peaks_list) + if 1 <= julia_index <= length(new_list) + deleteat!(new_list, julia_index) + reference_peaks_list = new_list + end + action_index = -1 # Reset + end + end + + @onbutton move_step_up_trigger begin + if action_index > -1 + julia_index = action_index + 1 + if julia_index > 1 + new_order = deepcopy(pipeline_step_order) + temp = new_order[julia_index] + new_order[julia_index] = new_order[julia_index - 1] + new_order[julia_index - 1] = temp + pipeline_step_order = new_order + end + action_index = -1 # Reset + end + end + + @onbutton move_step_down_trigger begin + if action_index > -1 + julia_index = action_index + 1 + if julia_index < length(pipeline_step_order) + new_order = deepcopy(pipeline_step_order) + temp = new_order[julia_index] + new_order[julia_index] = new_order[julia_index + 1] + new_order[julia_index + 1] = temp + pipeline_step_order = new_order + end + action_index = -1 # Reset + end + end + + @onbutton toggle_step_trigger begin + if action_index > -1 + julia_index = action_index + 1 + if 1 <= julia_index <= length(pipeline_step_order) + new_order = deepcopy(pipeline_step_order) + new_order[julia_index]["enabled"] = !new_order[julia_index]["enabled"] + pipeline_step_order = new_order + end + action_index = -1 # Reset end end @@ -1633,9 +2103,11 @@ end # Convert to positive coordinates for processing y_positive = yCoord < 0 ? abs(yCoord) : yCoord - plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y_positive, imgWidth, imgHeight, selected_folder_main, mask_path=mask_path_for_plot) + plotdata, plotlayout, xSpectraMz, ySpectraMz, spectrum_id = xySpectrumPlot(msi_data, xCoord, y_positive, imgWidth, imgHeight, selected_folder_main, mask_path=mask_path_for_plot) plotdata_before = plotdata plotlayout_before = plotlayout + selected_spectrum_id_for_plot = spectrum_id + idSpectrum = spectrum_id # we set the same obtained spectrum id to the UI # Update coordinates based on actual plot title # Extract title text from the Dict safely @@ -1688,6 +2160,93 @@ end end end + @onbutton createNSpectrumPlot @time begin + if isempty(selected_folder_main) + msg = "No dataset selected. Please process a file and select a folder first." + warning_msg = true + return + end + + progressSpectraPlot = true + btnStartDisable = true + btnPlotDisable = true + btnSpectraDisable = true + msg = "Loading plot for $(selected_folder_main)..." + + try + sTime = time() + registry = load_registry(registry_path) + + # Add error handling for registry access + if !haskey(registry, selected_folder_main) + msg = "Dataset '$(selected_folder_main)' not found in registry." + warning_msg = true + return + end + + entry = registry[selected_folder_main] + target_path = entry["source_path"] + + if target_path == "unknown (manually added)" + msg = "Dataset selected contained no route." + warning_msg = true + return + end + + if msi_data === nothing || full_route != target_path + if msi_data !== nothing + close(msi_data) + end + msg = "Reloading $(basename(target_path)) for analysis..." + full_route = target_path + msi_data = OpenMSIData(target_path) + if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing + raw_min = entry["metadata"]["global_min_mz"] + raw_max = entry["metadata"]["global_max_mz"] + min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min + max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max + set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) + else + precompute_analytics(msi_data) + end + end + + local mask_path_for_plot::Union{String, Nothing} = nothing + if maskEnabled && get(entry, "has_mask", false) + mask_path_for_plot = get(entry, "mask_path", "") + if !isfile(mask_path_for_plot) + @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." + mask_path_for_plot = nothing + end + end + + # Call the new nSpectrumPlot function + plotdata, plotlayout, xSpectraMz, ySpectraMz, spectrum_id = nSpectrumPlot(msi_data, idSpectrum, selected_folder_main, mask_path=mask_path_for_plot) + plotdata_before = plotdata + plotlayout_before = plotlayout + selected_spectrum_id_for_plot = spectrum_id + + selectedTab = "tab2" + fTime = time() + eTime = round(fTime - sTime, digits=3) + msg = "Plot loaded in $(eTime) seconds" + log_memory_usage("nSpectrum Plot Generated", msi_data) + catch e + msg = "Could not retrieve spectrum: $e" + warning_msg = true + @error "nSpectrum plotting failed" exception=(e, catch_backtrace()) + finally + progressSpectraPlot = false + btnPlotDisable = false + btnSpectraDisable = false + btnStartDisable = false + GC.gc() # Trigger garbage collection + if Sys.islinux() + ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS + end + end + end + # --- Main View Handlers --- @onbutton imgMinus begin if isempty(selected_folder_main) return end diff --git a/app.jl.html b/app.jl.html index ae5a7f3..910ae13 100644 --- a/app.jl.html +++ b/app.jl.html @@ -62,6 +62,11 @@ Spectrum plot (X,Y) + + + Spectrum plot (ID) + +
@@ -86,20 +91,21 @@
Internal Standards
-
Reference peaks for calibration and alignment
+
Reference peaks for calibration, alignment and precise suggestion calculations
+ - +
+ v-model="peak.mz" :rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']">
@@ -109,13 +115,15 @@ - + - - - - + +
+ +
+ +
Preprocessing Pipeline
@@ -128,9 +136,9 @@
+ :disable="index === 0" v-on:click.stop="action_index = index; move_step_up_trigger = true"> + :disable="index === pipeline_step_order.length - 1" v-on:click.stop="action_index = index; move_step_down_trigger = true">
@@ -139,7 +147,7 @@ - + @@ -160,10 +168,10 @@
- +
- +
@@ -186,11 +194,11 @@
+ :placeholder="suggested_baseline_iterations" v-model="baseline_iterations" hint="The number of iterations for the SNIP algorithm. A higher number results in a more aggressive baseline.">
+ :placeholder="suggested_baseline_window" v-model="baseline_window" hint="The window size for the median method, determining the local region for median calculation.">
@@ -226,11 +234,11 @@
+ :placeholder="suggested_alignment_span" v-model="alignment_span" :rules="[val => val >= 0.0 && val <= 1.0 || 'Needs to be between 0 and 1']" hint="The span parameter for LOWESS regression, controlling smoothness (0.0 to 1.0).">
+ :placeholder="suggested_alignment_tolerance" v-model="alignment_tolerance" hint="The tolerance for matching peaks between the target and reference spectrum.">
+ :placeholder="suggested_alignment_max_shift_ppm" v-model="alignment_max_shift_ppm" hint="The maximum allowed m/z shift in ppm to prevent spurious peak matches.">
+ :placeholder="suggested_alignment_min_matched_peaks" v-model="alignment_min_matched_peaks" hint="The minimum number of matching peaks required to perform the alignment.">
@@ -256,12 +264,12 @@
@@ -285,35 +293,35 @@
+ :placeholder="suggested_peak_picking_snr_threshold" v-model="peak_picking_snr_threshold" hint="Signal-to-Noise Ratio threshold. Peaks with SNR below this value are discarded.">
+ :placeholder="suggested_peak_picking_half_window" v-model="peak_picking_half_window" hint="Number of data points to the left and right of a potential peak to consider for local maximum detection (for Profile method).">
+ :placeholder="suggested_peak_picking_min_peak_prominence" v-model="peak_picking_min_peak_prominence" hint="Minimum required prominence of a peak, expressed as a fraction of its height.">
+ :placeholder="suggested_peak_picking_merge_peaks_tolerance" v-model="peak_picking_merge_peaks_tolerance" hint="The m/z tolerance within which to merge adjacent peaks, keeping the more intense one.">
+ :placeholder="suggested_peak_picking_min_peak_width_ppm" v-model="peak_picking_min_peak_width_ppm" hint="Minimum acceptable peak width (FWHM) in ppm.">
+ :placeholder="suggested_peak_picking_max_peak_width_ppm" v-model="peak_picking_max_peak_width_ppm" hint="Maximum acceptable peak width (FWHM) in ppm.">
+ :placeholder="suggested_peak_picking_min_peak_shape_r2" v-model="peak_picking_min_peak_shape_r2" hint="Minimum R-squared value from a Gaussian fit to the peak, used as a quality measure for peak shape."> @@ -325,32 +333,32 @@
+ :placeholder="suggested_peak_selection_min_snr" v-model="peak_selection_min_snr" hint="Minimum Signal-to-Noise Ratio for a peak to be kept.">
+ :placeholder="suggested_peak_selection_min_fwhm_ppm" v-model="peak_selection_min_fwhm_ppm" hint="Minimum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept.">
+ :placeholder="suggested_peak_selection_max_fwhm_ppm" v-model="peak_selection_max_fwhm_ppm" hint="Maximum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept.">
+ :placeholder="suggested_peak_selection_min_shape_r2" v-model="peak_selection_min_shape_r2" hint="Minimum R-squared value from a Gaussian fit, filtering for good peak shape.">
@@ -373,7 +381,7 @@
+ :placeholder="suggested_binning_tolerance" v-model="binning_tolerance" hint="Tolerance for grouping peaks into a bin in adaptive mode.">
+ :placeholder="suggested_binning_frequency_threshold" v-model="binning_frequency_threshold" hint="The minimum fraction of spectra a bin must contain a peak in to be kept (0.0 to 1.0).">
+ :placeholder="suggested_binning_min_peak_per_bin" v-model="binning_min_peak_per_bin" hint="The minimum number of individual peaks required to form a bin in adaptive mode.">
+ :placeholder="suggested_binning_max_bin_width_ppm" v-model="binning_max_bin_width_ppm" hint="Maximum width of a bin in ppm for adaptive mode.">
+ :placeholder="suggested_binning_num_uniform_bins" v-model="binning_num_uniform_bins" hint="The number of bins to create for the uniform method.">
- @@ -410,8 +418,8 @@
- - + +
@@ -422,8 +430,8 @@
- - + +
@@ -525,6 +533,11 @@ Spectrum plot (X,Y) + + + Spectrum plot (ID) + +
@@ -688,7 +701,7 @@
+ v-on:click="data_click">
@@ -711,7 +724,7 @@
+ class="q-pa-none q-ma-none sync_data" v-on:click="data_click">
@@ -747,6 +760,11 @@ Spectrum plot (X,Y) + + + Spectrum plot (ID) + +
diff --git a/app_snippet.html b/app_snippet.html deleted file mode 100644 index 398a10d..0000000 --- a/app_snippet.html +++ /dev/null @@ -1,665 +0,0 @@ - - - - -
-
- -
- - - - - - - - -
imzML & mzML Data Pre-Treatment
-
- - - - - -
- - - - {{ file }} - - - - - - - -
-
- - - - - - - - - - - - -
- - - -
- - - - - - -
Method
-
Applies a variance-stabilizing transformation to the intensity vector.
-
- -
-
-
-
-
-
-
-
- - - - -
Method
-
The smoothing algorithm.
-
- -
-
-
-
- - -
Parameters
-
- - - - -
-
- - - - -
Method
-
The algorithm to use for baseline correction.
-
- -
-
-
-
-
- - -
Parameters
-
- - - - -
-
- - - - -
Method
-
The normalization method to apply.
-
- -
-
-
-
-
-
-
- - - - -
Method
-
The alignment algorithm.
-
- -
-
-
-
-
- - -
Parameters
-
- - - - - - - -
-
- - -
Define Internal Standards / Reference Peaks
-

Provide m/z values and optional labels for internal standards or reference peaks. These are used for mass calibration and alignment.

- - - - - - -
-
- -
-
- -
-
-
-
- - - - - -
-
- - -

This step uses the peaks defined in the 'Internal Standards' tab to correct the m/z axis.

- - -
Parameters
-
- - - - -
-
- - -

Select the appropriate peak picking method and set its parameters.

- - -
Method
-
The peak detection algorithm.
-
- -
-
-
-
-
- - -
Parameters
-
- - - - - - - - - -
-
- - - - -
Peak Quality Filters
-
- - - - - - - - -
-
- - - - -
Method
-
The binning strategy.
-
- -
-
-
-
- - -
Parameters
-
- - - - - - - - - -
-
-
-
- -
imzML & mzML Data Processor
-

Please make sure the ibd and imzML file are located in the same directory and have the same name. -
It may take a while to generate the slice / spectrum, please be patient. -
To generate the contour or surface plots, you have to select the desired slice first using the - interface. -

-
- - - - - -
- - - - {{ file }} - - - - - - - - -
-
- - -
-
- -
-
- -
-
-
- -
-
- - - -
-
-
- -
-
-
- -
-
- - - - - - - Mean spectrum plot - - - - - Sum Spectrum plot - - - - - Spectrum plot (X,Y) - - - - -
-
-
- -
-
- -
-
-
-
-
- - - - - - -
- -
{{ progress_message }}
-
-
-

{{msg}}

-
- - - - - - - Image topography Plot - - - - - - TrIQ topography Plot - - - - - - Image surface Plot - - - - - - TrIQ Surface Plot - - - - - - - - Over normal image - - - - - Over TrIQ image - - - -
- - Transparency: {{ imgTrans }} -
-
-
- -
mzML to imzML Converter
-

Select the .mzML file and the corresponding .txt synchronization file to convert them into an .imzML/.ibd - pair.

- - - - - - - - - - - -

{{msg_conversion}}

-
-
-
-
-
- -
-
-
Spectrum View
- - -
Before Preprocessing
-
- - - -
- - -
After Preprocessing
-
- - - - -
-
-
- - - - -
Image visualizer
-
- - - - -
- -
-
- -
-
- -
-
-

-
- - - -
TrIQ visualizer
-
- - - - -
- -
-
- -
-
- -
-
-

-
- - -
- - - - - - - - Mean spectrum plot - - - - - Sum Spectrum plot - - - - - Spectrum plot (X,Y) - - - - -
- -
- - - - - - - - - -
-
-
-
-
diff --git a/app_snippet.jl b/app_snippet.jl deleted file mode 100644 index 118af40..0000000 --- a/app_snippet.jl +++ /dev/null @@ -1,1524 +0,0 @@ -# app.jl - -module App -# ==Packages == -using GenieFramework -using Pkg -using Libz -using PlotlyBase -using CairoMakie -using Colors -using MSI_src # Import the new MSIData library -using Statistics -using NaturalSort -using Images -using LinearAlgebra -using NativeFileDialog # Opens the file explorer depending on the OS -using StipplePlotly -using Base.Filesystem: mv # To rename files in the system -using Printf # Required for @sprintf macro in colorbar generation -using JSON -using Dates -using Base.Threads - -# Bring MSIData into App module's scope -using .MSI_src: MSIData, OpenMSIData, process_spectrum, IterateSpectra, ImzMLSource, _iterate_spectra_fast, MzMLSource, find_mass, ViridisPalette, get_mz_slice, get_multiple_mz_slices, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile, generate_colorbar_image, load_and_prepare_mask, set_global_mz_range!, main_precalculation, MutableSpectrum - -if !@isdefined(increment_image) - include("./julia_imzML_visual.jl") -end - -# --- Memory Validation Logging --- -if get(ENV, "GENIE_ENV", "dev") != "prod" - function get_rss_mb() - if !Sys.islinux() - return 0.0 - end - try - pid = getpid() - cmd = `ps -p $pid -o rss=` - rss_kb_str = read(cmd, String) - rss_kb = parse(Int, strip(rss_kb_str)) - return round(rss_kb / 1024, digits=2) - catch e - @warn "Could not get RSS via `ps` command. Error: $e" - return 0.0 - end - end - - function log_memory_usage(context::String, msi_data_val) - rss_mb = get_rss_mb() - - msi_data_size_mb = 0 - if msi_data_val !== nothing - msi_data_size_mb = round(Base.summarysize(msi_data_val) / (1024^2), digits=2) - end - - gc_time_s = round(GC.time(), digits=3) - - println("--- MEMORY LOG [$(context)] ---") - println(" Timestamp: $(now())") - println(" Process RSS: $(rss_mb) MB") - println(" msi_data size: $(msi_data_size_mb) MB") - println(" Cumulative GC time: $(gc_time_s) s") - println("--------------------------") - end -else - log_memory_usage(context::String, msi_data_val) = nothing # No-op for production -end - -@genietools - -# == Reactive code == -# Reactive code to make the UI interactive -@app begin - # == Loading Screen Variables == - @in is_initializing = true - @in initialization_message = "Initializing..." - - # == Reactive variables == - # reactive variables exist in both the Julia backend and the browser with two-way synchronization - # @out variables can only be modified by the backend - # @in variables can be modified by both the backend and the browser - # variables must be initialized with constant values, or variables defined outside of the @app block - - ## Interface non Variables - @out btnStartDisable=true - @out btnPlotDisable=false - @out btnSpectraDisable=false - # Loading animations - @in progress=false - @in progressPlot=false - @in progressSpectraPlot=false - # Text field validations - @in triqEnabled=false - @in SpectraEnabled=false - @in MFilterEnabled=false - @in maskEnabled=false - # Dialogs - @in warning_msg=false - @in CompareDialog=false - - ## Interface Variables - @in file_route="" - @in file_name="" - @in Nmass="0.0" - @in Tol=0.1 - @in triqProb=0.98 - @in colorLevel=20 - - ## Interface Buttons - @in btnSearch=false # To search for files in your device - @in btnAddBatch = false - @in clear_batch_btn = false - @out batch_file_count = 0 - @in mainProcess=false # To generate images - @in compareBtn=false # To open dialog - @in createMeanPlot=false # To generate mean spectrum plot - @in createXYPlot=false # To generate an spectrum plot according to the xy values inputed - @in createSumPlot=false # To generate a sum of all the spectrum plots - @in image3dPlot=false # To generate 3d plot based on current image - @in triq3dPlot=false # To generate 3d plot based on current triq image - @in imageCPlot=false # To generate contour plots of current image - @in triqCPlot=false # To generate contour plots of current triq image - # Image change buttons - @in imgPlus=false - @in imgMinus=false - @in imgPlusT=false - @in imgMinusT=false - # Image change comparative buttons - @in imgPlusCompLeft=false - @in imgMinusCompLeft=false - @in imgPlusTCompLeft=false - @in imgMinusTCompLeft=false - @in imgPlusCompRight=false - @in imgMinusCompRight=false - @in imgPlusTCompRight=false - @in imgMinusTCompRight=false - - ## Tabulation variables - @out tabIDs=["tab0","tab1","tab2","tab3","tab4"] - @out tabLabels=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] - @in selectedTab="tab0" - @out CompTabIDsLeft=["tab0","tab1","tab2","tab3","tab4"] - @out CompTabLabelsLeft=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] - @in CompSelectedTabLeft="tab0" - @out CompTabIDsRight=["tab0","tab1","tab2","tab3","tab4"] - @out CompTabLabelsRight=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] - @in CompSelectedTabRight="tab0" - - # Interface Images - @out imgInt="/.bmp" # image Interface - @out imgIntT="/.bmp" # image Interface TrIQ - @out colorbar="/.png" - @out colorbarT="/.png" - # Interface controlling for the comparative view - @out imgIntCompLeft="/.bmp" - @out imgIntTCompLeft="/.bmp" - @out colorbarCompLeft="/.png" - @out colorbarTCompLeft="/.png" - @out imgIntCompRight="/.bmp" - @out imgIntTCompRight="/.bmp" - @out colorbarCompRight="/.png" - @out colorbarTCompRight="/.png" - @out imgWidth=0 - @out imgHeight=0 - - # Optical Image Overlay & Transparency - @in imgTrans=1.0 - @in progressOptical=false - @out btnOpticalDisable=true - @in btnOptical=false - @in btnOpticalT=false - @in opticalOverTriq=false - @out imgRoute="" - - # Messages to interface variables - @out msg="" - @out msgimg="" - @out msgtriq="" - # Reiteration of the messages under the image to know which spectra is being visualized - @out msgimgCompLeft="" - @out msgtriqCompLeft="" - @out msgimgCompRight="" - @out msgtriqCompRight="" - - # Centralized MSIData object - @out msi_data::Union{MSIData, Nothing} = nothing - - # Metadata table variables - @in showMetadataDialog = false - @in showMetadataBtn = false - @out metadata_columns = [] - @out metadata_rows = [] - @out btnMetadataDisable = false - @in selected_folder_metadata = "" - - # Saves the route where imzML and mzML files are located - @out full_route="" - - # == Converter Tab Variables == - @in left_tab = "generator" - @out mzml_full_route = "" - @out sync_full_route = "" - @in btnSearchMzml = false - @in btnSearchSync = false - @in convert_process = false - @out progress_conversion = false - @out msg_conversion = "" - @out btnConvertDisable = true - - # == Pre Processing Variables == - @in pre_tab = "stabilization" - - ## Preprocessing Parameters - @in progressPrep=false - @in stabilization_method="sqrt" - @in smoothing_method="sg" - @in smoothing_window = "" - @in smoothing_order = "" - @in baseline_method="snip" - @in baseline_iterations = "" - @in baseline_window = "" - @in normalization_method="tic" - @in alignment_method="lowess" - @in alignment_span = "" - @in alignment_tolerance = "" - @in alignment_tolerance_unit="mz" - @in alignment_max_shift_ppm = "" - @in alignment_min_matched_peaks = "" - @in peak_picking_method="profile" - @in peak_picking_snr_threshold = "" - @in peak_picking_half_window = "" - @in peak_picking_min_peak_prominence = "" - @in peak_picking_merge_peaks_tolerance = "" - @in peak_picking_min_peak_width_ppm = "" - @in peak_picking_max_peak_width_ppm = "" - @in peak_picking_min_peak_shape_r2 = "" - @in binning_method="adaptive" - @in binning_tolerance = "" - @in binning_tolerance_unit="ppm" - @in binning_frequency_threshold = "" - @in binning_min_peak_per_bin = "" - @in binning_max_bin_width_ppm = "" - @in binning_intensity_weighted_centers=true - @in binning_num_uniform_bins = "" - @in calibration_fit_order = "" - @in calibration_ppm_tolerance = "" - @in peak_selection_min_snr = "" - @in peak_selection_min_fwhm_ppm = "" - @in peak_selection_max_fwhm_ppm = "" - @in peak_selection_min_shape_r2 = "" - @in peak_selection_frequency_threshold = "" - @in peak_selection_correlation_threshold = "" - - @in reference_peaks_list = [ - Dict("mz" => 137.0244, "label" => "DHB_fragment"), - Dict("mz" => 155.0349, "label" => "DHB_M+H"), - ] - - # --- Methods for Reference Peaks List --- - function addReferencePeak() - push!(reference_peaks_list, Dict("mz" => 0.0, "label" => "")) - reference_peaks_list = deepcopy(reference_peaks_list) # Force reactivity - end - - function removeReferencePeak(index::Int) - deleteat!(reference_peaks_list, index) - reference_peaks_list = deepcopy(reference_peaks_list) # Force reactivity - end - - # Individual step enable/disable flags - @in enable_stabilization = true - @in enable_smoothing = true - @in enable_baseline = true - @in enable_normalization = true - @in enable_standards = true - @in enable_alignment = true - @in enable_peak_picking = true - @in enable_binning = true - @in enable_calibration = true - @in enable_peak_selection = true - - # Trigger for running the full pipeline - @in run_full_pipeline = false - @out current_pipeline_step = "" # To indicate which step is currently running in the full pipeline - - @in export_params_btn = false - @in import_params_btn = false - @in imported_params_file = nothing - - @in suggested_smoothing_window = "" - @in suggested_smoothing_order = "" - @in suggested_baseline_iterations = "" - @in suggested_baseline_window = "" - @in suggested_alignment_span = "" - @in suggested_alignment_tolerance = "" - @in suggested_alignment_max_shift_ppm = "" - @in suggested_alignment_min_matched_peaks = "" - @in suggested_peak_picking_snr_threshold = "" - @in suggested_peak_picking_half_window = "" - @in suggested_peak_picking_min_peak_prominence = "" - @in suggested_peak_picking_merge_peaks_tolerance = "" - @in suggested_peak_picking_min_peak_width_ppm = "" - @in suggested_peak_picking_max_peak_width_ppm = "" - @in suggested_peak_picking_min_peak_shape_r2 = "" - @in suggested_binning_tolerance = "" - @in suggested_binning_frequency_threshold = "" - @in suggested_binning_min_peak_per_bin = "" - @in suggested_binning_max_bin_width_ppm = "" - @in suggested_binning_num_uniform_bins = "" - @in suggested_calibration_fit_order = "" - @in suggested_calibration_ppm_tolerance = "" - @in suggested_peak_selection_min_snr = "" - @in suggested_peak_selection_min_fwhm_ppm = "" - @in suggested_peak_selection_max_fwhm_ppm = "" - @in suggested_peak_selection_min_shape_r2 = "" - @in suggested_peak_selection_frequency_threshold = "" - @in suggested_peak_selection_correlation_threshold = "" - - # == Batch Summary Dialog == - @in showBatchSummary = false - @out batch_summary = "" - - # == Batch Processing & Registry Variables == - @private registry_init_done = false - @in refetch_folders = false - @in selected_files = String[] - @in available_folders = String[] - @in image_available_folders = String[] - @out registry_path = abspath(joinpath(@__DIR__, "public", "registry.json")) - # Progress reporting - @out overall_progress = 0.0 - @out progress_message = "" - - # == Folder-based UI State == - @in selected_folder_main = "" - @in selected_folder_compare_left = "" - @in selected_folder_compare_right = "" - - - # For the creation of images with a more specific mass charge - @out text_nmass="" - - # For image search image lists we apply a filter that searches specific type of images into our public folder, then we sort it in a "numerical" order - @in msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")),lt=natural) - @in col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir("public")),lt=natural) - @in triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")),lt=natural) - @in col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir("public")),lt=natural) - - # Set current image for the list to display - @out current_msi="" - @out current_col_msi="" - @out current_triq="" - @out current_col_triq="" - # We reiterate the process to display in the comparative view - @out current_msiCompLeft="" - @out current_col_msiCompLeft="" - @out current_triqCompLeft="" - @out current_col_triqCompLeft="" - @out current_msiCompRight="" - @out current_col_msiCompRight="" - @out current_triqCompRight="" - @out current_col_triqCompRight="" - - ## Time measurement variables - @out sTime=time() - @out fTime=time() - @out eTime=time() - - ## Plots - # Local image to plot - layoutImg=PlotlyBase.Layout( - title=PlotlyBase.attr( - text="", - font=PlotlyBase.attr( - family="Roboto, Lato, sans-serif", - size=14, - color="black" - ) - ), - xaxis=PlotlyBase.attr( - visible=false, - scaleanchor="y", - range=[0, 0] - ), - yaxis=PlotlyBase.attr( - visible=false, - range=[0, 0] - ), - margin=attr(l=0,r=0,t=0,b=0,pad=0) - ) - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - @out plotdataImg=[traceImg] - @out plotlayoutImg=layoutImg - # For the image in the comparative view - @out plotdataImgCompLeft=[traceImg] - @out plotlayoutImgCompLeft=layoutImg - @out plotdataImgCompRight=[traceImg] - @out plotlayoutImgCompRight=layoutImg - - # For triq image - @out plotdataImgT=[traceImg] - @out plotlayoutImgT=layoutImg - # For the triq image in the comparative view - @out plotdataImgTCompLeft=[traceImg] - @out plotlayoutImgTCompLeft=layoutImg - @out plotdataImgTCompRight=[traceImg] - @out plotlayoutImgTCompRight=layoutImg - # Interface Plot Spectrum - layoutSpectra=PlotlyBase.Layout( - title=PlotlyBase.attr( - text="Spectrum plot", - font=PlotlyBase.attr( - family="Roboto, Lato, sans-serif", - size=18, - color="black" - ) - ), - hovermode="closest", - xaxis=PlotlyBase.attr( - title="m/z", - showgrid=true - ), - yaxis=PlotlyBase.attr( - title="Intensity", - showgrid=true, - tickformat = ".3g" - ), - margin=attr(l=0,r=0,t=120,b=0,pad=0) - ) - # Dummy 2D scatter plot - traceSpectra=PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines", marker=attr(size=1, color="blue", opacity=0.1)) - # Create conection to frontend - @out plotdata=[traceSpectra] - @out plotlayout=layoutSpectra - @in xCoord=0 - @in yCoord=0 - @out xSpectraMz = Vector{Float64}() - @out ySpectraMz = Vector{Float64}() - - # UI plot data for Preprocessing - @out plotdata_before = [traceSpectra] - @out plotlayout_before = layoutSpectra - @out plotdata_after = [traceSpectra] - @out plotlayout_after = layoutSpectra - - # Interactive plot reactions - @in data_click=Dict{String,Any}() - #@in data_selected=Dict{String,Any}() # Selected is for areas, this can work for the masks - # - - # Interface Plot Surface - layoutContour=PlotlyBase.Layout( - title=PlotlyBase.attr( - text="2D Topographic map", - font=PlotlyBase.attr( - family="Roboto, Lato, sans-serif", - size=18, - color="black" - ) - ), - xaxis=PlotlyBase.attr( - visible=false, - scaleanchor="y" - ), - yaxis=PlotlyBase.attr( - visible=false - ), - margin=attr(l=0,r=0,t=100,b=0,pad=0) - ) - # Dummy 2D surface plot - traceContour=PlotlyBase.contour(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines") - # Create conection to frontend - @out plotdataC=[traceContour] - @out plotlayoutC=layoutContour - - # Interface Plot 3d - # Define the layout for the 3D plot - layout3D=PlotlyBase.Layout( - title=PlotlyBase.attr( - text="3D Surface plot", - font=PlotlyBase.attr( - family="Roboto, Lato, sans-serif", - size=18, - color="black" - ) - ), - scene=attr( - xaxis_title="X", - yaxis_title="Y", - zaxis_title="Z", - xaxis_nticks=20, - yaxis_nticks=20, - zaxis_nticks=4, - camera=attr(eye=attr(x=0, y=-1, z=0.5)), - aspectratio=attr(x=1, y=1, z=0.2) - ), - margin=attr(l=0,r=0,t=120,b=0,pad=0) - ) - - # Dummy 3D surface plot - x=1:10 - y=1:10 - z=[sin(i * j / 10) for i in x, j in y] - trace3D=PlotlyBase.surface(x=Vector{Float64}(), y=Vector{Float64}(), z=Matrix{Float64}(undef, 0, 0), - contours_z=attr( - show=true, - usecolormap=true, - highlightcolor="limegreen", - project_z=true - ), colorscale="Viridis") - # Create conection to frontend - @out plotdata3d=[trace3D] - @out plotlayout3d=layout3D - - # == Reactive handlers == - # Reactive handlers watch a variable and execute a block of code when its value changes - # The onbutton handler will set the variable to false after the block is executed - - # This handler correctly uses pick_file and loads the selected file - # as the active dataset for the UI. - @onbutton btnSearch begin - picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml") - if isempty(picked_route) - return - end - - progress = true - msg = "Opening file: $(basename(picked_route))..." - - try - dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML|mzml))$ "i => "") - registry = load_registry(registry_path) - existing_entry = get(registry, dataset_name, nothing) - - # --- Fast Load Path --- - is_same_file = (existing_entry !== nothing && existing_entry["source_path"] == picked_route) - if is_same_file && !isempty(get(existing_entry, "metadata", Dict())) - msg = "Fast loading pre-processed file: $(dataset_name)" - println(msg) - - full_route = existing_entry["source_path"] - metadata_rows = existing_entry["metadata"]["summary"] - - dims_str = first(filter(r -> r["parameter"] == "Image Dimensions", metadata_rows))["value"] - dims = parse.(Int, split(dims_str, " x ")) - imgWidth, imgHeight = dims[1], dims[2] - - msi_data = nothing # Ensure data is not held in memory - log_memory_usage("Fast Load (msi_data cleared)", msi_data) - btnMetadataDisable = false - btnStartDisable = false - btnPlotDisable = false - btnSpectraDisable = false - SpectraEnabled = true - selected_folder_main = dataset_name - - # Update folder lists in UI - all_folders = sort(collect(keys(registry)), lt=natural) - img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) - available_folders = deepcopy(all_folders) - image_available_folders = deepcopy(img_folders) - - msg = "Successfully loaded pre-processed dataset: $(dataset_name)" - progress = false - return - end - - # --- Full Load Path --- - msg = "Performing first-time analysis for: $(basename(picked_route))..." - local local_full_route - if endswith(picked_route, r"imzml"i) - local_full_route = replace(picked_route, r"\.imzml$"i => ".imzML") - if picked_route != local_full_route - mv(picked_route, local_full_route, force=true) - end - else - local_full_route = picked_route - end - full_route = local_full_route - - sTime = time() - loaded_data = OpenMSIData(local_full_route) - is_imzML = loaded_data.source isa ImzMLSource - - precompute_analytics(loaded_data) - - # Auto-suggest parameters - try - println("Calling main_precalculation to get recommended parameters...") - recommended_params = main_precalculation(loaded_data) - - for (step_name, params) in recommended_params - for (param_key, value) in params - # Convert value to appropriate type before assignment - processed_value = if value === nothing - nothing - elseif value isa Tuple - @warn "Skipping invalid parameter suggestion (tuple): $value for $param_key" - "" # Set to empty string for safety - elseif value isa Number - value - else - string(value) - end - - if processed_value !== nothing - if step_name == :Smoothing - if param_key == :window - suggested_smoothing_window = string(processed_value) - smoothing_window = string(processed_value) - println(" suggested_smoothing_window set to $(suggested_smoothing_window)") - elseif param_key == :order - suggested_smoothing_order = string(processed_value) - smoothing_order = string(processed_value) - println(" suggested_smoothing_order set to $(suggested_smoothing_order)") - end - elseif step_name == :BaselineCorrection - if param_key == :iterations - suggested_baseline_iterations = string(processed_value) - baseline_iterations = string(processed_value) - println(" suggested_baseline_iterations set to $(suggested_baseline_iterations)") - elseif param_key == :window - suggested_baseline_window = string(processed_value) - baseline_window = string(processed_value) - println(" suggested_baseline_window set to $(suggested_baseline_window)") - end - elseif step_name == :PeakAlignment - if param_key == :span - suggested_alignment_span = string(processed_value) - alignment_span = string(processed_value) - println(" suggested_alignment_span set to $(suggested_alignment_span)") - elseif param_key == :tolerance - suggested_alignment_tolerance = string(processed_value) - alignment_tolerance = string(processed_value) - println(" suggested_alignment_tolerance set to $(suggested_alignment_tolerance)") - elseif param_key == :max_shift_ppm - suggested_alignment_max_shift_ppm = string(processed_value) - alignment_max_shift_ppm = string(processed_value) - println(" suggested_alignment_max_shift_ppm set to $(suggested_alignment_max_shift_ppm)") - elseif param_key == :min_matched_peaks - suggested_alignment_min_matched_peaks = string(processed_value) - alignment_min_matched_peaks = string(processed_value) - println(" suggested_alignment_min_matched_peaks set to $(suggested_alignment_min_matched_peaks)") - end - elseif step_name == :Calibration - if param_key == :fit_order - suggested_calibration_fit_order = string(processed_value) - calibration_fit_order = string(processed_value) - println(" suggested_calibration_fit_order set to $(suggested_calibration_fit_order)") - elseif param_key == :ppm_tolerance - suggested_calibration_ppm_tolerance = string(processed_value) - calibration_ppm_tolerance = string(processed_value) - println(" suggested_calibration_ppm_tolerance set to $(suggested_calibration_ppm_tolerance)") - end - elseif step_name == :PeakPicking - if param_key == :snr_threshold - suggested_peak_picking_snr_threshold = string(processed_value) - peak_picking_snr_threshold = string(processed_value) - println(" suggested_peak_picking_snr_threshold set to $(suggested_peak_picking_snr_threshold)") - elseif param_key == :half_window - suggested_peak_picking_half_window = string(processed_value) - peak_picking_half_window = string(processed_value) - println(" suggested_peak_picking_half_window set to $(suggested_peak_picking_half_window)") - elseif param_key == :min_peak_prominence - suggested_peak_picking_min_peak_prominence = string(processed_value) - peak_picking_min_peak_prominence = string(processed_value) - println(" suggested_peak_picking_min_peak_prominence set to $(suggested_peak_picking_min_peak_prominence)") - elseif param_key == :merge_peaks_tolerance - suggested_peak_picking_merge_peaks_tolerance = string(processed_value) - peak_picking_merge_peaks_tolerance = string(processed_value) - println(" suggested_peak_picking_merge_peaks_tolerance set to $(suggested_peak_picking_merge_peaks_tolerance)") - elseif param_key == :min_peak_width_ppm - suggested_peak_picking_min_peak_width_ppm = string(processed_value) - peak_picking_min_peak_width_ppm = string(processed_value) - println(" suggested_peak_picking_min_peak_width_ppm set to $(suggested_peak_picking_min_peak_width_ppm)") - elseif param_key == :max_peak_width_ppm - suggested_peak_picking_max_peak_width_ppm = string(processed_value) - peak_picking_max_peak_width_ppm = string(processed_value) - println(" suggested_peak_picking_max_peak_width_ppm set to $(suggested_peak_picking_max_peak_width_ppm)") - elseif param_key == :min_peak_shape_r2 - suggested_peak_picking_min_peak_shape_r2 = string(processed_value) - peak_picking_min_peak_shape_r2 = string(processed_value) - println(" suggested_peak_picking_min_peak_shape_r2 set to $(suggested_peak_picking_min_peak_shape_r2)") - end - elseif step_name == :PeakSelection - if param_key == :min_snr - suggested_peak_selection_min_snr = string(processed_value) - peak_selection_min_snr = string(processed_value) - println(" suggested_peak_selection_min_snr set to $(suggested_peak_selection_min_snr)") - elseif param_key == :min_fwhm_ppm - suggested_peak_selection_min_fwhm_ppm = string(processed_value) - peak_selection_min_fwhm_ppm = string(processed_value) - println(" suggested_peak_selection_min_fwhm_ppm set to $(suggested_peak_selection_min_fwhm_ppm)") - elseif param_key == :max_fwhm_ppm - suggested_peak_selection_max_fwhm_ppm = string(processed_value) - peak_selection_max_fwhm_ppm = string(processed_value) - println(" suggested_peak_selection_max_fwhm_ppm set to $(suggested_peak_selection_max_fwhm_ppm)") - elseif param_key == :min_shape_r2 - suggested_peak_selection_min_shape_r2 = string(processed_value) - peak_selection_min_shape_r2 = string(processed_value) - println(" suggested_peak_selection_min_shape_r2 set to $(suggested_peak_selection_min_shape_r2)") - elseif param_key == :frequency_threshold - suggested_peak_selection_frequency_threshold = string(processed_value) - peak_selection_frequency_threshold = string(processed_value) - println(" suggested_peak_selection_frequency_threshold set to $(suggested_peak_selection_frequency_threshold)") - elseif param_key == :correlation_threshold - suggested_peak_selection_correlation_threshold = string(processed_value) - peak_selection_correlation_threshold = string(processed_value) - println(" suggested_peak_selection_correlation_threshold set to $(suggested_peak_selection_correlation_threshold)") - end - elseif step_name == :PeakBinning - if param_key == :tolerance - suggested_binning_tolerance = string(processed_value) - binning_tolerance = string(processed_value) - println(" suggested_binning_tolerance set to $(suggested_binning_tolerance)") - elseif param_key == :frequency_threshold - suggested_binning_frequency_threshold = string(processed_value) - binning_frequency_threshold = string(processed_value) - println(" suggested_binning_frequency_threshold set to $(suggested_binning_frequency_threshold)") - elseif param_key == :min_peak_per_bin - suggested_binning_min_peak_per_bin = string(processed_value) - binning_min_peak_per_bin = string(processed_value) - println(" suggested_binning_min_peak_per_bin set to $(suggested_binning_min_peak_per_bin)") - elseif param_key == :max_bin_width_ppm - suggested_binning_max_bin_width_ppm = string(processed_value) - binning_max_bin_width_ppm = string(processed_value) - println(" suggested_binning_max_bin_width_ppm set to $(suggested_binning_max_bin_width_ppm)") - elseif param_key == :num_uniform_bins - suggested_binning_num_uniform_bins = string(processed_value) - binning_num_uniform_bins = string(processed_value) - println(" suggested_binning_num_uniform_bins set to $(suggested_binning_num_uniform_bins)") - end - end - end - end - end - - # Also set method types for steps - if haskey(recommended_params, :Smoothing) && haskey(recommended_params[:Smoothing], :method) - smoothing_method = string(recommended_params[:Smoothing][:method]) - end - if haskey(recommended_params, :BaselineCorrection) && haskey(recommended_params[:BaselineCorrection], :method) - baseline_method = string(recommended_params[:BaselineCorrection][:method]) - end - if haskey(recommended_params, :Normalization) && haskey(recommended_params[:Normalization], :method) - normalization_method = string(recommended_params[:Normalization][:method]) - end - if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :method) - alignment_method = string(recommended_params[:PeakAlignment][:method]) - end - if haskey(recommended_params, :PeakPicking) && haskey(recommended_params[:PeakPicking], :method) - peak_picking_method = string(recommended_params[:PeakPicking][:method]) - end - if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :method) - binning_method = string(recommended_params[:PeakBinning][:method]) - end - if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :tolerance_unit) - alignment_tolerance_unit = string(recommended_params[:PeakAlignment][:tolerance_unit]) - end - if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :tolerance_unit) - binning_tolerance_unit = string(recommended_params[:PeakBinning][:tolerance_unit]) - end - - msg = "File loaded and parameters suggested." - catch e - @warn "Could not suggest parameters. Using defaults. Error: $e" - end - - metadata_columns = [ - Dict("name" => "parameter", "label" => "Parameter", "field" => "parameter", "align" => "left"), - Dict("name" => "value", "label" => "Value", "field" => "value", "align" => "left"), - ] - summary_stats = extract_metadata(loaded_data, local_full_route) - metadata_rows = summary_stats["summary"] - btnMetadataDisable = isempty(metadata_rows) - - w, h = loaded_data.image_dims - imgWidth, imgHeight = w > 0 ? (w, h) : (500, 500) - - update_registry(registry_path, dataset_name, local_full_route, summary_stats, is_imzML) - - # Update folder lists in UI - registry = load_registry(registry_path) - all_folders = sort(collect(keys(registry)), lt=natural) - img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) - available_folders = deepcopy(all_folders) - image_available_folders = deepcopy(img_folders) - - selected_folder_main = dataset_name - msi_data = loaded_data - log_memory_usage("Full Load", msi_data) - - eTime = round(time() - sTime, digits=3) - msg = "Active file loaded in $(eTime) seconds. Dataset '$(dataset_name)' is ready for analysis." - - btnStartDisable = false - btnPlotDisable = false - btnSpectraDisable = false - SpectraEnabled = true - - catch e - msi_data = nothing - msg = "Error loading active file: $e" - warning_msg = true - btnStartDisable = true - btnSpectraDisable = true - SpectraEnabled = false - btnMetadataDisable = true - @error "File loading failed" exception=(e, catch_backtrace()) - finally - GC.gc() # Trigger garbage collection - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS - end - progress = false - progressSpectraPlot = false - end - end - - @onbutton export_params_btn begin - params_to_export = Dict( - "stabilization_method" => stabilization_method, - "smoothing_method" => smoothing_method, - "smoothing_window" => smoothing_window, - "smoothing_order" => smoothing_order, - "baseline_method" => baseline_method, - "baseline_iterations" => baseline_iterations, - "baseline_window" => baseline_window, - "normalization_method" => normalization_method, - "alignment_method" => alignment_method, - "alignment_span" => alignment_span, - "alignment_tolerance" => alignment_tolerance, - "alignment_tolerance_unit" => alignment_tolerance_unit, - "alignment_max_shift_ppm" => alignment_max_shift_ppm, - "alignment_min_matched_peaks" => alignment_min_matched_peaks, - "peak_picking_method" => peak_picking_method, - "peak_picking_snr_threshold" => peak_picking_snr_threshold, - "peak_picking_half_window" => peak_picking_half_window, - "peak_picking_min_peak_prominence" => peak_picking_min_peak_prominence, - "peak_picking_merge_peaks_tolerance" => peak_picking_merge_peaks_tolerance, - "peak_picking_min_peak_width_ppm" => peak_picking_min_peak_width_ppm, - "peak_picking_max_peak_width_ppm" => peak_picking_max_peak_width_ppm, - "peak_picking_min_peak_shape_r2" => peak_picking_min_peak_shape_r2, - "binning_method" => binning_method, - "binning_tolerance" => binning_tolerance, - "binning_tolerance_unit" => binning_tolerance_unit, - "binning_frequency_threshold" => binning_frequency_threshold, - "binning_min_peak_per_bin" => binning_min_peak_per_bin, - "binning_max_bin_width_ppm" => binning_max_bin_width_ppm, - "binning_intensity_weighted_centers" => binning_intensity_weighted_centers, - "binning_num_uniform_bins" => binning_num_uniform_bins, - "calibration_fit_order" => calibration_fit_order, - "calibration_ppm_tolerance" => calibration_ppm_tolerance, - "peak_selection_min_snr" => peak_selection_min_snr, - "peak_selection_min_fwhm_ppm" => peak_selection_min_fwhm_ppm, - "peak_selection_max_fwhm_ppm" => peak_selection_max_fwhm_ppm, - "peak_selection_min_shape_r2" => peak_selection_min_shape_r2, - "peak_selection_frequency_threshold" => peak_selection_frequency_threshold, - "peak_selection_correlation_threshold" => peak_selection_correlation_threshold, - "reference_peaks_list" => reference_peaks_list - ) - json_string = JSON.json(params_to_export) - js_script = """ - var element = document.createElement('a'); - element.setAttribute('href', 'data:text/json;charset=utf-8,' + encodeURIComponent(`$json_string`)); - element.setAttribute('download', 'preprocessing_params.json'); - element.style.display = 'none'; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - """ - run_js(js_script) - msg = "Parameters exported." - end - - @onchange import_params_btn begin - if import_params_btn - try - json_string = String(imported_params_file.data) - params = JSON.parse(json_string) - - for (key, value) in params - if key == "reference_peaks_list" - reference_peaks_list = value - else - # Use getfield and setproperty! to update reactive variables by name - if hasfield(typeof(@__MODULE__), Symbol(key)) - getfield(@__MODULE__, Symbol(key))[] = value - end - end - end - msg = "Parameters imported successfully." - catch e - msg = "Failed to import parameters: $e" - warning_msg = true - finally - import_params_btn = false # Reset button state - end - end - end - - @onbutton run_full_pipeline begin - progressPrep = true - msg = "Running preprocessing pipeline..." - try - # 1. Get data from "before" plot - if isempty(plotdata_before.traces) || isempty(plotdata_before.traces[1].x) - msg = "Please generate a spectrum plot first (e.g., Mean, Sum, or X,Y)." - warning_msg = true - return - end - - mz_data = plotdata_before.traces[1].x - intensity_data = plotdata_before.traces[1].y - - # 2. Create a temporary MutableSpectrum - temp_spectrum = MutableSpectrum(mz_data, intensity_data, [], 1) # id=1 is arbitrary - - # 3. Run the pipeline - pipeline_spectra = [temp_spectrum] - - if enable_stabilization - current_pipeline_step = "Stabilizing..." - params = Dict(:method => Symbol(stabilization_method)) - apply_intensity_transformation(pipeline_spectra, params) - end - if enable_smoothing - current_pipeline_step = "Smoothing..." - params = Dict(:method => Symbol(smoothing_method), :window => parse(Int, smoothing_window), :order => parse(Int, smoothing_order)) - apply_smoothing(pipeline_spectra, params) - end - if enable_baseline - current_pipeline_step = "Correcting baseline..." - params = Dict(:method => Symbol(baseline_method), :iterations => parse(Int, baseline_iterations), :window => parse(Int, baseline_window)) - apply_baseline_correction(pipeline_spectra, params) - end - if enable_normalization - current_pipeline_step = "Normalizing..." - params = Dict(:method => Symbol(normalization_method)) - apply_normalization(pipeline_spectra, params) - end - if enable_standards && enable_calibration - current_pipeline_step = "Calibrating..." - ref_peaks = Dict(p["mz"] => p["label"] for p in reference_peaks_list) - params = Dict(:ppm_tolerance => parse(Float64, calibration_ppm_tolerance), :fit_order => parse(Int, calibration_fit_order)) - apply_calibration(pipeline_spectra, params) - end - if enable_alignment - # Alignment is a no-op for a single spectrum, but we call it for completeness - current_pipeline_step = "Aligning (skipped for single spectrum)..." - end - if enable_peak_picking - current_pipeline_step = "Picking peaks..." - params = Dict( - :method => Symbol(peak_picking_method), - :snr_threshold => parse(Float64, peak_picking_snr_threshold), - :half_window => parse(Int, peak_picking_half_window), - :min_peak_prominence => parse(Float64, peak_picking_min_peak_prominence), - :merge_peaks_tolerance => parse(Float64, peak_picking_merge_peaks_tolerance) - ) - apply_peak_picking(pipeline_spectra, params) - end - - # 4. Update "After" plot - processed_spectrum = pipeline_spectra[1] - - # Main spectrum trace - after_trace = PlotlyBase.scatter(x=processed_spectrum.mz, y=processed_spectrum.intensity, mode="lines", name="Processed Spectrum") - - traces_after = [after_trace] - - # Add peaks if they exist - if !isempty(processed_spectrum.peaks) - peak_mzs = [p.mz for p in processed_spectrum.peaks] - peak_intensities = [p.intensity for p in processed_spectrum.peaks] - peak_trace = PlotlyBase.scatter(x=peak_mzs, y=peak_intensities, mode="markers", name="Picked Peaks", marker=attr(color="red", size=8)) - push!(traces_after, peak_trace) - end - - plotdata_after = traces_after - plotlayout_after = PlotlyBase.Layout(title="After Preprocessing") - msg = "Pipeline finished." - - catch e - msg = "Error during pipeline execution: $e" - warning_msg = true - @error "Pipeline failed" exception=(e, catch_backtrace()) - finally - progressPrep = false - current_pipeline_step = "" - end - end - - # This new handler correctly adds the file from full_route to the batch list. - @onbutton btnAddBatch begin - if isempty(full_route) || full_route == "unknown (manually added)" - msg = "No active file selected to add to batch." - warning_msg = true - return - end - - if !(full_route in selected_files) - push!(selected_files, full_route) - selected_files = deepcopy(selected_files) # Force reactivity - batch_file_count = length(selected_files) - msg = "File added to batch." - else - msg = "File is already in the batch list." - warning_msg = true - end - end - - @onbutton clear_batch_btn begin - selected_files = String[] - batch_file_count = 0 - msg = "Batch cleared" - end - - @onchange selected_files begin - batch_file_count = length(selected_files) - end - - @onchange full_route begin - if !isempty(full_route) && !(full_route in selected_files) - push!(selected_files, full_route) - selected_files = deepcopy(selected_files) # Force reactivity - batch_file_count = length(selected_files) - msg = "File automatically added to batch" - end - end - - @onbutton showMetadataBtn begin - if !isempty(available_folders) - if !isempty(selected_folder_main) - selected_folder_metadata = selected_folder_main - elseif !isempty(available_folders) - selected_folder_metadata = first(available_folders) - end - showMetadataDialog = true - else - msg = "No processed datasets available." - warning_msg = true - end - end - - @onchange selected_folder_metadata begin - if !isempty(selected_folder_metadata) - registry = load_registry(registry_path) - dataset_info = get(registry, selected_folder_metadata, nothing) - - if dataset_info !== nothing && haskey(dataset_info, "metadata") && !isempty(get(dataset_info["metadata"], "summary", [])) - metadata_rows = dataset_info["metadata"]["summary"] - btnMetadataDisable = false - else - metadata_rows = [] - btnMetadataDisable = true - msg = "Metadata not found in registry for $(selected_folder_metadata)." - end - end - end - - - @onbutton mainProcess @time begin - # --- UI State Update --- - progress = true - btnStartDisable = true - btnPlotDisable = true - btnSpectraDisable = true - overall_progress = 0.0 - progress_message = "Preparing batch process..." - - # --- CAPTURE CURRENT VALUES HERE --- - current_selected_files = selected_files - current_nmass = Nmass - current_tol = Tol - current_color_level = colorLevel - current_triq_enabled = triqEnabled - current_triq_prob = triqProb - current_mfilter_enabled = MFilterEnabled - current_mask_enabled = maskEnabled - current_registry_path = registry_path - - println("starting main process with $(length(current_selected_files)) files") - total_time_start = time() - try - # --- 1. Parameter Validation --- - if isempty(current_selected_files) - progress_message = "No .imzML files in batch. Please add files first." - warning_msg = true - println(progress_message) - return - end - - masses = Float64[] - try - masses = [parse(Float64, strip(m)) for m in split(current_nmass, ',', keepempty=false)] - catch e - progress_message = "Invalid m/z value(s). Please provide a comma-separated list of numbers. Error: $e" - warning_msg = true - return - end - - if isempty(masses) - progress_message = "No valid m/z values found. Please provide comma-separated positive numbers." - warning_msg = true - return - end - - # --- 2. Batch Processing Loop --- - num_files = length(current_selected_files) - total_steps = num_files - current_step = 0 - errors = Dict("load_errors" => String[], "slice_errors" => String[], "io_errors" => String[]) - newly_created_folders = String[] - files_without_mask = 0 - - for (file_idx, file_path) in enumerate(current_selected_files) - progress_message = "Processing file $(file_idx)/$(num_files): $(basename(file_path))" - overall_progress = current_step / total_steps - - all_params = ( - tolerance = current_tol, - colorL = current_color_level, - triqE = current_triq_enabled, - triqP = current_triq_prob, - medianF = current_mfilter_enabled, - registry = current_registry_path, - fileIdx = file_idx, - nFiles = num_files - ) - - success, error_msg = process_file_safely(file_path, masses, all_params, progress_message, overall_progress, use_mask=current_mask_enabled) - - if !success - push!(errors["load_errors"], error_msg) - else - push!(newly_created_folders, replace(basename(file_path), r"\.imzML$"i => "")) - end - current_step += 1 - end - - # --- 3. Final Report --- - total_time_end = round(time() - total_time_start, digits=3) - - registry = load_registry(current_registry_path) - all_folders = sort(collect(keys(registry)), lt=natural) - img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) - available_folders = deepcopy(all_folders) - image_available_folders = deepcopy(img_folders) - - if !isempty(newly_created_folders) - selected_folder_main = first(newly_created_folders) - end - - successful_files = length(newly_created_folders) - total_errors = sum(length, values(errors)) - - if total_errors == 0 - msg = "Successfully processed all $(successful_files) file(s) in $(total_time_end) seconds." - else - msg = "Batch completed in $(total_time_end) seconds with $(total_errors) error(s)." - warning_msg = true - end - - mask_summary = current_mask_enabled ? "\nFiles processed without a mask: $(files_without_mask)" : "" - - batch_summary = """ - Processed $(successful_files)/$(num_files) files successfully. - $(mask_summary) - - Errors by category: - • Load failures: $(length(errors["load_errors"])) - • Slice generation: $(length(errors["slice_errors"])) - • I/O issues: $(length(errors["io_errors"])) - - Detailed errors: - $(join(vcat(values(errors)...), "\n")) - """ - showBatchSummary = true - - # Update UI to display the last generated image - if !isempty(newly_created_folders) - timestamp = string(time_ns()) - folder_path = joinpath("public", selected_folder_main) - - if current_triq_enabled - triq_files = filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)) - col_triq_files = filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)) - - if !isempty(triq_files) - latest_triq = triq_files[argmax([mtime(joinpath(folder_path, f)) for f in triq_files])] - current_triq = latest_triq - imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)" - plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) - text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") - msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" - - if !isempty(col_triq_files) - latest_col_triq = col_triq_files[argmax([mtime(joinpath(folder_path, f)) for f in col_triq_files])] - current_col_triq = latest_col_triq - colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)" - else - colorbarT = "" - end - selectedTab = "tab1" - end - else # Not TrIQ enabled, display regular MSI image - msi_files = filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)) - col_msi_files = filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)) - - if !isempty(msi_files) - latest_msi = msi_files[argmax([mtime(joinpath(folder_path, f)) for f in msi_files])] - current_msi = latest_msi - imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)" - plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt) - text_nmass = replace(current_msi, r"MSI_|.bmp" => "") - msgimg = "m/z: $(replace(text_nmass, "_" => "."))" - - if !isempty(col_msi_files) - latest_col_msi = col_msi_files[argmax([mtime(joinpath(folder_path, f)) for f in col_msi_files])] - current_col_msi = latest_col_msi - colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)" - else - colorbar = "" - end - selectedTab = "tab0" - end - end - end - - catch e - println("Error in main process: $e") - msg = "Batch processing failed: $e" - warning_msg = true - @error "Main process failed" exception=(e, catch_backtrace()) - finally - # --- UI State Reset --- - progress = false - btnStartDisable = false - btnPlotDisable = false - btnOpticalDisable = false - btnSpectraDisable = false - SpectraEnabled = true - overall_progress = 0.0 - println("Done") - GC.gc() # Trigger garbage collection - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS - end - end - end - - @onbutton createMeanPlot @time begin - if isempty(selected_folder_main) - msg = "No dataset selected. Please process a file and select a folder first." - warning_msg = true - return - end - - progressSpectraPlot = true - btnPlotDisable = true - btnStartDisable = true - msg = "Loading plot for $(selected_folder_main)..." - - try - sTime = time() - registry = load_registry(registry_path) - entry = registry[selected_folder_main] - target_path = entry["source_path"] - - if target_path == "unknown (manually added)" - msg = "Dataset selected contained no route." - warning_msg = true - return - end - - if msi_data === nothing || full_route != target_path - if msi_data !== nothing - close(msi_data) - end - msg = "Reloading $(basename(target_path)) for analysis..." - full_route = target_path - msi_data = OpenMSIData(target_path) - if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing - raw_min = entry["metadata"]["global_min_mz"] - raw_max = entry["metadata"]["global_max_mz"] - min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min - max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max - set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) - else - precompute_analytics(msi_data) - end - end - - local mask_path_for_plot::Union{String, Nothing} = nothing - if maskEnabled && get(entry, "has_mask", false) - mask_path_for_plot = get(entry, "mask_path", "") - if !isfile(mask_path_for_plot) - @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." - mask_path_for_plot = nothing - end - end - - plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data, selected_folder_main, mask_path=mask_path_for_plot) - plotdata_before = plotdata - plotlayout_before = plotlayout - selectedTab = "tab2" - fTime = time() - eTime = round(fTime - sTime, digits=3) - msg = "Plot loaded in $(eTime) seconds" - log_memory_usage("Mean Plot Generated", msi_data) - catch e - msg = "Could not generate mean spectrum plot: $e" - warning_msg = true - @error "Mean spectrum plotting failed" exception=(e, catch_backtrace()) - finally - progressSpectraPlot = false - btnPlotDisable = false - btnSpectraDisable = false - btnStartDisable = false - GC.gc() # Trigger garbage collection - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS - end - end - end - - @onbutton createSumPlot @time begin - if isempty(selected_folder_main) - msg = "No dataset selected. Please process a file and select a folder first." - warning_msg = true - return - end - - progressSpectraPlot = true - btnPlotDisable = true - btnStartDisable = true - msg = "Loading total spectrum plot for $(selected_folder_main)..." - - try - sTime = time() - registry = load_registry(registry_path) - entry = registry[selected_folder_main] - target_path = entry["source_path"] - - if target_path == "unknown (manually added)" - msg = "Dataset selected contained no route." - warning_msg = true - return - end - - if msi_data === nothing || full_route != target_path - if msi_data !== nothing - close(msi_data) - end - msg = "Reloading $(basename(target_path)) for analysis..." - full_route = target_path - msi_data = OpenMSIData(target_path) - if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing - raw_min = entry["metadata"]["global_min_mz"] - raw_max = entry["metadata"]["global_max_mz"] - min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min - max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max - set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) - else - precompute_analytics(msi_data) - end - end - local mask_path_for_plot::Union{String, Nothing} = nothing - if maskEnabled && get(entry, "has_mask", false) - mask_path_for_plot = get(entry, "mask_path", "") - if !isfile(mask_path_for_plot) - @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." - mask_path_for_plot = nothing - end - end - - plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data, selected_folder_main, mask_path=mask_path_for_plot) - plotdata_before = plotdata - plotlayout_before = plotlayout - selectedTab = "tab2" - fTime = time() - eTime = round(fTime - sTime, digits=3) - msg = "Total plot loaded in $(eTime) seconds" - log_memory_usage("Sum Plot Generated", msi_data) - catch e - msg = "Could not generate total spectrum plot: $e" - warning_msg = true - @error "Total spectrum plotting failed" exception=(e, catch_backtrace()) - finally - progressSpectraPlot = false - btnPlotDisable = false - btnSpectraDisable = false - btnStartDisable = false - GC.gc() # Trigger garbage collection - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS - end - end - end - - @onbutton createXYPlot @time begin - if isempty(selected_folder_main) - msg = "No dataset selected. Please process a file and select a folder first." - warning_msg = true - return - end - - progressSpectraPlot = true - btnStartDisable = true - btnPlotDisable = true - btnSpectraDisable = true - msg = "Loading plot for $(selected_folder_main)..." - - try - sTime = time() - registry = load_registry(registry_path) - - if !haskey(registry, selected_folder_main) - msg = "Dataset '$(selected_folder_main)' not found in registry." - warning_msg = true - return - end - - entry = registry[selected_folder_main] - target_path = entry["source_path"] - - if target_path == "unknown (manually added)" - msg = "Dataset selected contained no route." - warning_msg = true - return - end - - if msi_data === nothing || full_route != target_path - if msi_data !== nothing - close(msi_data) - end - msg = "Reloading $(basename(target_path)) for analysis..." - full_route = target_path - msi_data = OpenMSIData(target_path) - if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing - raw_min = entry["metadata"]["global_min_mz"] - raw_max = entry["metadata"]["global_max_mz"] - min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min - max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max - set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) - else - precompute_analytics(msi_data) - end - end - - local mask_path_for_plot::Union{String, Nothing} = nothing - if maskEnabled && get(entry, "has_mask", false) - mask_path_for_plot = get(entry, "mask_path", "") - if !isfile(mask_path_for_plot) - @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." - mask_path_for_plot = nothing - end - end - - y_positive = yCoord < 0 ? abs(yCoord) : yCoord - plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y_positive, imgWidth, imgHeight, selected_folder_main, mask_path=mask_path_for_plot) - plotdata_before = plotdata - plotlayout_before = plotlayout - - actual_title = if plotlayout.title isa Dict && haskey(plotlayout.title, :text) - plotlayout.title[:text] - elseif plotlayout.title isa Dict && haskey(plotlayout.title, "text") - plotlayout.title["text"] - else - string(plotlayout.title) # Fallback - end - - if occursin("Masked Spectrum at", actual_title) - # Extract coordinates from masked spectrum title - coords_match = match(r"Masked Spectrum at \((\d+), (\d+)\)", actual_title) - if coords_match !== nothing - xCoord = parse(Int, coords_match.captures[1]) - yCoord = -parse(Int, coords_match.captures[2]) # Negative for display - end - elseif occursin("Spectrum at", actual_title) - # Extract coordinates from regular spectrum title - coords_match = match(r"Spectrum at \((\d+), (\d+)\)", actual_title) - if coords_match !== nothing - xCoord = parse(Int, coords_match.captures[1]) - yCoord = -parse(Int, coords_match.captures[2]) # Negative for display - end - else - # For non-imaging data or fallback, just clamp the coordinates - xCoord = clamp(xCoord, 1, imgWidth) - yCoord = yCoord < 0 ? yCoord : -clamp(yCoord, 1, imgHeight) - end - - selectedTab = "tab2" - fTime = time() - eTime = round(fTime - sTime, digits=3) - msg = "Plot loaded in $(eTime) seconds" - log_memory_usage("XY Plot Generated", msi_data) - catch e - msg = "Could not retrieve spectrum: $e" - warning_msg = true - @error "Spectrum plotting failed" exception=(e, catch_backtrace()) - finally - progressSpectraPlot = false - btnPlotDisable = false - btnSpectraDisable = false - btnStartDisable = false - GC.gc() # Trigger garbage collection - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS - end - end - end - - GC.gc() # Trigger garbage collection - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure julia returns the freed memory to OS - end -end -# == Pages == -# Register a new route and the page that will be loaded on access -@page("/", "app.jl.html") -end \ No newline at end of file diff --git a/julia_imzML_visual.jl b/julia_imzML_visual.jl index 0195551..f4bef93 100644 --- a/julia_imzML_visual.jl +++ b/julia_imzML_visual.jl @@ -531,7 +531,13 @@ function meanSpectrumPlot(data::MSIData, dataset_name::String=""; mask_path::Uni showgrid=true, tickformat=".3g" ), - margin=attr(l=0, r=0, t=120, b=0, pad=0) + margin=attr(l=0, r=0, t=120, b=0, pad=0), + legend=attr( + x=1.0, + y=1.0, + xanchor="right", + yanchor="top" + ) ) # Use the new, efficient function from the backend @@ -582,22 +588,25 @@ Generates a plot for the spectrum at a specific coordinate (for imaging data) or """ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int, dataset_name::String=""; mask_path::Union{String, Nothing}=nothing) local mz::AbstractVector, intensity::AbstractVector - local plot_title::String - local spectrum_mode = MSI_src.CENTROID # Default to centroid + local spectrum_id::Int = -1 # Initialize spectrum_id + mz, intensity = Float64[], Float64[] + base_title = "" + spectrum_mode = MSI_src.PROFILE - is_imaging = data.source isa ImzMLSource + if data.source isa ImzMLSource + w, h = data.image_dims + x = clamp(xCoord, 1, w) + y = clamp(yCoord, 1, h) + spectrum_id = (y - 1) * w + x # Calculate spectrum_id for imaging data - if is_imaging - x = clamp(xCoord, 1, imgWidth) - y = clamp(yCoord, 1, imgHeight) - - # Get spectrum mode + # Check if spectrum stats are available and retrieve the mode if data.spectrum_stats_df !== nothing && hasproperty(data.spectrum_stats_df, :Mode) - w, h = data.image_dims - if y > 0 && x > 0 && y <= h && x <= w - idx = (y - 1) * w + x - if idx <= length(data.spectrum_stats_df.Mode) - spectrum_mode = data.spectrum_stats_df.Mode[idx] + if spectrum_id <= length(data.spectrum_stats_df.Mode) + try + spectrum_mode = data.spectrum_stats_df.Mode[spectrum_id] + catch e + @warn "Could not retrieve spectrum mode for index $spectrum_id. Defaulting to PROFILE." + spectrum_mode = MSI_src.PROFILE end end end @@ -625,6 +634,7 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, else # For non-imaging data, treat xCoord as the spectrum index index = clamp(xCoord, 1, length(data.spectra_metadata)) + spectrum_id = index # Assign index to spectrum_id for non-imaging data if data.spectrum_stats_df !== nothing && hasproperty(data.spectrum_stats_df, :Mode) if index <= length(data.spectrum_stats_df.Mode) @@ -660,7 +670,13 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, showgrid=true, tickformat=".3g" ), - margin=attr(l=0, r=0, t=120, b=0, pad=0) + margin=attr(l=0, r=0, t=120, b=0, pad=0), + legend=attr( + x=1.0, + y=1.0, + xanchor="right", + yanchor="top" + ) ) # Downsample for plotting performance @@ -675,7 +691,98 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, plotdata = [trace] plotlayout = layout - return plotdata, plotlayout, mz, intensity + return plotdata, plotlayout, mz, intensity, spectrum_id +end + +""" + nSpectrumPlot(data::MSIData, id::Int, dataset_name::String=""; mask_path::Union{String, Nothing}=nothing) + +Generates a plot for a spectrum specified by its linear `id` for any type of MSIData. + +# Arguments +- `data`: The `MSIData` object. +- `id`: The linear index (ID) of the spectrum to plot. +- `dataset_name`: Optional name of the dataset for the plot title. +- `mask_path`: Optional path to a mask file (currently not used for plotting by ID, but kept for signature consistency). + +# Returns +- `plotdata`: A vector containing the Plotly trace. +- `plotlayout`: The Plotly layout for the plot. +- `mz`: The m/z values of the spectrum. +- `intensity`: The intensity values of the spectrum. +- `spectrum_id`: The linear index of the spectrum (same as input `id`). +""" +function nSpectrumPlot(data::MSIData, id::Int, dataset_name::String=""; mask_path::Union{String, Nothing}=nothing) + local mz::AbstractVector, intensity::AbstractVector + local plot_title::String + local spectrum_mode = MSI_src.CENTROID # Default to centroid + local spectrum_id::Int = id # Spectrum ID is the input id + + # Validate ID + if id < 1 || id > length(data.spectra_metadata) + @warn "Spectrum ID $id is out of bounds." + mz, intensity = Float64[], Float64[] + base_title = "Spectrum ID $id (Out of Bounds)" + spectrum_id = 0 # Indicate invalid spectrum ID + else + # Get spectrum mode + if data.spectrum_stats_df !== nothing && hasproperty(data.spectrum_stats_df, :Mode) + if id <= length(data.spectrum_stats_df.Mode) + spectrum_mode = data.spectrum_stats_df.Mode[id] + end + end + + # Retrieve spectrum data + process_spectrum(data, id) do recieved_mz, recieved_intensity + mz = recieved_mz + intensity = recieved_intensity + end + base_title = "Spectrum #$id" + end + + plot_title = isempty(dataset_name) ? base_title : "$base_title for: $dataset_name" + + layout = PlotlyBase.Layout( + title=PlotlyBase.attr( + text=plot_title, + font=PlotlyBase.attr( + family="Roboto, Lato, sans-serif", + size=18, + color="black" + ) + ), + hovermode="closest", + xaxis=PlotlyBase.attr( + title="m/z", + showgrid=true + ), + yaxis=PlotlyBase.attr( + title="Intensity", + showgrid=true, + tickformat=".3g" + ), + margin=attr(l=0, r=0, t=120, b=0, pad=0), + legend=attr( + x=1.0, + y=1.0, + xanchor="right", + yanchor="top" + ) + ) + + # Downsample for plotting performance + mz_down, int_down = MSI_src.downsample_spectrum(mz, intensity) + + trace = if spectrum_mode == MSI_src.CENTROID + PlotlyBase.stem(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}") + else + PlotlyBase.scatter(x=mz_down, y=int_down, mode="lines", marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}") + end + + plotdata = [trace] + plotlayout = layout + + return plotdata, plotlayout, mz, intensity, spectrum_id end """ @@ -722,7 +829,13 @@ function sumSpectrumPlot(data::MSIData, dataset_name::String=""; mask_path::Unio showgrid=true, tickformat=".3g" ), - margin=attr(l=0, r=0, t=120, b=0, pad=0) + margin=attr(l=0, r=0, t=120, b=0, pad=0), + legend=attr( + x=1.0, + y=1.0, + xanchor="right", + yanchor="top" + ) ) # Use the get_total_spectrum function from the backend diff --git a/src/PreprocessingPipeline.jl b/src/PreprocessingPipeline.jl index 012f0fb..5118dad 100644 --- a/src/PreprocessingPipeline.jl +++ b/src/PreprocessingPipeline.jl @@ -389,10 +389,11 @@ function save_feature_matrix(feature_matrix::Matrix{Float64}, bin_info, output_d return csv_path, csv_path_standard end -function execute_full_preprocessing(spectra::Vector{MutableSpectrum}, params::Dict, +function execute_full_preprocessing(progress_callback::Function, # Now first positional + spectra::Vector{MutableSpectrum}, params::Dict, pipeline_steps::Vector{String}, reference_peaks::Dict, - mask_path::Union{String, Nothing}=nothing; - progress_callback::Function=(step -> nothing)) + mask_path::Union{String, Nothing} # Positional argument + ) println("Starting preprocessing pipeline with $(length(spectra)) spectra") println("Steps: $(join(pipeline_steps, " -> "))") diff --git a/src/imzML.jl b/src/imzML.jl index 51dd401..101d148 100644 --- a/src/imzML.jl +++ b/src/imzML.jl @@ -229,53 +229,189 @@ function get_spectrum_attributes(stream::IO, hIbd::IO) return [x_skip, y_skip, mz_first, array_len_skip, spectrum_end_skip] end -function determine_parser(stream::IO, mz_is_compressed::Bool, int_is_compressed::Bool) - start_pos = position(stream) - spectrum_xml = "" + +# Helper function to read an entire spectrum block from the stream +function read_spectrum_block(stream::IO) + lines = String[] + in_spectrum = false - try - # Find the start of the first spectrum tag - while !eof(stream) - line = readline(stream) - if occursin("", line) - break - end - end - spectrum_xml = String(take!(spectrum_buffer)) - break # Found the first spectrum, so we can stop + # Store the stream's position before starting to read this block. + # This is important for determining if a block was successfully read. + start_pos = position(stream) + + while !eof(stream) + line = readline(stream) + + if occursin("", line) + return join(lines, "\n") end end - finally - seek(stream, start_pos) # Always reset stream position end - - if isempty(spectrum_xml) - # Fallback based on compression flags if no spectrum tag found - return (mz_is_compressed || int_is_compressed) ? :compressed : :uncompressed - end - - # Inspect the XML content - has_neofx_markers = occursin("encodedLength=\"0\"", spectrum_xml) && - occursin("external encoded length", spectrum_xml) - has_external_data_markers = occursin("IMS:1000101", spectrum_xml) && - occursin("IMS:1000102", spectrum_xml) && - occursin("IMS:1000103", spectrum_xml) + # If we reach EOF without finding for a started block, + # or if no was found at all, return empty. + if isempty(lines) + seek(stream, start_pos) # Reset stream position if no block was found + return "" + else + # Should not happen if XML is well-formed, but just in case + @warn "Reached EOF while parsing spectrum block without finding tag. Returning partial block." + return join(lines, "\n") + end +end - if has_neofx_markers - return :neofx - end - - # If not neofx, it must be compressed (as uncompressed path is no longer supported) - return :compressed +# Helper function to parse coordinates from a spectrum block +function parse_coordinates(spectrum_block::String) + x = Int32(0) + y = Int32(0) + + x_match = match(r"IMS:1000050[^>]*value=\"(\d+)\"", spectrum_block) + y_match = match(r"IMS:1000051[^>]*value=\"(\d+)\"", spectrum_block) + + x = x_match !== nothing ? parse(Int32, x_match.captures[1]) : x + y = y_match !== nothing ? parse(Int32, y_match.captures[1]) : y + + return x, y +end + +# Helper function to parse spectrum mode from a spectrum block +function parse_spectrum_mode(spectrum_block::String, global_mode::SpectrumMode) + if occursin("MS:1000127", spectrum_block) + return CENTROID + elseif occursin("MS:1000128", spectrum_block) + return PROFILE + else + return global_mode + end +end + +# Helper function to parse binary data arrays +function parse_binary_data_arrays(spectrum_block::String, spectrum_start_line::String, + default_mz_format::DataType, default_intensity_format::DataType, + mz_is_compressed::Bool, int_is_compressed::Bool) + + array_data_type = @NamedTuple{is_mz::Bool, array_length::Int32, encoded_length::Int64, offset::Int64} + current_array_data = array_data_type[] + + for bda_match in eachmatch(r"(.*?)"sm, spectrum_block) + bda_content = bda_match.match # This will be the full block + + is_mz = occursin("MS:1000514", bda_content) || occursin("mzArray", bda_content) + + array_len_cv_match = match(r"IMS:1000103.*?value=\"(\d+)\"", bda_content) + array_length = Int32(0) + if array_len_cv_match !== nothing + array_length = parse(Int32, array_len_cv_match.captures[1]) + end + + # Fallback for defaultArrayLength from the spectrum_start_line + if array_length == 0 + default_match = match(r"defaultArrayLength=\"(\d+)\"", spectrum_start_line) + if default_match !== nothing + array_length = parse(Int32, default_match.captures[1]) end + end + + encoded_len_cv_match = match(r"IMS:1000104.*?value=\"(\d+)\"", bda_content) + encoded_length = Int64(0) + if encoded_len_cv_match !== nothing + encoded_length = parse(Int64, encoded_len_cv_match.captures[1]) + else + encoded_len_attr_match = match(r"encodedLength=\"(\d+)\"", bda_content) + if encoded_len_attr_match !== nothing + encoded_length = parse(Int64, encoded_len_attr_match.captures[1]) + end + end + + offset_match = match(r"IMS:1000102.*?value=\"(\d+)\"", bda_content) + offset = Int64(0) + if offset_match !== nothing + offset = parse(Int64, offset_match.captures[1]) + end + + if array_length > 0 && offset > 0 + push!(current_array_data, (is_mz=is_mz, array_length=array_length, + encoded_length=encoded_length, offset=offset)) + end + end + return current_array_data +end + + +# Unified function to parse spectrum metadata +function parse_imzml_spectrum_block(stream::IO, hIbd::Union{IO, ThreadSafeFileHandle}, param_groups::Dict{String, SpecDim}, + width::Int32, height::Int32, num_spectra::Int32, + default_mz_format::DataType, default_intensity_format::DataType, + mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode) + + spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra) + + # Store the initial position of the stream, in case read_spectrum_block fails to find a spectrum. + # This helps reset the stream for subsequent debugging if needed, though typically it should find a spectrum. + initial_stream_pos = position(stream) + + for k in 1:num_spectra + spectrum_block_content = read_spectrum_block(stream) + if isempty(spectrum_block_content) + @warn "Expected spectrum block $k but found none or reached EOF prematurely. Stopping parsing." + # Fill remaining spectra_metadata with placeholder or error. + for j in k:num_spectra + mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, Int64(0), 0, :mz) + int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, Int64(0), 0, :intensity) + spectra_metadata[j] = SpectrumMetadata(Int32(0), Int32(0), "", :sample, global_mode, mz_asset, int_asset) + end + break + end + + x, y = parse_coordinates(spectrum_block_content) + spectrum_mode = parse_spectrum_mode(spectrum_block_content, global_mode) + + # The spectrum_start_line is needed for defaultArrayLength fallback in parse_binary_data_arrays + # We need to extract it from spectrum_block_content or pass the first line of the spectrum_block_content + # Let's assume the first line of spectrum_block_content is the spectrum_start_line for this purpose + spectrum_start_line_from_block = String(split(spectrum_block_content, '\n')[1]) + + current_array_data = parse_binary_data_arrays(spectrum_block_content, spectrum_start_line_from_block, + default_mz_format, default_intensity_format, + mz_is_compressed, int_is_compressed) + + # Separate m/z and intensity arrays + mz_data = filter(d -> d.is_mz, current_array_data) + int_data = filter(d -> !d.is_mz, current_array_data) + + if length(mz_data) != 1 || length(int_data) != 1 + println("DEBUG: Spectrum $k is empty or invalid - creating placeholder metadata") + mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, Int64(0), 0, :mz) + int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, Int64(0), 0, :intensity) + else + mz_info = mz_data[1] + int_info = int_data[1] + + if k == 1 # Only print debug for the first spectrum + println("DEBUG First spectrum parsed (unified):") + println(" Coordinates: x=$x, y=$y") + println(" Mode: $spectrum_mode") + println(" m/z array: array_length=$(mz_info.array_length), encoded_length=$(mz_info.encoded_length), offset=$(mz_info.offset)") + println(" intensity array: array_length=$(int_info.array_length), encoded_length=$(int_info.encoded_length), offset=$(int_info.offset)") + end + + mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, mz_info.offset, + mz_is_compressed ? mz_info.encoded_length : mz_info.array_length, :mz) + int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, int_info.offset, + int_is_compressed ? int_info.encoded_length : int_info.array_length, :intensity) + end + + spectra_metadata[k] = SpectrumMetadata(x, y, "", :sample, spectrum_mode, mz_asset, int_asset) + end + + return spectra_metadata +end + function load_imzml_lazy(file_path::String; cache_size::Int=100) println("DEBUG: Checking for .imzML file at $file_path") if !isfile(file_path) @@ -342,22 +478,9 @@ function load_imzml_lazy(file_path::String; cache_size::Int=100) println("DEBUG: m/z compressed: $mz_is_compressed, Intensity compressed: $int_is_compressed") println("DEBUG: Global mode: $global_mode") - # --- Parser Selection --- - parser_type = determine_parser(stream, mz_is_compressed, int_is_compressed) - println("DEBUG: Selected parser: $parser_type") - - local spectra_metadata - if parser_type == :neofx - println("DEBUG: Using neofx parser.") - spectra_metadata = parse_neofx(stream, ts_hIbd, param_groups, width, height, num_spectra, - default_mz_format, default_intensity_format, - mz_is_compressed, int_is_compressed, global_mode) - else - println("DEBUG: Using compressed parser.") - spectra_metadata = parse_compressed(stream, ts_hIbd, param_groups, width, height, num_spectra, - default_mz_format, default_intensity_format, - mz_is_compressed, int_is_compressed, global_mode) - end + local spectra_metadata = parse_imzml_spectrum_block(stream, ts_hIbd, param_groups, width, height, num_spectra, + default_mz_format, default_intensity_format, + mz_is_compressed, int_is_compressed, global_mode) println("DEBUG: Metadata parsing complete.") @@ -554,150 +677,6 @@ function parse_compressed(stream::IO, hIbd::Union{IO, ThreadSafeFileHandle}, par return spectra_metadata end -function parse_neofx(stream::IO, hIbd::Union{IO, ThreadSafeFileHandle}, param_groups::Dict{String, SpecDim}, - width::Int32, height::Int32, num_spectra::Int32, - default_mz_format::DataType, default_intensity_format::DataType, - mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode) - spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra) - - array_data_type = @NamedTuple{is_mz::Bool, array_length::Int32, encoded_length::Int64, offset::Int64} - - for k in 1:num_spectra - # Initialize variables for this spectrum - x = Int32(0) - y = Int32(0) - spectrum_mode = global_mode - current_array_data = array_data_type[] - spectrum_start_line = "" - - # Find the start of the spectrum tag - line = "" - while !eof(stream) - line = readline(stream) - if occursin("", line) - break - end - - # Parse coordinates - x_match = match(r"IMS:1000050.*?value=\"(\d+)\"", line) - if x_match !== nothing - x = parse(Int32, x_match.captures[1]) - end - y_match = match(r"IMS:1000051.*?value=\"(\d+)\"", line) - if y_match !== nothing - y = parse(Int32, y_match.captures[1]) - end - - # Parse mode - if occursin("MS:1000127", line) - spectrum_mode = CENTROID - elseif occursin("MS:1000128", line) - spectrum_mode = PROFILE - end - - # More robust binaryDataArray detection - if occursin("", line) - while !eof(stream) - bda_line = readline(stream) - push!(bda_lines, bda_line) - if occursin("", bda_line) - break - end - end - end - bda_content = join(bda_lines, "\n") - - # Parse from bda_content - is_mz = occursin("MS:1000514", bda_content) || occursin("mzArray", bda_content) - - array_len_cv_match = match(r"IMS:1000103.*?value=\"(\d+)\"", bda_content) - array_length = Int32(0) - if array_len_cv_match !== nothing - array_length = parse(Int32, array_len_cv_match.captures[1]) - end - - # Fallback for defaultArrayLength - if array_length == 0 - default_match = match(r"defaultArrayLength=\"(\d+)\"", spectrum_start_line) - if default_match !== nothing - array_length = parse(Int32, default_match.captures[1]) - end - end - - encoded_len_cv_match = match(r"IMS:1000104.*?value=\"(\d+)\"", bda_content) - encoded_length = Int64(0) - if encoded_len_cv_match !== nothing - encoded_length = parse(Int64, encoded_len_cv_match.captures[1]) - else - encoded_len_attr_match = match(r"encodedLength=\"(\d+)\"", bda_content) - if encoded_len_attr_match !== nothing - encoded_length = parse(Int64, encoded_len_attr_match.captures[1]) - end - end - - offset_match = match(r"IMS:1000102.*?value=\"(\d+)\"", bda_content) - offset = Int64(0) - if offset_match !== nothing - offset = parse(Int64, offset_match.captures[1]) - end - - if array_length > 0 && offset > 0 - push!(current_array_data, (is_mz=is_mz, array_length=array_length, - encoded_length=encoded_length, offset=offset)) - end - end - - # Reset line to continue loop - line = "" - end - - # Separate m/z and intensity arrays - mz_data = filter(d -> d.is_mz, current_array_data) - int_data = filter(d -> !d.is_mz, current_array_data) - - if length(mz_data) != 1 || length(int_data) != 1 - println("DEBUG: Spectrum $k is empty or invalid - creating placeholder metadata") - mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, Int64(0), 0, :mz) - int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, Int64(0), 0, :intensity) - else - mz_info = mz_data[1] - int_info = int_data[1] - - if k == 1 - println("DEBUG First spectrum parsed:") - println(" Coordinates: x=$x, y=$y") - println(" Mode: $spectrum_mode") - println(" m/z array: array_length=$(mz_info.array_length), encoded_length=$(mz_info.encoded_length), offset=$(mz_info.offset)") - println(" intensity array: array_length=$(int_info.array_length), encoded_length=$(int_info.encoded_length), offset=$(int_info.offset)") - end - - mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, mz_info.offset, - mz_is_compressed ? mz_info.encoded_length : mz_info.array_length, :mz) - int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, int_info.offset, - int_is_compressed ? int_info.encoded_length : int_info.array_length, :intensity) - end - - spectra_metadata[k] = SpectrumMetadata(x, y, "", :sample, spectrum_mode, mz_asset, int_asset) - end - - return spectra_metadata -end - -# --- End of content from imzML.jl --- # ============================================================================= #