# app.jl module App # ==Packages == using GenieFramework using Pkg using Libz using PlotlyBase using CairoMakie using Colors using MSI_src # Import the new MSIData library using Statistics using NaturalSort using Images using LinearAlgebra using NativeFileDialog # Opens the file explorer depending on the OS using StipplePlotly using Base.Filesystem: mv # To rename files in the system using Printf # Required for @sprintf macro in colorbar generation using JSON using Dates using Base.Threads # Bring MSIData into App module's scope using .MSI_src: MSIData, OpenMSIData, process_spectrum, IterateSpectra, ImzMLSource, _iterate_spectra_fast, MzMLSource, find_mass, ViridisPalette, get_mz_slice, get_multiple_mz_slices, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile, generate_colorbar_image, load_and_prepare_mask, set_global_mz_range!, main_precalculation, MutableSpectrum if !@isdefined(increment_image) include("./julia_imzML_visual.jl") end # --- Memory Validation Logging --- if get(ENV, "GENIE_ENV", "dev") != "prod" function get_rss_mb() if !Sys.islinux() return 0.0 end try pid = getpid() cmd = `ps -p $pid -o rss=` rss_kb_str = read(cmd, String) rss_kb = parse(Int, strip(rss_kb_str)) return round(rss_kb / 1024, digits=2) catch e @warn "Could not get RSS via `ps` command. Error: $e" return 0.0 end end function log_memory_usage(context::String, msi_data_val) rss_mb = get_rss_mb() msi_data_size_mb = 0 if msi_data_val !== nothing msi_data_size_mb = round(Base.summarysize(msi_data_val) / (1024^2), digits=2) end gc_time_s = round(GC.time(), digits=3) println("--- MEMORY LOG [$(context)] ---") println(" Timestamp: $(now())") println(" Process RSS: $(rss_mb) MB") println(" msi_data size: $(msi_data_size_mb) MB") println(" Cumulative GC time: $(gc_time_s) s") println("--------------------------") end else log_memory_usage(context::String, msi_data_val) = nothing # No-op for production end @genietools # == Reactive code == # Reactive code to make the UI interactive @app begin # == Loading Screen Variables == @in is_initializing = true @in initialization_message = "Initializing..." # == Reactive variables == # reactive variables exist in both the Julia backend and the browser with two-way synchronization # @out variables can only be modified by the backend # @in variables can be modified by both the backend and the browser # variables must be initialized with constant values, or variables defined outside of the @app block ## Interface non Variables @out btnStartDisable=true @out btnPlotDisable=false @out btnSpectraDisable=false # Loading animations @in progress=false @in progressPlot=false @in progressSpectraPlot=false # Text field validations @in triqEnabled=false @in SpectraEnabled=false @in MFilterEnabled=false @in maskEnabled=false # Dialogs @in warning_msg=false @in CompareDialog=false ## Interface Variables @in file_route="" @in file_name="" @in Nmass="0.0" @in Tol=0.1 @in triqProb=0.98 @in colorLevel=20 ## Interface Buttons @in btnSearch=false # To search for files in your device @in btnAddBatch = false @in clear_batch_btn = false @out batch_file_count = 0 @in mainProcess=false # To generate images @in compareBtn=false # To open dialog @in createMeanPlot=false # To generate mean spectrum plot @in createXYPlot=false # To generate an spectrum plot according to the xy values inputed @in createSumPlot=false # To generate a sum of all the spectrum plots @in image3dPlot=false # To generate 3d plot based on current image @in triq3dPlot=false # To generate 3d plot based on current triq image @in imageCPlot=false # To generate contour plots of current image @in triqCPlot=false # To generate contour plots of current triq image # Image change buttons @in imgPlus=false @in imgMinus=false @in imgPlusT=false @in imgMinusT=false # Image change comparative buttons @in imgPlusCompLeft=false @in imgMinusCompLeft=false @in imgPlusTCompLeft=false @in imgMinusTCompLeft=false @in imgPlusCompRight=false @in imgMinusCompRight=false @in imgPlusTCompRight=false @in imgMinusTCompRight=false ## Tabulation variables @out tabIDs=["tab0","tab1","tab2","tab3","tab4"] @out tabLabels=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in selectedTab="tab0" @out CompTabIDsLeft=["tab0","tab1","tab2","tab3","tab4"] @out CompTabLabelsLeft=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in CompSelectedTabLeft="tab0" @out CompTabIDsRight=["tab0","tab1","tab2","tab3","tab4"] @out CompTabLabelsRight=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in CompSelectedTabRight="tab0" # Interface Images @out imgInt="/.bmp" # image Interface @out imgIntT="/.bmp" # image Interface TrIQ @out colorbar="/.png" @out colorbarT="/.png" # Interface controlling for the comparative view @out imgIntCompLeft="/.bmp" @out imgIntTCompLeft="/.bmp" @out colorbarCompLeft="/.png" @out colorbarTCompLeft="/.png" @out imgIntCompRight="/.bmp" @out imgIntTCompRight="/.bmp" @out colorbarCompRight="/.png" @out colorbarTCompRight="/.png" @out imgWidth=0 @out imgHeight=0 # Optical Image Overlay & Transparency @in imgTrans=1.0 @in progressOptical=false @out btnOpticalDisable=true @in btnOptical=false @in btnOpticalT=false @in opticalOverTriq=false @out imgRoute="" # Messages to interface variables @out msg="" @out msgimg="" @out msgtriq="" # Reiteration of the messages under the image to know which spectra is being visualized @out msgimgCompLeft="" @out msgtriqCompLeft="" @out msgimgCompRight="" @out msgtriqCompRight="" # Centralized MSIData object @out msi_data::Union{MSIData, Nothing} = nothing # Metadata table variables @in showMetadataDialog = false @in showMetadataBtn = false @out metadata_columns = [] @out metadata_rows = [] @out btnMetadataDisable = false @in selected_folder_metadata = "" # Saves the route where imzML and mzML files are located @out full_route="" # == Converter Tab Variables == @in left_tab = "generator" @out mzml_full_route = "" @out sync_full_route = "" @in btnSearchMzml = false @in btnSearchSync = false @in convert_process = false @out progress_conversion = false @out msg_conversion = "" @out btnConvertDisable = true # == Pre Processing Variables == @in pre_tab = "stabilization" ## Preprocessing Parameters @in progressPrep=false @in stabilization_method="sqrt" @in smoothing_method="sg" @in smoothing_window = "" @in smoothing_order = "" @in baseline_method="snip" @in baseline_iterations = "" @in baseline_window = "" @in normalization_method="tic" @in alignment_method="lowess" @in alignment_span = "" @in alignment_tolerance = "" @in alignment_tolerance_unit="mz" @in alignment_max_shift_ppm = "" @in alignment_min_matched_peaks = "" @in peak_picking_method="profile" @in peak_picking_snr_threshold = "" @in peak_picking_half_window = "" @in peak_picking_min_peak_prominence = "" @in peak_picking_merge_peaks_tolerance = "" @in peak_picking_min_peak_width_ppm = "" @in peak_picking_max_peak_width_ppm = "" @in peak_picking_min_peak_shape_r2 = "" @in binning_method="adaptive" @in binning_tolerance = "" @in binning_tolerance_unit="ppm" @in binning_frequency_threshold = "" @in binning_min_peak_per_bin = "" @in binning_max_bin_width_ppm = "" @in binning_intensity_weighted_centers=true @in binning_num_uniform_bins = "" @in calibration_fit_order = "" @in calibration_ppm_tolerance = "" @in peak_selection_min_snr = "" @in peak_selection_min_fwhm_ppm = "" @in peak_selection_max_fwhm_ppm = "" @in peak_selection_min_shape_r2 = "" @in peak_selection_frequency_threshold = "" @in peak_selection_correlation_threshold = "" @in reference_peaks_list = [ Dict("mz" => 137.0244, "label" => "DHB_fragment"), Dict("mz" => 155.0349, "label" => "DHB_M+H"), ] # --- Methods for Reference Peaks List --- function addReferencePeak() push!(reference_peaks_list, Dict("mz" => 0.0, "label" => "")) reference_peaks_list = deepcopy(reference_peaks_list) # Force reactivity end function removeReferencePeak(index::Int) deleteat!(reference_peaks_list, index) reference_peaks_list = deepcopy(reference_peaks_list) # Force reactivity end # Individual step enable/disable flags @in enable_stabilization = true @in enable_smoothing = true @in enable_baseline = true @in enable_normalization = true @in enable_standards = true @in enable_alignment = true @in enable_peak_picking = true @in enable_binning = true @in enable_calibration = true @in enable_peak_selection = true # Trigger for running the full pipeline @in run_full_pipeline = false @out current_pipeline_step = "" # To indicate which step is currently running in the full pipeline @in export_params_btn = false @in import_params_btn = false @in imported_params_file = nothing @in suggested_smoothing_window = "" @in suggested_smoothing_order = "" @in suggested_baseline_iterations = "" @in suggested_baseline_window = "" @in suggested_alignment_span = "" @in suggested_alignment_tolerance = "" @in suggested_alignment_max_shift_ppm = "" @in suggested_alignment_min_matched_peaks = "" @in suggested_peak_picking_snr_threshold = "" @in suggested_peak_picking_half_window = "" @in suggested_peak_picking_min_peak_prominence = "" @in suggested_peak_picking_merge_peaks_tolerance = "" @in suggested_peak_picking_min_peak_width_ppm = "" @in suggested_peak_picking_max_peak_width_ppm = "" @in suggested_peak_picking_min_peak_shape_r2 = "" @in suggested_binning_tolerance = "" @in suggested_binning_frequency_threshold = "" @in suggested_binning_min_peak_per_bin = "" @in suggested_binning_max_bin_width_ppm = "" @in suggested_binning_num_uniform_bins = "" @in suggested_calibration_fit_order = "" @in suggested_calibration_ppm_tolerance = "" @in suggested_peak_selection_min_snr = "" @in suggested_peak_selection_min_fwhm_ppm = "" @in suggested_peak_selection_max_fwhm_ppm = "" @in suggested_peak_selection_min_shape_r2 = "" @in suggested_peak_selection_frequency_threshold = "" @in suggested_peak_selection_correlation_threshold = "" # == Batch Summary Dialog == @in showBatchSummary = false @out batch_summary = "" # == Batch Processing & Registry Variables == @private registry_init_done = false @in refetch_folders = false @in selected_files = String[] @in available_folders = String[] @in image_available_folders = String[] @out registry_path = abspath(joinpath(@__DIR__, "public", "registry.json")) # Progress reporting @out overall_progress = 0.0 @out progress_message = "" # == Folder-based UI State == @in selected_folder_main = "" @in selected_folder_compare_left = "" @in selected_folder_compare_right = "" # For the creation of images with a more specific mass charge @out text_nmass="" # For image search image lists we apply a filter that searches specific type of images into our public folder, then we sort it in a "numerical" order @in msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")),lt=natural) @in col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir("public")),lt=natural) @in triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")),lt=natural) @in col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir("public")),lt=natural) # Set current image for the list to display @out current_msi="" @out current_col_msi="" @out current_triq="" @out current_col_triq="" # We reiterate the process to display in the comparative view @out current_msiCompLeft="" @out current_col_msiCompLeft="" @out current_triqCompLeft="" @out current_col_triqCompLeft="" @out current_msiCompRight="" @out current_col_msiCompRight="" @out current_triqCompRight="" @out current_col_triqCompRight="" ## Time measurement variables @out sTime=time() @out fTime=time() @out eTime=time() ## Plots # Local image to plot layoutImg=PlotlyBase.Layout( title=PlotlyBase.attr( text="", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=14, color="black" ) ), xaxis=PlotlyBase.attr( visible=false, scaleanchor="y", range=[0, 0] ), yaxis=PlotlyBase.attr( visible=false, range=[0, 0] ), margin=attr(l=0,r=0,t=0,b=0,pad=0) ) traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) @out plotdataImg=[traceImg] @out plotlayoutImg=layoutImg # For the image in the comparative view @out plotdataImgCompLeft=[traceImg] @out plotlayoutImgCompLeft=layoutImg @out plotdataImgCompRight=[traceImg] @out plotlayoutImgCompRight=layoutImg # For triq image @out plotdataImgT=[traceImg] @out plotlayoutImgT=layoutImg # For the triq image in the comparative view @out plotdataImgTCompLeft=[traceImg] @out plotlayoutImgTCompLeft=layoutImg @out plotdataImgTCompRight=[traceImg] @out plotlayoutImgTCompRight=layoutImg # Interface Plot Spectrum layoutSpectra=PlotlyBase.Layout( title=PlotlyBase.attr( text="Spectrum plot", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, color="black" ) ), hovermode="closest", xaxis=PlotlyBase.attr( title="m/z", showgrid=true ), yaxis=PlotlyBase.attr( title="Intensity", showgrid=true, tickformat = ".3g" ), margin=attr(l=0,r=0,t=120,b=0,pad=0) ) # Dummy 2D scatter plot traceSpectra=PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines", marker=attr(size=1, color="blue", opacity=0.1)) # Create conection to frontend @out plotdata=[traceSpectra] @out plotlayout=layoutSpectra @in xCoord=0 @in yCoord=0 @out xSpectraMz = Vector{Float64}() @out ySpectraMz = Vector{Float64}() # UI plot data for Preprocessing @out plotdata_before = [traceSpectra] @out plotlayout_before = layoutSpectra @out plotdata_after = [traceSpectra] @out plotlayout_after = layoutSpectra # Interactive plot reactions @in data_click=Dict{String,Any}() #@in data_selected=Dict{String,Any}() # Selected is for areas, this can work for the masks # # Interface Plot Surface layoutContour=PlotlyBase.Layout( title=PlotlyBase.attr( text="2D Topographic map", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, color="black" ) ), xaxis=PlotlyBase.attr( visible=false, scaleanchor="y" ), yaxis=PlotlyBase.attr( visible=false ), margin=attr(l=0,r=0,t=100,b=0,pad=0) ) # Dummy 2D surface plot traceContour=PlotlyBase.contour(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines") # Create conection to frontend @out plotdataC=[traceContour] @out plotlayoutC=layoutContour # Interface Plot 3d # Define the layout for the 3D plot layout3D=PlotlyBase.Layout( title=PlotlyBase.attr( text="3D Surface plot", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, color="black" ) ), scene=attr( xaxis_title="X", yaxis_title="Y", zaxis_title="Z", xaxis_nticks=20, yaxis_nticks=20, zaxis_nticks=4, camera=attr(eye=attr(x=0, y=-1, z=0.5)), aspectratio=attr(x=1, y=1, z=0.2) ), margin=attr(l=0,r=0,t=120,b=0,pad=0) ) # Dummy 3D surface plot x=1:10 y=1:10 z=[sin(i * j / 10) for i in x, j in y] trace3D=PlotlyBase.surface(x=Vector{Float64}(), y=Vector{Float64}(), z=Matrix{Float64}(undef, 0, 0), contours_z=attr( show=true, usecolormap=true, highlightcolor="limegreen", project_z=true ), colorscale="Viridis") # Create conection to frontend @out plotdata3d=[trace3D] @out plotlayout3d=layout3D # == Reactive handlers == # Reactive handlers watch a variable and execute a block of code when its value changes # The onbutton handler will set the variable to false after the block is executed # This handler correctly uses pick_file and loads the selected file # as the active dataset for the UI. @onbutton btnSearch begin picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml") if isempty(picked_route) return end progress = true msg = "Opening file: $(basename(picked_route))..." try dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML|mzml))$ "i => "") registry = load_registry(registry_path) existing_entry = get(registry, dataset_name, nothing) # --- Fast Load Path --- is_same_file = (existing_entry !== nothing && existing_entry["source_path"] == picked_route) if is_same_file && !isempty(get(existing_entry, "metadata", Dict())) msg = "Fast loading pre-processed file: $(dataset_name)" println(msg) full_route = existing_entry["source_path"] metadata_rows = existing_entry["metadata"]["summary"] dims_str = first(filter(r -> r["parameter"] == "Image Dimensions", metadata_rows))["value"] dims = parse.(Int, split(dims_str, " x ")) imgWidth, imgHeight = dims[1], dims[2] msi_data = nothing # Ensure data is not held in memory log_memory_usage("Fast Load (msi_data cleared)", msi_data) btnMetadataDisable = false btnStartDisable = false btnPlotDisable = false btnSpectraDisable = false SpectraEnabled = true selected_folder_main = dataset_name # Update folder lists in UI all_folders = sort(collect(keys(registry)), lt=natural) img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) available_folders = deepcopy(all_folders) image_available_folders = deepcopy(img_folders) msg = "Successfully loaded pre-processed dataset: $(dataset_name)" progress = false return end # --- Full Load Path --- msg = "Performing first-time analysis for: $(basename(picked_route))..." local local_full_route if endswith(picked_route, r"imzml"i) local_full_route = replace(picked_route, r"\.imzml$"i => ".imzML") if picked_route != local_full_route mv(picked_route, local_full_route, force=true) end else local_full_route = picked_route end full_route = local_full_route sTime = time() loaded_data = OpenMSIData(local_full_route) is_imzML = loaded_data.source isa ImzMLSource precompute_analytics(loaded_data) # Auto-suggest parameters try println("Calling main_precalculation to get recommended parameters...") recommended_params = main_precalculation(loaded_data) for (step_name, params) in recommended_params for (param_key, value) in params # Convert value to appropriate type before assignment processed_value = if value === nothing nothing elseif value isa Tuple @warn "Skipping invalid parameter suggestion (tuple): $value for $param_key" "" # Set to empty string for safety elseif value isa Number value else string(value) end if processed_value !== nothing if step_name == :Smoothing if param_key == :window suggested_smoothing_window = string(processed_value) smoothing_window = string(processed_value) println(" suggested_smoothing_window set to $(suggested_smoothing_window)") elseif param_key == :order suggested_smoothing_order = string(processed_value) smoothing_order = string(processed_value) println(" suggested_smoothing_order set to $(suggested_smoothing_order)") end elseif step_name == :BaselineCorrection if param_key == :iterations suggested_baseline_iterations = string(processed_value) baseline_iterations = string(processed_value) println(" suggested_baseline_iterations set to $(suggested_baseline_iterations)") elseif param_key == :window suggested_baseline_window = string(processed_value) baseline_window = string(processed_value) println(" suggested_baseline_window set to $(suggested_baseline_window)") end elseif step_name == :PeakAlignment if param_key == :span suggested_alignment_span = string(processed_value) alignment_span = string(processed_value) println(" suggested_alignment_span set to $(suggested_alignment_span)") elseif param_key == :tolerance suggested_alignment_tolerance = string(processed_value) alignment_tolerance = string(processed_value) println(" suggested_alignment_tolerance set to $(suggested_alignment_tolerance)") elseif param_key == :max_shift_ppm suggested_alignment_max_shift_ppm = string(processed_value) alignment_max_shift_ppm = string(processed_value) println(" suggested_alignment_max_shift_ppm set to $(suggested_alignment_max_shift_ppm)") elseif param_key == :min_matched_peaks suggested_alignment_min_matched_peaks = string(processed_value) alignment_min_matched_peaks = string(processed_value) println(" suggested_alignment_min_matched_peaks set to $(suggested_alignment_min_matched_peaks)") end elseif step_name == :Calibration if param_key == :fit_order suggested_calibration_fit_order = string(processed_value) calibration_fit_order = string(processed_value) println(" suggested_calibration_fit_order set to $(suggested_calibration_fit_order)") elseif param_key == :ppm_tolerance suggested_calibration_ppm_tolerance = string(processed_value) calibration_ppm_tolerance = string(processed_value) println(" suggested_calibration_ppm_tolerance set to $(suggested_calibration_ppm_tolerance)") end elseif step_name == :PeakPicking if param_key == :snr_threshold suggested_peak_picking_snr_threshold = string(processed_value) peak_picking_snr_threshold = string(processed_value) println(" suggested_peak_picking_snr_threshold set to $(suggested_peak_picking_snr_threshold)") elseif param_key == :half_window suggested_peak_picking_half_window = string(processed_value) peak_picking_half_window = string(processed_value) println(" suggested_peak_picking_half_window set to $(suggested_peak_picking_half_window)") elseif param_key == :min_peak_prominence suggested_peak_picking_min_peak_prominence = string(processed_value) peak_picking_min_peak_prominence = string(processed_value) println(" suggested_peak_picking_min_peak_prominence set to $(suggested_peak_picking_min_peak_prominence)") elseif param_key == :merge_peaks_tolerance suggested_peak_picking_merge_peaks_tolerance = string(processed_value) peak_picking_merge_peaks_tolerance = string(processed_value) println(" suggested_peak_picking_merge_peaks_tolerance set to $(suggested_peak_picking_merge_peaks_tolerance)") elseif param_key == :min_peak_width_ppm suggested_peak_picking_min_peak_width_ppm = string(processed_value) peak_picking_min_peak_width_ppm = string(processed_value) println(" suggested_peak_picking_min_peak_width_ppm set to $(suggested_peak_picking_min_peak_width_ppm)") elseif param_key == :max_peak_width_ppm suggested_peak_picking_max_peak_width_ppm = string(processed_value) peak_picking_max_peak_width_ppm = string(processed_value) println(" suggested_peak_picking_max_peak_width_ppm set to $(suggested_peak_picking_max_peak_width_ppm)") elseif param_key == :min_peak_shape_r2 suggested_peak_picking_min_peak_shape_r2 = string(processed_value) peak_picking_min_peak_shape_r2 = string(processed_value) println(" suggested_peak_picking_min_peak_shape_r2 set to $(suggested_peak_picking_min_peak_shape_r2)") end elseif step_name == :PeakSelection if param_key == :min_snr suggested_peak_selection_min_snr = string(processed_value) peak_selection_min_snr = string(processed_value) println(" suggested_peak_selection_min_snr set to $(suggested_peak_selection_min_snr)") elseif param_key == :min_fwhm_ppm suggested_peak_selection_min_fwhm_ppm = string(processed_value) peak_selection_min_fwhm_ppm = string(processed_value) println(" suggested_peak_selection_min_fwhm_ppm set to $(suggested_peak_selection_min_fwhm_ppm)") elseif param_key == :max_fwhm_ppm suggested_peak_selection_max_fwhm_ppm = string(processed_value) peak_selection_max_fwhm_ppm = string(processed_value) println(" suggested_peak_selection_max_fwhm_ppm set to $(suggested_peak_selection_max_fwhm_ppm)") elseif param_key == :min_shape_r2 suggested_peak_selection_min_shape_r2 = string(processed_value) peak_selection_min_shape_r2 = string(processed_value) println(" suggested_peak_selection_min_shape_r2 set to $(suggested_peak_selection_min_shape_r2)") elseif param_key == :frequency_threshold suggested_peak_selection_frequency_threshold = string(processed_value) peak_selection_frequency_threshold = string(processed_value) println(" suggested_peak_selection_frequency_threshold set to $(suggested_peak_selection_frequency_threshold)") elseif param_key == :correlation_threshold suggested_peak_selection_correlation_threshold = string(processed_value) peak_selection_correlation_threshold = string(processed_value) println(" suggested_peak_selection_correlation_threshold set to $(suggested_peak_selection_correlation_threshold)") end elseif step_name == :PeakBinning if param_key == :tolerance suggested_binning_tolerance = string(processed_value) binning_tolerance = string(processed_value) println(" suggested_binning_tolerance set to $(suggested_binning_tolerance)") elseif param_key == :frequency_threshold suggested_binning_frequency_threshold = string(processed_value) binning_frequency_threshold = string(processed_value) println(" suggested_binning_frequency_threshold set to $(suggested_binning_frequency_threshold)") elseif param_key == :min_peak_per_bin suggested_binning_min_peak_per_bin = string(processed_value) binning_min_peak_per_bin = string(processed_value) println(" suggested_binning_min_peak_per_bin set to $(suggested_binning_min_peak_per_bin)") elseif param_key == :max_bin_width_ppm suggested_binning_max_bin_width_ppm = string(processed_value) binning_max_bin_width_ppm = string(processed_value) println(" suggested_binning_max_bin_width_ppm set to $(suggested_binning_max_bin_width_ppm)") elseif param_key == :num_uniform_bins suggested_binning_num_uniform_bins = string(processed_value) binning_num_uniform_bins = string(processed_value) println(" suggested_binning_num_uniform_bins set to $(suggested_binning_num_uniform_bins)") end end end end end # Also set method types for steps if haskey(recommended_params, :Smoothing) && haskey(recommended_params[:Smoothing], :method) smoothing_method = string(recommended_params[:Smoothing][:method]) end if haskey(recommended_params, :BaselineCorrection) && haskey(recommended_params[:BaselineCorrection], :method) baseline_method = string(recommended_params[:BaselineCorrection][:method]) end if haskey(recommended_params, :Normalization) && haskey(recommended_params[:Normalization], :method) normalization_method = string(recommended_params[:Normalization][:method]) end if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :method) alignment_method = string(recommended_params[:PeakAlignment][:method]) end if haskey(recommended_params, :PeakPicking) && haskey(recommended_params[:PeakPicking], :method) peak_picking_method = string(recommended_params[:PeakPicking][:method]) end if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :method) binning_method = string(recommended_params[:PeakBinning][:method]) end if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :tolerance_unit) alignment_tolerance_unit = string(recommended_params[:PeakAlignment][:tolerance_unit]) end if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :tolerance_unit) binning_tolerance_unit = string(recommended_params[:PeakBinning][:tolerance_unit]) end msg = "File loaded and parameters suggested." catch e @warn "Could not suggest parameters. Using defaults. Error: $e" end metadata_columns = [ Dict("name" => "parameter", "label" => "Parameter", "field" => "parameter", "align" => "left"), Dict("name" => "value", "label" => "Value", "field" => "value", "align" => "left"), ] summary_stats = extract_metadata(loaded_data, local_full_route) metadata_rows = summary_stats["summary"] btnMetadataDisable = isempty(metadata_rows) w, h = loaded_data.image_dims imgWidth, imgHeight = w > 0 ? (w, h) : (500, 500) update_registry(registry_path, dataset_name, local_full_route, summary_stats, is_imzML) # Update folder lists in UI registry = load_registry(registry_path) all_folders = sort(collect(keys(registry)), lt=natural) img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) available_folders = deepcopy(all_folders) image_available_folders = deepcopy(img_folders) selected_folder_main = dataset_name msi_data = loaded_data log_memory_usage("Full Load", msi_data) eTime = round(time() - sTime, digits=3) msg = "Active file loaded in $(eTime) seconds. Dataset '$(dataset_name)' is ready for analysis." btnStartDisable = false btnPlotDisable = false btnSpectraDisable = false SpectraEnabled = true catch e msi_data = nothing msg = "Error loading active file: $e" warning_msg = true btnStartDisable = true btnSpectraDisable = true SpectraEnabled = false btnMetadataDisable = true @error "File loading failed" exception=(e, catch_backtrace()) finally GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end progress = false progressSpectraPlot = false end end @onbutton export_params_btn begin params_to_export = Dict( "stabilization_method" => stabilization_method, "smoothing_method" => smoothing_method, "smoothing_window" => smoothing_window, "smoothing_order" => smoothing_order, "baseline_method" => baseline_method, "baseline_iterations" => baseline_iterations, "baseline_window" => baseline_window, "normalization_method" => normalization_method, "alignment_method" => alignment_method, "alignment_span" => alignment_span, "alignment_tolerance" => alignment_tolerance, "alignment_tolerance_unit" => alignment_tolerance_unit, "alignment_max_shift_ppm" => alignment_max_shift_ppm, "alignment_min_matched_peaks" => alignment_min_matched_peaks, "peak_picking_method" => peak_picking_method, "peak_picking_snr_threshold" => peak_picking_snr_threshold, "peak_picking_half_window" => peak_picking_half_window, "peak_picking_min_peak_prominence" => peak_picking_min_peak_prominence, "peak_picking_merge_peaks_tolerance" => peak_picking_merge_peaks_tolerance, "peak_picking_min_peak_width_ppm" => peak_picking_min_peak_width_ppm, "peak_picking_max_peak_width_ppm" => peak_picking_max_peak_width_ppm, "peak_picking_min_peak_shape_r2" => peak_picking_min_peak_shape_r2, "binning_method" => binning_method, "binning_tolerance" => binning_tolerance, "binning_tolerance_unit" => binning_tolerance_unit, "binning_frequency_threshold" => binning_frequency_threshold, "binning_min_peak_per_bin" => binning_min_peak_per_bin, "binning_max_bin_width_ppm" => binning_max_bin_width_ppm, "binning_intensity_weighted_centers" => binning_intensity_weighted_centers, "binning_num_uniform_bins" => binning_num_uniform_bins, "calibration_fit_order" => calibration_fit_order, "calibration_ppm_tolerance" => calibration_ppm_tolerance, "peak_selection_min_snr" => peak_selection_min_snr, "peak_selection_min_fwhm_ppm" => peak_selection_min_fwhm_ppm, "peak_selection_max_fwhm_ppm" => peak_selection_max_fwhm_ppm, "peak_selection_min_shape_r2" => peak_selection_min_shape_r2, "peak_selection_frequency_threshold" => peak_selection_frequency_threshold, "peak_selection_correlation_threshold" => peak_selection_correlation_threshold, "reference_peaks_list" => reference_peaks_list ) json_string = JSON.json(params_to_export) js_script = """ var element = document.createElement('a'); element.setAttribute('href', 'data:text/json;charset=utf-8,' + encodeURIComponent(`$json_string`)); element.setAttribute('download', 'preprocessing_params.json'); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); """ run_js(js_script) msg = "Parameters exported." end @onchange import_params_btn begin if import_params_btn try json_string = String(imported_params_file.data) params = JSON.parse(json_string) for (key, value) in params if key == "reference_peaks_list" reference_peaks_list = value else # Use getfield and setproperty! to update reactive variables by name if hasfield(typeof(@__MODULE__), Symbol(key)) getfield(@__MODULE__, Symbol(key))[] = value end end end msg = "Parameters imported successfully." catch e msg = "Failed to import parameters: $e" warning_msg = true finally import_params_btn = false # Reset button state end end end @onbutton run_full_pipeline begin progressPrep = true msg = "Running preprocessing pipeline..." try # 1. Get data from "before" plot if isempty(plotdata_before.traces) || isempty(plotdata_before.traces[1].x) msg = "Please generate a spectrum plot first (e.g., Mean, Sum, or X,Y)." warning_msg = true return end mz_data = plotdata_before.traces[1].x intensity_data = plotdata_before.traces[1].y # 2. Create a temporary MutableSpectrum temp_spectrum = MutableSpectrum(mz_data, intensity_data, [], 1) # id=1 is arbitrary # 3. Run the pipeline pipeline_spectra = [temp_spectrum] if enable_stabilization current_pipeline_step = "Stabilizing..." params = Dict(:method => Symbol(stabilization_method)) apply_intensity_transformation(pipeline_spectra, params) end if enable_smoothing current_pipeline_step = "Smoothing..." params = Dict(:method => Symbol(smoothing_method), :window => parse(Int, smoothing_window), :order => parse(Int, smoothing_order)) apply_smoothing(pipeline_spectra, params) end if enable_baseline current_pipeline_step = "Correcting baseline..." params = Dict(:method => Symbol(baseline_method), :iterations => parse(Int, baseline_iterations), :window => parse(Int, baseline_window)) apply_baseline_correction(pipeline_spectra, params) end if enable_normalization current_pipeline_step = "Normalizing..." params = Dict(:method => Symbol(normalization_method)) apply_normalization(pipeline_spectra, params) end if enable_standards && enable_calibration current_pipeline_step = "Calibrating..." ref_peaks = Dict(p["mz"] => p["label"] for p in reference_peaks_list) params = Dict(:ppm_tolerance => parse(Float64, calibration_ppm_tolerance), :fit_order => parse(Int, calibration_fit_order)) apply_calibration(pipeline_spectra, params) end if enable_alignment # Alignment is a no-op for a single spectrum, but we call it for completeness current_pipeline_step = "Aligning (skipped for single spectrum)..." end if enable_peak_picking current_pipeline_step = "Picking peaks..." params = Dict( :method => Symbol(peak_picking_method), :snr_threshold => parse(Float64, peak_picking_snr_threshold), :half_window => parse(Int, peak_picking_half_window), :min_peak_prominence => parse(Float64, peak_picking_min_peak_prominence), :merge_peaks_tolerance => parse(Float64, peak_picking_merge_peaks_tolerance) ) apply_peak_picking(pipeline_spectra, params) end # 4. Update "After" plot processed_spectrum = pipeline_spectra[1] # Main spectrum trace after_trace = PlotlyBase.scatter(x=processed_spectrum.mz, y=processed_spectrum.intensity, mode="lines", name="Processed Spectrum") traces_after = [after_trace] # Add peaks if they exist if !isempty(processed_spectrum.peaks) peak_mzs = [p.mz for p in processed_spectrum.peaks] peak_intensities = [p.intensity for p in processed_spectrum.peaks] peak_trace = PlotlyBase.scatter(x=peak_mzs, y=peak_intensities, mode="markers", name="Picked Peaks", marker=attr(color="red", size=8)) push!(traces_after, peak_trace) end plotdata_after = traces_after plotlayout_after = PlotlyBase.Layout(title="After Preprocessing") msg = "Pipeline finished." catch e msg = "Error during pipeline execution: $e" warning_msg = true @error "Pipeline failed" exception=(e, catch_backtrace()) finally progressPrep = false current_pipeline_step = "" end end # This new handler correctly adds the file from full_route to the batch list. @onbutton btnAddBatch begin if isempty(full_route) || full_route == "unknown (manually added)" msg = "No active file selected to add to batch." warning_msg = true return end if !(full_route in selected_files) push!(selected_files, full_route) selected_files = deepcopy(selected_files) # Force reactivity batch_file_count = length(selected_files) msg = "File added to batch." else msg = "File is already in the batch list." warning_msg = true end end @onbutton clear_batch_btn begin selected_files = String[] batch_file_count = 0 msg = "Batch cleared" end @onchange selected_files begin batch_file_count = length(selected_files) end @onchange full_route begin if !isempty(full_route) && !(full_route in selected_files) push!(selected_files, full_route) selected_files = deepcopy(selected_files) # Force reactivity batch_file_count = length(selected_files) msg = "File automatically added to batch" end end @onbutton showMetadataBtn begin if !isempty(available_folders) if !isempty(selected_folder_main) selected_folder_metadata = selected_folder_main elseif !isempty(available_folders) selected_folder_metadata = first(available_folders) end showMetadataDialog = true else msg = "No processed datasets available." warning_msg = true end end @onchange selected_folder_metadata begin if !isempty(selected_folder_metadata) registry = load_registry(registry_path) dataset_info = get(registry, selected_folder_metadata, nothing) if dataset_info !== nothing && haskey(dataset_info, "metadata") && !isempty(get(dataset_info["metadata"], "summary", [])) metadata_rows = dataset_info["metadata"]["summary"] btnMetadataDisable = false else metadata_rows = [] btnMetadataDisable = true msg = "Metadata not found in registry for $(selected_folder_metadata)." end end end @onbutton mainProcess @time begin # --- UI State Update --- progress = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true overall_progress = 0.0 progress_message = "Preparing batch process..." # --- CAPTURE CURRENT VALUES HERE --- current_selected_files = selected_files current_nmass = Nmass current_tol = Tol current_color_level = colorLevel current_triq_enabled = triqEnabled current_triq_prob = triqProb current_mfilter_enabled = MFilterEnabled current_mask_enabled = maskEnabled current_registry_path = registry_path println("starting main process with $(length(current_selected_files)) files") total_time_start = time() try # --- 1. Parameter Validation --- if isempty(current_selected_files) progress_message = "No .imzML files in batch. Please add files first." warning_msg = true println(progress_message) return end masses = Float64[] try masses = [parse(Float64, strip(m)) for m in split(current_nmass, ',', keepempty=false)] catch e progress_message = "Invalid m/z value(s). Please provide a comma-separated list of numbers. Error: $e" warning_msg = true return end if isempty(masses) progress_message = "No valid m/z values found. Please provide comma-separated positive numbers." warning_msg = true return end # --- 2. Batch Processing Loop --- num_files = length(current_selected_files) total_steps = num_files current_step = 0 errors = Dict("load_errors" => String[], "slice_errors" => String[], "io_errors" => String[]) newly_created_folders = String[] files_without_mask = 0 for (file_idx, file_path) in enumerate(current_selected_files) progress_message = "Processing file $(file_idx)/$(num_files): $(basename(file_path))" overall_progress = current_step / total_steps all_params = ( tolerance = current_tol, colorL = current_color_level, triqE = current_triq_enabled, triqP = current_triq_prob, medianF = current_mfilter_enabled, registry = current_registry_path, fileIdx = file_idx, nFiles = num_files ) success, error_msg = process_file_safely(file_path, masses, all_params, progress_message, overall_progress, use_mask=current_mask_enabled) if !success push!(errors["load_errors"], error_msg) else push!(newly_created_folders, replace(basename(file_path), r"\.imzML$"i => "")) end current_step += 1 end # --- 3. Final Report --- total_time_end = round(time() - total_time_start, digits=3) registry = load_registry(current_registry_path) all_folders = sort(collect(keys(registry)), lt=natural) img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) available_folders = deepcopy(all_folders) image_available_folders = deepcopy(img_folders) if !isempty(newly_created_folders) selected_folder_main = first(newly_created_folders) end successful_files = length(newly_created_folders) total_errors = sum(length, values(errors)) if total_errors == 0 msg = "Successfully processed all $(successful_files) file(s) in $(total_time_end) seconds." else msg = "Batch completed in $(total_time_end) seconds with $(total_errors) error(s)." warning_msg = true end mask_summary = current_mask_enabled ? "\nFiles processed without a mask: $(files_without_mask)" : "" batch_summary = """ Processed $(successful_files)/$(num_files) files successfully. $(mask_summary) Errors by category: • Load failures: $(length(errors["load_errors"])) • Slice generation: $(length(errors["slice_errors"])) • I/O issues: $(length(errors["io_errors"])) Detailed errors: $(join(vcat(values(errors)...), "\n")) """ showBatchSummary = true # Update UI to display the last generated image if !isempty(newly_created_folders) timestamp = string(time_ns()) folder_path = joinpath("public", selected_folder_main) if current_triq_enabled triq_files = filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)) col_triq_files = filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)) if !isempty(triq_files) latest_triq = triq_files[argmax([mtime(joinpath(folder_path, f)) for f in triq_files])] current_triq = latest_triq imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)" plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_files) latest_col_triq = col_triq_files[argmax([mtime(joinpath(folder_path, f)) for f in col_triq_files])] current_col_triq = latest_col_triq colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)" else colorbarT = "" end selectedTab = "tab1" end else # Not TrIQ enabled, display regular MSI image msi_files = filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)) col_msi_files = filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)) if !isempty(msi_files) latest_msi = msi_files[argmax([mtime(joinpath(folder_path, f)) for f in msi_files])] current_msi = latest_msi imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)" plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt) text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_files) latest_col_msi = col_msi_files[argmax([mtime(joinpath(folder_path, f)) for f in col_msi_files])] current_col_msi = latest_col_msi colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)" else colorbar = "" end selectedTab = "tab0" end end end catch e println("Error in main process: $e") msg = "Batch processing failed: $e" warning_msg = true @error "Main process failed" exception=(e, catch_backtrace()) finally # --- UI State Reset --- progress = false btnStartDisable = false btnPlotDisable = false btnOpticalDisable = false btnSpectraDisable = false SpectraEnabled = true overall_progress = 0.0 println("Done") GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end end end @onbutton createMeanPlot @time begin if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true msg = "Loading plot for $(selected_folder_main)..." try sTime = time() registry = load_registry(registry_path) entry = registry[selected_folder_main] target_path = entry["source_path"] if target_path == "unknown (manually added)" msg = "Dataset selected contained no route." warning_msg = true return end if msi_data === nothing || full_route != target_path if msi_data !== nothing close(msi_data) end msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing raw_min = entry["metadata"]["global_min_mz"] raw_max = entry["metadata"]["global_max_mz"] min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) else precompute_analytics(msi_data) end end local mask_path_for_plot::Union{String, Nothing} = nothing if maskEnabled && get(entry, "has_mask", false) mask_path_for_plot = get(entry, "mask_path", "") if !isfile(mask_path_for_plot) @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." mask_path_for_plot = nothing end end plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data, selected_folder_main, mask_path=mask_path_for_plot) plotdata_before = plotdata plotlayout_before = plotlayout selectedTab = "tab2" fTime = time() eTime = round(fTime - sTime, digits=3) msg = "Plot loaded in $(eTime) seconds" log_memory_usage("Mean Plot Generated", msi_data) catch e msg = "Could not generate mean spectrum plot: $e" warning_msg = true @error "Mean spectrum plotting failed" exception=(e, catch_backtrace()) finally progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false btnStartDisable = false GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end end end @onbutton createSumPlot @time begin if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true msg = "Loading total spectrum plot for $(selected_folder_main)..." try sTime = time() registry = load_registry(registry_path) entry = registry[selected_folder_main] target_path = entry["source_path"] if target_path == "unknown (manually added)" msg = "Dataset selected contained no route." warning_msg = true return end if msi_data === nothing || full_route != target_path if msi_data !== nothing close(msi_data) end msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing raw_min = entry["metadata"]["global_min_mz"] raw_max = entry["metadata"]["global_max_mz"] min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) else precompute_analytics(msi_data) end end local mask_path_for_plot::Union{String, Nothing} = nothing if maskEnabled && get(entry, "has_mask", false) mask_path_for_plot = get(entry, "mask_path", "") if !isfile(mask_path_for_plot) @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." mask_path_for_plot = nothing end end plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data, selected_folder_main, mask_path=mask_path_for_plot) plotdata_before = plotdata plotlayout_before = plotlayout selectedTab = "tab2" fTime = time() eTime = round(fTime - sTime, digits=3) msg = "Total plot loaded in $(eTime) seconds" log_memory_usage("Sum Plot Generated", msi_data) catch e msg = "Could not generate total spectrum plot: $e" warning_msg = true @error "Total spectrum plotting failed" exception=(e, catch_backtrace()) finally progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false btnStartDisable = false GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end end end @onbutton createXYPlot @time begin if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end progressSpectraPlot = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true msg = "Loading plot for $(selected_folder_main)..." try sTime = time() registry = load_registry(registry_path) if !haskey(registry, selected_folder_main) msg = "Dataset '$(selected_folder_main)' not found in registry." warning_msg = true return end entry = registry[selected_folder_main] target_path = entry["source_path"] if target_path == "unknown (manually added)" msg = "Dataset selected contained no route." warning_msg = true return end if msi_data === nothing || full_route != target_path if msi_data !== nothing close(msi_data) end msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing raw_min = entry["metadata"]["global_min_mz"] raw_max = entry["metadata"]["global_max_mz"] min_val = isa(raw_min, Dict) ? get(raw_min, "value", raw_min) : raw_min max_val = isa(raw_max, Dict) ? get(raw_max, "value", raw_max) : raw_max set_global_mz_range!(msi_data, convert(Float64, min_val), convert(Float64, max_val)) else precompute_analytics(msi_data) end end local mask_path_for_plot::Union{String, Nothing} = nothing if maskEnabled && get(entry, "has_mask", false) mask_path_for_plot = get(entry, "mask_path", "") if !isfile(mask_path_for_plot) @warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask." mask_path_for_plot = nothing end end y_positive = yCoord < 0 ? abs(yCoord) : yCoord plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y_positive, imgWidth, imgHeight, selected_folder_main, mask_path=mask_path_for_plot) plotdata_before = plotdata plotlayout_before = plotlayout actual_title = if plotlayout.title isa Dict && haskey(plotlayout.title, :text) plotlayout.title[:text] elseif plotlayout.title isa Dict && haskey(plotlayout.title, "text") plotlayout.title["text"] else string(plotlayout.title) # Fallback end if occursin("Masked Spectrum at", actual_title) # Extract coordinates from masked spectrum title coords_match = match(r"Masked Spectrum at \((\d+), (\d+)\)", actual_title) if coords_match !== nothing xCoord = parse(Int, coords_match.captures[1]) yCoord = -parse(Int, coords_match.captures[2]) # Negative for display end elseif occursin("Spectrum at", actual_title) # Extract coordinates from regular spectrum title coords_match = match(r"Spectrum at \((\d+), (\d+)\)", actual_title) if coords_match !== nothing xCoord = parse(Int, coords_match.captures[1]) yCoord = -parse(Int, coords_match.captures[2]) # Negative for display end else # For non-imaging data or fallback, just clamp the coordinates xCoord = clamp(xCoord, 1, imgWidth) yCoord = yCoord < 0 ? yCoord : -clamp(yCoord, 1, imgHeight) end selectedTab = "tab2" fTime = time() eTime = round(fTime - sTime, digits=3) msg = "Plot loaded in $(eTime) seconds" log_memory_usage("XY Plot Generated", msi_data) catch e msg = "Could not retrieve spectrum: $e" warning_msg = true @error "Spectrum plotting failed" exception=(e, catch_backtrace()) finally progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false btnStartDisable = false GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS end end end GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure julia returns the freed memory to OS end end # == Pages == # Register a new route and the page that will be loaded on access @page("/", "app.jl.html") end