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 # Bring MSIData into App module's scope using .MSI_src: MSIData, OpenMSIData, GetSpectrum, 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 include("./julia_imzML_visual.jl") @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=true # 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 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 imgPlusComp=false @in imgMinusComp=false @in imgPlusTComp=false @in imgMinusTComp=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" # 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 imgIntComp="/.bmp" # image Interface @out imgIntTComp="/.bmp" # image Interface TrIQ @out colorbarComp="/.png" @out colorbarTComp="/.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 msgimgComp="" @out msgtriqComp="" # 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 = true # Saves the route where imzML and mzML files are located @out full_route="" # 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_msiComp="" @out current_col_msiComp="" @out current_triqComp="" @out current_col_triqComp="" ## 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 plotdataImgComp=[traceImg] @out plotlayoutImgComp=layoutImg # For triq image @out plotdataImgT=[traceImg] @out plotlayoutImgT=layoutImg # For the triq image in the comparative view @out plotdataImgTComp=[traceImg] @out plotlayoutImgTComp=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.stem(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 @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 # --- 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 # 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))), ] metadata_rows = summary_stats btnMetadataDisable = false end w, h = msi_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." # Enable UI controls btnStartDisable = !(msi_data.source isa ImzMLSource) btnPlotDisable = false btnSpectraDisable = false SpectraEnabled = true catch e msi_data = nothing msg = "Error loading file: $e" warning_msg = true btnStartDisable = true btnSpectraDisable = true SpectraEnabled = false 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) end progress = false progressSpectraPlot = false end end end @onbutton showMetadataBtn begin showMetadataDialog = true end @onbutton mainProcess @time begin # UI updates immediately progress = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true @async begin 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." 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) # 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()) 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 println("save_bitmap time:") @time save_bitmap(joinpath("public", "TrIQ_$(text_nmass).bmp"), sliceTriq, 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, "_" => "."))" 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" 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) 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 println("save_bitmap time:") @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, "_" => "."))" 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" 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()) end else msg = "Invalid parameters. Nmass, Tol, or colorLevel are incorrect." warning_msg = true @error msg end 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 btnStartDisable = false btnPlotDisable = false btnOpticalDisable = false progress = false btnSpectraDisable = false SpectraEnabled = true end end end @onbutton createMeanPlot begin if msi_data === nothing msg = "No data loaded. Please select a file first." warning_msg = true return end # UI updates immediately progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true msg = "Loading plot..." @async begin try sTime = time() plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data) 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 # 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 end end end @onbutton createSumPlot begin if msi_data === nothing msg = "No data loaded. Please select a file first." warning_msg = true return end # UI updates immediately progressSpectraPlot = true btnPlotDisable = true btnStartDisable = true msg = "Loading total spectrum plot..." @async begin try sTime = time() plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data) 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 # 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 end end end @onbutton createXYPlot begin if msi_data === nothing msg = "No data loaded. Please select a file first." warning_msg = true return end # UI updates immediately progressSpectraPlot = true btnStartDisable = true btnPlotDisable = true btnSpectraDisable = true msg = "Loading plot..." @async begin try sTime = time() # The UI uses negative Y values, so we adjust before calling the plot function y = yCoord < 0 ? abs(yCoord) : yCoord plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y, imgWidth, imgHeight) # Update UI coordinates 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 # 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 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 @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) 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="" end end @onbutton imgPlus 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=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="" 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) 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="" 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) 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="" 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 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=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="" 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="" 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="" 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) # 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) # 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) plotdata = [traceSpectra, trace2] 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() 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 #= 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 =#