From 39c92ba8506af9f5d5199fc1dfbcdc9499395493 Mon Sep 17 00:00:00 2001 From: Pixelguy14 Date: Tue, 21 Oct 2025 11:56:15 -0600 Subject: [PATCH] bulk and batch processing for multiple imzml and multiple m/z is now available, folder organization for imzml slices in public folder, JSON tracking of processed datasets to save memory time, multiple performance optimizations and UI bug fixes, metadata viewing for all processed files, comparative view between multiple datasets --- .gitignore | 3 - Manifest.toml | 2 +- Project.toml | 1 + README.md | 2 +- app.jl | 1558 ++++++++++++++++++++--------- app.jl.html | 223 +++-- julia_imzML_visual.jl | 65 +- public/css/genieapp.css | 2 +- public/{ => css}/masks/static.txt | 0 src/MzmlConverter.jl | 56 +- src/imzML.jl | 78 ++ src/mzML.jl | 32 +- 12 files changed, 1437 insertions(+), 585 deletions(-) rename public/{ => css}/masks/static.txt (100%) diff --git a/.gitignore b/.gitignore index 6f882ee..fcda959 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ public/* public/css/imgOver.png !public/css/ -!public/masks/ -public/masks/* -!public/masks/static.txt log/* R_original_scripts/ test/results/ diff --git a/Manifest.toml b/Manifest.toml index 456263d..40c25fa 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.11.7" manifest_format = "2.0" -project_hash = "d4aecbadf6a54893101b3b89b57e8ae0ab391000" +project_hash = "b3d73d786a430f468e0f2f1b3b05da6a3addc7fe" [[deps.ATK_jll]] deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"] diff --git a/Project.toml b/Project.toml index ff9fc31..eed2f9b 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e" GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Libz = "2ec943e9-cfe8-584d-b93d-64dcb6d567b7" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Mmap = "a63ad114-7e13-5084-954f-fe012c677804" diff --git a/README.md b/README.md index 489bd0f..df99a18 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ https://codeberg.org/LabABI/JuliaMSI ~/Downloads/JuliaMSI-main/juliamsi 2. Without entering the Julia environment, launch the project in your terminal with the following command (which works for all operating systems): ``` - julia --threads auto --project=. start_MSI_GUI.jl + julia--project=. start_MSI_GUI.jl ``` 3. After the script has finished loading, you can open a [page](http://127.0.0.1:1481/) in your browser with the web app running. diff --git a/app.jl b/app.jl index a62df52..1737fa9 100644 --- a/app.jl +++ b/app.jl @@ -16,12 +16,161 @@ 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, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile +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 == @@ -36,7 +185,7 @@ include("./julia_imzML_visual.jl") ## Interface non Variables @out btnStartDisable=true @out btnPlotDisable=false - @out btnSpectraDisable=true + @out btnSpectraDisable=false # Loading animations @in progress=false @in progressPlot=false @@ -52,13 +201,16 @@ include("./julia_imzML_visual.jl") ## Interface Variables @in file_route="" @in file_name="" - @in Nmass=0.0 + @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 @@ -74,18 +226,25 @@ include("./julia_imzML_visual.jl") @in imgPlusT=false @in imgMinusT=false # Image change comparative buttons - @in imgPlusComp=false - @in imgMinusComp=false - @in imgPlusTComp=false - @in imgMinusTComp=false + @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 CompTabIDs=["tab0","tab1","tab2","tab3","tab4"] - @out CompTabLabels=["Image", "TrIQ", "Spectrum Plot", "Topography Plot","Surface Plot"] - @in CompSelectedTab="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 @@ -93,10 +252,14 @@ include("./julia_imzML_visual.jl") @out colorbar="/.png" @out colorbarT="/.png" # Interface controlling for the comparative view - @out imgIntComp="/.bmp" # image Interface - @out imgIntTComp="/.bmp" # image Interface TrIQ - @out colorbarComp="/.png" - @out colorbarTComp="/.png" + @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 @@ -114,8 +277,10 @@ include("./julia_imzML_visual.jl") @out msgimg="" @out msgtriq="" # Reiteration of the messages under the image to know which spectra is being visualized - @out msgimgComp="" - @out msgtriqComp="" + @out msgimgCompLeft="" + @out msgtriqCompLeft="" + @out msgimgCompRight="" + @out msgtriqCompRight="" # Centralized MSIData object @out msi_data::Union{MSIData, Nothing} = nothing @@ -125,7 +290,8 @@ include("./julia_imzML_visual.jl") @in showMetadataBtn = false @out metadata_columns = [] @out metadata_rows = [] - @out btnMetadataDisable = true + @out btnMetadataDisable = false + @in selected_folder_metadata = "" # Saves the route where imzML and mzML files are located @out full_route="" @@ -141,6 +307,25 @@ include("./julia_imzML_visual.jl") @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="" @@ -157,10 +342,14 @@ include("./julia_imzML_visual.jl") @out current_triq="" @out current_col_triq="" # We reiterate the process to display in the comparative view - @out current_msiComp="" - @out current_col_msiComp="" - @out current_triqComp="" - @out current_col_triqComp="" + @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() @@ -193,15 +382,19 @@ include("./julia_imzML_visual.jl") @out plotdataImg=[traceImg] @out plotlayoutImg=layoutImg # For the image in the comparative view - @out plotdataImgComp=[traceImg] - @out plotlayoutImgComp=layoutImg + @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 plotdataImgTComp=[traceImg] - @out plotlayoutImgTComp=layoutImg + @out plotdataImgTCompLeft=[traceImg] + @out plotlayoutImgTCompLeft=layoutImg + @out plotdataImgTCompRight=[traceImg] + @out plotlayoutImgTCompRight=layoutImg # Interface Plot Spectrum layoutSpectra=PlotlyBase.Layout( title=PlotlyBase.attr( @@ -225,7 +418,7 @@ include("./julia_imzML_visual.jl") margin=attr(l=0,r=0,t=120,b=0,pad=0) ) # Dummy 2D scatter plot - traceSpectra=PlotlyBase.stem(x=Vector{Float64}(), y=Vector{Float64}(),marker=attr(size=1, color="blue", opacity=0.1)) + 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 @@ -307,82 +500,108 @@ include("./julia_imzML_visual.jl") # 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 - # This part is synchronous and blocking, which is unavoidable picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml") - if isempty(picked_route) - msg = "No file selected." - warning_msg = true return end - # UI updates immediately progress = true msg = "Opening file: $(basename(picked_route))..." - + @async begin try - # --- Normalize file extension and path --- - if endswith(picked_route, "imzml") - full_route = replace(picked_route, r"\.imzml$"i => ".imzML") - mv(picked_route, full_route, force=true) - elseif endswith(picked_route, "mzml") - full_route = replace(picked_route, r"\.mzml$"i => ".mzML") - mv(picked_route, full_route, force=true) - else - full_route = picked_route - end + dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML))$"i => "") + registry = load_registry(registry_path) + existing_entry = get(registry, dataset_name, nothing) - # --- Load data using the new MSIData library --- - sTime = time() - msi_data = OpenMSIData(full_route) - - # --- Pre-compute analytics for performance --- - precompute_analytics(msi_data) - - # --- Prepare metadata for display --- - if msi_data.spectrum_stats_df !== nothing - df = msi_data.spectrum_stats_df + # --- 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) - # Define columns for the key-value summary table - metadata_columns = [ - Dict("name" => "parameter", "label" => "Parameter", "field" => "parameter", "align" => "left"), - Dict("name" => "value", "label" => "Value", "field" => "value", "align" => "left"), - ] - - # Calculate summary statistics - summary_stats = [ - Dict("parameter" => "File Name", "value" => basename(full_route)), - 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))), - ] + full_route = existing_entry["source_path"] + metadata_rows = existing_entry["metadata"]["summary"] - metadata_rows = summary_stats + 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 - w, h = msi_data.image_dims + # --- 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) - fTime = time() - eTime = round(fTime - sTime, digits=3) - msg = "File loaded and indexed in $(eTime) seconds." + 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) - # Enable UI controls - btnStartDisable = !(msi_data.source isa ImzMLSource) + 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 file: $e" + msg = "Error loading active file: $e" warning_msg = true btnStartDisable = true btnSpectraDisable = true @@ -390,7 +609,6 @@ include("./julia_imzML_visual.jl") btnMetadataDisable = true @error "File loading failed" exception=(e, catch_backtrace()) finally - # This block will always run at the end of the async task GC.gc() if Sys.islinux() ccall(:malloc_trim, Int32, (Int32,), 0) @@ -401,8 +619,72 @@ include("./julia_imzML_visual.jl") 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 - showMetadataDialog = true + 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 @@ -469,172 +751,211 @@ include("./julia_imzML_visual.jl") end @onbutton mainProcess @time begin - # UI updates immediately + # --- 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 - text_nmass = replace(string(Nmass), "." => "_") - sTime = time() - - if msi_data === nothing || !(msi_data.source isa ImzMLSource) - msg = "No .imzML file loaded or selected file is not an .imzML. Please select a valid file." + # --- 1. Parameter Validation --- + if isempty(current_selected_files) + progress_message = "No .imzML files in batch. Please add files first." warning_msg = true - elseif Nmass > 0 && Tol > 0 && Tol <= 1 && colorLevel > 1 && colorLevel < 257 - msg = "Creating image for m/z=$(Nmass) Tol=$(Tol). Please be patient." - try - # Use the new get_mz_slice with the centralized MSIData object - println("get_mz_slice time:") - slice = @time get_mz_slice(msi_data, Nmass, Tol) + println(progress_message) + return + end - # Failsafe check for empty slice - if all(iszero, slice) - msg = "No intensity data found for m/z = $Nmass with tolerance = $Tol. The resulting image is black. Please consider using a larger tolerance." - warning_msg = true - - # Generate a black image but skip the colorbar that would crash - timestamp = string(time_ns()) - text_nmass = replace(string(Nmass), "." => "_") - sliceQuant = zeros(UInt8, size(slice)) - - if triqEnabled - @time save_bitmap(joinpath("public", "TrIQ_$(text_nmass).bmp"), sliceQuant, ViridisPalette) - imgIntT = "/TrIQ_$(text_nmass).bmp?t=$(timestamp)" - plotdataImgT, plotlayoutImgT, imgWidth, imgHeight = loadImgPlot(imgIntT) - current_triq = "TrIQ_$(text_nmass).bmp" - msgtriq = "TrIQ image with the Nmass of $(replace(text_nmass, "_" => ".")) (No data)" - colorbarT = "" # Clear colorbar - current_col_triq = "" - triq_bmp = sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")), lt=natural) - selectedTab = "tab1" - else - @time save_bitmap(joinpath("public", "MSI_$(text_nmass).bmp"), sliceQuant, ViridisPalette) - imgInt = "/MSI_$(text_nmass).bmp?t=$(timestamp)" - plotdataImg, plotlayoutImg, imgWidth, imgHeight = loadImgPlot(imgInt) - current_msi = "MSI_$(text_nmass).bmp" - msgimg = "Image with the Nmass of $(replace(text_nmass, "_" => ".")) (No data)" - colorbar = "" # Clear colorbar - current_col_msi = "" - msi_bmp = sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")), lt=natural) - selectedTab = "tab0" - end - fTime = time() - eTime = round(fTime - sTime, digits=3) - # The warning message is already set above - else - # Original path for when data is found - fig = CairoMakie.Figure(size=(150, 250)) # Container - timestamp = string(time_ns()) + println("Nmass value: '$current_nmass'") + println("Type of current_nmass: $(typeof(current_nmass))") - if triqEnabled # If we have TrIQ - if triqProb < 0.8 || triqProb > 1 - msg = "Incorrect TrIQ values, please adjust accordingly and try again." - warning_msg = true - else - println("TrIQ time:") - sliceTriq = @time TrIQ(slice, colorLevel, triqProb) - if MFilterEnabled - sliceTriq = round.(UInt8, median_filter(sliceTriq)) - end + masses_str = split(current_nmass, ',', keepempty=false) + println("Parsed masses strings: $masses_str") - println("save_bitmap time:") - @time save_bitmap(joinpath("public", "TrIQ_$(text_nmass).bmp"), sliceTriq, ViridisPalette) + 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 - imgIntT = "/TrIQ_$(text_nmass).bmp?t=$(timestamp)" - plotdataImgT, plotlayoutImgT, imgWidth, imgHeight = loadImgPlot(imgIntT) - current_triq = "TrIQ_$(text_nmass).bmp" - msgtriq = "TrIQ image with the Nmass of $(replace(text_nmass, "_" => "."))" + println("Masses array: $masses, type: $(typeof(masses))") - colorbar_path = joinpath("public", "colorbar_TrIQ_$(text_nmass).png") - println("generate_colorbar_image time:") - @time generate_colorbar_image(slice, colorLevel, colorbar_path, use_triq=true, triq_prob=triqProb) - colorbarT = "/colorbar_TrIQ_$(text_nmass).png?t=$(timestamp)" - current_col_triq = "colorbar_TrIQ_$(text_nmass).png" + 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 - 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) + # 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 - fTime = time() - eTime = round(fTime - sTime, digits=3) - msg = "The TrIQ image has been created in $(eTime) seconds successfully inside the 'public' folder of the app" - selectedTab = "tab1" - end - else # If we don't use TrIQ - println("quantize_intensity time:") - sliceQuant = @time quantize_intensity(slice, colorLevel) - if MFilterEnabled - sliceQuant = round.(UInt8, median_filter(sliceQuant)) - 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("save_bitmap time:") - @time save_bitmap(joinpath("public", "MSI_$(text_nmass).bmp"), sliceQuant, ViridisPalette) + 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[] - imgInt = "/MSI_$(text_nmass).bmp?t=$(timestamp)" - plotdataImg, plotlayoutImg, imgWidth, imgHeight = loadImgPlot(imgInt) - current_msi = "MSI_$(text_nmass).bmp" - msgimg = "Image with the Nmass of $(replace(text_nmass, "_" => "."))" + 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 + ) - colorbar_path = joinpath("public", "colorbar_MSI_$(text_nmass).png") - println("generate_colorbar_image time:") - @time generate_colorbar_image(slice, colorLevel, colorbar_path) - colorbar = "/colorbar_MSI_$(text_nmass).png?t=$(timestamp)" - current_col_msi = "colorbar_MSI_$(text_nmass).png" + # Process the file + success, error_msg = process_file_safely(file_path, masses, all_params, progress_message, overall_progress) - 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) - selectedTab = "tab0" - fTime = time() - eTime = round(fTime - sTime, digits=3) - msg = "The image has been created in $(eTime) seconds successfully inside the 'public' folder of the app" - end - end - catch e - msg = "There was an error creating the image: $e" - warning_msg = true - @error "Image creation failed" exception=(e, catch_backtrace()) + 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 = "Invalid parameters. Nmass, Tol, or colorLevel are incorrect." + msg = "Batch completed in $(total_time_end) seconds with $(total_errors) error(s)." warning_msg = true - @error msg 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 - # This block will always run at the end of the async task - GC.gc() - if Sys.islinux() - ccall(:malloc_trim, Int32, (Int32,), 0) - end + # --- UI State Reset --- + progress = false btnStartDisable = false btnPlotDisable = false btnOpticalDisable = false - progress = false btnSpectraDisable = false SpectraEnabled = true + overall_progress = 0.0 + println("Done") end end end @onbutton createMeanPlot begin - if msi_data === nothing - msg = "No data loaded. Please select a file first." + if isempty(selected_folder_main) + msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end - # UI updates immediately progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true - msg = "Loading plot..." + msg = "Loading plot for $(selected_folder_main)..." @async begin try sTime = time() - plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data) - + 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) @@ -644,35 +965,53 @@ include("./julia_imzML_visual.jl") warning_msg = true @error "Mean spectrum plotting failed" exception=(e, catch_backtrace()) finally - # This runs after the async task is finished progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false - if msi_data !== nothing && msi_data.source isa ImzMLSource - btnStartDisable = false - end + btnStartDisable = false end end end @onbutton createSumPlot begin - if msi_data === nothing - msg = "No data loaded. Please select a file first." + if isempty(selected_folder_main) + msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end - # UI updates immediately progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true - msg = "Loading total spectrum plot..." + msg = "Loading total spectrum plot for $(selected_folder_main)..." @async begin try sTime = time() - plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data) - + 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) @@ -682,44 +1021,58 @@ include("./julia_imzML_visual.jl") warning_msg = true @error "Total spectrum plotting failed" exception=(e, catch_backtrace()) finally - # This runs after the async task is finished progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false - if msi_data !== nothing && msi_data.source isa ImzMLSource - btnStartDisable = false - end + btnStartDisable = false end end end @onbutton createXYPlot begin - if msi_data === nothing - msg = "No data loaded. Please select a file first." + if isempty(selected_folder_main) + msg = "No dataset selected. Please process a file and select a folder first." warning_msg = true return end - # UI updates immediately progressSpectraPlot = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true - msg = "Loading plot..." + msg = "Loading plot for $(selected_folder_main)..." @async begin try sTime = time() - # The UI uses negative Y values, so we adjust before calling the plot function + 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) - - # Update UI coordinates + 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" @@ -728,249 +1081,460 @@ include("./julia_imzML_visual.jl") warning_msg = true @error "Spectrum plotting failed" exception=(e, catch_backtrace()) finally - # This runs after the async task is finished progressSpectraPlot = false btnPlotDisable = false btnSpectraDisable = false - if msi_data !== nothing && msi_data.source isa ImzMLSource - btnStartDisable = false - end + btnStartDisable = false end end end - # Image loaders based on the position of the current image (increment and decrement for both normal and filter) - # And a pre-generated list from all image files from /public folder + # --- Main View Handlers --- @onbutton imgMinus begin - # Append a query string to force the image to refresh - timestamp=string(time_ns()) - # Update the array of images listed in the public folder - 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) + 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="/$(current_msi)?t=$(timestamp)" - colorbar="/$(current_col_msi)?t=$(timestamp)" - - text_nmass=replace(current_msi, "MSI_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgimg="Image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImg=[traceImg] - plotlayoutImg=layoutImg - msgimg="" + 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 - # Append a query string to force the image to refresh + if isempty(selected_folder_main) return end timestamp=string(time_ns()) - # Update the array of images listed in the public folder - 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) + 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="/$(current_msi)?t=$(timestamp)" - colorbar="/$(current_col_msi)?t=$(timestamp)" - - text_nmass=replace(current_msi, "MSI_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgimg="Image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImg, plotlayoutImg, imgWidth, imgHeight=loadImgPlot(imgInt) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImg=[traceImg] - plotlayoutImg=layoutImg - msgimg="" + 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 - # Append a query string to force the image to refresh - timestamp=string(time_ns()) - # Update the array of images with TrIQ filter listed in the public folder - 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) + 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="/$(current_triq)?t=$(timestamp)" - colorbarT="/$(current_col_triq)?t=$(timestamp)" - - text_nmass=replace(current_triq, "TrIQ_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgtriq="TrIQ image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImgT=[traceImg] - plotlayoutImgT=layoutImg - msgtriq="" + 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 - # Append a query string to force the image to refresh - timestamp=string(time_ns()) - # Update the array of images with TrIQ filter listed in the public folder - 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) + 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="/$(current_triq)?t=$(timestamp)" - colorbarT="/$(current_col_triq)?t=$(timestamp)" - - text_nmass=replace(current_triq, "TrIQ_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgtriq="TrIQ image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImgT, plotlayoutImgT, imgWidth, imgHeight=loadImgPlot(imgIntT) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImgT=[traceImg] - plotlayoutImgT=layoutImg - msgtriq="" + 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 - # Image loaders for the comparative view - @onbutton imgMinusComp begin - # Append a query string to force the image to refresh - timestamp=string(time_ns()) - # Update the array of images listed in the public folder - 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) - - new_msi=decrement_image(current_msiComp, msi_bmp) - new_col_msi=decrement_image(current_col_msiComp, col_msi_png) - if new_msi!=nothing || new_col_msi!=nothing - current_msiComp=new_msi - current_col_msiComp=new_col_msi - imgIntComp="/$(current_msiComp)?t=$(timestamp)" - colorbarComp="/$(current_col_msiComp)?t=$(timestamp)" - - text_nmass=replace(current_msiComp, "MSI_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgimgComp="Image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImgComp, plotlayoutImgComp, _, _=loadImgPlot(imgIntComp) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImgComp=[traceImg] - plotlayoutImgComp=layoutImg - msgimgComp="" - end - end - - @onbutton imgPlusComp begin - # Append a query string to force the image to refresh + # --- Compare View Handlers --- + @onbutton imgMinusCompLeft begin + if isempty(selected_folder_compare_left) return end timestamp=string(time_ns()) - # Update the array of images listed in the public folder - 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) + 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_msiComp, msi_bmp) - new_col_msi=increment_image(current_col_msiComp, col_msi_png) - if new_msi!=nothing || new_col_msi!=nothing - current_msiComp=new_msi - current_col_msiComp=new_col_msi - imgIntComp="/$(current_msiComp)?t=$(timestamp)" - colorbarComp="/$(current_col_msiComp)?t=$(timestamp)" - - text_nmass=replace(current_msiComp, "MSI_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgimgComp="Image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImgComp, plotlayoutImgComp, _, _=loadImgPlot(imgIntComp) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImgComp=[traceImg] - plotlayoutImgComp=layoutImg - msgimgComp="" + 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 imgMinusTComp begin - # Append a query string to force the image to refresh - timestamp=string(time_ns()) - # Update the array of images with TrIQ filter listed in the public folder - 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) - - new_msi=decrement_image(current_triqComp, triq_bmp) - new_col_msi=decrement_image(current_col_triqComp, col_triq_png) - if new_msi!=nothing || new_col_msi!=nothing - current_triqComp=new_msi - current_col_triqComp=new_col_msi - imgIntTComp="/$(current_triqComp)?t=$(timestamp)" - colorbarTComp="/$(current_col_triqComp)?t=$(timestamp)" - - text_nmass=replace(current_triqComp, "TrIQ_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgtriqComp="TrIQ image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImgTComp, plotlayoutImgTComp, _, _=loadImgPlot(imgIntTComp) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImgTComp=[traceImg] - plotlayoutImgTComp=layoutImg - msgtriqComp="" + + @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 imgPlusTComp begin - # Append a query string to force the image to refresh - timestamp=string(time_ns()) - # Update the array of images with TrIQ filter listed in the public folder - 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) - - new_msi=increment_image(current_triqComp, triq_bmp) - new_col_msi=increment_image(current_col_triqComp, col_triq_png) - if new_msi!=nothing || new_col_msi!=nothing - current_triqComp=new_msi - current_col_triqComp=new_col_msi - imgIntTComp="/$(current_triqComp)?t=$(timestamp)" - colorbarTComp="/$(current_col_triqComp)?t=$(timestamp)" - - text_nmass=replace(current_triqComp, "TrIQ_" => "") - text_nmass=replace(text_nmass, ".bmp" => "") - msgtriqComp="TrIQ image with the Nmass of $(replace(text_nmass, "_" => "."))" - # Process the image in the function - plotdataImgTComp, plotlayoutImgTComp, _, _=loadImgPlot(imgIntTComp) - btnOpticalDisable=false - else - traceImg=PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}()) - plotdataImgTComp=[traceImg] - plotlayoutImgTComp=layoutImg - msgtriqComp="" + + @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 @@ -1161,13 +1725,49 @@ include("./julia_imzML_visual.jl") # To include a visualization in the spectrum plot indicating where is the selected mass @onchange Nmass begin if !isempty(xSpectraMz) - # Use a stem plot for the main spectrum for consistency - traceSpectra = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}", showlegend=false) + # 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 + ) - # Keep this as a scatter plot to draw the vertical line - trace2 = PlotlyBase.scatter(x=[Nmass, Nmass], y=[0, maximum(ySpectraMz)], mode="lines", line=attr(color="red", width=0.5), name="m/z selected", 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 - plotdata = [traceSpectra, trace2] + 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 @@ -1258,6 +1858,67 @@ include("./julia_imzML_visual.jl") @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 @@ -1267,40 +1928,3 @@ end # Register a new route and the page that will be loaded on access @page("/", "app.jl.html") end - -#= -function __init__() - @async begin - println("Pre-compiling functions at startup...") - - # Create a dummy MSIData object to be used for pre-compilation - dummy_source = ImzMLSource("dummy.ibd", Float32, Float32) - dummy_meta = MSI_src.SpectrumMetadata(0,0,"",MSI_src.UNKNOWN, MSI_src.SpectrumAsset(Float32,false,0,0,:mz), MSI_src.SpectrumAsset(Float32,false,0,0,:intensity)) - dummy_msi_data = MSIData(dummy_source, [dummy_meta], (1,1), zeros(Int,1,1), 0) - - # Pre-compile functions from btnSearch - try OpenMSIData("dummy.imzML") catch end - try precompute_analytics(dummy_msi_data) catch end - - # Pre-compile functions from mainProcess - try get_mz_slice(dummy_msi_data, 1.0, 1.0) catch end - try TrIQ(zeros(10,10), 256, 0.98) catch end - try quantize_intensity(zeros(10,10), 256) catch end - - dummy_bmp_path = joinpath("public", "dummy.bmp") - dummy_png_path = joinpath("public", "dummy.png") - try - save_bitmap(dummy_bmp_path, zeros(UInt8, 10, 10), ViridisPalette) - loadImgPlot("/dummy.bmp") - generate_colorbar_image(zeros(10,10), 256, dummy_png_path) - catch e - @warn "Pre-compilation step failed (this is expected if dummy files can't be created/read)" - finally - rm(dummy_bmp_path, force=true) - rm(dummy_png_path, force=true) - end - - println("Pre-compilation finished.") - end -end -=# diff --git a/app.jl.html b/app.jl.html index 7eb8234..ad97d23 100644 --- a/app.jl.html +++ b/app.jl.html @@ -17,19 +17,42 @@
imzML & mzML Data Processor

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

- - - - +
It may take a while to generate the slice / spectrum, please be patient. +
To generate the contour or surface plots, you have to select the desired slice first using the interface.

+
+ + + + + +
+ + + + {{ file }} + + + + + + + +
- + +
- - - - - -
-

{{msg}}

- + + + + + + +
+ +
{{ progress_message }}
+
+
+

{{msg}}

@@ -177,13 +204,13 @@ @@ -205,9 +232,10 @@ -
Image visualizer
-
+
+ +
@@ -221,14 +249,15 @@
-

{{msgimg}}

+

-
TrIQ visualizer
-
+
+ +
@@ -242,11 +271,38 @@
-

{{msgtriq}}

+

- +
+ + + + + + + + Mean spectrum plot + + + + + Sum Spectrum plot + + + + + Spectrum plot (X,Y) + + + + +
@@ -281,8 +337,8 @@ - -
Compare two diferent images or plots
+ +
Compare two different images or plots
Transparency: {{ imgTrans }} @@ -294,46 +350,50 @@
- - +
+ + + +
+
- - + +
- +
- +
-

{{msgimg}}

+

- - + +
- +
- +
-

{{msgtriq}}

+

@@ -350,51 +410,54 @@
- - +
+ + + +
+
- + + v-on:click="imgPlusCompRight=true">
-
- +
-

{{msgimgComp}}

+

- + + v-on:click="imgPlusTCompRight=true">
-
- +
-

{{msgtriqComp}}

+

@@ -419,23 +482,53 @@ + + + +
Batch Process Summary
+
+ + +
{{ batch_summary }}
+
+ + + + +
+
+ - +
Dataset Summary
+ +
- + - {{ row.parameter }} + {{ row.parameter }} - {{ row.value }} + {{ row.value }} +
+ +

No metadata found in registry for this dataset.

+

You may need to process the file first.

+
diff --git a/julia_imzML_visual.jl b/julia_imzML_visual.jl index 2d7fbeb..439cd14 100644 --- a/julia_imzML_visual.jl +++ b/julia_imzML_visual.jl @@ -452,10 +452,11 @@ end # returns the layout and data for the surface plotly plot # this function loads the spectra data and makes a mean to display # its values in the spectrum plot -function meanSpectrumPlot(data::MSIData) +function meanSpectrumPlot(data::MSIData, dataset_name::String="") + title_text = isempty(dataset_name) ? "Average Spectrum Plot" : "Average Spectrum for: $dataset_name" layout = PlotlyBase.Layout( title=PlotlyBase.attr( - text="Average Spectrum Plot", + text=title_text, font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, @@ -480,9 +481,9 @@ function meanSpectrumPlot(data::MSIData) if isempty(xSpectraMz) @warn "Average spectrum is empty." - trace = PlotlyBase.stem(x=Float64[], y=Float64[]) + trace = PlotlyBase.scatter(x=Float64[], y=Float64[]) else - trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x",hovertemplate="m/z: %{x:.4f}") + trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x",hovertemplate="m/z: %{x:.4f}") end @@ -491,7 +492,7 @@ function meanSpectrumPlot(data::MSIData) return plotdata, plotlayout, xSpectraMz, ySpectraMz end -function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int) +function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int, dataset_name::String="") local mz::AbstractVector, intensity::AbstractVector local plot_title::String @@ -502,24 +503,24 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, x = clamp(xCoord, 1, imgWidth) y = clamp(yCoord, 1, imgHeight) - # mz, intensity = GetSpectrum(data, Int(x), Int(y)) process_spectrum(data, Int(x), Int(y)) do recieved_mz, recieved_intensity mz = recieved_mz intensity = recieved_intensity end - plot_title = "Spectrum at ($x, $y)" + base_title = "Spectrum at ($x, $y)" else # For non-imaging data, treat xCoord as the spectrum index index = clamp(xCoord, 1, length(data.spectra_metadata)) - # mz, intensity = GetSpectrum(data, index) process_spectrum(data, index) do recieved_mz, recieved_intensity mz = recieved_mz intensity = recieved_intensity end - plot_title = "Spectrum #$index" + base_title = "Spectrum #$index" end + plot_title = isempty(dataset_name) ? base_title : "$base_title for: $dataset_name" + layout = PlotlyBase.Layout( title=PlotlyBase.attr( text=plot_title, @@ -545,7 +546,7 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, # Downsample for plotting performance mz_down, int_down = MSI_src.downsample_spectrum(mz, intensity) - trace = PlotlyBase.stem(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}") + trace = PlotlyBase.scatter(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="m/z: %{x:.4f}") plotdata = [trace] plotlayout = layout @@ -553,10 +554,11 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, return plotdata, plotlayout, mz, intensity end -function sumSpectrumPlot(data::MSIData) +function sumSpectrumPlot(data::MSIData, dataset_name::String="") + title_text = isempty(dataset_name) ? "Total Spectrum Plot" : "Total Spectrum for: $dataset_name" layout = PlotlyBase.Layout( title=PlotlyBase.attr( - text="Total Spectrum Plot", + text=title_text, font=PlotlyBase.attr( family="Roboto, Lato, sans-serif", size=18, @@ -581,12 +583,47 @@ function sumSpectrumPlot(data::MSIData) if isempty(xSpectraMz) @warn "Total spectrum is empty." - trace = PlotlyBase.stem(x=Float64[], y=Float64[]) + trace = PlotlyBase.scatter(x=Float64[], y=Float64[]) else - trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x",hovertemplate="m/z: %{x:.4f}") + trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x",hovertemplate="m/z: %{x:.4f}") end plotdata = [trace] plotlayout = layout return plotdata, plotlayout, xSpectraMz, ySpectraMz end + +function warmup_init() + @async begin + println("Pre-compiling functions at startup...") + + # Create a dummy MSIData object to be used for pre-compilation + # dummy_source = ImzMLSource("dummy.ibd", Float32, Float32) + # dummy_meta = MSI_src.SpectrumMetadata(0,0,"",MSI_src.UNKNOWN, MSI_src.SpectrumAsset(Float32,false,0,0,:mz), MSI_src.SpectrumAsset(Float32,false,0,0,:intensity)) + # dummy_msi_data = MSIData(dummy_source, [dummy_meta], (1,1), zeros(Int,1,1), 0) + + # Pre-compile functions from btnSearch + # try OpenMSIData("dummy.imzML") catch end + # try precompute_analytics(dummy_msi_data) catch end + + # Pre-compile functions from mainProcess + # try get_mz_slice(dummy_msi_data, 1.0, 1.0) catch end + try TrIQ(zeros(10,10), 256, 0.98) catch end + try quantize_intensity(zeros(10,10), 256) catch end + + dummy_bmp_path = joinpath("public", "dummy.bmp") + dummy_png_path = joinpath("public", "dummy.png") + try + save_bitmap(dummy_bmp_path, zeros(UInt8, 10, 10), ViridisPalette) + loadImgPlot("/dummy.bmp") + generate_colorbar_image(zeros(10,10), 256, dummy_png_path) + catch e + @warn "Pre-compilation step failed (this is expected if dummy files can't be created/read)" + finally + rm(dummy_bmp_path, force=true) + rm(dummy_png_path, force=true) + end + + println("Pre-compilation finished.") + end +end \ No newline at end of file diff --git a/public/css/genieapp.css b/public/css/genieapp.css index e3779af..f50c1f7 100644 --- a/public/css/genieapp.css +++ b/public/css/genieapp.css @@ -82,7 +82,7 @@ } #intDivStyle-left .q-tab-panels { - height: 670px; /* Set this to accommodate your tallest content */ + height: 700px; /* Set this to accommodate your tallest content */ } #intDivStyle-left .q-tab-panel { diff --git a/public/masks/static.txt b/public/css/masks/static.txt similarity index 100% rename from public/masks/static.txt rename to public/css/masks/static.txt diff --git a/src/MzmlConverter.jl b/src/MzmlConverter.jl index d2997d6..4142c41 100644 --- a/src/MzmlConverter.jl +++ b/src/MzmlConverter.jl @@ -497,7 +497,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing open(target_ibd_file, "w") do ibd_stream write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder end - return BinaryMetadata[], Tuple{Int, Int}[], (0, 0) + return BinaryMetadata[], Tuple{Int, Int}[], (0, 0), UNKNOWN end width = maximum(timing_matrix[:, 1]) @@ -505,6 +505,11 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing msi_data = OpenMSIData(source_file) + source_mode = UNKNOWN + if !isempty(msi_data.spectra_metadata) + source_mode = msi_data.spectra_metadata[1].mode + end + scan_time_deltas = zeros(Int64, size(scans, 1)) if size(scans, 1) > 1 for i in 1:(size(scans, 1) - 1) @@ -553,7 +558,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing # Write m/z array mz_offset = position(ibd_stream) for val in mz - write(ibd_stream, htol(Float64(val))) + write(ibd_stream, htol(Float32(val))) end mz_length = position(ibd_stream) - mz_offset @@ -569,7 +574,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing end @info "Found and processed $empty_pixel_count empty pixels out of $(size(timing_matrix, 1)) total." - return binary_meta_vec, coords_vec, (width, height) + return binary_meta_vec, coords_vec, (width, height), source_mode end """ @@ -589,7 +594,7 @@ experiment and data format. # Returns - `true` on success, `false` on failure. """ -function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, coords::Vector{Tuple{Int, Int}}, dims::Tuple{Int, Int}) +function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, coords::Vector{Tuple{Int, Int}}, dims::Tuple{Int, Int}, mode::SpectrumMode) ibd_file = replace(target_file, r"\.imzML$"i => ".ibd") if isempty(binary_meta) @@ -604,8 +609,7 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c open(target_file, "w") do imzml_stream # XML Header write(imzml_stream, """ - - + """) # CV List, File Description, etc. (static parts) @@ -627,13 +631,11 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c - - + - @@ -699,13 +701,20 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c push!(spectrum_offsets, spectrum_start) # Calculate number of points from byte length - mz_points = meta.mz_length ÷ sizeof(Float64) + mz_points = meta.mz_length ÷ sizeof(Float32) int_points = meta.int_length ÷ sizeof(Float32) write(imzml_stream, """ - - +""") + if mode == CENTROID + write(imzml_stream, """ +""") + else + write(imzml_stream, """ +""") + end + write(imzml_stream, """ @@ -714,16 +723,14 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c - - + - - + @@ -734,19 +741,6 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c write(imzml_stream, """ -""") - # Index List - index_list_start = position(imzml_stream) - write(imzml_stream, """ - -""") - for (i, offset) in enumerate(spectrum_offsets) - write(imzml_stream, " $offset\n") - end - write(imzml_stream, """ - - $index_list_start - """) end @@ -784,13 +778,13 @@ function ImportMzmlFile(source_file::String, sync_file::String, target_file::Str println("Step 3: Converting spectra and writing .ibd file...") ibd_file = replace(target_file, r"\.imzML$"i => ".ibd") - binary_meta, coords, (width, height) = ConvertMzmlToImzml(source_file, ibd_file, timing_matrix, scans) + binary_meta, coords, (width, height), source_mode = ConvertMzmlToImzml(source_file, ibd_file, timing_matrix, scans) # Flip image vertically to match R script output flipped_coords = [(x, height - y + 1) for (x, y) in coords] println("Step 4: Exporting .imzML metadata file...") - success = ExportImzml(target_file, binary_meta, flipped_coords, (width, height)) + success = ExportImzml(target_file, binary_meta, flipped_coords, (width, height), source_mode) if success println("Conversion successful: $target_file") diff --git a/src/imzML.jl b/src/imzML.jl index 9b74f63..6c3b48a 100644 --- a/src/imzML.jl +++ b/src/imzML.jl @@ -793,6 +793,84 @@ function get_mz_slice(data::MSIData, mass::Real, tolerance::Real) end +""" + get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real) + +Extracts multiple image slices for a given list of m/z values in a single pass. +This is a highly performant function that iterates through the full dataset only once. + +# Returns +- A `Dict{Real, Matrix{Float64}}` mapping each mass to its intensity slice matrix. +""" +function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real) + width, height = data.image_dims + + # 1. Initialize a dictionary to hold the output slice matrices + slice_dict = Dict{Real, Matrix{Float64}}() + for mass in masses + slice_dict[mass] = zeros(Float64, height, width) + end + + # 2. Ensure analytics are computed for filtering. + if data.spectrum_stats_df === nothing || !hasproperty(data.spectrum_stats_df, :MinMZ) + println("Per-spectrum metadata not found. Running one-time analytics computation...") + precompute_analytics(data) + end + + println("Filtering candidate spectra for $(length(masses)) m/z values...") + stats_df = data.spectrum_stats_df + candidate_indices = Set{Int}() + + # 3. Find all spectra that could contain *any* of the requested masses. + for mass in masses + target_min = mass - tolerance + target_max = mass + tolerance + for i in 1:length(data.spectra_metadata) + # If already a candidate, no need to check again + if i in candidate_indices + continue + end + spec_min_mz = stats_df.MinMZ[i] + spec_max_mz = stats_df.MaxMZ[i] + if target_max >= spec_min_mz && target_min <= spec_max_mz + push!(candidate_indices, i) + end + end + end + + println("Found $(length(candidate_indices)) total candidate spectra.") + + # 4. Iterate through the data a single time using the optimized iterator. + _iterate_spectra_fast(data) do idx, mz_array, intensity_array + # Process only the spectra that are candidates + if idx in candidate_indices + meta = data.spectra_metadata[idx] + # For this single spectrum, check all masses of interest + for mass in masses + # Check if this spectrum's range actually covers the current mass + # This is a finer-grained check than the initial filtering + if !isempty(mz_array) && (mass + tolerance) >= first(mz_array) && (mass - tolerance) <= last(mz_array) + intensity = find_mass(mz_array, intensity_array, mass, tolerance) + if intensity > 0.0 + if 1 <= meta.x <= width && 1 <= meta.y <= height + slice_dict[mass][meta.y, meta.x] = intensity + end + end + end + end + end + end + + # 5. Clean up and return + for mass in masses + replace!(slice_dict[mass], NaN => 0.0) + end + + println("Finished generating $(length(masses)) slices in a single pass.") + return slice_dict +end + + """ plot_slice(msi_data::MSIData, mass::Float64, tolerance::Float64, output_dir::String; stage_name="slice", bins=256) diff --git a/src/mzML.jl b/src/mzML.jl index 8413845..aac75e9 100644 --- a/src/mzML.jl +++ b/src/mzML.jl @@ -103,9 +103,37 @@ the m/z and intensity array metadata. function parse_spectrum_metadata(stream::IO, offset::Int64) seek(stream, offset) - id_match = find_tag(stream, r"", line) + break + end + end + spectrum_xml = String(take!(spectrum_buffer)) + seek(stream, spectrum_start_pos) # Reset for other parsing + + id_match = match(r"