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 React, { useEffect, useState } from 'react';
import { StudySummary, MeasurementTable } from '@ohif/ui'; import PropTypes from 'prop-types';
import ActionButtons from './ActionButtons.jsx'; import { MeasurementTable } from '@ohif/ui';
import { DicomMetadataStore } from '@ohif/core';
import debounce from './debounce.js';
export default function PanelMeasurementTable({ export default function PanelMeasurementTable({
servicesManager, servicesManager,
commandsManager, // commandsManager,
}) { }) {
const { MeasurementService } = servicesManager.services; const { MeasurementService } = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
console.log('MeasurementTable rendering!!!!!!!!!!!!!'); useEffect(() => {
const debouncedSetDisplayMeasurements = debounce(
setDisplayMeasurements,
100
);
// ~~ Initial
setDisplayMeasurements(_getMappedMeasurements(MeasurementService));
const descriptionData = { // ~~ Subscription
date: '07-Sep-2010', const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
modality: 'CT', const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
description: 'CHEST/ABD/PELVIS W CONTRAST', 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 = { return () => {
title: 'Measurements', subscriptions.forEach(unsub => {
amount: 10, unsub();
data: new Array(10).fill({}).map((el, i) => ({ });
id: i + 1, };
label: 'Label short description', }, [MeasurementService]);
displayText: '24.0 x 24.0 mm (S:4, I:22)',
isActive: activeMeasurementItem === i + 1, // const activeMeasurementItem = 0;
})),
onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)),
onEdit: id => alert(`Edit: ${id}`),
};
return ( return (
<> <>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar"> <div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<StudySummary
date={descriptionData.date}
modality={descriptionData.modality}
description={descriptionData.description}
/>
<MeasurementTable <MeasurementTable
title="Measurements" title="Measurements"
amount={measurementTableData.data.length} amount={displayMeasurements.length}
data={measurementTableData.data} data={displayMeasurements}
onClick={() => {}} onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)} onEdit={id => alert(`Edit: ${id}`)}
/> />
</div> </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, measurementChangeTimestamp,
200 200
); );
const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services; const {
MeasurementService,
UINotificationService,
UIDialogService,
DisplaySetService,
} = servicesManager.services;
const [ const [
trackedMeasurements, trackedMeasurements,
sendTrackedMeasurementsEvent, sendTrackedMeasurementsEvent,
@ -31,9 +36,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
); );
const [displayMeasurements, setDisplayMeasurements] = useState([]); const [displayMeasurements, setDisplayMeasurements] = useState([]);
// TODO: measurements subscribtion
// Initial?
useEffect(() => { useEffect(() => {
const measurements = MeasurementService.getMeasurements(); const measurements = MeasurementService.getMeasurements();
const filteredMeasurements = measurements.filter( const filteredMeasurements = measurements.filter(
@ -120,7 +123,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
isDraggable: false, isDraggable: false,
centralize: true, centralize: true,
// TODO: Create a loading indicator component + zeplin design? // 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 { try {
@ -136,13 +139,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
// Would need some way of saying which one is the "push" dataSource // Would need some way of saying which one is the "push" dataSource
const dataSource = dataSources[0]; 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({ UINotificationService.show({
title: 'STOW SR', title: 'STOW SR',
message: 'Measurements saved successfully', message: 'Measurements saved successfully',
type: 'success' type: 'success',
}); });
} catch (error) { } catch (error) {
UINotificationService.show({ UINotificationService.show({
@ -169,7 +177,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
title="Measurements" title="Measurements"
amount={displayMeasurements.length} amount={displayMeasurements.length}
data={displayMeasurements} data={displayMeasurements}
onClick={() => { }} onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)} onEdit={id => alert(`Edit: ${id}`)}
/> />
</div> </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 // TODO: This could be a MeasurementService mapper
function _mapMeasurementToDisplay(measurement, index, types) { function _mapMeasurementToDisplay(measurement, index, types) {
@ -203,9 +220,6 @@ function _mapMeasurementToDisplay(measurement, index, types) {
); );
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance; const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
console.log('mapping....', measurement);
console.log(instance);
return { return {
id: index + 1, id: index + 1,
label: '(empty)', // 'Label short description', label: '(empty)', // 'Label short description',
@ -234,15 +248,7 @@ function _getDisplayText(
instanceNumber, instanceNumber,
types 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 { type, points } = measurement;
const hasPixelSpacing = const hasPixelSpacing =
pixelSpacing !== undefined && pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) && Array.isArray(pixelSpacing) &&
@ -253,18 +259,16 @@ function _getDisplayText(
const unit = hasPixelSpacing ? 'mm' : 'px'; const unit = hasPixelSpacing ? 'mm' : 'px';
switch (type) { switch (type) {
case types.POLYLINE: case types.POLYLINE: {
const { length } = measurement; const { length } = measurement;
const roundedLength = _round(length, 1); const roundedLength = _round(length, 1);
return [ return [
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, `${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
]; ];
}
case types.BIDIRECTIONAL: case types.BIDIRECTIONAL: {
const { shortestDiameter, longestDiameter } = measurement; const { shortestDiameter, longestDiameter } = measurement;
const roundedShortestDiameter = _round(shortestDiameter, 1); const roundedShortestDiameter = _round(shortestDiameter, 1);
const roundedLongestDiameter = _round(longestDiameter, 1); const roundedLongestDiameter = _round(longestDiameter, 1);
@ -272,16 +276,19 @@ function _getDisplayText(
`l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, `l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
`s: ${roundedShortestDiameter} ${unit}`, `s: ${roundedShortestDiameter} ${unit}`,
]; ];
case types.ELLIPSE: }
case types.ELLIPSE: {
const { area } = measurement; const { area } = measurement;
const roundedArea = _round(area, 1); const roundedArea = _round(area, 1);
return [ return [
`${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`, `${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
]; ];
case types.POINT: }
case types.POINT: {
const { text } = measurement; const { text } = measurement;
return [`${text} (S:${seriesNumber}, I:${instanceNumber})`]; return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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