@@ -129,14 +98,10 @@ export default function PanelMeasurementTable({
{additionalFindings.length > 0 && (
diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts
index bac5b4a2a..8f3b96952 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 147e9c651..f33a1ce96 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 { 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 { useViewportGrid } from '@ohif/ui-next';
+import { Button, Icons } from '@ohif/ui-next';
+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 && (
-
- )}
+
- 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,
+ });
}}
>
@@ -140,7 +79,7 @@ function PanelMeasurementTableTracking({
variant="ghost"
className="pl-0.5"
onClick={() => {
- measurementService.clearMeasurements();
+ commandsManager.runCommand('clearMeasurements', { measurementFilter });
}}
>
diff --git a/modes/basic-test-mode/src/index.ts b/modes/basic-test-mode/src/index.ts
index 740ff9079..41d9d6341 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 0138521f9..096054da8 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
): unknown {
if (!toRun) {
diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts
index 588695d10..af620b535 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 4f823c956..0fa3bf7c8 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 85d18f71d..9e7edfd85 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 74c9cdd38..e2e4d0381 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 000000000..db36a305c
--- /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);
+}