3784 lines
177 KiB
Julia
3784 lines
177 KiB
Julia
# 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="<i>m/z</i>",
|
|
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 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
|
|
|
|
@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}(p["mz"] => p["label"] for p in reference_peaks_list)
|
|
|
|
final_params = Dict{Symbol, Dict{Symbol, Any}}()
|
|
validation_errors = String[]
|
|
|
|
# --- Stabilization ---
|
|
println("DEBUG: Checking Stabilization step (name: stabilization)")
|
|
if is_step_enabled("stabilization", pipeline_step_order)
|
|
println("DEBUG: Stabilization step is ENABLED. Setting method: $(stabilization_method).")
|
|
final_params[:Stabilization] = Dict{Symbol, Any}(:method => Symbol(stabilization_method))
|
|
else
|
|
println("DEBUG: Stabilization step is DISABLED. Skipping.")
|
|
end
|
|
|
|
# --- 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="<i>m/z</i>",
|
|
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="<b>m/z</b>: %{x:.4f}<extra></extra>"
|
|
)
|
|
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="<b>m/z</b>: %{x:.4f}<extra></extra>"
|
|
)
|
|
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="<i>m/z</i>",
|
|
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="<b>m/z</b>: %{x:.4f}<extra></extra>"
|
|
)
|
|
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="<b>m/z</b>: %{x:.4f}<extra></extra>"
|
|
)
|
|
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="<i>m/z</i>",
|
|
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 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(p["mz"] => p["label"] for p in reference_peaks_list)
|
|
recommended_params = main_precalculation(msi_data, reference_peaks=ref_peaks)
|
|
|
|
|
|
for (step_name, params) in recommended_params
|
|
for (param_key, value) in params
|
|
# Convert value to appropriate type before assignment
|
|
processed_value = if value === nothing
|
|
""
|
|
elseif value isa Tuple
|
|
@warn "Skipping invalid parameter suggestion (tuple): $value for $param_key"
|
|
"" # Set to empty string for safety
|
|
elseif value isa Number
|
|
string(value)
|
|
else
|
|
string(value)
|
|
end
|
|
|
|
if isempty(processed_value) && !(processed_value isa Number)
|
|
continue # Skip if processed_value is an empty string and not a number type
|
|
end
|
|
|
|
# Map recommended parameters to suggested_* reactive variables
|
|
if step_name == :Smoothing
|
|
if param_key == :window
|
|
suggested_smoothing_window = processed_value
|
|
smoothing_window = processed_value
|
|
elseif param_key == :order
|
|
suggested_smoothing_order = processed_value
|
|
smoothing_order = processed_value
|
|
end
|
|
elseif step_name == :BaselineCorrection
|
|
if param_key == :iterations
|
|
suggested_baseline_iterations = processed_value
|
|
baseline_iterations = processed_value
|
|
elseif param_key == :window
|
|
suggested_baseline_window = processed_value
|
|
baseline_window = processed_value
|
|
end
|
|
elseif step_name == :PeakAlignment
|
|
if param_key == :span
|
|
suggested_alignment_span = processed_value
|
|
alignment_span = processed_value
|
|
elseif param_key == :tolerance
|
|
suggested_alignment_tolerance = processed_value
|
|
alignment_tolerance = processed_value
|
|
elseif param_key == :max_shift_ppm
|
|
suggested_alignment_max_shift_ppm = processed_value
|
|
alignment_max_shift_ppm = processed_value
|
|
elseif param_key == :min_matched_peaks
|
|
suggested_alignment_min_matched_peaks = processed_value
|
|
alignment_min_matched_peaks = processed_value
|
|
end
|
|
elseif step_name == :Calibration
|
|
if param_key == :fit_order
|
|
suggested_calibration_fit_order = processed_value
|
|
calibration_fit_order = processed_value
|
|
elseif param_key == :ppm_tolerance
|
|
suggested_calibration_ppm_tolerance = processed_value
|
|
calibration_ppm_tolerance = processed_value
|
|
end
|
|
elseif step_name == :PeakPicking
|
|
if param_key == :snr_threshold
|
|
suggested_peak_picking_snr_threshold = processed_value
|
|
peak_picking_snr_threshold = processed_value
|
|
elseif param_key == :half_window
|
|
suggested_peak_picking_half_window = processed_value
|
|
peak_picking_half_window = processed_value
|
|
elseif param_key == :min_peak_prominence
|
|
suggested_peak_picking_min_peak_prominence = processed_value
|
|
peak_picking_min_peak_prominence = processed_value
|
|
elseif param_key == :merge_peaks_tolerance
|
|
suggested_peak_picking_merge_peaks_tolerance = processed_value
|
|
peak_picking_merge_peaks_tolerance = processed_value
|
|
elseif param_key == :min_peak_width_ppm
|
|
suggested_peak_picking_min_peak_width_ppm = processed_value
|
|
peak_picking_min_peak_width_ppm = processed_value
|
|
elseif param_key == :max_peak_width_ppm
|
|
suggested_peak_picking_max_peak_width_ppm = processed_value
|
|
peak_picking_max_peak_width_ppm = processed_value
|
|
elseif param_key == :min_peak_shape_r2
|
|
suggested_peak_picking_min_peak_shape_r2 = processed_value
|
|
peak_picking_min_peak_shape_r2 = processed_value
|
|
end
|
|
elseif step_name == :PeakSelection
|
|
if param_key == :min_snr
|
|
suggested_peak_selection_min_snr = processed_value
|
|
peak_selection_min_snr = processed_value
|
|
elseif param_key == :min_fwhm_ppm
|
|
suggested_peak_selection_min_fwhm_ppm = processed_value
|
|
peak_selection_min_fwhm_ppm = processed_value
|
|
elseif param_key == :max_fwhm_ppm
|
|
suggested_peak_selection_max_fwhm_ppm = processed_value
|
|
peak_selection_max_fwhm_ppm = processed_value
|
|
elseif param_key == :min_shape_r2
|
|
suggested_peak_selection_min_shape_r2 = processed_value
|
|
peak_selection_min_shape_r2 = processed_value
|
|
elseif param_key == :frequency_threshold
|
|
suggested_peak_selection_frequency_threshold = processed_value
|
|
peak_selection_frequency_threshold = processed_value
|
|
elseif param_key == :correlation_threshold
|
|
suggested_peak_selection_correlation_threshold = processed_value
|
|
peak_selection_correlation_threshold = processed_value
|
|
end
|
|
elseif step_name == :PeakBinning
|
|
if param_key == :tolerance
|
|
suggested_binning_tolerance = processed_value
|
|
binning_tolerance = processed_value
|
|
elseif param_key == :frequency_threshold
|
|
suggested_binning_frequency_threshold = processed_value
|
|
binning_frequency_threshold = processed_value
|
|
elseif param_key == :min_peak_per_bin
|
|
suggested_binning_min_peak_per_bin = processed_value
|
|
binning_min_peak_per_bin = processed_value
|
|
elseif param_key == :max_bin_width_ppm
|
|
suggested_binning_max_bin_width_ppm = processed_value
|
|
binning_max_bin_width_ppm = processed_value
|
|
elseif param_key == :num_uniform_bins
|
|
suggested_binning_num_uniform_bins = processed_value
|
|
binning_num_uniform_bins = processed_value
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Also set method types for steps
|
|
if haskey(recommended_params, :Smoothing) && haskey(recommended_params[:Smoothing], :method)
|
|
smoothing_method = string(recommended_params[:Smoothing][:method])
|
|
end
|
|
if haskey(recommended_params, :BaselineCorrection) && haskey(recommended_params[:BaselineCorrection], :method)
|
|
baseline_method = string(recommended_params[:BaselineCorrection][:method])
|
|
end
|
|
if haskey(recommended_params, :Normalization) && haskey(recommended_params[:Normalization], :method)
|
|
normalization_method = string(recommended_params[:Normalization][:method])
|
|
end
|
|
if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :method)
|
|
alignment_method = string(recommended_params[:PeakAlignment][:method])
|
|
end
|
|
if haskey(recommended_params, :PeakPicking) && haskey(recommended_params[:PeakPicking], :method)
|
|
peak_picking_method = string(recommended_params[:PeakPicking][:method])
|
|
end
|
|
if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :method)
|
|
binning_method = string(recommended_params[:PeakBinning][:method])
|
|
end
|
|
if haskey(recommended_params, :PeakAlignment) && haskey(recommended_params[:PeakAlignment], :tolerance_unit)
|
|
alignment_tolerance_unit = string(recommended_params[:PeakAlignment][:tolerance_unit])
|
|
end
|
|
if haskey(recommended_params, :PeakBinning) && haskey(recommended_params[:PeakBinning], :tolerance_unit)
|
|
binning_tolerance_unit = string(recommended_params[:PeakBinning][:tolerance_unit])
|
|
end
|
|
|
|
msg = "Suggestions have been recalculated."
|
|
catch e
|
|
msg = "Failed to recalculate suggestions: $e"
|
|
warning_msg = true
|
|
@error "Recalculation failed" exception=(e, catch_backtrace())
|
|
finally
|
|
is_processing = false
|
|
end
|
|
end
|
|
|
|
@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 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
|
|
|
|
# This new handler correctly adds the file from full_route 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 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 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
|
|
|
|
@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 <i>m/z</i>: $(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 = "<i>m/z</i>: $(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 = "<i>m/z</i>: $(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 = "<i>m/z</i>: $(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 <i>m/z</i>: $(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 <i>m/z</i>: $(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 = "<i>m/z</i>: $(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 = "<i>m/z</i>: $(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 <i>m/z</i>: $(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 <i>m/z</i>: $(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 = "<i>m/z</i>: $(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 = "<i>m/z</i>: $(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 <i>m/z</i>: $(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 <i>m/z</i>: $(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 = "<i>m/z</i>: $(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 <i>m/z</i>: $(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 = "<i>m/z</i>: $(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 = "<i>m/z</i>: $(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 = "<i>m/z</i>: $(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 <i>m/z</i>: $(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="<b>m/z</b>: %{x:.4f}<extra></extra>",
|
|
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="<b>m/z</b>: %{x:.4f}<extra></extra>",
|
|
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="<b>%{data.name}</b><extra></extra>"
|
|
)
|
|
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 |