module App # ==Packages == using GenieFramework # Set up Genie development environment. using Pkg using Libz using PlotlyBase using CairoMakie using Colors # using julia_mzML_imzML 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 # Bring MSIData into App module's scope using .MSI_src: MSIData, OpenMSIData, #=GetSpectrum,=# 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 include("./julia_imzML_visual.jl") function load_registry(registry_path) if isfile(registry_path) try return JSON.parsefile(registry_path, dicttype=Dict{String, Any}) catch e @error "Failed to parse registry.json: $e" return Dict{String, Any}() end end return Dict{String, Any}() end function extract_metadata(msi_data::MSIData, source_path::String) df = msi_data.spectrum_stats_df if df === nothing # This can happen if precompute_analytics hasn't been run # We can still return basic info return Dict( "summary" => [ Dict("parameter" => "File Name", "value" => basename(source_path)), Dict("parameter" => "Number of Spectra", "value" => length(msi_data.spectra_metadata)), Dict("parameter" => "Image Dimensions", "value" => "$(msi_data.image_dims[1]) x $(msi_data.image_dims[2])"), ], "global_min_mz" => nothing, "global_max_mz" => nothing ) end summary_stats = [ Dict("parameter" => "File Name", "value" => basename(source_path)), Dict("parameter" => "Number of Spectra", "value" => length(msi_data.spectra_metadata)), Dict("parameter" => "Image Dimensions", "value" => "$(msi_data.image_dims[1]) x $(msi_data.image_dims[2])"), Dict("parameter" => "Global Min m/z", "value" => @sprintf("%.4f", msi_data.global_min_mz)), Dict("parameter" => "Global Max m/z", "value" => @sprintf("%.4f", msi_data.global_max_mz)), Dict("parameter" => "Mean TIC", "value" => @sprintf("%.2e", mean(df.TIC))), Dict("parameter" => "Mean BPI", "value" => @sprintf("%.2e", mean(df.BPI))), Dict("parameter" => "Mean # Points", "value" => @sprintf("%.1f", mean(df.NumPoints))), ] if hasproperty(df, :Mode) centroid_count = count(==(MSI_src.CENTROID), df.Mode) profile_count = count(==(MSI_src.PROFILE), df.Mode) unknown_count = count(==(MSI_src.UNKNOWN), df.Mode) push!(summary_stats, Dict("parameter" => "Centroid Spectra", "value" => string(centroid_count))) push!(summary_stats, Dict("parameter" => "Profile Spectra", "value" => string(profile_count))) if unknown_count > 0 push!(summary_stats, Dict("parameter" => "Unknown Mode Spectra", "value" => string(unknown_count))) end end return Dict( "summary" => summary_stats, "global_min_mz" => msi_data.global_min_mz, "global_max_mz" => msi_data.global_max_mz ) end function update_registry(registry_path, dataset_name, source_path, metadata=nothing, is_imzML=false) registry = load_registry(registry_path) entry = Dict{String, Any}( # Explicitly type the dictionary to allow mixed value types "source_path" => source_path, "processed_date" => string(now()), "is_imzML" => is_imzML ) if metadata !== nothing entry["metadata"] = metadata end registry[dataset_name] = entry try open(registry_path, "w") do f JSON.print(f, registry, 4) end catch e @error "Failed to write to registry.json: $e" end end function process_file_safely(file_path, masses, params, progress_message_ref, overall_progress_ref) local_msi_data = nothing dataset_name = replace(basename(file_path), r"\.imzML$"i => "") output_dir = joinpath("public", dataset_name) println("Processing: $dataset_name -> $output_dir") try # --- Load Data --- progress_message_ref = "Loading: $(basename(file_path))" local_msi_data = OpenMSIData(file_path) if !(local_msi_data.source isa ImzMLSource) @warn "Skipping non-imzML file: $(basename(file_path))" return (false, "Skipped: Not an imzML file") end # --- Generate Slices (this will call precompute_analytics if needed) --- progress_message_ref = "Generating $(length(masses)) slices for $(dataset_name)..." slice_dict = get_multiple_mz_slices(local_msi_data, masses, params.tolerance) # --- Extract metadata *after* it has been computed --- metadata = extract_metadata(local_msi_data, file_path) # --- Save Slices --- mkpath(output_dir) # Ensure output directory exists for (mass_idx, mass) in enumerate(masses) progress_message_ref = "File $(params.fileIdx)/$(params.nFiles): Saving slice for m/z=$mass" slice = slice_dict[mass] text_nmass = replace(string(mass), "." => "_") bitmap_filename = params.triqE ? "TrIQ_$(text_nmass).bmp" : "MSI_$(text_nmass).bmp" colorbar_filename = params.triqE ? "colorbar_TrIQ_$(text_nmass).png" : "colorbar_MSI_$(text_nmass).png" if all(iszero, slice) sliceQuant = zeros(UInt8, size(slice)) @warn "No intensity data for m/z = $mass in $(dataset_name)" else sliceQuant = params.triqE ? TrIQ(slice, params.colorL, params.triqP) : quantize_intensity(slice, params.colorL) if params.medianF sliceQuant = round.(UInt8, median_filter(sliceQuant)) end end save_bitmap(joinpath(output_dir, bitmap_filename), sliceQuant, ViridisPalette) if !all(iszero, slice) generate_colorbar_image(slice, params.colorL, joinpath(output_dir, colorbar_filename); use_triq=params.triqE, triq_prob=params.triqP) end end is_imzML = local_msi_data.source isa ImzMLSource update_registry(params.registry, dataset_name, file_path, metadata, is_imzML) return (true, "") catch e @error "File processing failed" file=file_path exception=(e, catch_backtrace()) return (false, "File: $(basename(file_path)) - $(sprint(showerror, e))") finally if local_msi_data !== nothing # Cleanup end local_msi_data = nothing GC.gc(true) if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end end end @genietools # == Reactive code == # Reactive code to make the UI interactive @app begin # == Reactive variables == # reactive variables exist in both the Julia backend and the browser with two-way synchronization # @out variables can only be modified by the backend # @in variables can be modified by both the backend and the browser # variables must be initialized with constant values, or variables defined outside of the @app block ## Interface non Variables @out btnStartDisable=true @out btnPlotDisable=false @out btnSpectraDisable=false # Loading animations @in progress=false @in progressPlot=false @in progressSpectraPlot=false # Text field validations @in triqEnabled=false @in SpectraEnabled=false @in MFilterEnabled=false # Dialogs @in warning_msg=false @in CompareDialog=false ## Interface Variables @in file_route="" @in file_name="" @in Nmass="0.0" @in Tol=0.1 @in triqProb=0.98 @in colorLevel=20 ## Interface Buttons @in btnSearch=false # To search for files in your device @in btnAddBatch = false @in clear_batch_btn = false @out batch_file_count = 0 @in mainProcess=false # To generate images @in compareBtn=false # To open dialog @in createMeanPlot=false # To generate mean spectrum plot @in createXYPlot=false # To generate an spectrum plot according to the xy values inputed @in createSumPlot=false # To generate a sum of all the spectrum plots @in image3dPlot=false # To generate 3d plot based on current image @in triq3dPlot=false # To generate 3d plot based on current triq image @in imageCPlot=false # To generate contour plots of current image @in triqCPlot=false # To generate contour plots of current triq image # Image change buttons @in imgPlus=false @in imgMinus=false @in imgPlusT=false @in imgMinusT=false # Image change comparative buttons @in imgPlusCompLeft=false @in imgMinusCompLeft=false @in imgPlusTCompLeft=false @in imgMinusTCompLeft=false @in imgPlusCompRight=false @in imgMinusCompRight=false @in imgPlusTCompRight=false @in imgMinusTCompRight=false ## Tabulation variables @out tabIDs=["tab0","tab1","tab2","tab3","tab4"] @out tabLabels=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in selectedTab="tab0" @out CompTabIDsLeft=["tab0","tab1","tab2","tab3","tab4"] @out CompTabLabelsLeft=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in CompSelectedTabLeft="tab0" @out CompTabIDsRight=["tab0","tab1","tab2","tab3","tab4"] @out CompTabLabelsRight=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] @in CompSelectedTabRight="tab0" # Interface Images @out imgInt="/.bmp" # image Interface @out imgIntT="/.bmp" # image Interface TrIQ @out colorbar="/.png" @out colorbarT="/.png" # Interface controlling for the comparative view @out imgIntCompLeft="/.bmp" @out imgIntTCompLeft="/.bmp" @out colorbarCompLeft="/.png" @out colorbarTCompLeft="/.png" @out imgIntCompRight="/.bmp" @out imgIntTCompRight="/.bmp" @out colorbarCompRight="/.png" @out colorbarTCompRight="/.png" @out imgWidth=0 @out imgHeight=0 # Optical Image Overlay & Transparency @in imgTrans=1.0 @in progressOptical=false @out btnOpticalDisable=true @in btnOptical=false @in btnOpticalT=false @in opticalOverTriq=false @out imgRoute="" # Messages to interface variables @out msg="" @out msgimg="" @out msgtriq="" # Reiteration of the messages under the image to know which spectra is being visualized @out msgimgCompLeft="" @out msgtriqCompLeft="" @out msgimgCompRight="" @out msgtriqCompRight="" # Centralized MSIData object @out msi_data::Union{MSIData, Nothing} = nothing # Metadata table variables @in showMetadataDialog = false @in showMetadataBtn = false @out metadata_columns = [] @out metadata_rows = [] @out btnMetadataDisable = false @in selected_folder_metadata = "" # Saves the route where imzML and mzML files are located @out full_route="" # == Converter Tab Variables == @in left_tab = "generator" @out mzml_full_route = "" @out sync_full_route = "" @in btnSearchMzml = false @in btnSearchSync = false @in convert_process = false @out progress_conversion = false @out msg_conversion = "" @out btnConvertDisable = true # == Batch Summary Dialog == @in showBatchSummary = false @out batch_summary = "" # == Batch Processing & Registry Variables == @private registry_init_done = false @in selected_files = String[] @out available_folders = String[] @out image_available_folders = String[] @out registry_path = joinpath("public", "registry.json") # Progress reporting @out overall_progress = 0.0 @out progress_message = "" # == Folder-based UI State == @in selected_folder_main = "" @in selected_folder_compare_left = "" @in selected_folder_compare_right = "" # For the creation of images with a more specific mass charge @out text_nmass="" # For image search image lists we apply a filter that searches specific type of images into our public folder, then we sort it in a "numerical" order @in msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")),lt=natural) @in col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir("public")),lt=natural) @in triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")),lt=natural) @in col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir("public")),lt=natural) # Set current image for the list to display @out current_msi="" @out current_col_msi="" @out current_triq="" @out current_col_triq="" # We reiterate the process to display in the comparative view @out current_msiCompLeft="" @out current_col_msiCompLeft="" @out current_triqCompLeft="" @out current_col_triqCompLeft="" @out current_msiCompRight="" @out current_col_msiCompRight="" @out current_triqCompRight="" @out current_col_triqCompRight="" ## Time measurement variables @out sTime=time() @out fTime=time() @out eTime=time() ## Plots # Local image to plot layoutImg=PlotlyBase.Layout( title=PlotlyBase.attr( text="", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=14, color="black" ) ), xaxis=PlotlyBase.attr( visible=false, scaleanchor="y", range=[0, 0] ), yaxis=PlotlyBase.attr( visible=false, range=[0, 0] ), margin=attr(l=0,r=0,t=0,b=0,pad=0) ) traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) @out plotdataImg=[traceImg] @out plotlayoutImg=layoutImg # For the image in the comparative view @out plotdataImgCompLeft=[traceImg] @out plotlayoutImgCompLeft=layoutImg @out plotdataImgCompRight=[traceImg] @out plotlayoutImgCompRight=layoutImg # For triq image @out plotdataImgT=[traceImg] @out plotlayoutImgT=layoutImg # For the triq image in the comparative view @out plotdataImgTCompLeft=[traceImg] @out plotlayoutImgTCompLeft=layoutImg @out plotdataImgTCompRight=[traceImg] @out plotlayoutImgTCompRight=layoutImg # Interface Plot Spectrum layoutSpectra=PlotlyBase.Layout( title=PlotlyBase.attr( text="Spectrum plot", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, color="black" ) ), hovermode="closest", xaxis=PlotlyBase.attr( title="m/z", showgrid=true ), yaxis=PlotlyBase.attr( title="Intensity", showgrid=true, tickformat = ".3g" ), margin=attr(l=0,r=0,t=120,b=0,pad=0) ) # Dummy 2D scatter plot traceSpectra=PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(),marker=attr(size=1, color="blue", opacity=0.1)) # Create conection to frontend @out plotdata=[traceSpectra] @out plotlayout=layoutSpectra @in xCoord=0 @in yCoord=0 @out xSpectraMz = Vector{Float64}() @out ySpectraMz = Vector{Float64}() # Interactive plot reactions @in data_click=Dict{String,Any}() #@in data_selected=Dict{String,Any}() # Selected is for areas, this can work for the masks # # Interface Plot Surface layoutContour=PlotlyBase.Layout( title=PlotlyBase.attr( text="2D Topographic map", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, color="black" ) ), xaxis=PlotlyBase.attr( visible=false, scaleanchor="y" ), yaxis=PlotlyBase.attr( visible=false ), margin=attr(l=0,r=0,t=100,b=0,pad=0) ) # Dummy 2D surface plot traceContour=PlotlyBase.contour(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines") # Create conection to frontend @out plotdataC=[traceContour] @out plotlayoutC=layoutContour # Interface Plot 3d # Define the layout for the 3D plot layout3D=PlotlyBase.Layout( title=PlotlyBase.attr( text="3D Surface plot", font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, color="black" ) ), scene=attr( xaxis_title="X", yaxis_title="Y", zaxis_title="Z", xaxis_nticks=20, yaxis_nticks=20, zaxis_nticks=4, camera=attr(eye=attr(x=0, y=-1, z=0.5)), aspectratio=attr(x=1, y=1, z=0.2) ), margin=attr(l=0,r=0,t=120,b=0,pad=0) ) # Dummy 3D surface plot x=1:10 y=1:10 z=[sin(i * j / 10) for i in x, j in y] trace3D=PlotlyBase.surface(x=Vector{Float64}(), y=Vector{Float64}(), z=Matrix{Float64}(undef, 0, 0), contours_z=attr( show=true, usecolormap=true, highlightcolor="limegreen", project_z=true ), colorscale="Viridis") # Create conection to frontend @out plotdata3d=[trace3D] @out plotlayout3d=layout3D # == Reactive handlers == # Reactive handlers watch a variable and execute a block of code when its value changes # The onbutton handler will set the variable to false after the block is executed # This handler correctly uses pick_file and loads the selected file # as the active dataset for the UI. @onbutton btnSearch @time begin picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml") if isempty(picked_route) return end progress = true msg = "Opening file: $(basename(picked_route))..." @async begin try dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|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 btnMetadataDisable = false btnStartDisable = false btnPlotDisable = false btnSpectraDisable = false SpectraEnabled = true selected_folder_main = dataset_name # Update folder lists in UI all_folders = sort(collect(keys(registry)), lt=natural) img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) available_folders = deepcopy(all_folders) image_available_folders = deepcopy(img_folders) msg = "Successfully loaded pre-processed dataset: $(dataset_name)" progress = false return end # --- Full Load Path --- msg = "Performing first-time analysis for: $(basename(picked_route))..." local local_full_route if endswith(picked_route, r"imzml"i) local_full_route = replace(picked_route, r"\.imzml$"i => ".imzML") if picked_route != local_full_route mv(picked_route, local_full_route, force=true) end else local_full_route = picked_route end full_route = local_full_route sTime = time() loaded_data = OpenMSIData(local_full_route) is_imzML = loaded_data.source isa ImzMLSource precompute_analytics(loaded_data) 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 eTime = round(time() - sTime, digits=3) msg = "Active file loaded in $(eTime) seconds. Dataset '$(dataset_name)' is ready for analysis." btnStartDisable = false btnPlotDisable = false btnSpectraDisable = false SpectraEnabled = true catch e msi_data = nothing msg = "Error loading active file: $e" warning_msg = true btnStartDisable = true btnSpectraDisable = true SpectraEnabled = false btnMetadataDisable = true @error "File loading failed" exception=(e, catch_backtrace()) finally GC.gc() if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) end progress = false progressSpectraPlot = false end end end # This new handler correctly adds the file from full_route to the batch list. @onbutton btnAddBatch begin if isempty(full_route) || full_route == "unknown (manually added)" msg = "No active file selected to add to batch." warning_msg = true return end if !(full_route in selected_files) push!(selected_files, full_route) selected_files = deepcopy(selected_files) # Force reactivity batch_file_count = length(selected_files) msg = "File added to batch." else msg = "File is already in the batch list." warning_msg = true end end @onbutton clear_batch_btn begin selected_files = String[] batch_file_count = 0 msg = "Batch cleared" end @onchange selected_files begin batch_file_count = length(selected_files) end @onchange full_route begin if !isempty(full_route) && !(full_route in selected_files) push!(selected_files, full_route) selected_files = deepcopy(selected_files) # Force reactivity batch_file_count = length(selected_files) msg = "File automatically added to batch" end end @onbutton showMetadataBtn begin if !isempty(available_folders) if !isempty(selected_folder_main) selected_folder_metadata = selected_folder_main elseif !isempty(available_folders) selected_folder_metadata = first(available_folders) end showMetadataDialog = true else msg = "No processed datasets available." warning_msg = true end end @onchange selected_folder_metadata begin if !isempty(selected_folder_metadata) registry = load_registry(registry_path) dataset_info = get(registry, selected_folder_metadata, nothing) if dataset_info !== nothing && haskey(dataset_info, "metadata") && !isempty(get(dataset_info["metadata"], "summary", [])) metadata_rows = dataset_info["metadata"]["summary"] btnMetadataDisable = false else metadata_rows = [] btnMetadataDisable = true msg = "Metadata not found in registry for $(selected_folder_metadata)." end end end @onchange btnSearchMzml, btnSearchSync begin 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) end @onbutton convert_process begin 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 progress_conversion = true btnConvertDisable = true msg_conversion = "Starting conversion process..." @async begin 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 progress_conversion = false # Re-enable button if files are still selected btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route) end end end @onbutton mainProcess @time begin # --- UI State Update --- progress = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true overall_progress = 0.0 progress_message = "Preparing batch process..." # --- CAPTURE CURRENT VALUES HERE (NO []) --- 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_registry_path = registry_path println("starting main process with $(length(current_selected_files)) files") @async begin 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 println("Nmass value: '$current_nmass'") println("Type of current_nmass: $(typeof(current_nmass))") masses_str = split(current_nmass, ',', keepempty=false) println("Parsed masses strings: $masses_str") masses = Float64[] try masses = [parse(Float64, strip(m)) for m in masses_str] println("Parsed masses: $masses") catch e progress_message = "Invalid m/z value(s). Please provide a comma-separated list of numbers. Error: $e" warning_msg = true println(progress_message) return end println("Masses array: $masses, type: $(typeof(masses))") if !(masses isa AbstractArray) || isempty(masses) progress_message = "No valid m/z values found. Please provide comma-separated positive numbers." warning_msg = true println(progress_message) return end # Check other parameters if !(0 < current_tol <= 1) progress_message = "Tolerance must be between 0 and 1." warning_msg = true println(progress_message) return end if !(1 < current_color_level < 257) progress_message = "Color levels must be between 2 and 256." warning_msg = true println(progress_message) return end println("entering batch processing loop with masses: $masses") # --- 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[] for (file_idx, file_path) in enumerate(current_selected_files) # Update progress for current file progress_message = "Processing file $file_idx/$num_files: $(basename(file_path))" overall_progress = current_step / total_steps # Create parameters for this specific file 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 ) # Process the file success, error_msg = process_file_safely(file_path, masses, all_params, progress_message, overall_progress) if !success push!(errors["load_errors"], error_msg) else dataset_name = replace(basename(file_path), r"\.imzML$"i => "") push!(newly_created_folders, dataset_name) end current_step += 1 end # --- 3. Final Report --- total_time_end = round(time() - total_time_start, digits=3) # Update folder lists in UI 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 batch_summary = """ Processed $(successful_files)/$(num_files) files successfully. 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 catch e println("Error in main process: $e") msg = "Batch processing failed: $e" warning_msg = true @error "Main process failed" exception=(e, catch_backtrace()) finally # --- UI State Reset --- progress = false btnStartDisable = false btnPlotDisable = false btnOpticalDisable = false btnSpectraDisable = false SpectraEnabled = true overall_progress = 0.0 println("Done") end end end @onbutton createMeanPlot begin if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true msg = "Loading plot for $(selected_folder_main)..." @async begin try sTime = time() registry = load_registry(registry_path) target_path = registry[selected_folder_main]["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 msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) existing_entry = get(registry, selected_folder_main, nothing) if existing_entry !== nothing && haskey(get(existing_entry, "metadata", Dict()), "global_min_mz") && existing_entry["metadata"]["global_min_mz"] !== nothing println("Injecting cached m/z range to skip Pass 1...") msi_data.global_min_mz = existing_entry["metadata"]["global_min_mz"] msi_data.global_max_mz = existing_entry["metadata"]["global_max_mz"] else precompute_analytics(msi_data) end end plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data, selected_folder_main) selectedTab = "tab2" fTime = time() eTime = round(fTime - sTime, digits=3) msg = "Plot loaded in $(eTime) seconds" catch e msg = "Could not generate mean spectrum plot: $e" warning_msg = true @error "Mean spectrum plotting failed" exception=(e, catch_backtrace()) finally progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false btnStartDisable = false end end end @onbutton createSumPlot begin if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true msg = "Loading total spectrum plot for $(selected_folder_main)..." @async begin try sTime = time() registry = load_registry(registry_path) target_path = registry[selected_folder_main]["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 msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) existing_entry = get(registry, selected_folder_main, nothing) if existing_entry !== nothing && haskey(get(existing_entry, "metadata", Dict()), "global_min_mz") && existing_entry["metadata"]["global_min_mz"] !== nothing println("Injecting cached m/z range to skip Pass 1...") msi_data.global_min_mz = existing_entry["metadata"]["global_min_mz"] msi_data.global_max_mz = existing_entry["metadata"]["global_max_mz"] else precompute_analytics(msi_data) end end plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data, selected_folder_main) selectedTab = "tab2" fTime = time() eTime = round(fTime - sTime, digits=3) msg = "Total plot loaded in $(eTime) seconds" catch e msg = "Could not generate total spectrum plot: $e" warning_msg = true @error "Total spectrum plotting failed" exception=(e, catch_backtrace()) finally progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false btnStartDisable = false end end end @onbutton createXYPlot begin if isempty(selected_folder_main) msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end progressSpectraPlot = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true msg = "Loading plot for $(selected_folder_main)..." @async begin try sTime = time() registry = load_registry(registry_path) target_path = registry[selected_folder_main]["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 msg = "Reloading $(basename(target_path)) for analysis..." full_route = target_path msi_data = OpenMSIData(target_path) existing_entry = get(registry, selected_folder_main, nothing) if existing_entry !== nothing && haskey(get(existing_entry, "metadata", Dict()), "global_min_mz") && existing_entry["metadata"]["global_min_mz"] !== nothing println("Injecting cached m/z range to skip Pass 1...") msi_data.global_min_mz = existing_entry["metadata"]["global_min_mz"] msi_data.global_max_mz = existing_entry["metadata"]["global_max_mz"] else precompute_analytics(msi_data) end end y = yCoord < 0 ? abs(yCoord) : yCoord plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y, imgWidth, imgHeight, selected_folder_main) xCoord = plotlayout.title == "Spectrum #$(xCoord)" ? xCoord : clamp(xCoord, 1, imgWidth) yCoord = plotlayout.title == "Spectrum #$(xCoord)" ? 0 : -clamp(y, 1, imgHeight) selectedTab = "tab2" fTime = time() eTime = round(fTime - sTime, digits=3) msg = "Plot loaded in $(eTime) seconds" catch e msg = "Could not retrieve spectrum: $e" warning_msg = true @error "Spectrum plotting failed" exception=(e, catch_backtrace()) finally progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false btnStartDisable = false end end end # --- Main View Handlers --- @onbutton imgMinus begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) # Check if folder exists to prevent errors if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_msi, msi_bmp) new_col_msi=decrement_image(current_col_msi, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msi = new_msi current_col_msi = new_col_msi imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)" colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)" text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt) btnOpticalDisable = false end end @onbutton imgPlus begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_msi, msi_bmp) new_col_msi=increment_image(current_col_msi, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msi = new_msi current_col_msi = new_col_msi imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)" colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)" text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt) btnOpticalDisable = false end end @onbutton imgMinusT begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_triq, triq_bmp) new_col_msi=decrement_image(current_col_triq, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triq = new_msi current_col_triq = new_col_msi imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)" colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)" text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) btnOpticalDisable = false end end @onbutton imgPlusT begin if isempty(selected_folder_main) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_main) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_triq, triq_bmp) new_col_msi=increment_image(current_col_triq, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triq = new_msi current_col_triq = new_col_msi imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)" colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)" text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) btnOpticalDisable = false end end # --- Compare View Handlers --- @onbutton imgMinusCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_msiCompLeft, msi_bmp) new_col_msi=decrement_image(current_col_msiCompLeft, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompLeft = new_msi current_col_msiCompLeft = new_col_msi imgIntCompLeft = "/$(selected_folder_compare_left)/$(current_msiCompLeft)?t=$(timestamp)" colorbarCompLeft = "/$(selected_folder_compare_left)/$(current_col_msiCompLeft)?t=$(timestamp)" text_nmass = replace(current_msiCompLeft, r"MSI_|.bmp" => "") msgimgCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompLeft, plotlayoutImgCompLeft, _, _ = loadImgPlot(imgIntCompLeft) end end @onbutton imgPlusCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_msiCompLeft, msi_bmp) new_col_msi=increment_image(current_col_msiCompLeft, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompLeft = new_msi current_col_msiCompLeft = new_col_msi imgIntCompLeft = "/$(selected_folder_compare_left)/$(current_msiCompLeft)?t=$(timestamp)" colorbarCompLeft = "/$(selected_folder_compare_left)/$(current_col_msiCompLeft)?t=$(timestamp)" text_nmass = replace(current_msiCompLeft, r"MSI_|.bmp" => "") msgimgCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompLeft, plotlayoutImgCompLeft, _, _ = loadImgPlot(imgIntCompLeft) end end @onbutton imgMinusTCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_triqCompLeft, triq_bmp) new_col_msi=decrement_image(current_col_triqCompLeft, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompLeft = new_msi current_col_triqCompLeft = new_col_msi imgIntTCompLeft = "/$(selected_folder_compare_left)/$(current_triqCompLeft)?t=$(timestamp)" colorbarTCompLeft = "/$(selected_folder_compare_left)/$(current_col_triqCompLeft)?t=$(timestamp)" text_nmass = replace(current_triqCompLeft, r"TrIQ_|.bmp" => "") msgtriqCompLeft = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompLeft, plotlayoutImgTCompLeft, _, _ = loadImgPlot(imgIntTCompLeft) end end @onbutton imgPlusTCompLeft begin if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_triqCompLeft, triq_bmp) new_col_msi=increment_image(current_col_triqCompLeft, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompLeft = new_msi current_col_triqCompLeft = new_col_msi imgIntTCompLeft = "/$(selected_folder_compare_left)/$(current_triqCompLeft)?t=$(timestamp)" colorbarTCompLeft = "/$(selected_folder_compare_left)/$(current_col_triqCompLeft)?t=$(timestamp)" text_nmass = replace(current_triqCompLeft, r"TrIQ_|.bmp" => "") msgtriqCompLeft = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompLeft, plotlayoutImgTCompLeft, _, _ = loadImgPlot(imgIntTCompLeft) end end @onbutton imgMinusCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_msiCompRight, msi_bmp) new_col_msi=decrement_image(current_col_msiCompRight, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompRight = new_msi current_col_msiCompRight = new_col_msi imgIntCompRight = "/$(selected_folder_compare_right)/$(current_msiCompRight)?t=$(timestamp)" colorbarCompRight = "/$(selected_folder_compare_right)/$(current_col_msiCompRight)?t=$(timestamp)" text_nmass = replace(current_msiCompRight, r"MSI_|.bmp" => "") msgimgCompRight = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompRight, plotlayoutImgCompRight, _, _ = loadImgPlot(imgIntCompRight) end end @onbutton imgPlusCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end msi_bmp=sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_msi_png=sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_msiCompRight, msi_bmp) new_col_msi=increment_image(current_col_msiCompRight, col_msi_png) if new_msi !== nothing && new_col_msi !== nothing current_msiCompRight = new_msi current_col_msiCompRight = new_col_msi imgIntCompRight = "/$(selected_folder_compare_right)/$(current_msiCompRight)?t=$(timestamp)" colorbarCompRight = "/$(selected_folder_compare_right)/$(current_col_msiCompRight)?t=$(timestamp)" text_nmass = replace(current_msiCompRight, r"MSI_|.bmp" => "") msgimgCompRight = "m/z: $(replace(text_nmass, "_" => "."))" plotdataImgCompRight, plotlayoutImgCompRight, _, _ = loadImgPlot(imgIntCompRight) end end @onbutton imgMinusTCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=decrement_image(current_triqCompRight, triq_bmp) new_col_msi=decrement_image(current_col_triqCompRight, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompRight = new_msi current_col_triqCompRight = new_col_msi imgIntTCompRight = "/$(selected_folder_compare_right)/$(current_triqCompRight)?t=$(timestamp)" colorbarTCompRight = "/$(selected_folder_compare_right)/$(current_col_triqCompRight)?t=$(timestamp)" text_nmass = replace(current_triqCompRight, r"TrIQ_|.bmp" => "") msgtriqCompRight = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompRight, plotlayoutImgTCompRight, _, _ = loadImgPlot(imgIntTCompRight) end end @onbutton imgPlusTCompRight begin if isempty(selected_folder_compare_right) return end timestamp=string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) return end triq_bmp=sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)),lt=natural) col_triq_png=sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)),lt=natural) new_msi=increment_image(current_triqCompRight, triq_bmp) new_col_msi=increment_image(current_col_triqCompRight, col_triq_png) if new_msi !== nothing && new_col_msi !== nothing current_triqCompRight = new_msi current_col_triqCompRight = new_col_msi imgIntTCompRight = "/$(selected_folder_compare_right)/$(current_triqCompRight)?t=$(timestamp)" colorbarTCompRight = "/$(selected_folder_compare_right)/$(current_col_triqCompRight)?t=$(timestamp)" text_nmass = replace(current_triqCompRight, r"TrIQ_|.bmp" => "") msgtriqCompRight = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" plotdataImgTCompRight, plotlayoutImgTCompRight, _, _ = loadImgPlot(imgIntTCompRight) end end # This handler will now correctly load the first image from the newly selected folder. @onchange selected_folder_main begin 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 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, _, _ = loadImgPlot(imgInt) text_nmass = replace(current_msi, r"MSI_|.bmp" => "") msgimg = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_png) current_col_msi = first(col_msi_png) colorbar = "/$(selected_folder_main)/$(current_col_msi)" else colorbar = "" end else imgInt = "" colorbar = "" msgimg = "No MSI images found in this dataset." plotdataImg = [traceImg] plotlayoutImg = layoutImg end # Handle TrIQ images triq_bmp = sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path)), lt=natural) col_triq_png = sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path)), lt=natural) if !isempty(triq_bmp) current_triq = first(triq_bmp) imgIntT = "/$(selected_folder_main)/$(current_triq)" plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT) text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "") msgtriq = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_png) current_col_triq = first(col_triq_png) colorbarT = "/$(selected_folder_main)/$(current_col_triq)" else colorbarT = "" end else imgIntT = "" colorbarT = "" msgtriq = "No TrIQ images found in this dataset." plotdataImgT = [traceImg] plotlayoutImgT = layoutImg end end end @onchange selected_folder_compare_left begin if !isempty(selected_folder_compare_left) timestamp = string(time_ns()) folder_path = joinpath("public", selected_folder_compare_left) if !isdir(folder_path) imgIntCompLeft, colorbarCompLeft, imgIntTCompLeft, colorbarTCompLeft = "", "", "", "" msgimgCompLeft, msgtriqCompLeft = "Folder not found.", "Folder not found." return end # Handle normal images msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_msi_png = sort(filter(f -> startswith(f, "colorbar_MSI_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(msi_bmp) current_msiCompLeft = first(msi_bmp) imgIntCompLeft = "/$(selected_folder_compare_left)/$(current_msiCompLeft)?t=$(timestamp)" plotdataImgCompLeft, plotlayoutImgCompLeft, _, _ = loadImgPlot(imgIntCompLeft) text_nmass = replace(current_msiCompLeft, r"MSI_|.bmp" => "") msgimgCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_png) current_col_msiCompLeft = first(col_msi_png) colorbarCompLeft = "/$(selected_folder_compare_left)/$(current_col_msiCompLeft)?t=$(timestamp)" else colorbarCompLeft = "" end else imgIntCompLeft, colorbarCompLeft, msgimgCompLeft = "", "", "No MSI images." end # Handle TrIQ images triq_bmp = sort(filter(f -> startswith(f, "TrIQ_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_triq_png = sort(filter(f -> startswith(f, "colorbar_TrIQ_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(triq_bmp) current_triqCompLeft = first(triq_bmp) imgIntTCompLeft = "/$(selected_folder_compare_left)/$(current_triqCompLeft)?t=$(timestamp)" plotdataImgTCompLeft, plotlayoutImgTCompLeft, _, _ = loadImgPlot(imgIntTCompLeft) text_nmass = replace(current_triqCompLeft, r"TrIQ_|.bmp" => "") msgtriqCompLeft = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_png) current_col_triqCompLeft = first(col_triq_png) colorbarTCompLeft = "/$(selected_folder_compare_left)/$(current_col_triqCompLeft)?t=$(timestamp)" else colorbarTCompLeft = "" end else imgIntTCompLeft, colorbarTCompLeft, msgtriqCompLeft = "", "", "No TrIQ images." end end end @onchange selected_folder_compare_right begin if !isempty(selected_folder_compare_right) timestamp = string(time_ns()) folder_path = joinpath("public", selected_folder_compare_right) if !isdir(folder_path) imgIntCompRight, colorbarCompRight, imgIntTCompRight, colorbarTCompRight = "", "", "", "" msgimgCompRight, msgtriqCompRight = "Folder not found.", "Folder not found." return end # Handle normal images msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_msi_png = sort(filter(f -> startswith(f, "colorbar_MSI_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(msi_bmp) current_msiCompRight = first(msi_bmp) imgIntCompRight = "/$(selected_folder_compare_right)/$(current_msiCompRight)?t=$(timestamp)" plotdataImgCompRight, plotlayoutImgCompRight, _, _ = loadImgPlot(imgIntCompRight) text_nmass = replace(current_msiCompRight, r"MSI_|.bmp" => "") msgimgCompRight = "m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_msi_png) current_col_msiCompRight = first(col_msi_png) colorbarCompRight = "/$(selected_folder_compare_right)/$(current_col_msiCompRight)?t=$(timestamp)" else colorbarCompRight = "" end else imgIntCompRight, colorbarCompRight, msgimgCompRight = "", "", "No MSI images." end # Handle TrIQ images triq_bmp = sort(filter(f -> startswith(f, "TrIQ_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural) col_triq_png = sort(filter(f -> startswith(f, "colorbar_TrIQ_") && endswith(f, ".png"), readdir(folder_path)), lt=natural) if !isempty(triq_bmp) current_triqCompRight = first(triq_bmp) imgIntTCompRight = "/$(selected_folder_compare_right)/$(current_triqCompRight)?t=$(timestamp)" plotdataImgTCompRight, plotlayoutImgTCompRight, _, _ = loadImgPlot(imgIntTCompRight) text_nmass = replace(current_triqCompRight, r"TrIQ_|.bmp" => "") msgtriqCompRight = "TrIQ m/z: $(replace(text_nmass, "_" => "."))" if !isempty(col_triq_png) current_col_triqCompRight = first(col_triq_png) colorbarTCompRight = "/$(selected_folder_compare_right)/$(current_col_triqCompRight)?t=$(timestamp)" else colorbarTCompRight = "" end else imgIntTCompRight, colorbarTCompRight, msgtriqCompRight = "", "", "No TrIQ images." end end end # 3d plot @onbutton image3dPlot begin msg="Image 3D plot selected" cleaned_imgInt=replace(imgInt, r"\?.*" => "") cleaned_imgInt=lstrip(cleaned_imgInt, '/') var=joinpath( "./public", cleaned_imgInt ) if !isfile(var) msg="Image could not be 3d plotted" warning_msg=true return end progressPlot=true btnPlotDisable=true btnStartDisable=true btnSpectraDisable=true @async begin try sTime=time() plotdata3d, plotlayout3d=loadSurfacePlot(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="tab4" 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 progressPlot=false btnPlotDisable=false btnStartDisable=false if msi_data !== nothing # We enable coord search and spectra plot creation btnSpectraDisable=false SpectraEnabled=true end end end end # 3d plot for TrIQ @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 progressPlot=true btnPlotDisable=true btnStartDisable=true btnSpectraDisable=true @async begin try sTime=time() plotdata3d, plotlayout3d=loadSurfacePlot(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="tab4" 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 progressPlot=false btnPlotDisable=false btnStartDisable=false if msi_data !== nothing # We enable coord search and spectra plot creation btnSpectraDisable=false SpectraEnabled=true end 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 progressPlot=true btnPlotDisable=true btnStartDisable=true btnSpectraDisable=true @async begin 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 progressPlot=false btnPlotDisable=false btnStartDisable=false if msi_data !== nothing # We enable coord search and spectra plot creation btnSpectraDisable=false SpectraEnabled=true end 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 progressPlot=true btnPlotDisable=true btnStartDisable=true btnSpectraDisable=true @async begin 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 progressPlot=false btnPlotDisable=false btnStartDisable=false if msi_data !== nothing # We enable coord search and spectra plot creation btnSpectraDisable=false SpectraEnabled=true end 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) # Main spectrum trace traceSpectra = PlotlyBase.scatter( x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}", showlegend=false ) # Parse all valid masses from the comma-separated string mass_strs = split(Nmass, ',', keepempty=false) mass_traces = [traceSpectra] # Start with the main spectrum valid_masses = Float64[] for (idx, mass_str) in enumerate(mass_strs) try mass_val = parse(Float64, strip(mass_str)) if mass_val > 0 # Only add valid positive masses push!(valid_masses, mass_val) # Create a vertical line for this mass (Plotly will auto-assign colors) mass_trace = PlotlyBase.scatter( x=[mass_val, mass_val], y=[0, maximum(ySpectraMz)], mode="lines", line=attr(width=1.5, dash="dash"), name="m/z $(round(mass_val, digits=4))", showlegend=false, hoverinfo="x+name", hovertemplate="%{data.name}" ) push!(mass_traces, mass_trace) end catch e # Skip invalid entries, continue with next continue end end # Update the plot data plotdata = mass_traces end end # Event detection for clicking on the images @onchange data_click begin if selectedTab == "tab1" || selectedTab == "tab0" # This is for the image heatmaps cursor_data = get(data_click, "cursor", nothing) if cursor_data === nothing return end x_val = get(cursor_data, "x", nothing) y_val = get(cursor_data, "y", nothing) if x_val === nothing || y_val === nothing return # Do nothing if coordinates are not provided by the event end x = Int32(round(x_val)) y = Int32(round(y_val)) # y is negative in the UI # Update the reactive coordinates, which will trigger the crosshair update xCoord = clamp(x, 1, imgWidth) yCoord = clamp(y, -imgHeight, -1) end end @onchange xCoord, yCoord begin if selectedTab == "tab1" plotdataImgT = filter(trace -> !(get(trace, :name, "") in ["Line X", "Line Y"]), plotdataImgT) trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight) plotdataImgT = append!(plotdataImgT, [trace1, trace2]) elseif selectedTab == "tab0" plotdataImg = filter(trace -> !(get(trace, :name, "") in ["Line X", "Line Y", "Optical"]), plotdataImg) trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight) plotdataImg = append!(plotdataImg, [trace1, trace2]) end end @onbutton btnOptical begin 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 end @onbutton btnOpticalT begin 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 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 @mounted watchplots() @onchange isready begin if isready && !registry_init_done @async begin # Run asynchronously to not block startup sleep(1.0) # Give frontend time to initialize try println("Synchronizing registry with filesystem on backend init...") reg_path = joinpath("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) println("Registry changed, saving...") open(reg_path, "w") do f JSON.print(f, registry, 4) end 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 end end end warmup_init() end GC.gc() # Trigger garbage collection if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure julia returns the freed memory to OS end end # == Pages == # Register a new route and the page that will be loaded on access @page("/", "app.jl.html") end