import styled from '@emotion/styled'; import type { Ranges } from '@zakodium/nmr-types'; import type { BoundingBox } from '@zakodium/nmrium-core'; import { checkMultiplicity } from 'nmr-processing'; import { memo, useEffect, useState } from 'react'; import { BsArrowsMove } from 'react-icons/bs'; import { FaTimes } from 'react-icons/fa'; import { Rnd } from 'react-rnd'; import { isSpectrum1D } from '../../data/data1d/Spectrum1D/index.js'; import { isSignalRange } from '../../data/utilities/RangeUtilities.js'; import type { SVGTableColumn } from '../SVGTable.js'; import { SVGTable } from '../SVGTable.js'; import { useChartData } from '../context/ChartContext.js'; import { useDispatch } from '../context/DispatchContext.js'; import { useGlobal } from '../context/GlobalContext.js'; import type { ActionsButtonsPopoverProps } from '../elements/ActionsButtonsPopover.js'; import { ActionsButtonsPopover } from '../elements/ActionsButtonsPopover.js'; import { useActiveNucleusTab } from '../hooks/useActiveNucleusTab.js'; import { usePanelPreferences } from '../hooks/usePanelPreferences.js'; import { useSVGUnitConverter } from '../hooks/useSVGUnitConverter.js'; import useSpectraByActiveNucleus from '../hooks/useSpectraPerNucleus.js'; import { useCheckExportStatus } from '../hooks/useViewportSize.js'; import { extractChemicalElement } from '../utility/extractChemicalElement.js'; import { formatNumber } from '../utility/formatNumber.js'; const ReactRnd = styled(Rnd)` border: 1px solid transparent; :hover { background-color: white; border: 1px solid #ebecf1; button { visibility: visible; } } `; interface RangesTableProps { ranges: Ranges['values']; } interface RangeItem { id: string; delta: string; multiplicity: string; integration: string; coupling: string; } function useMapRanges(ranges: Ranges['values']) { const output: RangeItem[] = []; const activeTab = useActiveNucleusTab(); const preferences = usePanelPreferences('ranges', activeTab); for (const range of ranges) { const { id, from, to, integration, signals = [] } = range; const relativeFlag = isSignalRange(range); const formattedValue = formatNumber( integration, preferences.relative.format, ); const integrationValue = relativeFlag ? formattedValue : `[ ${formattedValue} ]`; const rangeText = `${formatNumber(from, preferences.from.format)} - ${formatNumber( to, preferences.to.format, )}`; if (signals.length > 0) { for (const signal of signals) { const { multiplicity, delta, js = [] } = signal; const coupling = js .map((jsItem) => !Number.isNaN(jsItem.coupling) ? formatNumber(jsItem.coupling, preferences.coupling.format) : '', ) .join(','); const signalDelta = !checkMultiplicity(multiplicity, ['m']) ? rangeText : formatNumber(delta, preferences.deltaPPM.format); output.push({ id, delta: signalDelta, multiplicity, integration: integrationValue, coupling, }); } } else { output.push({ id, delta: rangeText, multiplicity: 'm', integration: integrationValue, coupling: '', }); } } return output; } function InnerSVGRangesTable(props: RangesTableProps) { const { ranges } = props; const { view: { spectra: { activeTab }, }, } = useChartData(); const data = useMapRanges(ranges); if (!data) return null; const element = extractChemicalElement(activeTab); const columns: Array> = [ { accessorKey: 'delta', header: 'δ (ppm)', width: 100, rowSpanGroupKey: 'id', headerTextProps: { fontWeight: 'bold' }, cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, }, { accessorKey: 'integration', header: `Rel. ${element}`, width: 60, rowSpanGroupKey: 'id', headerTextProps: { fontWeight: 'bold' }, cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, }, { accessorKey: 'multiplicity', header: 'Mult.', width: 60, rowSpanGroupKey: 'id', headerTextProps: { fontWeight: 'bold' }, cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, }, { accessorKey: 'coupling', header: 'J (Hz)', width: 120, headerTextProps: { fontWeight: 'bold' }, cellBoxProps: { stroke: '#dedede', fill: 'white', fillOpacity: 0.8 }, headerBoxProps: { stroke: '#dedede', fill: '#E5E8EB' }, }, ]; return data={data} columns={columns} />; } const SVGRangesTable = memo(InnerSVGRangesTable); interface DraggablePublicationStringProps { ranges: Ranges['values'] | undefined; bonding: BoundingBox; spectrumKey: string; } function DraggableRanges(props: DraggablePublicationStringProps) { const { ranges = [], bonding: externalBounding, spectrumKey } = props; const dispatch = useDispatch(); const { viewerRef } = useGlobal(); const [bounding, setBounding] = useState(externalBounding); const [isMoveActive, setIsMoveActive] = useState(false); const { percentToPixel, pixelToPercent } = useSVGUnitConverter(); const isExportProcessStart = useCheckExportStatus(); useEffect(() => { setBounding({ ...externalBounding }); }, [externalBounding]); function handleResize( internalBounding: Pick, ) { const { width = 0, height = 0 } = convertToPixel(externalBounding); internalBounding.width += width; internalBounding.height += height; setBounding((prevBounding) => ({ ...prevBounding, ...convertToPercent(internalBounding), })); } function handleDrag(internalBounding: Pick) { setBounding((prevBounding) => ({ ...prevBounding, ...convertToPercent(internalBounding), })); } function handleChangeInsetBounding(bounding: Partial) { if ( typeof bounding?.width === 'number' && typeof bounding?.height === 'number' ) { const { width, height } = externalBounding; bounding.width += width; bounding.height += height; } dispatch({ type: 'CHANGE_RANGES_VIEW_FLOATING_BOX_BOUNDING', payload: { spectrumKey, bounding: convertToPercent(bounding), target: 'rangesBounding', }, }); } function convertToPixel(bounding: Partial) { const { x, y, height, width } = bounding; const output: Partial = {}; if (x) { output.x = percentToPixel(x, 'x'); } if (y) { output.y = percentToPixel(y, 'y'); } if (width) { output.width = width; } if (height) { output.height = height; } return output; } function convertToPercent(bounding: Partial) { const { x, y, height, width } = bounding; const output: Partial = {}; if (x) { output.x = pixelToPercent(x, 'x'); } if (y) { output.y = pixelToPercent(y, 'y'); } if (width) { output.width = width; } if (height) { output.height = height; } return output; } function handleRemove() { dispatch({ type: 'TOGGLE_RANGES_VIEW_PROPERTY', payload: { key: 'showRanges', spectrumKey }, }); } const actionButtons: ActionsButtonsPopoverProps['buttons'] = [ { icon: , intent: 'none', title: 'Move ranges table', style: { cursor: 'move' }, className: 'handle', }, { icon: , intent: 'danger', title: 'Hide ranges table', onClick: handleRemove, }, ]; if (!viewerRef || ranges.length === 0) return null; const { x: xInPercent, y: yInPercent } = bounding; const x = percentToPixel(xInPercent, 'x'); const y = percentToPixel(yInPercent, 'y'); if (isExportProcessStart) { return ( ); } return ( setIsMoveActive(true)} onResize={(e, dir, eRef, size, position) => handleResize({ ...size, ...position }) } onResizeStop={(e, dir, eRef, size, position) => handleChangeInsetBounding({ ...size, ...position }) } onDrag={(e, { x, y }) => { handleDrag({ x, y }); }} onDragStop={(e, { x, y }) => { handleChangeInsetBounding({ x, y }); setIsMoveActive(false); }} resizeHandleWrapperStyle={{ backgroundColor: 'white' }} > ); } function useSpectraRanges() { const spectra = useSpectraByActiveNucleus(); const output: Record = {}; for (const spectrum of spectra) { if (!isSpectrum1D(spectrum)) { continue; } const { id: spectrumKey, ranges } = spectrum; if (!Array.isArray(ranges?.values) || ranges.values.length === 0) { continue; } output[spectrumKey] = ranges.values; } return output; } export function FloatingRanges() { const spectraRanges = useSpectraRanges(); const { view: { ranges }, } = useChartData(); const options = Object.entries(ranges); return options.map(([spectrumKey, viewOptions]) => { const { showRanges, rangesBounding } = viewOptions; if (!showRanges) return null; return ( ); }); }