Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: group measurements by study #4617

5 changes: 2 additions & 3 deletions extensions/cornerstone-dicom-sr/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
};

const commandsModule = (props: withAppTypes) => {
const { servicesManager, extensionManager, commandsManager } = props;
const { customizationService, measurementService, viewportGridService, uiDialogService } =
servicesManager.services;
const { servicesManager, extensionManager } = props;
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;

const actions = {
changeColorMeasurement: ({ uid }) => {
Expand Down
128 changes: 87 additions & 41 deletions extensions/cornerstone/src/panels/PanelMeasurement.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import { utils } from '@ohif/core';
import { MeasurementTable } from '@ohif/ui-next';
import debounce from 'lodash.debounce';
import { useMeasurements } from '../hooks/useMeasurements';
import { StudySummaryFromMetadata } from '../components/StudySummaryFromMetadata';

const { filterAdditionalFinding, filterOr, filterAny } = utils.MeasurementFilters;
const { groupByStudy } = utils.MeasurementGroupings;
const { filterAdditionalFinding, filterAny } = utils.MeasurementFilters;

export type withAppAndFilters = withAppTypes & {
measurementFilters: Record<string, (item) => boolean>;
groupingFunction?: (groupedMeasurements: Map<string, object[]>, item) => Map<string, object[]>;
title: string;
};

export default function PanelMeasurementTable({
servicesManager,
commandsManager,
customHeader,
measurementFilters = { measurementFilter: filterAny },
pedrokohler marked this conversation as resolved.
Show resolved Hide resolved
groupingFunction,
title,
}: withAppAndFilters): React.ReactNode {
const measurementsPanelRef = useRef(null);

const { measurementService, customizationService } = servicesManager.services;
const { measurementService, displaySetService } = servicesManager.services;

const displayMeasurements = useMeasurements(servicesManager, {
measurementFilter: measurementFilters.measurementFilter.bind(measurementFilters),
Expand Down Expand Up @@ -48,13 +54,34 @@ export default function PanelMeasurementTable({

const additionalFilter = filterAdditionalFinding(measurementService);

const { measurementFilter: trackedFilter } = measurementFilters;
const measurements = displayMeasurements.filter(
item => !additionalFilter(item) && trackedFilter(item)
);
const additionalFindings = displayMeasurements.filter(
item => additionalFilter(item) && trackedFilter(item)
);
const { measurementFilter } = measurementFilters;

const defaultGroupingFunction = groupByStudy(displaySetService);
pedrokohler marked this conversation as resolved.
Show resolved Hide resolved
const effectiveGroupingFunction = groupingFunction ?? defaultGroupingFunction;

const measurements = displayMeasurements
pedrokohler marked this conversation as resolved.
Show resolved Hide resolved
.filter(item => !additionalFilter(item) && measurementFilter(item))
.reduce(effectiveGroupingFunction, new Map<string, object[]>());

const additionalFindings = displayMeasurements
.filter(item => additionalFilter(item) && measurementFilter(item))
.reduce(effectiveGroupingFunction, new Map<string, object[]>());

const measurementItemKeys = Array.from(measurements).map(([key]) => {
return key;
});

const additionalItemKeys = Array.from(additionalFindings).map(([key]) => {
return key;
});

const items = Array.from(new Set([...measurementItemKeys, ...additionalItemKeys])).map(study => {
return {
study,
measurements: measurements.get(study) ?? [],
additionalFindings: additionalFindings.get(study) ?? [],
};
});

const onArgs = {
onClick: jumpToImage,
Expand All @@ -69,39 +96,58 @@ export default function PanelMeasurementTable({
<div
className="invisible-scrollbar overflow-y-auto overflow-x-hidden"
ref={measurementsPanelRef}
data-cy={'trackedMeasurements-panel'}
data-cy={'measurements-panel'}
pedrokohler marked this conversation as resolved.
Show resolved Hide resolved
>
<MeasurementTable
key="tracked"
title="Measurements"
data={measurements}
{...onArgs}
// onColor={changeColorMeasurement}
>
<MeasurementTable.Header>
{customHeader && (
<>
{typeof customHeader === 'function'
? customHeader({
additionalFindings,
measurements,
})
: customHeader}
</>
)}
</MeasurementTable.Header>
<MeasurementTable.Body />
</MeasurementTable>
{additionalFindings.length > 0 && (
<MeasurementTable
key="additional"
data={additionalFindings}
title="Additional Findings"
{...onArgs}
>
<MeasurementTable.Body />
</MeasurementTable>
{items.length === 0 ? (
<div className="text-primary-light mb-1 flex flex-1 items-center px-2 py-2 text-base">
No measurements
</div>
) : (
<></>
)}
{items.map(item => {
return (
<div key={`${item.study}`}>
<StudySummaryFromMetadata studyInstanceUID={item.study} />
{item.measurements.length === 0 ? (
<></>
) : (
<MeasurementTable
title={title ? title : `Measurements`}
data={item.measurements}
{...onArgs}
>
<MeasurementTable.Header>
{customHeader && (
<>
{typeof customHeader === 'function'
? customHeader({
pedrokohler marked this conversation as resolved.
Show resolved Hide resolved
additionalFindings,
measurements,
})
: customHeader}
</>
)}
</MeasurementTable.Header>
<MeasurementTable.Body />
</MeasurementTable>
)}

{item.additionalFindings.length === 0 ? (
<></>
) : (
<MeasurementTable
key="additional"
data={item.additionalFindings}
title={`Additional Findings`}
{...onArgs}
>
<MeasurementTable.Body />
</MeasurementTable>
)}
</div>
);
})}
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react';
import { DicomMetadataStore, utils } from '@ohif/core';
import { utils } from '@ohif/core';
import { useViewportGrid } from '@ohif/ui';
import { Button, Icons } from '@ohif/ui-next';
import { PanelMeasurement, StudySummaryFromMetadata } from '@ohif/extension-cornerstone';
import { useTrackedMeasurements } from '../getContextModule';
import { useTranslation } from 'react-i18next';

const { filterAny, filterNone, filterNot, filterTracked } = utils.MeasurementFilters;
const { filterAny, filterNot, filterTracked } = utils.MeasurementFilters;

function PanelMeasurementTableTracking({
servicesManager,
Expand All @@ -28,7 +28,7 @@ function PanelMeasurementTableTracking({
});

useEffect(() => {
let updatedMeasurementFilters = { ...measurementFilters };
const updatedMeasurementFilters = { ...measurementFilters };
if (trackedMeasurements.matches('tracking') && trackedStudy) {
updatedMeasurementFilters.measurementFilter = filterTracked(trackedStudy, trackedSeries);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you fix the measurementFilter to be just a single filter, and make it independent of the tracked study.

} else {
Expand All @@ -46,65 +46,63 @@ function PanelMeasurementTableTracking({
);

return (
<>
<StudySummaryFromMetadata studyInstanceUID={trackedStudy} />
<PanelMeasurement
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
measurementFilters={measurementFilters}
customHeader={({ additionalFindings, measurements }) => {
const disabled = additionalFindings.length === 0 && measurements.length === 0;
<PanelMeasurement
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
measurementFilters={measurementFilters}
title="Measurements"
customHeader={({ additionalFindings, measurements }) => {
const disabled = additionalFindings.length === 0 && measurements.length === 0;

if (disableEditing || disabled) {
return null;
}
if (disableEditing || disabled) {
return null;
}

return (
<div className="bg-background flex h-9 w-full items-center rounded pr-0.5">
<div className="flex space-x-1">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('clearMeasurements', measurementFilters);
}}
>
<Icons.Download className="h-5 w-5" />
<span className="pl-1">CSV</span>
</Button>
<Button
size="sm"
variant="ghost"
className="pl-0.5"
onClick={() => {
sendTrackedMeasurementsEvent('SAVE_REPORT', {
viewportId: viewportGrid.activeViewportId,
isBackupSave: true,
});
}}
>
<Icons.Add />
Create SR
</Button>
<Button
size="sm"
variant="ghost"
className="pl-0.5"
onClick={() => {
commandsManager.runCommand('clearMeasurements', measurementFilters);
}}
>
<Icons.Delete />
Delete All
</Button>
</div>
return (
<div className="bg-background flex h-9 w-full items-center rounded pr-0.5">
<div className="flex space-x-1">
<Button
size="sm"
variant="ghost"
className="pl-1.5"
onClick={() => {
commandsManager.runCommand('clearMeasurements', measurementFilters);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch - this one is running clear measurements instead of saveCSV - can you fix that so it runs saveCSV (or whatever the command name is to save the CSV)
Also, can you pass in the nested measurement filter instead of the top level measurement filter for all 3 commands - that is the new filter I (just) asked to be created in the PanelMeasurement table - that way the command applies to the correct set of data.

}}
>
<Icons.Download className="h-5 w-5" />
<span className="pl-1">CSV</span>
</Button>
<Button
size="sm"
variant="ghost"
className="pl-0.5"
onClick={() => {
sendTrackedMeasurementsEvent('SAVE_REPORT', {
viewportId: viewportGrid.activeViewportId,
isBackupSave: true,
});
}}
>
<Icons.Add />
Create SR
</Button>
<Button
size="sm"
variant="ghost"
className="pl-0.5"
onClick={() => {
commandsManager.runCommand('clearMeasurements', measurementFilters);
}}
>
<Icons.Delete />
Delete All
</Button>
</div>
);
}}
></PanelMeasurement>
</>
</div>
);
}}
/>
);
}

Expand Down
2 changes: 1 addition & 1 deletion platform/app/cypress/support/aliases.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function initCommonElementsAliases(skipMarkers) {
cy.get('[data-cy="trackedMeasurements-btn"]').click();

// TODO: Panels are not in DOM when closed, move this somewhere else
cy.get('[data-cy="trackedMeasurements-panel"]').as('measurementsPanel');
cy.get('[data-cy="measurements-panel"]').as('measurementsPanel');
cy.get('[data-cy="panelSegmentation-btn"]').as('segmentationPanel');
cy.get('[data-cy="studyBrowser-panel"]').as('seriesPanel');
cy.get('[data-cy="viewport-overlay-top-right"]').as('viewportInfoTopRight');
Expand Down
2 changes: 2 additions & 0 deletions platform/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { splitComma, getSplitParam } from './splitComma';
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
import { sopClassDictionary } from './sopClassDictionary';
import * as MeasurementFilters from './measurementFilters';
import * as MeasurementGroupings from './measurementGroupings';

// Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager.
Expand Down Expand Up @@ -86,6 +87,7 @@ const utils = {
generateAcceptHeader,
createStudyBrowserTabs,
MeasurementFilters,
MeasurementGroupings,
};

export {
Expand Down
13 changes: 13 additions & 0 deletions platform/core/src/utils/measurementGroupings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const groupByStudy = displaySetService => (groupedMeasurements, item) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just add a docs to indicate that this groups measurements by study in order to allow display and saving by study.

const displaySet = displaySetService.getDisplaySetByUID(item.displaySetInstanceUID);
const key = displaySet.instances[0].StudyInstanceUID;

if (!groupedMeasurements.has(key)) {
groupedMeasurements.set(key, [item]);
return groupedMeasurements;
}

const oldValues = groupedMeasurements.get(key);
oldValues.push(item);
return groupedMeasurements;
};