diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index f31f783f03..e8aab79b31 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -1,8 +1,9 @@ import { metaData, utilities } from '@cornerstonejs/core'; -import OHIF, { DicomMetadataStore } from '@ohif/core'; +import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; import dcmjs from 'dcmjs'; import { adaptersSR } from '@cornerstonejs/adapters'; +import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default'; import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState'; import hydrateStructuredReport from './utils/hydrateStructuredReport'; @@ -11,7 +12,6 @@ const { MeasurementReport } = adaptersSR.Cornerstone3D; const { log } = OHIF; /** - * * @param measurementData An array of measurements from the measurements service * that you wish to serialize. * @param additionalFindingTypes toolTypes that should be stored with labels as Findings @@ -43,8 +43,30 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {}) const commandsModule = (props: withAppTypes) => { const { servicesManager, extensionManager, commandsManager } = props; - const { customizationService, displaySetService, viewportGridService } = servicesManager.services; + const { customizationService, measurementService, viewportGridService, uiDialogService } = + servicesManager.services; + const actions = { + changeColorMeasurement: ({ uid }) => { + // When this gets supported, it probably belongs in cornerstone, not sr + throw new Error('Unsupported operation: changeColorMeasurement'); + // const { color } = measurementService.getMeasurement(uid); + // const rgbaColor = { + // r: color[0], + // g: color[1], + // b: color[2], + // a: color[3] / 255.0, + // }; + // colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { + // if (actionId === 'cancel') { + // return; + // } + + // const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; + // segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); + // }); + }, + /** * * @param measurementData An array of measurements from the measurements service diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 89e498904a..229281ee21 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -14,7 +14,7 @@ import { annotation, } from '@cornerstonejs/tools'; -import { Types as OhifTypes } from '@ohif/core'; +import { Types as OhifTypes, utils } from '@ohif/core'; import i18n from '@ohif/i18n'; import { callLabelAutocompleteDialog, @@ -295,6 +295,60 @@ function commandsModule({ measurementService.update(updatedMeasurement.uid, updatedMeasurement, true); }, + /** + * Jumps to the specified (by uid) measurement in the active viewport. + * Also marks any provided display measurements isActive value + */ + jumpToMeasurement: ({ uid, displayMeasurements = [] }) => { + measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), uid); + for (const measurement of displayMeasurements) { + measurement.isActive = measurement.uid === uid; + } + }, + + removeMeasurement: ({ uid }) => { + measurementService.remove(uid); + }, + + renameMeasurement: ({ uid }) => { + const labelConfig = customizationService.get('measurementLabels'); + const measurement = measurementService.getMeasurement(uid); + showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => { + measurementService.update( + uid, + { + ...val, + }, + true + ); + }); + }, + + toggleLockMeasurement: ({ uid }) => { + measurementService.toggleLockMeasurement(uid); + }, + + toggleVisibilityMeasurement: ({ uid }) => { + measurementService.toggleVisibilityMeasurement(uid); + }, + + /** + * Clear the measurements + */ + clearMeasurements: options => { + const { measurementFilter } = options; + measurementService.clearMeasurements( + measurementFilter ? measurementFilter.bind(options) : null + ); + }, + + /** + * Download the CSV report for the measurements. + */ + downloadCSVMeasurementsReport: ({ measurementFilter }) => { + utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter)); + }, + // Retrieve value commands getActiveViewportEnabledElement: _getActiveViewportEnabledElement, @@ -1281,6 +1335,27 @@ function commandsModule({ updateMeasurement: { commandFn: actions.updateMeasurement, }, + clearMeasurements: { + commandFn: actions.clearMeasurements, + }, + jumpToMeasurement: { + commandFn: actions.jumpToMeasurement, + }, + removeMeasurement: { + commandFn: actions.removeMeasurement, + }, + renameMeasurement: { + commandFn: actions.renameMeasurement, + }, + toggleLockMeasurement: { + commandFn: actions.toggleLockMeasurement, + }, + toggleVisibilityMeasurement: { + commandFn: actions.toggleVisibilityMeasurement, + }, + downloadCSVMeasurementsReport: { + commandFn: actions.downloadCSVMeasurementsReport, + }, setViewportWindowLevel: { commandFn: actions.setViewportWindowLevel, }, diff --git a/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx b/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx new file mode 100644 index 0000000000..60d8b94d51 --- /dev/null +++ b/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { DicomMetadataStore, utils } from '@ohif/core'; +import { StudySummary } from '@ohif/ui-next'; + +const { formatDate } = utils; + +export function StudySummaryFromMetadata({ StudyInstanceUID }) { + if (!StudyInstanceUID) { + return null; + } + const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID); + if (!studyMeta?.series?.length) { + return null; + } + + const instanceMeta = studyMeta.series[0].instances[0]; + const { StudyDate, StudyDescription } = instanceMeta; + + return ( + <StudySummary + date={formatDate(StudyDate)} + description={StudyDescription} + ></StudySummary> + ); +} diff --git a/extensions/cornerstone/src/getPanelModule.tsx b/extensions/cornerstone/src/getPanelModule.tsx index bb3f65d9d8..20c3a43e7b 100644 --- a/extensions/cornerstone/src/getPanelModule.tsx +++ b/extensions/cornerstone/src/getPanelModule.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Toolbox } from '@ohif/ui-next'; import PanelSegmentation from './panels/PanelSegmentation'; import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel'; -import PanelMeasurementTable from './panels/PanelMeasurement'; +import PanelMeasurement from './panels/PanelMeasurement'; const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: withAppTypes) => { const wrappedPanelSegmentation = ({ configuration }) => { @@ -59,7 +59,7 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: const wrappedPanelMeasurement = ({ configuration }) => { return ( - <PanelMeasurementTable + <PanelMeasurement commandsManager={commandsManager} servicesManager={servicesManager} extensionManager={extensionManager} diff --git a/extensions/cornerstone/src/hooks/useMeasurements.ts b/extensions/cornerstone/src/hooks/useMeasurements.ts index ba659a35a5..e76445375d 100644 --- a/extensions/cornerstone/src/hooks/useMeasurements.ts +++ b/extensions/cornerstone/src/hooks/useMeasurements.ts @@ -62,10 +62,7 @@ export function useMeasurements(servicesManager, { measurementFilter }) { useEffect(() => { const updateDisplayMeasurements = () => { - let measurements = measurementService.getMeasurements(); - if (measurementFilter) { - measurements = measurements.filter(measurementFilter); - } + let measurements = measurementService.getMeasurements(measurementFilter); const mappedMeasurements = measurements.map(m => mapMeasurementToDisplay(m, displaySetService) ); diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 41f4c33ff0..f5204e82e8 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -53,6 +53,7 @@ import PanelSegmentation from './panels/PanelSegmentation'; import PanelMeasurement from './panels/PanelMeasurement'; import DicomUpload from './components/DicomUpload/DicomUpload'; import { useSegmentations } from './hooks/useSegmentations'; +import { StudySummaryFromMetadata } from './components/StudySummaryFromMetadata'; const { imageRetrieveMetadataProvider } = cornerstone.utilities; @@ -254,5 +255,6 @@ export { PanelSegmentation, PanelMeasurement, DicomUpload, + StudySummaryFromMetadata, }; export default cornerstoneExtension; diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index ed86b2ef7e..b2279d80a3 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -1,19 +1,29 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { utils } from '@ohif/core'; import { useViewportGrid } from '@ohif/ui-next'; import { MeasurementTable } from '@ohif/ui-next'; import debounce from 'lodash.debounce'; import { useMeasurements } from '../hooks/useMeasurements'; -import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default'; -export default function PanelMeasurementTable({ +const { + filterAdditionalFindings: filterAdditionalFinding, + filterOr, + filterAny, +} = utils.MeasurementFilters; + +export type withAppAndFilters = withAppTypes & { + measurementFilter: (item) => boolean; +}; + +export default function PanelMeasurement({ servicesManager, + commandsManager, customHeader, - measurementFilter, -}: withAppTypes): React.ReactNode { + measurementFilter = filterAny, +}: withAppAndFilters): React.ReactNode { const measurementsPanelRef = useRef(null); - const [viewportGrid] = useViewportGrid(); - const { measurementService, customizationService, uiDialogService } = servicesManager.services; + const { measurementService } = servicesManager.services; const displayMeasurements = useMeasurements(servicesManager, { measurementFilter, @@ -27,75 +37,37 @@ export default function PanelMeasurementTable({ } }, [displayMeasurements.length]); - const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => { - if (isActive) { - return; - } - - const measurements = [...displayMeasurements]; - const measurement = measurements.find(m => m.uid === uid); - - measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true)); - measurement.isActive = true; - }; - - const jumpToImage = (uid: string) => { - measurementService.jumpToMeasurement(viewportGrid.activeViewportId, uid); - onMeasurementItemClickHandler(uid, true); - }; - - const removeMeasurement = (uid: string) => { - measurementService.remove(uid); - }; - - const renameMeasurement = (uid: string) => { - jumpToImage(uid); - const labelConfig = customizationService.get('measurementLabels'); - const measurement = measurementService.getMeasurement(uid); - showLabelAnnotationPopup(measurement, uiDialogService, labelConfig).then(val => { - measurementService.update( - uid, - { - ...val, - }, - true - ); - }); - }; - - const changeColorMeasurement = (uid: string) => { - const { color } = measurementService.getMeasurement(uid); - const rgbaColor = { - r: color[0], - g: color[1], - b: color[2], - a: color[3] / 255.0, + const bindCommand = (name: string | string[], options?) => { + return (uid: string) => { + commandsManager.run(name, { ...options, uid }); }; - colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { - if (actionId === 'cancel') { - return; - } - - const color = [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]; - // segmentationService.setSegmentColor(viewportId, segmentationId, segmentIndex, color); - }); }; - const toggleLockMeasurement = (uid: string) => { - measurementService.toggleLockMeasurement(uid); - }; + const jumpToImage = bindCommand('jumpToMeasurement', { displayMeasurements }); + const removeMeasurement = bindCommand('removeMeasurement'); + const renameMeasurement = bindCommand(['jumpToMeasurement', 'renameMeasurement'], { + displayMeasurements, + }); + const toggleLockMeasurement = bindCommand('toggleLockMeasurement'); + const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement'); - const toggleVisibilityMeasurement = (uid: string) => { - measurementService.toggleVisibilityMeasurement(uid); - }; + const additionalFilter = filterAdditionalFinding(measurementService); const measurements = displayMeasurements.filter( - dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId + item => !additionalFilter(item) && measurementFilter(item) ); const additionalFindings = displayMeasurements.filter( - dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId + item => additionalFilter(item) && measurementFilter(item) ); + const onArgs = { + onClick: jumpToImage, + onDelete: removeMeasurement, + onToggleVisibility: toggleVisibilityMeasurement, + onToggleLocked: toggleLockMeasurement, + onRename: renameMeasurement, + }; + return ( <> <div @@ -104,13 +76,10 @@ export default function PanelMeasurementTable({ data-cy={'trackedMeasurements-panel'} > <MeasurementTable + key="tracked" title="Measurements" data={measurements} - onClick={jumpToImage} - onDelete={removeMeasurement} - onToggleVisibility={toggleVisibilityMeasurement} - onToggleLocked={toggleLockMeasurement} - onRename={renameMeasurement} + {...onArgs} // onColor={changeColorMeasurement} > <MeasurementTable.Header> @@ -129,14 +98,10 @@ export default function PanelMeasurementTable({ </MeasurementTable> {additionalFindings.length > 0 && ( <MeasurementTable + key="additional" data={additionalFindings} title="Additional Findings" - onClick={jumpToImage} - onDelete={removeMeasurement} - onToggleVisibility={toggleVisibilityMeasurement} - onToggleLocked={toggleLockMeasurement} - onRename={renameMeasurement} - // onColor={changeColorMeasurement} + {...onArgs} > <MeasurementTable.Body /> </MeasurementTable> diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index bac5b4a2ae..8f3b96952b 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -562,27 +562,13 @@ const commandsModule = ({ }; const definitions = { - showContextMenu: { - commandFn: actions.showContextMenu, - }, - closeContextMenu: { - commandFn: actions.closeContextMenu, - }, - clearMeasurements: { - commandFn: actions.clearMeasurements, - }, - displayNotification: { - commandFn: actions.displayNotification, - }, - setHangingProtocol: { - commandFn: actions.setHangingProtocol, - }, - toggleHangingProtocol: { - commandFn: actions.toggleHangingProtocol, - }, - navigateHistory: { - commandFn: actions.navigateHistory, - }, + showContextMenu: actions.showContextMenu, + closeContextMenu: actions.closeContextMenu, + clearMeasurements: actions.clearMeasurements, + displayNotification: actions.displayNotification, + setHangingProtocol: actions.setHangingProtocol, + toggleHangingProtocol: actions.toggleHangingProtocol, + navigateHistory: actions.navigateHistory, nextStage: { commandFn: actions.deltaStage, options: { direction: 1 }, @@ -591,18 +577,10 @@ const commandsModule = ({ commandFn: actions.deltaStage, options: { direction: -1 }, }, - setViewportGridLayout: { - commandFn: actions.setViewportGridLayout, - }, - toggleOneUp: { - commandFn: actions.toggleOneUp, - }, - openDICOMTagViewer: { - commandFn: actions.openDICOMTagViewer, - }, - updateViewportDisplaySet: { - commandFn: actions.updateViewportDisplaySet, - }, + setViewportGridLayout: actions.setViewportGridLayout, + toggleOneUp: actions.toggleOneUp, + openDICOMTagViewer: actions.openDICOMTagViewer, + updateViewportDisplaySet: actions.updateViewportDisplaySet, }; return { diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx index 147e9c6512..f33a1ce968 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -1,20 +1,12 @@ import React, { useEffect, useState } from 'react'; -import { PanelMeasurement } from '@ohif/extension-cornerstone'; +import { DicomMetadataStore, utils } from '@ohif/core'; import { useViewportGrid } from '@ohif/ui-next'; -import { StudySummary } from '@ohif/ui-next'; import { Button, Icons } from '@ohif/ui-next'; -import { DicomMetadataStore, utils } from '@ohif/core'; +import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone'; import { useTrackedMeasurements } from '../getContextModule'; -import { useTranslation } from 'react-i18next'; -const { downloadCSVReport, formatDate } = utils; - -const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { - key: undefined, // - date: '', // '07-Sep-2010', - modality: '', // 'CT', - description: '', // 'CHEST/ABD/PELVIS W CONTRAST', -}; +const { filterAnd, filterPlanarMeasurement, filterAny, filterMeasurementsBySeriesUID } = + utils.MeasurementFilters; function PanelMeasurementTableTracking({ servicesManager, @@ -22,52 +14,12 @@ function PanelMeasurementTableTracking({ commandsManager, }: withAppTypes) { const [viewportGrid] = useViewportGrid(); - const { t } = useTranslation('MeasurementTable'); - const { measurementService, customizationService } = servicesManager.services; + const { customizationService } = servicesManager.services; const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); const { trackedStudy, trackedSeries } = trackedMeasurements.context; - const [displayStudySummary, setDisplayStudySummary] = useState( - DISPLAY_STUDY_SUMMARY_INITIAL_VALUE - ); - - useEffect(() => { - const updateDisplayStudySummary = async () => { - if (trackedMeasurements.matches('tracking') && trackedStudy) { - const studyMeta = DicomMetadataStore.getStudy(trackedStudy); - if (!studyMeta || !studyMeta.series || studyMeta.series.length === 0) { - console.debug('Study metadata not available'); - return; - } - - const instanceMeta = studyMeta.series[0].instances[0]; - const { StudyDate, StudyDescription } = instanceMeta; - - const modalities = new Set(); - studyMeta.series.forEach(series => { - if (trackedSeries.includes(series.SeriesInstanceUID)) { - modalities.add(series.instances[0].Modality); - } - }); - const modality = Array.from(modalities).join('/'); - - setDisplayStudySummary(prevSummary => { - if (prevSummary.key !== trackedStudy) { - return { - key: trackedStudy, - date: StudyDate, - modality, - description: StudyDescription, - }; - } - return prevSummary; - }); - } else if (!trackedStudy) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); - } - }; - - updateDisplayStudySummary(); - }, [trackedMeasurements, trackedStudy, trackedSeries]); + const measurementFilter = trackedStudy + ? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries)) + : filterPlanarMeasurement; const { disableEditing } = customizationService.getCustomization( 'PanelMeasurement.disableEditing', @@ -79,20 +31,12 @@ function PanelMeasurementTableTracking({ return ( <> - {displayStudySummary.key && ( - <StudySummary - date={formatDate(displayStudySummary.date)} - description={displayStudySummary.description} - /> - )} + <StudySummaryFromMetadata StudyInstanceUID={trackedStudy} /> <PanelMeasurement servicesManager={servicesManager} extensionManager={extensionManager} commandsManager={commandsManager} - measurementFilter={measurement => - trackedStudy === measurement.referenceStudyUID && - trackedSeries.includes(measurement.referenceSeriesUID) - } + measurementFilter={measurementFilter} customHeader={({ additionalFindings, measurements }) => { const disabled = additionalFindings.length === 0 && measurements.length === 0; @@ -108,14 +52,9 @@ function PanelMeasurementTableTracking({ variant="ghost" className="pl-1.5" onClick={() => { - const measurements = measurementService.getMeasurements(); - const trackedMeasurements = measurements.filter( - m => - trackedStudy === m.referenceStudyUID && - trackedSeries.includes(m.referenceSeriesUID) - ); - - downloadCSVReport(trackedMeasurements); + commandsManager.runCommand('downloadCSVMeasurementsReport', { + measurementFilter, + }); }} > <Icons.Download className="h-5 w-5" /> @@ -140,7 +79,7 @@ function PanelMeasurementTableTracking({ variant="ghost" className="pl-0.5" onClick={() => { - measurementService.clearMeasurements(); + commandsManager.runCommand('clearMeasurements', { measurementFilter }); }} > <Icons.Delete /> diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts index 740ff90796..41d9d63413 100644 --- a/modes/basic-test-mode/src/index.ts +++ b/modes/basic-test-mode/src/index.ts @@ -15,7 +15,6 @@ const ohif = { wsiSopClassHandler: '@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler', thumbnailList: '@ohif/extension-default.panelModule.seriesList', - measurements: '@ohif/extension-default.panelModule.measurements', }; const tracked = { @@ -46,6 +45,7 @@ const dicomSeg = { const cornerstone = { panel: '@ohif/extension-cornerstone.panelModule.panelSegmentation', + measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement', }; const dicomPmap = { @@ -150,7 +150,8 @@ function modeFactory() { // leftPanels: [ohif.thumbnailList], // rightPanels: [dicomSeg.panel, ohif.measurements], leftPanels: [tracked.thumbnailList], - rightPanels: [cornerstone.panel, tracked.measurements], + // Can use cornerstone.measurements for all measurements + rightPanels: [cornerstone.panel, tracked.measurements, cornerstone.measurements], // rightPanelClosed: true, // optional prop to start with collapse panels viewports: [ { diff --git a/platform/core/src/classes/CommandsManager.ts b/platform/core/src/classes/CommandsManager.ts index 0138521f90..096054da80 100644 --- a/platform/core/src/classes/CommandsManager.ts +++ b/platform/core/src/classes/CommandsManager.ts @@ -161,6 +161,16 @@ export class CommandsManager { * Run one or more commands with specified extra options. * Returns the result of the last command run. * + * Example commands to run are: + * * 'updateMeasurement' + * * `{commandName: 'displayWhatever'}` + * * `['updateMeasurement', {commandName: 'displayWhatever'}]` + * * `{ commands: 'updateMeasurement' }` + * * `{ commands: ['updateMeasurement', {commandName: 'displayWhatever'}]}` + * + * Note how the various styles can be mixed, simplifying the declaration of + * sets of commands. + * * @param toRun - A specification of one or more commands, * typically an object of { commandName, commandOptions, context } * or an array of such objects. It can also be a single commandName as string @@ -169,7 +179,7 @@ export class CommandsManager { * the commandOptions specified in the base. */ public run( - toRun: Command | Commands | Command[] | string | undefined, + toRun: Command | Commands | (Command | string)[] | string | undefined, options?: Record<string, unknown> ): unknown { if (!toRun) { diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index 588695d107..af620b535a 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -597,7 +597,10 @@ export default class ExtensionManager extends PubSubService { } Object.keys(definitions).forEach(commandName => { - const commandDefinition = definitions[commandName]; + let commandDefinition = definitions[commandName]; + if (typeof commandDefinition === 'function') { + commandDefinition = { commandFn: commandDefinition }; + } const commandHasContextThatDoesNotExist = commandDefinition.context && !this._commandsManager.getContext(commandDefinition.context); diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 4f823c9563..0fa3bf7c82 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -92,6 +92,8 @@ const VALUE_TYPES = { ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual', }; +export type MeasurementFilter = (measurement) => boolean; + /** * MeasurementService class that supports source management and measurement management. * Sources can be any library that can provide "annotations" (e.g. cornerstone-tools, cornerstone, etc.) @@ -108,7 +110,7 @@ class MeasurementService extends PubSubService { public static REGISTRATION = { name: 'measurementService', altName: 'MeasurementService', - create: ({ configuration = {} }) => { + create: _options => { return new MeasurementService(); }, }; @@ -120,10 +122,11 @@ class MeasurementService extends PubSubService { private measurements = new Map(); private unmappedMeasurements = new Map(); + private sources = {}; + private mappings = {}; + constructor() { super(EVENTS); - this.sources = {}; - this.mappings = {}; } /** @@ -164,12 +167,16 @@ class MeasurementService extends PubSubService { } /** - * Get all measurements. + * Gets measurements, optionally filtered by the filter + * function. * * @return {Measurement[]} Array of measurements */ - getMeasurements() { - return [...this.measurements.values()]; + public getMeasurements(filter?: MeasurementFilter) { + const measurements = [...this.measurements.values()]; + return filter + ? measurements.filter(measurement => filter.call(this, measurement)) + : measurements; } /** @@ -582,11 +589,20 @@ class MeasurementService extends PubSubService { }); } - clearMeasurements() { + /** + * Clears measurements that match the filter, defaulting to all of them. + * That allows, for example, clearing all of a single studies measurements + * without needing to clear other measurements. + */ + public clearMeasurements(filter?: MeasurementFilter) { // Make a copy of the measurements - const measurements = [...this.measurements.values(), ...this.unmappedMeasurements.values()]; - this.unmappedMeasurements.clear(); - this.measurements.clear(); + const toClear = this.getMeasurements(filter); + const unmappedClear = filter + ? [...this.unmappedMeasurements.values()].filter(filter) + : this.unmappedMeasurements; + const measurements = [...toClear, ...unmappedClear]; + unmappedClear.forEach(measurement => this.unmappedMeasurements.delete(measurement.uid)); + toClear.forEach(measurement => this.measurements.delete(measurement.uid)); this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); } diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 85d18f71d5..9e7edfd85f 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -43,6 +43,7 @@ describe('Top level exports', () => { 'progressTrackingUtils', 'uuidv4', 'addAccessors', + 'MeasurementFilters', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts index 74c9cdd381..e2e4d03818 100644 --- a/platform/core/src/utils/index.ts +++ b/platform/core/src/utils/index.ts @@ -39,6 +39,7 @@ import { import { splitComma, getSplitParam } from './splitComma'; import { createStudyBrowserTabs } from './createStudyBrowserTabs'; import { sopClassDictionary } from './sopClassDictionary'; +import * as MeasurementFilters from './measurementFilters'; // Commented out unused functionality. // Need to implement new mechanism for derived displaySets using the displaySetManager. @@ -84,6 +85,7 @@ const utils = { getSplitParam, generateAcceptHeader, createStudyBrowserTabs, + MeasurementFilters, }; export { @@ -117,6 +119,7 @@ export { getSplitParam, generateAcceptHeader, createStudyBrowserTabs, + MeasurementFilters, }; export default utils; diff --git a/platform/core/src/utils/measurementFilters.ts b/platform/core/src/utils/measurementFilters.ts new file mode 100644 index 0000000000..db36a305c7 --- /dev/null +++ b/platform/core/src/utils/measurementFilters.ts @@ -0,0 +1,110 @@ +/** + * Returns a filter function which filters for measurements belonging to both + * the study and series. + */ +export function filterMeasurementsBySeriesUID(selectedSeries: string[]) { + return measurement => selectedSeries.includes(measurement.referenceSeriesUID); +} + +/** + * @returns true for measurements include referencedImageId (coplanar with an image) + */ +export function filterPlanarMeasurement(measurement) { + return measurement?.referencedImageId; +} + +/** A filter that always returns true */ +export function filterAny(_measurement) { + return true; +} + +/** A filter that excludes everything */ +export function filterNone(_measurement) { + return false; +} + +/** + * Filters the measurements which are found in any of the specified + * filters. Strings will be looked up by name. + */ +export function filterOr(...filters) { + return function (item) { + for (let filter of filters) { + if (typeof filter === 'string') { + filter = this[filter]; + } + if (typeof filter !== 'function') { + continue; + } + if (filter.call(this, item)) { + return true; + } + } + return false; + }; +} + +/** + * Filters for additional findings, that is, measurements with + * a value of type point, and having a referenced image + */ +export function filterAdditionalFindings(measurementService) { + const { POINT } = measurementService.VALUE_TYPES; + return dm => dm.type === POINT && dm.referencedImageId; +} + +/** + * Returns a filter that applies the second filter unless the first filter would + * include the given measurement. + * That is, (!filterUnless) && filterThen + */ +export function filterUnless(filterUnless, filterThen) { + return item => (filterUnless(item) ? false : filterThen(item)); +} + +const isString = s => typeof s === 'string' || s instanceof String; + +/** + * Returns true if all the filters return true. + * Any filter can be a string name of a filter on the "this" object + * called on the final filter call. + */ +export function filterAnd(...filters) { + return function (item) { + for (const filter of filters) { + if (isString(filter)) { + if (!this[filter](item)) { + return false; + } + } else if (!filter.call(this, item)) { + return false; + } + } + return true; + }; +} + +/** + * Returns a filter that returns true if none of the filters supplied return true. + * Any filter supplied can be a name, in which case hte filter will be retrieved + * from "this" object on the call. + * + * For example, for filterNot("otherFilterName"), if that is called on + * `{ otherFilterName: filterNone }` + * then otherFilterName will be called, returning false in this case and + * filterNot will return true. + * + * + */ +export function filterNot(...filters) { + if (filters.length !== 1) { + return filterAnd.apply(null, filters.map(filterNot)); + } + const [filter] = filters; + if (isString(filter)) { + return function (item) { + return !this[filter](item); + }; + } + return item => !filter(item); +}