import { assign } from 'xstate'; const RESPONSE = { NO_NEVER: -1, CANCEL: 0, CREATE_REPORT: 1, ADD_SERIES: 2, SET_STUDY_AND_SERIES: 3, NO_NOT_FOR_SERIES: 4, HYDRATE_REPORT: 5, }; const machineConfiguration = { id: 'measurementTracking', initial: 'idle', context: { activeViewportId: null, trackedStudy: '', trackedSeries: [], ignoredSeries: [], // prevTrackedStudy: '', prevTrackedSeries: [], prevIgnoredSeries: [], // ignoredSRSeriesForHydration: [], isDirty: false, }, states: { off: { type: 'final', }, labellingOnly: { on: { TRACK_SERIES: [ { target: 'promptLabelAnnotation', actions: ['setPreviousState'], }, { target: 'off', }, ], }, }, idle: { entry: 'clearContext', on: { TRACK_SERIES: [ { target: 'promptLabelAnnotation', cond: 'isLabelOnMeasure', actions: ['setPreviousState'], }, { target: 'promptBeginTracking', actions: ['setPreviousState'], }, ], // Unused? We may only do PROMPT_HYDRATE_SR now? SET_TRACKED_SERIES: [ { target: 'tracking', actions: ['setTrackedStudyAndMultipleSeries', 'setIsDirtyToClean'], }, ], PROMPT_HYDRATE_SR: { target: 'promptHydrateStructuredReport', cond: 'hasNotIgnoredSRSeriesForHydration', }, RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport', HYDRATE_SR: 'hydrateStructuredReport', UPDATE_ACTIVE_VIEWPORT_ID: { actions: assign({ activeViewportId: (_, event) => event.activeViewportId, }), }, }, }, promptBeginTracking: { invoke: { src: 'promptBeginTracking', onDone: [ { target: 'tracking', actions: ['setTrackedStudyAndSeries', 'setIsDirty'], cond: 'shouldSetStudyAndSeries', }, { target: 'labellingOnly', cond: 'isLabelOnMeasureAndShouldKillMachine', }, { target: 'off', cond: 'shouldKillMachine', }, { target: 'idle', }, ], onError: { target: 'idle', }, }, }, tracking: { on: { TRACK_SERIES: [ { target: 'promptLabelAnnotation', cond: 'isLabelOnMeasure', actions: ['setPreviousState'], }, { target: 'promptTrackNewStudy', cond: 'isNewStudy', }, { target: 'promptTrackNewSeries', cond: 'isNewSeries', }, ], UNTRACK_SERIES: [ { target: 'tracking', actions: ['removeTrackedSeries', 'setIsDirty'], cond: 'hasRemainingTrackedSeries', }, { target: 'idle', }, ], SET_TRACKED_SERIES: [ { target: 'tracking', actions: ['setTrackedStudyAndMultipleSeries'], }, ], SAVE_REPORT: 'promptSaveReport', SET_DIRTY: [ { target: 'tracking', actions: ['setIsDirty'], cond: 'shouldSetDirty', }, { target: 'tracking', }, ], }, }, promptTrackNewSeries: { invoke: { src: 'promptTrackNewSeries', onDone: [ { target: 'tracking', actions: ['addTrackedSeries', 'setIsDirty'], cond: 'shouldAddSeries', }, { target: 'tracking', actions: [ 'discardPreviouslyTrackedMeasurements', 'setTrackedStudyAndSeries', 'setIsDirty', ], cond: 'shouldSetStudyAndSeries', }, { target: 'promptSaveReport', cond: 'shouldPromptSaveReport', }, { target: 'tracking', }, ], onError: { target: 'idle', }, }, }, promptTrackNewStudy: { invoke: { src: 'promptTrackNewStudy', onDone: [ { target: 'tracking', actions: [ 'discardPreviouslyTrackedMeasurements', 'setTrackedStudyAndSeries', 'setIsDirty', ], cond: 'shouldSetStudyAndSeries', }, { target: 'tracking', actions: ['ignoreSeries'], cond: 'shouldAddIgnoredSeries', }, { target: 'promptSaveReport', cond: 'shouldPromptSaveReport', }, { target: 'tracking', }, ], onError: { target: 'idle', }, }, }, promptSaveReport: { invoke: { src: 'promptSaveReport', onDone: [ // "clicked the save button" // - should clear all measurements // - show DICOM SR { target: 'idle', actions: ['clearAllMeasurements', 'showStructuredReportDisplaySetInActiveViewport'], cond: 'shouldSaveAndContinueWithSameReport', }, // "starting a new report" // - remove "just saved" measurements // - start tracking a new study + report { target: 'tracking', actions: ['discardPreviouslyTrackedMeasurements', 'setTrackedStudyAndSeries'], cond: 'shouldSaveAndStartNewReport', }, // Cancel, back to tracking { target: 'tracking', }, ], onError: { target: 'idle', }, }, }, promptHydrateStructuredReport: { invoke: { src: 'promptHydrateStructuredReport', onDone: [ { target: 'tracking', actions: [ 'setTrackedStudyAndMultipleSeries', 'jumpToSameImageInActiveViewport', 'setIsDirtyToClean', ], cond: 'shouldHydrateStructuredReport', }, { target: 'idle', actions: ['ignoreHydrationForSRSeries'], cond: 'shouldIgnoreHydrationForSR', }, ], onError: { target: 'idle', }, }, }, hydrateStructuredReport: { invoke: { src: 'hydrateStructuredReport', onDone: [ { target: 'tracking', actions: [ 'setTrackedStudyAndMultipleSeries', 'jumpToSameImageInActiveViewport', 'setIsDirtyToClean', ], }, ], onError: { target: 'idle', }, }, }, promptLabelAnnotation: { invoke: { src: 'promptLabelAnnotation', onDone: [ { target: 'labellingOnly', cond: 'wasLabellingOnly', }, { target: 'promptBeginTracking', cond: 'wasIdle', }, { target: 'promptTrackNewStudy', cond: 'wasTrackingAndIsNewStudy', }, { target: 'promptTrackNewSeries', cond: 'wasTrackingAndIsNewSeries', }, { target: 'tracking', cond: 'wasTracking', }, { target: 'off', }, ], }, }, }, strict: true, }; const defaultOptions = { services: { promptBeginTracking: (ctx, evt) => { // return { userResponse, StudyInstanceUID, SeriesInstanceUID } }, promptTrackNewStudy: (ctx, evt) => { // return { userResponse, StudyInstanceUID, SeriesInstanceUID } }, promptTrackNewSeries: (ctx, evt) => { // return { userResponse, StudyInstanceUID, SeriesInstanceUID } }, }, actions: { discardPreviouslyTrackedMeasurements: (ctx, evt) => { console.log('discardPreviouslyTrackedMeasurements: not implemented'); }, clearAllMeasurements: (ctx, evt) => { console.log('clearAllMeasurements: not implemented'); }, jumpToFirstMeasurementInActiveViewport: (ctx, evt) => { console.warn('jumpToFirstMeasurementInActiveViewport: not implemented'); }, showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => { console.warn('showStructuredReportDisplaySetInActiveViewport: not implemented'); }, clearContext: assign({ trackedStudy: '', trackedSeries: [], ignoredSeries: [], prevTrackedStudy: '', prevTrackedSeries: [], prevIgnoredSeries: [], }), // Promise resolves w/ `evt.data.*` setTrackedStudyAndSeries: assign((ctx, evt) => ({ prevTrackedStudy: ctx.trackedStudy, prevTrackedSeries: ctx.trackedSeries.slice(), prevIgnoredSeries: ctx.ignoredSeries.slice(), // trackedStudy: evt.data.StudyInstanceUID, trackedSeries: [evt.data.SeriesInstanceUID], ignoredSeries: [], })), setTrackedStudyAndMultipleSeries: assign((ctx, evt) => { const studyInstanceUID = evt.StudyInstanceUID || evt.data.StudyInstanceUID; const seriesInstanceUIDs = evt.SeriesInstanceUIDs || evt.data.SeriesInstanceUIDs; return { prevTrackedStudy: ctx.trackedStudy, prevTrackedSeries: ctx.trackedSeries.slice(), prevIgnoredSeries: ctx.ignoredSeries.slice(), // trackedStudy: studyInstanceUID, trackedSeries: [...ctx.trackedSeries, ...seriesInstanceUIDs], ignoredSeries: [], }; }), setIsDirtyToClean: assign((ctx, evt) => ({ isDirty: false, })), setIsDirty: assign((ctx, evt) => ({ isDirty: true, })), ignoreSeries: assign((ctx, evt) => ({ prevIgnoredSeries: [...ctx.ignoredSeries], ignoredSeries: [...ctx.ignoredSeries, evt.data.SeriesInstanceUID], })), ignoreHydrationForSRSeries: assign((ctx, evt) => ({ ignoredSRSeriesForHydration: [ ...ctx.ignoredSRSeriesForHydration, evt.data.srSeriesInstanceUID, ], })), addTrackedSeries: assign((ctx, evt) => ({ prevTrackedSeries: [...ctx.trackedSeries], trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID], })), removeTrackedSeries: assign((ctx, evt) => ({ prevTrackedSeries: ctx.trackedSeries.slice().filter(ser => ser !== evt.SeriesInstanceUID), trackedSeries: ctx.trackedSeries.slice().filter(ser => ser !== evt.SeriesInstanceUID), })), setPreviousState: assign((ctx, evt, meta) => { return { prevState: meta.state.value, }; }), }, guards: { // We set dirty any time we performan an action that: // - Tracks a new study // - Tracks a new series // - Adds a measurement to an already tracked study/series // // We set clean any time we restore from an SR // // This guard/condition is specific to "new measurements" // to make sure we only track dirty when the new measurement is specific // to a series we're already tracking // // tl;dr // Any report change, that is not a hydration of an existing report, should // result in a "dirty" report // // Where dirty means there would be "loss of data" if we blew away measurements // without creating a new SR. shouldSetDirty: (ctx, evt) => { return ( // When would this happen? evt.SeriesInstanceUID === undefined || ctx.trackedSeries.includes(evt.SeriesInstanceUID) ); }, wasLabellingOnly: (ctx, evt, condMeta) => { return ctx.prevState === 'labellingOnly'; }, wasIdle: (ctx, evt, condMeta) => { return ctx.prevState === 'idle'; }, wasTracking: (ctx, evt, condMeta) => { return ctx.prevState === 'tracking'; }, wasTrackingAndIsNewStudy: (ctx, evt, condMeta) => { return ( ctx.prevState === 'tracking' && !ctx.ignoredSeries.includes(evt.data.SeriesInstanceUID) && ctx.trackedStudy !== evt.data.StudyInstanceUID ); }, wasTrackingAndIsNewSeries: (ctx, evt, condMeta) => { return ( ctx.prevState === 'tracking' && !ctx.ignoredSeries.includes(evt.data.SeriesInstanceUID) && !ctx.trackedSeries.includes(evt.data.SeriesInstanceUID) ); }, shouldKillMachine: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.NO_NEVER, shouldAddSeries: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES, shouldSetStudyAndSeries: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES, shouldAddIgnoredSeries: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.NO_NOT_FOR_SERIES, shouldPromptSaveReport: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.CREATE_REPORT, shouldIgnoreHydrationForSR: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.CANCEL, shouldSaveAndContinueWithSameReport: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.CREATE_REPORT && evt.data.isBackupSave === true, shouldSaveAndStartNewReport: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.CREATE_REPORT && evt.data.isBackupSave === false, shouldHydrateStructuredReport: (ctx, evt) => evt.data && evt.data.userResponse === RESPONSE.HYDRATE_REPORT, // Has more than 1, or SeriesInstanceUID is not in list // --> Post removal would have non-empty trackedSeries array hasRemainingTrackedSeries: (ctx, evt) => ctx.trackedSeries.length > 1 || !ctx.trackedSeries.includes(evt.SeriesInstanceUID), hasNotIgnoredSRSeriesForHydration: (ctx, evt) => { return !ctx.ignoredSRSeriesForHydration.includes(evt.SeriesInstanceUID); }, isNewStudy: (ctx, evt) => !ctx.ignoredSeries.includes(evt.SeriesInstanceUID) && ctx.trackedStudy !== evt.StudyInstanceUID, isNewSeries: (ctx, evt) => !ctx.ignoredSeries.includes(evt.SeriesInstanceUID) && !ctx.trackedSeries.includes(evt.SeriesInstanceUID), }, }; export { defaultOptions, machineConfiguration, RESPONSE };