From 5d91e72081dee90c6b4acffdae0f997e69c028d3 Mon Sep 17 00:00:00 2001 From: hamed musallam Date: Wed, 25 Mar 2026 19:57:37 +0100 Subject: [PATCH] feat: resurrect all spectra from database record --- .../panels/databasePanel/DatabasePanel.tsx | 84 +++---- .../panels/databasePanel/DatabaseTable.tsx | 32 ++- src/component/reducer/Reducer.ts | 9 +- .../reducer/actions/DatabaseActions.ts | 205 +++++++++++------- 4 files changed, 208 insertions(+), 122 deletions(-) diff --git a/src/component/panels/databasePanel/DatabasePanel.tsx b/src/component/panels/databasePanel/DatabasePanel.tsx index 6eeb9df210..0f960ae40f 100644 --- a/src/component/panels/databasePanel/DatabasePanel.tsx +++ b/src/component/panels/databasePanel/DatabasePanel.tsx @@ -11,7 +11,6 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useOnOff } from 'react-science/ui'; import { getSum } from '../../../data/data1d/Spectrum1D/SumManager.js'; -import { isSpectrum1D } from '../../../data/data1d/Spectrum1D/index.js'; import type { InitiateDatabaseResult, LocalDatabase, @@ -100,6 +99,22 @@ function mapKeywordsToArray(searchKeywords: string, solvent: string) { return values; } +function resolveSpectraURL( + rowData: { + baseURL: string; + jcampURL: string; + jcampFullURL: string; + }, + isFullJcamp: boolean, +): URL | null { + const { baseURL, jcampURL, jcampFullURL } = rowData; + const spectraURL = isFullJcamp ? jcampFullURL : jcampURL; + + if (!spectraURL) return null; + + return new URL(spectraURL, baseURL); +} + function DatabasePanelInner({ nucleus, selectedTool, @@ -285,49 +300,44 @@ function DatabasePanelInner({ const core = useCore(); const resurrectHandler = useCallback( - (rowData: any) => { - const { - index, - baseURL, - jcampURL: jcampRelativeURL, - ocl, - smiles, - } = rowData; + (rowData: any, isFullJcamp = false) => { + const { index, ocl, smiles } = rowData; const molfile = getMolfile({ ocl, smiles }); const databaseEntry = result.data[index]; - if (jcampRelativeURL) { - const url = new URL(jcampRelativeURL, baseURL); - setTimeout(async () => { - const hideLoading = toaster.showLoading({ - message: `load jcamp in progress...`, - }); + const spectraURL = resolveSpectraURL(rowData, isFullJcamp); - try { - const { - state: { data }, - } = await core.readFromWebSource({ - entries: [{ baseURL: url.origin, relativePath: url.pathname }], - }); - const spectrum = data?.spectra?.[0] || null; - if (spectrum && isSpectrum1D(spectrum)) { - dispatch({ - type: 'RESURRECTING_SPECTRUM', - payload: { source: 'jcamp', databaseEntry, spectrum, molfile }, - }); - } - } catch { - toaster.show({ message: 'Failed to load Jcamp', intent: 'danger' }); - } finally { - hideLoading(); - } - }, 0); - } else { + if (!spectraURL) { dispatch({ - type: 'RESURRECTING_SPECTRUM', - payload: { source: 'rangesOrSignals', databaseEntry, molfile }, + type: 'RESURRECTING_SPECTRUM_FROM_SIGNALS_OR_RANGES', + payload: { databaseEntry, molfile }, }); + return; } + + setTimeout(async () => { + const hideLoading = toaster.showLoading({ + message: `load jcamp in progress...`, + }); + + try { + const { + state: { data }, + } = await core.readFromWebSource({ + entries: [ + { baseURL: spectraURL.origin, relativePath: spectraURL.pathname }, + ], + }); + dispatch({ + type: 'RESURRECTING_SPECTRUM_FROM_JCAMP', + payload: { databaseEntry, spectra: data?.spectra || [], molfile }, + }); + } catch { + toaster.show({ message: 'Failed to load Jcamp', intent: 'danger' }); + } finally { + hideLoading(); + } + }, 0); }, [core, dispatch, result.data, toaster], ); diff --git a/src/component/panels/databasePanel/DatabaseTable.tsx b/src/component/panels/databasePanel/DatabaseTable.tsx index e91485a0aa..e29895a515 100644 --- a/src/component/panels/databasePanel/DatabaseTable.tsx +++ b/src/component/panels/databasePanel/DatabaseTable.tsx @@ -2,7 +2,7 @@ import { Classes } from '@blueprintjs/core'; import dlv from 'dlv'; import type { DatabaseNMREntry } from 'nmr-processing'; import type { CSSProperties } from 'react'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { ResponsiveChart } from 'react-d3-utils'; import { FaDownload, FaInfoCircle, FaMinus, FaPlus } from 'react-icons/fa'; import { IdcodeSvgRenderer, SmilesSvgRenderer } from 'react-ocl'; @@ -11,6 +11,7 @@ import type { CellProps } from 'react-table'; import type { PrepareDataResult } from '../../../data/data1d/database.js'; import { ColumnWrapper } from '../../elements/ColumnWrapper.js'; +import type { ContextMenuItem } from '../../elements/ContextMenuBluePrint.tsx'; import type { Column } from '../../elements/ReactTable/ReactTable.js'; import ReactTable from '../../elements/ReactTable/ReactTable.js'; import type { CustomColumn } from '../../elements/ReactTable/utility/addCustomColumn.js'; @@ -21,8 +22,17 @@ import { formatNumber } from '../../utility/formatNumber.js'; import { DatabaseInfo } from './DatabaseInfo.js'; +const contextMenu: ContextMenuItem[] = [ + { + text: 'Add all spectra', + icon: , + data: { id: 'addAllSpectra' }, + disabled: (data) => !data.jcampFullURL, + }, +]; + interface ToggleEvent { - onAdd: (row: any) => void; + onAdd: (row: any, isFullJcamp: boolean) => void; onRemove: (row: any) => void; } interface DatabaseTableProps extends ToggleEvent { @@ -266,9 +276,25 @@ function DatabaseTable({ columns.sort((object1, object2) => object1.index - object2.index); return columns; }, [databasePreferences, initialColumns]); + + const selectContextMenuHandler = useCallback( + (option: any, data: any) => { + const { id } = option; + + if (id !== 'addAllSpectra') { + return; + } + + onAdd(data, true); + }, + [onAdd], + ); + return ( ({ @@ -304,7 +330,7 @@ function ToggleBtn(props: ToggleBtnProps) { if (isAdded) { onRemove(data); } else { - onAdd(data); + onAdd(data, false); } }} > diff --git a/src/component/reducer/Reducer.ts b/src/component/reducer/Reducer.ts index 40d05bf6e1..04032deebb 100644 --- a/src/component/reducer/Reducer.ts +++ b/src/component/reducer/Reducer.ts @@ -788,8 +788,13 @@ function innerSpectrumReducer(draft: Draft, action: Action) { action, ); - case 'RESURRECTING_SPECTRUM': - return DatabaseActions.handleResurrectSpectrum(draft, action); + case 'RESURRECTING_SPECTRUM_FROM_SIGNALS_OR_RANGES': + return DatabaseActions.handleResurrectSpectrumFromRangesOrSignals( + draft, + action, + ); + case 'RESURRECTING_SPECTRUM_FROM_JCAMP': + return DatabaseActions.handleResurrectSpectrumFromJCAMP(draft, action); case 'TOGGLE_SIMILARITY_TREE': return DatabaseActions.handleToggleSimilarityTree(draft); diff --git a/src/component/reducer/actions/DatabaseActions.ts b/src/component/reducer/actions/DatabaseActions.ts index 37ce99c87c..82ba260aab 100644 --- a/src/component/reducer/actions/DatabaseActions.ts +++ b/src/component/reducer/actions/DatabaseActions.ts @@ -1,18 +1,22 @@ import type { Info1D } from '@zakodium/nmr-types'; -import type { Spectrum1D, Spectrum } from '@zakodium/nmrium-core'; +import type { Spectrum } from '@zakodium/nmrium-core'; import type { Draft } from 'immer'; import type { DatabaseNMREntry } from 'nmr-processing'; import { - get1DColor, + initiateDatum1D, isSpectrum1D, mapRanges, + updateRangesRelativeValues, } from '../../../data/data1d/Spectrum1D/index.js'; import { resurrectSpectrumFromRanges, resurrectSpectrumFromSignals, } from '../../../data/data1d/Spectrum1D/ranges/resurrectSpectrum.js'; +import { initializeContoursLevels } from '../../../data/data2d/Spectrum2D/contours.ts'; +import { initiateDatum2D } from '../../../data/data2d/Spectrum2D/initiateDatum2D.ts'; import { filterDatabaseInfoEntry } from '../../utility/filterDatabaseInfoEntry.js'; +import { getSpectraByNucleus } from '../../utility/getSpectraByNucleus.ts'; import type { State } from '../Reducer.js'; import { setZoom } from '../helper/Zoom1DManager.js'; import zoomHistoryManager from '../helper/ZoomHistoryManager.js'; @@ -28,34 +32,103 @@ interface BaseResurrectSpectrum { molfile?: string; } interface ResurrectSpectrumFromJCAMP extends BaseResurrectSpectrum { - source: 'jcamp'; - spectrum: Spectrum1D; + spectra: Spectrum[]; } -interface ResurrectSpectrumFromRangesOrSignals extends BaseResurrectSpectrum { - source: 'rangesOrSignals'; -} - -type ResurrectSpectrum = - | ResurrectSpectrumFromJCAMP - | ResurrectSpectrumFromRangesOrSignals; -type ResurrectSpectrumAction = ActionType< - 'RESURRECTING_SPECTRUM', - ResurrectSpectrum +type ResurrectSpectrumFromRangesOrSignalsAction = ActionType< + 'RESURRECTING_SPECTRUM_FROM_SIGNALS_OR_RANGES', + BaseResurrectSpectrum +>; +type ResurrectSpectrumFromJCAMPAction = ActionType< + 'RESURRECTING_SPECTRUM_FROM_JCAMP', + ResurrectSpectrumFromJCAMP >; export type DatabaseActions = | ActionType<'TOGGLE_SIMILARITY_TREE'> - | ResurrectSpectrumAction; + | ResurrectSpectrumFromRangesOrSignalsAction + | ResurrectSpectrumFromJCAMPAction; + +function updateDomain(draft: Draft) { + setDomain(draft, { isYDomainShared: false }); + changeSpectrumVerticalAlignment(draft, { verticalAlign: 'stack' }); + + const zoomHistory = zoomHistoryManager( + draft.zoom.history, + draft.view.spectra.activeTab, + ); + const zoomValue = zoomHistory.getLast(); + if (zoomValue) { + draft.xDomain = zoomValue.xDomain; + draft.yDomain = zoomValue.yDomain; + } +} -function handleResurrectSpectrum( +function handleResurrectSpectrumFromJCAMP( draft: Draft, - action: ResurrectSpectrumAction, + action: ResurrectSpectrumFromJCAMPAction, +) { + const { databaseEntry, spectra, molfile } = action.payload; + const { ranges, id: spectrumID, nucleus } = databaseEntry; + + if (spectra.length === 0) return; + + const containOneSpectrum = spectra.length === 1; + const spectra1D = []; + + if (molfile) { + addMolecule(draft, { molfile, id: spectra[0].id }); + } + + for (const spectrum of spectra) { + const filterDatabaseEntryInfo = filterDatabaseInfoEntry(databaseEntry); + if (isSpectrum1D(spectrum)) { + const spectrum1D = initiateDatum1D(spectrum, { + usedColors: draft.usedColors, + }); + + if (spectrum.info.nucleus === nucleus) { + spectrum1D.ranges.values = mapRanges(ranges, spectrum); + updateRangesRelativeValues(spectrum1D); + } + + if (containOneSpectrum) { + spectrum1D.id = spectrumID; + } + + draft.data.push(spectrum1D); + spectra1D.push(spectrum1D); + } else { + const spectrum2d = initiateDatum2D(spectrum, { + usedColors: draft.usedColors, + }); + spectrum2d.customInfo = filterDatabaseEntryInfo; + draft.view.spectraContourLevels[spectrum2d.id] = + initializeContoursLevels(spectrum2d); + draft.data.push(spectrum2d); + } + } + + updateDomain(draft); + + const active1DSpectra = getSpectraByNucleus( + draft.view.spectra.activeTab, + spectra1D, + ); + + for (const spectrum of active1DSpectra) { + setZoom(draft, { scale: 0.8, spectrumID: spectrum.id }); + } +} + +function handleResurrectSpectrumFromRangesOrSignals( + draft: Draft, + action: ResurrectSpectrumFromRangesOrSignalsAction, ) { const { spectra: { activeTab: nucleus }, } = draft.view; - const { databaseEntry, source, molfile } = action.payload; + const { databaseEntry, molfile } = action.payload; const { ranges, signals, @@ -64,54 +137,35 @@ function handleResurrectSpectrum( id: spectrumID, } = databaseEntry; - let resurrectedSpectrum: Spectrum | null | undefined = null; - - if (source === 'jcamp') { - const { spectrum } = action.payload; - - resurrectedSpectrum = { - ...spectrum, - id: spectrumID, - ranges: { - ...spectrum.ranges, - values: mapRanges(ranges, spectrum), - }, - display: { - ...spectrum.display, - ...get1DColor(spectrum.display, { usedColors: draft.usedColors }), - }, - }; + let options: { from?: number; to?: number } = {}; + let info: Partial = { solvent, name: names[0], nucleus }; + + const activeSpectrum = getSpectrum(draft) || draft.data[0]; + if (isSpectrum1D(activeSpectrum)) { + const { + data: { x }, + info: spectrumInfo, + } = activeSpectrum; + options = { from: x[0], to: x.at(-1) }; + info = { ...spectrumInfo, ...info }; } - if (source === 'rangesOrSignals') { - const activeSpectrum = getSpectrum(draft) || draft.data[0]; - let options: { from?: number; to?: number } = {}; - let info: Partial = { solvent, name: names[0], nucleus }; - - if (isSpectrum1D(activeSpectrum)) { - const { - data: { x }, - info: spectrumInfo, - } = activeSpectrum; - options = { from: x[0], to: x.at(-1) }; - info = { ...spectrumInfo, ...info }; - } + let resurrectedSpectrum: Spectrum | null | undefined = null; - if (signals) { - resurrectedSpectrum = resurrectSpectrumFromSignals(signals, { - spectrumID, - info, - usedColors: draft.usedColors, - ...options, - }); - } else if (ranges) { - resurrectedSpectrum = resurrectSpectrumFromRanges(ranges, { - spectrumID, - info, - usedColors: draft.usedColors, - ...options, - }); - } + if (signals) { + resurrectedSpectrum = resurrectSpectrumFromSignals(signals, { + spectrumID, + info, + usedColors: draft.usedColors, + ...options, + }); + } else if (ranges) { + resurrectedSpectrum = resurrectSpectrumFromRanges(ranges, { + spectrumID, + info, + usedColors: draft.usedColors, + ...options, + }); } if (!resurrectedSpectrum) return; @@ -121,25 +175,12 @@ function handleResurrectSpectrum( draft.data.push(resurrectedSpectrum); - setDomain(draft, { isYDomainShared: false }); - //rescale the vertical zoom - setZoom(draft, { scale: 0.8, spectrumID: resurrectedSpectrum.id }); - - changeSpectrumVerticalAlignment(draft, { verticalAlign: 'stack' }); + updateDomain(draft); - //keep the last horizontal zoom - const zoomHistory = zoomHistoryManager( - draft.zoom.history, - draft.view.spectra.activeTab, - ); - const zoomValue = zoomHistory.getLast(); - if (zoomValue) { - draft.xDomain = zoomValue.xDomain; - draft.yDomain = zoomValue.yDomain; - } + setZoom(draft, { scale: 0.8, spectrumID: resurrectedSpectrum.id }); if (molfile) { - addMolecule(draft, { molfile, id: spectrumID }); + addMolecule(draft, { molfile, id: resurrectedSpectrum.id }); } } @@ -148,4 +189,8 @@ function handleToggleSimilarityTree(draft: Draft) { !draft.view.spectra.showSimilarityTree; } -export { handleResurrectSpectrum, handleToggleSimilarityTree }; +export { + handleResurrectSpectrumFromJCAMP, + handleResurrectSpectrumFromRangesOrSignals, + handleToggleSimilarityTree, +};