diff --git a/package-lock.json b/package-lock.json index 42c2dd91b..52c844537 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "ml-tree-similarity": "^2.2.0", "multiplet-analysis": "^2.1.2", "nmr-correlation": "^2.3.3", - "nmr-load-save": "^2.0.4", + "nmr-load-save": "^2.1.0", "nmr-processing": "^14.0.3", "nmredata": "^0.9.11", "numeral": "^2.0.6", @@ -10021,9 +10021,9 @@ } }, "node_modules/nmr-load-save": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nmr-load-save/-/nmr-load-save-2.0.4.tgz", - "integrity": "sha512-+HnY0Zlg9Texd5ZKjdPPE8mxCd7SstjwLXutMgw/NJQfHBG8swDB9OfITPhM+ibO60U6o9I3VoCoDqIE+gADrw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nmr-load-save/-/nmr-load-save-2.1.0.tgz", + "integrity": "sha512-4QIjH/2W713uhLd4i417v1XuV+pknPDOJcYoeRiRHoFf4rev7/gx+QnYZ/f1g3PXzSvARFq6k70Wgwxckn2KBg==", "license": "MIT", "dependencies": { "@lukeed/uuid": "^2.0.1", diff --git a/package.json b/package.json index 06a8fcbcc..450896679 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "ml-tree-similarity": "^2.2.0", "multiplet-analysis": "^2.1.2", "nmr-correlation": "^2.3.3", - "nmr-load-save": "^2.0.4", + "nmr-load-save": "^2.1.0", "nmr-processing": "^14.0.3", "nmredata": "^0.9.11", "numeral": "^2.0.6", diff --git a/src/component/1d/ApodizationLine.tsx b/src/component/1d/ApodizationLine.tsx index 32468bbde..c07ce0660 100644 --- a/src/component/1d/ApodizationLine.tsx +++ b/src/component/1d/ApodizationLine.tsx @@ -7,11 +7,13 @@ import { } from 'nmr-processing'; import { useChartData } from '../context/ChartContext.js'; +import { useFilterSyncOptions } from '../context/FilterSyncOptionsContext.js'; import { useScaleChecked } from '../context/ScaleContext.js'; import { useActiveSpectrum } from '../hooks/useActiveSpectrum.js'; import useSpectrum from '../hooks/useSpectrum.js'; import { useVerticalAlign } from '../hooks/useVerticalAlign.js'; import useXYReduce, { XYReducerDomainAxis } from '../hooks/useXYReduce.js'; +import type { ApodizationOptions } from '../panels/filtersPanel/Filters/hooks/useSharedApodization.js'; import { PathBuilder } from '../utility/PathBuilder.js'; import { getYScale } from './utilities/scale.js'; @@ -32,16 +34,15 @@ function useWindowYScale() { export function ApodizationLine() { const { - toolOptions: { - selectedTool, - data: { apodizationOptions: externalApodizationOptions }, - }, + toolOptions: { selectedTool }, } = useChartData(); const activeSpectrum = useActiveSpectrum(); const { scaleX } = useScaleChecked(); const spectrum = useSpectrum({ emptyData }) as Spectrum1D; const xyReduce = useXYReduce(XYReducerDomainAxis.XAxis); const scaleY = useWindowYScale(); + const { sharedFilterOptions: externalApodizationOptions } = + useFilterSyncOptions(); if (!activeSpectrum?.id || selectedTool !== Filters1D.apodization.name) { return null; @@ -53,7 +54,7 @@ export function ApodizationLine() { const apodizationOptions = merge( default1DApodization, - externalApodizationOptions, + externalApodizationOptions?.options, ); const length = re.length; const dw = (x[length - 1] - x[0]) / (length - 1); diff --git a/src/component/header/BaseSimpleApodizationOptionsPanel.tsx b/src/component/header/BaseSimpleApodizationOptionsPanel.tsx new file mode 100644 index 000000000..c0bb4cbcf --- /dev/null +++ b/src/component/header/BaseSimpleApodizationOptionsPanel.tsx @@ -0,0 +1,104 @@ +import { Checkbox } from '@blueprintjs/core'; + +import { useToaster } from '../context/ToasterContext.js'; +import ActionButtons from '../elements/ActionButtons.js'; +import Label from '../elements/Label.js'; +import { NumberInput2Controller } from '../elements/NumberInput2Controller.js'; +import { useSharedApodization } from '../panels/filtersPanel/Filters/hooks/useSharedApodization.js'; +import type { + ApodizationFilterOptions, + UseSharedApodizationOptions, +} from '../panels/filtersPanel/Filters/hooks/useSharedApodization.js'; + +import { headerLabelStyle } from './Header.js'; +import { HeaderWrapper } from './HeaderWrapper.js'; + +interface BaseSimpleApodizationOptionsPanelProps + extends Pick< + UseSharedApodizationOptions, + 'onApplyDispatch' | 'onChangeDispatch' + > { + filter: ApodizationFilterOptions | null; +} + +export function BaseSimpleApodizationOptionsPanel( + props: BaseSimpleApodizationOptionsPanelProps, +) { + const toaster = useToaster(); + + const { filter, onApplyDispatch, onChangeDispatch } = props; + + const { formMethods, submitHandler, handleApplyFilter, handleCancelFilter } = + useSharedApodization(filter, { + applyFilterOnload: true, + onApplyDispatch, + onChangeDispatch, + }); + + const { + register, + handleSubmit, + control, + formState: { isValid }, + watch, + } = formMethods; + + const isExponentialActive = watch('options.exponential.apply') || false; + + const { onChange: onLivePreviewFieldChange, ...livePreviewFieldOptions } = + register('livePreview'); + + function handleClick() { + if (!isExponentialActive) { + toaster.show({ + intent: 'danger', + message: + 'Activate "Exponential" filter from the Processing panel first', + }); + } + } + + function handleConfirm() { + void handleSubmit((values) => handleApplyFilter(values))(); + } + + function handleCancel() { + handleCancelFilter(); + } + + return ( + + + + + + + + ); +} diff --git a/src/component/header/Header.tsx b/src/component/header/Header.tsx index dc03d3a4d..7a9cf2983 100644 --- a/src/component/header/Header.tsx +++ b/src/component/header/Header.tsx @@ -31,6 +31,8 @@ import { options } from '../toolbar/ToolTypes.js'; import { AutoPeakPickingOptionPanel } from './AutoPeakPickingOptionPanel.js'; import { HeaderWrapper } from './HeaderWrapper.js'; import RangesPickingOptionPanel from './RangesPickingOptionPanel.js'; +import { SimpleApodizationDimensionOneOptionsPanel } from './SimpleApodizationDimensionOneOptionsPanel.js'; +import { SimpleApodizationDimensionTwoOptionsPanel } from './SimpleApodizationDimensionTwoOptionsPanel.js'; import { SimpleApodizationOptionsPanel } from './SimpleApodizationOptionsPanel.js'; import { SimpleBaseLineCorrectionOptionsPanel } from './SimpleBaseLineCorrectionOptionsPanel.js'; import { SimplePhaseCorrectionOptionsPanel } from './SimplePhaseCorrectionOptionsPanel.js'; @@ -104,6 +106,10 @@ function HeaderInner(props: HeaderInnerProps) { switch (selectedOptionPanel) { case options.apodization.id: return ; + case options.apodizationDimension1.id: + return ; + case options.apodizationDimension2.id: + return ; case options.zeroFilling.id: return ; case options.phaseCorrection.id: diff --git a/src/component/header/SimpleApodizationDimensionOneOptionsPanel.tsx b/src/component/header/SimpleApodizationDimensionOneOptionsPanel.tsx new file mode 100644 index 000000000..22671435c --- /dev/null +++ b/src/component/header/SimpleApodizationDimensionOneOptionsPanel.tsx @@ -0,0 +1,54 @@ +import { memo, useCallback } from 'react'; + +import type { ExtractFilterEntry } from '../../data/types/common/ExtractFilterEntry.js'; +import { useDispatch } from '../context/DispatchContext.js'; +import { useFilter } from '../hooks/useFilter.js'; + +import { BaseSimpleApodizationOptionsPanel } from './BaseSimpleApodizationOptionsPanel.js'; + +interface ApodizationOptionsInnerPanelProps { + filter: ExtractFilterEntry<'apodizationDimension1'> | null; +} + +function ApodizationOptionsInnerPanel( + props: ApodizationOptionsInnerPanelProps, +) { + const dispatch = useDispatch(); + + const applyHandler = useCallback( + (data) => { + const { options } = data; + dispatch({ + type: 'APPLY_APODIZATION_DIMENSION_ONE_FILTER', + payload: { options }, + }); + }, + [dispatch], + ); + const changeHandler = useCallback( + (data) => { + const { livePreview, options } = data; + + dispatch({ + type: 'CALCULATE_APODIZATION_DIMENSION_ONE_FILTER', + payload: { livePreview, options: structuredClone(options) }, + }); + }, + [dispatch], + ); + + return ( + + ); +} + +const MemoizedApodizationPanel = memo(ApodizationOptionsInnerPanel); + +export function SimpleApodizationDimensionOneOptionsPanel() { + const filter = useFilter('apodizationDimension1'); + return ; +} diff --git a/src/component/header/SimpleApodizationDimensionTwoOptionsPanel.tsx b/src/component/header/SimpleApodizationDimensionTwoOptionsPanel.tsx new file mode 100644 index 000000000..d60922012 --- /dev/null +++ b/src/component/header/SimpleApodizationDimensionTwoOptionsPanel.tsx @@ -0,0 +1,54 @@ +import { memo, useCallback } from 'react'; + +import type { ExtractFilterEntry } from '../../data/types/common/ExtractFilterEntry.js'; +import { useDispatch } from '../context/DispatchContext.js'; +import { useFilter } from '../hooks/useFilter.js'; + +import { BaseSimpleApodizationOptionsPanel } from './BaseSimpleApodizationOptionsPanel.js'; + +interface ApodizationOptionsInnerPanelProps { + filter: ExtractFilterEntry<'apodizationDimension2'> | null; +} + +function ApodizationOptionsInnerPanel( + props: ApodizationOptionsInnerPanelProps, +) { + const dispatch = useDispatch(); + + const applyHandler = useCallback( + (data) => { + const { options } = data; + dispatch({ + type: 'APPLY_APODIZATION_DIMENSION_TWO_FILTER', + payload: { options }, + }); + }, + [dispatch], + ); + const changeHandler = useCallback( + (data) => { + const { livePreview, options } = data; + + dispatch({ + type: 'CALCULATE_APODIZATION_DIMENSION_TWO_FILTER', + payload: { livePreview, options: structuredClone(options) }, + }); + }, + [dispatch], + ); + + return ( + + ); +} + +const MemoizedApodizationPanel = memo(ApodizationOptionsInnerPanel); + +export function SimpleApodizationDimensionTwoOptionsPanel() { + const filter = useFilter('apodizationDimension2'); + return ; +} diff --git a/src/component/header/SimpleApodizationOptionsPanel.tsx b/src/component/header/SimpleApodizationOptionsPanel.tsx index 7d7d4bc4b..37e598545 100644 --- a/src/component/header/SimpleApodizationOptionsPanel.tsx +++ b/src/component/header/SimpleApodizationOptionsPanel.tsx @@ -1,16 +1,10 @@ -import { Checkbox } from '@blueprintjs/core'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import type { ExtractFilterEntry } from '../../data/types/common/ExtractFilterEntry.js'; -import { useToaster } from '../context/ToasterContext.js'; -import ActionButtons from '../elements/ActionButtons.js'; -import Label from '../elements/Label.js'; -import { NumberInput2Controller } from '../elements/NumberInput2Controller.js'; +import { useDispatch } from '../context/DispatchContext.js'; import { useFilter } from '../hooks/useFilter.js'; -import { useSharedApodization } from '../panels/filtersPanel/Filters/hooks/useSharedApodization.js'; -import { headerLabelStyle } from './Header.js'; -import { HeaderWrapper } from './HeaderWrapper.js'; +import { BaseSimpleApodizationOptionsPanel } from './BaseSimpleApodizationOptionsPanel.js'; interface ApodizationOptionsInnerPanelProps { filter: ExtractFilterEntry<'apodization'> | null; @@ -19,67 +13,35 @@ interface ApodizationOptionsInnerPanelProps { function ApodizationOptionsInnerPanel( props: ApodizationOptionsInnerPanelProps, ) { - const toaster = useToaster(); - const { formMethods, submitHandler, handleApplyFilter, handleCancelFilter } = - useSharedApodization(props.filter, { applyFilterOnload: true }); - - const { - register, - handleSubmit, - control, - formState: { isValid }, - watch, - } = formMethods; - - const isExponentialActive = watch('options.exponential.apply') || false; - - const { onChange: onLivePreviewFieldChange, ...livePreviewFieldOptions } = - register('livePreview'); - - function handleClick() { - if (!isExponentialActive) { - toaster.show({ - intent: 'danger', - message: - 'Activate "Exponential" filter from the Processing panel first', + const dispatch = useDispatch(); + + const applyHandler = useCallback( + (data) => { + const { options } = data; + dispatch({ + type: 'APPLY_APODIZATION_FILTER', + payload: { options }, }); - } - } + }, + [dispatch], + ); + const changeHandler = useCallback( + (data) => { + const { livePreview, options } = data; + dispatch({ + type: 'CALCULATE_APODIZATION_FILTER', + payload: { livePreview, options: structuredClone(options) }, + }); + }, + [dispatch], + ); return ( - - - - - - handleSubmit((values) => handleApplyFilter(values))()} - onCancel={handleCancelFilter} - /> - + ); } diff --git a/src/component/modal/setting/settings-tabs/ToolsTabContent.tsx b/src/component/modal/setting/settings-tabs/ToolsTabContent.tsx index 3739cb0c5..fc47d3f85 100644 --- a/src/component/modal/setting/settings-tabs/ToolsTabContent.tsx +++ b/src/component/modal/setting/settings-tabs/ToolsTabContent.tsx @@ -67,6 +67,14 @@ const LIST: ListItem[] = [ label: 'Apodization', name: 'apodization', }, + { + label: 'Apodization dimension one', + name: 'apodizationDimension1', + }, + { + label: 'Apodization dimension two', + name: 'apodizationDimension2', + }, { label: 'Zero filling', name: 'zeroFilling', diff --git a/src/component/panels/filtersPanel/Filters/ApodizationDimensionOneOptionsPanel.tsx b/src/component/panels/filtersPanel/Filters/ApodizationDimensionOneOptionsPanel.tsx new file mode 100644 index 000000000..a24bfa8dd --- /dev/null +++ b/src/component/panels/filtersPanel/Filters/ApodizationDimensionOneOptionsPanel.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react'; + +import type { ExtractFilterEntry } from '../../../../data/types/common/ExtractFilterEntry.js'; +import { useDispatch } from '../../../context/DispatchContext.js'; + +import { BaseApodizationOptions } from './apodization/BaseApodizationOptions.js'; + +import type { BaseFilterOptionsPanelProps } from './index.js'; + +export default function ApodizationDimensionOneOptionsPanel( + props: BaseFilterOptionsPanelProps< + ExtractFilterEntry<'apodizationDimension1'> + >, +) { + const dispatch = useDispatch(); + + const { filter, enableEdit = true, onCancel, onConfirm } = props; + + const applyHandler = useCallback( + (data) => { + const { options } = data; + dispatch({ + type: 'APPLY_APODIZATION_DIMENSION_ONE_FILTER', + payload: { options }, + }); + }, + [dispatch], + ); + const changeHandler = useCallback( + (data) => { + const { livePreview, options } = data; + + dispatch({ + type: 'CALCULATE_APODIZATION_DIMENSION_ONE_FILTER', + payload: { livePreview, options: structuredClone(options) }, + }); + }, + [dispatch], + ); + + return ( + + ); +} diff --git a/src/component/panels/filtersPanel/Filters/ApodizationDimensionTwoOptionsPanel.tsx b/src/component/panels/filtersPanel/Filters/ApodizationDimensionTwoOptionsPanel.tsx new file mode 100644 index 000000000..9f3928b9e --- /dev/null +++ b/src/component/panels/filtersPanel/Filters/ApodizationDimensionTwoOptionsPanel.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react'; + +import type { ExtractFilterEntry } from '../../../../data/types/common/ExtractFilterEntry.js'; +import { useDispatch } from '../../../context/DispatchContext.js'; + +import { BaseApodizationOptions } from './apodization/BaseApodizationOptions.js'; + +import type { BaseFilterOptionsPanelProps } from './index.js'; + +export default function ApodizationDimensionTwoOptionsPanel( + props: BaseFilterOptionsPanelProps< + ExtractFilterEntry<'apodizationDimension2'> + >, +) { + const dispatch = useDispatch(); + + const { filter, enableEdit = true, onCancel, onConfirm } = props; + + const applyHandler = useCallback( + (data) => { + const { options } = data; + dispatch({ + type: 'APPLY_APODIZATION_DIMENSION_TWO_FILTER', + payload: { options }, + }); + }, + [dispatch], + ); + const changeHandler = useCallback( + (data) => { + const { livePreview, options } = data; + + dispatch({ + type: 'CALCULATE_APODIZATION_DIMENSION_TWO_FILTER', + payload: { livePreview, options: structuredClone(options) }, + }); + }, + [dispatch], + ); + + return ( + + ); +} diff --git a/src/component/panels/filtersPanel/Filters/ApodizationOptionsPanel.tsx b/src/component/panels/filtersPanel/Filters/ApodizationOptionsPanel.tsx index 97b02399b..987dccd1d 100644 --- a/src/component/panels/filtersPanel/Filters/ApodizationOptionsPanel.tsx +++ b/src/component/panels/filtersPanel/Filters/ApodizationOptionsPanel.tsx @@ -1,295 +1,49 @@ -import { Switch } from '@blueprintjs/core'; -import type { Apodization1DOptions } from 'nmr-processing'; -import type { ReactNode } from 'react'; -import { FormProvider, useFormContext } from 'react-hook-form'; -import * as Yup from 'yup'; +import { useCallback } from 'react'; import type { ExtractFilterEntry } from '../../../../data/types/common/ExtractFilterEntry.js'; -import Label from '../../../elements/Label.js'; -import { NumberInput2Controller } from '../../../elements/NumberInput2Controller.js'; -import { ReadOnly } from '../../../elements/ReadOnly.js'; -import { Sections } from '../../../elements/Sections.js'; +import { useDispatch } from '../../../context/DispatchContext.js'; -import { FilterActionButtons } from './FilterActionButtons.js'; -import { HeaderContainer, StickyHeader } from './InnerFilterHeader.js'; -import { useSharedApodization } from './hooks/useSharedApodization.js'; +import { BaseApodizationOptions } from './apodization/BaseApodizationOptions.js'; import type { BaseFilterOptionsPanelProps } from './index.js'; -import { formLabelStyle } from './index.js'; - -const advanceValidationSchema = Yup.object().shape({ - options: Yup.object().shape({ - gaussian: Yup.object() - .shape({ - options: Yup.object().shape({ - lineBroadening: Yup.number().required(), - lineBroadeningCenter: Yup.number().required().min(0).max(1), - }), - }) - .notRequired(), - }), - livePreview: Yup.boolean().required(), -}); export default function ApodizationOptionsPanel( props: BaseFilterOptionsPanelProps>, ) { - const { filter, enableEdit = true, onCancel, onConfirm } = props; - const { submitHandler, handleApplyFilter, handleCancelFilter, formMethods } = - useSharedApodization(filter, { - validationSchema: advanceValidationSchema, - }); - - const { - handleSubmit, - register, - formState: { isDirty }, - } = formMethods; - function handleConfirm(event) { - void handleSubmit((values) => handleApplyFilter(values))(); - onConfirm?.(event); - } - - function handleCancel(event) { - handleCancelFilter(); - onCancel?.(event); - } - - const { onChange: onLivePreviewFieldChange, ...livePreviewFieldOptions } = - register('livePreview'); - - const disabledAction = filter.value && !isDirty; - - return ( - - - {enableEdit && ( - - - { - void onLivePreviewFieldChange(event); - submitHandler(); - }} - label="Live preview" - /> - - - - )} - - - submitHandler()} - /> - submitHandler()} /> - submitHandler()} /> - submitHandler()} /> - submitHandler()} /> - - - - - ); -} + const dispatch = useDispatch(); -interface SectionOptions { - onChange: () => void; -} - -function ExponentialSectionOptionsSection(options: SectionOptions) { - const { control } = useFormContext(); - const { onChange } = options; - const basedPath = getBaseKeyPath('exponential'); - - return ( - - - - ); -} -function GaussianOptionSection(options: SectionOptions) { - const { control } = useFormContext(); - const { onChange } = options; - const basedPath = getBaseKeyPath('gaussian'); - - return ( - - - - - ); -} -function SineBellOptionSection(options: SectionOptions) { - const { control } = useFormContext(); - const { onChange } = options; - const basedPath = getBaseKeyPath('sineBell'); - - return ( - - - - ); -} -function SineSquareOptionSection(options: SectionOptions) { - const { control } = useFormContext(); - const { onChange } = options; - const basedPath = getBaseKeyPath('sineSquare'); + const { filter, enableEdit = true, onCancel, onConfirm } = props; - return ( - - - + const applyHandler = useCallback( + (data) => { + const { options } = data; + dispatch({ + type: 'APPLY_APODIZATION_FILTER', + payload: { options }, + }); + }, + [dispatch], ); -} -function TrafOptionSection(options: SectionOptions) { - const { control } = useFormContext(); - const { onChange } = options; - const basedPath = getBaseKeyPath('traf'); - - return ( - - - + const changeHandler = useCallback( + (data) => { + const { livePreview, options } = data; + + dispatch({ + type: 'CALCULATE_APODIZATION_FILTER', + payload: { livePreview, options: structuredClone(options) }, + }); + }, + [dispatch], ); -} - -interface OptionsSectionProps extends SectionOptions { - algorithm: keyof Apodization1DOptions; - algorithmTitle: string; - children: ReactNode; -} - -function OptionsSection(options: OptionsSectionProps) { - const { onChange, algorithm, algorithmTitle, children } = options; - - const { setValue, watch } = useFormContext(); - const isApplyChecked = watch(`options.${algorithm}.apply`) || false; - - function handleToggleApply(event) { - const checked = event.target.checked; - setValue(`options.${algorithm}.apply`, checked); - onChange(); - } return ( - - } - headerStyle={{ backgroundColor: 'white' }} - > - -
- {children} -
-
-
+ ); } - -function getBaseKeyPath( - algorithm: keyof Apodization1DOptions, -): `options.${keyof Apodization1DOptions}.options` { - return `options.${algorithm}.options`; -} diff --git a/src/component/panels/filtersPanel/Filters/apodization/BaseApodizationOptions.tsx b/src/component/panels/filtersPanel/Filters/apodization/BaseApodizationOptions.tsx new file mode 100644 index 000000000..04475a86d --- /dev/null +++ b/src/component/panels/filtersPanel/Filters/apodization/BaseApodizationOptions.tsx @@ -0,0 +1,307 @@ +import { Switch } from '@blueprintjs/core'; +import type { Apodization1DOptions } from 'nmr-processing'; +import type { ReactNode } from 'react'; +import { FormProvider, useFormContext } from 'react-hook-form'; +import * as Yup from 'yup'; + +import Label from '../../../../elements/Label.js'; +import { NumberInput2Controller } from '../../../../elements/NumberInput2Controller.js'; +import { ReadOnly } from '../../../../elements/ReadOnly.js'; +import { Sections } from '../../../../elements/Sections.js'; +import { FilterActionButtons } from '../FilterActionButtons.js'; +import { HeaderContainer, StickyHeader } from '../InnerFilterHeader.js'; +import { useSharedApodization } from '../hooks/useSharedApodization.js'; +import type { + ApodizationFilterOptions, + UseSharedApodizationOptions, +} from '../hooks/useSharedApodization.js'; +import type { BaseFilterOptionsPanelProps } from '../index.js'; +import { formLabelStyle } from '../index.js'; + +const advanceValidationSchema = Yup.object().shape({ + options: Yup.object().shape({ + gaussian: Yup.object() + .shape({ + options: Yup.object().shape({ + lineBroadening: Yup.number().required(), + lineBroadeningCenter: Yup.number().required().min(0).max(1), + }), + }) + .notRequired(), + }), + livePreview: Yup.boolean().required(), +}); + +export function BaseApodizationOptions( + props: BaseFilterOptionsPanelProps & + Pick, +) { + const { + filter, + enableEdit = true, + onCancel, + onConfirm, + onApplyDispatch, + onChangeDispatch, + } = props; + + const { submitHandler, handleApplyFilter, handleCancelFilter, formMethods } = + useSharedApodization(filter, { + validationSchema: advanceValidationSchema, + onApplyDispatch, + onChangeDispatch, + }); + + const { + handleSubmit, + register, + formState: { isDirty }, + } = formMethods; + function handleConfirm(event) { + void handleSubmit((values) => handleApplyFilter(values))(); + onConfirm?.(event); + } + + function handleCancel(event) { + handleCancelFilter(); + onCancel?.(event); + } + + const { onChange: onLivePreviewFieldChange, ...livePreviewFieldOptions } = + register('livePreview'); + + const disabledAction = filter.value && !isDirty; + + return ( + + + {enableEdit && ( + + + { + void onLivePreviewFieldChange(event); + submitHandler(); + }} + label="Live preview" + /> + + + + )} + + + submitHandler()} + /> + submitHandler()} /> + submitHandler()} /> + submitHandler()} /> + submitHandler()} /> + + + + + ); +} + +interface SectionOptions { + onChange: () => void; +} + +function ExponentialSectionOptionsSection(options: SectionOptions) { + const { control } = useFormContext(); + const { onChange } = options; + const basedPath = getBaseKeyPath('exponential'); + + return ( + + + + ); +} +function GaussianOptionSection(options: SectionOptions) { + const { control } = useFormContext(); + const { onChange } = options; + const basedPath = getBaseKeyPath('gaussian'); + + return ( + + + + + ); +} +function SineBellOptionSection(options: SectionOptions) { + const { control } = useFormContext(); + const { onChange } = options; + const basedPath = getBaseKeyPath('sineBell'); + + return ( + + + + ); +} +function SineSquareOptionSection(options: SectionOptions) { + const { control } = useFormContext(); + const { onChange } = options; + const basedPath = getBaseKeyPath('sineSquare'); + + return ( + + + + ); +} +function TrafOptionSection(options: SectionOptions) { + const { control } = useFormContext(); + const { onChange } = options; + const basedPath = getBaseKeyPath('traf'); + + return ( + + + + ); +} + +interface OptionsSectionProps extends SectionOptions { + algorithm: keyof Apodization1DOptions; + algorithmTitle: string; + children: ReactNode; +} + +function OptionsSection(options: OptionsSectionProps) { + const { onChange, algorithm, algorithmTitle, children } = options; + + const { setValue, watch } = useFormContext(); + const isApplyChecked = watch(`options.${algorithm}.apply`) || false; + + function handleToggleApply(event) { + const checked = event.target.checked; + setValue(`options.${algorithm}.apply`, checked); + onChange(); + } + + return ( + + } + headerStyle={{ backgroundColor: 'white' }} + > + +
+ {children} +
+
+
+ ); +} + +function getBaseKeyPath( + algorithm: keyof Apodization1DOptions, +): `options.${keyof Apodization1DOptions}.options` { + return `options.${algorithm}.options`; +} diff --git a/src/component/panels/filtersPanel/Filters/hooks/useSharedApodization.tsx b/src/component/panels/filtersPanel/Filters/hooks/useSharedApodization.tsx index d8477405e..05a263dfb 100644 --- a/src/component/panels/filtersPanel/Filters/hooks/useSharedApodization.tsx +++ b/src/component/panels/filtersPanel/Filters/hooks/useSharedApodization.tsx @@ -33,18 +33,27 @@ const initialValues: ApodizationOptions = { livePreview: true, }; -interface UseSharedApodizationOptions { +export interface UseSharedApodizationOptions { validationSchema?: Yup.ObjectSchema; applyFilterOnload?: boolean; + onApplyDispatch: (options: ApodizationOptions) => void; + onChangeDispatch: (options: ApodizationOptions) => void; } +export type ApodizationFilterOptions = + | ExtractFilterEntry<'apodization'> + | ExtractFilterEntry<'apodizationDimension1'> + | ExtractFilterEntry<'apodizationDimension2'>; + export const useSharedApodization = ( - filter: ExtractFilterEntry<'apodization'> | null, + filter: ApodizationFilterOptions | null, options: UseSharedApodizationOptions, ) => { const { validationSchema = simpleValidationSchema, applyFilterOnload = false, + onApplyDispatch, + onChangeDispatch, } = options; const dispatch = useDispatch(); @@ -73,15 +82,12 @@ export const useSharedApodization = ( const onChange = useCallback( (values: ApodizationOptions) => { - const { livePreview, options } = values; + const { livePreview } = values; if (livePreview || previousPreviewRef.current !== livePreview) { - dispatch({ - type: 'CALCULATE_APODIZATION_FILTER', - payload: { livePreview, options: structuredClone(options) }, - }); + onChangeDispatch(structuredClone(values)); } }, - [dispatch], + [onChangeDispatch], ); const handleApplyFilter = useCallback( @@ -89,28 +95,20 @@ export const useSharedApodization = ( values: ApodizationOptions, triggerSource: 'apply' | 'onChange' = 'apply', ) => { - const { livePreview, options } = values; - switch (triggerSource) { - case 'onChange': { - onChange(values); - break; - } - case 'apply': { - dispatch({ - type: 'APPLY_APODIZATION_FILTER', - payload: { options }, - }); - clearSyncFilterOptions(); - - break; - } - default: - break; + const { livePreview } = values; + + if (triggerSource === 'onChange') { + onChange(values); + } + + if (triggerSource === 'apply') { + onApplyDispatch(values); + clearSyncFilterOptions(); } previousPreviewRef.current = livePreview; }, - [clearSyncFilterOptions, dispatch, onChange], + [clearSyncFilterOptions, onApplyDispatch, onChange], ); const handleCancelFilter = useCallback(() => { diff --git a/src/component/panels/filtersPanel/Filters/index.tsx b/src/component/panels/filtersPanel/Filters/index.tsx index d2fd5cff4..368d03549 100644 --- a/src/component/panels/filtersPanel/Filters/index.tsx +++ b/src/component/panels/filtersPanel/Filters/index.tsx @@ -3,6 +3,8 @@ import { Filters1D, Filters2D } from 'nmr-processing'; import type { LabelStyle } from '../../../elements/Label.js'; +import ApodizationDimensionOneOptionsPanel from './ApodizationDimensionOneOptionsPanel.js'; +import ApodizationDimensionTwoOptionsPanel from './ApodizationDimensionTwoOptionsPanel.js'; import ApodizationOptionsPanel from './ApodizationOptionsPanel.js'; import BaseLineCorrectionOptionsPanel from './BaseLineCorrectionOptionsPanel.js'; import ExclusionZonesOptionsPanel from './ExclusionZonesOptionsPanel.js'; @@ -20,10 +22,18 @@ const { exclusionZones, } = Filters1D; -const { shift2DX, shift2DY, phaseCorrectionTwoDimensions } = Filters2D; +const { + shift2DX, + shift2DY, + phaseCorrectionTwoDimensions, + apodizationDimension1, + apodizationDimension2, +} = Filters2D; export const filterOptionPanels = { [apodization.name]: ApodizationOptionsPanel, + [apodizationDimension1.name]: ApodizationDimensionOneOptionsPanel, + [apodizationDimension2.name]: ApodizationDimensionTwoOptionsPanel, [phaseCorrection.name]: PhaseCorrectionOptionsPanel, [zeroFilling.name]: ZeroFillingOptionsPanel, [phaseCorrectionTwoDimensions.name]: PhaseCorrectionTwoDimensionsOptionsPanel, diff --git a/src/component/reducer/Reducer.ts b/src/component/reducer/Reducer.ts index 29b3785d0..efb932087 100644 --- a/src/component/reducer/Reducer.ts +++ b/src/component/reducer/Reducer.ts @@ -5,10 +5,7 @@ import { produce, original } from 'immer'; import type { CorrelationData } from 'nmr-correlation'; import { buildCorrelationData } from 'nmr-correlation'; import type { Spectrum, ViewState } from 'nmr-load-save'; -import type { - BaselineCorrectionZone, - Apodization1DOptions, -} from 'nmr-processing'; +import type { BaselineCorrectionZone } from 'nmr-processing'; import type { Reducer } from 'react'; import type { StateMoleculeExtended } from '../../data/molecules/Molecule.js'; @@ -169,7 +166,6 @@ export const getInitialState = (): State => ({ zones: [], livePreview: true, }, - apodizationOptions: {} as Apodization1DOptions, twoDimensionPhaseCorrection: { activeTraceDirection: 'horizontal', addTracesToBothDirections: true, @@ -347,7 +343,6 @@ export interface State { options: any; livePreview: boolean; }; - apodizationOptions: Apodization1DOptions; /** * pivot point for manual phase correction * @default {value:0,index:0} @@ -463,8 +458,28 @@ function innerSpectrumReducer(draft: Draft, action: Action) { return FiltersActions.handleShiftSpectrumAlongXAxis(draft, action); case 'APPLY_APODIZATION_FILTER': return FiltersActions.handleApplyApodizationFilter(draft, action); + case 'APPLY_APODIZATION_DIMENSION_ONE_FILTER': + return FiltersActions.handleApplyApodizationDimensionOneFilter( + draft, + action, + ); + case 'APPLY_APODIZATION_DIMENSION_TWO_FILTER': + return FiltersActions.handleApplyApodizationDimensionTwoFilter( + draft, + action, + ); case 'CALCULATE_APODIZATION_FILTER': return FiltersActions.handleCalculateApodizationFilter(draft, action); + case 'CALCULATE_APODIZATION_DIMENSION_ONE_FILTER': + return FiltersActions.handleCalculateApodizationDimensionOneFilter( + draft, + action, + ); + case 'CALCULATE_APODIZATION_DIMENSION_TWO_FILTER': + return FiltersActions.handleCalculateApodizationDimensionTwoFilter( + draft, + action, + ); case 'APPLY_ZERO_FILLING_FILTER': return FiltersActions.handleApplyZeroFillingFilter(draft, action); case 'CALCULATE_ZERO_FILLING_FILTER': diff --git a/src/component/reducer/actions/FiltersActions.ts b/src/component/reducer/actions/FiltersActions.ts index 84f183528..fe8c41cc1 100644 --- a/src/component/reducer/actions/FiltersActions.ts +++ b/src/component/reducer/actions/FiltersActions.ts @@ -3,14 +3,18 @@ import type { NmrData1D, NmrData2DFt } from 'cheminfo-types'; import type { Draft } from 'immer'; import { current } from 'immer'; import { xFindClosestIndex } from 'ml-spectra-processing'; -import type { ActiveSpectrum, Spectrum, Spectrum1D } from 'nmr-load-save'; +import type { + ActiveSpectrum, + Spectrum, + Spectrum1D, + Spectrum2D, +} from 'nmr-load-save'; import { getBaselineZonesByDietrich, Filters1DManager, Filters2DManager, Filters1D, Filters2D, - default1DApodization, } from 'nmr-processing'; import type { BaselineCorrectionOptions, @@ -90,10 +94,26 @@ type ApodizationFilterAction = ActionType< 'APPLY_APODIZATION_FILTER', { options: Apodization1DOptions } >; +type ApodizationDimensionOneFilterAction = ActionType< + 'APPLY_APODIZATION_DIMENSION_ONE_FILTER', + { options: Apodization1DOptions } +>; +type ApodizationDimensionTwoFilterAction = ActionType< + 'APPLY_APODIZATION_DIMENSION_TWO_FILTER', + { options: Apodization1DOptions } +>; type ApodizationFilterLiveAction = ActionType< 'CALCULATE_APODIZATION_FILTER', { options: Apodization1DOptions; livePreview: boolean } >; +type ApodizationDimensionOneFilterLiveAction = ActionType< + 'CALCULATE_APODIZATION_DIMENSION_ONE_FILTER', + { options: Apodization1DOptions; livePreview: boolean } +>; +type ApodizationDimensionTwoFilterLiveAction = ActionType< + 'CALCULATE_APODIZATION_DIMENSION_TWO_FILTER', + { options: Apodization1DOptions; livePreview: boolean } +>; type ZeroFillingFilterAction = ActionType< 'APPLY_ZERO_FILLING_FILTER', { options: { nbPoints: number } } @@ -180,7 +200,11 @@ type SetTwoDimensionPhaseCorrectionPivotPoint = ActionType< export type FiltersActions = | ShiftSpectrumAction | ApodizationFilterAction + | ApodizationDimensionOneFilterAction + | ApodizationDimensionTwoFilterAction | ApodizationFilterLiveAction + | ApodizationDimensionOneFilterLiveAction + | ApodizationDimensionTwoFilterLiveAction | ZeroFillingFilterAction | ZeroFillingFilterLiveAction | ManualPhaseCorrectionFilterAction @@ -549,14 +573,8 @@ function beforeRollback(draft: Draft, filterKey) { } } function afterRollback(draft: Draft, filterKey) { - // const activeSpectrum = getActiveSpectrum(draft); - switch (filterKey) { - case apodization.name: { - draft.toolOptions.data.apodizationOptions = - structuredClone(default1DApodization); - break; - } + //specify the filters here default: break; } @@ -600,17 +618,6 @@ function disableLivePreview(draft: Draft, id: string) { if (baselineCorrection.name !== id) { setDomain(draft); } - - // reset default options - switch (id) { - case apodization.name: { - draft.toolOptions.data.apodizationOptions = - structuredClone(default1DApodization); - break; - } - default: - break; - } } function isOneDimensionShift( @@ -750,7 +757,6 @@ function handleCalculateApodizationFilter( const _data = { data: { x, re, im }, info } as Spectrum1D; - draft.toolOptions.data.apodizationOptions = options; apodization.apply(_data, options); const { im: newIm, re: newRe } = _data.data; const datum = draft.data[index]; @@ -764,6 +770,72 @@ function handleCalculateApodizationFilter( disableLivePreview(draft, apodization.name); } } +//action +function handleCalculateApodizationDimensionOneFilter( + draft: Draft, + action: ApodizationDimensionOneFilterLiveAction, +) { + const activeSpectrum = getActiveSpectrum(draft); + + if (!activeSpectrum || !draft.tempData) { + return; + } + + const index = activeSpectrum.index; + const { livePreview, options } = action.payload; + if (livePreview) { + const { data, info } = current(draft).tempData[index]; + + const _data = structuredClone({ + data, + info, + }) as Spectrum2D; + + Filters2D.apodizationDimension1.apply(_data, options); + + const datum = draft.data[index]; + + if (!isSpectrum2D(datum)) { + return; + } + datum.data = _data.data; + } else { + disableLivePreview(draft, Filters2D.apodizationDimension1.name); + } +} +//action +function handleCalculateApodizationDimensionTwoFilter( + draft: Draft, + action: ApodizationDimensionTwoFilterLiveAction, +) { + const activeSpectrum = getActiveSpectrum(draft); + + if (!activeSpectrum || !draft.tempData) { + return; + } + + const index = activeSpectrum.index; + const { livePreview, options } = action.payload; + if (livePreview) { + const { data, info } = current(draft).tempData[index]; + + const _data = structuredClone({ + data, + info, + }) as Spectrum2D; + + Filters2D.apodizationDimension2.apply(_data, options); + + const datum = draft.data[index]; + + if (!isSpectrum2D(datum)) { + return; + } + datum.data = _data.data; + } else { + disableLivePreview(draft, Filters2D.apodizationDimension2.name); + } +} //action function handleApplyApodizationFilter( @@ -788,6 +860,52 @@ function handleApplyApodizationFilter( updateView(draft, apodization.domainUpdateRules); } +//action +function handleApplyApodizationDimensionOneFilter( + draft: Draft, + action: ApodizationDimensionOneFilterAction, +) { + const activeSpectrum = getActiveSpectrum(draft); + + if (!activeSpectrum || !draft.tempData) { + return; + } + + const index = activeSpectrum.index; + + Filters2DManager.applyFilters(draft.tempData[index], [ + { + name: 'apodizationDimension1', + value: action.payload.options, + }, + ]); + draft.data[index] = draft.tempData[index]; + + updateView(draft, apodization.domainUpdateRules); +} +//action +function handleApplyApodizationDimensionTwoFilter( + draft: Draft, + action: ApodizationDimensionTwoFilterAction, +) { + const activeSpectrum = getActiveSpectrum(draft); + + if (!activeSpectrum || !draft.tempData) { + return; + } + + const index = activeSpectrum.index; + + Filters2DManager.applyFilters(draft.tempData[index], [ + { + name: 'apodizationDimension2', + value: action.payload.options, + }, + ]); + draft.data[index] = draft.tempData[index]; + + updateView(draft, apodization.domainUpdateRules); +} //action function handleApplyFFTFilter(draft: Draft) { @@ -1589,6 +1707,8 @@ export { handleShiftSpectrumAlongXAxis, handleApplyZeroFillingFilter, handleApplyApodizationFilter, + handleApplyApodizationDimensionOneFilter, + handleApplyApodizationDimensionTwoFilter, handleApplyFFTFilter, handleApplyFFtDimension1Filter, handleApplyFFtDimension2Filter, @@ -1600,6 +1720,8 @@ export { calculateBaseLineCorrection, handleCalculateBaseLineCorrection, handleCalculateApodizationFilter, + handleCalculateApodizationDimensionOneFilter, + handleCalculateApodizationDimensionTwoFilter, handleCalculateZeroFillingFilter, handleEnableFilter, handleDeleteFilter, diff --git a/src/component/toolbar/ToolBar.tsx b/src/component/toolbar/ToolBar.tsx index 1cc965752..da82c7aed 100644 --- a/src/component/toolbar/ToolBar.tsx +++ b/src/component/toolbar/ToolBar.tsx @@ -305,6 +305,20 @@ export default function ToolBar() { }, icon: , }, + { + id: 'apodizationDimension1', + tooltip: { + title: options.apodizationDimension1.label, + }, + icon: , + }, + { + id: 'apodizationDimension2', + tooltip: { + title: options.apodizationDimension2.label, + }, + icon: , + }, { id: 'zeroFilling', tooltip: { diff --git a/src/component/toolbar/ToolTypes.ts b/src/component/toolbar/ToolTypes.ts index aa5207699..53af6461d 100644 --- a/src/component/toolbar/ToolTypes.ts +++ b/src/component/toolbar/ToolTypes.ts @@ -1,6 +1,6 @@ import type { NMRiumToolBarPreferences } from 'nmr-load-save'; import type { Info1D, Info2D } from 'nmr-processing'; -import { Filters1D } from 'nmr-processing'; +import { Filters1D, Filters2D } from 'nmr-processing'; import type { DisplayerMode } from '../reducer/Reducer.js'; @@ -417,4 +417,34 @@ export const options: RecordOptions = { isFilter: false, isToggle: false, }, + apodizationDimension1: { + id: Filters2D.apodizationDimension1.name, + label: Filters2D.apodizationDimension1.label, + hasOptionPanel: true, + isFilter: true, + mode: '2D', + spectraOptions: [ + { + info: [{ key: 'isFid', value: true }], + active: true, + }, + ], + isToggle: true, + isExperimental: true, + }, + apodizationDimension2: { + id: Filters2D.apodizationDimension2.name, + label: Filters2D.apodizationDimension2.label, + hasOptionPanel: true, + isFilter: true, + mode: '2D', + spectraOptions: [ + { + info: [{ key: 'isFt', value: true }], + active: true, + }, + ], + isToggle: true, + isExperimental: true, + }, };