Merge pull request #1838 from dannyrb/dannyrb/fix/correct-available-modes

Dannyrb/fix/correct available modes
This commit is contained in:
Danny Brown 2020-06-30 16:18:57 -04:00 committed by GitHub
commit 77bdd6c807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 220 additions and 117 deletions

View File

@ -1,37 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
function ActionButtons() {
return (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="px-2 py-2 text-base text-white bg-black border-primary-main"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="px-2 text-white bg-black border-primary-main"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="px-2 py-2 ml-2 text-base text-white bg-black border border-primary-main"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</React.Fragment>
);
}
export default ActionButtons;

View File

@ -1,55 +1,170 @@
import React from 'react';
import { StudySummary, MeasurementTable } from '@ohif/ui';
import ActionButtons from './ActionButtons.jsx';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { MeasurementTable } from '@ohif/ui';
import { DicomMetadataStore } from '@ohif/core';
import debounce from './debounce.js';
export default function PanelMeasurementTable({
servicesManager,
commandsManager,
// commandsManager,
}) {
const { MeasurementService } = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
console.log('MeasurementTable rendering!!!!!!!!!!!!!');
useEffect(() => {
const debouncedSetDisplayMeasurements = debounce(
setDisplayMeasurements,
100
);
// ~~ Initial
setDisplayMeasurements(_getMappedMeasurements(MeasurementService));
const descriptionData = {
date: '07-Sep-2010',
modality: 'CT',
description: 'CHEST/ABD/PELVIS W CONTRAST',
};
// ~~ Subscription
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
const subscriptions = [];
const activeMeasurementItem = 0;
[added, updated, removed].forEach(evt => {
subscriptions.push(
MeasurementService.subscribe(evt, () => {
debouncedSetDisplayMeasurements(
_getMappedMeasurements(MeasurementService)
);
}).unsubscribe
);
});
const measurementTableData = {
title: 'Measurements',
amount: 10,
data: new Array(10).fill({}).map((el, i) => ({
id: i + 1,
label: 'Label short description',
displayText: '24.0 x 24.0 mm (S:4, I:22)',
isActive: activeMeasurementItem === i + 1,
})),
onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)),
onEdit: id => alert(`Edit: ${id}`),
};
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, [MeasurementService]);
// const activeMeasurementItem = 0;
return (
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<StudySummary
date={descriptionData.date}
modality={descriptionData.modality}
description={descriptionData.description}
/>
<MeasurementTable
title="Measurements"
amount={measurementTableData.data.length}
data={measurementTableData.data}
amount={displayMeasurements.length}
data={displayMeasurements}
onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)}
/>
</div>
<div className="flex justify-center p-4">
<ActionButtons />
</div>
</>
);
}
PanelMeasurementTable.propTypes = {
servicesManager: PropTypes.shape({
services: PropTypes.shape({
MeasurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
EVENTS: PropTypes.object.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
function _getMappedMeasurements(MeasurementService) {
const measurements = MeasurementService.getMeasurements();
const mappedMeasurements = measurements.map((m, index) =>
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
);
return mappedMeasurements;
}
function _mapMeasurementToDisplay(measurement, index, types) {
const {
id,
label,
description,
// Reference IDs
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID,
} = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID
);
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
return {
id: index + 1,
label: '(empty)', // 'Label short description',
displayText:
_getDisplayText(
measurement,
PixelSpacing,
SeriesNumber,
InstanceNumber,
types
) || [],
// TODO: handle one layer down
isActive: false, // activeMeasurementItem === i + 1,
};
}
function _getDisplayText(
measurement,
pixelSpacing,
seriesNumber,
instanceNumber,
types
) {
const { type, points } = measurement;
const hasPixelSpacing =
pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) &&
pixelSpacing.length === 2;
const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing
? pixelSpacing
: [1, 1];
const unit = hasPixelSpacing ? 'mm' : 'px';
switch (type) {
case types.POLYLINE: {
const { length } = measurement;
const roundedLength = _round(length, 1);
return [
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
];
}
case types.BIDIRECTIONAL: {
const { shortestDiameter, longestDiameter } = measurement;
const roundedShortestDiameter = _round(shortestDiameter, 1);
const roundedLongestDiameter = _round(longestDiameter, 1);
return [
`l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
`s: ${roundedShortestDiameter} ${unit}`,
];
}
case types.ELLIPSE: {
const { area } = measurement;
const roundedArea = _round(area, 1);
return [
`${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
];
}
case types.POINT: {
const { text } = measurement;
return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
}
}
}
function _round(value, decimals) {
return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
}

View File

@ -0,0 +1,21 @@
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
export default debounce;

View File

@ -21,7 +21,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
measurementChangeTimestamp,
200
);
const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services;
const {
MeasurementService,
UINotificationService,
UIDialogService,
DisplaySetService,
} = servicesManager.services;
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
@ -31,9 +36,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
);
const [displayMeasurements, setDisplayMeasurements] = useState([]);
// TODO: measurements subscribtion
// Initial?
useEffect(() => {
const measurements = MeasurementService.getMeasurements();
const filteredMeasurements = measurements.filter(
@ -120,7 +123,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
isDraggable: false,
centralize: true,
// TODO: Create a loading indicator component + zeplin design?
content: () => <div className="text-primary-active">Loading...</div>
content: () => <div className="text-primary-active">Loading...</div>,
});
try {
@ -136,13 +139,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
// Would need some way of saying which one is the "push" dataSource
const dataSource = dataSources[0];
const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource);
const naturalizedReport = await DICOMSR.storeMeasurements(
trackedMeasurements,
dataSource
);
DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true });
DisplaySetService.makeDisplaySets([naturalizedReport], {
madeInClient: true,
});
UINotificationService.show({
title: 'STOW SR',
message: 'Measurements saved successfully',
type: 'success'
type: 'success',
});
} catch (error) {
UINotificationService.show({
@ -169,7 +177,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
title="Measurements"
amount={displayMeasurements.length}
data={displayMeasurements}
onClick={() => { }}
onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)}
/>
</div>
@ -183,7 +191,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
);
}
PanelMeasurementTableTracking.propTypes = {};
PanelMeasurementTableTracking.propTypes = {
servicesManager: PropTypes.shape({
services: PropTypes.shape({
MeasurementService: PropTypes.shape({
getMeasurements: PropTypes.func.isRequired,
VALUE_TYPES: PropTypes.object.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
};
// TODO: This could be a MeasurementService mapper
function _mapMeasurementToDisplay(measurement, index, types) {
@ -203,9 +220,6 @@ function _mapMeasurementToDisplay(measurement, index, types) {
);
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
console.log('mapping....', measurement);
console.log(instance);
return {
id: index + 1,
label: '(empty)', // 'Label short description',
@ -234,15 +248,7 @@ function _getDisplayText(
instanceNumber,
types
) {
// TODO: determination of shape influences text
// Length: 'xx.x unit (S:x, I:x)'
// Rectangle: 'xx.x x xx.x unit (S:x, I:x)',
// Ellipse?
// Bidirectional?
// Freehand?
const { type, points } = measurement;
const hasPixelSpacing =
pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) &&
@ -253,18 +259,16 @@ function _getDisplayText(
const unit = hasPixelSpacing ? 'mm' : 'px';
switch (type) {
case types.POLYLINE:
case types.POLYLINE: {
const { length } = measurement;
const roundedLength = _round(length, 1);
return [
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
];
case types.BIDIRECTIONAL:
}
case types.BIDIRECTIONAL: {
const { shortestDiameter, longestDiameter } = measurement;
const roundedShortestDiameter = _round(shortestDiameter, 1);
const roundedLongestDiameter = _round(longestDiameter, 1);
@ -272,16 +276,19 @@ function _getDisplayText(
`l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
`s: ${roundedShortestDiameter} ${unit}`,
];
case types.ELLIPSE:
}
case types.ELLIPSE: {
const { area } = measurement;
const roundedArea = _round(area, 1);
return [
`${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
];
case types.POINT:
}
case types.POINT: {
const { text } = measurement;
return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
}
}
}

View File

@ -20,8 +20,8 @@ export default function mode({ modeConfiguration }) {
return {
// TODO: We're using this as a route segment
// We should not be.
id: 'longitudinal-workflow',
displayName: 'Comparison',
id: 'viewer',
displayName: 'Basic Viewer',
validationTags: {
study: [],
series: [],
@ -95,9 +95,7 @@ export default function mode({ modeConfiguration }) {
'org.ohif.dicom-sr',
],
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
hotkeys: [
...hotkeys.defaults.hotkeyBindings
]
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}

View File

@ -1,7 +1,7 @@
{
"name": "@ohif/mode-example",
"name": "@ohif/mode-segmentation",
"version": "0.0.1",
"description": "Example mode for OHIF",
"description": "Segmentation mode for OHIF",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",

View File

@ -3,8 +3,10 @@ import { hotkeys } from '@ohif/core';
export default function mode({ modeConfiguration }) {
return {
id: 'example-mode',
displayName: 'Basic Viewer',
// TODO: Mode uses 'id' for route when it should use `slug`, if provided, and
// the route path
id: 'segmentation',
displayName: 'Segmentation',
validationTags: {
study: [],
series: [],
@ -15,7 +17,7 @@ export default function mode({ modeConfiguration }) {
},
routes: [
{
path: 'viewer',
path: 'segmentation',
init: ({ servicesManager, extensionManager }) => {
const { ToolBarService } = servicesManager.services;
ToolBarService.init(extensionManager);
@ -51,7 +53,6 @@ export default function mode({ modeConfiguration }) {
return {
id: 'org.ohif.default.layoutTemplateModule.viewerLayout',
props: {
// named slots
leftPanels: ['org.ohif.default.panelModule.seriesList'],
rightPanels: ['org.ohif.default.panelModule.measure'],
viewports: [
@ -69,10 +70,8 @@ export default function mode({ modeConfiguration }) {
],
extensions: ['org.ohif.default', 'org.ohif.cornerstone'],
sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'],
hotkeys: [
...hotkeys.defaults.hotkeyBindings
]
hotkeys: [...hotkeys.defaults.hotkeyBindings],
};
}
window.exampleMode = mode({});
window.segmentationMode = mode({});

View File

@ -20,8 +20,8 @@ import createRoutes from './routes';
import appInit from './appInit.js';
// TODO: Temporarily for testing
import '@ohif/mode-example';
import '@ohif/mode-longitudinal';
import '@ohif/mode-segmentation';
/**
* ENV Variable to determine routing behavior
@ -50,7 +50,7 @@ function App({ config, defaultExtensions }) {
dataSources,
extensionManager,
servicesManager,
hotkeysManager
hotkeysManager,
});
const {
UIDialogService,

View File

@ -71,8 +71,8 @@ function appInit(appConfigOrFunc, defaultExtensions) {
// TODO: Remove this
if (!appConfig.modes.length) {
appConfig.modes.push(window.exampleMode);
appConfig.modes.push(window.longitudinalMode);
appConfig.modes.push(window.segmentationMode);
}
return {
@ -80,7 +80,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
commandsManager,
extensionManager,
servicesManager,
hotkeysManager
hotkeysManager,
};
}