From dd246f3501fd320876480871f64b4e8bc5567e2c Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 13 Nov 2024 20:22:32 -0500 Subject: [PATCH 01/10] feat: Start using group filtering to define measurements table layout --- .../src/commandsModule.ts | 24 ++++- .../src/panels/PanelMeasurement.tsx | 8 +- .../panels/PanelMeasurementTableTracking.tsx | 47 +++++--- .../MeasurementService/MeasurementService.ts | 101 ++++++++++++++++-- 4 files changed, 152 insertions(+), 28 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index e4cfb53b463..42dd7ff9894 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -1,6 +1,6 @@ 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'; @@ -42,7 +42,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {}) const commandsModule = (props: withAppTypes) => { const { servicesManager } = props; - const { customizationService } = servicesManager.services; + const { customizationService, measurementService } = servicesManager.services; const actions = { /** * @@ -61,6 +61,20 @@ const commandsModule = (props: withAppTypes) => { window.location.assign(objectUrl); }, + /** + * Clear the measurements + */ + clearMeasurements: ({ measurementFilter }) => { + measurementService.clearMeasurements(measurementFilter); + }, + + /** + * Download the CSV report for the measurements. + */ + downloadCSVMeasurementsReport: ({ measurementFilter }) => { + utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter)); + }, + /** * * @param measurementData An array of measurements from the measurements service @@ -132,6 +146,12 @@ const commandsModule = (props: withAppTypes) => { storeMeasurements: { commandFn: actions.storeMeasurements, }, + clearMeasurements: { + commandFn: actions.clearMeasurements, + }, + downloadCSVMeasurementsReport: { + commandFn: actions.downloadCSVMeasurementsReport, + }, }; return { diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index 3652b31bb58..f75dc29f8eb 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -8,7 +8,7 @@ import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-def export default function PanelMeasurementTable({ servicesManager, customHeader, - measurementFilter, + groups, }: withAppTypes): React.ReactNode { const measurementsPanelRef = useRef(null); @@ -32,11 +32,7 @@ export default function PanelMeasurementTable({ 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; + displayMeasurements.forEach(m => (m.isActive = m.uid === uid)); }; const jumpToImage = (uid: string) => { diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx index 619c5d65390..5718082224c 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -7,7 +7,7 @@ import { DicomMetadataStore, utils } from '@ohif/core'; import { useTrackedMeasurements } from '../getContextModule'; import { useTranslation } from 'react-i18next'; -const { downloadCSVReport, formatDate } = utils; +const { formatDate } = utils; const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { key: undefined, // @@ -16,6 +16,19 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { description: '', // 'CHEST/ABD/PELVIS W CONTRAST', }; +export enum MeasurementGroups { + Measurement = 'Measurement', + Untracked = 'Untracked', + AdditionalFindings = 'AdditionalFindings', + Label = 'Label', +} + +const groups = [MeasurementGroups.Measurement, MeasurementGroups.AdditionalFindings]; + +const MeasurementGroupFilter = { group: MeasurementGroups.Measurement }; +// const UntrackedGroupFilter = { group: MeasurementGroups.Untracked }; +// const LabelGroupFilter = { group: MeasurementGroups.Label }; + function PanelMeasurementTableTracking({ servicesManager, extensionManager, @@ -63,7 +76,19 @@ function PanelMeasurementTableTracking({ }); } else if (!trackedStudy) { setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); + console.log('*********** No tracked study'); } + measurementService.setGroupFilter( + 'Measurement', + measurement => + (trackedStudy === measurement.referenceStudyUID && + trackedSeries.includes(measurement.referenceSeriesUID)) || + !trackedStudy + ); + measurementService.setGroupFilter( + 'Untracked', + measurement => !measurementService.isInOtherGroup(measurement, 'Untracked') + ); }; updateDisplayStudySummary(); @@ -89,10 +114,7 @@ function PanelMeasurementTableTracking({ servicesManager={servicesManager} extensionManager={extensionManager} commandsManager={commandsManager} - measurementFilter={measurement => - trackedStudy === measurement.referenceStudyUID && - trackedSeries.includes(measurement.referenceSeriesUID) - } + groups={groups} customHeader={({ additionalFindings, measurements }) => { const disabled = additionalFindings.length === 0 && measurements.length === 0; @@ -108,14 +130,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('clearMeasurements', { + measurementFilter: MeasurementGroupFilter, + }); }} > @@ -140,7 +157,9 @@ function PanelMeasurementTableTracking({ variant="ghost" className="pl-0.5" onClick={() => { - measurementService.clearMeasurements(); + commandsManager.runCommand('clearMeasurements', { + measurementFilter: MeasurementGroupFilter, + }); }} > diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 4f823c95634..69252018c69 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -92,6 +92,18 @@ const VALUE_TYPES = { ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual', }; +/** Selection for measurements to be in different groups */ +export type MeasurementSelectionFilter = { + studyInstanceUID?: string; + group?: string; + groups?: string[]; +}; + +export type MeasurementGroupInfo = { + name: string; + filter?: (measurement, measurementService: MeasurementService) => 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.) @@ -120,10 +132,13 @@ class MeasurementService extends PubSubService { private measurements = new Map(); private unmappedMeasurements = new Map(); + private sources = {}; + private mappings = {}; + + private groupInfos = new Map(); + constructor() { super(EVENTS); - this.sources = {}; - this.mappings = {}; } /** @@ -168,10 +183,75 @@ class MeasurementService extends PubSubService { * * @return {Measurement[]} Array of measurements */ - getMeasurements() { + public getMeasurements(options?: MeasurementSelectionFilter) { + if (options) { + return [ + ...this.measurements + .values() + .filter(measurement => this.isMeasurementFilter(measurement, options)), + ]; + } return [...this.measurements.values()]; } + public isMeasurementFilter(measurement, options?: MeasurementSelectionFilter) { + if (!options || !(options.studyInstanceUID || options.group || options.groups?.length)) { + return true; + } + const { studyInstanceUID, group, groups } = options; + if (studyInstanceUID && studyInstanceUID !== measurement.referenceStudyUID) { + return false; + } + if (groups?.length) { + return !!groups.find(groupName => this.isMeasurementInGroup(measurement, groupName)); + } + if (group) { + return this.isMeasurementInGroup(measurement, group); + } + return true; + } + + /** + * Determines if the measurement is in another group + */ + public isInOtherGroup(measurement, group) { + for (const groupInfo of this.groupInfos.values()) { + if (groupInfo.name === group) { + continue; + } + if (groupInfo.filter && groupInfo.filter(measurement, this)) { + return true; + } + } + return false; + } + + public getGroupsForMeasurement(measurement) { + const groups = []; + for (const groupInfo of this.groupInfos.values()) { + if (!groupInfo.filter || groupInfo.filter(measurement, this)) { + groups.push(groupInfo.name); + } + } + if (!groups.length) { + groups.push('Measurements'); + } + } + + /** Find out if the measurement is in this group */ + public isMeasurementInGroup(measurement, group: string) { + const groupInfo = this.groupInfos.get(group); + return groupInfo?.filter ? groupInfo.filter(measurement, this) : true; + } + + public setGroupFilter(name: string, filter) { + if (!this.groupInfos.has(name)) { + this.groupInfos.set(name, { name, filter }); + return; + } + this.groupInfos.get(name).filter = filter; + } + /** * Get specific measurement by its uid. * @@ -582,11 +662,13 @@ class MeasurementService extends PubSubService { }); } - clearMeasurements() { + public clearMeasurements(options?: MeasurementSelectionFilter) { // Make a copy of the measurements - const measurements = [...this.measurements.values(), ...this.unmappedMeasurements.values()]; + const toClear = this.getMeasurements(options); + const measurements = [...toClear, ...this.unmappedMeasurements.values()]; this.unmappedMeasurements.clear(); - this.measurements.clear(); + toClear.forEach(measurement => this.measurements.delete(measurement.uid)); + console.log('After clearMeasurements, size is', this.measurements.size); this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); } @@ -597,6 +679,13 @@ class MeasurementService extends PubSubService { */ onModeExit() { this.clearMeasurements(); + this.groupInfos.clear(); + // Creates a default measurements group filter which excludes everything but measurements + this.setGroupFilter( + 'Measurements', + (measurement, service: MeasurementService) => + !service.isInOtherGroup(measurement, 'Measurements') + ); } /** From 58a0b0811d86644b968ed4a5af3df8dfe4fc5dbe Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 19 Nov 2024 17:11:12 -0500 Subject: [PATCH 02/10] fix: Add untracked findings section --- .../src/panels/PanelMeasurement.tsx | 79 ++++++++++---- .../panels/PanelMeasurementTableTracking.tsx | 53 ++++----- .../MeasurementService/MeasurementService.ts | 103 ++---------------- platform/core/src/utils/index.ts | 3 + platform/core/src/utils/measurementFilters.ts | 54 +++++++++ 5 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 platform/core/src/utils/measurementFilters.ts diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index f75dc29f8eb..6803aff7bc5 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -1,22 +1,32 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { utils } from '@ohif/core'; import { useViewportGrid } from '@ohif/ui'; import { MeasurementTable } from '@ohif/ui-next'; import debounce from 'lodash.debounce'; import { useMeasurements } from '../hooks/useMeasurements'; import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default'; +const { filterAdditionalFinding, filterOr, filterAny } = utils.MeasurementFilters; + +export type withAppAndFilters = withAppTypes & { + measurementFilters: Record boolean>; +}; + export default function PanelMeasurementTable({ servicesManager, customHeader, - groups, -}: withAppTypes): React.ReactNode { + measurementFilters, +}: withAppAndFilters): React.ReactNode { const measurementsPanelRef = useRef(null); const [viewportGrid] = useViewportGrid(); const { measurementService, customizationService, uiDialogService } = servicesManager.services; + const [measurements, setMeasurements] = useState([]); + const [additionalFindings, setAdditionalFindings] = useState([]); + const [untrackedFindings, setUntrackedFindings] = useState([]); const displayMeasurements = useMeasurements(servicesManager, { - measurementFilter, + measurementFilter: filterAny, // filterOr(measurementFilters); }); useEffect(() => { @@ -85,12 +95,26 @@ export default function PanelMeasurementTable({ measurementService.toggleVisibilityMeasurement(uid); }; - const measurements = displayMeasurements.filter( - dm => dm.measurementType !== measurementService.VALUE_TYPES.POINT && dm.referencedImageId - ); - const additionalFindings = displayMeasurements.filter( - dm => dm.measurementType === measurementService.VALUE_TYPES.POINT && dm.referencedImageId - ); + const additionalFilter = filterAdditionalFinding(measurementService); + + useEffect(() => { + const { measurementFilter: trackedFilter, untrackedFilter } = measurementFilters; + setMeasurements( + displayMeasurements.filter(item => !additionalFilter(item) && trackedFilter(item)) + ); + setAdditionalFindings( + displayMeasurements.filter(item => additionalFilter(item) && trackedFilter(item)) + ); + setUntrackedFindings(displayMeasurements.filter(untrackedFilter.bind(measurementFilters))); + }, [measurementFilters, displayMeasurements]); + + const onArgs = { + onClick: jumpToImage, + onDelete: removeMeasurement, + onToggleVisibility: toggleVisibilityMeasurement, + onToggleLocked: toggleLockMeasurement, + onRename: renameMeasurement, + }; return ( <> @@ -100,13 +124,10 @@ export default function PanelMeasurementTable({ data-cy={'trackedMeasurements-panel'} > @@ -125,15 +146,31 @@ export default function PanelMeasurementTable({ {additionalFindings.length > 0 && ( + + <> +
Hello World
+ +
+ +
+ )} + {untrackedFindings.length > 0 && ( + + + <> + Untracked + + )} diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx index 5718082224c..f414882928a 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -1,12 +1,14 @@ import React, { useEffect, useState } from 'react'; -import { PanelMeasurement } from '@ohif/extension-cornerstone'; +import { DicomMetadataStore, utils } from '@ohif/core'; import { useViewportGrid } from '@ohif/ui'; import { StudySummary } from '@ohif/ui-next'; import { Button, Icons } from '@ohif/ui-next'; -import { DicomMetadataStore, utils } from '@ohif/core'; +import { PanelMeasurement } from '@ohif/extension-cornerstone'; import { useTrackedMeasurements } from '../getContextModule'; import { useTranslation } from 'react-i18next'; +const { filterAny, filterNone, filterNot, filterTracked } = utils.MeasurementFilters; + const { formatDate } = utils; const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { @@ -16,19 +18,6 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { description: '', // 'CHEST/ABD/PELVIS W CONTRAST', }; -export enum MeasurementGroups { - Measurement = 'Measurement', - Untracked = 'Untracked', - AdditionalFindings = 'AdditionalFindings', - Label = 'Label', -} - -const groups = [MeasurementGroups.Measurement, MeasurementGroups.AdditionalFindings]; - -const MeasurementGroupFilter = { group: MeasurementGroups.Measurement }; -// const UntrackedGroupFilter = { group: MeasurementGroups.Untracked }; -// const LabelGroupFilter = { group: MeasurementGroups.Label }; - function PanelMeasurementTableTracking({ servicesManager, extensionManager, @@ -42,9 +31,18 @@ function PanelMeasurementTableTracking({ const [displayStudySummary, setDisplayStudySummary] = useState( DISPLAY_STUDY_SUMMARY_INITIAL_VALUE ); + const initialTrackedFilter = trackedStudy + ? filterTracked(trackedStudy, trackedSeries) + : filterAny; + const [measurementFilters, setMeasurementFilters] = useState({ + measurementFilter: initialTrackedFilter, + untrackedFilter: filterNot('measurementFilter'), + unmappedFilter: filterAny, + }); useEffect(() => { const updateDisplayStudySummary = async () => { + let updatedMeasurementFilters = { ...measurementFilters }; if (trackedMeasurements.matches('tracking') && trackedStudy) { const studyMeta = DicomMetadataStore.getStudy(trackedStudy); if (!studyMeta || !studyMeta.series || studyMeta.series.length === 0) { @@ -74,21 +72,12 @@ function PanelMeasurementTableTracking({ } return prevSummary; }); + updatedMeasurementFilters.measurementFilter = filterTracked(trackedStudy, trackedSeries); } else if (!trackedStudy) { setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); - console.log('*********** No tracked study'); + updatedMeasurementFilters.measurementFilter = filterAny; } - measurementService.setGroupFilter( - 'Measurement', - measurement => - (trackedStudy === measurement.referenceStudyUID && - trackedSeries.includes(measurement.referenceSeriesUID)) || - !trackedStudy - ); - measurementService.setGroupFilter( - 'Untracked', - measurement => !measurementService.isInOtherGroup(measurement, 'Untracked') - ); + setMeasurementFilters(updatedMeasurementFilters); }; updateDisplayStudySummary(); @@ -114,7 +103,7 @@ function PanelMeasurementTableTracking({ servicesManager={servicesManager} extensionManager={extensionManager} commandsManager={commandsManager} - groups={groups} + measurementFilters={measurementFilters} customHeader={({ additionalFindings, measurements }) => { const disabled = additionalFindings.length === 0 && measurements.length === 0; @@ -130,9 +119,7 @@ function PanelMeasurementTableTracking({ variant="ghost" className="pl-1.5" onClick={() => { - commandsManager.runCommand('clearMeasurements', { - measurementFilter: MeasurementGroupFilter, - }); + commandsManager.runCommand('clearMeasurements', measurementFilters); }} > @@ -157,9 +144,7 @@ function PanelMeasurementTableTracking({ variant="ghost" className="pl-0.5" onClick={() => { - commandsManager.runCommand('clearMeasurements', { - measurementFilter: MeasurementGroupFilter, - }); + commandsManager.runCommand('clearMeasurements', measurementFilters); }} > diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 69252018c69..1cf711adc13 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -92,17 +92,7 @@ const VALUE_TYPES = { ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual', }; -/** Selection for measurements to be in different groups */ -export type MeasurementSelectionFilter = { - studyInstanceUID?: string; - group?: string; - groups?: string[]; -}; - -export type MeasurementGroupInfo = { - name: string; - filter?: (measurement, measurementService: MeasurementService) => boolean; -}; +export type MeasurementFilter = (measurement) => boolean; /** * MeasurementService class that supports source management and measurement management. @@ -120,7 +110,7 @@ class MeasurementService extends PubSubService { public static REGISTRATION = { name: 'measurementService', altName: 'MeasurementService', - create: ({ configuration = {} }) => { + create: _options => { return new MeasurementService(); }, }; @@ -135,8 +125,6 @@ class MeasurementService extends PubSubService { private sources = {}; private mappings = {}; - private groupInfos = new Map(); - constructor() { super(EVENTS); } @@ -183,75 +171,13 @@ class MeasurementService extends PubSubService { * * @return {Measurement[]} Array of measurements */ - public getMeasurements(options?: MeasurementSelectionFilter) { - if (options) { - return [ - ...this.measurements - .values() - .filter(measurement => this.isMeasurementFilter(measurement, options)), - ]; + public getMeasurements(filter?: MeasurementFilter) { + if (filter) { + return [...this.measurements.values()].filter(measurement => filter.call(this, measurement)); } return [...this.measurements.values()]; } - public isMeasurementFilter(measurement, options?: MeasurementSelectionFilter) { - if (!options || !(options.studyInstanceUID || options.group || options.groups?.length)) { - return true; - } - const { studyInstanceUID, group, groups } = options; - if (studyInstanceUID && studyInstanceUID !== measurement.referenceStudyUID) { - return false; - } - if (groups?.length) { - return !!groups.find(groupName => this.isMeasurementInGroup(measurement, groupName)); - } - if (group) { - return this.isMeasurementInGroup(measurement, group); - } - return true; - } - - /** - * Determines if the measurement is in another group - */ - public isInOtherGroup(measurement, group) { - for (const groupInfo of this.groupInfos.values()) { - if (groupInfo.name === group) { - continue; - } - if (groupInfo.filter && groupInfo.filter(measurement, this)) { - return true; - } - } - return false; - } - - public getGroupsForMeasurement(measurement) { - const groups = []; - for (const groupInfo of this.groupInfos.values()) { - if (!groupInfo.filter || groupInfo.filter(measurement, this)) { - groups.push(groupInfo.name); - } - } - if (!groups.length) { - groups.push('Measurements'); - } - } - - /** Find out if the measurement is in this group */ - public isMeasurementInGroup(measurement, group: string) { - const groupInfo = this.groupInfos.get(group); - return groupInfo?.filter ? groupInfo.filter(measurement, this) : true; - } - - public setGroupFilter(name: string, filter) { - if (!this.groupInfos.has(name)) { - this.groupInfos.set(name, { name, filter }); - return; - } - this.groupInfos.get(name).filter = filter; - } - /** * Get specific measurement by its uid. * @@ -662,13 +588,15 @@ class MeasurementService extends PubSubService { }); } - public clearMeasurements(options?: MeasurementSelectionFilter) { + public clearMeasurements(filter?: MeasurementFilter) { // Make a copy of the measurements - const toClear = this.getMeasurements(options); - const measurements = [...toClear, ...this.unmappedMeasurements.values()]; - this.unmappedMeasurements.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)); - console.log('After clearMeasurements, size is', this.measurements.size); this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); } @@ -679,13 +607,6 @@ class MeasurementService extends PubSubService { */ onModeExit() { this.clearMeasurements(); - this.groupInfos.clear(); - // Creates a default measurements group filter which excludes everything but measurements - this.setGroupFilter( - 'Measurements', - (measurement, service: MeasurementService) => - !service.isInOtherGroup(measurement, 'Measurements') - ); } /** diff --git a/platform/core/src/utils/index.ts b/platform/core/src/utils/index.ts index 74c9cdd381a..e2e4d038185 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 00000000000..6ddde113046 --- /dev/null +++ b/platform/core/src/utils/measurementFilters.ts @@ -0,0 +1,54 @@ +export function filterTracked(trackedStudy: string, trackedSeries) { + return measurement => { + const result = + trackedStudy === measurement.referenceStudyUID && + trackedSeries.includes(measurement.referenceSeriesUID); + return result; + }; +} + +export function filterAny(_measurement) { + return true; +} + +export function filterNone(_measurement) { + return false; +} + +export function filterOthers(...filters) { + return item => !filters.find(filter => filter(item)); +} + +export function filterOr(measurementFilters) { + return item => { + for (const filter of Object.values(measurementFilters)) { + if (typeof filter !== 'function') { + continue; + } + if (filter(item)) { + return true; + } + } + return false; + }; +} + +export function filterAdditionalFinding(measurementService) { + const { POINT } = measurementService.VALUE_TYPES; + return dm => dm.type === POINT && dm.referencedImageId; +} + +export function filterUnless(filterUnless, filterThen) { + return item => (filterUnless(item) ? false : filterThen(item)); +} + +const isString = s => typeof s === 'string' || s instanceof String; + +export function filterNot(filter) { + if (isString(filter)) { + return function (item) { + return !this[filter](item); + }; + } + return item => !filter(item); +} From 8142804668f3e8efed34f53b0aeb42edcb459a5b Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 19 Nov 2024 17:43:16 -0500 Subject: [PATCH 03/10] fix: Convert to methods to allow external use --- .../src/commandsModule.ts | 66 ++++++++++++++++++- .../src/panels/PanelMeasurement.tsx | 64 +++++------------- 2 files changed, 83 insertions(+), 47 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index 42dd7ff9894..f971a50097b 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -42,8 +42,57 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {}) const commandsModule = (props: withAppTypes) => { const { servicesManager } = props; - const { customizationService, measurementService } = servicesManager.services; + const { customizationService, measurementService, viewportGridService } = + servicesManager.services; const actions = { + jumpToMeasurement: ({ uid }) => { + measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), 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 + ); + }); + }, + + changeColorMeasurement: ({ uid }) => { + 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); + }); + }, + + toggleLockMeasurement: ({ uid }) => { + measurementService.toggleLockMeasurement(uid); + }, + + toggleVisibilityMeasurement: ({ uid }) => { + measurementService.toggleVisibilityMeasurement(uid); + }, + /** * * @param measurementData An array of measurements from the measurements service @@ -152,6 +201,21 @@ const commandsModule = (props: withAppTypes) => { downloadCSVMeasurementsReport: { commandFn: actions.downloadCSVMeasurementsReport, }, + jumpToMeasurement: { + commandFn: actions.jumpToMeasurement, + }, + removeMeasurement: { + commandFn: actions.removeMeasurement, + }, + renameMeasurement: { + commandFn: actions.renameMeasurement, + }, + toggleLockMeasurement: { + commandFn: actions.toggleLockMeasurement, + }, + toggleVisibilityMeasurement: { + commandFn: actions.toggleVisibilityMeasurement, + }, }; return { diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index 6803aff7bc5..d025dc7696d 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -14,6 +14,7 @@ export type withAppAndFilters = withAppTypes & { export default function PanelMeasurementTable({ servicesManager, + commandsManager, customHeader, measurementFilters, }: withAppAndFilters): React.ReactNode { @@ -45,55 +46,26 @@ export default function PanelMeasurementTable({ displayMeasurements.forEach(m => (m.isActive = m.uid === uid)); }; - 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, - }; - colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => { - if (actionId === 'cancel') { - return; + const bindCommand = (name: string, callJump = false) => { + return (uid: string) => { + if (!uid) { + debugger; } - - 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); + if (callJump) { + commandsManager.runCommand('jumpToMeasurement'); + } + commandsManager.runCommand(name, { uid }); + if (callJump === null) { + onMeasurementItemClickHandler(uid, true); + } + }; }; - const toggleVisibilityMeasurement = (uid: string) => { - measurementService.toggleVisibilityMeasurement(uid); - }; + const jumpToImage = bindCommand('jumpToMeasurement', null); + const removeMeasurement = bindCommand('removeMeasurement'); + const renameMeasurement = bindCommand('renameMeasurement', true); + const toggleLockMeasurement = bindCommand('toggleLockMeasurement'); + const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement'); const additionalFilter = filterAdditionalFinding(measurementService); From 22f437585db88c13a2692c388332b5e35e9c8f22 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 19 Nov 2024 17:47:30 -0500 Subject: [PATCH 04/10] fix: Make actions work --- extensions/cornerstone-dicom-sr/src/commandsModule.ts | 3 ++- extensions/cornerstone/src/panels/PanelMeasurement.tsx | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index f971a50097b..78704fb35ab 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -3,6 +3,7 @@ import { metaData, utilities } from '@cornerstonejs/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'; @@ -42,7 +43,7 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {}) const commandsModule = (props: withAppTypes) => { const { servicesManager } = props; - const { customizationService, measurementService, viewportGridService } = + const { customizationService, measurementService, viewportGridService, uiDialogService } = servicesManager.services; const actions = { jumpToMeasurement: ({ uid }) => { diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index d025dc7696d..6fdc98e38b1 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -4,7 +4,6 @@ import { useViewportGrid } from '@ohif/ui'; import { MeasurementTable } from '@ohif/ui-next'; import debounce from 'lodash.debounce'; import { useMeasurements } from '../hooks/useMeasurements'; -import { showLabelAnnotationPopup, colorPickerDialog } from '@ohif/extension-default'; const { filterAdditionalFinding, filterOr, filterAny } = utils.MeasurementFilters; From 2a6722e6d363b8def9cb84611c205df401ae08db Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 22 Nov 2024 09:15:53 -0500 Subject: [PATCH 05/10] PR review comments --- .../src/commandsModule.ts | 88 ++++--------------- extensions/cornerstone/src/commandsModule.ts | 54 ++++++++++++ .../src/panels/PanelMeasurement.tsx | 10 --- extensions/default/src/commandsModule.ts | 38 +++++++- .../core/src/extensions/ExtensionManager.ts | 5 +- platform/core/src/utils/measurementFilters.ts | 66 ++++++++++++-- 6 files changed, 169 insertions(+), 92 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index 78704fb35ab..c572ca00a56 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -11,7 +11,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 @@ -46,52 +45,24 @@ const commandsModule = (props: withAppTypes) => { const { customizationService, measurementService, viewportGridService, uiDialogService } = servicesManager.services; const actions = { - jumpToMeasurement: ({ uid }) => { - measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), 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 - ); - }); - }, - changeColorMeasurement: ({ uid }) => { - 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); - }); - }, - - toggleLockMeasurement: ({ uid }) => { - measurementService.toggleLockMeasurement(uid); - }, - - toggleVisibilityMeasurement: ({ uid }) => { - measurementService.toggleVisibilityMeasurement(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); + // }); }, /** @@ -111,13 +82,6 @@ const commandsModule = (props: withAppTypes) => { window.location.assign(objectUrl); }, - /** - * Clear the measurements - */ - clearMeasurements: ({ measurementFilter }) => { - measurementService.clearMeasurements(measurementFilter); - }, - /** * Download the CSV report for the measurements. */ @@ -196,27 +160,9 @@ const commandsModule = (props: withAppTypes) => { storeMeasurements: { commandFn: actions.storeMeasurements, }, - clearMeasurements: { - commandFn: actions.clearMeasurements, - }, downloadCSVMeasurementsReport: { commandFn: actions.downloadCSVMeasurementsReport, }, - jumpToMeasurement: { - commandFn: actions.jumpToMeasurement, - }, - removeMeasurement: { - commandFn: actions.removeMeasurement, - }, - renameMeasurement: { - commandFn: actions.renameMeasurement, - }, - toggleLockMeasurement: { - commandFn: actions.toggleLockMeasurement, - }, - toggleVisibilityMeasurement: { - commandFn: actions.toggleVisibilityMeasurement, - }, }; return { diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 857997dee1f..b5adc54f33f 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -266,6 +266,42 @@ function commandsModule({ measurementService.update(updatedMeasurement.uid, updatedMeasurement, true); }, + jumpToMeasurement: ({ uid }) => { + measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), 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: ({ measurementFilter }) => { + measurementService.clearMeasurements(measurementFilter); + }, + // Retrieve value commands getActiveViewportEnabledElement: _getActiveViewportEnabledElement, @@ -1252,6 +1288,24 @@ 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, + }, setViewportWindowLevel: { commandFn: actions.setViewportWindowLevel, }, diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index 6fdc98e38b1..5a1d5fb021e 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -122,11 +122,6 @@ export default function PanelMeasurementTable({ title="Additional Findings" {...onArgs} > - - <> -
Hello World
- -
)} @@ -137,11 +132,6 @@ export default function PanelMeasurementTable({ title="Untracked Findings" {...onArgs} > - - <> - Untracked - - )} diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 6e128e726e5..a6875dbb43f 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -47,6 +47,39 @@ const commandsModule = ({ const contextMenuController = new ContextMenuController(servicesManager, commandsManager); const actions = { + /** + * Runs commands specified in the options. + * This allows running multiple commands when only one is specified. It + * also allows an async instantiation of a command so that the return is + * immediate without waiting for the command to complete. + * + * @param options.options - shared options for the child command + * @param options.commands - a list of commands to run. This is an array, and can + * be either a full command, or just a name. + * @param options.async - set to true to run async + * @param options.parallel - set to false, WITH async true to run non-parallel + */ + run: options => { + const { options: sharedOptions, commands = [], parallel = true, async = false } = options; + const childOptions = { + ...options, + options: undefined, + commands: undefined, + parallel: undefined, + async: undefined, + ...sharedOptions, + }; + if (async) { + if (parallel) { + return Promise.all( + commands.map(async command => commandsManager.run(command, childOptions)) + ); + } + return (async () => await commandsManager.run(commands, childOptions))(); + } + return commandsManager.run(commands, childOptions); + }, + /** * Show the context menu. * @param options.menuId defines the menu name to lookup, from customizationService @@ -554,9 +587,8 @@ const commandsModule = ({ }; const definitions = { - showContextMenu: { - commandFn: actions.showContextMenu, - }, + run: actions.run, + showContextMenu: actions.showContextMenu, closeContextMenu: { commandFn: actions.closeContextMenu, }, diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index 6c7d7707eed..c9d1987e1b6 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -596,7 +596,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/utils/measurementFilters.ts b/platform/core/src/utils/measurementFilters.ts index 6ddde113046..9f608204690 100644 --- a/platform/core/src/utils/measurementFilters.ts +++ b/platform/core/src/utils/measurementFilters.ts @@ -1,3 +1,7 @@ +/** + * Returns a filter function which filters for measurements belonging to both + * the study and series. + */ export function filterTracked(trackedStudy: string, trackedSeries) { return measurement => { const result = @@ -7,25 +11,28 @@ export function filterTracked(trackedStudy: string, trackedSeries) { }; } +/** A filter that always returns true */ export function filterAny(_measurement) { return true; } +/** A filter that excludes everything */ export function filterNone(_measurement) { return false; } -export function filterOthers(...filters) { - return item => !filters.find(filter => filter(item)); -} - +/** + * Filters the measurements which are found in any of the filters in the provided + * object. This can be used to query for any matching of a set. + * This passes the this argument to the child function(s) + */ export function filterOr(measurementFilters) { - return item => { + return function (item) { for (const filter of Object.values(measurementFilters)) { if (typeof filter !== 'function') { continue; } - if (filter(item)) { + if (filter.call(this, item)) { return true; } } @@ -33,18 +40,63 @@ export function filterOr(measurementFilters) { }; } +/** + * Filters for additional findings, that is, measurements with + * a value of type point, and having a referenced image + */ export function filterAdditionalFinding(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; -export function filterNot(filter) { +/** + * 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); From d274163125f52beadb926cda0266fd710dbb72ce Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 22 Nov 2024 09:49:12 -0500 Subject: [PATCH 06/10] fix: Removed unnecessary tracked data --- .../src/panels/PanelMeasurement.tsx | 44 +++++++------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index 5a1d5fb021e..29b166e6c07 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; import { utils } from '@ohif/core'; -import { useViewportGrid } from '@ohif/ui'; import { MeasurementTable } from '@ohif/ui-next'; import debounce from 'lodash.debounce'; import { useMeasurements } from '../hooks/useMeasurements'; @@ -19,11 +18,7 @@ export default function PanelMeasurementTable({ }: withAppAndFilters): React.ReactNode { const measurementsPanelRef = useRef(null); - const [viewportGrid] = useViewportGrid(); - const { measurementService, customizationService, uiDialogService } = servicesManager.services; - const [measurements, setMeasurements] = useState([]); - const [additionalFindings, setAdditionalFindings] = useState([]); - const [untrackedFindings, setUntrackedFindings] = useState([]); + const { measurementService, customizationService } = servicesManager.services; const displayMeasurements = useMeasurements(servicesManager, { measurementFilter: filterAny, // filterOr(measurementFilters); @@ -45,39 +40,34 @@ export default function PanelMeasurementTable({ displayMeasurements.forEach(m => (m.isActive = m.uid === uid)); }; - const bindCommand = (name: string, callJump = false) => { + const bindCommand = (name: string, options?) => { return (uid: string) => { - if (!uid) { - debugger; - } - if (callJump) { - commandsManager.runCommand('jumpToMeasurement'); - } - commandsManager.runCommand(name, { uid }); - if (callJump === null) { + commandsManager.runCommand(name, { ...options, uid }); + if (options?.clickUid) { onMeasurementItemClickHandler(uid, true); } }; }; - const jumpToImage = bindCommand('jumpToMeasurement', null); + const jumpToImage = bindCommand('jumpToMeasurement', { clickUid: true }); const removeMeasurement = bindCommand('removeMeasurement'); - const renameMeasurement = bindCommand('renameMeasurement', true); + const renameMeasurement = bindCommand('run', { + commands: ['jumpToMeasurement', 'renameMeasurement'], + clickUid: true, + }); const toggleLockMeasurement = bindCommand('toggleLockMeasurement'); const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement'); const additionalFilter = filterAdditionalFinding(measurementService); - useEffect(() => { - const { measurementFilter: trackedFilter, untrackedFilter } = measurementFilters; - setMeasurements( - displayMeasurements.filter(item => !additionalFilter(item) && trackedFilter(item)) - ); - setAdditionalFindings( - displayMeasurements.filter(item => additionalFilter(item) && trackedFilter(item)) - ); - setUntrackedFindings(displayMeasurements.filter(untrackedFilter.bind(measurementFilters))); - }, [measurementFilters, displayMeasurements]); + const { measurementFilter: trackedFilter, untrackedFilter } = measurementFilters; + const measurements = displayMeasurements.filter( + item => !additionalFilter(item) && trackedFilter(item) + ); + const additionalFindings = displayMeasurements.filter( + item => additionalFilter(item) && trackedFilter(item) + ); + const untrackedFindings = displayMeasurements.filter(untrackedFilter.bind(measurementFilters)); const onArgs = { onClick: jumpToImage, From ee992bd0d577ca4c6429928ec0651f5daf3031ef Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 22 Nov 2024 11:47:37 -0500 Subject: [PATCH 07/10] fix: Removed the study summary special handling --- extensions/cornerstone/src/commandsModule.ts | 9 ++- .../components/StudySummaryFromMetadata.tsx | 25 +++++++ extensions/cornerstone/src/index.tsx | 2 + .../src/panels/PanelMeasurement.tsx | 30 ++------ .../panels/PanelMeasurementTableTracking.tsx | 71 +++---------------- 5 files changed, 49 insertions(+), 88 deletions(-) create mode 100644 extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index b5adc54f33f..1bc0b3541e2 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -266,8 +266,15 @@ function commandsModule({ measurementService.update(updatedMeasurement.uid, updatedMeasurement, true); }, - jumpToMeasurement: ({ uid }) => { + /** + * 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 }) => { diff --git a/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx b/extensions/cornerstone/src/components/StudySummaryFromMetadata.tsx new file mode 100644 index 00000000000..3c34218ac50 --- /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 ( + + ); +} diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 47d630c1026..c2a69789e19 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 29b166e6c07..abd81e6449c 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -21,7 +21,7 @@ export default function PanelMeasurementTable({ const { measurementService, customizationService } = servicesManager.services; const displayMeasurements = useMeasurements(servicesManager, { - measurementFilter: filterAny, // filterOr(measurementFilters); + measurementFilter: filterAny, }); useEffect(() => { @@ -32,42 +32,30 @@ export default function PanelMeasurementTable({ } }, [displayMeasurements.length]); - const onMeasurementItemClickHandler = (uid: string, isActive: boolean) => { - if (isActive) { - return; - } - - displayMeasurements.forEach(m => (m.isActive = m.uid === uid)); - }; - const bindCommand = (name: string, options?) => { return (uid: string) => { commandsManager.runCommand(name, { ...options, uid }); - if (options?.clickUid) { - onMeasurementItemClickHandler(uid, true); - } }; }; - const jumpToImage = bindCommand('jumpToMeasurement', { clickUid: true }); + const jumpToImage = bindCommand('jumpToMeasurement', { displayMeasurements }); const removeMeasurement = bindCommand('removeMeasurement'); const renameMeasurement = bindCommand('run', { commands: ['jumpToMeasurement', 'renameMeasurement'], - clickUid: true, + displayMeasurements, }); const toggleLockMeasurement = bindCommand('toggleLockMeasurement'); const toggleVisibilityMeasurement = bindCommand('toggleVisibilityMeasurement'); const additionalFilter = filterAdditionalFinding(measurementService); - const { measurementFilter: trackedFilter, untrackedFilter } = measurementFilters; + const { measurementFilter: trackedFilter } = measurementFilters; const measurements = displayMeasurements.filter( item => !additionalFilter(item) && trackedFilter(item) ); const additionalFindings = displayMeasurements.filter( item => additionalFilter(item) && trackedFilter(item) ); - const untrackedFindings = displayMeasurements.filter(untrackedFilter.bind(measurementFilters)); const onArgs = { onClick: jumpToImage, @@ -115,16 +103,6 @@ export default function PanelMeasurementTable({ )} - {untrackedFindings.length > 0 && ( - - - - )} ); diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx index f414882928a..37eab2ff96e 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -1,23 +1,13 @@ import React, { useEffect, useState } from 'react'; import { DicomMetadataStore, utils } from '@ohif/core'; import { useViewportGrid } from '@ohif/ui'; -import { StudySummary } from '@ohif/ui-next'; import { Button, Icons } from '@ohif/ui-next'; -import { PanelMeasurement } from '@ohif/extension-cornerstone'; +import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone'; import { useTrackedMeasurements } from '../getContextModule'; import { useTranslation } from 'react-i18next'; const { filterAny, filterNone, filterNot, filterTracked } = utils.MeasurementFilters; -const { formatDate } = utils; - -const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { - key: undefined, // - date: '', // '07-Sep-2010', - modality: '', // 'CT', - description: '', // 'CHEST/ABD/PELVIS W CONTRAST', -}; - function PanelMeasurementTableTracking({ servicesManager, extensionManager, @@ -25,12 +15,9 @@ function PanelMeasurementTableTracking({ }: 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 - ); const initialTrackedFilter = trackedStudy ? filterTracked(trackedStudy, trackedSeries) : filterAny; @@ -41,46 +28,13 @@ function PanelMeasurementTableTracking({ }); useEffect(() => { - const updateDisplayStudySummary = async () => { - let updatedMeasurementFilters = { ...measurementFilters }; - 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; - }); - updatedMeasurementFilters.measurementFilter = filterTracked(trackedStudy, trackedSeries); - } else if (!trackedStudy) { - setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE); - updatedMeasurementFilters.measurementFilter = filterAny; - } - setMeasurementFilters(updatedMeasurementFilters); - }; - - updateDisplayStudySummary(); + let updatedMeasurementFilters = { ...measurementFilters }; + if (trackedMeasurements.matches('tracking') && trackedStudy) { + updatedMeasurementFilters.measurementFilter = filterTracked(trackedStudy, trackedSeries); + } else { + updatedMeasurementFilters.measurementFilter = filterAny; + } + setMeasurementFilters(updatedMeasurementFilters); }, [trackedMeasurements, trackedStudy, trackedSeries]); const { disableEditing } = customizationService.getCustomization( @@ -93,12 +47,7 @@ function PanelMeasurementTableTracking({ return ( <> - {displayStudySummary.key && ( - - )} + Date: Fri, 22 Nov 2024 12:03:32 -0500 Subject: [PATCH 08/10] Top level unit tests --- platform/core/src/utils/index.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 85d18f71d5b..9e7edfd85f6 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(); From 04cbc18a6d54bb07d36406a9dcdca59e592f949d Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 22 Nov 2024 15:12:47 -0500 Subject: [PATCH 09/10] PR review comments - removed run command --- .../src/panels/PanelMeasurement.tsx | 7 +- extensions/default/src/commandsModule.ts | 74 +++---------------- platform/core/src/classes/CommandsManager.ts | 2 +- 3 files changed, 14 insertions(+), 69 deletions(-) diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index abd81e6449c..f11f9bada99 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -32,16 +32,15 @@ export default function PanelMeasurementTable({ } }, [displayMeasurements.length]); - const bindCommand = (name: string, options?) => { + const bindCommand = (name: string | string[], options?) => { return (uid: string) => { - commandsManager.runCommand(name, { ...options, uid }); + commandsManager.run(name, { ...options, uid }); }; }; const jumpToImage = bindCommand('jumpToMeasurement', { displayMeasurements }); const removeMeasurement = bindCommand('removeMeasurement'); - const renameMeasurement = bindCommand('run', { - commands: ['jumpToMeasurement', 'renameMeasurement'], + const renameMeasurement = bindCommand(['jumpToMeasurement', 'renameMeasurement'], { displayMeasurements, }); const toggleLockMeasurement = bindCommand('toggleLockMeasurement'); diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index a6875dbb43f..a18182e97b8 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -47,39 +47,6 @@ const commandsModule = ({ const contextMenuController = new ContextMenuController(servicesManager, commandsManager); const actions = { - /** - * Runs commands specified in the options. - * This allows running multiple commands when only one is specified. It - * also allows an async instantiation of a command so that the return is - * immediate without waiting for the command to complete. - * - * @param options.options - shared options for the child command - * @param options.commands - a list of commands to run. This is an array, and can - * be either a full command, or just a name. - * @param options.async - set to true to run async - * @param options.parallel - set to false, WITH async true to run non-parallel - */ - run: options => { - const { options: sharedOptions, commands = [], parallel = true, async = false } = options; - const childOptions = { - ...options, - options: undefined, - commands: undefined, - parallel: undefined, - async: undefined, - ...sharedOptions, - }; - if (async) { - if (parallel) { - return Promise.all( - commands.map(async command => commandsManager.run(command, childOptions)) - ); - } - return (async () => await commandsManager.run(commands, childOptions))(); - } - return commandsManager.run(commands, childOptions); - }, - /** * Show the context menu. * @param options.menuId defines the menu name to lookup, from customizationService @@ -587,26 +554,13 @@ const commandsModule = ({ }; const definitions = { - run: actions.run, showContextMenu: 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, - }, + 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 }, @@ -615,18 +569,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/platform/core/src/classes/CommandsManager.ts b/platform/core/src/classes/CommandsManager.ts index 0138521f90a..2a9b31b5610 100644 --- a/platform/core/src/classes/CommandsManager.ts +++ b/platform/core/src/classes/CommandsManager.ts @@ -169,7 +169,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 ): unknown { if (!toRun) { From a9c9755f81dbcddc2a06b8f28532aca414e247ba Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 25 Nov 2024 12:46:48 -0500 Subject: [PATCH 10/10] fix: Remove untracked measurements from tracking filter --- .../cornerstone-dicom-sr/src/commandsModule.ts | 10 ---------- extensions/cornerstone/src/commandsModule.ts | 18 ++++++++++++++++-- extensions/cornerstone/src/getPanelModule.tsx | 2 +- .../src/panels/PanelMeasurement.tsx | 4 ++-- modes/basic-test-mode/src/index.ts | 5 +++-- platform/core/src/utils/measurementFilters.ts | 12 +++++++----- 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index c572ca00a56..e124ba67f97 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -82,13 +82,6 @@ const commandsModule = (props: withAppTypes) => { window.location.assign(objectUrl); }, - /** - * Download the CSV report for the measurements. - */ - downloadCSVMeasurementsReport: ({ measurementFilter }) => { - utils.downloadCSVReport(measurementService.getMeasurements(measurementFilter)); - }, - /** * * @param measurementData An array of measurements from the measurements service @@ -160,9 +153,6 @@ const commandsModule = (props: withAppTypes) => { storeMeasurements: { commandFn: actions.storeMeasurements, }, - downloadCSVMeasurementsReport: { - commandFn: actions.downloadCSVMeasurementsReport, - }, }; return { diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 1bc0b3541e2..08651e05b28 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -302,11 +302,22 @@ function commandsModule({ toggleVisibilityMeasurement: ({ uid }) => { measurementService.toggleVisibilityMeasurement(uid); }, + /** * Clear the measurements */ - clearMeasurements: ({ measurementFilter }) => { - measurementService.clearMeasurements(measurementFilter); + 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 @@ -1313,6 +1324,9 @@ function commandsModule({ toggleVisibilityMeasurement: { commandFn: actions.toggleVisibilityMeasurement, }, + downloadCSVMeasurementsReport: { + commandFn: actions.downloadCSVMeasurementsReport, + }, setViewportWindowLevel: { commandFn: actions.setViewportWindowLevel, }, diff --git a/extensions/cornerstone/src/getPanelModule.tsx b/extensions/cornerstone/src/getPanelModule.tsx index bb3f65d9d80..f5c15e99ff4 100644 --- a/extensions/cornerstone/src/getPanelModule.tsx +++ b/extensions/cornerstone/src/getPanelModule.tsx @@ -81,7 +81,7 @@ const getPanelModule = ({ commandsManager, servicesManager, extensionManager }: name: 'panelMeasurement', iconName: 'tab-linear', iconLabel: 'Measure', - label: 'Measurement', + label: 'All Measurements', component: wrappedPanelMeasurement, }, { diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index f11f9bada99..039fd83fee6 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -14,14 +14,14 @@ export default function PanelMeasurementTable({ servicesManager, commandsManager, customHeader, - measurementFilters, + measurementFilters = { measurementFilter: filterAny }, }: withAppAndFilters): React.ReactNode { const measurementsPanelRef = useRef(null); const { measurementService, customizationService } = servicesManager.services; const displayMeasurements = useMeasurements(servicesManager, { - measurementFilter: filterAny, + measurementFilter: measurementFilters.measurementFilter.bind(measurementFilters), }); useEffect(() => { diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts index 740ff907964..41d9d634132 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/utils/measurementFilters.ts b/platform/core/src/utils/measurementFilters.ts index 9f608204690..8e1814ed455 100644 --- a/platform/core/src/utils/measurementFilters.ts +++ b/platform/core/src/utils/measurementFilters.ts @@ -22,13 +22,15 @@ export function filterNone(_measurement) { } /** - * Filters the measurements which are found in any of the filters in the provided - * object. This can be used to query for any matching of a set. - * This passes the this argument to the child function(s) + * Filters the measurements which are found in any of the specified + * filters. Strings will be looked up by name. */ -export function filterOr(measurementFilters) { +export function filterOr(...filters) { return function (item) { - for (const filter of Object.values(measurementFilters)) { + for (let filter of filters) { + if (typeof filter === 'string') { + filter = this[filter]; + } if (typeof filter !== 'function') { continue; }