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

This commit is contained in:
Pixelguy14 2025-10-21 11:56:15 -06:00
parent 39fe2bf52c
commit 39c92ba850
12 changed files with 1437 additions and 585 deletions

3
.gitignore vendored
View File

@ -1,9 +1,6 @@
public/* public/*
public/css/imgOver.png public/css/imgOver.png
!public/css/ !public/css/
!public/masks/
public/masks/*
!public/masks/static.txt
log/* log/*
R_original_scripts/ R_original_scripts/
test/results/ test/results/

View File

@ -2,7 +2,7 @@
julia_version = "1.11.7" julia_version = "1.11.7"
manifest_format = "2.0" manifest_format = "2.0"
project_hash = "d4aecbadf6a54893101b3b89b57e8ae0ab391000" project_hash = "b3d73d786a430f468e0f2f1b3b05da6a3addc7fe"
[[deps.ATK_jll]] [[deps.ATK_jll]]
deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"] deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"]

View File

@ -13,6 +13,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e" Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e"
GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353" GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353"
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Libz = "2ec943e9-cfe8-584d-b93d-64dcb6d567b7" Libz = "2ec943e9-cfe8-584d-b93d-64dcb6d567b7"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Mmap = "a63ad114-7e13-5084-954f-fe012c677804" Mmap = "a63ad114-7e13-5084-954f-fe012c677804"

View File

@ -22,7 +22,7 @@ https://codeberg.org/LabABI/JuliaMSI
~/Downloads/JuliaMSI-main/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): 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. 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.

1512
app.jl

File diff suppressed because it is too large Load Diff

View File

@ -17,19 +17,42 @@
<q-tab-panel name="generator"> <q-tab-panel name="generator">
<div class="text-h6">imzML & mzML Data Processor</div> <div class="text-h6">imzML & mzML Data Processor</div>
<p>Please make sure the ibd and imzML file are located in the same directory and have the same name. <p>Please make sure the ibd and imzML file are located in the same directory and have the same name.
<br>It may take a while to generate the image and the plot, please be patient. <br>It may take a while to generate the slice / spectrum, please be patient.
<br>To generate the contour or surface plots, you have to select the desired image first using the interface.</p> <br>To generate the contour or surface plots, you have to select the desired slice first using the interface.</p>
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="full_route" readonly label="Select your imzML or mzML file" v-on:click="btnSearch=true"> <div class="row items-center">
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer col"
v-model="full_route" readonly
:label="batch_file_count > 0 ? batch_file_count + ' file(s) in batch' : 'Select an imzML file'"
v-on:click="btnSearch=true">
<template v-slot:append> <template v-slot:append>
<q-icon name="search" v:onclick="btnSearch=true" class="cursor-pointer" /> <q-icon name="search" v-on:click="btnSearch=true" class="cursor-pointer" />
</template> </template>
</q-input> </q-input>
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add"></q-btn>
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true"
:disable="batch_file_count === 0" label="Clear"></q-btn>
</div>
<q-list bordered separator v-if="selected_files.length > 0">
<q-item v-for="(file, index) in selected_files" :key="index">
<q-item-section>
{{ file }}
</q-item-section>
<q-item-section side>
<q-btn flat round icon="delete" size="sm" v-on:click="selected_files.splice(index, 1)"></q-btn>
</q-item-section>
</q-item>
</q-list>
<!-- Variable Manipulation --> <!-- Variable Manipulation -->
<div class="row"> <div class="row">
<div class="st-col col-4 col-sm q-ma-sm"> <div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textNmass" step="0.001" v-model="Nmass" <q-input standout="custom-standout" id="textNmass" v-model="Nmass"
label="Mass-to-charge ratio of interest" type="number" label="Mass-to-charge ratio(s) of interest" type="text"
:rules="[ val => !!val || '* Required', val => val >= 0.0 || 'Need positive mass values']"></q-input> :rules="[
val => !!val || '* Required',
val => val.split(',').every(m => !isNaN(parseFloat(m.trim())) && parseFloat(m.trim()) > 0) || 'Need comma-separated positive numbers'
]">
</q-input>
</div> </div>
<div class="st-col col-4 col-sm q-ma-sm"> <div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol" <q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol"
@ -113,11 +136,15 @@
</q-btn> </q-btn>
<q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm" <q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm"
label="Compare"></q-btn> label="Compare"></q-btn>
<q-btn class="q-ma-sm btn-style" :disable="btnMetadataDisable" <!--<q-btn class="q-ma-sm btn-style" :disable="btnMetadataDisable"
v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn> v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>-->
<q-btn class="q-ma-sm btn-style" v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>
<div class="q-pa-md row items-center" v-show="progress">
<q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner>
<div class="text-caption">{{ progress_message }}</div>
</div>
</div> </div>
<p>{{msg}}</p> <p>{{msg}}</p>
<div class="row st-col col-12"> <div class="row st-col col-12">
<q-btn-dropdown class="q-ma-sm btn-style" :loading="progressPlot" :disable="btnPlotDisable" <q-btn-dropdown class="q-ma-sm btn-style" :loading="progressPlot" :disable="btnPlotDisable"
label="Generate Plots" icon="play_arrow"> label="Generate Plots" icon="play_arrow">
@ -177,13 +204,13 @@
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="mzml_full_route" readonly label="Select your .mzML file" v-on:click="btnSearchMzml=true"> <q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="mzml_full_route" readonly label="Select your .mzML file" v-on:click="btnSearchMzml=true">
<template v-slot:append> <template v-slot:append>
<q-icon name="search" v:onclick="btnSearchMzml=true" class="cursor-pointer" /> <q-icon name="search" v-on:click="btnSearchMzml=true" class="cursor-pointer" />
</template> </template>
</q-input> </q-input>
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="sync_full_route" readonly label="Select your .txt sync file" v-on:click="btnSearchSync=true"> <q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="sync_full_route" readonly label="Select your .txt sync file" v-on:click="btnSearchSync=true">
<template v-slot:append> <template v-slot:append>
<q-icon name="search" v:onclick="btnSearchSync=true" class="cursor-pointer" /> <q-icon name="search" v-on:click="btnSearchSync=true" class="cursor-pointer" />
</template> </template>
</q-input> </q-input>
@ -205,9 +232,10 @@
<q-tab-panels v-model="selectedTab"> <q-tab-panels v-model="selectedTab">
<q-tab-panel name="tab0"> <q-tab-panel name="tab0">
<!-- Content for Tab 0 --> <!-- Content for Tab 0 -->
<!-- Btn image changer -->
<h6>Image visualizer</h6> <h6>Image visualizer</h6>
<div> <div class="row items-center">
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"></q-btn> <q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"></q-btn> <q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"></q-btn>
</div> </div>
@ -221,14 +249,15 @@
<q-img id="colorbar-normal" class="q-ma-none q-pa-none" :src="colorbar"></q-img> <q-img id="colorbar-normal" class="q-ma-none q-pa-none" :src="colorbar"></q-img>
</div> </div>
</div> </div>
<p>{{msgimg}}</p> <p v-html="msgimg"></p>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="tab1"> <q-tab-panel name="tab1">
<!-- Content for Tab 1 --> <!-- Content for Tab 1 -->
<!-- Triq Btn image changer -->
<h6>TrIQ visualizer</h6> <h6>TrIQ visualizer</h6>
<div> <div class="row items-center">
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"></q-btn> <q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"></q-btn> <q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"></q-btn>
</div> </div>
@ -242,11 +271,38 @@
<q-img id="colorbar-triq" class="q-ma-none q-pa-none" :src="colorbarT"></q-img> <q-img id="colorbar-triq" class="q-ma-none q-pa-none" :src="colorbarT"></q-img>
</div> </div>
</div> </div>
<p>{{msgtriq}}</p> <p v-html="msgtriq"></p>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="tab2"> <q-tab-panel name="tab2">
<!-- Content for Tab 2 --> <div class="row items-center">
<q-select v-model="selected_folder_main" :options="available_folders" label="Select Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-btn-dropdown class="q-ma-sm btn-style" :loading="progressSpectraPlot" :disable="btnSpectraDisable"
label="Generate Spectra" icon="play_arrow">
<template v-slot:loading>
<q-spinner-hourglass class="on-left" />
Loading plot
</template>
<q-list>
<q-item clickable v-close-popup v-on:click="createMeanPlot=true">
<q-item-section>
<q-item-label>Mean spectrum plot</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup v-on:click="createSumPlot=true">
<q-item-section>
<q-item-label>Sum Spectrum plot</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup v-on:click="createXYPlot=true">
<q-item-section>
<q-item-label>Spectrum plot (X,Y)</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<plotly id="plotSpectra" :data="plotdata" :layout="plotlayout" class="q-pa-none q-ma-none"></plotly> <plotly id="plotSpectra" :data="plotdata" :layout="plotlayout" class="q-pa-none q-ma-none"></plotly>
</q-tab-panel> </q-tab-panel>
@ -281,8 +337,8 @@
<q-dialog v-model="CompareDialog" full-width> <q-dialog v-model="CompareDialog" full-width>
<q-card> <q-card>
<q-card-section class="row"> <q-card-section class="row items-center">
<div class="text-h6 q-ma-sm">Compare two diferent images or plots</div> <div class="text-h6 q-ma-sm">Compare two different images or plots</div>
<div class="q-mx-sm"> <div class="q-mx-sm">
<q-slider color="black" v-model="imgTrans" :min="0.0" :max="1" :step="0.1" :disable="btnOpticalDisable" /> <q-slider color="black" v-model="imgTrans" :min="0.0" :max="1" :step="0.1" :disable="btnOpticalDisable" />
<q-badge style="background-color: #009f90;"> Transparency: {{ imgTrans }}</q-badge> <q-badge style="background-color: #009f90;"> Transparency: {{ imgTrans }}</q-badge>
@ -294,46 +350,50 @@
<q-card-section class="q-pt-none col-12"> <q-card-section class="q-pt-none col-12">
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<st-tabs id="tabHeaderCompareLeft" :ids="tabIDs" :labels="tabLabels" v-model="selectedTab"></st-tabs> <div class="row items-center">
<q-tab-panels v-model="selectedTab"> <q-select v-model="selected_folder_compare_left" :options="image_available_folders" label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<st-tabs id="tabHeaderCompareLeft" :ids="CompTabIDsLeft" :labels="CompTabLabelsLeft" v-model="CompSelectedTabLeft"></st-tabs>
</div>
<q-tab-panels v-model="CompSelectedTabLeft">
<q-tab-panel name="tab0"> <q-tab-panel name="tab0">
<!-- Content for Tab 0 --> <!-- Content for Tab 0 -->
<!-- Btn image changer --> <!-- Btn image changer -->
<div> <div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"></q-btn> <q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"></q-btn> <q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusCompLeft=true"></q-btn>
</div> </div>
<!-- Image manager --> <!-- Image manager -->
<div id="image-container-compare-left-normal" class="row st-col col-12"> <div id="image-container-compare-left-normal" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none "> <div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgCompareLeft" :data="plotdataImg" :layout="plotlayoutImg" class="q-pa-none q-ma-none"> <plotly id="plotImgCompareLeft" :data="plotdataImgCompLeft" :layout="plotlayoutImgCompLeft" class="q-pa-none q-ma-none">
</plotly> </plotly>
</div> </div>
<div class="col-2"> <div class="col-2">
<q-img id="colorbarCompareLeft" class="q-ma-none q-pa-none" :src="colorbar"></q-img> <q-img id="colorbarCompareLeft" class="q-ma-none q-pa-none" :src="colorbarCompLeft"></q-img>
</div> </div>
</div> </div>
<p>{{msgimg}}</p> <p v-html="msgimgCompLeft"></p>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="tab1"> <q-tab-panel name="tab1">
<!-- Content for Tab 1 --> <!-- Content for Tab 1 -->
<!-- Triq Btn image changer --> <!-- Triq Btn image changer -->
<div> <div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"></q-btn> <q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"></q-btn> <q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusTCompLeft=true"></q-btn>
</div> </div>
<!-- Triq Image manager --> <!-- Triq Image manager -->
<div id="image-container-compare-left-triq" class="row st-col col-12"> <div id="image-container-compare-left-triq" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none "> <div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgTCompareLeft" :data="plotdataImgT" :layout="plotlayoutImgT" class="q-pa-none q-ma-none"> <plotly id="plotImgTCompareLeft" :data="plotdataImgTCompLeft" :layout="plotlayoutImgTCompLeft" class="q-pa-none q-ma-none">
</plotly> </plotly>
</div> </div>
<div class="col-2"> <div class="col-2">
<q-img id="colorbarTCompareLeft" class="q-ma-none q-pa-none" :src="colorbarT"></q-img> <q-img id="colorbarTCompareLeft" class="q-ma-none q-pa-none" :src="colorbarTCompLeft"></q-img>
</div> </div>
</div> </div>
<p>{{msgtriq}}</p> <p v-html="msgtriqCompLeft"></p>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="tab2"> <q-tab-panel name="tab2">
<!-- Content for Tab 2 --> <!-- Content for Tab 2 -->
@ -350,51 +410,54 @@
</q-tab-panels> </q-tab-panels>
</div> </div>
<div class="col-6"> <div class="col-6">
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDs" :labels="CompTabLabels" v-model="CompSelectedTab" outside-arrows <div class="row items-center">
mobile-arrows></st-tabs> <q-select v-model="selected_folder_compare_right" :options="image_available_folders" label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-tab-panels v-model="CompSelectedTab"> <q-space></q-space>
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDsRight" :labels="CompTabLabelsRight" v-model="CompSelectedTabRight"></st-tabs>
</div>
<q-tab-panels v-model="CompSelectedTabRight">
<q-tab-panel name="tab0"> <q-tab-panel name="tab0">
<!-- Content for Tab 0 --> <!-- Content for Tab 0 -->
<!-- Btn image changer --> <!-- Btn image changer -->
<div> <div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusComp=true"></q-btn> <q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" <q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusComp=true"></q-btn> v-on:click="imgPlusCompRight=true"></q-btn>
</div> </div>
<!-- Image manager --> <!-- Image manager -->
<div id="image-container-compare-right-normal" class="row st-col col-12"> <div id="image-container-compare-right-normal" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none "> <div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgCompareRight" :data="plotdataImgComp" :layout="plotlayoutImgComp" <plotly id="plotImgCompareRight" :data="plotdataImgCompRight" :layout="plotlayoutImgCompRight"
class="q-pa-none q-ma-none"> class="q-pa-none q-ma-none">
</plotly> </plotly>
</div> </div>
<div class="col-2 q-pa-none q-ma-none "> <div class="col-2 q-pa-none q-ma-none ">
<q-img id="colorbarComp" class="q-ma-none q-pa-none" :src="colorbarComp"></q-img> <q-img id="colorbarCompRight" class="q-ma-none q-pa-none" :src="colorbarCompRight"></q-img>
</div> </div>
</div> </div>
<p>{{msgimgComp}}</p> <p v-html="msgimgCompRight"></p>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="tab1"> <q-tab-panel name="tab1">
<!-- Content for Tab 1 --> <!-- Content for Tab 1 -->
<!-- Triq Btn image changer --> <!-- Triq Btn image changer -->
<div> <div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTComp=true"></q-btn> <q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" <q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusTComp=true"></q-btn> v-on:click="imgPlusTCompRight=true"></q-btn>
</div> </div>
<!-- Triq Image manager --> <!-- Triq Image manager -->
<div id="image-container-compare-right-triq" class="row st-col col-12"> <div id="image-container-compare-right-triq" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none "> <div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgTCompareRight" :data="plotdataImgTComp" :layout="plotlayoutImgTComp" <plotly id="plotImgTCompareRight" :data="plotdataImgTCompRight" :layout="plotlayoutImgTCompRight"
class="q-pa-none q-ma-none"> class="q-pa-none q-ma-none">
</plotly> </plotly>
</div> </div>
<div class="col-2 q-pa-none q-ma-none "> <div class="col-2 q-pa-none q-ma-none ">
<q-img id="colorbarTComp" class="q-ma-none q-pa-none" :src="colorbarTComp"></q-img> <q-img id="colorbarTCompRight" class="q-ma-none q-pa-none" :src="colorbarTCompRight"></q-img>
</div> </div>
</div> </div>
<p>{{msgtriqComp}}</p> <p v-html="msgtriqCompRight"></p>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="tab2"> <q-tab-panel name="tab2">
<!-- Content for Tab 2 --> <!-- Content for Tab 2 -->
@ -419,23 +482,53 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-dialog v-model="showMetadataDialog" full-width full-height> <q-dialog v-model="showBatchSummary">
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="text-h6">Dataset Summary</div> <div class="text-h6">Batch Process Summary</div>
</q-card-section> </q-card-section>
<q-card-section class="q-pt-none"> <q-card-section class="q-pt-none">
<q-list bordered separator> <pre>{{ batch_summary }}</pre>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Ok" style="color:#009f90" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showMetadataDialog" full-width full-height>
<q-card>
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Dataset Summary</div>
<q-space />
<q-select
v-model="selected_folder_metadata"
:options="available_folders"
label="Select Dataset"
class="q-ma-sm"
style="min-width: 250px;"
standout="custom-standout"
></q-select>
</q-card-section>
<q-card-section class="q-pt-none">
<q-list bordered separator v-if="metadata_rows.length > 0">
<q-item v-for="row in metadata_rows" :key="row.parameter"> <q-item v-for="row in metadata_rows" :key="row.parameter">
<q-item-section> <q-item-section>
<q-item-label>{{ row.parameter }}</q-item-label> <q-item-label class="text-body1">{{ row.parameter }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-item-label caption>{{ row.value }}</q-item-label> <q-item-label caption class="text-body1">{{ row.value }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
<div v-else class="text-center q-pa-md text-grey-7">
<q-icon name="info" size="3em" />
<p class="q-mt-md">No metadata found in registry for this dataset.</p>
<p class="text-caption">You may need to process the file first.</p>
</div>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">

View File

@ -452,10 +452,11 @@ end
# returns the layout and data for the surface plotly plot # returns the layout and data for the surface plotly plot
# this function loads the spectra data and makes a mean to display # this function loads the spectra data and makes a mean to display
# its values in the spectrum plot # 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( layout = PlotlyBase.Layout(
title=PlotlyBase.attr( title=PlotlyBase.attr(
text="Average Spectrum Plot", text=title_text,
font=PlotlyBase.attr( font=PlotlyBase.attr(
family="Roboto, Lato, sans-serif", family="Roboto, Lato, sans-serif",
size=18, size=18,
@ -480,9 +481,9 @@ function meanSpectrumPlot(data::MSIData)
if isempty(xSpectraMz) if isempty(xSpectraMz)
@warn "Average spectrum is empty." @warn "Average spectrum is empty."
trace = PlotlyBase.stem(x=Float64[], y=Float64[]) trace = PlotlyBase.scatter(x=Float64[], y=Float64[])
else else
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>") trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
end end
@ -491,7 +492,7 @@ function meanSpectrumPlot(data::MSIData)
return plotdata, plotlayout, xSpectraMz, ySpectraMz return plotdata, plotlayout, xSpectraMz, ySpectraMz
end 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 mz::AbstractVector, intensity::AbstractVector
local plot_title::String local plot_title::String
@ -502,24 +503,24 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
x = clamp(xCoord, 1, imgWidth) x = clamp(xCoord, 1, imgWidth)
y = clamp(yCoord, 1, imgHeight) 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 process_spectrum(data, Int(x), Int(y)) do recieved_mz, recieved_intensity
mz = recieved_mz mz = recieved_mz
intensity = recieved_intensity intensity = recieved_intensity
end end
plot_title = "Spectrum at ($x, $y)" base_title = "Spectrum at ($x, $y)"
else else
# For non-imaging data, treat xCoord as the spectrum index # For non-imaging data, treat xCoord as the spectrum index
index = clamp(xCoord, 1, length(data.spectra_metadata)) index = clamp(xCoord, 1, length(data.spectra_metadata))
# mz, intensity = GetSpectrum(data, index)
process_spectrum(data, index) do recieved_mz, recieved_intensity process_spectrum(data, index) do recieved_mz, recieved_intensity
mz = recieved_mz mz = recieved_mz
intensity = recieved_intensity intensity = recieved_intensity
end end
plot_title = "Spectrum #$index" base_title = "Spectrum #$index"
end end
plot_title = isempty(dataset_name) ? base_title : "$base_title for: $dataset_name"
layout = PlotlyBase.Layout( layout = PlotlyBase.Layout(
title=PlotlyBase.attr( title=PlotlyBase.attr(
text=plot_title, text=plot_title,
@ -545,7 +546,7 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
# Downsample for plotting performance # Downsample for plotting performance
mz_down, int_down = MSI_src.downsample_spectrum(mz, intensity) 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="<b>m/z</b>: %{x:.4f}<extra></extra>") trace = PlotlyBase.scatter(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
plotdata = [trace] plotdata = [trace]
plotlayout = layout plotlayout = layout
@ -553,10 +554,11 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
return plotdata, plotlayout, mz, intensity return plotdata, plotlayout, mz, intensity
end 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( layout = PlotlyBase.Layout(
title=PlotlyBase.attr( title=PlotlyBase.attr(
text="Total Spectrum Plot", text=title_text,
font=PlotlyBase.attr( font=PlotlyBase.attr(
family="Roboto, Lato, sans-serif", family="Roboto, Lato, sans-serif",
size=18, size=18,
@ -581,12 +583,47 @@ function sumSpectrumPlot(data::MSIData)
if isempty(xSpectraMz) if isempty(xSpectraMz)
@warn "Total spectrum is empty." @warn "Total spectrum is empty."
trace = PlotlyBase.stem(x=Float64[], y=Float64[]) trace = PlotlyBase.scatter(x=Float64[], y=Float64[])
else else
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>") trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
end end
plotdata = [trace] plotdata = [trace]
plotlayout = layout plotlayout = layout
return plotdata, plotlayout, xSpectraMz, ySpectraMz return plotdata, plotlayout, xSpectraMz, ySpectraMz
end 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

View File

@ -82,7 +82,7 @@
} }
#intDivStyle-left .q-tab-panels { #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 { #intDivStyle-left .q-tab-panel {

View File

@ -497,7 +497,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
open(target_ibd_file, "w") do ibd_stream open(target_ibd_file, "w") do ibd_stream
write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder
end end
return BinaryMetadata[], Tuple{Int, Int}[], (0, 0) return BinaryMetadata[], Tuple{Int, Int}[], (0, 0), UNKNOWN
end end
width = maximum(timing_matrix[:, 1]) width = maximum(timing_matrix[:, 1])
@ -505,6 +505,11 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
msi_data = OpenMSIData(source_file) 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)) scan_time_deltas = zeros(Int64, size(scans, 1))
if size(scans, 1) > 1 if size(scans, 1) > 1
for i in 1:(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 # Write m/z array
mz_offset = position(ibd_stream) mz_offset = position(ibd_stream)
for val in mz for val in mz
write(ibd_stream, htol(Float64(val))) write(ibd_stream, htol(Float32(val)))
end end
mz_length = position(ibd_stream) - mz_offset mz_length = position(ibd_stream) - mz_offset
@ -569,7 +574,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
end end
@info "Found and processed $empty_pixel_count empty pixels out of $(size(timing_matrix, 1)) total." @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 end
""" """
@ -589,7 +594,7 @@ experiment and data format.
# Returns # Returns
- `true` on success, `false` on failure. - `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") ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
if isempty(binary_meta) 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 open(target_file, "w") do imzml_stream
# XML Header # XML Header
write(imzml_stream, """<?xml version="1.0" encoding="ISO-8859-1"?> write(imzml_stream, """<?xml version="1.0" encoding="ISO-8859-1"?>
<indexedmzML xmlns="http://psi.hupo.org/ms/mzml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.0_idx.xsd"> <mzML xmlns="http://psi.hupo.org/ms/mzml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.0.xsd" version="1.1" id="$(splitext(basename(target_file))[1])">
<mzML version="1.1" id="$(splitext(basename(target_file))[1])">
""") """)
# CV List, File Description, etc. (static parts) # CV List, File Description, etc. (static parts)
@ -627,13 +631,11 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
<referenceableParamGroup id="mzArray"> <referenceableParamGroup id="mzArray">
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/> <cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
<cvParam cvRef="MS" accession="MS:1000514" name="m/z array" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/> <cvParam cvRef="MS" accession="MS:1000514" name="m/z array" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>
<cvParam cvRef="IMS" accession="IMS:1000101" name="external data" value="true"/> <cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/>
<cvParam cvRef="MS" accession="MS:1000523" name="64-bit float"/>
</referenceableParamGroup> </referenceableParamGroup>
<referenceableParamGroup id="intensityArray"> <referenceableParamGroup id="intensityArray">
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/> <cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
<cvParam cvRef="MS" accession="MS:1000515" name="intensity array" unitCvRef="MS" unitAccession="MS:1000131" unitName="number of counts"/> <cvParam cvRef="MS" accession="MS:1000515" name="intensity array" unitCvRef="MS" unitAccession="MS:1000131" unitName="number of counts"/>
<cvParam cvRef="IMS" accession="IMS:1000101" name="external data" value="true"/>
<cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/> <cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/>
</referenceableParamGroup> </referenceableParamGroup>
</referenceableParamGroupList> </referenceableParamGroupList>
@ -699,13 +701,20 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
push!(spectrum_offsets, spectrum_start) push!(spectrum_offsets, spectrum_start)
# Calculate number of points from byte length # 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) int_points = meta.int_length ÷ sizeof(Float32)
write(imzml_stream, """ <spectrum id="Scan=$(i)" defaultArrayLength="$(mz_points)" index="$(i-1)"> write(imzml_stream, """ <spectrum id="Scan=$(i)" defaultArrayLength="$(mz_points)" index="$(i-1)">
<cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="1"/> <cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="1"/>
<cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/> """)
<scanList count="1"> if mode == CENTROID
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000127" name="centroid spectrum"/>
""")
else
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/>
""")
end
write(imzml_stream, """ <scanList count="1">
<scan instrumentConfigurationRef="instrument1"> <scan instrumentConfigurationRef="instrument1">
<cvParam cvRef="IMS" accession="IMS:1000050" name="position x" value="$(x)"/> <cvParam cvRef="IMS" accession="IMS:1000050" name="position x" value="$(x)"/>
<cvParam cvRef="IMS" accession="IMS:1000051" name="position y" value="$(y)"/> <cvParam cvRef="IMS" accession="IMS:1000051" name="position y" value="$(y)"/>
@ -714,16 +723,14 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
<binaryDataArrayList count="2"> <binaryDataArrayList count="2">
<binaryDataArray encodedLength="0"> <binaryDataArray encodedLength="0">
<referenceableParamGroupRef ref="mzArray"/> <referenceableParamGroupRef ref="mzArray"/>
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.mz_offset)"/>
<cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(mz_points)"/> <cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(mz_points)"/>
<cvParam cvRef="IMS" accession="IMS:1000104" name="external encoded length" value="$(meta.mz_length)"/> <cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.mz_offset)"/>
<binary/> <binary/>
</binaryDataArray> </binaryDataArray>
<binaryDataArray encodedLength="0"> <binaryDataArray encodedLength="0">
<referenceableParamGroupRef ref="intensityArray"/> <referenceableParamGroupRef ref="intensityArray"/>
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.int_offset)"/>
<cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(int_points)"/> <cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(int_points)"/>
<cvParam cvRef="IMS" accession="IMS:1000104" name="external encoded length" value="$(meta.int_length)"/> <cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.int_offset)"/>
<binary/> <binary/>
</binaryDataArray> </binaryDataArray>
</binaryDataArrayList> </binaryDataArrayList>
@ -734,19 +741,6 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
write(imzml_stream, """ </spectrumList> write(imzml_stream, """ </spectrumList>
</run> </run>
</mzML> </mzML>
""")
# Index List
index_list_start = position(imzml_stream)
write(imzml_stream, """ <indexList count="1">
<index name="spectrum">
""")
for (i, offset) in enumerate(spectrum_offsets)
write(imzml_stream, " <offset idRef=\"Scan=$(i)\">$offset</offset>\n")
end
write(imzml_stream, """ </index>
</indexList>
<indexListOffset>$index_list_start</indexListOffset>
</indexedmzML>
""") """)
end 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...") println("Step 3: Converting spectra and writing .ibd file...")
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd") 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 # Flip image vertically to match R script output
flipped_coords = [(x, height - y + 1) for (x, y) in coords] flipped_coords = [(x, height - y + 1) for (x, y) in coords]
println("Step 4: Exporting .imzML metadata file...") 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 if success
println("Conversion successful: $target_file") println("Conversion successful: $target_file")

View File

@ -793,6 +793,84 @@ function get_mz_slice(data::MSIData, mass::Real, tolerance::Real)
end 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) plot_slice(msi_data::MSIData, mass::Float64, tolerance::Float64, output_dir::String; stage_name="slice", bins=256)

View File

@ -103,9 +103,37 @@ the m/z and intensity array metadata.
function parse_spectrum_metadata(stream::IO, offset::Int64) function parse_spectrum_metadata(stream::IO, offset::Int64)
seek(stream, offset) seek(stream, offset)
id_match = find_tag(stream, r"<spectrum\s+index=\"\d+\"\s+id=\"([^\"]+)" ) # Read the whole spectrum block to parse mode
spectrum_start_pos = position(stream)
line = ""
spectrum_buffer = IOBuffer()
while !eof(stream)
line = readline(stream)
write(spectrum_buffer, line)
if occursin("</spectrum>", line)
break
end
end
spectrum_xml = String(take!(spectrum_buffer))
seek(stream, spectrum_start_pos) # Reset for other parsing
id_match = match(r"<spectrum\s+index=\"\d+\"\s+id=\"([^\"]+)", spectrum_xml)
id = id_match === nothing ? "" : id_match.captures[1] id = id_match === nothing ? "" : id_match.captures[1]
# Determine mode from the XML block
mode = UNKNOWN
if occursin("MS:1000127", spectrum_xml)
mode = CENTROID
elseif occursin("MS:1000128", spectrum_xml)
mode = PROFILE
end
# Find where the binary data list starts to parse assets
binary_list_match = findfirst("<binaryDataArrayList", spectrum_xml)
if binary_list_match !== nothing
seek(stream, spectrum_start_pos + binary_list_match.start - 1)
end
asset1 = get_spectrum_asset_metadata(stream) asset1 = get_spectrum_asset_metadata(stream)
asset2 = get_spectrum_asset_metadata(stream) asset2 = get_spectrum_asset_metadata(stream)
@ -118,7 +146,7 @@ function parse_spectrum_metadata(stream::IO, offset::Int64)
# Create the new unified metadata object # Create the new unified metadata object
# For mzML, x and y coordinates are not applicable, so we use 0. # For mzML, x and y coordinates are not applicable, so we use 0.
return SpectrumMetadata(Int32(0), Int32(0), id, UNKNOWN, mz_asset, int_asset) return SpectrumMetadata(Int32(0), Int32(0), id, mode, mz_asset, int_asset)
end end
""" """