# app.jl module App # ==Packages == using GenieFramework using Pkg using Libz using PlotlyBase using CairoMakie using Colors using Dates 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, execute_full_preprocessing 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 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 function get_processed_mean_spectrum(spectra::Vector{MutableSpectrum}; num_bins=2000) # 1. Find global m/z range from all spectra min_mz, max_mz = Inf, -Inf for s in spectra if !isempty(s.mz) min_mz = min(min_mz, minimum(s.mz)) max_mz = max(max_mz, maximum(s.mz)) end end if !isfinite(min_mz) return Float64[], Float64[] end # 2. Create bins mz_bins = range(min_mz, stop=max_mz, length=num_bins) intensity_sum = zeros(Float64, num_bins) bin_step = step(mz_bins) inv_bin_step = 1.0 / bin_step # 3. Bin intensities for s in spectra # Use minimum length to avoid bounds errors if arrays are mismatched n_points = min(length(s.mz), length(s.intensity)) for i in 1:n_points bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0) final_index = clamp(bin_index, 1, num_bins) intensity_sum[final_index] += s.intensity[i] end end # 4. Average and return if isempty(spectra) return collect(mz_bins), intensity_sum end average_intensity = intensity_sum ./ length(spectra) return collect(mz_bins), average_intensity end function get_processed_sum_spectrum(spectra::Vector{MutableSpectrum}; num_bins=2000) min_mz, max_mz = Inf, -Inf for s in spectra if !isempty(s.mz) min_mz = min(min_mz, minimum(s.mz)) max_mz = max(max_mz, maximum(s.mz)) end end if !isfinite(min_mz) return Float64[], Float64[] end mz_bins = range(min_mz, stop=max_mz, length=num_bins) intensity_sum = zeros(Float64, num_bins) bin_step = step(mz_bins) inv_bin_step = 1.0 / bin_step for s in spectra # Use minimum length to avoid bounds errors if arrays are mismatched n_points = min(length(s.mz), length(s.intensity)) for i in 1:n_points bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0) final_index = clamp(bin_index, 1, num_bins) intensity_sum[final_index] += s.intensity[i] end end return collect(mz_bins), intensity_sum end INITIAL_MODEL_STATE = Dict{Symbol,Any}() # Function to capture initial state (also outside @app block) function capture_initial_state!(model) empty!(INITIAL_MODEL_STATE) for name in fieldnames(typeof(model)) if !startswith(String(name), "_") INITIAL_MODEL_STATE[name] = deepcopy(getfield(model, name)) end end println("Captured $(length(INITIAL_MODEL_STATE)) reactive variables") end @genietools # == Reactive code == #= macro ui_log(message, level="INFO", log_entries) quote local timestamp = Dates.format(now(), "HH:MM:SS") local new_entry = Dict("time" => timestamp, "message" => string($(esc(message))), "level" => $(esc(level))) println("log entries value: $log_entries") pushfirst!(log_entries, new_entry) if length(log_entries) > 100 popfirst!(log_entries) end push!(__model__) end end =# # Reactive code to make the UI interactive @app begin # == Notification & Logs == # @in log_entries = Dict{String,Any}[] # @in show_log_sidebar = false @in showBugModal = false # == Loading Screen Variables == @in is_initializing = true @in initialization_message = "Initializing..." # Loading animations and readonly / disable elements are all handled by this variable. @in is_processing = false # == SLICE GENERATOR TAB VARIABLES == # File selection and batch processing @in file_route="" @in file_name="" @in btnSearch=false # To search for files in your device @in btnAddBatch = false @in clear_batch_btn = false @out batch_file_count = 0 @in selected_files = String[] @out full_route="" # Saves the route where imzML and mzML files are located # Mass-to-charge parameters @in Nmass="0.0" # Mass-to-charge ratio(s) of interest @in Tol=0.1 # Mass-to-charge ratio tolerance @in colorLevel=20 # Color levels for visualization # Processing toggles @in triqEnabled=false # Threshold Intensity Quantization @in MFilterEnabled=false # Median Filter @in maskEnabled=false # Use Mask To Filter Data @in triqProb=0.98 # TrIQ probability parameter # Spectrum selection and coordinates @in idSpectrum=0 # Spectrum ID for ID-based plots @in xCoord=0 # X coordinate for coordinate-based plots @in yCoord=0 # Y coordinate for coordinate-based plots @in SpectraEnabled=false # Enables xCoord and yCoord inputs when spectral data is loaded # Plot generation triggers @in mainProcess=false # To generate images/slices @in createMeanPlot=false # To generate mean spectrum plot @in createXYPlot=false # To generate spectrum plot according to xy values @in createNSpectrumPlot=false # To generate spectrum plot according to spectrum order @in createSumPlot=false # To generate sum of all 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 navigation controls @in imgPlus=false # Next image in normal mode @in imgMinus=false # Previous image in normal mode @in imgPlusT=false # Next image in TrIQ mode @in imgMinusT=false # Previous image in TrIQ mode # Optical image overlay @in imgTrans=1.0 # Transparency level for optical overlay @in btnOptical=false # Load optical image over normal image @in btnOpticalT=false # Load optical image over TrIQ image @in opticalOverTriq=false # Toggle optical overlay mode # Messages and status @out msg="" # Main status message @out msgimg="" # Image status message @out msgtriq="" # TrIQ status message # == CONVERTER TAB VARIABLES == @in left_tab = "generator" # Active left tab (generator, converter, pre_treatment) @out mzml_full_route = "" # Path to .mzML file @out sync_full_route = "" # Path to .txt synchronization file @in btnSearchMzml = false # Trigger mzML file search @in btnSearchSync = false # Trigger sync file search @in convert_process = false # Start conversion process @out progress_conversion = false # Conversion progress indicator @out msg_conversion = "" # Conversion status message @out btnConvertDisable = true # Disable convert button when files not selected # == PRE-TREATMENT TAB VARIABLES == # File selection and batch @in pre_tab = "stabilization" # Active preprocessing subtab # Subset processing @in enable_subset_processing = false # Enable processing only first N spectra @in spectra_subset_size = 100 # Number of spectra for subset processing # Internal standards management @in enable_standards = true # Use internal standards for calibration @in reference_peaks_list = [ Dict("mz" => 137.0244, "label" => "DHB_fragment"), Dict("mz" => 155.0349, "label" => "DHB_M+H"), ] @in addReferencePeak = false # Add new reference peak @in remove_peak_trigger = false # Remove reference peak @in export_standards_btn = false # Export standards to JSON @in import_standards_btn = false # Import standards from JSON # Pipeline step management @in pipeline_step_order = [ Dict("name" => "stabilization", "label" => "Stabilization", "enabled" => true), Dict("name" => "smoothing", "label" => "Smoothing", "enabled" => true), Dict("name" => "baseline_correction", "label" => "Baseline Correction", "enabled" => true), Dict("name" => "peak_picking", "label" => "Peak Picking", "enabled" => true), Dict("name" => "peak_selection", "label" => "Peak Selection", "enabled" => true), Dict("name" => "calibration", "label" => "Calibration", "enabled" => true), Dict("name" => "peak_alignment", "label" => "Peak Alignment", "enabled" => true), Dict("name" => "normalization", "label" => "Normalization", "enabled" => true), Dict("name" => "peak_binning", "label" => "Peak Binning", "enabled" => true) ] @in action_index = -1 # Index for step operations @in move_step_up_trigger = false # Move step up in pipeline @in move_step_down_trigger = false # Move step down in pipeline @in toggle_step_trigger = false # Toggle step enabled/disabled @out current_pipeline_step = "" # Current running step in full pipeline # Preprocessing method parameters @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 = "" # Suggested parameter values @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 = "" # Pipeline control triggers @in run_full_pipeline = false # Trigger full pipeline execution @in recalculate_suggestions_btn = false # Recalculate parameter suggestions @in export_params_btn = false # Export parameters to file @in import_params_btn = false # Import parameters from file @in save_feature_matrix_btn = false # Save feature matrix results @in reset_session_btn = false # Deep session reset # Preprocessing results @in selected_spectrum_id_for_plot = 1 @in last_plot_type = "single" @in last_plot_mode = "lines" @in feature_matrix_result::Union{Nothing, Matrix{Float64}} = nothing @in bin_info_result::Union{Nothing, Vector} = nothing # == RIGHT PANEL VARIABLES (intDivStyle-right) == # Tab management @out tabIDs=["tab0","tab1","tab2","tab3","tab4"] @out tabLabels=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in selectedTab="tab0" # Compare dialog tabs @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" # Compare dialog controls @in CompareDialog=false @in compareBtn=false # Open compare dialog @in imgPlusCompLeft=false # Next image in compare left panel @in imgMinusCompLeft=false # Previous image in compare left panel @in imgPlusTCompLeft=false # Next TrIQ image in compare left panel @in imgMinusTCompLeft=false # Previous TrIQ image in compare left panel @in imgPlusCompRight=false # Next image in compare right panel @in imgMinusCompRight=false # Previous image in compare right panel @in imgPlusTCompRight=false # Next TrIQ image in compare right panel @in imgMinusTCompRight=false # Previous TrIQ image in compare right panel # Image display variables @out imgInt="/.bmp" # Normal image interface @out imgIntT="/.bmp" # TrIQ image interface @out colorbar="/.png" # Normal colorbar @out colorbarT="/.png" # TrIQ colorbar # Compare dialog images @out imgIntCompLeft="/.bmp" # Left compare normal image @out imgIntTCompLeft="/.bmp" # Left compare TrIQ image @out colorbarCompLeft="/.png" # Left compare normal colorbar @out colorbarTCompLeft="/.png" # Left compare TrIQ colorbar @out imgIntCompRight="/.bmp" # Right compare normal image @out imgIntTCompRight="/.bmp" # Right compare TrIQ image @out colorbarCompRight="/.png" # Right compare normal colorbar @out colorbarTCompRight="/.png" # Right compare TrIQ colorbar @out imgWidth=0 @out imgHeight=0 # Compare dialog messages @out msgimgCompLeft="" @out msgtriqCompLeft="" @out msgimgCompRight="" @out msgtriqCompRight="" # == BATCH PROCESSING & REGISTRY VARIABLES == @private registry_init_done = false @in refetch_folders = false @in available_folders = String[] @in image_available_folders = String[] @out registry_path = abspath(joinpath(@__DIR__, "public", "registry.json")) # Folder selection state @in selected_folder_main = "" @in selected_folder_compare_left = "" @in selected_folder_compare_right = "" # Progress reporting @out overall_progress = 0.0 @out progress_message = "" # Batch summary @in showBatchSummary = false @out batch_summary = "" # == METADATA VARIABLES == @in showMetadataDialog = false @in showMetadataBtn = false @out metadata_columns = [] @out metadata_rows = [] @out btnMetadataDisable = false @in selected_folder_metadata = "" # == DATA MANAGEMENT VARIABLES == # Centralized MSIData object @out msi_data::Union{MSIData, Nothing} = nothing # Image file management @out text_nmass="" # For specific mass charge image creation @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) # Current image display @out current_msi="" @out current_col_msi="" @out current_triq="" @out current_col_triq="" @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="" # Optical image @out imgRoute="" # == PLOTTING VARIABLES == # Image plots 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 @out plotdataImgCompLeft=[traceImg] @out plotlayoutImgCompLeft=layoutImg @out plotdataImgCompRight=[traceImg] @out plotlayoutImgCompRight=layoutImg # TrIQ image plots @out plotdataImgT=[traceImg] @out plotlayoutImgT=layoutImg @out plotdataImgTCompLeft=[traceImg] @out plotlayoutImgTCompLeft=layoutImg @out plotdataImgTCompRight=[traceImg] @out plotlayoutImgTCompRight=layoutImg # Spectrum plots 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), legend=attr( x=1.0, y=1.0, xanchor="right", yanchor="top" ) ) traceSpectra=PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines", marker=attr(size=1, color="blue", opacity=0.1)) @out plotdata=[traceSpectra] @out plotlayout=layoutSpectra # Preprocessing spectrum plots @out plotdata_before = [traceSpectra] @out plotlayout_before = layoutSpectra @out plotdata_after = [traceSpectra] @out plotlayout_after = layoutSpectra # Spectrum data @out xSpectraMz = Vector{Float64}() @out ySpectraMz = Vector{Float64}() # Contour plots 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) ) traceContour=PlotlyBase.contour(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines") @out plotdataC=[traceContour] @out plotlayoutC=layoutContour # 3D surface plots 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) ) 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") @out plotdata3d=[trace3D] @out plotlayout3d=layout3D # Interactive plot reactions @in data_click=Dict{String,Any}() # == TIME MEASUREMENT VARIABLES == @out sTime=time() @out fTime=time() @out eTime=time() # == DIALOGS AND MESSAGES == @in warning_msg=false # == 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 @onbutton reset_session_btn begin is_processing = true push!(__model__) try # 1. Clear large data objects explicitly msi_data = nothing feature_matrix_result = nothing bin_info_result = nothing # 2. Reset ALL reactive variables using captured initial state if !isempty(INITIAL_MODEL_STATE) for (name, value) in INITIAL_MODEL_STATE setfield!(__model__, name, deepcopy(value)) end msg = "Session reset: restored $(length(INITIAL_MODEL_STATE)) variables to initial state." else msg = "Warning: No initial state captured. Using partial reset." end # 3. Reset file lists (these will be repopulated by normal operation) msi_bmp = sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")), lt=natural) col_msi_png = sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir("public")), lt=natural) triq_bmp = sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")), lt=natural) col_triq_png = sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir("public")), lt=natural) # 4. Clear any cached images/plots imgInt = "/.bmp" imgIntT = "/.bmp" colorbar = "/.png" colorbarT = "/.png" # 5. Reset plot data to default traces traceImg = PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) plotdataImg = [traceImg] plotdataImgT = [traceImg] plotdata = [PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines")] # 6. Aggressive garbage collection GC.gc(true) if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end println("Session reset successfully.") catch e println("Error during session reset: $e") msg = "Reset error: $e" finally is_processing = false end end #= @onbutton btnSearch This reactive handler job is to open a file dialog and prompt the user to select a file its the entry point to any process done later in the interface, it creates a new dataset entry in the json and updates the list of available datasets in the interface. preprocesses the file and saves metadata to the json file. if the file is already in the json file, it will skip the preprocessing and load the metadata from the json file. creates the suggestions to the preprocessing pipeline, and makes sure the file can be processed by later steps like mainProcess =# @onbutton btnSearch begin is_processing = true push!(__model__) picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml") if isnothing(picked_route) || isempty(picked_route) is_processing = false return end # --- Close previous dataset if one is open --- if msi_data !== nothing println("DEBUG: Closing previously loaded dataset before opening new one: $(basename(full_route))") close(msi_data) msi_data = nothing GC.gc() if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end end 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 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)" end # --- Full Load Path --- 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 if existing_entry == nothing msg = "Performing first-time metadata analysis for: $(basename(picked_route))..." precompute_analytics(loaded_data) end # 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 # Determine plot mode from loaded data df = msi_data.spectrum_stats_df if df !== nothing && "Mode" in names(df) profile_count = count(==(MSI_src.PROFILE), df.Mode) total_count = length(df.Mode) last_plot_mode = profile_count > total_count / 2 ? "lines" : "stem" println("DEBUG: Auto-detected plot mode: $(last_plot_mode)") else last_plot_mode = "lines" # Default end 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." SpectraEnabled = true catch e msi_data = nothing msg = "Error loading active file: $e" warning_msg = 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 is_processing = false end end @onbutton export_params_btn begin is_processing = true push!(__model__) params_to_export = Dict( "pipeline_step_order" => pipeline_step_order, "enable_standards" => enable_standards, # Export global flag "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 ) # 1. Generate the JSON string json_string = JSON.json(params_to_export) # 2. ESCAPING (Crucial for stability) # We must escape backslashes (for Windows paths) and single quotes # so they don't break the JavaScript string literal. safe_json = replace(json_string, "\\" => "\\\\") safe_json = replace(safe_json, "'" => "\\'") # 3. Create the JavaScript payload # We inject 'safe_json' into the JS 'encodeURIComponent' js_script = """ var element = document.createElement('a'); element.setAttribute('href', 'data:text/json;charset=utf-8,' + encodeURIComponent('$safe_json')); element.setAttribute('download', 'preprocessing_params.json'); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); """ # 4. Execute on the client run(__model__, js_script) is_processing = false msg = "Parameters exported." end @onbutton import_params_btn begin is_processing = true push!(__model__) picked_file = pick_file(filterlist="json") if isempty(picked_file) is_processing = false return end try json_string = read(picked_file, String) params = JSON.parse(json_string) # Import special variables first if haskey(params, "reference_peaks_list") reference_peaks_list = params["reference_peaks_list"] end if haskey(params, "pipeline_step_order") pipeline_step_order = params["pipeline_step_order"] end if haskey(params, "enable_standards") enable_standards = params["enable_standards"] end # Import regular parameters with explicit assignments haskey(params, "stabilization_method") && (stabilization_method = params["stabilization_method"]) haskey(params, "smoothing_method") && (smoothing_method = params["smoothing_method"]) haskey(params, "smoothing_window") && (smoothing_window = params["smoothing_window"]) haskey(params, "smoothing_order") && (smoothing_order = params["smoothing_order"]) haskey(params, "baseline_method") && (baseline_method = params["baseline_method"]) haskey(params, "baseline_iterations") && (baseline_iterations = params["baseline_iterations"]) haskey(params, "baseline_window") && (baseline_window = params["baseline_window"]) haskey(params, "normalization_method") && (normalization_method = params["normalization_method"]) haskey(params, "alignment_method") && (alignment_method = params["alignment_method"]) haskey(params, "alignment_span") && (alignment_span = params["alignment_span"]) haskey(params, "alignment_tolerance") && (alignment_tolerance = params["alignment_tolerance"]) haskey(params, "alignment_tolerance_unit") && (alignment_tolerance_unit = params["alignment_tolerance_unit"]) haskey(params, "alignment_max_shift_ppm") && (alignment_max_shift_ppm = params["alignment_max_shift_ppm"]) haskey(params, "alignment_min_matched_peaks") && (alignment_min_matched_peaks = params["alignment_min_matched_peaks"]) haskey(params, "peak_picking_method") && (peak_picking_method = params["peak_picking_method"]) haskey(params, "peak_picking_snr_threshold") && (peak_picking_snr_threshold = params["peak_picking_snr_threshold"]) haskey(params, "peak_picking_half_window") && (peak_picking_half_window = params["peak_picking_half_window"]) haskey(params, "peak_picking_min_peak_prominence") && (peak_picking_min_peak_prominence = params["peak_picking_min_peak_prominence"]) haskey(params, "peak_picking_merge_peaks_tolerance") && (peak_picking_merge_peaks_tolerance = params["peak_picking_merge_peaks_tolerance"]) haskey(params, "peak_picking_min_peak_width_ppm") && (peak_picking_min_peak_width_ppm = params["peak_picking_min_peak_width_ppm"]) haskey(params, "peak_picking_max_peak_width_ppm") && (peak_picking_max_peak_width_ppm = params["peak_picking_max_peak_width_ppm"]) haskey(params, "peak_picking_min_peak_shape_r2") && (peak_picking_min_peak_shape_r2 = params["peak_picking_min_peak_shape_r2"]) haskey(params, "binning_method") && (binning_method = params["binning_method"]) haskey(params, "binning_tolerance") && (binning_tolerance = params["binning_tolerance"]) haskey(params, "binning_tolerance_unit") && (binning_tolerance_unit = params["binning_tolerance_unit"]) haskey(params, "binning_frequency_threshold") && (binning_frequency_threshold = params["binning_frequency_threshold"]) haskey(params, "binning_min_peak_per_bin") && (binning_min_peak_per_bin = params["binning_min_peak_per_bin"]) haskey(params, "binning_max_bin_width_ppm") && (binning_max_bin_width_ppm = params["binning_max_bin_width_ppm"]) haskey(params, "binning_intensity_weighted_centers") && (binning_intensity_weighted_centers = params["binning_intensity_weighted_centers"]) haskey(params, "binning_num_uniform_bins") && (binning_num_uniform_bins = params["binning_num_uniform_bins"]) haskey(params, "calibration_fit_order") && (calibration_fit_order = params["calibration_fit_order"]) haskey(params, "calibration_ppm_tolerance") && (calibration_ppm_tolerance = params["calibration_ppm_tolerance"]) haskey(params, "peak_selection_min_snr") && (peak_selection_min_snr = params["peak_selection_min_snr"]) haskey(params, "peak_selection_min_fwhm_ppm") && (peak_selection_min_fwhm_ppm = params["peak_selection_min_fwhm_ppm"]) haskey(params, "peak_selection_max_fwhm_ppm") && (peak_selection_max_fwhm_ppm = params["peak_selection_max_fwhm_ppm"]) haskey(params, "peak_selection_min_shape_r2") && (peak_selection_min_shape_r2 = params["peak_selection_min_shape_r2"]) haskey(params, "peak_selection_frequency_threshold") && (peak_selection_frequency_threshold = params["peak_selection_frequency_threshold"]) haskey(params, "peak_selection_correlation_threshold") && (peak_selection_correlation_threshold = params["peak_selection_correlation_threshold"]) msg = "Parameters imported successfully from $(basename(picked_file))." catch e msg = "Failed to import parameters: $e" warning_msg = true @error "Parameter import failed" exception=(e, catch_backtrace()) end is_processing = false end @onbutton export_standards_btn begin is_processing = true json_string = JSON.json(reference_peaks_list) safe_json = replace(json_string, "\\" => "\\\\") safe_json = replace(safe_json, "'" => "\\'") js_script = """ var element = document.createElement('a'); element.setAttribute('href', 'data:text/json;charset=utf-8,' + encodeURIComponent('$safe_json')); element.setAttribute('download', 'internal_standards.json'); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); """ run(__model__, js_script) is_processing = false msg = "Internal standards exported." end @onbutton import_standards_btn begin is_processing = true picked_file = pick_file(filterlist="json") if isempty(picked_file) return end try json_string = read(picked_file, String) new_standards = JSON.parse(json_string) # Basic validation if new_standards isa Vector && all(p -> p isa Dict && haskey(p, "mz") && haskey(p, "label"), new_standards) reference_peaks_list = new_standards msg = "Internal standards imported successfully from $(basename(picked_file))." else msg = "Invalid format for internal standards file." warning_msg = true end catch e msg = "Failed to import internal standards: $e" warning_msg = true @error "Standards import failed" exception=(e, catch_backtrace()) end is_processing = false end #= This reactive handler job is to run the full preprocessing pipeline on the selected dataset. =# @onbutton run_full_pipeline begin is_processing = true push!(__model__) overall_progress = 0.0 local pipeline_msi_data = nothing local current_spectra = Vector{MutableSpectrum}() current_pipeline_step = "Initializing..." try # --- 1. Initial Checks and Data Loading --- if isempty(selected_folder_main) msg = "No dataset loaded. Please load a file using 'Select an imzMl / mzML file'." warning_msg = true println("DEBUG: $msg") is_processing = false return end registry = load_registry(registry_path) 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"] # Ensure msi_data is for the currently selected file and load if needed # NOTE: For the pipeline, we will open a DEDICATED instance to avoid race conditions # with the global msi_data used for plotting/interactive exploration. println("DEBUG: Opening isolated MSIData instance for pipeline stability...") pipeline_msi_data = OpenMSIData(target_path) # Determine plot mode from metadata for correct visualization late metadata = pipeline_msi_data.instrument_metadata acq_mode = metadata !== nothing ? metadata.acquisition_mode : :unknown if acq_mode == :centroid last_plot_mode = "stem" elseif acq_mode == :profile last_plot_mode = "lines" else # Fallback to stats if mode is unknown df = pipeline_msi_data.spectrum_stats_df if df !== nothing && "Mode" in names(df) profile_count = count(==(MSI_src.PROFILE), df.Mode) total_count = length(df.Mode) last_plot_mode = profile_count > total_count / 2 ? "lines" : "stem" else last_plot_mode = "lines" # Default end end println("DEBUG: Auto-detected plot mode from metadata: $(last_plot_mode) (acq_mode: $(acq_mode)) [Initial set]") # 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") close(pipeline_msi_data) # Important cleanup 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") close(pipeline_msi_data) # Important cleanup return end else println("DEBUG: Masking is DISABLED. No mask will be applied.") end # Apply mask if enabled to get indices to process # Use pipeline_msi_data for consistency spectrum_indices_to_process = collect(1:length(pipeline_msi_data.spectra_metadata)) 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, pipeline_msi_data.image_dims) masked_indices_set = get_masked_spectrum_indices(pipeline_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") close(pipeline_msi_data) # Important cleanup return end println("DEBUG: $(length(spectrum_indices_to_process)) spectra remaining after mask application.") else println("DEBUG: No mask applied. Processing all $(length(pipeline_msi_data.spectra_metadata)) spectra.") end # Apply subset processing if enabled if enable_subset_processing && spectra_subset_size > 0 n_total = length(spectrum_indices_to_process) n_to_process = min(spectra_subset_size, n_total) spectrum_indices_to_process = spectrum_indices_to_process[1:n_to_process] println("DEBUG: Subset processing enabled. Processing first $(length(spectrum_indices_to_process)) of $n_total spectra.") end # --- BOUNDS VALIDATION AND DIAGNOSTIC LOGGING --- # Validate all indices are within bounds before attempting to load max_spectra_idx = length(pipeline_msi_data.spectra_metadata) println("DEBUG: Total spectra in dataset: $max_spectra_idx") println("DEBUG: Number of indices to process: $(length(spectrum_indices_to_process))") if !isempty(spectrum_indices_to_process) min_idx = minimum(spectrum_indices_to_process) max_idx = maximum(spectrum_indices_to_process) println("DEBUG: Spectrum indices range: $min_idx to $max_idx") # Check for invalid indices invalid_indices = filter(idx -> idx < 1 || idx > max_spectra_idx, spectrum_indices_to_process) if !isempty(invalid_indices) n_invalid = length(invalid_indices) sample_invalid = first(sort(invalid_indices), min(10, n_invalid)) msg = "Invalid spectrum indices detected: $n_invalid indices out of range [1, $max_spectra_idx]. First few invalid indices: $sample_invalid" warning_msg = true @error msg println("DEBUG: $msg") close(pipeline_msi_data) # Important cleanup return end println("DEBUG: All spectrum indices are valid (within [1, $max_spectra_idx]).") else println("DEBUG: Warning - spectrum_indices_to_process is empty!") end # CRITICAL: Verify indices are unique to prevent race conditions during loading if length(Set(spectrum_indices_to_process)) != length(spectrum_indices_to_process) @warn "Non-unique indices detected in spectrum_indices_to_process. This may cause issues during parallel loading." end # Use pipeline_msi_data for reading # Split loading into chunks to update progress bar n_spectra = length(spectrum_indices_to_process) println("DEBUG: Loading $n_spectra spectra into MutableSpectrum objects...") current_spectra = Vector{MutableSpectrum}(undef, n_spectra) chunk_size = max(1, n_spectra ÷ 10) # Update progress every 10% for chunk_start in 1:chunk_size:n_spectra chunk_end = min(chunk_start + chunk_size - 1, n_spectra) Threads.@threads for i in chunk_start:chunk_end local original_idx = spectrum_indices_to_process[i] local mz, intensity # Enforce thread-local scope try mz, intensity = GetSpectrum(pipeline_msi_data, original_idx) # Diagnostic check for length mismatch and defensive truncation l_mz = length(mz) l_int = length(intensity) if l_mz != l_int new_len = min(l_mz, l_int) @warn "CRITICAL: Mismatch during loading at index $original_idx. mz=$l_mz, int=$l_int. TRUNCATING." mz = mz[1:new_len] intensity = intensity[1:new_len] end current_spectra[i] = MutableSpectrum(original_idx, copy(Float64.(mz)), copy(Float64.(intensity)), NamedTuple{(:mz, :intensity, :fwhm, :shape_r2, :snr, :prominence), NTuple{6, Float64}}[]) catch loop_error rethrow(loop_error) end end overall_progress = (chunk_end / n_spectra) * 0.2 # Loading is first 20% push!(__model__) end println("DEBUG: All spectra loaded into temporary structure for processing.") # We can now close the local MSI data instance as we have loaded everything into memory # However, if we want to support lazy loading scenarios later, we might keep it open. # For now, let's close it here to free up file handles early, # UNLESS `execute_full_preprocessing` needs it (it doesn't seem to based on signature). close(pipeline_msi_data) pipeline_msi_data = nothing # Prevent accidental use # Aggressive memory cleanup to return memory to OS println("DEBUG: Performing aggressive memory cleanup...") GC.gc(true) # Full garbage collection with all generations if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end println("DEBUG: Closed local pipeline MSIData instance and freed memory.") # --- 2. Parameter Assembly with Validation --- current_pipeline_step = "Configuring parameters..." println("DEBUG: Configuring parameters and validating enabled steps...") ref_peaks = Dict{Float64, String}( parse(Float64, string(p["mz"])) => string(get(p, "label", "")) for p in reference_peaks_list if tryparse(Float64, string(p["mz"])) !== nothing ) 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 # --- 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 => 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") # 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, mask_path_for_pipeline ) do step current_pipeline_step = "Processing: $step" # Update progress based on step index step_idx = findfirst(==(step), pipeline_stp) if step_idx !== nothing # Preprocessing is 20% to 90% (total 70%) overall_progress = 0.2 + (step_idx / length(pipeline_stp)) * 0.7 end push!(__model__) end println("DEBUG: Pipeline execution finished.") # 4. Update Results Display current_pipeline_step = "Updating results..." subset_label = enable_subset_processing ? " (from subset of $(length(current_spectra)) spectra)" : "" println("DEBUG: Updating results display after pipeline completion for plot type: $(last_plot_type), mode: $(last_plot_mode)") if last_plot_type == "single" 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.") # Determine plot mode for this specific spectrum spectrum_mode_for_plot = "lines" # Default to lines if msi_data.spectrum_stats_df !== nothing && "Mode" in names(msi_data.spectrum_stats_df) if selected_spectrum_id_for_plot > 0 && selected_spectrum_id_for_plot <= length(msi_data.spectrum_stats_df.Mode) mode = msi_data.spectrum_stats_df.Mode[selected_spectrum_id_for_plot] if mode == MSI_src.CENTROID spectrum_mode_for_plot = "stem" end end end mz_down, int_down = downsample_spectrum(processed_spectrum.mz, processed_spectrum.intensity) local after_trace if spectrum_mode_for_plot == "stem" after_trace = PlotlyBase.stem( x=mz_down, y=int_down, name="Processed Spectrum", marker=attr(size=1, color="blue", opacity=0) ) else # lines after_trace = PlotlyBase.scatter( x=mz_down, y=int_down, mode="lines", name="Processed Spectrum" ) end traces_after = [after_trace] 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=PlotlyBase.attr( text="After Preprocessing (Spectrum $(selected_spectrum_id_for_plot))$(subset_label)", 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=0.98, y=0.98, xanchor="right", yanchor="top") ) else println("DEBUG: Selected spectrum for display ($(selected_spectrum_id_for_plot)) not found in processed spectra.") end elseif last_plot_type == "mean" mz, intensity = get_processed_mean_spectrum(current_spectra) mz_down, int_down = downsample_spectrum(mz, intensity) local trace if last_plot_mode == "stem" trace = PlotlyBase.stem( x=mz_down, y=int_down, name="Processed Mean Spectrum", marker=attr(size=1, color="blue", opacity=0.5), hoverinfo="x", hovertemplate="m/z: %{x:.4f}" ) else trace = PlotlyBase.scatter( x=mz_down, y=int_down, mode="lines", name="Processed Mean Spectrum", marker=attr(size=1, color="blue", opacity=0.5), hoverinfo="x", hovertemplate="m/z: %{x:.4f}" ) end plotdata_after = [trace] plotlayout_after = PlotlyBase.Layout( title=PlotlyBase.attr( text="After Preprocessing (Mean Spectrum)$(subset_label)", 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="Average 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") ) elseif last_plot_type == "sum" mz, intensity = get_processed_sum_spectrum(current_spectra) mz_down, int_down = downsample_spectrum(mz, intensity) local trace if last_plot_mode == "stem" trace = PlotlyBase.stem( x=mz_down, y=int_down, name="Processed Sum Spectrum", marker=attr(size=1, color="blue", opacity=0.5), hoverinfo="x", hovertemplate="m/z: %{x:.4f}" ) else trace = PlotlyBase.scatter( x=mz_down, y=int_down, mode="lines", name="Processed Sum Spectrum", marker=attr(size=1, color="blue", opacity=0.5), hoverinfo="x", hovertemplate="m/z: %{x:.4f}" ) end plotdata_after = [trace] plotlayout_after = PlotlyBase.Layout( title=PlotlyBase.attr( text="After Preprocessing (Sum Spectrum)$(subset_label)", 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="Total 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") ) end # Save feature matrix if binning was performed if feature_matrix_result !== nothing output_dir = joinpath("public", selected_folder_main, "preprocessing_results") mkpath(output_dir) save_feature_matrix(feature_matrix_result, bin_info_result, output_dir) msg = "Pipeline completed successfully. Feature matrix saved." overall_progress = 1.0 push!(__model__) else 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 # Aggressive memory cleanup println("DEBUG: Starting aggressive memory cleanup...") # Explicitly clear large data structures try if current_spectra !== nothing && !isempty(current_spectra) # Deep clear individual objects to break references effectively # Use isassigned to prevent UndefRefError if loading failed halfway for i in eachindex(current_spectra) if isassigned(current_spectra, i) s = current_spectra[i] s.mz = Float64[] s.intensity = Float64[] empty!(s.peaks) end end empty!(current_spectra) end current_spectra = nothing if feature_matrix_result !== nothing feature_matrix_result = nothing end # Close any open pipeline data handles if pipeline_msi_data !== nothing try close(pipeline_msi_data) catch # Already closed, ignore end pipeline_msi_data = nothing end println("DEBUG: Data structures cleared. Triggering garbage collection...") catch cleanup_error @warn "Error during data cleanup: $cleanup_error" end is_processing = false overall_progress = 0.0 current_pipeline_step = "" println("DEBUG: run_full_pipeline finished (finally block).") # Force garbage collection multiple times for thorough cleanup GC.gc() GC.gc() # Second pass to catch any circular references # On Linux/Unix, force Julia to return memory to OS if Sys.islinux() try ccall(:malloc_trim, Int32, (Int32,), 0) println("DEBUG: malloc_trim called successfully (Linux).") catch e @warn "malloc_trim failed: $e" end end println("DEBUG: Memory cleanup complete.") end end #= @onbutton recalculate_suggestions_btn This reactive handler job is to recalculate the suggestions for the preprocessing pipeline. =# @onbutton recalculate_suggestions_btn begin is_processing = true push!(__model__) if msi_data === nothing msg = "Please load a file first." warning_msg = true return end try msg = "Recalculating suggestions..." ref_peaks = Dict{Float64, String}( parse(Float64, string(p["mz"])) => string(get(p, "label", "")) for p in reference_peaks_list if tryparse(Float64, string(p["mz"])) !== nothing ) 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 is_processing = false end end #= @onbutton addReferencePeak This reactive handler job is to add a new reference peak to the list of reference peaks. =# @onbutton addReferencePeak begin is_processing = true 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 is_processing = false end #= @onbutton remove_peak_trigger This reactive handler job is to remove a reference peak from the list of reference peaks. =# @onbutton remove_peak_trigger begin is_processing = true 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 is_processing = false end @onbutton move_step_up_trigger begin is_processing = true 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 is_processing = false end @onbutton move_step_down_trigger begin is_processing = true 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 is_processing = false end @onbutton toggle_step_trigger begin is_processing = true 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 is_processing = false end #= @onbutton btnAddBatch This reactive handler job is to add a new file to the batch list. =# @onbutton btnAddBatch begin is_processing = true push!(__model__) 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 is_processing = false end #= @onbutton clear_batch_btn This reactive handler job is to clear the batch list in the UI =# @onbutton clear_batch_btn begin is_processing = true push!(__model__) selected_files = String[] batch_file_count = 0 msg = "Batch cleared" is_processing = false 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 This reactive handler job is to show the metadata of the selected dataset. =# @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 This reactive handler job is to update the metadata table when the selected folder changes. =# @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 @onchange btnSearchMzml, btnSearchSync begin is_processing = true push!(__model__) if btnSearchMzml picked_route = pick_file(; filterlist="mzML,mzml") if !isempty(picked_route) mzml_full_route = picked_route end btnSearchMzml = false # Reset the button end if btnSearchSync picked_route = pick_file(; filterlist="txt") if !isempty(picked_route) sync_full_route = picked_route end btnSearchSync = false # Reset the button end # Enable button only if both files are selected btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route) is_processing = false end @onbutton convert_process begin is_processing = true push!(__model__) if isempty(mzml_full_route) || isempty(sync_full_route) msg_conversion = "Please select both an .mzML file and a .txt sync file." warning_msg = true return end msg_conversion = "Starting conversion process..." try sTime = time() target_imzml = replace(mzml_full_route, r"\.(mzml|mzML)$" => ".imzML") msg_conversion = "Converting $(basename(mzml_full_route)) to $(basename(target_imzml))... This may take a while." success = ImportMzmlFile(mzml_full_route, sync_full_route, target_imzml) fTime = time() eTime = round(fTime - sTime, digits=3) if success msg_conversion = "Conversion successful in $(eTime) seconds. Output file: $(basename(target_imzml))" else msg_conversion = "Conversion failed after $(eTime) seconds. Check console for errors." warning_msg = true end catch e msg_conversion = "An error occurred during conversion: $e" warning_msg = true @error "Conversion failed" exception=(e, catch_backtrace()) finally is_processing = false overall_progress = 0.0 # Re-enable button if files are still selected btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route) end end @onbutton mainProcess @time begin # --- UI State Update --- 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 is_processing = true push!(__model__) 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 = (file_idx - 1) / num_files push!(__model__) 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 --- is_processing = 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 # Pre-initialize for safe cleanup in finally block local xSpectraMz = Vector{Float64}() local ySpectraMz = Vector{Float64}() if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end is_processing = true push!(__model__) 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 last_plot_type = "mean" 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 is_processing = false try if plotdata_before !== nothing plotdata_before = nothing end if !isempty(xSpectraMz) empty!(xSpectraMz) end if !isempty(ySpectraMz) empty!(ySpectraMz) end catch 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 end @onbutton createSumPlot @time begin # Pre-initialize for safe cleanup in finally block local xSpectraMz = Vector{Float64}() local ySpectraMz = Vector{Float64}() if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end is_processing = true push!(__model__) 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 last_plot_type = "sum" 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 is_processing = false try if plotdata_before !== nothing plotdata_before = nothing end if !isempty(xSpectraMz) empty!(xSpectraMz) end if !isempty(ySpectraMz) empty!(ySpectraMz) end catch 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 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 is_processing = true push!(__model__) 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 # Convert to positive coordinates for processing y_positive = yCoord < 0 ? abs(yCoord) : yCoord 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 last_plot_type = "single" 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 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 is_processing = 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 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 is_processing = true push!(__model__) 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 last_plot_type = "single" 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 is_processing = 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 timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) # Check if folder exists to prevent errors if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_msi, msi_bmp) new_col_msi=decrement_image(current_col_msi, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msi = new_msi current_col_msi = new_col_msi imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)" colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)" text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt) end end @onbutton imgPlus begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_msi, msi_bmp) new_col_msi=increment_image(current_col_msi, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msi = new_msi current_col_msi = new_col_msi imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)" colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)" text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt) end end @onbutton imgMinusT begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_triq, triq_bmp) new_col_msi=decrement_image(current_col_triq, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triq = new_msi current_col_triq = new_col_msi imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)" colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)" text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) end end @onbutton imgPlusT begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_triq, triq_bmp) new_col_msi=increment_image(current_col_triq, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triq = new_msi current_col_triq = new_col_msi imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)" colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)" text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) end end # --- Compare View Handlers --- @onbutton imgMinusCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_msiCompLeft, msi_bmp) new_col_msi=decrement_image(current_col_msiCompLeft, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompLeft = new_msi current_col_msiCompLeft = new_col_msi imgIntCompLeft = "/$(selected_folder_compare_left)/$(current_msiCompLeft)?t=$(timestamp)" colorbarCompLeft = "/$(selected_folder_compare_left)/$(current_col_msiCompLeft)?t=$(timestamp)" text_nmass = replace(current_msiCompLeft, r"MSI_|.bmp" => "") msgimgCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompLeft, plotlayoutImgCompLeft, _, _ = loadImgPlot(imgIntCompLeft) end end @onbutton imgPlusCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_msiCompLeft, msi_bmp) new_col_msi=increment_image(current_col_msiCompLeft, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompLeft = new_msi current_col_msiCompLeft = new_col_msi imgIntCompLeft = "/$(selected_folder_compare_left)/$(current_msiCompLeft)?t=$(timestamp)" colorbarCompLeft = "/$(selected_folder_compare_left)/$(current_col_msiCompLeft)?t=$(timestamp)" text_nmass = replace(current_msiCompLeft, r"MSI_|.bmp" => "") msgimgCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompLeft, plotlayoutImgCompLeft, _, _ = loadImgPlot(imgIntCompLeft) end end @onbutton imgMinusTCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_triqCompLeft, triq_bmp) new_col_msi=decrement_image(current_col_triqCompLeft, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompLeft = new_msi current_col_triqCompLeft = new_col_msi imgIntTCompLeft = "/$(selected_folder_compare_left)/$(current_triqCompLeft)?t=$(timestamp)" colorbarTCompLeft = "/$(selected_folder_compare_left)/$(current_col_triqCompLeft)?t=$(timestamp)" text_nmass = replace(current_triqCompLeft, r"TrIQ_|.bmp" => "") msgtriqCompLeft = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompLeft, plotlayoutImgTCompLeft, _, _ = loadImgPlot(imgIntTCompLeft) end end @onbutton imgPlusTCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_triqCompLeft, triq_bmp) new_col_msi=increment_image(current_col_triqCompLeft, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompLeft = new_msi current_col_triqCompLeft = new_col_msi imgIntTCompLeft = "/$(selected_folder_compare_left)/$(current_triqCompLeft)?t=$(timestamp)" colorbarTCompLeft = "/$(selected_folder_compare_left)/$(current_col_triqCompLeft)?t=$(timestamp)" text_nmass = replace(current_triqCompLeft, r"TrIQ_|.bmp" => "") msgtriqCompLeft = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompLeft, plotlayoutImgTCompLeft, _, _ = loadImgPlot(imgIntTCompLeft) end end @onbutton imgMinusCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_msiCompRight, msi_bmp) new_col_msi=decrement_image(current_col_msiCompRight, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompRight = new_msi current_col_msiCompRight = new_col_msi imgIntCompRight = "/$(selected_folder_compare_right)/$(current_msiCompRight)?t=$(timestamp)" colorbarCompRight = "/$(selected_folder_compare_right)/$(current_col_msiCompRight)?t=$(timestamp)" text_nmass = replace(current_msiCompRight, r"MSI_|.bmp" => "") msgimgCompRight = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompRight, plotlayoutImgCompRight, _, _ = loadImgPlot(imgIntCompRight) end end @onbutton imgPlusCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_msiCompRight, msi_bmp) new_col_msi=increment_image(current_col_msiCompRight, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompRight = new_msi current_col_msiCompRight = new_col_msi imgIntCompRight = "/$(selected_folder_compare_right)/$(current_msiCompRight)?t=$(timestamp)" colorbarCompRight = "/$(selected_folder_compare_right)/$(current_col_msiCompRight)?t=$(timestamp)" text_nmass = replace(current_msiCompRight, r"MSI_|.bmp" => "") msgimgCompRight = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompRight, plotlayoutImgCompRight, _, _ = loadImgPlot(imgIntCompRight) end end @onbutton imgMinusTCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_triqCompRight, triq_bmp) new_col_msi=decrement_image(current_col_triqCompRight, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompRight = new_msi current_col_triqCompRight = new_col_msi imgIntTCompRight = "/$(selected_folder_compare_right)/$(current_triqCompRight)?t=$(timestamp)" colorbarTCompRight = "/$(selected_folder_compare_right)/$(current_col_triqCompRight)?t=$(timestamp)" text_nmass = replace(current_triqCompRight, r"TrIQ_|.bmp" => "") msgtriqCompRight = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompRight, plotlayoutImgTCompRight, _, _ = loadImgPlot(imgIntTCompRight) end end @onbutton imgPlusTCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_triqCompRight, triq_bmp) new_col_msi=increment_image(current_col_triqCompRight, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompRight = new_msi current_col_triqCompRight = new_col_msi imgIntTCompRight = "/$(selected_folder_compare_right)/$(current_triqCompRight)?t=$(timestamp)" colorbarTCompRight = "/$(selected_folder_compare_right)/$(current_col_triqCompRight)?t=$(timestamp)" text_nmass = replace(current_triqCompRight, r"TrIQ_|.bmp" => "") msgtriqCompRight = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompRight, plotlayoutImgTCompRight, _, _ = loadImgPlot(imgIntTCompRight) end end # This handler will now correctly load the first image from the newly selected folder. @onchange selected_folder_main begin # The msi_data object lifecycle is managed by the btnSearch handler. # This handler is now only for updating the UI images when the folder changes. if !isempty(selected_folder_main) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) imgInt = "" colorbar = "" imgIntT = "" colorbarT = "" msgimg = "Folder not found." msgtriq = "Folder not found." plotdataImg = [traceImg] plotlayoutImg = layoutImg plotdataImgT = [traceImg] plotlayoutImgT = layoutImg imgWidth, imgHeight = 0, 0 return end # Handle normal images msi_bmp = sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)), lt=natural) col_msi_png = sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)), lt=natural) if !isempty(msi_bmp) current_msi = first(msi_bmp) imgInt = "/$(selected_folder_main)/$(current_msi)" plotdataImg, plotlayoutImg, w, h = loadImgPlot(imgInt) imgWidth, imgHeight = w, h text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_png) current_col_msi = first(col_msi_png) colorbar = "/$(selected_folder_main)/$(current_col_msi)" else colorbar = "" end else imgInt = "" colorbar = "" msgimg = "No MSI images found in this dataset." plotdataImg = [traceImg] plotlayoutImg = layoutImg end # Handle TrIQ images triq_bmp = sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)), lt=natural) col_triq_png = sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)), lt=natural) if !isempty(triq_bmp) current_triq = first(triq_bmp) imgIntT = "/$(selected_folder_main)/$(current_triq)" plotdataImgT, plotlayoutImgT, w, h = loadImgPlot(imgIntT) # If no MSI image was loaded, dimensions from TrIQ image are used. if isempty(msi_bmp) imgWidth, imgHeight = w, h end text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_png) current_col_triq = first(col_triq_png) colorbarT = "/$(selected_folder_main)/$(current_col_triq)" else colorbarT = "" end else imgIntT = "" colorbarT = "" msgtriq = "No TrIQ images found in this dataset." plotdataImgT = [traceImg] plotlayoutImgT = layoutImg end if isempty(msi_bmp) && isempty(triq_bmp) imgWidth, imgHeight = 0, 0 end end end @onchange selected_folder_compare_left begin if !isempty(selected_folder_compare_left) timestamp = string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) imgIntCompLeft, colorbarCompLeft, imgIntTCompLeft, colorbarTCompLeft = "", "", "", "" msgimgCompLeft, msgtriqCompLeft = "Folder not found.", "Folder not found." return end # Handle normal images msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_msi_png = sort(filter(f -> startswith(f, "colorbar_MSI_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(msi_bmp) current_msiCompLeft = first(msi_bmp) imgIntCompLeft = "/$(selected_folder_compare_left)/$(current_msiCompLeft)?t=$(timestamp)" plotdataImgCompLeft, plotlayoutImgCompLeft, _, _ = loadImgPlot(imgIntCompLeft) text_nmass = replace(current_msiCompLeft, r"MSI_|.bmp" => "") msgimgCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_png) current_col_msiCompLeft = first(col_msi_png) colorbarCompLeft = "/$(selected_folder_compare_left)/$(current_col_msiCompLeft)?t=$(timestamp)" else colorbarCompLeft = "" end else imgIntCompLeft, colorbarCompLeft, msgimgCompLeft = "", "", "No MSI images." end # Handle TrIQ images triq_bmp = sort(filter(f -> startswith(f, "TrIQ_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_triq_png = sort(filter(f -> startswith(f, "colorbar_TrIQ_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(triq_bmp) current_triqCompLeft = first(triq_bmp) imgIntTCompLeft = "/$(selected_folder_compare_left)/$(current_triqCompLeft)?t=$(timestamp)" plotdataImgTCompLeft, plotlayoutImgTCompLeft, _, _ = loadImgPlot(imgIntTCompLeft) text_nmass = replace(current_triqCompLeft, r"TrIQ_|.bmp" => "") msgtriqCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_png) current_col_triqCompLeft = first(col_triq_png) colorbarTCompLeft = "/$(selected_folder_compare_left)/$(current_col_triqCompLeft)?t=$(timestamp)" else colorbarTCompLeft = "" end else imgIntTCompLeft, colorbarTCompLeft, msgtriqCompLeft = "", "", "No TrIQ images." end end end @onchange selected_folder_compare_right begin if !isempty(selected_folder_compare_right) timestamp = string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) imgIntCompRight, colorbarCompRight, imgIntTCompRight, colorbarTCompRight = "", "", "", "" msgimgCompRight, msgtriqCompRight = "Folder not found.", "Folder not found." return end # Handle normal images msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_msi_png = sort(filter(f -> startswith(f, "colorbar_MSI_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(msi_bmp) current_msiCompRight = first(msi_bmp) imgIntCompRight = "/$(selected_folder_compare_right)/$(current_msiCompRight)?t=$(timestamp)" plotdataImgCompRight, plotlayoutImgCompRight, _, _ = loadImgPlot(imgIntCompRight) text_nmass = replace(current_msiCompRight, r"MSI_|.bmp" => "") msgimgCompRight = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_png) current_col_msiCompRight = first(col_msi_png) colorbarCompRight = "/$(selected_folder_compare_right)/$(current_col_msiCompRight)?t=$(timestamp)" else colorbarCompRight = "" end else imgIntCompRight, colorbarCompRight, msgimgCompRight = "", "", "No MSI images." end # Handle TrIQ images triq_bmp = sort(filter(f -> startswith(f, "TrIQ_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_triq_png = sort(filter(f -> startswith(f, "colorbar_TrIQ_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(triq_bmp) current_triqCompRight = first(triq_bmp) imgIntTCompRight = "/$(selected_folder_compare_right)/$(current_triqCompRight)?t=$(timestamp)" plotdataImgTCompRight, plotlayoutImgTCompRight, _, _ = loadImgPlot(imgIntTCompRight) text_nmass = replace(current_triqCompRight, r"TrIQ_|.bmp" => "") msgtriqCompRight = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_png) current_col_triqCompRight = first(col_triq_png) colorbarTCompRight = "/$(selected_folder_compare_right)/$(current_col_triqCompRight)?t=$(timestamp)" else colorbarTCompRight = "" end else imgIntTCompRight, colorbarTCompRight, msgtriqCompRight = "", "", "No TrIQ images." end end end # 3d plot @onbutton image3dPlot begin msg = "Image 3D plot selected" cleaned_imgInt = replace(imgInt, r"\?.*" => "") cleaned_imgInt = lstrip(cleaned_imgInt, '/') var = joinpath("./public", cleaned_imgInt) if !isfile(var) msg = "Image could not be 3d plotted" warning_msg = true return end is_processing = true push!(__model__) try # --- Get Mask Path --- local mask_path_for_plot::Union{String, Nothing} = nothing if maskEnabled && !isempty(selected_folder_main) registry = load_registry(registry_path) entry = get(registry, selected_folder_main, nothing) if entry !== nothing && get(entry, "has_mask", false) mask_path_candidate = get(entry, "mask_path", "") if isfile(mask_path_candidate) mask_path_for_plot = mask_path_candidate else @warn "Mask enabled but file not found: $(mask_path_candidate). Plotting without mask." end end end # --- sTime = time() if mask_path_for_plot !== nothing plotdata3d, plotlayout3d = loadSurfacePlot(imgInt, mask_path_for_plot) else plotdata3d, plotlayout3d = loadSurfacePlot(imgInt) end selectedTab = "tab4" 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 = "Failed to load and process image: $e" warning_msg = true @error "3D plot generation failed" exception=(e, catch_backtrace()) finally is_processing = false SpectraEnabled=true 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 triq3dPlot begin msg = "TrIQ 3D plot selected" cleaned_imgIntT = replace(imgIntT, r"\?.*" => "") cleaned_imgIntT = lstrip(cleaned_imgIntT, '/') var = joinpath("./public", cleaned_imgIntT) if !isfile(var) msg = "Image could not be 3d plotted" warning_msg = true return end is_processing = true push!(__model__) try # --- Get Mask Path --- local mask_path_for_plot::Union{String, Nothing} = nothing if maskEnabled && !isempty(selected_folder_main) registry = load_registry(registry_path) entry = get(registry, selected_folder_main, nothing) if entry !== nothing && get(entry, "has_mask", false) mask_path_candidate = get(entry, "mask_path", "") if isfile(mask_path_candidate) mask_path_for_plot = mask_path_candidate else @warn "Mask enabled but file not found: $(mask_path_candidate). Plotting without mask." end end end # --- sTime = time() if mask_path_for_plot !== nothing plotdata3d, plotlayout3d = loadSurfacePlot(imgIntT, mask_path_for_plot) else plotdata3d, plotlayout3d = loadSurfacePlot(imgIntT) end selectedTab = "tab4" 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 = "Failed to load and process image: $e" warning_msg = true @error "3D TrIQ plot generation failed" exception=(e, catch_backtrace()) finally is_processing = false SpectraEnabled=true 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 # Contour 2d plot @onbutton imageCPlot begin msg="Image 2D plot selected" cleaned_imgInt=replace(imgInt, r"\?.*" => "") cleaned_imgInt=lstrip(cleaned_imgInt, '/') var=joinpath("./public", cleaned_imgInt) if !isfile(var) msg="Image could not be 2D plotted" warning_msg=true return end is_processing = true push!(__model__) try sTime=time() plotdataC,plotlayoutC=loadContourPlot(imgInt) GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end selectedTab="tab3" fTime=time() eTime=round(fTime-sTime,digits=3) msg="Plot loaded in $(eTime) seconds" catch e msg="Failed to load and process image: $e" warning_msg=true finally is_processing = false SpectraEnabled=true GC.gc() if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end end end # Contour 2d plot for TrIQ @onbutton triqCPlot begin msg="Image 2D plot selected" cleaned_imgIntT=replace(imgIntT, r"\?.*" => "") cleaned_imgIntT=lstrip(cleaned_imgIntT, '/') var=joinpath("./public", cleaned_imgIntT) if !isfile(var) msg="Image could not be 2D plotted" warning_msg=true return end is_processing = true push!(__model__) try sTime=time() plotdataC,plotlayoutC=loadContourPlot(imgIntT) GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end selectedTab="tab3" fTime=time() eTime=round(fTime-sTime,digits=3) msg="Plot loaded in $(eTime) seconds" catch e msg="Failed to load and process image: $e" warning_msg=true finally is_processing = false SpectraEnabled=true GC.gc() if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end end end @onbutton compareBtn begin CompareDialog=true end # To include a visualization in the spectrum plot indicating where is the selected mass @onchange Nmass begin if !isempty(xSpectraMz) df = msi_data.spectrum_stats_df plot_as_lines = false # Default to stem if df !== nothing && hasproperty(df, :Mode) && !isempty(df.Mode) profile_count = count(==(MSI_src.PROFILE), df.Mode) plot_as_lines = profile_count > length(df.Mode) / 2 end # Downsample for plotting performance mz_down, int_down = MSI_src.downsample_spectrum(xSpectraMz, ySpectraMz) local traceSpectra if plot_as_lines # Main spectrum trace traceSpectra = PlotlyBase.scatter( x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}", showlegend=false ) else # Main spectrum trace traceSpectra = 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}", showlegend=false ) end # Parse all valid masses from the comma-separated string mass_strs = split(Nmass, ',', keepempty=false) mass_traces = [traceSpectra] # Start with the main spectrum valid_masses = Float64[] for (idx, mass_str) in enumerate(mass_strs) try mass_val = parse(Float64, strip(mass_str)) if mass_val > 0 # Only add valid positive masses push!(valid_masses, mass_val) # Create a vertical line for this mass (Plotly will auto-assign colors) mass_trace = PlotlyBase.scatter( x=[mass_val, mass_val], y=[0, maximum(ySpectraMz)], mode="lines", line=attr(width=1.5, dash="dash"), name="m/z $(round(mass_val, digits=4))", showlegend=false, hoverinfo="x+name", hovertemplate="%{data.name}" ) push!(mass_traces, mass_trace) end catch e # Skip invalid entries, continue with next continue end end # Update the plot data plotdata = mass_traces end end # Event detection for clicking on the images @onchange data_click begin if selectedTab == "tab1" || selectedTab == "tab0" # This is for the image heatmaps cursor_data = get(data_click, "cursor", nothing) if cursor_data === nothing return end x_val = get(cursor_data, "x", nothing) y_val = get(cursor_data, "y", nothing) if x_val === nothing || y_val === nothing return # Do nothing if coordinates are not provided by the event end x = Int32(round(x_val)) y = Int32(round(y_val)) # y is negative in the UI # Update the reactive coordinates, which will trigger the crosshair update xCoord = clamp(x, 1, imgWidth) yCoord = clamp(y, -imgHeight, -1) end end @onchange xCoord, yCoord begin if selectedTab == "tab1" main_trace = plotdataImgT[1] # The heatmap/image trace trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight) plotdataImgT = [main_trace, trace1, trace2] # Fresh array every time elseif selectedTab == "tab0" main_trace = plotdataImg[1] # The heatmap/image trace trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight) plotdataImg = [main_trace, trace1, trace2] end end @onbutton btnOptical begin is_processing = true imgRoute=pick_file(; filterlist="png,bmp,jpg,jpeg") if imgRoute=="" msg="No optical image selected" else selectedTab="tab0" plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT) img=load(imgRoute) save("./public/css/imgOver.png",img) plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt,"/css/imgOver.png",imgTrans) end is_processing = false end @onbutton btnOpticalT begin is_processing = true imgRoute=pick_file(; filterlist="png,bmp,jpg,jpeg") if imgRoute=="" msg="No optical image selected" else selectedTab="tab1" plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt) img=load(imgRoute) save("./public/css/imgOver.png",img) plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT,"/css/imgOver.png",imgTrans) opticalOverTriq=true end is_processing = false end @onchange imgTrans begin if !opticalOverTriq && imgRoute!="" plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt,"/css/imgOver.png",imgTrans) elseif opticalOverTriq && imgRoute!="" plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT,"/css/imgOver.png",imgTrans) end end @onchange opticalOverTriq begin if !opticalOverTriq && imgRoute!="" plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt,"/css/imgOver.png",imgTrans) plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT) selectedTab="tab0" elseif opticalOverTriq && imgRoute!="" plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt) plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT,"/css/imgOver.png",imgTrans) selectedTab="tab1" end end @onbutton refetch_folders begin is_processing = true # Re-load registry and update folder lists 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) # For q-selects using image_available_folders if !isempty(image_available_folders) first_img_folder = first(image_available_folders) if isempty(selected_folder_main) selected_folder_main = first_img_folder end if isempty(selected_folder_compare_left) selected_folder_compare_left = first_img_folder end if isempty(selected_folder_compare_right) selected_folder_compare_right = first_img_folder end end # For q-selects using available_folders if !isempty(available_folders) if isempty(selected_folder_metadata) selected_folder_metadata = first(available_folders) end end is_processing = false end @mounted watchplots() @onchange isready begin # Capture state on first run only if isempty(INITIAL_MODEL_STATE) capture_initial_state!(__model__) end # is_processing = true if isready && !registry_init_done sTime=time() msg = "Pre-compiling functions at startup..." warmup_init() msg = "Pre-compilation finished." try msg = "Synchronizing registry with filesystem on backend init..." reg_path = abspath(joinpath(@__DIR__, "public", "registry.json")) registry = isfile(reg_path) ? load_registry(reg_path) : Dict{String, Any}() public_dirs = isdir("public") ? readdir("public") : [] ignored_dirs = ["css", "masks"] dataset_dirs = filter(d -> isdir(joinpath("public", d)) && !(d in ignored_dirs), public_dirs) registry_keys = Set(keys(registry)) folder_set = Set(dataset_dirs) new_folders = setdiff(folder_set, registry_keys) for folder in new_folders println("Found new folder: $folder") registry[folder] = Dict( "source_path" => "unknown (manually added)", "processed_date" => "unknown", "metadata" => Dict(), "is_imzML" => true # Assume folder contains images if found this way ) end removed_folders = setdiff(registry_keys, folder_set) for folder in removed_folders delete!(registry, folder) end if !isempty(new_folders) || !isempty(removed_folders) msg = "Registry changed, saving..." save_registry(reg_path, registry) end 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) println("UI lists updated. All: $(length(available_folders)), Images: $(length(image_available_folders))") catch e @warn "Registry synchronization failed: $e" available_folders = [] image_available_folders = [] selected_files = String[] finally registry_init_done = true is_initializing = false # Hide loading screen when initialization is complete end end fTime=time() eTime=round(fTime-sTime,digits=3) is_initializing = false # Hide loading screen when initialization is complete (current code is hidden due to incompatibility) msg = "The app took $(eTime) seconds to get ready." log_memory_usage("App Ready", msi_data) end # is_processing = 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 # == Pages == @page("/", "app.jl.html") end