Merge branch 'feat/v2-main' of https://github.com/OHIF/Viewers into feat/ui-v2-Dropdown
This commit is contained in:
commit
0d53346d61
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
# docker build -t ohif/viewer:latest .
|
# docker build -t ohif/viewer:latest .
|
||||||
FROM node:10.16.3-slim as builder
|
FROM node:14.3.0-slim as builder
|
||||||
|
|
||||||
RUN mkdir /usr/src/app
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@ -30,6 +30,7 @@ WORKDIR /usr/src/app
|
|||||||
COPY .docker /usr/src/app/.docker
|
COPY .docker /usr/src/app/.docker
|
||||||
COPY .webpack /usr/src/app/.webpack
|
COPY .webpack /usr/src/app/.webpack
|
||||||
COPY extensions /usr/src/app/extensions
|
COPY extensions /usr/src/app/extensions
|
||||||
|
COPY modes /usr/src/app/modes
|
||||||
COPY platform /usr/src/app/platform
|
COPY platform /usr/src/app/platform
|
||||||
COPY .browserslistrc /usr/src/app/.browserslistrc
|
COPY .browserslistrc /usr/src/app/.browserslistrc
|
||||||
COPY aliases.config.js /usr/src/app/aliases.config.js
|
COPY aliases.config.js /usr/src/app/aliases.config.js
|
||||||
|
|||||||
@ -33,8 +33,8 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "4.15.1",
|
"cornerstone-tools": "4.16.0",
|
||||||
"dcmjs": "^0.12.3",
|
"dcmjs": "0.14.0",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
@ -90,6 +90,7 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
|
|
||||||
/* Add extension tools configuration here. */
|
/* Add extension tools configuration here. */
|
||||||
const internalToolsConfig = {
|
const internalToolsConfig = {
|
||||||
|
/* TODO ArrowAnnotate input
|
||||||
ArrowAnnotate: {
|
ArrowAnnotate: {
|
||||||
configuration: {
|
configuration: {
|
||||||
getTextCallback: (callback, eventDetails) =>
|
getTextCallback: (callback, eventDetails) =>
|
||||||
@ -98,6 +99,7 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
callInputDialog(data, eventDetails, callback),
|
callInputDialog(data, eventDetails, callback),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Abstract tools configuration using extension configuration. */
|
/* Abstract tools configuration using extension configuration. */
|
||||||
@ -166,27 +168,48 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
|
|
||||||
const _initMeasurementService = measurementService => {
|
const _initMeasurementService = measurementService => {
|
||||||
/* Initialization */
|
/* Initialization */
|
||||||
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(
|
const {
|
||||||
measurementService
|
Length,
|
||||||
);
|
Bidirectional,
|
||||||
|
EllipticalRoi,
|
||||||
|
ArrowAnnotate,
|
||||||
|
} = measurementServiceMappingsFactory(measurementService);
|
||||||
const csToolsVer4MeasurementSource = measurementService.createSource(
|
const csToolsVer4MeasurementSource = measurementService.createSource(
|
||||||
'CornerstoneTools',
|
'CornerstoneTools',
|
||||||
'4'
|
'4'
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Matching Criterias */
|
|
||||||
const matchingCriteria = {
|
|
||||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
|
||||||
points: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Mappings */
|
/* Mappings */
|
||||||
measurementService.addMapping(
|
measurementService.addMapping(
|
||||||
csToolsVer4MeasurementSource,
|
csToolsVer4MeasurementSource,
|
||||||
'Length',
|
'Length',
|
||||||
matchingCriteria,
|
Length.matchingCriteria,
|
||||||
toAnnotation,
|
Length.toAnnotation,
|
||||||
toMeasurement
|
Length.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
measurementService.addMapping(
|
||||||
|
csToolsVer4MeasurementSource,
|
||||||
|
'Bidirectional',
|
||||||
|
Bidirectional.matchingCriteria,
|
||||||
|
Bidirectional.toAnnotation,
|
||||||
|
Bidirectional.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
measurementService.addMapping(
|
||||||
|
csToolsVer4MeasurementSource,
|
||||||
|
'EllipticalRoi',
|
||||||
|
EllipticalRoi.matchingCriteria,
|
||||||
|
EllipticalRoi.toAnnotation,
|
||||||
|
EllipticalRoi.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
measurementService.addMapping(
|
||||||
|
csToolsVer4MeasurementSource,
|
||||||
|
'ArrowAnnotate',
|
||||||
|
ArrowAnnotate.matchingCriteria,
|
||||||
|
ArrowAnnotate.toAnnotation,
|
||||||
|
ArrowAnnotate.toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
return csToolsVer4MeasurementSource;
|
return csToolsVer4MeasurementSource;
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
|
import getPointsFromHandles from './utils/getPointsFromHandles';
|
||||||
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
|
const ArrowAnnotate = {
|
||||||
|
toAnnotation: (measurement, definition) => {
|
||||||
|
// TODO -> Implement when this is needed.
|
||||||
|
},
|
||||||
|
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
|
||||||
|
const { element, measurementData } = csToolsAnnotation;
|
||||||
|
const tool =
|
||||||
|
csToolsAnnotation.toolType ||
|
||||||
|
csToolsAnnotation.toolName ||
|
||||||
|
measurementData.toolType;
|
||||||
|
|
||||||
|
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType(tool)) {
|
||||||
|
throw new Error('Tool not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = getSOPInstanceAttributes(element);
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
points.push(measurementData.handles);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData._measurementServiceId,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
text: measurementData.text,
|
||||||
|
type: getValueTypeFromToolType(tool),
|
||||||
|
points: getPointsFromHandles(measurementData.handles),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArrowAnnotate;
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
|
const Bidirectional = {
|
||||||
|
toAnnotation: (measurement, definition) => {
|
||||||
|
// TODO -> Implement when this is needed.
|
||||||
|
},
|
||||||
|
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
|
||||||
|
const { element, measurementData } = csToolsAnnotation;
|
||||||
|
const tool =
|
||||||
|
csToolsAnnotation.toolType ||
|
||||||
|
csToolsAnnotation.toolName ||
|
||||||
|
measurementData.toolType;
|
||||||
|
|
||||||
|
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType(tool)) {
|
||||||
|
throw new Error('Tool not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = getSOPInstanceAttributes(element);
|
||||||
|
|
||||||
|
const { handles } = measurementData;
|
||||||
|
|
||||||
|
const longAxis = [handles.start, handles.end];
|
||||||
|
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData._measurementServiceId,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
shortestDiameter: measurementData.shortestDiameter,
|
||||||
|
longestDiameter: measurementData.longestDiameter,
|
||||||
|
type: getValueTypeFromToolType(tool),
|
||||||
|
points: { longAxis, shortAxis },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bidirectional;
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
|
const EllipticalRoi = {
|
||||||
|
toAnnotation: (measurement, definition) => {
|
||||||
|
// TODO -> Implement when this is needed.
|
||||||
|
},
|
||||||
|
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
|
||||||
|
const { element, measurementData } = csToolsAnnotation;
|
||||||
|
const tool =
|
||||||
|
csToolsAnnotation.toolType ||
|
||||||
|
csToolsAnnotation.toolName ||
|
||||||
|
measurementData.toolType;
|
||||||
|
|
||||||
|
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType(tool)) {
|
||||||
|
throw new Error('Tool not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = getSOPInstanceAttributes(element);
|
||||||
|
|
||||||
|
const { start, end } = measurementData.handles;
|
||||||
|
|
||||||
|
const halfXLength = Math.abs(start.x - end.x) / 2;
|
||||||
|
const halfYLength = Math.abs(start.y - end.y) / 2;
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||||
|
|
||||||
|
// To store similar to SR.
|
||||||
|
if (halfXLength > halfYLength) {
|
||||||
|
// X-axis major
|
||||||
|
// Major axis
|
||||||
|
points.push({ x: center.x - halfXLength, y: center.y });
|
||||||
|
points.push({ x: center.x + halfXLength, y: center.y });
|
||||||
|
// Minor axis
|
||||||
|
points.push({ x: center.x, y: center.y - halfYLength });
|
||||||
|
points.push({ x: center.x, y: center.y + halfYLength });
|
||||||
|
} else {
|
||||||
|
// Y-axis major
|
||||||
|
// Major axis
|
||||||
|
points.push({ x: center.x, y: center.y - halfYLength });
|
||||||
|
points.push({ x: center.x, y: center.y + halfYLength });
|
||||||
|
// Minor axis
|
||||||
|
points.push({ x: center.x - halfXLength, y: center.y });
|
||||||
|
points.push({ x: center.x + halfXLength, y: center.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData._measurementServiceId,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
area:
|
||||||
|
measurementData.cachedStats &&
|
||||||
|
measurementData.cachedStats
|
||||||
|
.area /* TODO: Add concept names instead (descriptor) */,
|
||||||
|
type: getValueTypeFromToolType(tool),
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EllipticalRoi;
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
|
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||||
|
import getPointsFromHandles from './utils/getPointsFromHandles';
|
||||||
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
|
const Length = {
|
||||||
|
toAnnotation: (measurement, definition) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
points,
|
||||||
|
unit,
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID,
|
||||||
|
} = measurement;
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolName: definition,
|
||||||
|
measurementData: {
|
||||||
|
sopInstanceUid: SOPInstanceUID,
|
||||||
|
frameOfReferenceUID: FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID: referenceSeriesUID,
|
||||||
|
unit,
|
||||||
|
text: label,
|
||||||
|
description,
|
||||||
|
handles: getHandlesFromPoints(points),
|
||||||
|
_measurementServiceId: id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps cornerstone annotation event data to measurement service format.
|
||||||
|
*
|
||||||
|
* @param {Object} cornerstone Cornerstone event data
|
||||||
|
* @return {Measurement} Measurement instance
|
||||||
|
*/
|
||||||
|
toMeasurement: (csToolsAnnotation, getValueTypeFromToolType) => {
|
||||||
|
const { element, measurementData } = csToolsAnnotation;
|
||||||
|
const tool =
|
||||||
|
csToolsAnnotation.toolType ||
|
||||||
|
csToolsAnnotation.toolName ||
|
||||||
|
measurementData.toolType;
|
||||||
|
|
||||||
|
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType(tool)) {
|
||||||
|
throw new Error('Tool not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = getSOPInstanceAttributes(element);
|
||||||
|
|
||||||
|
const points = [];
|
||||||
|
points.push(measurementData.handles);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: measurementData._measurementServiceId,
|
||||||
|
SOPInstanceUID: SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
label: measurementData.text,
|
||||||
|
description: measurementData.description,
|
||||||
|
unit: measurementData.unit,
|
||||||
|
length: measurementData.length,
|
||||||
|
type: getValueTypeFromToolType(tool),
|
||||||
|
points: getPointsFromHandles(measurementData.handles),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Length;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate'];
|
||||||
@ -1,11 +1,7 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import Length from './Length';
|
||||||
|
import Bidirectional from './Bidirectional';
|
||||||
const SUPPORTED_TOOLS = [
|
import ArrowAnnotate from './ArrowAnnotate';
|
||||||
'Length',
|
import EllipticalRoi from './EllipticalRoi';
|
||||||
'EllipticalRoi',
|
|
||||||
'RectangleRoi',
|
|
||||||
'ArrowAnnotate',
|
|
||||||
];
|
|
||||||
|
|
||||||
const measurementServiceMappingsFactory = measurementService => {
|
const measurementServiceMappingsFactory = measurementService => {
|
||||||
/**
|
/**
|
||||||
@ -15,129 +11,87 @@ const measurementServiceMappingsFactory = measurementService => {
|
|||||||
* @param {string} definition The source definition
|
* @param {string} definition The source definition
|
||||||
* @return {Object} Cornerstone annotation data
|
* @return {Object} Cornerstone annotation data
|
||||||
*/
|
*/
|
||||||
const toAnnotation = (measurement, definition) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
points,
|
|
||||||
unit,
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
referenceSeriesUID,
|
|
||||||
} = measurement;
|
|
||||||
|
|
||||||
return {
|
|
||||||
toolName: definition,
|
|
||||||
measurementData: {
|
|
||||||
sopInstanceUid: SOPInstanceUID,
|
|
||||||
frameOfReferenceUID: FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID: referenceSeriesUID,
|
|
||||||
unit,
|
|
||||||
text: label,
|
|
||||||
description,
|
|
||||||
handles: _getHandlesFromPoints(points),
|
|
||||||
_measurementServiceId: id,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps cornerstone annotation event data to measurement service format.
|
|
||||||
*
|
|
||||||
* @param {Object} cornerstone Cornerstone event data
|
|
||||||
* @return {Measurement} Measurement instance
|
|
||||||
*/
|
|
||||||
const toMeasurement = csToolsAnnotation => {
|
|
||||||
const { element, measurementData } = csToolsAnnotation;
|
|
||||||
const tool =
|
|
||||||
csToolsAnnotation.toolType ||
|
|
||||||
csToolsAnnotation.toolName ||
|
|
||||||
measurementData.toolType;
|
|
||||||
|
|
||||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
|
||||||
|
|
||||||
if (!validToolType(tool)) {
|
|
||||||
throw new Error('Tool not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = _getAttributes(element);
|
|
||||||
|
|
||||||
const points = [];
|
|
||||||
points.push(measurementData.handles);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: measurementData._measurementServiceId,
|
|
||||||
SOPInstanceUID: SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
|
||||||
referenceStudyUID: StudyInstanceUID,
|
|
||||||
label: measurementData.text,
|
|
||||||
description: measurementData.description,
|
|
||||||
unit: measurementData.unit,
|
|
||||||
area:
|
|
||||||
measurementData.cachedStats &&
|
|
||||||
measurementData.cachedStats
|
|
||||||
.area /* TODO: Add concept names instead (descriptor) */,
|
|
||||||
type: _getValueTypeFromToolType(tool),
|
|
||||||
points: _getPointsFromHandles(measurementData.handles),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getAttributes = element => {
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
|
||||||
const imageId = enabledElement.image.imageId;
|
|
||||||
const instance = cornerstone.metaData.get('instance', imageId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
SOPInstanceUID: instance.SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
|
||||||
StudyInstanceUID: instance.StudyInstanceUID,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getValueTypeFromToolType = toolType => {
|
const _getValueTypeFromToolType = toolType => {
|
||||||
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;
|
const {
|
||||||
|
POLYLINE,
|
||||||
|
ELLIPSE,
|
||||||
|
POINT,
|
||||||
|
BIDIRECTIONAL,
|
||||||
|
} = measurementService.VALUE_TYPES;
|
||||||
|
|
||||||
/* TODO: Relocate static value types */
|
// TODO -> I get why this was attemped, but its not nearly flexible enough.
|
||||||
|
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
||||||
|
// You can't define a bidirectional tool as a single type..
|
||||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||||
Length: POLYLINE,
|
Length: POLYLINE,
|
||||||
EllipticalRoi: ELLIPSE,
|
EllipticalRoi: ELLIPSE,
|
||||||
RectangleRoi: POLYLINE,
|
Bidirectional: BIDIRECTIONAL,
|
||||||
ArrowAnnotate: POINT,
|
ArrowAnnotate: POINT,
|
||||||
};
|
};
|
||||||
|
|
||||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getPointsFromHandles = handles => {
|
|
||||||
let points = [];
|
|
||||||
Object.keys(handles).map(handle => {
|
|
||||||
if (['start', 'end'].includes(handle)) {
|
|
||||||
let point = {};
|
|
||||||
if (handles[handle].x) point.x = handles[handle].x;
|
|
||||||
if (handles[handle].y) point.y = handles[handle].y;
|
|
||||||
points.push(point);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return points;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getHandlesFromPoints = points => {
|
|
||||||
return points
|
|
||||||
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
|
||||||
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toAnnotation,
|
Length: {
|
||||||
toMeasurement,
|
toAnnotation: Length.toAnnotation,
|
||||||
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
Length.toMeasurement(csToolsAnnotation, _getValueTypeFromToolType),
|
||||||
|
matchingCriteria: [
|
||||||
|
{
|
||||||
|
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
|
points: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Bidirectional: {
|
||||||
|
toAnnotation: Bidirectional.toAnnotation,
|
||||||
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
Bidirectional.toMeasurement(
|
||||||
|
csToolsAnnotation,
|
||||||
|
_getValueTypeFromToolType
|
||||||
|
),
|
||||||
|
matchingCriteria: [
|
||||||
|
// TODO -> We should eventually do something like shortAxis + longAxis,
|
||||||
|
// But its still a little unclear how these automatic interpretations will work.
|
||||||
|
{
|
||||||
|
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
|
points: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
|
points: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ArrowAnnotate: {
|
||||||
|
toAnnotation: ArrowAnnotate.toAnnotation,
|
||||||
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
ArrowAnnotate.toMeasurement(
|
||||||
|
csToolsAnnotation,
|
||||||
|
_getValueTypeFromToolType
|
||||||
|
),
|
||||||
|
matchingCriteria: [
|
||||||
|
{
|
||||||
|
valueType: measurementService.VALUE_TYPES.POINT,
|
||||||
|
points: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
EllipticalRoi: {
|
||||||
|
toAnnotation: EllipticalRoi.toAnnotation,
|
||||||
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
EllipticalRoi.toMeasurement(
|
||||||
|
csToolsAnnotation,
|
||||||
|
_getValueTypeFromToolType
|
||||||
|
),
|
||||||
|
matchingCriteria: [
|
||||||
|
{
|
||||||
|
valueType: measurementService.VALUE_TYPES.ELLIPSE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
export default function getHandlesFromPoints(points) {
|
||||||
|
return points
|
||||||
|
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
||||||
|
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
export default function getPointsFromHandles(handles) {
|
||||||
|
let points = [];
|
||||||
|
Object.keys(handles).map(handle => {
|
||||||
|
if (['start', 'end'].includes(handle)) {
|
||||||
|
let point = {};
|
||||||
|
if (handles[handle].x) point.x = handles[handle].x;
|
||||||
|
if (handles[handle].y) point.y = handles[handle].y;
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return points;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
|
export default function getSOPInstanceAttributes(element) {
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
const imageId = enabledElement.image.imageId;
|
||||||
|
const instance = cornerstone.metaData.get('instance', imageId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
SOPInstanceUID: instance.SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"webpack": "^4.0.0",
|
"webpack": "^4.0.0",
|
||||||
"dcmjs": "^0.12.4"
|
"dcmjs": "0.14.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.7.6"
|
"@babel/runtime": "7.7.6"
|
||||||
|
|||||||
@ -12,9 +12,16 @@ import getImageId from './utils/getImageId';
|
|||||||
import * as dcmjs from 'dcmjs';
|
import * as dcmjs from 'dcmjs';
|
||||||
import { retrieveStudyMetadata } from './retrieveStudyMetadata.js';
|
import { retrieveStudyMetadata } from './retrieveStudyMetadata.js';
|
||||||
|
|
||||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||||
|
|
||||||
|
const { naturalizeDataset, denaturalizeDataset } = DicomMetaDictionary;
|
||||||
const { urlUtil } = utils;
|
const { urlUtil } = utils;
|
||||||
|
|
||||||
|
const ImplementationClassUID =
|
||||||
|
'2.25.270695996825855179949881587723571202391.2.0.0';
|
||||||
|
const ImplementationVersionName = 'OHIF-VIEWER-2.0.0';
|
||||||
|
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} name - Data source name
|
* @param {string} name - Data source name
|
||||||
@ -128,6 +135,32 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
store: {
|
||||||
|
dicom: async dataset => {
|
||||||
|
const meta = {
|
||||||
|
FileMetaInformationVersion:
|
||||||
|
dataset._meta.FileMetaInformationVersion.Value,
|
||||||
|
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||||
|
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||||
|
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||||
|
ImplementationClassUID,
|
||||||
|
ImplementationVersionName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const denaturalized = denaturalizeDataset(meta);
|
||||||
|
const dicomDict = new DicomDict(denaturalized);
|
||||||
|
|
||||||
|
dicomDict.dict = denaturalizeDataset(dataset);
|
||||||
|
|
||||||
|
const part10Buffer = dicomDict.write();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
datasets: [part10Buffer],
|
||||||
|
};
|
||||||
|
|
||||||
|
await wadoDicomWebClient.storeInstances(options);
|
||||||
|
},
|
||||||
|
},
|
||||||
retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => {
|
retrieveSeriesMetadata: async ({ StudyInstanceUID } = {}) => {
|
||||||
if (!StudyInstanceUID) {
|
if (!StudyInstanceUID) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
* | limit | {number} |
|
* | limit | {number} |
|
||||||
* | offset | {number} |
|
* | offset | {number} |
|
||||||
*/
|
*/
|
||||||
import { DICOMWeb } from '@ohif/core';
|
import { DICOMWeb, utils } from '@ohif/core';
|
||||||
|
|
||||||
const { getString, getName, getModalities } = DICOMWeb;
|
const { getString, getName, getModalities } = DICOMWeb;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ function processResults(qidoStudies) {
|
|||||||
time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds)
|
time: getString(qidoStudy['00080030']), // HHmmss.SSS (24-hour, minutes, seconds, fractional seconds)
|
||||||
accession: getString(qidoStudy['00080050']) || '', // short string, probably a number?
|
accession: getString(qidoStudy['00080050']) || '', // short string, probably a number?
|
||||||
mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber
|
mrn: getString(qidoStudy['00100020']) || '', // medicalRecordNumber
|
||||||
patientName: getName(qidoStudy['00100010']) || '',
|
patientName: utils.formatPN(getName(qidoStudy['00100010'])) || '',
|
||||||
instances: Number(getString(qidoStudy['00201208'])) || 0, // number
|
instances: Number(getString(qidoStudy['00201208'])) || 0, // number
|
||||||
description: getString(qidoStudy['00081030']) || '',
|
description: getString(qidoStudy['00081030']) || '',
|
||||||
modalities:
|
modalities:
|
||||||
|
|||||||
@ -127,7 +127,6 @@ function PanelStudyBrowser({
|
|||||||
changedDisplaySets,
|
changedDisplaySets,
|
||||||
thumbnailImageSrcMap
|
thumbnailImageSrcMap
|
||||||
);
|
);
|
||||||
|
|
||||||
setDisplaySets(mappedDisplaySets);
|
setDisplaySets(mappedDisplaySets);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -152,11 +151,11 @@ function PanelStudyBrowser({
|
|||||||
);
|
);
|
||||||
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
||||||
? // eslint-disable-next-line prettier/prettier
|
? // eslint-disable-next-line prettier/prettier
|
||||||
[
|
[
|
||||||
...expandedStudyInstanceUIDs.filter(
|
...expandedStudyInstanceUIDs.filter(
|
||||||
stdyUid => stdyUid !== StudyInstanceUID
|
stdyUid => stdyUid !== StudyInstanceUID
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||||
|
|
||||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||||
|
|||||||
@ -20,7 +20,7 @@ function WrappedPanelStudyBrowser({
|
|||||||
}) {
|
}) {
|
||||||
// TODO: This should be made available a different way; route should have
|
// TODO: This should be made available a different way; route should have
|
||||||
// already determined our datasource
|
// already determined our datasource
|
||||||
const dataSource = extensionManager.getDataSources('dicomweb')[0];
|
const dataSource = extensionManager.getDataSources()[0];
|
||||||
const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind(
|
const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind(
|
||||||
null,
|
null,
|
||||||
dataSource
|
dataSource
|
||||||
|
|||||||
@ -5,22 +5,40 @@ import {
|
|||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
const DEFAULT_LAYOUT = {
|
||||||
|
type: 'SET_LAYOUT',
|
||||||
|
payload: {
|
||||||
|
numCols: 1,
|
||||||
|
numRows: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function LayoutSelector() {
|
function LayoutSelector() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [viewportGridState, dispatch] = useViewportGrid();
|
const [viewportGridState, dispatch] = useViewportGrid();
|
||||||
|
|
||||||
useEffect(() => {
|
const closeOnOutsideClick = () => {
|
||||||
function closeOnOutsideClick() {
|
if (isOpen) {
|
||||||
if (isOpen) {
|
setIsOpen(false);
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
window.addEventListener('click', closeOnOutsideClick);
|
window.addEventListener('click', closeOnOutsideClick);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('click', closeOnOutsideClick);
|
window.removeEventListener('click', closeOnOutsideClick);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/* Reset to default layout when component unmounts */
|
||||||
|
return () => {
|
||||||
|
dispatch(DEFAULT_LAYOUT);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onClickHandler = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,9 +46,7 @@ function LayoutSelector() {
|
|||||||
id="Layout"
|
id="Layout"
|
||||||
label="Grid Layout"
|
label="Grid Layout"
|
||||||
icon="tool-layout"
|
icon="tool-layout"
|
||||||
onClick={() => {
|
onClick={onClickHandler}
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}}
|
|
||||||
dropdownContent={
|
dropdownContent={
|
||||||
DropdownContent !== null && (
|
DropdownContent !== null && (
|
||||||
<DropdownContent
|
<DropdownContent
|
||||||
|
|||||||
@ -2,33 +2,32 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ToolbarButton } from '@ohif/ui';
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
|
|
||||||
function NestedMenu({ children }) {
|
function NestedMenu({ children, label, icon, isActive }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const toggleNestedMenu = () => setIsOpen(!isOpen);
|
||||||
function closeNestedMenu() {
|
|
||||||
if (isOpen) {
|
const closeNestedMenu = () => {
|
||||||
setIsOpen(false);
|
if (isOpen) {
|
||||||
}
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
window.addEventListener('click', closeNestedMenu);
|
window.addEventListener('click', closeNestedMenu);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('click', closeNestedMenu);
|
window.removeEventListener('click', closeNestedMenu);
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const dropdownContent = isOpen ? children : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
id="More"
|
id="NestedMenu"
|
||||||
label="More"
|
label={label}
|
||||||
icon="tool-more-menu"
|
icon={icon}
|
||||||
onClick={() => {
|
onClick={toggleNestedMenu}
|
||||||
setIsOpen(!isOpen);
|
dropdownContent={isOpen && children}
|
||||||
}}
|
isActive={isActive || isOpen}
|
||||||
dropdownContent={dropdownContent}
|
|
||||||
isActive={isOpen}
|
|
||||||
type="primary"
|
type="primary"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -36,6 +35,13 @@ function NestedMenu({ children }) {
|
|||||||
|
|
||||||
NestedMenu.propTypes = {
|
NestedMenu.propTypes = {
|
||||||
children: PropTypes.any.isRequired,
|
children: PropTypes.any.isRequired,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
NestedMenu.defaultProps = {
|
||||||
|
icon: "tool-more-menu",
|
||||||
|
label: "More",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NestedMenu;
|
export default NestedMenu;
|
||||||
|
|||||||
@ -55,7 +55,20 @@ function ViewerLayout({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultTool = { icon: 'tool-more-menu', label: 'More', isActive: false };
|
||||||
const [toolbars, setToolbars] = useState({ primary: [], secondary: [] });
|
const [toolbars, setToolbars] = useState({ primary: [], secondary: [] });
|
||||||
|
const [activeTool, setActiveTool] = useState(defaultTool);
|
||||||
|
|
||||||
|
const setActiveToolHandler = (tool, isNested) => {
|
||||||
|
setActiveTool(isNested ? tool : defaultTool);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPrimaryClickHandler = (evt, btn) => {
|
||||||
|
if (btn.props && btn.props.commands && evt.value && btn.props.commands[evt.value]) {
|
||||||
|
const { commandName, commandOptions } = btn.props.commands[evt.value];
|
||||||
|
commandsManager.runCommand(commandName, commandOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = ToolBarService.subscribe(
|
const { unsubscribe } = ToolBarService.subscribe(
|
||||||
@ -63,8 +76,8 @@ function ViewerLayout({
|
|||||||
() => {
|
() => {
|
||||||
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
|
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
|
||||||
const updatedToolbars = {
|
const updatedToolbars = {
|
||||||
primary: ToolBarService.getButtonSection('primary'),
|
primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler, setActiveTool: setActiveToolHandler }),
|
||||||
secondary: ToolBarService.getButtonSection('secondary'),
|
secondary: ToolBarService.getButtonSection('secondary', { setActiveTool: setActiveToolHandler }),
|
||||||
};
|
};
|
||||||
setToolbars(updatedToolbars);
|
setToolbars(updatedToolbars);
|
||||||
}
|
}
|
||||||
@ -86,11 +99,10 @@ function ViewerLayout({
|
|||||||
|
|
||||||
if (!isNested) {
|
if (!isNested) {
|
||||||
const { id, Component, componentProps } = toolDef;
|
const { id, Component, componentProps } = toolDef;
|
||||||
|
|
||||||
return <Component key={id} id={id} {...componentProps} />;
|
return <Component key={id} id={id} {...componentProps} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<NestedMenu>
|
<NestedMenu isActive={activeTool.isActive} icon={activeTool.icon} label={activeTool.label}>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{toolDef.map(x => {
|
{toolDef.map(x => {
|
||||||
const { id, Component, componentProps } = x;
|
const { id, Component, componentProps } = x;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
{
|
{
|
||||||
name: 'ohif.divider',
|
name: 'ohif.divider',
|
||||||
defaultComponent: ToolbarDivider,
|
defaultComponent: ToolbarDivider,
|
||||||
clickHandler: () => {},
|
clickHandler: () => { },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ohif.action',
|
name: 'ohif.action',
|
||||||
@ -30,7 +30,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
optionalConfig: [],
|
optionalConfig: [],
|
||||||
requiredProps: [],
|
requiredProps: [],
|
||||||
optionalProps: [],
|
optionalProps: [],
|
||||||
clickHandler: (evt, clickedBtn, btnSectionName) => {
|
clickHandler: (evt, clickedBtn, btnSectionName, metadata, viewerProps) => {
|
||||||
const { props } = clickedBtn;
|
const { props } = clickedBtn;
|
||||||
const allButtons = toolbarService.getButtons();
|
const allButtons = toolbarService.getButtons();
|
||||||
|
|
||||||
@ -47,6 +47,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
clickedBtn.config.groupName === btn.config.groupName
|
clickedBtn.config.groupName === btn.config.groupName
|
||||||
) {
|
) {
|
||||||
btn.props.isActive = false;
|
btn.props.isActive = false;
|
||||||
|
|
||||||
|
if (viewerProps.setActiveTool) {
|
||||||
|
viewerProps.setActiveTool(props, metadata.isNested);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,7 +67,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
{
|
{
|
||||||
name: 'ohif.layoutSelector',
|
name: 'ohif.layoutSelector',
|
||||||
defaultComponent: ToolbarLayoutSelector,
|
defaultComponent: ToolbarLayoutSelector,
|
||||||
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
clickHandler: (evt, clickedBtn, btnSectionName) => { },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ohif.toggle',
|
name: 'ohif.toggle',
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.12.3",
|
"dcmjs": "0.14.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.11.0",
|
"react": "^16.11.0",
|
||||||
"react-dom": "^16.11.0"
|
"react-dom": "^16.11.0"
|
||||||
|
|||||||
@ -30,8 +30,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "4.15.1",
|
"cornerstone-tools": "4.16.0",
|
||||||
"dcmjs": "^0.12.3",
|
"dcmjs": "0.14.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-tools": "4.15.1",
|
"cornerstone-tools": "4.15.1",
|
||||||
"dcmjs": "^0.12.2",
|
"dcmjs": "0.14.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@ -33,8 +33,8 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "4.15.1",
|
"cornerstone-tools": "4.16.0",
|
||||||
"dcmjs": "^0.12.3",
|
"dcmjs": "0.14.0",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
@ -203,9 +203,11 @@ function OHIFCornerstoneSRViewport({
|
|||||||
PatientSex,
|
PatientSex,
|
||||||
PatientAge,
|
PatientAge,
|
||||||
SliceThickness,
|
SliceThickness,
|
||||||
|
ManufacturerModelName,
|
||||||
StudyDate,
|
StudyDate,
|
||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
PixelSpacing,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
} = activeDisplaySetData;
|
} = activeDisplaySetData;
|
||||||
|
|
||||||
@ -233,13 +235,10 @@ function OHIFCornerstoneSRViewport({
|
|||||||
updateViewport(newMeasurementSelected);
|
updateViewport(newMeasurementSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(currentImageIdIndex);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewportActionBar
|
<ViewportActionBar
|
||||||
onSeriesChange={onMeasurementChange}
|
onSeriesChange={onMeasurementChange}
|
||||||
showPatientInfo={viewportIndex === activeViewportIndex}
|
|
||||||
showNavArrows={viewportIndex === activeViewportIndex}
|
showNavArrows={viewportIndex === activeViewportIndex}
|
||||||
studyData={{
|
studyData={{
|
||||||
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
||||||
@ -250,13 +249,13 @@ function OHIFCornerstoneSRViewport({
|
|||||||
seriesDescription: SeriesDescription,
|
seriesDescription: SeriesDescription,
|
||||||
modality: Modality,
|
modality: Modality,
|
||||||
patientInformation: {
|
patientInformation: {
|
||||||
patientName: PatientName ? PatientName.Alphabetic || '' : '',
|
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
|
||||||
patientSex: PatientSex || '',
|
patientSex: PatientSex || '',
|
||||||
patientAge: PatientAge || '',
|
patientAge: PatientAge || '',
|
||||||
MRN: PatientID || '',
|
MRN: PatientID || '',
|
||||||
thickness: `${SliceThickness}mm`,
|
thickness: `${SliceThickness}mm`,
|
||||||
spacing: '',
|
spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '',
|
||||||
scanner: '',
|
scanner: ManufacturerModelName || '',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -28,11 +28,12 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.12.4",
|
"dcmjs": "0.14.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"webpack": "^4.0.0"
|
"webpack": "^4.0.0",
|
||||||
|
"cornerstone-tools": "4.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.7.6",
|
"@babel/runtime": "7.7.6",
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
|
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
|
||||||
|
|
||||||
function ActionButtons() {
|
function ActionButtons({ onExportClick, onCreateReportClick }) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ButtonGroup onClick={() => alert('Export')}>
|
<ButtonGroup onClick={onExportClick}>
|
||||||
<Button
|
<Button
|
||||||
className="px-2 py-2 text-base text-white bg-black border-primary-main"
|
className="px-2 py-2 text-base text-white bg-black border-primary-main"
|
||||||
size="initial"
|
size="initial"
|
||||||
@ -26,7 +26,7 @@ function ActionButtons() {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="initial"
|
size="initial"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => alert('Create Report')}
|
onClick={onCreateReportClick}
|
||||||
>
|
>
|
||||||
Create Report
|
Create Report
|
||||||
</Button>
|
</Button>
|
||||||
@ -34,4 +34,14 @@ function ActionButtons() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionButtons.propTypes = {
|
||||||
|
onExportClick: PropTypes.func,
|
||||||
|
onCreateReportClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
ActionButtons.defaultProps = {
|
||||||
|
onExportClick: () => alert('Export'),
|
||||||
|
onCreateReportClick: () => alert('Create Report'),
|
||||||
|
};
|
||||||
|
|
||||||
export default ActionButtons;
|
export default ActionButtons;
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StudySummary, MeasurementTable } from '@ohif/ui';
|
import { StudySummary, MeasurementTable } from '@ohif/ui';
|
||||||
import { DicomMetadataStore } from '@ohif/core';
|
import { DicomMetadataStore, DICOMSR } from '@ohif/core';
|
||||||
import { useDebounce } from '@hooks';
|
import { useDebounce } from '@hooks';
|
||||||
import ActionButtons from './ActionButtons';
|
import ActionButtons from './ActionButtons';
|
||||||
import { useTrackedMeasurements } from '../../getContextModule';
|
import { useTrackedMeasurements } from '../../getContextModule';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
||||||
key: undefined, //
|
key: undefined, //
|
||||||
@ -13,7 +16,7 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
|||||||
description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST',
|
description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST',
|
||||||
};
|
};
|
||||||
|
|
||||||
function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
|
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||||
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
|
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
|
||||||
Date.now().toString()
|
Date.now().toString()
|
||||||
);
|
);
|
||||||
@ -42,7 +45,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
|
|||||||
trackedSeries.includes(m.referenceSeriesUID)
|
trackedSeries.includes(m.referenceSeriesUID)
|
||||||
);
|
);
|
||||||
const mappedMeasurements = filteredMeasurements.map((m, index) =>
|
const mappedMeasurements = filteredMeasurements.map((m, index) =>
|
||||||
_mapMeasurementToDisplay(m, index)
|
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
|
||||||
);
|
);
|
||||||
setDisplayMeasurements(mappedMeasurements);
|
setDisplayMeasurements(mappedMeasurements);
|
||||||
// eslint-ignore-next-line
|
// eslint-ignore-next-line
|
||||||
@ -102,6 +105,35 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
|
|||||||
|
|
||||||
const activeMeasurementItem = 0;
|
const activeMeasurementItem = 0;
|
||||||
|
|
||||||
|
const onExportClick = () => {
|
||||||
|
const measurements = MeasurementService.getMeasurements();
|
||||||
|
const trackedMeasurements = measurements.filter(
|
||||||
|
m =>
|
||||||
|
trackedStudy === m.referenceStudyUID &&
|
||||||
|
trackedSeries.includes(m.referenceSeriesUID)
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO -> local download.
|
||||||
|
DICOMSR.downloadReport(trackedMeasurements, dataSource);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateReportClick = () => {
|
||||||
|
const measurements = MeasurementService.getMeasurements();
|
||||||
|
const trackedMeasurements = measurements.filter(
|
||||||
|
m =>
|
||||||
|
trackedStudy === m.referenceStudyUID &&
|
||||||
|
trackedSeries.includes(m.referenceSeriesUID)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const dataSources = extensionManager.getDataSources();
|
||||||
|
// TODO -> Eventually deal with multiple dataSources.
|
||||||
|
// Would need some way of saying which one is the "push" dataSource
|
||||||
|
const dataSource = dataSources[0];
|
||||||
|
|
||||||
|
DICOMSR.storeMeasurements(trackedMeasurements, dataSource);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
||||||
@ -121,7 +153,10 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center p-4">
|
<div className="flex justify-center p-4">
|
||||||
<ActionButtons />
|
<ActionButtons
|
||||||
|
onExportClick={onExportClick}
|
||||||
|
onCreateReportClick={onCreateReportClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -130,7 +165,7 @@ function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
|
|||||||
PanelMeasurementTableTracking.propTypes = {};
|
PanelMeasurementTableTracking.propTypes = {};
|
||||||
|
|
||||||
// TODO: This could be a MeasurementService mapper
|
// TODO: This could be a MeasurementService mapper
|
||||||
function _mapMeasurementToDisplay(measurement, index) {
|
function _mapMeasurementToDisplay(measurement, index, types) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
@ -153,12 +188,14 @@ function _mapMeasurementToDisplay(measurement, index) {
|
|||||||
return {
|
return {
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
label: '(empty)', // 'Label short description',
|
label: '(empty)', // 'Label short description',
|
||||||
displayText: _getDisplayText(
|
displayText:
|
||||||
measurement.points,
|
_getDisplayText(
|
||||||
PixelSpacing,
|
measurement,
|
||||||
SeriesNumber,
|
PixelSpacing,
|
||||||
InstanceNumber
|
SeriesNumber,
|
||||||
),
|
InstanceNumber,
|
||||||
|
types
|
||||||
|
) || [],
|
||||||
// TODO: handle one layer down
|
// TODO: handle one layer down
|
||||||
isActive: false, // activeMeasurementItem === i + 1,
|
isActive: false, // activeMeasurementItem === i + 1,
|
||||||
};
|
};
|
||||||
@ -169,7 +206,13 @@ function _mapMeasurementToDisplay(measurement, index) {
|
|||||||
* @param {*} points
|
* @param {*} points
|
||||||
* @param {*} pixelSpacing
|
* @param {*} pixelSpacing
|
||||||
*/
|
*/
|
||||||
function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
|
function _getDisplayText(
|
||||||
|
measurement,
|
||||||
|
pixelSpacing,
|
||||||
|
seriesNumber,
|
||||||
|
instanceNumber,
|
||||||
|
types
|
||||||
|
) {
|
||||||
// TODO: determination of shape influences text
|
// TODO: determination of shape influences text
|
||||||
// Length: 'xx.x unit (S:x, I:x)'
|
// Length: 'xx.x unit (S:x, I:x)'
|
||||||
// Rectangle: 'xx.x x xx.x unit (S:x, I:x)',
|
// Rectangle: 'xx.x x xx.x unit (S:x, I:x)',
|
||||||
@ -177,6 +220,8 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
|
|||||||
// Bidirectional?
|
// Bidirectional?
|
||||||
// Freehand?
|
// Freehand?
|
||||||
|
|
||||||
|
const { type, points } = measurement;
|
||||||
|
|
||||||
const hasPixelSpacing =
|
const hasPixelSpacing =
|
||||||
pixelSpacing !== undefined &&
|
pixelSpacing !== undefined &&
|
||||||
Array.isArray(pixelSpacing) &&
|
Array.isArray(pixelSpacing) &&
|
||||||
@ -186,13 +231,37 @@ function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
|
|||||||
: [1, 1];
|
: [1, 1];
|
||||||
const unit = hasPixelSpacing ? 'mm' : 'px';
|
const unit = hasPixelSpacing ? 'mm' : 'px';
|
||||||
|
|
||||||
const { x: x1, y: y1 } = points[0];
|
switch (type) {
|
||||||
const { x: x2, y: y2 } = points[1];
|
case types.POLYLINE:
|
||||||
const dx = (x2 - x1) * colPixelSpacing;
|
const { length } = measurement;
|
||||||
const dy = (y2 - y1) * rowPixelSpacing;
|
|
||||||
const length = _round(Math.sqrt(dx * dx + dy * dy), 1);
|
|
||||||
|
|
||||||
return `${length} ${unit} (S:${seriesNumber}, I:${instanceNumber})`;
|
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) {
|
function _round(value, decimals) {
|
||||||
|
|||||||
@ -201,10 +201,10 @@ function PanelStudyBrowserTracking({
|
|||||||
);
|
);
|
||||||
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
|
||||||
? [
|
? [
|
||||||
...expandedStudyInstanceUIDs.filter(
|
...expandedStudyInstanceUIDs.filter(
|
||||||
stdyUid => stdyUid !== StudyInstanceUID
|
stdyUid => stdyUid !== StudyInstanceUID
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
|
||||||
|
|
||||||
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
|
||||||
@ -285,7 +285,6 @@ function _mapDisplaySets(
|
|||||||
) {
|
) {
|
||||||
const thumbnailDisplaySets = [];
|
const thumbnailDisplaySets = [];
|
||||||
const thumbnailNoImageDisplaySets = [];
|
const thumbnailNoImageDisplaySets = [];
|
||||||
|
|
||||||
displaySets.forEach(ds => {
|
displaySets.forEach(ds => {
|
||||||
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
|
||||||
const componentType = _getComponentType(ds.Modality);
|
const componentType = _getComponentType(ds.Modality);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
@ -9,10 +10,20 @@ import {
|
|||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
useViewportDialog,
|
useViewportDialog,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
import throttle from 'lodash.throttle';
|
|
||||||
import { useTrackedMeasurements } from './../getContextModule';
|
import { useTrackedMeasurements } from './../getContextModule';
|
||||||
|
|
||||||
|
// TODO -> Get this list from the list of tracked measurements.
|
||||||
|
const {
|
||||||
|
ArrowAnnotateTool,
|
||||||
|
BidirectionalTool,
|
||||||
|
EllipticalRoiTool,
|
||||||
|
LengthTool,
|
||||||
|
} = cornerstoneTools;
|
||||||
|
|
||||||
|
const BaseAnnotationTool = cornerstoneTools.importInternal(
|
||||||
|
'base/BaseAnnotationTool'
|
||||||
|
);
|
||||||
|
|
||||||
// const cine = viewportSpecificData.cine;
|
// const cine = viewportSpecificData.cine;
|
||||||
|
|
||||||
// isPlaying = cine.isPlaying === true;
|
// isPlaying = cine.isPlaying === true;
|
||||||
@ -27,13 +38,15 @@ function TrackedCornerstoneViewport({
|
|||||||
viewportIndex,
|
viewportIndex,
|
||||||
}) {
|
}) {
|
||||||
const [trackedMeasurements] = useTrackedMeasurements();
|
const [trackedMeasurements] = useTrackedMeasurements();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ activeViewportIndex, viewports },
|
{ activeViewportIndex, viewports },
|
||||||
dispatchViewportGrid,
|
|
||||||
] = useViewportGrid();
|
] = useViewportGrid();
|
||||||
// viewportIndex, onSubmit
|
// viewportIndex, onSubmit
|
||||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||||
const [viewportData, setViewportData] = useState(null);
|
const [viewportData, setViewportData] = useState(null);
|
||||||
|
const [element, setElement] = useState(null);
|
||||||
|
const [isTracked, setIsTracked] = useState(false);
|
||||||
// TODO: Still needed? Better way than import `OHIF` and destructure?
|
// TODO: Still needed? Better way than import `OHIF` and destructure?
|
||||||
// Why is this managed by `core`?
|
// Why is this managed by `core`?
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -42,6 +55,75 @@ function TrackedCornerstoneViewport({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allTools = cornerstoneTools.store.state.tools;
|
||||||
|
const toolsForElement = allTools.filter(tool => tool.element === element);
|
||||||
|
|
||||||
|
toolsForElement.forEach(tool => {
|
||||||
|
if (
|
||||||
|
tool instanceof ArrowAnnotateTool ||
|
||||||
|
tool instanceof BidirectionalTool ||
|
||||||
|
tool instanceof EllipticalRoiTool ||
|
||||||
|
tool instanceof LengthTool
|
||||||
|
) {
|
||||||
|
const configuration = tool.configuration;
|
||||||
|
|
||||||
|
configuration.renderDashed = !isTracked;
|
||||||
|
|
||||||
|
tool.configuration = configuration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
|
||||||
|
if (enabledElement.image) {
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
}
|
||||||
|
}, [isTracked]);
|
||||||
|
|
||||||
|
const onElementEnabled = evt => {
|
||||||
|
const eventData = evt.detail;
|
||||||
|
const targetElement = eventData.element;
|
||||||
|
|
||||||
|
const allTools = cornerstoneTools.store.state.tools;
|
||||||
|
|
||||||
|
const toolsForElement = allTools.filter(
|
||||||
|
tool => tool.element === targetElement
|
||||||
|
);
|
||||||
|
|
||||||
|
toolsForElement.forEach(tool => {
|
||||||
|
if (
|
||||||
|
tool instanceof ArrowAnnotateTool ||
|
||||||
|
tool instanceof BidirectionalTool ||
|
||||||
|
tool instanceof EllipticalRoiTool ||
|
||||||
|
tool instanceof LengthTool
|
||||||
|
) {
|
||||||
|
const configuration = tool.configuration;
|
||||||
|
|
||||||
|
configuration.renderDashed = !isTracked;
|
||||||
|
|
||||||
|
tool.configuration = configuration;
|
||||||
|
} else if (tool instanceof BaseAnnotationTool) {
|
||||||
|
const configuration = tool.configuration;
|
||||||
|
|
||||||
|
configuration.renderDashed = true;
|
||||||
|
|
||||||
|
tool.configuration = configuration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(targetElement);
|
||||||
|
|
||||||
|
if (enabledElement.image) {
|
||||||
|
cornerstone.updateImage(targetElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setElement(targetElement);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const {
|
const {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
@ -116,6 +198,7 @@ function TrackedCornerstoneViewport({
|
|||||||
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
|
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
|
||||||
);
|
);
|
||||||
const { trackedSeries } = trackedMeasurements.context;
|
const { trackedSeries } = trackedMeasurements.context;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Modality,
|
Modality,
|
||||||
SeriesDate,
|
SeriesDate,
|
||||||
@ -123,19 +206,25 @@ function TrackedCornerstoneViewport({
|
|||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PatientID,
|
PatientID,
|
||||||
PatientName,
|
PatientName,
|
||||||
PatientSex,
|
PatientSex,
|
||||||
PatientAge,
|
PatientAge,
|
||||||
SliceThickness,
|
SliceThickness,
|
||||||
|
PixelSpacing,
|
||||||
|
ManufacturerModelName
|
||||||
} = displaySet.images[0];
|
} = displaySet.images[0];
|
||||||
|
|
||||||
|
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
|
||||||
|
setIsTracked(!isTracked);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewportActionBar
|
<ViewportActionBar
|
||||||
onSeriesChange={direction => alert(`Series ${direction}`)}
|
onSeriesChange={direction => alert(`Series ${direction}`)}
|
||||||
showPatientInfo={viewportIndex === activeViewportIndex}
|
|
||||||
showNavArrows={viewportIndex === activeViewportIndex}
|
showNavArrows={viewportIndex === activeViewportIndex}
|
||||||
studyData={{
|
studyData={{
|
||||||
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid],
|
||||||
@ -146,19 +235,20 @@ function TrackedCornerstoneViewport({
|
|||||||
seriesDescription: SeriesDescription,
|
seriesDescription: SeriesDescription,
|
||||||
modality: Modality,
|
modality: Modality,
|
||||||
patientInformation: {
|
patientInformation: {
|
||||||
patientName: PatientName ? PatientName.Alphabetic || '' : '',
|
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
|
||||||
patientSex: PatientSex || '',
|
patientSex: PatientSex || '',
|
||||||
patientAge: PatientAge || '',
|
patientAge: PatientAge || '',
|
||||||
MRN: PatientID || '',
|
MRN: PatientID || '',
|
||||||
thickness: `${SliceThickness}mm`,
|
thickness: `${SliceThickness}mm`,
|
||||||
spacing: '',
|
spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '',
|
||||||
scanner: '',
|
scanner: ManufacturerModelName || '',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
||||||
<div className="relative flex flex-row w-full h-full">
|
<div className="relative flex flex-row w-full h-full">
|
||||||
<CornerstoneViewport
|
<CornerstoneViewport
|
||||||
|
onElementEnabled={onElementEnabled}
|
||||||
viewportIndex={viewportIndex}
|
viewportIndex={viewportIndex}
|
||||||
imageIds={imageIds}
|
imageIds={imageIds}
|
||||||
imageIdIndex={currentImageIdIndex}
|
imageIdIndex={currentImageIdIndex}
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dcmjs": "0.12.2",
|
"dcmjs": "0.14.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.9.6",
|
"@ohif/core": "^2.9.6",
|
||||||
"cornerstone-tools": "4.15.1",
|
"cornerstone-tools": "4.16.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||||
// Only ways that you can pass in a custom React component for render :l
|
// Only ways that you can pass in a custom React component for render :l
|
||||||
|
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
|
||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// Divider
|
// Divider
|
||||||
@ -29,13 +32,58 @@ export default [
|
|||||||
config: {
|
config: {
|
||||||
groupName: 'primaryTool',
|
groupName: 'primaryTool',
|
||||||
},
|
},
|
||||||
|
component: ExpandableToolbarButton,
|
||||||
props: {
|
props: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
icon: 'tool-window-level',
|
icon: 'tool-window-level',
|
||||||
label: 'Levels',
|
|
||||||
commandName: 'setToolActive',
|
commandName: 'setToolActive',
|
||||||
commandOptions: { toolName: 'Wwwc' },
|
commandOptions: { toolName: 'Wwwc' },
|
||||||
|
commands: {
|
||||||
|
'windowLevelPreset1': {
|
||||||
|
commandName: 'windowLevelPreset1',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
'windowLevelPreset2': {
|
||||||
|
commandName: 'windowLevelPreset2',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
'windowLevelPreset3': {
|
||||||
|
commandName: 'windowLevelPreset3',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
'windowLevelPreset4': {
|
||||||
|
commandName: 'windowLevelPreset4',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
'windowLevelPreset5': {
|
||||||
|
commandName: 'windowLevelPreset5',
|
||||||
|
commandOptions: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
|
content: ListMenu,
|
||||||
|
contentProps: {
|
||||||
|
options: [
|
||||||
|
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
|
||||||
|
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
|
||||||
|
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
|
||||||
|
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
|
||||||
|
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
|
||||||
|
],
|
||||||
|
renderer: ({ title, subtitle, isActive, index }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -31,14 +31,14 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-tools": "^4.12.0",
|
"cornerstone-tools": "4.16.0",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dicom-parser": "^1.8.3"
|
"dicom-parser": "^1.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.7.6",
|
"@babel/runtime": "7.7.6",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
"dcmjs": "^0.12.4",
|
"dcmjs": "0.14.0",
|
||||||
"dicomweb-client": "^0.6.0",
|
"dicomweb-client": "^0.6.0",
|
||||||
"immer": "6.0.2",
|
"immer": "6.0.2",
|
||||||
"isomorphic-base64": "^1.0.2",
|
"isomorphic-base64": "^1.0.2",
|
||||||
|
|||||||
@ -6,6 +6,10 @@ import {
|
|||||||
stowSRFromMeasurements,
|
stowSRFromMeasurements,
|
||||||
} from './handleStructuredReport';
|
} from './handleStructuredReport';
|
||||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
|
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -47,7 +51,7 @@ const retrieveMeasurements = server => {
|
|||||||
* @param {serverType} server
|
* @param {serverType} server
|
||||||
* @returns {Object} With message to be displayed on success
|
* @returns {Object} With message to be displayed on success
|
||||||
*/
|
*/
|
||||||
const storeMeasurements = async (measurementData, filter, server) => {
|
const storeMeasurementsOld = async (measurementData, filter, server) => {
|
||||||
log.info('[DICOMSR] storeMeasurements');
|
log.info('[DICOMSR] storeMeasurements');
|
||||||
|
|
||||||
if (!server || server.type !== 'dicomWeb') {
|
if (!server || server.type !== 'dicomWeb') {
|
||||||
@ -78,4 +82,128 @@ const storeMeasurements = async (measurementData, filter, server) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { retrieveMeasurements, storeMeasurements };
|
/**
|
||||||
|
*
|
||||||
|
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||||
|
* that you wish to serialize.
|
||||||
|
*/
|
||||||
|
const downloadReport = measurementData => {
|
||||||
|
const srDataset = generateReport(measurementData);
|
||||||
|
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||||
|
|
||||||
|
//Create a URL for the binary.
|
||||||
|
var objectUrl = URL.createObjectURL(reportBlob);
|
||||||
|
window.location.assign(objectUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||||
|
* that you wish to serialize.
|
||||||
|
*/
|
||||||
|
const generateReport = measurementData => {
|
||||||
|
const ids = measurementData.map(md => md.id);
|
||||||
|
const filteredToolState = _getFilteredCornerstoneToolState(ids);
|
||||||
|
|
||||||
|
const report = MeasurementReport.generateReport(
|
||||||
|
filteredToolState,
|
||||||
|
cornerstone.metaData
|
||||||
|
);
|
||||||
|
|
||||||
|
return report.dataset;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||||
|
* that you wish to serialize.
|
||||||
|
* @param {object} dataSource The dataSource that you wish to use to persist the data.
|
||||||
|
*/
|
||||||
|
const storeMeasurements = async (measurementData, dataSource) => {
|
||||||
|
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
|
||||||
|
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||||
|
log.info('[DICOMSR] storeMeasurements');
|
||||||
|
|
||||||
|
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
|
||||||
|
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
|
||||||
|
return Promise.reject({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const naturalizedReport = generateReport(measurementData);
|
||||||
|
const { StudyInstanceUID } = naturalizedReport;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dataSource.store.dicom(naturalizedReport);
|
||||||
|
|
||||||
|
if (StudyInstanceUID) {
|
||||||
|
studies.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Measurements saved successfully',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||||
|
);
|
||||||
|
throw new Error('Error while saving the measurements.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getFilteredCornerstoneToolState(uidFilter) {
|
||||||
|
const globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
const filteredToolState = {};
|
||||||
|
|
||||||
|
function addToFilteredToolState(imageId, toolType, toolDataI) {
|
||||||
|
if (!filteredToolState[imageId]) {
|
||||||
|
filteredToolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdSpecificToolState = filteredToolState[imageId];
|
||||||
|
|
||||||
|
if (!imageIdSpecificToolState[toolType]) {
|
||||||
|
imageIdSpecificToolState[toolType] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdSpecificToolState[toolType].data;
|
||||||
|
|
||||||
|
toolData.push(toolDataI);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uids = uidFilter.slice();
|
||||||
|
const imageIds = Object.keys(globalToolState);
|
||||||
|
|
||||||
|
for (let i = 0; i < imageIds.length; i++) {
|
||||||
|
const imageId = imageIds[i];
|
||||||
|
const imageIdSpecificToolState = globalToolState[imageId];
|
||||||
|
|
||||||
|
const toolTypes = Object.keys(imageIdSpecificToolState);
|
||||||
|
|
||||||
|
for (let j = 0; j < toolTypes.length; j++) {
|
||||||
|
const toolType = toolTypes[j];
|
||||||
|
const toolData = imageIdSpecificToolState[toolType].data;
|
||||||
|
|
||||||
|
if (toolData) {
|
||||||
|
for (let k = 0; k < toolData.length; k++) {
|
||||||
|
const toolDataK = toolData[k];
|
||||||
|
const uidIndex = uids.findIndex(uid => uid === toolDataK.id);
|
||||||
|
|
||||||
|
if (uidIndex !== -1) {
|
||||||
|
addToFilteredToolState(imageId, toolType, toolDataK);
|
||||||
|
uids.splice(uidIndex, 1);
|
||||||
|
|
||||||
|
if (!uids.length) {
|
||||||
|
return filteredToolState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredToolState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { retrieveMeasurements, storeMeasurements, downloadReport };
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
|
import {
|
||||||
|
retrieveMeasurements,
|
||||||
|
storeMeasurements,
|
||||||
|
downloadReport,
|
||||||
|
} from './dataExchange';
|
||||||
import isToolSupported from './utils/isToolSupported';
|
import isToolSupported from './utils/isToolSupported';
|
||||||
|
|
||||||
const DICOMSR = {
|
const DICOMSR = {
|
||||||
retrieveMeasurements,
|
retrieveMeasurements,
|
||||||
storeMeasurements,
|
storeMeasurements,
|
||||||
|
downloadReport,
|
||||||
isToolSupported,
|
isToolSupported,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { DicomMetadataStore } from '@ohif/core';
|
|||||||
function create({
|
function create({
|
||||||
query,
|
query,
|
||||||
retrieve,
|
retrieve,
|
||||||
|
store,
|
||||||
retrieveSeriesMetadata,
|
retrieveSeriesMetadata,
|
||||||
getImageIdsForDisplaySet,
|
getImageIdsForDisplaySet,
|
||||||
}) {
|
}) {
|
||||||
@ -44,13 +45,20 @@ function create({
|
|||||||
series: {},
|
series: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultStore = {
|
||||||
|
dicom: async naturalizedDataset => {
|
||||||
|
throw new Error(
|
||||||
|
'store.dicom(naturalizedDicom, StudyInstanceUID) not implemented for dataSource.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: query || defaultQuery,
|
query: query || defaultQuery,
|
||||||
retrieve: retrieve || defaultRetrieve,
|
retrieve: retrieve || defaultRetrieve,
|
||||||
|
store: store || defaultStore,
|
||||||
getImageIdsForDisplaySet,
|
getImageIdsForDisplaySet,
|
||||||
retrieveSeriesMetadata,
|
retrieveSeriesMetadata,
|
||||||
// then go get all series level metadata.
|
|
||||||
// Store this in the DICOM MetadataStore.
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -137,6 +137,7 @@ export default class ExtensionManager {
|
|||||||
|
|
||||||
getDataSources = dataSourceName => {
|
getDataSources = dataSourceName => {
|
||||||
if (dataSourceName === undefined) {
|
if (dataSourceName === undefined) {
|
||||||
|
// Default to the activeDataSource
|
||||||
dataSourceName = this.activeDataSource;
|
dataSourceName = this.activeDataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +145,10 @@ export default class ExtensionManager {
|
|||||||
return this.dataSourceMap[dataSourceName];
|
return this.dataSourceMap[dataSourceName];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getActiveDataSource = () => {
|
||||||
|
return this.activeDataSource;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {string} moduleType
|
* @param {string} moduleType
|
||||||
|
|||||||
@ -40,6 +40,10 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
|||||||
'type',
|
'type',
|
||||||
'unit',
|
'unit',
|
||||||
'area', // TODO: Add concept names instead (descriptor)
|
'area', // TODO: Add concept names instead (descriptor)
|
||||||
|
'length',
|
||||||
|
'shortestDiameter',
|
||||||
|
'longestDiameter',
|
||||||
|
'text', // NOTE: There is nothing like this in SR.
|
||||||
'points',
|
'points',
|
||||||
'source',
|
'source',
|
||||||
];
|
];
|
||||||
@ -53,6 +57,7 @@ const EVENTS = {
|
|||||||
const VALUE_TYPES = {
|
const VALUE_TYPES = {
|
||||||
POLYLINE: 'value_type::polyline',
|
POLYLINE: 'value_type::polyline',
|
||||||
POINT: 'value_type::point',
|
POINT: 'value_type::point',
|
||||||
|
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
|
||||||
ELLIPSE: 'value_type::ellipse',
|
ELLIPSE: 'value_type::ellipse',
|
||||||
MULTIPOINT: 'value_type::multipoint',
|
MULTIPOINT: 'value_type::multipoint',
|
||||||
CIRCLE: 'value_type::circle',
|
CIRCLE: 'value_type::circle',
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export default class ToolBarService {
|
|||||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtonSection(key) {
|
getButtonSection(key, props) {
|
||||||
const buttonSectionIds = this.buttonSections[key];
|
const buttonSectionIds = this.buttonSections[key];
|
||||||
const buttonsInSection = [];
|
const buttonsInSection = [];
|
||||||
|
|
||||||
@ -79,7 +79,8 @@ export default class ToolBarService {
|
|||||||
|
|
||||||
btnIds.forEach(nestedBtnId => {
|
btnIds.forEach(nestedBtnId => {
|
||||||
const nestedBtn = this.buttons[nestedBtnId];
|
const nestedBtn = this.buttons[nestedBtnId];
|
||||||
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key);
|
const metadata = { isNested: true };
|
||||||
|
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, metadata, props);
|
||||||
|
|
||||||
nestedButtons.push(mappedNestedBtn);
|
nestedButtons.push(mappedNestedBtn);
|
||||||
});
|
});
|
||||||
@ -90,7 +91,8 @@ export default class ToolBarService {
|
|||||||
} else {
|
} else {
|
||||||
const btnId = btnIdOrArray;
|
const btnId = btnIdOrArray;
|
||||||
const btn = this.buttons[btnId];
|
const btn = this.buttons[btnId];
|
||||||
const mappedBtn = this._mapButtonToDisplay(btn, key);
|
const metadata = { isNested: false };
|
||||||
|
const mappedBtn = this._mapButtonToDisplay(btn, key, metadata, props);
|
||||||
|
|
||||||
buttonsInSection.push(mappedBtn);
|
buttonsInSection.push(mappedBtn);
|
||||||
}
|
}
|
||||||
@ -136,8 +138,8 @@ export default class ToolBarService {
|
|||||||
* @param {*} btn
|
* @param {*} btn
|
||||||
* @param {*} btnSection
|
* @param {*} btnSection
|
||||||
*/
|
*/
|
||||||
_mapButtonToDisplay(btn, btnSection) {
|
_mapButtonToDisplay(btn, btnSection, metadata, props) {
|
||||||
const { id, type, component, props } = btn;
|
const { id, type, component } = btn;
|
||||||
const buttonType = this._buttonTypes()[type];
|
const buttonType = this._buttonTypes()[type];
|
||||||
|
|
||||||
if (!buttonType) {
|
if (!buttonType) {
|
||||||
@ -146,7 +148,7 @@ export default class ToolBarService {
|
|||||||
|
|
||||||
const onClick = evt => {
|
const onClick = evt => {
|
||||||
if (buttonType.clickHandler) {
|
if (buttonType.clickHandler) {
|
||||||
buttonType.clickHandler(evt, btn, btnSection);
|
buttonType.clickHandler(evt, btn, btnSection, metadata, props);
|
||||||
}
|
}
|
||||||
if (btn.props.onClick) {
|
if (btn.props.onClick) {
|
||||||
btn.onClick(evt, btn, btnSection);
|
btn.onClick(evt, btn, btnSection);
|
||||||
@ -154,12 +156,15 @@ export default class ToolBarService {
|
|||||||
if (btn.props.clickHandler) {
|
if (btn.props.clickHandler) {
|
||||||
btn.clickHandler(evt, btn, btnSection);
|
btn.clickHandler(evt, btn, btnSection);
|
||||||
}
|
}
|
||||||
|
if (props && props.onClick) {
|
||||||
|
props.onClick(evt, btn, btnSection, props);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
Component: component || buttonType.defaultComponent,
|
Component: component || buttonType.defaultComponent,
|
||||||
componentProps: Object.assign({}, props, { onClick }), //
|
componentProps: Object.assign({}, btn.props, { onClick }), //
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
platform/core/src/utils/formatPN.js
Normal file
18
platform/core/src/utils/formatPN.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Formats a patient name for display purposes
|
||||||
|
*/
|
||||||
|
export default function formatPN(name) {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the first ^ to a ', '. String.replace() only affects
|
||||||
|
// the first appearance of the character.
|
||||||
|
const commaBetweenFirstAndLast = name.replace('^', ', ');
|
||||||
|
|
||||||
|
// Replace any remaining '^' characters with spaces
|
||||||
|
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
|
||||||
|
|
||||||
|
// Trim any extraneous whitespace
|
||||||
|
return cleaned.trim();
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ import makeCancelable from './makeCancelable';
|
|||||||
import hotkeys from './hotkeys';
|
import hotkeys from './hotkeys';
|
||||||
import Queue from './Queue';
|
import Queue from './Queue';
|
||||||
import isDicomUid from './isDicomUid';
|
import isDicomUid from './isDicomUid';
|
||||||
|
import formatPN from './formatPN';
|
||||||
import resolveObjectPath from './resolveObjectPath';
|
import resolveObjectPath from './resolveObjectPath';
|
||||||
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
||||||
import * as progressTrackingUtils from './progressTrackingUtils';
|
import * as progressTrackingUtils from './progressTrackingUtils';
|
||||||
@ -26,6 +27,7 @@ const utils = {
|
|||||||
addServers,
|
addServers,
|
||||||
sortBy,
|
sortBy,
|
||||||
writeScript,
|
writeScript,
|
||||||
|
formatPN,
|
||||||
b64toBlob,
|
b64toBlob,
|
||||||
StackManager,
|
StackManager,
|
||||||
studyMetadataManager,
|
studyMetadataManager,
|
||||||
|
|||||||
@ -33,6 +33,8 @@ export {
|
|||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
EmptyStudies,
|
EmptyStudies,
|
||||||
|
ExpandableToolbarButton,
|
||||||
|
ListMenu,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
|||||||
7
platform/ui/src/assets/icons/close.svg
Normal file
7
platform/ui/src/assets/icons/close.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
|
||||||
|
<g fill="currentColor" fill-rule="evenodd">
|
||||||
|
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||||
|
<path d="M.188.187L8.813 8.812M8.813.187L.188 8.812" transform="translate(5 5)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Icon, Typography } from '@ohif/ui';
|
import { Icon, Typography } from '@ohif/ui';
|
||||||
|
|
||||||
const EmptyStudies = ({ className }) => {
|
// TODO: Add loading spinner to OHIF + use it here.
|
||||||
|
const EmptyStudies = ({ className, isLoading }) => {
|
||||||
return (
|
return (
|
||||||
<div className={classnames('flex-col inline-flex items-center', className)}>
|
<div className={classnames('flex-col inline-flex items-center', className)}>
|
||||||
<Icon name="magnifier" className="mb-4" />
|
<Icon name="magnifier" className="mb-4" />
|
||||||
<Typography className="text-primary-light" variant="h5">
|
<Typography className="text-primary-light" variant="h5">
|
||||||
No studies available
|
{!isLoading ? 'No studies available' : 'Loading...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
border-width: 10px 10px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color:#5acce6 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExpandableToolbarButton .ExpandableToolbarButton__content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExpandableToolbarButton:hover .ExpandableToolbarButton__content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { IconButton, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './ExpandableToolbarButton.css';
|
||||||
|
|
||||||
|
const ExpandableToolbarButton = ({
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
isActive,
|
||||||
|
onClick,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
content: Content,
|
||||||
|
contentProps
|
||||||
|
}) => {
|
||||||
|
const classes = {
|
||||||
|
type: {
|
||||||
|
primary: isActive
|
||||||
|
? 'text-black'
|
||||||
|
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
|
||||||
|
secondary: isActive
|
||||||
|
? 'text-black'
|
||||||
|
: 'text-white hover:bg-secondary-dark focus:bg-secondary-dark',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChildClickHandler = (...args) => {
|
||||||
|
onClick(...args);
|
||||||
|
|
||||||
|
if (contentProps.onClick) {
|
||||||
|
contentProps.onClick(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickHandler = (...args) => {
|
||||||
|
onClick(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="ExpandableToolbarButton">
|
||||||
|
<IconButton
|
||||||
|
variant={isActive ? 'contained' : 'text'}
|
||||||
|
className={classnames(
|
||||||
|
'mx-1',
|
||||||
|
classes.type[type],
|
||||||
|
isActive && 'ExpandableToolbarButton__arrow'
|
||||||
|
)}
|
||||||
|
onClick={onClickHandler}
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
|
<Icon name={icon} />
|
||||||
|
</IconButton>
|
||||||
|
<div className="absolute z-10 pt-4">
|
||||||
|
<div className={classnames("ExpandableToolbarButton__content w-48", className)}>
|
||||||
|
<Content {...contentProps} onClick={onChildClickHandler} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const noop = () => { };
|
||||||
|
|
||||||
|
ExpandableToolbarButton.defaultProps = {
|
||||||
|
isActive: false,
|
||||||
|
type: 'primary',
|
||||||
|
content: null,
|
||||||
|
onClick: noop,
|
||||||
|
};
|
||||||
|
|
||||||
|
ExpandableToolbarButton.propTypes = {
|
||||||
|
/* Influences background/hover styling */
|
||||||
|
type: PropTypes.oneOf(['primary', 'secondary']),
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
icon: PropTypes.string.isRequired,
|
||||||
|
/** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */
|
||||||
|
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||||
|
contentProps: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpandableToolbarButton;
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: Expandable Toolbar Button
|
||||||
|
menu: General
|
||||||
|
route: components/expandableToolbarButton
|
||||||
|
---
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Playground, Props } from 'docz';
|
||||||
|
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
|
||||||
|
|
||||||
|
# Toolbar Button
|
||||||
|
|
||||||
|
Expandable Toolbar Buttons are used to populate the Toolbar.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
{() => {
|
||||||
|
const props = {
|
||||||
|
content: ListMenu,
|
||||||
|
contentProps: {
|
||||||
|
options: [
|
||||||
|
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
|
||||||
|
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
|
||||||
|
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
|
||||||
|
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
|
||||||
|
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
|
||||||
|
],
|
||||||
|
renderer: ({ title, subtitle, isActive, index }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="py-10 flex justify-center">
|
||||||
|
<ExpandableToolbarButton {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
<Props of={ToolbarButton} />
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||||
|
export default ExpandableToolbarButton;
|
||||||
@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import arrowDown from './../../assets/icons/arrow-down.svg';
|
import arrowDown from './../../assets/icons/arrow-down.svg';
|
||||||
import calendar from './../../assets/icons/calendar.svg';
|
import calendar from './../../assets/icons/calendar.svg';
|
||||||
import cancel from './../../assets/icons/cancel.svg';
|
import cancel from './../../assets/icons/cancel.svg';
|
||||||
|
import close from './../../assets/icons/close.svg';
|
||||||
import dottedCircle from './../../assets/icons/dotted-circle.svg';
|
import dottedCircle from './../../assets/icons/dotted-circle.svg';
|
||||||
import circledCheckmark from './../../assets/icons/circled-checkmark.svg';
|
import circledCheckmark from './../../assets/icons/circled-checkmark.svg';
|
||||||
import chevronDown from './../../assets/icons/chevron-down.svg';
|
import chevronDown from './../../assets/icons/chevron-down.svg';
|
||||||
@ -49,6 +50,7 @@ const ICONS = {
|
|||||||
'arrow-down': arrowDown,
|
'arrow-down': arrowDown,
|
||||||
calendar: calendar,
|
calendar: calendar,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
close: close,
|
||||||
'dotted-circle': dottedCircle,
|
'dotted-circle': dottedCircle,
|
||||||
'circled-checkmark': circledCheckmark,
|
'circled-checkmark': circledCheckmark,
|
||||||
'chevron-down': chevronDown,
|
'chevron-down': chevronDown,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none';
|
'text-center items-center justify-center transition duration-300 ease-in-out outline-none font-bold focus:outline-none';
|
||||||
|
|
||||||
const roundedClasses = {
|
const roundedClasses = {
|
||||||
none: '',
|
none: '',
|
||||||
@ -84,7 +84,7 @@ const IconButton = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const buttonElement = useRef(null);
|
const buttonElement = useRef(null);
|
||||||
|
|
||||||
const handleOnClick = (e) => {
|
const handleOnClick = e => {
|
||||||
buttonElement.current.blur();
|
buttonElement.current.blur();
|
||||||
onClick(e);
|
onClick(e);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import Label from '../Label';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const baseInputClasses =
|
const baseInputClasses =
|
||||||
'shadow transition duration-300 appearance-none border rounded w-full py-2 px-3 text-sm text-white hover:border-gray-500 leading-tight focus:border-gray-500 focus:outline-none';
|
'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none';
|
||||||
|
|
||||||
const transparentClasses = {
|
const transparentClasses = {
|
||||||
true: 'bg-transparent',
|
true: 'bg-transparent',
|
||||||
false: '',
|
false: 'bg-black',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Input = ({
|
const Input = ({
|
||||||
@ -16,7 +16,7 @@ const Input = ({
|
|||||||
containerClassName = '',
|
containerClassName = '',
|
||||||
labelClassName = '',
|
labelClassName = '',
|
||||||
className = '',
|
className = '',
|
||||||
transparent = true,
|
transparent = false,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
|||||||
@ -43,7 +43,7 @@ InputText.propTypes = {
|
|||||||
isSortable: PropTypes.bool,
|
isSortable: PropTypes.bool,
|
||||||
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']),
|
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']),
|
||||||
onLabelClick: PropTypes.func,
|
onLabelClick: PropTypes.func,
|
||||||
value: PropTypes.string,
|
value: PropTypes.any,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
65
platform/ui/src/components/ListMenu/ListMenu.jsx
Normal file
65
platform/ui/src/components/ListMenu/ListMenu.jsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const ListMenu = ({ options = [], renderer, onClick }) => {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||||
|
|
||||||
|
const ListItem = (props) => {
|
||||||
|
const flex = 'flex flex-row justify-between items-center';
|
||||||
|
const theme = 'bg-indigo-dark';
|
||||||
|
const hover = 'hover:bg-primary-dark';
|
||||||
|
const spacing = 'p-3 h-8';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
flex,
|
||||||
|
theme,
|
||||||
|
spacing,
|
||||||
|
'cursor-pointer',
|
||||||
|
!props.isActive && hover,
|
||||||
|
props.isActive && 'bg-primary-light',
|
||||||
|
)}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
{renderer && renderer(props)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col rounded-md bg-secondary-dark pt-2 pb-2">
|
||||||
|
{options.map((option, index) => {
|
||||||
|
const onClickHandler = () => {
|
||||||
|
setSelectedIndex(index);
|
||||||
|
onClick({ ...option, index });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={`ListItem${index}`}
|
||||||
|
{...option}
|
||||||
|
index={index}
|
||||||
|
isActive={selectedIndex === index}
|
||||||
|
onClick={onClickHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const noop = () => { };
|
||||||
|
|
||||||
|
ListMenu.propTypes = {
|
||||||
|
options: PropTypes.array.isRequired,
|
||||||
|
renderer: PropTypes.func.isRequired,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
ListMenu.defaultProps = {
|
||||||
|
onClick: noop
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListMenu;
|
||||||
52
platform/ui/src/components/ListMenu/ListMenu.mdx
Normal file
52
platform/ui/src/components/ListMenu/ListMenu.mdx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: List Menu
|
||||||
|
menu: General
|
||||||
|
route: components/listMenu
|
||||||
|
---
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Playground, Props } from 'docz';
|
||||||
|
import { ListMenu } from '@ohif/ui';
|
||||||
|
|
||||||
|
# List Menu
|
||||||
|
|
||||||
|
List Menus are used to populate expandable Toolbar.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ListMenu } from '@ohif/ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<ToolbarButton options={[
|
||||||
|
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
|
||||||
|
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
|
||||||
|
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
|
||||||
|
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
|
||||||
|
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
|
||||||
|
]}
|
||||||
|
renderer={
|
||||||
|
({ title, subtitle, isActive, index }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
<Props of={ToolbarButton} />
|
||||||
2
platform/ui/src/components/ListMenu/index.js
Normal file
2
platform/ui/src/components/ListMenu/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import ListMenu from './ListMenu';
|
||||||
|
export default ListMenu;
|
||||||
@ -14,7 +14,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="overflow-y-auto overflow-x-hidden ohif-scrollbar max-h-112">
|
<div className="overflow-y-auto overflow-x-hidden ohif-scrollbar max-h-112">
|
||||||
{!!data.length &&
|
{!!data.length &&
|
||||||
data.map((measurementItem) => {
|
data.map(measurementItem => {
|
||||||
const { id, label, displayText, isActive } = measurementItem;
|
const { id, label, displayText, isActive } = measurementItem;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -45,9 +45,11 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
|
|||||||
<span className="text-base text-primary-light mb-1">
|
<span className="text-base text-primary-light mb-1">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span className="pl-2 border-l border-primary-light text-base text-white">
|
{displayText.map(line => (
|
||||||
{displayText}
|
<span className="pl-2 border-l border-primary-light text-base text-white">
|
||||||
</span>
|
{line}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
<Icon
|
<Icon
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'text-white w-4 absolute cursor-pointer transition duration-300',
|
'text-white w-4 absolute cursor-pointer transition duration-300',
|
||||||
@ -61,7 +63,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
|
|||||||
right: 4,
|
right: 4,
|
||||||
transform: isActive ? '' : 'translateX(100%)',
|
transform: isActive ? '' : 'translateX(100%)',
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={e => {
|
||||||
// stopPropagation needed to avoid disable the current active item
|
// stopPropagation needed to avoid disable the current active item
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit(id);
|
onEdit(id);
|
||||||
@ -108,7 +110,7 @@ MeasurementTable.propTypes = {
|
|||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
displayText: PropTypes.string,
|
displayText: PropTypes.arrayOf(PropTypes.string),
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|||||||
3
platform/ui/src/components/Modal/Modal.css
Normal file
3
platform/ui/src/components/Modal/Modal.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.modal-content {
|
||||||
|
max-height: calc(100vh - theme('spacing.250px'));
|
||||||
|
}
|
||||||
@ -1,25 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { Typography, useModal } from '@ohif/ui';
|
import './Modal.css';
|
||||||
|
|
||||||
const customStyle = {
|
import { Typography, useModal, IconButton, Icon } from '@ohif/ui';
|
||||||
overlay: {
|
|
||||||
zIndex: 1071,
|
|
||||||
backgroundColor: 'rgb(0, 0, 0, 0.8)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: '40px 0',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactModal.setAppElement(document.getElementById('root'));
|
ReactModal.setAppElement(document.getElementById('root'));
|
||||||
|
|
||||||
const Modal = ({
|
const Modal = ({
|
||||||
className,
|
|
||||||
closeButton,
|
closeButton,
|
||||||
shouldCloseOnEsc,
|
shouldCloseOnEsc,
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -39,13 +28,15 @@ const Modal = ({
|
|||||||
<header className="mb-6 pb-4 border-b border-secondary-main">
|
<header className="mb-6 pb-4 border-b border-secondary-main">
|
||||||
<Typography variant="h4">{title}</Typography>
|
<Typography variant="h4">{title}</Typography>
|
||||||
{closeButton && (
|
{closeButton && (
|
||||||
<button
|
<IconButton
|
||||||
className="absolute top-0 right-0 bg-primary-main focus:outline-none text-white rounded-full text-2xl w-8 h-8 flex justify-center items-center -mr-3 -mt-3"
|
className="absolute top-0 right-0 focus:outline-none flex -mr-3 -mt-3"
|
||||||
data-cy="close-button"
|
data-cy="close-button"
|
||||||
|
color="primary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
rounded="full"
|
||||||
>
|
>
|
||||||
×
|
<Icon name="close" className="text-white w-8 h-8" />
|
||||||
</button>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
@ -54,24 +45,17 @@ const Modal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
className={classNames(
|
className="relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded"
|
||||||
`relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded ${className}`
|
overlayClassName="fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-start justify-center py-16"
|
||||||
)}
|
|
||||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||||
onRequestClose={handleClose}
|
onRequestClose={handleClose}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={title}
|
title={title}
|
||||||
style={customStyle}
|
|
||||||
>
|
>
|
||||||
<>
|
<div className="px-6">{renderHeader()}</div>
|
||||||
<div className="px-6">{renderHeader()}</div>
|
<section className="ohif-scrollbar modal-content overflow-y-auto px-6">
|
||||||
<section
|
{children}
|
||||||
className="ohif-scrollbar overflow-y-auto px-6"
|
</section>
|
||||||
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -81,7 +65,6 @@ Modal.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Modal.propTypes = {
|
Modal.propTypes = {
|
||||||
className: PropTypes.string,
|
|
||||||
closeButton: PropTypes.bool,
|
closeButton: PropTypes.bool,
|
||||||
shouldCloseOnEsc: PropTypes.bool,
|
shouldCloseOnEsc: PropTypes.bool,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
|
|||||||
@ -76,15 +76,9 @@ const Select = ({
|
|||||||
options={options}
|
options={options}
|
||||||
value={selectedOptions}
|
value={selectedOptions}
|
||||||
onChange={(selectedOptions, { action }) => {
|
onChange={(selectedOptions, { action }) => {
|
||||||
if (!isMulti) {
|
const newSelection = !selectedOptions.length
|
||||||
onChange(selectedOptions, action);
|
? selectedOptions
|
||||||
return;
|
: selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []);
|
||||||
}
|
|
||||||
|
|
||||||
const newSelection = selectedOptions.reduce(
|
|
||||||
(acc, curr) => acc.concat([curr.value]),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
onChange(newSelection, action);
|
onChange(newSelection, action);
|
||||||
}}
|
}}
|
||||||
></ReactSelect>
|
></ReactSelect>
|
||||||
|
|||||||
@ -48,6 +48,7 @@ const StudyListFilter = ({
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
className="text-primary-active"
|
className="text-primary-active"
|
||||||
startIcon={<Icon name="info-link" className="w-2" />}
|
startIcon={<Icon name="info-link" className="w-2" />}
|
||||||
|
onClick={showLearnMoreContent}
|
||||||
>
|
>
|
||||||
<span className="flex flex-col flex-1">
|
<span className="flex flex-col flex-1">
|
||||||
<span onClick={showLearnMoreContent}>Learn more</span>
|
<span onClick={showLearnMoreContent}>Learn more</span>
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const ToolbarButton = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowDropdown = !!isActive && !!dropdownContent;
|
const shouldShowDropdown = !!isActive && !!dropdownContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={id}>
|
<div key={id}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const arrowPositionStyle = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
|
||||||
const handleMouseOver = () => {
|
const handleMouseOver = () => {
|
||||||
@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isOpen = isSticky || isActive;
|
const isOpen = (isSticky || isActive) && !isDisabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -84,10 +84,12 @@ Tooltip.defaultProps = {
|
|||||||
tight: false,
|
tight: false,
|
||||||
isSticky: false,
|
isSticky: false,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
|
isDisabled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
Tooltip.propTypes = {
|
||||||
// Allow null
|
/** prevents tooltip from rendering despite hover/active/sticky */
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||||
position: PropTypes.oneOf([
|
position: PropTypes.oneOf([
|
||||||
'bottom',
|
'bottom',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui';
|
import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui';
|
||||||
@ -13,9 +13,11 @@ const classes = {
|
|||||||
const ViewportActionBar = ({
|
const ViewportActionBar = ({
|
||||||
studyData,
|
studyData,
|
||||||
showNavArrows,
|
showNavArrows,
|
||||||
showPatientInfo,
|
showPatientInfo: patientInfoVisibility,
|
||||||
onSeriesChange,
|
onSeriesChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
isTracked,
|
isTracked,
|
||||||
@ -37,6 +39,8 @@ const ViewportActionBar = ({
|
|||||||
scanner,
|
scanner,
|
||||||
} = patientInformation;
|
} = patientInformation;
|
||||||
|
|
||||||
|
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo)
|
||||||
|
|
||||||
const renderIconStatus = () => {
|
const renderIconStatus = () => {
|
||||||
if (modality === 'SR') {
|
if (modality === 'SR') {
|
||||||
return (
|
return (
|
||||||
@ -60,29 +64,30 @@ const ViewportActionBar = ({
|
|||||||
{!isTracked ? (
|
{!isTracked ? (
|
||||||
<Icon name="dotted-circle" className="w-6 text-primary-light" />
|
<Icon name="dotted-circle" className="w-6 text-primary-light" />
|
||||||
) : (
|
) : (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="bottom-left"
|
position="bottom-left"
|
||||||
content={
|
content={
|
||||||
<div className="flex py-2">
|
<div className="flex py-2">
|
||||||
<div className="flex pt-1">
|
<div className="flex pt-1">
|
||||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex ml-4">
|
<div className="flex ml-4">
|
||||||
<span className="text-base text-common-light">
|
<span className="text-base text-common-light">
|
||||||
Series is
|
Series is
|
||||||
<span className="font-bold text-white"> tracked</span> and
|
<span className="font-bold text-white"> tracked</span> and
|
||||||
can be viewed <br /> in the measurement panel
|
can be viewed <br /> in the measurement panel
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
>
|
||||||
>
|
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
</Tooltip>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center p-2 border-b border-primary-light">
|
<div className="flex items-center p-2 border-b border-primary-light">
|
||||||
<div className="flex flex-grow">
|
<div className="flex flex-grow">
|
||||||
@ -131,19 +136,18 @@ const ViewportActionBar = ({
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showPatientInfo && (
|
<div className="flex ml-4 mr-2 cursor-pointer" onClick={onPatientInfoClick}>
|
||||||
<div className="flex ml-4 mr-2">
|
<PatientInfo
|
||||||
<PatientInfo
|
isOpen={showPatientInfo}
|
||||||
patientName={patientName}
|
patientName={patientName}
|
||||||
patientSex={patientSex}
|
patientSex={patientSex}
|
||||||
patientAge={patientAge}
|
patientAge={patientAge}
|
||||||
MRN={MRN}
|
MRN={MRN}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
spacing={spacing}
|
spacing={spacing}
|
||||||
scanner={scanner}
|
scanner={scanner}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -174,7 +178,7 @@ ViewportActionBar.propTypes = {
|
|||||||
|
|
||||||
ViewportActionBar.defaultProps = {
|
ViewportActionBar.defaultProps = {
|
||||||
showNavArrows: true,
|
showNavArrows: true,
|
||||||
showPatientInfo: true,
|
showPatientInfo: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PatientInfo({
|
function PatientInfo({
|
||||||
@ -185,11 +189,14 @@ function PatientInfo({
|
|||||||
thickness,
|
thickness,
|
||||||
spacing,
|
spacing,
|
||||||
scanner,
|
scanner,
|
||||||
|
isOpen,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isSticky
|
||||||
|
isDisabled={!isOpen}
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
content={
|
content={isOpen && (
|
||||||
<div className="flex py-2">
|
<div className="flex py-2">
|
||||||
<div className="flex pt-1">
|
<div className="flex pt-1">
|
||||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||||
@ -236,7 +243,7 @@ function PatientInfo({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative flex justify-end">
|
<div className="relative flex justify-end">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Typography,
|
Typography,
|
||||||
InputText,
|
Input,
|
||||||
InputNumber,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton,
|
IconButton,
|
||||||
Icon,
|
Icon,
|
||||||
@ -169,7 +170,10 @@ const ViewportDownloadForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validSize = value => (value >= minimumSize ? value : minimumSize);
|
const validSize = useCallback(
|
||||||
|
value => (value >= minimumSize ? value : minimumSize),
|
||||||
|
[minimumSize]
|
||||||
|
);
|
||||||
|
|
||||||
const loadAndUpdateViewports = useCallback(async () => {
|
const loadAndUpdateViewports = useCallback(async () => {
|
||||||
const { width: scaledWidth, height: scaledHeight } = await loadImage(
|
const { width: scaledWidth, height: scaledHeight } = await loadImage(
|
||||||
@ -274,7 +278,7 @@ const ViewportDownloadForm = ({
|
|||||||
|
|
||||||
<div className="mt-6 flex flex-col">
|
<div className="mt-6 flex flex-col">
|
||||||
<div className="w-full mb-4">
|
<div className="w-full mb-4">
|
||||||
<InputText
|
<Input
|
||||||
data-cy="file-name"
|
data-cy="file-name"
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={value => setFilename(value)}
|
onChange={value => setFilename(value)}
|
||||||
@ -286,7 +290,8 @@ const ViewportDownloadForm = ({
|
|||||||
<div className="flex w-1/3">
|
<div className="flex w-1/3">
|
||||||
<div className="flex flex-col flex-grow">
|
<div className="flex flex-col flex-grow">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<InputNumber
|
<Input
|
||||||
|
type="number"
|
||||||
min={minimumSize}
|
min={minimumSize}
|
||||||
max={maximumSize}
|
max={maximumSize}
|
||||||
label="Image width (px)"
|
label="Image width (px)"
|
||||||
@ -297,7 +302,8 @@ const ViewportDownloadForm = ({
|
|||||||
{renderErrorHandler('width')}
|
{renderErrorHandler('width')}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full mt-4">
|
<div className="w-full mt-4">
|
||||||
<InputNumber
|
<Input
|
||||||
|
type="number"
|
||||||
min={minimumSize}
|
min={minimumSize}
|
||||||
max={maximumSize}
|
max={maximumSize}
|
||||||
label="Image height (px)"
|
label="Image height (px)"
|
||||||
@ -366,20 +372,18 @@ const ViewportDownloadForm = ({
|
|||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div
|
<div
|
||||||
|
className="hidden"
|
||||||
style={{
|
style={{
|
||||||
height: viewportElementDimensions.height,
|
height: viewportElementDimensions.height,
|
||||||
width: viewportElementDimensions.width,
|
width: viewportElementDimensions.width,
|
||||||
position: 'absolute',
|
|
||||||
left: '9999px',
|
|
||||||
}}
|
}}
|
||||||
ref={ref => setViewportElement(ref)}
|
ref={ref => setViewportElement(ref)}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
className={canvasClass}
|
className={classnames('block', canvasClass)}
|
||||||
style={{
|
style={{
|
||||||
height: downloadCanvas.height,
|
height: downloadCanvas.height,
|
||||||
width: downloadCanvas.width,
|
width: downloadCanvas.width,
|
||||||
display: 'block',
|
|
||||||
}}
|
}}
|
||||||
width={downloadCanvas.width}
|
width={downloadCanvas.width}
|
||||||
height={downloadCanvas.height}
|
height={downloadCanvas.height}
|
||||||
|
|||||||
@ -41,6 +41,8 @@ import ThumbnailNoImage from './ThumbnailNoImage';
|
|||||||
import ThumbnailTracked from './ThumbnailTracked';
|
import ThumbnailTracked from './ThumbnailTracked';
|
||||||
import ThumbnailList from './ThumbnailList';
|
import ThumbnailList from './ThumbnailList';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||||
|
import ListMenu from './ListMenu';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import Typography from './Typography';
|
import Typography from './Typography';
|
||||||
import Viewport from './Viewport';
|
import Viewport from './Viewport';
|
||||||
@ -56,6 +58,8 @@ export {
|
|||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
EmptyStudies,
|
EmptyStudies,
|
||||||
|
ExpandableToolbarButton,
|
||||||
|
ListMenu,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
|||||||
@ -10,12 +10,20 @@ module.exports = {
|
|||||||
xl: '1280px',
|
xl: '1280px',
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
overlay: 'rgba(0, 0, 0, 0.8)',
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
black: '#000',
|
black: '#000',
|
||||||
white: '#fff',
|
white: '#fff',
|
||||||
initial: 'initial',
|
initial: 'initial',
|
||||||
inherit: 'inherit',
|
inherit: 'inherit',
|
||||||
|
|
||||||
|
indigo: {
|
||||||
|
dark: '#0b1a42',
|
||||||
|
},
|
||||||
|
aqua: {
|
||||||
|
pale: '#7bb2ce',
|
||||||
|
},
|
||||||
|
|
||||||
primary: {
|
primary: {
|
||||||
light: '#5acce6',
|
light: '#5acce6',
|
||||||
main: '#0944b3',
|
main: '#0944b3',
|
||||||
|
|||||||
@ -67,9 +67,9 @@
|
|||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "4.15.1",
|
"cornerstone-tools": "4.16.0",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dcmjs": "^0.12.2",
|
"dcmjs": "0.14.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"dicomweb-client": "^0.4.4",
|
"dicomweb-client": "^0.4.4",
|
||||||
"dotenv-webpack": "^1.7.0",
|
"dotenv-webpack": "^1.7.0",
|
||||||
|
|||||||
@ -19,7 +19,9 @@ window.config = {
|
|||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
enableStudyLazyLoad: true,
|
enableStudyLazyLoad: true,
|
||||||
|
supportsFuzzyMatching: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
defaultDataSourceName: 'dicomweb',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* CSS Grid Reference: http://grid.malven.co/
|
* CSS Grid Reference: http://grid.malven.co/
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
|
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
|
||||||
import EmptyViewport from './EmptyViewport';
|
import EmptyViewport from './EmptyViewport';
|
||||||
|
import { classes } from '@ohif/core';
|
||||||
|
const { ImageSet } = classes;
|
||||||
|
|
||||||
function ViewerViewportGrid(props) {
|
function ViewerViewportGrid(props) {
|
||||||
const { servicesManager, viewportComponents, dataSource } = props;
|
const { servicesManager, viewportComponents, dataSource } = props;
|
||||||
@ -20,6 +22,29 @@ function ViewerViewportGrid(props) {
|
|||||||
// TODO -> Need some way of selecting which displaySets hit the viewports.
|
// TODO -> Need some way of selecting which displaySets hit the viewports.
|
||||||
const { DisplaySetService } = servicesManager.services;
|
const { DisplaySetService } = servicesManager.services;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = DisplaySetService.subscribe(
|
||||||
|
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
||||||
|
displaySets => {
|
||||||
|
displaySets.sort((a, b) => {
|
||||||
|
const isImageSet = x => x instanceof ImageSet;
|
||||||
|
return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1;
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_DISPLAYSET_FOR_VIEWPORT',
|
||||||
|
payload: {
|
||||||
|
viewportIndex: 0,
|
||||||
|
displaySetInstanceUID: displaySets[0].displaySetInstanceUID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// TODO -> Make a HangingProtocolService
|
// TODO -> Make a HangingProtocolService
|
||||||
const HangingProtocolService = displaySets => {
|
const HangingProtocolService = displaySets => {
|
||||||
let displaySetInstanceUID;
|
let displaySetInstanceUID;
|
||||||
|
|||||||
@ -46,14 +46,17 @@ function DataSourceWrapper(props) {
|
|||||||
// studies.processResults --> <LayoutTemplate studies={} />
|
// studies.processResults --> <LayoutTemplate studies={} />
|
||||||
// But only for LayoutTemplate type of 'list'?
|
// But only for LayoutTemplate type of 'list'?
|
||||||
// Or no data fetching here, and just hand down my source
|
// Or no data fetching here, and just hand down my source
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 204: no content
|
// 204: no content
|
||||||
async function getData() {
|
async function getData() {
|
||||||
|
setIsLoading(true);
|
||||||
const searchResults = await dataSource.query.studies.search(
|
const searchResults = await dataSource.query.studies.search(
|
||||||
queryFilterValues
|
queryFilterValues
|
||||||
);
|
);
|
||||||
setData(searchResults);
|
setData(searchResults);
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -61,23 +64,19 @@ function DataSourceWrapper(props) {
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.warn(ex);
|
console.warn(ex);
|
||||||
}
|
}
|
||||||
console.log('DataSourceWrapper: useEffect');
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [history.location.search]);
|
}, [history.location.search]);
|
||||||
// queryFilterValues
|
// queryFilterValues
|
||||||
|
|
||||||
// TODO: Better way to pass DataSource?
|
// TODO: Better way to pass DataSource?
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<LayoutTemplate
|
||||||
{data && (
|
{...rest}
|
||||||
<LayoutTemplate
|
history={history}
|
||||||
{...rest}
|
data={data}
|
||||||
history={history}
|
dataSource={dataSource}
|
||||||
data={data}
|
isLoadingData={isLoading}
|
||||||
dataSource={dataSource}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { Suspense, useState, useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -29,15 +29,16 @@ const seriesInStudiesMap = new Map();
|
|||||||
* TODO:
|
* TODO:
|
||||||
* - debounce `setFilterValues` (150ms?)
|
* - debounce `setFilterValues` (150ms?)
|
||||||
*/
|
*/
|
||||||
function WorkList({ history, data: studies, dataSource }) {
|
function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||||
// ~ Modes
|
// ~ Modes
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
// ~ Filters
|
// ~ Filters
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const queryFilterValues = _getQueryFilterValues(query);
|
const queryFilterValues = _getQueryFilterValues(query);
|
||||||
const [filterValues, _setFilterValues] = useState(
|
const [filterValues, _setFilterValues] = useState({
|
||||||
Object.assign({}, defaultFilterValues, queryFilterValues)
|
...defaultFilterValues,
|
||||||
);
|
...queryFilterValues,
|
||||||
|
});
|
||||||
const debouncedFilterValues = useDebounce(filterValues, 200);
|
const debouncedFilterValues = useDebounce(filterValues, 200);
|
||||||
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
|
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
|
||||||
|
|
||||||
@ -81,6 +82,7 @@ function WorkList({ history, data: studies, dataSource }) {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ~ Rows & Studies
|
// ~ Rows & Studies
|
||||||
const [expandedRows, setExpandedRows] = useState([]);
|
const [expandedRows, setExpandedRows] = useState([]);
|
||||||
const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]);
|
const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]);
|
||||||
@ -191,6 +193,7 @@ function WorkList({ history, data: studies, dataSource }) {
|
|||||||
return filterValues[name] !== defaultFilterValues[name];
|
return filterValues[name] !== defaultFilterValues[name];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableDataSource = sortedStudies.map((study, key) => {
|
const tableDataSource = sortedStudies.map((study, key) => {
|
||||||
const rowKey = key + 1;
|
const rowKey = key + 1;
|
||||||
const isExpanded = expandedRows.some(k => k === rowKey);
|
const isExpanded = expandedRows.some(k => k === rowKey);
|
||||||
@ -395,7 +398,7 @@ function WorkList({ history, data: studies, dataSource }) {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center pt-48">
|
<div className="flex flex-col items-center justify-center pt-48">
|
||||||
<EmptyStudies />
|
<EmptyStudies isLoading={isLoadingData} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@ -6755,10 +6755,10 @@ cornerstone-math@^0.1.8:
|
|||||||
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5"
|
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5"
|
||||||
integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw==
|
integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw==
|
||||||
|
|
||||||
cornerstone-tools@4.15.1:
|
cornerstone-tools@4.16.0:
|
||||||
version "4.15.1"
|
version "4.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.15.1.tgz#f0b1026da9c7758defc088bb2f1e78426a23d91e"
|
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.16.0.tgz#af3d32d13722b97bec258492642e622312280196"
|
||||||
integrity sha512-fJuTUJW/NDSD520jPB++tX//Kq80jPDngChHZrhx2xjj/9dCnexD9kmTX30Z0WUq0a2JEHZ6FlcOX38kSmw1hQ==
|
integrity sha512-kUhuSb2Ixpd2hgbdem+740rnN4hmoxzcOBNaUcsizFRWjMAsgc0yUxBFwLl0mIs811mefq79JOL+juwRA8T3Tg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "7.1.2"
|
"@babel/runtime" "7.1.2"
|
||||||
cornerstone-math "0.1.7"
|
cornerstone-math "0.1.7"
|
||||||
@ -7353,10 +7353,10 @@ dateformat@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||||
|
|
||||||
dcmjs@^0.12.2, dcmjs@^0.12.4:
|
dcmjs@0.14.0:
|
||||||
version "0.12.4"
|
version "0.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.12.4.tgz#82c24abdc357ea5281b78eb2cae8b781f7392aa3"
|
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.14.0.tgz#0dc6cb2d15ddcff759bc9002f2a9704537735d1c"
|
||||||
integrity sha512-N1ZsXqZIysirqdytb7h572TyIjmxpvCjrzdjtQsuPN8gC2EpxsUHQ598CPzaJBpBy9i1kfKuq4h2Jwt99cr/QQ==
|
integrity sha512-VL/Ibxe5RDsc5j5SEv3aEqdlKuBXz81/bBuW59Or0cos9vgK3XnVl3rr0ct6DWXJGK8vGIUMBOguyd/NRvlN0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.8.3"
|
"@babel/polyfill" "^7.8.3"
|
||||||
"@babel/runtime" "^7.8.4"
|
"@babel/runtime" "^7.8.4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user