import type { Peak1D } from '@zakodium/nmr-types'; import type { PeaksViewState, RangesViewState, Spectrum1D, ViewState, } from '@zakodium/nmrium-core'; import type { Draft } from 'immer'; import { original } from 'immer'; import { xFindClosestIndex } from 'ml-spectra-processing'; import type { OptionsXYAutoPeaksPicking } from 'nmr-processing'; import { mapPeaks } from 'nmr-processing'; import { autoPeakPicking, getShiftX, isSpectrum1D, optimizePeaks, } from '../../../data/data1d/Spectrum1D/index.js'; import { defaultPeaksViewState } from '../../hooks/useActiveSpectrumPeaksViewState.js'; import { getDefaultRangesViewState } from '../../hooks/useActiveSpectrumRangesViewState.js'; import type { FilterType } from '../../utility/filterType.js'; import { getClosePeak } from '../../utility/getClosePeak.js'; import type { State } from '../Reducer.js'; import { getActiveSpectrum } from '../helper/getActiveSpectrum.js'; import getRange from '../helper/getRange.js'; import { getSpectrum } from '../helper/getSpectrum.js'; import type { ActionType } from '../types/ActionType.js'; type AddPeakAction = ActionType<'ADD_PEAK', { x: number }>; type AddPeaksAction = ActionType<'ADD_PEAKS', { startX: number; endX: number }>; type DeletePeakAction = ActionType< 'DELETE_PEAK', { id?: string; spectrumKey?: string } >; type OptimizePeaksAction = ActionType<'OPTIMIZE_PEAKS', { peaks: Peak1D[] }>; type AutoPeaksPickingAction = ActionType< 'AUTO_PEAK_PICKING', { maxNumberOfPeaks: number; minMaxRatio: number; noiseFactor: number; direction: OptionsXYAutoPeaksPicking['direction']; } >; type ChangePeaksShapeAction = ActionType< 'CHANGE_PEAK_SHAPE', { id: string; shape: Peak1D['shape']; } >; type TogglePeaksViewAction = ActionType< 'TOGGLE_PEAKS_VIEW_PROPERTY', { key: keyof FilterType; } >; export type PeaksActions = | AddPeakAction | AddPeaksAction | DeletePeakAction | OptimizePeaksAction | AutoPeaksPickingAction | ChangePeaksShapeAction | TogglePeaksViewAction | ActionType<'TOGGLE_PEAKS_DISPLAYING_MODE'>; //action function handleAddPeak(draft: Draft, action: AddPeakAction) { const { x: mouseXPosition } = action.payload; const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; const xShift = 10; const startX = mouseXPosition - xShift; const endX = mouseXPosition + xShift; const [from, to] = getRange(draft, { startX, endX }); const candidatePeak = getClosePeak(original(spectrum) as Spectrum1D, { from, to, }); if (candidatePeak) { const shiftX = getShiftX(spectrum); const peak: Peak1D = { id: crypto.randomUUID(), originalX: candidatePeak.x - shiftX, x: candidatePeak.x, y: candidatePeak.y, width: 1, shape: { kind: 'generalizedLorentzian', fwhm: 1, gamma: 0.5, }, }; spectrum.peaks.values.push(...mapPeaks([peak], spectrum)); } } //action function handleAddPeaks(draft: Draft, action: AddPeaksAction) { const { startX, endX } = action.payload; const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; const [from, to] = getRange(draft, { startX, endX }); if (from !== to) { const peak = getClosePeak(original(spectrum) as Spectrum1D, { from, to }); if (peak && !spectrum.peaks.values.some((p) => p.x === peak.x)) { const shiftX = getShiftX(spectrum); const newPeak: Peak1D = { id: crypto.randomUUID(), originalX: peak.x - shiftX, x: peak.x, y: peak.y, width: 1, shape: { kind: 'generalizedLorentzian', fwhm: 1, gamma: 0.5, }, }; spectrum.peaks.values.push(newPeak); } } } //action function handleDeletePeak(draft: Draft, action: DeletePeakAction) { const { id: peakId, spectrumKey } = action.payload; const spectrum = getSpectrum(draft, spectrumKey); if (!isSpectrum1D(spectrum)) return; if (!peakId) { spectrum.peaks.values = []; } else { const peakIndex = spectrum.peaks.values.findIndex( (p: any) => p.id === peakId, ); spectrum.peaks.values.splice(peakIndex, 1); } } //action function handleOptimizePeaks(draft: Draft, action: OptimizePeaksAction) { const { peaks } = action.payload; const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; const [from, to] = draft.xDomain; const newPeaks = optimizePeaks(spectrum, { from, to, peaks, }); spectrum.peaks.values = newPeaks; } //action function handleAutoPeakPicking( draft: Draft, action: AutoPeaksPickingAction, ) { const { maxNumberOfPeaks, minMaxRatio, noiseFactor, direction } = action.payload; const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; draft.toolOptions.selectedTool = 'zoom'; draft.toolOptions.selectedOptionPanel = null; const [from, to] = draft.xDomain; const windowFromIndex = xFindClosestIndex(spectrum.data.x, from); const windowToIndex = xFindClosestIndex(spectrum.data.x, to); const peaks = autoPeakPicking(spectrum, { maxNumberOfPeaks, minMaxRatio, noiseFactor, direction, windowFromIndex, windowToIndex, }); spectrum.peaks.values = spectrum.peaks.values.concat(peaks); } //action function handleChangePeakShape( draft: Draft, action: ChangePeaksShapeAction, ) { const { shape, id } = action.payload; const spectrum = getSpectrum(draft); if (!isSpectrum1D(spectrum)) return; const peakIndex = spectrum.peaks.values.findIndex((peak) => peak.id === id); if (peakIndex !== -1) { spectrum.peaks.values[peakIndex].shape = shape; } } //action function handleTogglePeaksViewProperty( draft: Draft, action: TogglePeaksViewAction, ) { const { key } = action.payload; togglePeaksViewProperty(draft, key); } function togglePeaksViewProperty( draft: Draft, key: keyof FilterType, ) { const activeSpectrum = getActiveSpectrum(draft); if (!activeSpectrum) return; const peaksView = draft.view.peaks; if (peaksView[activeSpectrum.id]) { peaksView[activeSpectrum.id][key] = !peaksView[activeSpectrum.id][key]; } else { const defaultPeaksView = { ...defaultPeaksViewState }; defaultPeaksView[key] = !defaultPeaksView[key]; peaksView[activeSpectrum.id] = defaultPeaksView; } } type TogglePeaksViewState = RangesViewState | PeaksViewState; function toggleDisplayingPeaks( draft: Draft, key: keyof Pick, ) { const { view: { spectra: { activeTab: nucleus }, }, } = draft; const activeSpectrum = getActiveSpectrum(draft); if (!activeSpectrum) return; const viewOptions = draft.view[key]; if (viewOptions[activeSpectrum.id]) { viewOptions[activeSpectrum.id].displayingMode = viewOptions[activeSpectrum.id].displayingMode === 'single' ? 'spread' : 'single'; } else { let defaultsViewOptions: TogglePeaksViewState; switch (key) { case 'peaks': defaultsViewOptions = { ...defaultPeaksViewState }; break; case 'ranges': defaultsViewOptions = getDefaultRangesViewState(nucleus); break; default: throw new Error(`Unknown view key: ${String(key)}`); } defaultsViewOptions.displayingMode = defaultsViewOptions.displayingMode === 'single' ? 'spread' : 'single'; viewOptions[activeSpectrum.id] = defaultsViewOptions; } } function handleChangePeaksDisplayingMode(draft: Draft) { toggleDisplayingPeaks(draft, 'peaks'); } export { handleAddPeak, handleAddPeaks, handleAutoPeakPicking, handleChangePeakShape, handleChangePeaksDisplayingMode, handleDeletePeak, handleOptimizePeaks, handleTogglePeaksViewProperty, toggleDisplayingPeaks, };